Skip to content

Commit

Permalink
houston: we have arrows
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed Apr 20, 2024
1 parent ab26555 commit b5dbeb4
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import type { ArrowProps } from "../index.js";
import { FloatingLayer } from "$lib/bits/utilities/index.js";
let { el = $bindable(), ...restProps }: ArrowProps = $props();
</script>

<FloatingLayer.Arrow bind:el {...restProps} />
6 changes: 3 additions & 3 deletions packages/bits-ui/src/lib/bits/popover/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Snippet } from "svelte";
import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
import type {
EventCallback,
HTMLDivAttributes,
OnChangeFn,
PrimitiveButtonAttributes,
PrimitiveDivAttributes,
Expand Down Expand Up @@ -53,9 +53,9 @@ export type PopoverClosePropsWithoutHTML = WithAsChild<{
export type PopoverCloseProps = PopoverClosePropsWithoutHTML &
Omit<PrimitiveButtonAttributes, "onclick" | "onkeydown">;

export type PopoverArrowPropsWithoutHTML = WithAsChild<object>;
export type PopoverArrowPropsWithoutHTML = ArrowPropsWithoutHTML;

export type PopoverArrowProps = PopoverArrowPropsWithoutHTML & HTMLDivAttributes;
export type PopoverArrowProps = ArrowProps;

export type PopoverTriggerEvents<T extends Element = HTMLButtonElement> = {
click: CustomEventHandler<MouseEvent, T>;
Expand Down
37 changes: 37 additions & 0 deletions packages/bits-ui/src/lib/bits/utilities/arrow/arrow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import type { ArrowProps } from "./types.js";
import { generateId } from "$lib/internal/id.js";
import { styleToString } from "$lib/internal/style.js";
let {
id = generateId(),
el = $bindable(),
children,
asChild,
child,
style = {},
height = 5,
width = 10,
...restProps
}: ArrowProps = $props();
const mergedProps = $derived({
...restProps,
style: styleToString(style),
id,
});
</script>

{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<span {...mergedProps} bind:this={el}>
{#if children}
{@render children?.()}
{:else}
<svg {width} {height} viewBox="0 0 30 10" preserveAspectRatio="none">
<polygon points="0,0 30,0 15,10" fill="currentColor" />
</svg>
{/if}
</span>
{/if}
2 changes: 2 additions & 0 deletions packages/bits-ui/src/lib/bits/utilities/arrow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Arrow } from "./arrow.svelte";
export type { ArrowProps } from "./types.js";
19 changes: 19 additions & 0 deletions packages/bits-ui/src/lib/bits/utilities/arrow/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PrimitiveSpanAttributes, WithAsChild } from "$lib/internal/types.js";

export type ArrowPropsWithoutHTML = WithAsChild<{
/**
* The width of the arrow in pixels.
*
* @defaultValue 10
*/
width?: number;

/**
* The height of the arrow in pixels.
*
* @defaultValue 5
*/
height?: number;
}>;

export type ArrowProps = ArrowPropsWithoutHTML & PrimitiveSpanAttributes;
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
<script lang="ts">
import type { ArrowProps } from "../index.js";
import { setFloatingArrowState } from "../floating-layer.svelte.js";
import { Arrow, type ArrowProps } from "$lib/bits/utilities/arrow/index.js";
import { readonlyBox } from "$lib/internal/box.svelte.js";
import { generateId } from "$lib/internal/id.js";
let { id, children, style = {} }: ArrowProps = $props();
let { id = generateId(), el = $bindable(), style = {}, ...restProps }: ArrowProps = $props();
setFloatingArrowState({ id: readonlyBox(() => id), style: readonlyBox(() => style) });
const state = setFloatingArrowState({
id: readonlyBox(() => id),
style: readonlyBox(() => style),
});
const mergedProps = $derived({
...restProps,
...state.props,
});
</script>

{@render children?.()}
<Arrow {...mergedProps} bind:el />
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import type { ContentProps } from "../index.js";
import { setFloatingContentState } from "../floating-layer.svelte.js";
import { readonlyBox } from "$lib/internal/box.svelte.js";
import { generateId } from "$lib/internal/id.js";
let {
content,
side = "bottom",
sideOffset = 0,
align = "center",
alignOffset = 0,
id,
id = generateId(),
arrowPadding = 0,
avoidCollisions = true,
collisionBoundary = [],
Expand All @@ -22,7 +23,7 @@
dir = "ltr",
style = {},
present,
wrapperId,
wrapperId = generateId(),
}: ContentProps = $props();
const state = setFloatingContentState({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,20 @@ export const ALIGN_OPTIONS = ["start", "center", "end"] as const;
// right: "rotate(315deg)",
// };

const OPPOSITE_SIDE: Record<Side, Side> = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
};

export type Side = (typeof SIDE_OPTIONS)[number];
export type Align = (typeof ALIGN_OPTIONS)[number];

export type Boundary = Element | null;

class FloatingRootState {
wrapperId = undefined as unknown as ReadonlyBox<string>;
contentNode = undefined as unknown as Box<HTMLElement | null>;
anchorNode = undefined as unknown as Box<HTMLElement | null>;
arrowNode = boxedState<HTMLElement | null>(null);
wrapperNode = undefined as unknown as Box<HTMLElement | null>;

createAnchor(props: FloatingAnchorStateProps) {
return new FloatingAnchorState(props, this);
Expand All @@ -54,10 +57,6 @@ class FloatingRootState {
createContent(props: FloatingContentStateProps) {
return new FloatingContentState(props, this);
}

createArrow(props: FloatingArrowStateProps) {
return new FloatingArrowState(props, this);
}
}

export type FloatingContentStateProps = ReadonlyBoxedValues<{
Expand All @@ -82,8 +81,19 @@ export type FloatingContentStateProps = ReadonlyBoxedValues<{
}>;

class FloatingContentState {
// state
root = undefined as unknown as FloatingRootState;

// nodes
contentNode = undefined as unknown as Box<HTMLElement | null>;
wrapperNode = undefined as unknown as Box<HTMLElement | null>;
arrowNode = boxedState<HTMLElement | null>(null);

// ids
arrowId = undefined as unknown as ReadonlyBox<string>;
id = undefined as unknown as FloatingContentStateProps["id"];
wrapperId = undefined as unknown as FloatingContentStateProps["wrapperId"];

style = undefined as unknown as FloatingContentStateProps["style"];
dir = undefined as unknown as FloatingContentStateProps["dir"];
side = undefined as unknown as FloatingContentStateProps["side"];
Expand Down Expand Up @@ -156,8 +166,8 @@ class FloatingContentState {
contentStyle.setProperty("--bits-popper-anchor-height", `${anchorHeight}px`);
},
}),
this.root.arrowNode.value &&
arrow({ element: this.root.arrowNode.value, padding: this.arrowPadding.value }),
this.arrowNode.value &&
arrow({ element: this.arrowNode.value, padding: this.arrowPadding.value }),
transformOrigin({ arrowWidth: this.arrowWidth, arrowHeight: this.arrowHeight }),
this.hideWhenDetached.value &&
hide({ strategy: "referenceHidden", ...this.detectOverflowOptions }),
Expand All @@ -170,8 +180,9 @@ class FloatingContentState {
arrowY = $derived(this.floating.middlewareData.arrow?.y ?? 0);
cannotCenterArrow = $derived(this.floating.middlewareData.arrow?.centerOffset !== 0);
contentZIndex = $state<string>();
arrowBaseSide = $derived(OPPOSITE_SIDE[this.placedSide]);
wrapperProps = $derived({
id: this.root.wrapperId.value,
id: this.wrapperId.value,
"data-bits-floating-content-wrapper": "",
style: styleToString({
...this.floating.floatingStyles,
Expand Down Expand Up @@ -200,7 +211,27 @@ class FloatingContentState {
// we prevent animations so that users's animation don't kick in too early referring wrong sides
// animation: !this.floating.isPositioned ? "none" : undefined,
}),
});
} as const);

arrowStyle = $derived({
position: "absolute",
left: this.arrowX ? `${this.arrowX}px` : undefined,
top: this.arrowY ? `${this.arrowY}px` : undefined,
[this.arrowBaseSide]: 0,
"transform-origin": {
top: "",
right: "0 0",
bottom: "center 0",
left: "100% 0",
}[this.placedSide],
transform: {
top: "translateY(100%)",
right: "translateY(50%) rotate(90deg) translateX(-50%)",
bottom: "rotate(180deg)",
left: "translateY(50%) rotate(-90deg) translateX(50%)",
}[this.placedSide],
visibility: this.cannotCenterArrow ? "hidden" : undefined,
} as const);

constructor(props: FloatingContentStateProps, root: FloatingRootState) {
this.id = props.id;
Expand All @@ -221,10 +252,10 @@ class FloatingContentState {
this.style = props.style;
this.root = root;
this.present = props.present;
this.arrowSize = useSize(this.root.arrowNode);
this.root.wrapperId = props.wrapperId;
this.root.wrapperNode = useNodeById(this.root.wrapperId);
this.root.contentNode = useNodeById(this.id);
this.arrowSize = useSize(this.arrowNode);
this.wrapperId = props.wrapperId;
this.wrapperNode = useNodeById(this.wrapperId);
this.contentNode = useNodeById(this.id);
this.floating = useFloating({
strategy: () => this.strategy.value,
placement: () => this.desiredPlacement,
Expand All @@ -240,13 +271,12 @@ class FloatingContentState {
});

$effect(() => {
if (this.floating.isPositioned) {
this.onPlaced?.value();
}
if (!this.floating.isPositioned) return;
this.onPlaced?.value();
});

$effect(() => {
const contentNode = this.root.contentNode.value;
const contentNode = this.contentNode.value;
if (!contentNode) return;

untrack(() => {
Expand All @@ -255,9 +285,13 @@ class FloatingContentState {
});

$effect(() => {
this.floating.floating.value = this.root.wrapperNode.value;
this.floating.floating.value = this.wrapperNode.value;
});
}

createArrow(props: FloatingArrowStateProps) {
return new FloatingArrowState(props, this);
}
}

type FloatingArrowStateProps = ReadonlyBoxedValues<{
Expand All @@ -266,13 +300,22 @@ type FloatingArrowStateProps = ReadonlyBoxedValues<{
}>;

class FloatingArrowState {
root = undefined as unknown as FloatingRootState;
content = undefined as unknown as FloatingContentState;
id = undefined as unknown as ReadonlyBox<string>;
style = undefined as unknown as FloatingArrowStateProps["style"];
props = $derived({
id: this.id.value,
style: {
...this.style.value,
...this.content.arrowStyle,
},
});

constructor(props: FloatingArrowStateProps, root: FloatingRootState) {
constructor(props: FloatingArrowStateProps, content: FloatingContentState) {
this.content = content;
this.id = props.id;
this.root = root;
this.root.arrowNode = useNodeById(this.id);
this.style = props.style;
this.content.arrowNode = useNodeById(this.id);
}
}

Expand All @@ -290,7 +333,8 @@ class FloatingAnchorState {
// CONTEXT METHODS
//

const FLOATING_ROOT_KEY = Symbol("Popper.Root");
const FLOATING_ROOT_KEY = Symbol("Floating.Root");
const FLOATING_CONTENT_KEY = Symbol("Floating.Content");

export function setFloatingRootState() {
return setContext(FLOATING_ROOT_KEY, new FloatingRootState());
Expand All @@ -301,11 +345,15 @@ export function getFloatingRootState(): FloatingRootState {
}

export function setFloatingContentState(props: FloatingContentStateProps): FloatingContentState {
return getFloatingRootState().createContent(props);
return setContext(FLOATING_CONTENT_KEY, getFloatingRootState().createContent(props));
}

export function getFloatingContentState(): FloatingContentState {
return getContext(FLOATING_CONTENT_KEY);
}

export function setFloatingArrowState(props: FloatingArrowStateProps): FloatingArrowState {
return getFloatingRootState().createArrow(props);
return getFloatingContentState().createArrow(props);
}

export function setFloatingAnchorState(props: FloatingAnchorStateProps): FloatingAnchorState {
Expand All @@ -315,6 +363,7 @@ export function setFloatingAnchorState(props: FloatingAnchorStateProps): Floatin
//
// HELPERS
//

function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,6 @@ export type FloatingLayerContentProps = {
wrapperId?: string;
};

export type FloatingLayerArrowProps = {
id: string;
children?: Snippet;
style: StyleProperties;
};

export type FloatingLayerAnchorProps = {
id: string;
children?: Snippet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { box, boxedState } from "../box.svelte.js";
import { afterTick } from "../after-tick.js";
import type { UseFloatingOptions, UseFloatingReturn } from "./types.js";
import { get, getDPR, roundByDPR } from "./utils.js";
import type { Side } from "$lib/bits/utilities/floating-layer/floating-layer.svelte.js";

export function useFloating(options: UseFloatingOptions): UseFloatingReturn {
/** Options */
Expand Down
Loading

0 comments on commit b5dbeb4

Please sign in to comment.