diff --git a/package.json b/package.json index 73fc05a59..c112d8947 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build:packages": "pnpm -F \"./packages/**\" --parallel build", "check": "pnpm build:packages && pnpm -r check", "ci:publish": "pnpm build:packages && changeset publish", - "dev": "pnpm -r --parallel dev", + "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel dev", "format": "prettier --write .", "lint": "prettier --check . && eslint .", "lint:fix": "eslint --fix .", diff --git a/packages/bits-ui/package.json b/packages/bits-ui/package.json index 0467998cc..d3c96cb5d 100644 --- a/packages/bits-ui/package.json +++ b/packages/bits-ui/package.json @@ -50,7 +50,7 @@ "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.2.8", - "vitest": "^1.5.0" + "vitest": "^1.6.0" }, "svelte": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts index 4f8f89933..b944391f4 100644 --- a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts +++ b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts @@ -1,4 +1,3 @@ -import type { WritableBox } from "svelte-toolbelt"; import { type Box, type ReadableBoxedValues, @@ -10,7 +9,7 @@ import { getDataOpenClosed, getDataOrientation, kbd, - useNodeById, + useRefById, } from "$lib/internal/index.js"; import { type UseRovingFocusReturn, useRovingFocus } from "$lib/internal/useRovingFocus.svelte.js"; import type { Orientation } from "$lib/shared/index.js"; @@ -31,25 +30,33 @@ type AccordionBaseStateProps = ReadableBoxedValues<{ disabled: boolean; orientation: Orientation; loop: boolean; - ref: HTMLElement | null | undefined; -}>; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; class AccordionBaseState { - id: AccordionBaseStateProps["id"]; - node: WritableBox; + #id: AccordionBaseStateProps["id"]; + #ref: AccordionBaseStateProps["ref"]; disabled: AccordionBaseStateProps["disabled"]; #loop: AccordionBaseStateProps["loop"]; orientation: AccordionBaseStateProps["orientation"]; rovingFocusGroup: UseRovingFocusReturn; constructor(props: AccordionBaseStateProps) { - this.id = props.id; + this.#id = props.id; this.disabled = props.disabled; - this.node = useNodeById(this.id); + this.#ref = props.ref; + + useRefById({ + id: props.id, + ref: this.#ref, + }); + this.orientation = props.orientation; this.#loop = props.loop; this.rovingFocusGroup = useRovingFocus({ - rootNodeId: this.id, + rootNodeId: this.#id, candidateSelector: TRIGGER_ATTR, loop: this.#loop, orientation: this.orientation, @@ -59,7 +66,7 @@ class AccordionBaseState { props = $derived.by( () => ({ - id: this.id.value, + id: this.#id.value, "data-orientation": getDataOrientation(this.orientation.value), "data-disabled": getDataDisabled(this.disabled.value), [ROOT_ATTR]: "", @@ -126,11 +133,16 @@ export class AccordionMultiState extends AccordionBaseState { type AccordionItemStateProps = ReadableBoxedValues<{ value: string; disabled: boolean; + id: string; }> & { rootState: AccordionState; -}; +} & WritableBoxedValues<{ + ref: HTMLElement | null; + }>; export class AccordionItemState { + #id: AccordionItemStateProps["id"]; + #ref: AccordionItemStateProps["ref"]; value: AccordionItemStateProps["value"]; disabled: AccordionItemStateProps["disabled"]; root: AccordionState; @@ -141,6 +153,13 @@ export class AccordionItemState { this.value = props.value; this.disabled = props.disabled; this.root = props.rootState; + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + }); } updateValue() { @@ -162,6 +181,7 @@ export class AccordionItemState { props = $derived.by( () => ({ + id: this.#id.value, [ITEM_ATTR]: "", "data-state": getDataOpenClosed(this.isSelected), "data-disabled": getDataDisabled(this.isDisabled), @@ -176,12 +196,15 @@ export class AccordionItemState { type AccordionTriggerStateProps = ReadableBoxedValues<{ disabled: boolean; id: string; -}>; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; class AccordionTriggerState { + #ref: AccordionTriggerStateProps["ref"]; #disabled: AccordionTriggerStateProps["disabled"]; #id: AccordionTriggerStateProps["id"]; - #node: Box; #root: AccordionState; #itemState: AccordionItemState; #isDisabled = $derived.by( @@ -193,8 +216,12 @@ class AccordionTriggerState { this.#itemState = itemState; this.#root = itemState.root; this.#id = props.id; + this.#ref = props.ref; - this.#node = useNodeById(this.#id); + useRefById({ + id: props.id, + ref: this.#ref, + }); } #onclick = () => { @@ -209,7 +236,7 @@ class AccordionTriggerState { return; } - this.#root.rovingFocusGroup.handleKeydown(this.#node.value, e); + this.#root.rovingFocusGroup.handleKeydown(this.#ref.value, e); }; props = $derived.by( @@ -238,11 +265,14 @@ class AccordionTriggerState { type AccordionContentStateProps = ReadableBoxedValues<{ forceMount: boolean; id: string; -}>; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; class AccordionContentState { item: AccordionItemState; - node: WritableBox; + #ref: AccordionContentStateProps["ref"]; #id: AccordionContentStateProps["id"]; #originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined; #isMountAnimationPrevented = false; @@ -257,8 +287,12 @@ class AccordionContentState { this.#forceMount = props.forceMount; this.#isMountAnimationPrevented = this.item.isSelected; this.#id = props.id; + this.#ref = props.ref; - this.node = useNodeById(this.#id); + useRefById({ + id: this.#id, + ref: this.#ref, + }); $effect.pre(() => { const rAF = requestAnimationFrame(() => { @@ -273,11 +307,11 @@ class AccordionContentState { $effect(() => { // eslint-disable-next-line no-unused-expressions this.present; - const node = this.node.value; + const node = this.#ref.value; if (!node) return; afterTick(() => { - if (!this.node) return; + if (!this.#ref.value) return; // get the dimensions of the element this.#originalStyles = this.#originalStyles || { transitionDuration: node.style.transitionDuration, @@ -320,19 +354,34 @@ class AccordionContentState { type AccordionHeaderStateProps = ReadableBoxedValues<{ level: 1 | 2 | 3 | 4 | 5 | 6; -}>; + id: string; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; class AccordionHeaderState { - #item: AccordionItemState; + #id: AccordionHeaderStateProps["id"]; + #ref: AccordionHeaderStateProps["ref"]; #level: AccordionHeaderStateProps["level"]; + #item: AccordionItemState; constructor(props: AccordionHeaderStateProps, item: AccordionItemState) { this.#level = props.level; + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + }); + this.#item = item; } props = $derived.by( () => ({ + id: this.#id.value, role: "heading", "aria-level": this.#level.value, "data-heading-level": this.#level.value, @@ -357,8 +406,10 @@ type InitAccordionProps = { disabled: boolean; orientation: Orientation; loop: boolean; - ref: HTMLElement | null | undefined; -}>; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; const [setAccordionRootContext, getAccordionRootContext] = createContext("Accordion.Root"); diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte index 455974937..b43627c5b 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte @@ -8,7 +8,7 @@ let { child, asChild, - ref = $bindable(), + ref = $bindable(null), id = useId(), forceMount = false, children, @@ -18,6 +18,10 @@ const contentState = useAccordionContent({ forceMount: box.with(() => forceMount), id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); @@ -31,7 +35,7 @@ props: mergedProps, })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-header.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-header.svelte index e12000778..8a6882d50 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-header.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-header.svelte @@ -3,18 +3,25 @@ import type { AccordionHeaderProps } from "../types.js"; import { useAccordionHeader } from "../accordion.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; let { + id = useId(), asChild, level = 2, children, child, - ref = $bindable(), + ref = $bindable(null), ...restProps }: AccordionHeaderProps = $props(); const headerState = useAccordionHeader({ + id: box.with(() => id), level: box.with(() => level), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, headerState.props)); @@ -23,7 +30,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte index a0fb0fbb1..3033313de 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte @@ -3,20 +3,27 @@ import type { AccordionItemProps } from "../types.js"; import { useAccordionItem } from "../accordion.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; let { + id = useId(), asChild, disabled = false, value, children, child, - ref = $bindable(), + ref = $bindable(null), ...restProps }: AccordionItemProps = $props(); const itemState = useAccordionItem({ value: box.with(() => value), disabled: box.with(() => disabled), + id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, itemState.props)); @@ -25,7 +32,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte index 0b60990b4..fdbb43f8c 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte @@ -7,7 +7,7 @@ let { disabled = false, asChild, - ref = $bindable(), + ref = $bindable(null), id = useId(), children, child, @@ -17,6 +17,10 @@ const triggerState = useAccordionTrigger({ disabled: box.with(() => disabled), id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, triggerState.props)); @@ -25,7 +29,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} - {/if} diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte index 28cca6dd2..dfcc85fab 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte @@ -12,7 +12,7 @@ child, type, value = $bindable(), - ref = $bindable(), + ref = $bindable(null), id = useId(), onValueChange, loop = true, @@ -35,7 +35,10 @@ disabled: box.with(() => disabled), loop: box.with(() => loop), orientation: box.with(() => orientation), - ref: box.with(() => ref), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, rootState.props)); @@ -44,7 +47,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts index 577779a84..13321e583 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts @@ -76,10 +76,14 @@ class CheckboxRootState { class CheckboxInputState { root: CheckboxRootState; - shouldRender = $derived.by(() => this.root.name.value !== undefined); + shouldRender = $derived.by(() => Boolean(this.root.name.value)); constructor(root: CheckboxRootState) { this.root = root; + + $effect(() => { + console.log("shouldRender", this.shouldRender); + }); } props = $derived.by( diff --git a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-input.svelte b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-input.svelte index c005f6c8d..060b977bb 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-input.svelte +++ b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-input.svelte @@ -1,9 +1,12 @@ {#if inputState.shouldRender} - + + + {/if} diff --git a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte index dd32f2cd6..6dcc50ed2 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte +++ b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte @@ -11,7 +11,7 @@ children, disabled = false, required = false, - name, + name = undefined, value = "on", ref = $bindable(), asChild, diff --git a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts index c44a8737c..1098c44e5 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts @@ -1,4 +1,3 @@ -import { box } from "svelte-toolbelt"; import { type ReadableBoxedValues, type WritableBoxedValues, @@ -6,8 +5,7 @@ import { getAriaExpanded, getDataDisabled, getDataOpenClosed, - useId, - useNodeById, + useRefById, } from "$lib/internal/index.js"; import { createContext } from "$lib/internal/createContext.js"; @@ -17,19 +15,30 @@ const TRIGGER_ATTR = "data-collapsible-trigger"; type CollapsibleRootStateProps = WritableBoxedValues<{ open: boolean; + ref: HTMLElement | null; }> & ReadableBoxedValues<{ disabled: boolean; + id: string; }>; class CollapsibleRootState { + #id: CollapsibleRootStateProps["id"]; + #ref: CollapsibleRootStateProps["ref"]; open: CollapsibleRootStateProps["open"]; disabled: CollapsibleRootStateProps["disabled"]; - contentId = box.with(() => useId()); + contentNode = $state(null); constructor(props: CollapsibleRootStateProps) { this.open = props.open; this.disabled = props.disabled; + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + }); } toggleOpen() { @@ -40,13 +49,14 @@ class CollapsibleRootState { return new CollapsibleContentState(props, this); } - createTrigger() { - return new CollapsibleTriggerState(this); + createTrigger(props: CollapsibleTriggerStateProps) { + return new CollapsibleTriggerState(props, this); } props = $derived.by( () => ({ + id: this.#id.value, "data-state": getDataOpenClosed(this.open.value), "data-disabled": getDataDisabled(this.disabled.value), [ROOT_ATTR]: "", @@ -57,42 +67,38 @@ class CollapsibleRootState { type CollapsibleContentStateProps = ReadableBoxedValues<{ id: string; forceMount: boolean; -}>; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; class CollapsibleContentState { + #id: CollapsibleContentStateProps["id"]; + #ref: CollapsibleContentStateProps["ref"]; root: CollapsibleRootState; #originalStyles: { transitionDuration: string; animationName: string } | undefined; - node = box(null); #isMountAnimationPrevented = $state(false); #width = $state(0); #height = $state(0); #forceMount: CollapsibleContentStateProps["forceMount"]; - props = $derived.by( - () => - ({ - id: this.root.contentId.value, - "data-state": getDataOpenClosed(this.root.open.value), - "data-disabled": getDataDisabled(this.root.disabled.value), - style: { - "--bits-collapsible-content-height": this.#height - ? `${this.#height}px` - : undefined, - "--bits-collapsible-content-width": this.#width - ? `${this.#width}px` - : undefined, - }, - [CONTENT_ATTR]: "", - }) as const - ); + present = $derived.by(() => this.#forceMount.value || this.root.open.value); constructor(props: CollapsibleContentStateProps, root: CollapsibleRootState) { this.root = root; this.#isMountAnimationPrevented = root.open.value; this.#forceMount = props.forceMount; - this.root.contentId = props.id; - - this.node = useNodeById(this.root.contentId); + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + condition: () => this.present, + onRefChange: (node) => { + this.root.contentNode = node; + }, + }); $effect.pre(() => { const rAF = requestAnimationFrame(() => { @@ -107,11 +113,11 @@ class CollapsibleContentState { $effect(() => { // eslint-disable-next-line no-unused-expressions this.present; - const node = this.node.value; + const node = this.#ref.value; if (!node) return; afterTick(() => { - if (!this.node) return; + if (!this.#ref.value) return; // get the dimensions of the element this.#originalStyles = this.#originalStyles || { transitionDuration: node.style.transitionDuration, @@ -135,13 +141,47 @@ class CollapsibleContentState { }); }); } + + props = $derived.by( + () => + ({ + id: this.#id.value, + "data-state": getDataOpenClosed(this.root.open.value), + "data-disabled": getDataDisabled(this.root.disabled.value), + style: { + "--bits-collapsible-content-height": this.#height + ? `${this.#height}px` + : undefined, + "--bits-collapsible-content-width": this.#width + ? `${this.#width}px` + : undefined, + }, + [CONTENT_ATTR]: "", + }) as const + ); } +type CollapsibleTriggerStateProps = ReadableBoxedValues<{ + id: string; +}> & + WritableBoxedValues<{ + ref: HTMLElement | null; + }>; + class CollapsibleTriggerState { #root: CollapsibleRootState; + #ref: CollapsibleTriggerStateProps["ref"]; + #id: CollapsibleTriggerStateProps["id"]; - constructor(root: CollapsibleRootState) { + constructor(props: CollapsibleTriggerStateProps, root: CollapsibleRootState) { this.#root = root; + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + }); } #onclick = () => { @@ -151,8 +191,9 @@ class CollapsibleTriggerState { props = $derived.by( () => ({ + id: this.#id.value, type: "button", - "aria-controls": this.#root.contentId.value, + "aria-controls": this.#root.contentNode?.id, "aria-expanded": getAriaExpanded(this.#root.open.value), "data-state": getDataOpenClosed(this.#root.open.value), "data-disabled": getDataDisabled(this.#root.disabled.value), @@ -171,8 +212,10 @@ export function useCollapsibleRoot(props: CollapsibleRootStateProps) { return setCollapsibleRootContext(new CollapsibleRootState(props)); } -export function useCollapsibleTrigger(): CollapsibleTriggerState { - return getCollapsibleRootContext().createTrigger(); +export function useCollapsibleTrigger( + props: CollapsibleTriggerStateProps +): CollapsibleTriggerState { + return getCollapsibleRootContext().createTrigger(props); } export function useCollapsibleContent( diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte index f70208e8b..bbfad982a 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte @@ -8,7 +8,7 @@ let { child, asChild, - ref = $bindable(), + ref = $bindable(null), forceMount = false, children, id = useId(), @@ -18,6 +18,10 @@ const contentState = useCollapsibleContent({ id: box.with(() => id), forceMount: box.with(() => forceMount), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte index 308dcabf5..5fbb95d70 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte @@ -2,10 +2,25 @@ import type { TriggerProps } from "../index.js"; import { useCollapsibleTrigger } from "../collapsible.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; + import { box } from "svelte-toolbelt"; - let { asChild, children, child, ref = $bindable(), ...restProps }: TriggerProps = $props(); + let { + asChild, + children, + child, + ref = $bindable(null), + id = useId(), + ...restProps + }: TriggerProps = $props(); - const triggerState = useCollapsibleTrigger(); + const triggerState = useCollapsibleTrigger({ + id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), + }); const mergedProps = $derived(mergeProps(restProps, triggerState.props)); diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte index 3a3af4bb1..764063312 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte @@ -3,12 +3,14 @@ import type { RootProps } from "../index.js"; import { useCollapsibleRoot } from "../collapsible.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; let { asChild, children, child, - ref = $bindable(), + id = useId(), + ref = $bindable(null), open = $bindable(false), disabled = false, onOpenChange, @@ -24,6 +26,11 @@ } ), disabled: box.with(() => disabled), + id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, rootState.props)); diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte index bcd244359..e224be16e 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte @@ -2,10 +2,25 @@ import { useDialogClose } from "../dialog.svelte.js"; import type { CloseProps } from "../index.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; + import { box } from "svelte-toolbelt"; - let { asChild, children, child, ref = $bindable(), ...restProps }: CloseProps = $props(); + let { + asChild, + children, + child, + id = useId(), + ref = $bindable(null), + ...restProps + }: CloseProps = $props(); - const closeState = useDialogClose(); + const closeState = useDialogClose({ + id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), + }); const mergedProps = $derived(mergeProps(restProps, closeState.props)); @@ -13,7 +28,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} - {/if} diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte index d56e8e2a7..b467d5049 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte @@ -17,7 +17,7 @@ asChild, children, child, - ref = $bindable(), + ref = $bindable(null), forceMount = false, onDestroyAutoFocus = noop, onEscapeKeydown = noop, @@ -27,6 +27,10 @@ const contentState = useDialogContent({ id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); @@ -42,7 +46,7 @@ onDestroyAutoFocus={(e) => { onDestroyAutoFocus(e); if (e.defaultPrevented) return; - contentState.root.triggerNode?.value?.focus(); + contentState.root.triggerNode?.focus(); }} > {#snippet focusScope({ props: focusScopeProps })} @@ -78,7 +82,6 @@ pointerEvents: "auto", }, })} - bind:this={ref} > {@render children?.()}
diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-description.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-description.svelte index a0188ba56..16666ded7 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-description.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-description.svelte @@ -10,12 +10,16 @@ asChild, children, child, - ref = $bindable(), + ref = $bindable(null), ...restProps }: DescriptionProps = $props(); const descriptionState = useDialogDescription({ id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, descriptionState.props)); @@ -24,7 +28,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-overlay.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-overlay.svelte index 77c7ae1bf..241e9162d 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-overlay.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-overlay.svelte @@ -12,12 +12,16 @@ asChild, child, children, - ref = $bindable(), + ref = $bindable(null), ...restProps }: OverlayProps = $props(); const overlayState = useDialogOverlay({ id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, overlayState.props)); @@ -28,7 +32,7 @@ {#if asChild} {@render child?.({ props: mergeProps(mergedProps, { hidden: !present.value }) })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-title.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-title.svelte index b0696be3a..06c8f07d7 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-title.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-title.svelte @@ -7,7 +7,7 @@ let { id = useId(), - ref = $bindable(), + ref = $bindable(null), asChild, child, children, @@ -18,6 +18,10 @@ const titleState = useDialogTitle({ id: box.with(() => id), level: box.with(() => level), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, titleState.props)); @@ -26,7 +30,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} -
+
{@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-trigger.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-trigger.svelte index faa4af2d9..dea61e1cf 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-trigger.svelte @@ -7,7 +7,7 @@ let { id = useId(), - ref = $bindable(), + ref = $bindable(null), asChild, children, child, @@ -16,6 +16,10 @@ const triggerState = useDialogTrigger({ id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), }); const mergedProps = $derived(mergeProps(restProps, triggerState.props)); @@ -24,7 +28,7 @@ {#if asChild} {@render child?.({ props: mergedProps })} {:else} - {/if} diff --git a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts index eeec042dd..93a42d113 100644 --- a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts +++ b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts @@ -1,8 +1,9 @@ import { box } from "svelte-toolbelt"; import { getAriaExpanded, getDataOpenClosed } from "$lib/internal/attrs.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; -import { useNodeById } from "$lib/internal/useNodeById.svelte.js"; +import { useRefById } from "$lib/internal/useNodeById.svelte.js"; import { createContext } from "$lib/internal/createContext.js"; +import type { WithRefProps } from "$lib/internal/types.js"; const CONTENT_ATTR = "data-dialog-content"; const TITLE_ATTR = "data-dialog-title"; @@ -17,16 +18,14 @@ type DialogRootStateProps = WritableBoxedValues<{ class DialogRootState { open: DialogRootStateProps["open"]; - triggerNode = box(null); - titleNode = box(null); - contentNode = box(null); - contentId = $derived(this.contentNode.value ? this.contentNode.value.id : undefined); - titleId = $derived(this.titleNode.value ? this.titleNode.value.id : undefined); - triggerId = $derived(this.triggerNode.value ? this.triggerNode.value.id : undefined); - descriptionNode = box(null); - descriptionId = $derived( - this.descriptionNode.value ? this.descriptionNode.value.id : undefined - ); + triggerNode = $state(null); + titleNode = $state(null); + contentNode = $state(null); + descriptionNode = $state(null); + contentId = $derived(this.contentNode ? this.contentNode.id : undefined); + titleId = $derived(this.titleNode ? this.titleNode.id : undefined); + triggerId = $derived(this.triggerNode ? this.triggerNode.id : undefined); + descriptionId = $derived(this.descriptionNode ? this.descriptionNode.id : undefined); constructor(props: DialogRootStateProps) { this.open = props.open; @@ -62,8 +61,8 @@ class DialogRootState { return new DialogDescriptionState(props, this); } - createClose() { - return new DialogCloseState(this); + createClose(props: DialogCloseStateProps) { + return new DialogCloseState(props, this); } sharedProps = $derived.by( @@ -74,18 +73,25 @@ class DialogRootState { ); } -type DialogTriggerStateProps = ReadableBoxedValues<{ - id: string; -}>; +type DialogTriggerStateProps = WithRefProps; class DialogTriggerState { #id: DialogTriggerStateProps["id"]; + #ref: DialogTriggerStateProps["ref"]; #root: DialogRootState; constructor(props: DialogTriggerStateProps, root: DialogRootState) { this.#id = props.id; this.#root = root; - this.#root.triggerNode = useNodeById(this.#id); + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.#root.triggerNode = node; + }, + }); } #onclick = () => { @@ -106,11 +112,22 @@ class DialogTriggerState { ); } +type DialogCloseStateProps = WithRefProps; class DialogCloseState { + #id: DialogCloseStateProps["id"]; + #ref: DialogCloseStateProps["ref"]; #root: DialogRootState; - constructor(root: DialogRootState) { + constructor(props: DialogCloseStateProps, root: DialogRootState) { this.#root = root; + this.#ref = props.ref; + this.#id = props.id; + + useRefById({ + id: this.#id, + ref: this.#ref, + condition: () => this.#root.open.value, + }); } #onclick = () => { @@ -120,6 +137,7 @@ class DialogCloseState { props = $derived.by( () => ({ + id: this.#id.value, [CLOSE_ATTR]: "", onclick: this.#onclick, ...this.#root.sharedProps, @@ -127,21 +145,31 @@ class DialogCloseState { ); } -type DialogTitleStateProps = ReadableBoxedValues<{ - id: string; - level: 1 | 2 | 3 | 4 | 5 | 6; -}>; - +type DialogTitleStateProps = WithRefProps< + ReadableBoxedValues<{ + level: 1 | 2 | 3 | 4 | 5 | 6; + }> +>; class DialogTitleState { #id: DialogTitleStateProps["id"]; + #ref: DialogTitleStateProps["ref"]; #root: DialogRootState; #level: DialogTitleStateProps["level"]; constructor(props: DialogTitleStateProps, root: DialogRootState) { this.#id = props.id; this.#root = root; - this.#root.titleNode = useNodeById(this.#id); + this.#ref = props.ref; this.#level = props.level; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.#root.titleNode = node; + }, + condition: () => this.#root.open.value, + }); } props = $derived.by( @@ -156,18 +184,26 @@ class DialogTitleState { ); } -type DialogDescriptionStateProps = ReadableBoxedValues<{ - id: string; -}>; +type DialogDescriptionStateProps = WithRefProps; class DialogDescriptionState { #id: DialogDescriptionStateProps["id"]; + #ref: DialogDescriptionStateProps["ref"]; #root: DialogRootState; constructor(props: DialogDescriptionStateProps, root: DialogRootState) { this.#id = props.id; this.#root = root; - this.#root.descriptionNode = useNodeById(this.#id); + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + condition: () => this.#root.open.value, + onRefChange: (node) => { + this.#root.descriptionNode = node; + }, + }); } props = $derived.by( @@ -180,18 +216,26 @@ class DialogDescriptionState { ); } -type DialogContentStateProps = ReadableBoxedValues<{ - id: string; -}>; +type DialogContentStateProps = WithRefProps; class DialogContentState { #id: DialogContentStateProps["id"]; + #ref: DialogContentStateProps["ref"]; root: DialogRootState; constructor(props: DialogContentStateProps, root: DialogRootState) { this.#id = props.id; this.root = root; - this.root.contentNode = useNodeById(this.#id); + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + condition: () => this.root.open.value, + onRefChange: (node) => { + this.root.contentNode = node; + }, + }); } props = $derived.by( @@ -207,17 +251,23 @@ class DialogContentState { ); } -type DialogOverlayStateProps = ReadableBoxedValues<{ - id: string; -}>; +type DialogOverlayStateProps = WithRefProps; class DialogOverlayState { #id: DialogOverlayStateProps["id"]; + #ref: DialogOverlayStateProps["ref"]; root: DialogRootState; constructor(props: DialogOverlayStateProps, root: DialogRootState) { this.#id = props.id; + this.#ref = props.ref; this.root = root; + + useRefById({ + id: this.#id, + ref: this.#ref, + condition: () => this.root.open.value, + }); } props = $derived.by( @@ -256,6 +306,6 @@ export function useDialogDescription(props: DialogDescriptionStateProps) { return getDialogRootContext().createDescription(props); } -export function useDialogClose() { - return getDialogRootContext().createClose(); +export function useDialogClose(props: DialogCloseStateProps) { + return getDialogRootContext().createClose(props); } diff --git a/packages/bits-ui/src/lib/bits/label/components/label.svelte b/packages/bits-ui/src/lib/bits/label/components/label.svelte index 232bc0b26..d55b995d5 100644 --- a/packages/bits-ui/src/lib/bits/label/components/label.svelte +++ b/packages/bits-ui/src/lib/bits/label/components/label.svelte @@ -2,24 +2,33 @@ import type { RootProps } from "../index.js"; import { setLabelRootState } from "../label.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.svelte.js"; + import { box } from "svelte-toolbelt"; let { asChild, children, child, - ref = $bindable(), + id = useId(), + ref = $bindable(null), for: forProp, ...restProps }: RootProps = $props(); - const rootState = setLabelRootState(); + const rootState = setLabelRootState({ + id: box.with(() => id), + ref: box.with( + () => ref, + (v) => (ref = v) + ), + }); const mergedProps = $derived(mergeProps(restProps, rootState.props, { for: forProp })); {#if asChild} {@render child?.({ props: mergedProps })} {:else} -