From b8f256c7fac0d9762c80b28f74eb89da4004fcd4 Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:19:11 -0400 Subject: [PATCH] next: more things (#680) --- .../context-menu-content-static.svelte | 4 +- .../components/context-menu-content.svelte | 16 +- .../date-field/components/date-field.svelte | 4 +- .../lib/bits/date-field/date-field.svelte.ts | 8 +- .../bits-ui/src/lib/bits/date-field/types.ts | 7 +- .../date-picker/components/date-picker.svelte | 6 +- .../components/date-range-field.svelte | 4 +- .../date-range-field.svelte.ts | 8 +- .../src/lib/bits/date-range-field/types.ts | 2 +- .../components/date-range-picker.svelte | 6 +- .../dropdown-menu-content-static.svelte | 4 +- .../components/dropdown-menu-content.svelte | 16 +- .../link-preview-content-static.svelte | 4 +- .../components/link-preview-content.svelte | 16 +- .../bits/link-preview/link-preview.svelte.ts | 2 + .../src/lib/bits/link-preview/types.ts | 14 +- .../components/listbox-content-static.svelte | 4 +- .../listbox/components/listbox-content.svelte | 4 +- .../src/lib/bits/listbox/listbox.svelte.ts | 2 + .../bits-ui/src/lib/bits/listbox/types.ts | 14 +- .../components/menu-content-static.svelte | 4 +- .../bits/menu/components/menu-content.svelte | 11 +- .../components/menu-sub-content-static.svelte | 9 +- .../menu/components/menu-sub-content.svelte | 9 +- .../bits-ui/src/lib/bits/menu/menu.svelte.ts | 2 + packages/bits-ui/src/lib/bits/menu/types.ts | 25 +- .../components/popover-content-static.svelte | 4 +- .../popover/components/popover-content.svelte | 13 +- .../src/lib/bits/popover/popover.svelte.ts | 2 + .../bits-ui/src/lib/bits/popover/types.ts | 16 +- .../components/tooltip-content-static.svelte | 4 +- .../tooltip/components/tooltip-content.svelte | 13 +- .../src/lib/bits/tooltip/tooltip.svelte.ts | 2 + .../bits-ui/src/lib/bits/tooltip/types.ts | 14 +- .../floating-svelte/floating-utils.svelte.ts | 10 + .../accordion-single-force-mount-test.svelte | 55 +++++ .../src/tests/accordion/accordion.test.ts | 38 ++- .../alert-dialog-force-mount-test.svelte | 105 +++++++++ .../tests/alert-dialog/alert-dialog.test.ts | 37 ++- .../collapsible-force-mount-test.svelte | 34 +++ .../src/tests/collapsible/collapsible.test.ts | 34 ++- .../combobox/combobox-force-mount-test.svelte | 140 +++++++++++ .../src/tests/combobox/combobox.test.ts | 36 ++- .../context-menu-force-mount-test.svelte | 213 +++++++++++++++++ .../tests/context-menu/context-menu.test.ts | 31 ++- .../src/tests/date-field/date-field.test.ts | 2 +- .../dropdown-menu-force-mount-test.svelte | 219 ++++++++++++++++++ .../tests/dropdown-menu/dropdown-menu.test.ts | 36 ++- .../link-preview-force-mount-test.svelte | 61 +++++ .../tests/link-preview/link-preview.test.ts | 29 ++- .../listbox/listbox-force-mount-test.svelte | 120 ++++++++++ .../tests/listbox/listbox-multi-test.svelte | 2 +- .../src/tests/listbox/listbox-test.svelte | 14 +- .../bits-ui/src/tests/listbox/listbox.test.ts | 40 +++- .../popover/popover-force-mount-test.svelte | 53 +++++ .../bits-ui/src/tests/popover/popover.test.ts | 29 ++- .../tooltip/tooltip-force-mount-test.svelte | 58 +++++ .../bits-ui/src/tests/tooltip/tooltip.test.ts | 38 ++- sites/docs/content/components/accordion.md | 2 +- sites/docs/content/components/aspect-ratio.md | 2 +- sites/docs/content/components/button.md | 2 +- .../content/components/date-range-field.md | 1 - sites/docs/content/components/tabs.md | 87 +++++-- sites/docs/content/components/toggle-group.md | 73 ++++-- sites/docs/content/components/toolbar.md | 106 ++++++++- .../components/component-preview-v2.svelte | 13 +- .../lib/components/demo-code-container.svelte | 21 +- .../src/lib/components/demo-code-tabs.svelte | 34 ++- .../{accordion.ts => accordion.api.ts} | 0 .../{alert-dialog.ts => alert-dialog.api.ts} | 0 .../{aspect-ratio.ts => aspect-ratio.api.ts} | 0 .../{avatar.ts => avatar.api.ts} | 0 .../{button.ts => button.api.ts} | 0 .../{calendar.ts => calendar.api.ts} | 0 .../{checkbox.ts => checkbox.api.ts} | 1 - .../{collapsible.ts => collapsible.api.ts} | 0 .../{combobox.ts => combobox.api.ts} | 0 .../{command.ts => command.api.ts} | 0 .../{context-menu.ts => context-menu.api.ts} | 2 +- .../{date-field.ts => date-field.api.ts} | 2 +- .../{date-picker.ts => date-picker.api.ts} | 13 +- ...range-field.ts => date-range-field.api.ts} | 7 +- ...nge-picker.ts => date-range-picker.api.ts} | 14 +- .../{dialog.ts => dialog.api.ts} | 0 ...{dropdown-menu.ts => dropdown-menu.api.ts} | 2 +- .../src/lib/content/api-reference/index.ts | 76 +++--- .../api-reference/{label.ts => label.api.ts} | 0 .../{link-preview.ts => link-preview.api.ts} | 0 .../{listbox.ts => listbox.api.ts} | 0 .../api-reference/{menu.ts => menu.api.ts} | 0 .../{menubar.ts => menubar.api.ts} | 2 +- ...igation-menu.ts => navigation-menu.api.ts} | 0 .../{pagination.ts => pagination.api.ts} | 0 .../{pin-input.ts => pin-input.api.ts} | 0 .../{popover.ts => popover.api.ts} | 0 .../{progress.ts => progress.api.ts} | 0 .../{radio-group.ts => radio-group.api.ts} | 0 ...ange-calendar.ts => range-calendar.api.ts} | 4 +- .../{scroll-area.ts => scroll-area.api.ts} | 0 .../{select.ts => select.api.ts} | 0 .../{separator.ts => separator.api.ts} | 0 .../{slider.ts => slider.api.ts} | 1 - .../{switch.ts => switch.api.ts} | 0 .../api-reference/{tabs.ts => tabs.api.ts} | 0 .../{toggle-group.ts => toggle-group.api.ts} | 2 - .../{toggle.ts => toggle.api.ts} | 0 .../{toolbar.ts => toolbar.api.ts} | 0 .../{tooltip.ts => tooltip.api.ts} | 0 108 files changed, 1890 insertions(+), 284 deletions(-) create mode 100644 packages/bits-ui/src/tests/accordion/accordion-single-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/alert-dialog/alert-dialog-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/collapsible/collapsible-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/combobox/combobox-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/context-menu/context-menu-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/dropdown-menu/dropdown-menu-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/link-preview/link-preview-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/listbox/listbox-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/popover/popover-force-mount-test.svelte create mode 100644 packages/bits-ui/src/tests/tooltip/tooltip-force-mount-test.svelte rename sites/docs/src/lib/content/api-reference/{accordion.ts => accordion.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{alert-dialog.ts => alert-dialog.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{aspect-ratio.ts => aspect-ratio.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{avatar.ts => avatar.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{button.ts => button.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{calendar.ts => calendar.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{checkbox.ts => checkbox.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{collapsible.ts => collapsible.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{combobox.ts => combobox.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{command.ts => command.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{context-menu.ts => context-menu.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{date-field.ts => date-field.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{date-picker.ts => date-picker.api.ts} (94%) rename sites/docs/src/lib/content/api-reference/{date-range-field.ts => date-range-field.api.ts} (96%) rename sites/docs/src/lib/content/api-reference/{date-range-picker.ts => date-range-picker.api.ts} (93%) rename sites/docs/src/lib/content/api-reference/{dialog.ts => dialog.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{dropdown-menu.ts => dropdown-menu.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{label.ts => label.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{link-preview.ts => link-preview.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{listbox.ts => listbox.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{menu.ts => menu.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{menubar.ts => menubar.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{navigation-menu.ts => navigation-menu.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{pagination.ts => pagination.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{pin-input.ts => pin-input.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{popover.ts => popover.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{progress.ts => progress.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{radio-group.ts => radio-group.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{range-calendar.ts => range-calendar.api.ts} (97%) rename sites/docs/src/lib/content/api-reference/{scroll-area.ts => scroll-area.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{select.ts => select.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{separator.ts => separator.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{slider.ts => slider.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{switch.ts => switch.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{tabs.ts => tabs.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{toggle-group.ts => toggle-group.api.ts} (99%) rename sites/docs/src/lib/content/api-reference/{toggle.ts => toggle.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{toolbar.ts => toolbar.api.ts} (100%) rename sites/docs/src/lib/content/api-reference/{tooltip.ts => tooltip.api.ts} (100%) diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte index 1b0bf1118..9e4a7672e 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte @@ -85,10 +85,10 @@ > {#snippet popper({ props })} {#if child} - {@render child({ props })} + {@render child({ props, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte index e8192c89f..96bdcdb1e 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte @@ -9,6 +9,7 @@ import { isElement } from "$lib/internal/is.js"; import type { InteractOutsideEvent } from "$lib/bits/utilities/dismissable-layer/types.js"; import Mounted from "$lib/bits/utilities/mounted.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { id = useId(), @@ -88,22 +89,13 @@ > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { - style: { - "--bits-context-menu-content-transform-origin": - "var(--bits-floating-transform-origin)", - "--bits-context-menu-content-available-width": - "var(--bits-floating-available-width)", - "--bits-context-menu-content-available-height": - "var(--bits-floating-available-height)", - "--bits-context-menu-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-context-menu-anchor-height": "var(--bits-floating-anchor-height)", - }, + style: getFloatingContentCSSVars("context-menu"), })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte index a4a7af0a3..074d82775 100644 --- a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte +++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte @@ -7,7 +7,6 @@ import { getDefaultDate } from "$lib/shared/date/utils.js"; let { - isDateUnavailable, disabled = false, granularity, hideTimeZone = false, @@ -17,6 +16,7 @@ minValue, onPlaceholderChange = noop, onValueChange = noop, + isDateInvalid, placeholder = $bindable(), value = $bindable(), readonly = false, @@ -71,7 +71,7 @@ locale: box.with(() => locale), maxValue: box.with(() => maxValue), minValue: box.with(() => minValue), - isDateUnavailable: box.with(() => isDateUnavailable), + isDateInvalid: box.with(() => isDateInvalid), readonly: box.with(() => readonly), readonlySegments: box.with(() => readonlySegments), required: box.with(() => required), diff --git a/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts index e01c00cc3..26b93c0e6 100644 --- a/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts @@ -65,7 +65,7 @@ export type DateFieldRootStateProps = WritableBoxedValues<{ }> & ReadableBoxedValues<{ readonlySegments: SegmentPart[]; - isDateUnavailable: DateMatcher | undefined; + isDateInvalid: DateMatcher | undefined; minValue: DateValue | undefined; maxValue: DateValue | undefined; disabled: boolean; @@ -80,7 +80,7 @@ export type DateFieldRootStateProps = WritableBoxedValues<{ export class DateFieldRootState { value: DateFieldRootStateProps["value"]; placeholder: WritableBox; - isDateUnavailable: DateFieldRootStateProps["isDateUnavailable"]; + isDateInvalid: DateFieldRootStateProps["isDateInvalid"]; minValue: DateFieldRootStateProps["minValue"]; maxValue: DateFieldRootStateProps["maxValue"]; disabled: DateFieldRootStateProps["disabled"]; @@ -116,7 +116,7 @@ export class DateFieldRootState { */ this.value = props.value; this.placeholder = rangeRoot ? rangeRoot.placeholder : props.placeholder; - this.isDateUnavailable = rangeRoot ? rangeRoot.isDateUnavailable : props.isDateUnavailable; + this.isDateInvalid = rangeRoot ? rangeRoot.isDateInvalid : props.isDateInvalid; this.minValue = rangeRoot ? rangeRoot.minValue : props.minValue; this.maxValue = rangeRoot ? rangeRoot.maxValue : props.maxValue; this.disabled = rangeRoot ? rangeRoot.disabled : props.disabled; @@ -340,7 +340,7 @@ export class DateFieldRootState { isInvalid = $derived.by(() => { const value = this.value.current; if (!value) return false; - if (this.isDateUnavailable.current?.(value)) return true; + if (this.isDateInvalid.current?.(value)) return true; const minValue = this.minValue.current; if (minValue && isBefore(value, minValue)) return true; const maxValue = this.maxValue.current; diff --git a/packages/bits-ui/src/lib/bits/date-field/types.ts b/packages/bits-ui/src/lib/bits/date-field/types.ts index 40036d923..9baf95707 100644 --- a/packages/bits-ui/src/lib/bits/date-field/types.ts +++ b/packages/bits-ui/src/lib/bits/date-field/types.ts @@ -34,10 +34,11 @@ export type DateFieldRootPropsWithoutHTML = WithChildren<{ onPlaceholderChange?: OnChangeFn; /** - * A function that returns true if the given date is unavailable, - * where if selected, the date field will be marked as invalid. + * A function that returns true if the given date is invalid. This will mark + * the field as invalid and you will be responsible for displaying an error message + * to the user to inform them of the invalid state. */ - isDateUnavailable?: DateMatcher; + isDateInvalid?: DateMatcher; /** * The minimum acceptable date. When provided, the date field diff --git a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte index bdda47ce2..a4767804b 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte +++ b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte @@ -131,12 +131,16 @@ open: pickerRootState.props.open, }); + function isUnavailableOrDisabled(date: DateValue) { + return isDateDisabled(date) || isDateUnavailable(date); + } + useDateFieldRoot({ value: pickerRootState.props.value, disabled: pickerRootState.props.disabled, readonly: pickerRootState.props.readonly, readonlySegments: pickerRootState.props.readonlySegments, - isDateUnavailable: pickerRootState.props.isDateUnavailable, + isDateInvalid: box.with(() => isUnavailableOrDisabled), minValue: pickerRootState.props.minValue, maxValue: pickerRootState.props.maxValue, granularity: pickerRootState.props.granularity, diff --git a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte index efbe96672..c7abacf87 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte +++ b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte @@ -23,7 +23,7 @@ granularity, locale = "en-US", hideTimeZone = false, - isDateUnavailable, + isDateInvalid, maxValue, minValue, readonlySegments = [], @@ -75,7 +75,7 @@ granularity: box.with(() => granularity), locale: box.with(() => locale), hideTimeZone: box.with(() => hideTimeZone), - isDateUnavailable: box.with(() => isDateUnavailable), + isDateInvalid: box.with(() => isDateInvalid), maxValue: box.with(() => maxValue), minValue: box.with(() => minValue), placeholder: box.with( diff --git a/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts b/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts index 90410a9f7..0374d4c24 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts @@ -28,7 +28,7 @@ type DateRangeFieldRootStateProps = WithRefProps< }> & ReadableBoxedValues<{ readonlySegments: SegmentPart[]; - isDateUnavailable: DateMatcher | undefined; + isDateInvalid: DateMatcher | undefined; minValue: DateValue | undefined; maxValue: DateValue | undefined; disabled: boolean; @@ -47,7 +47,7 @@ export class DateRangeFieldRootState { value: DateRangeFieldRootStateProps["value"]; placeholder: DateRangeFieldRootStateProps["placeholder"]; readonlySegments: DateRangeFieldRootStateProps["readonlySegments"]; - isDateUnavailable: DateRangeFieldRootStateProps["isDateUnavailable"]; + isDateInvalid: DateRangeFieldRootStateProps["isDateInvalid"]; minValue: DateRangeFieldRootStateProps["minValue"]; maxValue: DateRangeFieldRootStateProps["maxValue"]; disabled: DateRangeFieldRootStateProps["disabled"]; @@ -89,7 +89,7 @@ export class DateRangeFieldRootState { this.startValue = props.startValue; this.endValue = props.endValue; this.placeholder = props.placeholder; - this.isDateUnavailable = props.isDateUnavailable; + this.isDateInvalid = props.isDateInvalid; this.minValue = props.minValue; this.maxValue = props.maxValue; this.disabled = props.disabled; @@ -197,7 +197,7 @@ export class DateRangeFieldRootState { disabled: this.disabled, readonly: this.readonly, readonlySegments: this.readonlySegments, - isDateUnavailable: this.isDateUnavailable, + isDateInvalid: this.isDateInvalid, minValue: this.minValue, maxValue: this.maxValue, hourCycle: this.hourCycle, diff --git a/packages/bits-ui/src/lib/bits/date-range-field/types.ts b/packages/bits-ui/src/lib/bits/date-range-field/types.ts index 2d3012dc7..26cbe2fec 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/types.ts +++ b/packages/bits-ui/src/lib/bits/date-range-field/types.ts @@ -35,7 +35,7 @@ export type DateRangeFieldRootPropsWithoutHTML = WithChild<{ * A function that returns true if the given date is unavailable, * where if selected, the date field will be marked as invalid. */ - isDateUnavailable?: DateMatcher; + isDateInvalid?: DateMatcher; /** * The minimum acceptable date. When provided, the date field diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte b/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte index 6f7a14dfb..835664731 100644 --- a/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte +++ b/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte @@ -161,12 +161,16 @@ open: pickerRootState.props.open, }); + function isUnavailableOrDisabled(date: DateValue) { + return isDateDisabled(date) || isDateUnavailable(date); + } + const fieldRootState = useDateRangeFieldRoot({ value: pickerRootState.props.value, disabled: pickerRootState.props.disabled, readonly: pickerRootState.props.readonly, readonlySegments: pickerRootState.props.readonlySegments, - isDateUnavailable: pickerRootState.props.isDateUnavailable, + isDateInvalid: box.with(() => isUnavailableOrDisabled), minValue: pickerRootState.props.minValue, maxValue: pickerRootState.props.maxValue, granularity: pickerRootState.props.granularity, diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte index e4a2838b7..cc71b26f9 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte @@ -59,10 +59,10 @@ {#snippet popper({ props })} {@const finalProps = mergeProps(props)} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte index c947a8c3c..43146f835 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte @@ -7,6 +7,7 @@ import { noop } from "$lib/internal/callbacks.js"; import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; import Mounted from "$lib/bits/utilities/mounted.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { id = useId(), @@ -57,22 +58,13 @@ > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { - style: { - "--bits-dropdown-menu-content-transform-origin": - "var(--bits-floating-transform-origin)", - "--bits-dropdown-menu-content-available-width": - "var(--bits-floating-available-width)", - "--bits-dropdown-menu-content-available-height": - "var(--bits-floating-available-height)", - "--bits-dropdown-menu-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-dropdown-menu-anchor-height": "var(--bits-floating-anchor-height)", - }, + style: getFloatingContentCSSVars("dropdown-menu"), })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte index 4ab9e543a..7785e17f6 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte @@ -51,10 +51,10 @@ > {#snippet popper({ props })} {#if child} - {@render child({ props })} + {@render child({ props, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte index dc9bb6016..71bd6c732 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte +++ b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte @@ -5,6 +5,7 @@ import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { children, @@ -69,22 +70,13 @@ > {#snippet popper({ props })} {@const mergedProps = mergeProps(props, { - style: { - "--bits-link-preview-content-transform-origin": - "var(--bits-floating-transform-origin)", - "--bits-link-preview-content-available-width": - "var(--bits-floating-available-width)", - "--bits-link-preview-content-available-height": - "var(--bits-floating-available-height)", - "--bits-link-preview-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-link-preview-anchor-height": "var(--bits-floating-anchor-height)", - }, + style: getFloatingContentCSSVars("link-preview"), })} {#if child} - {@render child({ props: mergedProps })} + {@render child({ props: mergedProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts b/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts index 9c1ff0e24..ea81acf86 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts +++ b/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts @@ -231,6 +231,8 @@ class LinkPreviewContentState { e.preventDefault(); }; + snippetProps = $derived.by(() => ({ open: this.root.open.current })); + props = $derived.by( () => ({ diff --git a/packages/bits-ui/src/lib/bits/link-preview/types.ts b/packages/bits-ui/src/lib/bits/link-preview/types.ts index 6fcb782cd..06c87b087 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/types.ts +++ b/packages/bits-ui/src/lib/bits/link-preview/types.ts @@ -60,6 +60,14 @@ export type LinkPreviewRootPropsWithoutHTML = WithChildren<{ export type LinkPreviewRootProps = LinkPreviewRootPropsWithoutHTML; +export type LinkPreviewContentSnippetProps = { + /** + * Whether the content is open or closed. Used alongside the `forceMount` prop to + * conditionally render the content using Svelte transitions. + */ + open: boolean; +}; + export type LinkPreviewContentPropsWithoutHTML = WithChild< Pick< FloatingLayerContentProps, @@ -83,7 +91,8 @@ export type LinkPreviewContentPropsWithoutHTML = WithChild< * Useful for more control over the transition behavior. */ forceMount?: boolean; - } + }, + LinkPreviewContentSnippetProps >; export type LinkPreviewContentProps = LinkPreviewContentPropsWithoutHTML & @@ -99,7 +108,8 @@ export type LinkPreviewContentStaticPropsWithoutHTML = WithChild< * Useful for more control over the transition behavior. */ forceMount?: boolean; - } + }, + LinkPreviewContentSnippetProps >; export type LinkPreviewContentStaticProps = LinkPreviewContentStaticPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/listbox/components/listbox-content-static.svelte b/packages/bits-ui/src/lib/bits/listbox/components/listbox-content-static.svelte index 6e239446b..280765cd1 100644 --- a/packages/bits-ui/src/lib/bits/listbox/components/listbox-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/listbox/components/listbox-content-static.svelte @@ -59,10 +59,10 @@ style: contentState.props.style, })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/listbox/components/listbox-content.svelte b/packages/bits-ui/src/lib/bits/listbox/components/listbox-content.svelte index 6a626f30d..d5dd09332 100644 --- a/packages/bits-ui/src/lib/bits/listbox/components/listbox-content.svelte +++ b/packages/bits-ui/src/lib/bits/listbox/components/listbox-content.svelte @@ -67,10 +67,10 @@ }, })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts b/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts index 89945ba7b..10e0b25e8 100644 --- a/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts @@ -698,6 +698,8 @@ class ListboxContentState { } }; + snippetProps = $derived.by(() => ({ open: this.root.open.current })); + props = $derived.by( () => ({ diff --git a/packages/bits-ui/src/lib/bits/listbox/types.ts b/packages/bits-ui/src/lib/bits/listbox/types.ts index 89c5062d2..29795edc0 100644 --- a/packages/bits-ui/src/lib/bits/listbox/types.ts +++ b/packages/bits-ui/src/lib/bits/listbox/types.ts @@ -142,10 +142,19 @@ export type _SharedListboxContentProps = { loop?: boolean; }; +export type ListboxContentSnippetProps = { + /** + * Whether the content is open or closed. Used alongside the `forceMount` prop to conditionally + * render the content using Svelte transitions. + */ + open: boolean; +}; + export type ListboxContentPropsWithoutHTML = Expand< WithChild< Omit & - _SharedListboxContentProps + _SharedListboxContentProps, + ListboxContentSnippetProps > >; @@ -155,7 +164,8 @@ export type ListboxContentProps = ListboxContentPropsWithoutHTML & export type ListboxContentStaticPropsWithoutHTML = Expand< WithChild< Omit & - _SharedListboxContentProps + _SharedListboxContentProps, + ListboxContentSnippetProps > >; diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte index 6d6095a36..6889f3e37 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte @@ -77,10 +77,10 @@ }, })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte index c2d6d0a37..63e144b09 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte @@ -9,6 +9,7 @@ import { isElement } from "$lib/internal/is.js"; import type { InteractOutsideEvent } from "$lib/bits/utilities/dismissable-layer/types.js"; import Mounted from "$lib/bits/utilities/mounted.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { id = useId(), @@ -73,18 +74,14 @@ {@const finalProps = mergeProps(props, { style: { outline: "none", - "--bits-menu-content-transform-origin": "var(--bits-floating-transform-origin)", - "--bits-menu-content-available-width": "var(--bits-floating-available-width)", - "--bits-menu-content-available-height": "var(--bits-floating-available-height)", - "--bits-menu-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-menu-anchor-height": "var(--bits-floating-anchor-height)", + ...getFloatingContentCSSVars("menu"), }, })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte index d236d9ffa..f39461373 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte @@ -10,6 +10,7 @@ import { isHTMLElement } from "$lib/internal/is.js"; import { afterTick } from "$lib/internal/afterTick.js"; import Mounted from "$lib/bits/utilities/mounted.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { id = useId(), @@ -118,12 +119,14 @@ {loop} > {#snippet popper({ props })} - {@const finalProps = mergeProps(props, mergedProps)} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...subContentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(subContentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte index 76973c4f5..d3d708025 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte @@ -10,6 +10,7 @@ import { isHTMLElement } from "$lib/internal/is.js"; import { afterTick } from "$lib/internal/afterTick.js"; import Mounted from "$lib/bits/utilities/mounted.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { id = useId(), @@ -119,12 +120,14 @@ {loop} > {#snippet popper({ props })} - {@const finalProps = mergeProps(props, mergedProps)} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...subContentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(subContentState.snippetProps)}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts index 5cbdb5a0d..7b8e48ce0 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -401,6 +401,8 @@ class MenuContentState { } }; + snippetProps = $derived.by(() => ({ open: this.parentMenu.open.current })); + props = $derived.by( () => ({ diff --git a/packages/bits-ui/src/lib/bits/menu/types.ts b/packages/bits-ui/src/lib/bits/menu/types.ts index 1c7a745ba..c5796c7d8 100644 --- a/packages/bits-ui/src/lib/bits/menu/types.ts +++ b/packages/bits-ui/src/lib/bits/menu/types.ts @@ -45,15 +45,26 @@ export type _SharedMenuContentProps = { loop?: boolean; }; +export type MenuContentSnippetProps = { + /** + * Whether the content is open or closed. Used alongside the `forceMount` prop to + * conditionally render the content using Svelte transitions. + */ + open: boolean; +}; + export type MenuContentPropsWithoutHTML = Expand< - WithChild & _SharedMenuContentProps> + WithChild & _SharedMenuContentProps, MenuContentSnippetProps> >; export type MenuContentProps = MenuContentPropsWithoutHTML & Without; export type MenuContentStaticPropsWithoutHTML = Expand< - WithChild & _SharedMenuContentProps> + WithChild< + Omit & _SharedMenuContentProps, + MenuContentSnippetProps + > >; export type MenuContentStaticProps = MenuContentStaticPropsWithoutHTML & @@ -155,14 +166,20 @@ export type MenuSubPropsWithoutHTML = WithChildren<{ }>; export type MenuSubContentPropsWithoutHTML = Expand< - WithChild & _SharedMenuContentProps> + WithChild< + Omit & _SharedMenuContentProps, + MenuContentSnippetProps + > >; export type MenuSubContentProps = MenuSubContentPropsWithoutHTML & Without; export type MenuSubContentStaticPropsWithoutHTML = Expand< - WithChild & _SharedMenuContentProps> + WithChild< + Omit & _SharedMenuContentProps, + MenuContentSnippetProps + > >; export type MenuSubContentStaticProps = MenuSubContentStaticPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte index 76611e43c..f44cbed03 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte @@ -61,10 +61,10 @@ {#snippet popper({ props })} {@const finalProps = mergeProps(props)} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte index 42f2f93cc..2453e0f10 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte @@ -6,6 +6,7 @@ import { mergeProps } from "$lib/internal/mergeProps.js"; import { noop } from "$lib/internal/noop.js"; import { useId } from "$lib/internal/useId.js"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { child, @@ -59,19 +60,13 @@ > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { - style: { - "--bits-popover-content-transform-origin": "var(--bits-floating-transform-origin)", - "--bits-popover-content-available-width": "var(--bits-floating-available-width)", - "--bits-popover-content-available-height": "var(--bits-floating-available-height)", - "--bits-popover-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-popover-anchor-height": "var(--bits-floating-anchor-height)", - }, + style: getFloatingContentCSSVars("popover"), })} {#if child} - {@render child({ props: finalProps })} + {@render child({ props: finalProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts index d904e3802..bec0b1651 100644 --- a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts +++ b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts @@ -117,6 +117,8 @@ class PopoverContentState { }); } + snippetProps = $derived.by(() => ({ open: this.root.open.current })); + props = $derived.by(() => ({ id: this.#id.current, tabindex: -1, diff --git a/packages/bits-ui/src/lib/bits/popover/types.ts b/packages/bits-ui/src/lib/bits/popover/types.ts index 1ea881d4a..b540d5a93 100644 --- a/packages/bits-ui/src/lib/bits/popover/types.ts +++ b/packages/bits-ui/src/lib/bits/popover/types.ts @@ -26,13 +26,25 @@ export type PopoverRootPropsWithoutHTML = WithChildren<{ export type PopoverRootProps = PopoverRootPropsWithoutHTML; -export type PopoverContentPropsWithoutHTML = WithChild>; +export type PopoverContentSnippetProps = { + /** + * Whether the content is open or closed. Used alongside the `forceMount` prop to + * conditionally render the content using Svelte transitions. + */ + open: boolean; +}; + +export type PopoverContentPropsWithoutHTML = WithChild< + Omit, + PopoverContentSnippetProps +>; export type PopoverContentProps = PopoverContentPropsWithoutHTML & Without; export type PopoverContentStaticPropsWithoutHTML = WithChild< - Omit + Omit, + PopoverContentSnippetProps >; export type PopoverContentStaticProps = PopoverContentStaticPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte index 5992f184a..b704594e0 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte @@ -52,10 +52,10 @@ {#snippet popper({ props })} {@const mergedProps = mergeProps(props)} {#if child} - {@render child({ props: mergedProps })} + {@render child({ props: mergedProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte index 55f13adf7..a7b8a328e 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte +++ b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte @@ -5,6 +5,7 @@ import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; + import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; let { children, @@ -69,19 +70,13 @@ > {#snippet popper({ props })} {@const mergedProps = mergeProps(props, { - style: { - "--bits-tooltip-content-transform-origin": "var(--bits-floating-transform-origin)", - "--bits-tooltip-content-available-width": "var(--bits-floating-available-width)", - "--bits-tooltip-content-available-height": "var(--bits-floating-available-height)", - "--bits-tooltip-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-tooltip-anchor-height": "var(--bits-floating-anchor-height)", - }, + style: getFloatingContentCSSVars("tooltip"), })} {#if child} - {@render child({ props: mergedProps })} + {@render child({ props: mergedProps, ...contentState.snippetProps })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts index 515414e76..19913ecb8 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts +++ b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts @@ -348,6 +348,8 @@ class TooltipContentState { }); } + snippetProps = $derived.by(() => ({ open: this.root.open.current })); + props = $derived.by(() => ({ id: this.#id.current, "data-state": this.root.stateAttr, diff --git a/packages/bits-ui/src/lib/bits/tooltip/types.ts b/packages/bits-ui/src/lib/bits/tooltip/types.ts index 47e1059dd..ff0a5b9fb 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/types.ts +++ b/packages/bits-ui/src/lib/bits/tooltip/types.ts @@ -114,6 +114,14 @@ export type TooltipRootPropsWithoutHTML = WithChildren<{ export type TooltipRootProps = TooltipRootPropsWithoutHTML; +export type TooltipContentSnippetProps = { + /** + * Whether the content is open or closed. Used alongside the `forceMount` prop to + * conditionally render the content using Svelte transitions. + */ + open: boolean; +}; + export type TooltipContentPropsWithoutHTML = WithChild< Pick< FloatingLayerContentProps, @@ -137,7 +145,8 @@ export type TooltipContentPropsWithoutHTML = WithChild< * Useful for more control over the transition behavior. */ forceMount?: boolean; - } + }, + TooltipContentSnippetProps >; export type TooltipContentProps = TooltipContentPropsWithoutHTML & @@ -153,7 +162,8 @@ export type TooltipContentStaticPropsWithoutHTML = WithChild< * Useful for more control over the transition behavior. */ forceMount?: boolean; - } + }, + TooltipContentSnippetProps >; export type TooltipContentStaticProps = TooltipContentStaticPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/internal/floating-svelte/floating-utils.svelte.ts b/packages/bits-ui/src/lib/internal/floating-svelte/floating-utils.svelte.ts index 078726db3..664710286 100644 --- a/packages/bits-ui/src/lib/internal/floating-svelte/floating-utils.svelte.ts +++ b/packages/bits-ui/src/lib/internal/floating-svelte/floating-utils.svelte.ts @@ -16,3 +16,13 @@ export function roundByDPR(element: Element, value: number) { const dpr = getDPR(element); return Math.round(value * dpr) / dpr; } + +export function getFloatingContentCSSVars(name: string) { + return { + [`--bits-${name}-content-transform-origin`]: `var(--bits-floating-transform-origin)`, + [`--bits-${name}-content-available-width`]: `var(--bits-floating-available-width)`, + [`--bits-${name}-content-available-height`]: `var(--bits-floating-available-height)`, + [`--bits-${name}-anchor-width`]: `var(--bits-floating-anchor-width)`, + [`--bits-${name}-anchor-height`]: `var(--bits-floating-anchor-height)`, + }; +} diff --git a/packages/bits-ui/src/tests/accordion/accordion-single-force-mount-test.svelte b/packages/bits-ui/src/tests/accordion/accordion-single-force-mount-test.svelte new file mode 100644 index 000000000..95a1f8ebb --- /dev/null +++ b/packages/bits-ui/src/tests/accordion/accordion-single-force-mount-test.svelte @@ -0,0 +1,55 @@ + + + + {#each items as { value, title, disabled, content, level }} + + + + {title} + + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ {content} +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ {content} +
+ {/snippet} +
+ {/if} +
+ {/each} +
diff --git a/packages/bits-ui/src/tests/accordion/accordion.test.ts b/packages/bits-ui/src/tests/accordion/accordion.test.ts index bdb0db049..7bbb3f17c 100644 --- a/packages/bits-ui/src/tests/accordion/accordion.test.ts +++ b/packages/bits-ui/src/tests/accordion/accordion.test.ts @@ -9,6 +9,7 @@ import AccordionMultiTest from "./accordion-multi-test.svelte"; import AccordionTestIsolated from "./accordion-test-isolated.svelte"; import AccordionSingleTestControlledSvelte from "./accordion-single-test-controlled.svelte"; import AccordionMultiTestControlled from "./accordion-multi-test-controlled.svelte"; +import AccordionSingleForceMountTest from "./accordion-single-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; export type Item = { @@ -99,6 +100,41 @@ describe("accordion - single", () => { expect(triggerEls[1]).toHaveAttribute("data-disabled"); }); + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = render(AccordionSingleForceMountTest as any, { + items: itemsWithDisabled, + }); + const contentEls = items.map((item) => getByTestId(`${item.value}-content`)); + + for (const content of contentEls) { + expect(content).toBeVisible(); + } + }); + + it("work properly when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const user = setupUserEvents(); + const { getByTestId, queryByTestId } = render(AccordionSingleForceMountTest as any, { + items: itemsWithDisabled, + withOpenCheck: true, + }); + const initContentEls = items.map((item) => queryByTestId(`${item.value}-content`)); + + for (const content of initContentEls) { + expect(content).toBeNull(); + } + + const triggerEls = items.map((item) => getByTestId(`${item.value}-trigger`)); + + // open the first item + await user.click(triggerEls[0] as HTMLElement); + + const firstContentEl = getByTestId(`${items[0]!.value}-content`); + expect(firstContentEl).toBeVisible(); + + const secondContentEl = queryByTestId(`${items[1]!.value}-content`); + expect(secondContentEl).toBeNull(); + }); + it("should disable everything when the `disabled` prop is true", async () => { const user = setupUserEvents(); const { getByTestId } = render(AccordionSingleTest as any, { @@ -138,7 +174,7 @@ describe("accordion - single", () => { } }); - it("should expand only one item at a time when `multiple` is false", async () => { + it("should expand only one item at a time when type is `'single'`", async () => { const user = setupUserEvents(); const { getByTestId } = render(AccordionSingleTest as any, { items }); diff --git a/packages/bits-ui/src/tests/alert-dialog/alert-dialog-force-mount-test.svelte b/packages/bits-ui/src/tests/alert-dialog/alert-dialog-force-mount-test.svelte new file mode 100644 index 000000000..17f991d9c --- /dev/null +++ b/packages/bits-ui/src/tests/alert-dialog/alert-dialog-force-mount-test.svelte @@ -0,0 +1,105 @@ + + + + +
+ + open + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ {/snippet} +
+ {/if} + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ title + + description + + cancel + action +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ title + + description + + cancel + action +
+ {/snippet} +
+ {/if} +
+
+

{open}

+ +
+
diff --git a/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts b/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts index f9f8568d6..353d26cc7 100644 --- a/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts +++ b/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts @@ -8,8 +8,10 @@ import { import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; +import type { Component } from "svelte"; import { getTestKbd } from "../utils.js"; import AlertDialogTest, { type AlertDialogTestProps } from "./alert-dialog-test.svelte"; +import AlertDialogForceMountTest from "./alert-dialog-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; const kbd = getTestKbd(); @@ -28,10 +30,10 @@ async function expectIsOpen( await waitFor(() => expect(content).not.toBeNull()); } -function setup(props: AlertDialogTestProps = {}) { +function setup(props: AlertDialogTestProps = {}, component: Component = AlertDialogTest) { const user = userEvent.setup({ pointerEventsCheck: 0 }); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(AlertDialogTest, { ...props }); + const returned = render(component, { ...props }); const trigger = returned.getByTestId("trigger"); return { @@ -83,6 +85,37 @@ describe("alert dialog", () => { await open(); }); + it("should forceMount the content and overlay when their `forceMount` prop is true", async () => { + const { getByTestId } = setup({}, AlertDialogForceMountTest); + + expect(getByTestId("overlay")).toBeInTheDocument(); + expect(getByTestId("content")).toBeInTheDocument(); + }); + + it("should forceMount the content and overlay when their `forceMount` prop is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { getByTestId, queryByTestId, user } = setup( + { + // @ts-expect-error - testing lib needs to update their generic types + withOpenCheck: true, + }, + AlertDialogForceMountTest + ); + const initOverlay = queryByTestId("overlay"); + const initContent = queryByTestId("content"); + + expect(initOverlay).toBeNull(); + expect(initContent).toBeNull(); + + const trigger = getByTestId("trigger"); + await user.click(trigger); + + const overlay = getByTestId("overlay"); + expect(overlay).toBeInTheDocument(); + + const content = getByTestId("content"); + expect(content).toBeInTheDocument(); + }); + it("should focus the cancel button by default when opened", async () => { const { cancel } = await open(); expect(cancel).toHaveFocus(); diff --git a/packages/bits-ui/src/tests/collapsible/collapsible-force-mount-test.svelte b/packages/bits-ui/src/tests/collapsible/collapsible-force-mount-test.svelte new file mode 100644 index 000000000..440aaa390 --- /dev/null +++ b/packages/bits-ui/src/tests/collapsible/collapsible-force-mount-test.svelte @@ -0,0 +1,34 @@ + + +
+

{open}

+ + Trigger + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
Content
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
Content
+ {/snippet} +
+ {/if} +
+ +
diff --git a/packages/bits-ui/src/tests/collapsible/collapsible.test.ts b/packages/bits-ui/src/tests/collapsible/collapsible.test.ts index 6c7f08bb6..1b08c9ddd 100644 --- a/packages/bits-ui/src/tests/collapsible/collapsible.test.ts +++ b/packages/bits-ui/src/tests/collapsible/collapsible.test.ts @@ -1,14 +1,21 @@ import { render } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; +import type { Component } from "svelte"; import { setupUserEvents } from "../utils.js"; import CollapsibleTest from "./collapsible-test.svelte"; +import CollapsibleForceMountTest from "./collapsible-force-mount-test.svelte"; import type { Collapsible } from "$lib/index.js"; -function setup(props?: Collapsible.RootProps) { +function setup( + props: Collapsible.RootProps & { + withOpenCheck?: boolean; + } = {}, + component: Component = CollapsibleTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(CollapsibleTest, props); + const returned = render(component, props); const root = returned.getByTestId("root"); const trigger = returned.getByTestId("trigger"); const content = returned.queryByTestId("content"); @@ -62,4 +69,27 @@ describe("collapsible", () => { await user.click(altTrigger); expect(binding).toHaveTextContent("false"); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({ withOpenCheck: false }, CollapsibleForceMountTest); + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { getByTestId, queryByTestId, user } = setup( + { + withOpenCheck: true, + }, + CollapsibleForceMountTest + ); + const initContent = queryByTestId("content"); + expect(initContent).toBeNull(); + + const trigger = getByTestId("trigger"); + await user.click(trigger); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/packages/bits-ui/src/tests/combobox/combobox-force-mount-test.svelte b/packages/bits-ui/src/tests/combobox/combobox-force-mount-test.svelte new file mode 100644 index 000000000..5394eb7de --- /dev/null +++ b/packages/bits-ui/src/tests/combobox/combobox-force-mount-test.svelte @@ -0,0 +1,140 @@ + + + + +
+ { + onOpenChange?.(v); + if (!v) searchValue = ""; + }} + > + Open combobox + (searchValue = e.currentTarget.value)} + {...inputProps} + /> + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ + Options + {#each filteredItems as { value, label, disabled }} + + {#snippet children({ selected })} + {#if selected} + x + {/if} + {label} + {/snippet} + + {/each} + +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ + Options + {#each filteredItems as { value, label, disabled }} + + {#snippet children({ selected })} + {#if selected} + x + {/if} + {label} + {/snippet} + + {/each} + +
+ {/snippet} +
+ {/if} +
+
+
+ + + +
+
diff --git a/packages/bits-ui/src/tests/combobox/combobox.test.ts b/packages/bits-ui/src/tests/combobox/combobox.test.ts index 3261d5067..e8c0c7c6c 100644 --- a/packages/bits-ui/src/tests/combobox/combobox.test.ts +++ b/packages/bits-ui/src/tests/combobox/combobox.test.ts @@ -1,12 +1,15 @@ import { render, waitFor } from "@testing-library/svelte"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; -import { tick } from "svelte"; +import { type Component, tick } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import ComboboxTest from "./combobox-test.svelte"; import type { ComboboxSingleTestProps, Item } from "./combobox-test.svelte"; import type { ComboboxMultipleTestProps } from "./combobox-multi-test.svelte"; import ComboboxMultiTest from "./combobox-multi-test.svelte"; +import ComboboxForceMountTest, { + type ComboboxForceMountTestProps, +} from "./combobox-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; import type { AnyFn } from "$lib/internal/types.js"; @@ -31,10 +34,15 @@ const testItems: Item[] = [ }, ]; -function setupSingle(props: Partial = {}, items: Item[] = testItems) { +function setupSingle( + props: Partial = {}, + items: Item[] = testItems, + // eslint-disable-next-line ts/no-explicit-any + component: Component = ComboboxTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(ComboboxTest, { name: "test", ...props, items }); + const returned = render(component, { name: "test", ...props, items }); const input = returned.getByTestId("input"); const trigger = returned.getByTestId("trigger"); const openBinding = returned.getByTestId("open-binding"); @@ -396,6 +404,28 @@ describe("combobox - single", () => { await user.keyboard(kbd.ARROW_DOWN); expectHighlighted(i1!); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setupSingle({}, [], ComboboxForceMountTest); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setupSingle( + { withOpenCheck: true }, + [], + ComboboxForceMountTest + ); + + expect(queryByTestId("content")).toBeNull(); + + await user.click(trigger); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); //////////////////////////////////// diff --git a/packages/bits-ui/src/tests/context-menu/context-menu-force-mount-test.svelte b/packages/bits-ui/src/tests/context-menu/context-menu-force-mount-test.svelte new file mode 100644 index 000000000..e079a0ca6 --- /dev/null +++ b/packages/bits-ui/src/tests/context-menu/context-menu-force-mount-test.svelte @@ -0,0 +1,213 @@ + + + + +
+
outside
+
+ + + open + + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ + + + Stuff + + + item + + + + + + subtrigger + + + + Email + + + {#snippet children({ checked })} + + {checked} + + sub checkbox + {/snippet} + + + + disabled item + disabled item 2 + + {#snippet children({ checked })} + + {checked} + + Checkbox Item + {/snippet} + + item 2 + + + {#snippet children({ checked })} + + {checked} + + Radio Item 1 + {/snippet} + + + {#snippet children({ checked })} + + {checked} + + Radio Item 2 + {/snippet} + + +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ + + + Stuff + + + item + + + + + + subtrigger + + + + Email + + + {#snippet children({ checked })} + + {checked} + + sub checkbox + {/snippet} + + + + disabled item + disabled item 2 + + {#snippet children({ checked })} + + {checked} + + Checkbox Item + {/snippet} + + item 2 + + + {#snippet children({ checked })} + + {checked} + + Radio Item 1 + {/snippet} + + + {#snippet children({ checked })} + + {checked} + + Radio Item 2 + {/snippet} + + +
+ {/snippet} +
+ {/if} +
+
+
+ + + + + + + +
+
diff --git a/packages/bits-ui/src/tests/context-menu/context-menu.test.ts b/packages/bits-ui/src/tests/context-menu/context-menu.test.ts index 53b001627..0640cd781 100644 --- a/packages/bits-ui/src/tests/context-menu/context-menu.test.ts +++ b/packages/bits-ui/src/tests/context-menu/context-menu.test.ts @@ -1,19 +1,25 @@ import { cleanup, render, screen, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; +import type { Component } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import ContextMenuTest from "./context-menu-test.svelte"; import type { ContextMenuTestProps } from "./context-menu-test.svelte"; +import type { ContextMenuForceMountTestProps } from "./context-menu-force-mount-test.svelte"; +import ContextMenuForceMountTest from "./context-menu-force-mount-test.svelte"; const kbd = getTestKbd(); /** * Helper function to reduce boilerplate in tests */ -function setup(props: ContextMenuTestProps = {}) { +function setup( + props: ContextMenuTestProps | ContextMenuForceMountTestProps = {}, + component: Component = ContextMenuTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(ContextMenuTest, { ...props }); + const returned = render(component, { ...props }); const trigger = returned.getByTestId("trigger"); function getContent() { return returned.queryByTestId("content"); @@ -305,4 +311,25 @@ describe("context menu", () => { expect(wrapper?.parentElement).not.toEqual(document.body); expect(wrapper?.parentElement).toEqual(ogContainer); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({}, ContextMenuForceMountTest); + + expect(getByTestId("content")).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup( + { + withOpenCheck: true, + }, + ContextMenuForceMountTest + ); + expect(queryByTestId("content")).toBeNull(); + + await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/packages/bits-ui/src/tests/date-field/date-field.test.ts b/packages/bits-ui/src/tests/date-field/date-field.test.ts index 98fe3cf0c..fb66e5385 100644 --- a/packages/bits-ui/src/tests/date-field/date-field.test.ts +++ b/packages/bits-ui/src/tests/date-field/date-field.test.ts @@ -310,7 +310,7 @@ describe("date field", () => { it("should marks the field as invalid if the value is invalid", async () => { const { getByTestId, day, month, year, input, label, user } = setup({ granularity: "second", - isDateUnavailable: (date) => date.day === 19, + isDateInvalid: (date) => date.day === 19, value: zonedDateTime, }); diff --git a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu-force-mount-test.svelte b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu-force-mount-test.svelte new file mode 100644 index 000000000..caad8449c --- /dev/null +++ b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu-force-mount-test.svelte @@ -0,0 +1,219 @@ + + + + +
+
outside
+
+ + open + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ + + Stuff + + item + + + + + + subtrigger + + + + Email + + + {#snippet children({ checked })} + + {checked} + + sub checkbox + {/snippet} + + + + disabled item + disabled item 2 + + {#snippet children({ checked })} + + {checked} + + Checkbox Item + {/snippet} + + item 2 + + + {#snippet children({ checked })} + + {checked} + + Radio Item 1 + {/snippet} + + + {#snippet children({ checked })} + + {checked} + + Radio Item 2 + {/snippet} + + +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ + + Stuff + + item + + + + + + subtrigger + + + + Email + + + {#snippet children({ checked })} + + {checked} + + sub checkbox + {/snippet} + + + + disabled item + disabled item 2 + + {#snippet children({ checked })} + + {checked} + + Checkbox Item + {/snippet} + + item 2 + + + {#snippet children({ checked })} + + {checked} + + Radio Item 1 + {/snippet} + + + {#snippet children({ checked })} + + {checked} + + Radio Item 2 + {/snippet} + + +
+ {/snippet} +
+ {/if} +
+
+
+ + + + + + + +
+
diff --git a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts index f18522a8c..7c344af6a 100644 --- a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts +++ b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts @@ -2,10 +2,12 @@ import { render, screen, waitFor } from "@testing-library/svelte/svelte5"; import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; -import { tick } from "svelte"; -import { getTestKbd } from "../utils.js"; +import { type Component, tick } from "svelte"; +import { getTestKbd, setupUserEvents } from "../utils.js"; import DropdownMenuTest from "./dropdown-menu-test.svelte"; import type { DropdownMenuTestProps } from "./dropdown-menu-test.svelte"; +import type { DropdownMenuForceMountTestProps } from "./dropdown-menu-force-mount-test.svelte"; +import DropdownMenuForceMountTest from "./dropdown-menu-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; const kbd = getTestKbd(); @@ -14,10 +16,13 @@ const OPEN_KEYS = [kbd.ENTER, kbd.ARROW_DOWN, kbd.SPACE]; /** * Helper function to reduce boilerplate in tests */ -function setup(props: DropdownMenuTestProps = {}) { - const user = userEvent.setup({ pointerEventsCheck: 0 }); +function setup( + props: DropdownMenuTestProps | DropdownMenuForceMountTestProps = {}, + component: Component = DropdownMenuTest +) { + const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const { getByTestId, queryByTestId } = render(DropdownMenuTest, { ...props }); + const { getByTestId, queryByTestId } = render(component, { ...props }); const trigger = getByTestId("trigger"); return { getByTestId, @@ -339,4 +344,25 @@ describe("dropdown menu", () => { }); await waitFor(() => expect(getByTestId("item")).not.toHaveFocus()); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({}, DropdownMenuForceMountTest); + + expect(getByTestId("content")).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup( + { + withOpenCheck: true, + }, + DropdownMenuForceMountTest + ); + expect(queryByTestId("content")).toBeNull(); + + await user.click(trigger); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/packages/bits-ui/src/tests/link-preview/link-preview-force-mount-test.svelte b/packages/bits-ui/src/tests/link-preview/link-preview-force-mount-test.svelte new file mode 100644 index 000000000..b02a566d7 --- /dev/null +++ b/packages/bits-ui/src/tests/link-preview/link-preview-force-mount-test.svelte @@ -0,0 +1,61 @@ + + + + +
+ + + @sveltejs + + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
Content
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
Content
+ {/snippet} +
+ {/if} +
+
+ +
outside
+
+
diff --git a/packages/bits-ui/src/tests/link-preview/link-preview.test.ts b/packages/bits-ui/src/tests/link-preview/link-preview.test.ts index 8d20c5a8c..b0775fb0a 100644 --- a/packages/bits-ui/src/tests/link-preview/link-preview.test.ts +++ b/packages/bits-ui/src/tests/link-preview/link-preview.test.ts @@ -1,15 +1,21 @@ import { render, waitFor } from "@testing-library/svelte"; import { axe } from "jest-axe"; import { describe, it, vi } from "vitest"; +import type { Component } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import LinkPreviewTest, { type LinkPreviewTestProps } from "./link-preview-test.svelte"; +import type { LinkPreviewForceMountTestProps } from "./link-preview-force-mount-test.svelte"; +import LinkPreviewForceMountTest from "./link-preview-force-mount-test.svelte"; const kbd = getTestKbd(); -function setup(props: LinkPreviewTestProps = {}) { +function setup( + props: LinkPreviewTestProps | LinkPreviewForceMountTestProps = {}, + component: Component = LinkPreviewTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const { getByTestId, queryByTestId } = render(LinkPreviewTest, { ...props }); + const { getByTestId, queryByTestId } = render(component, { ...props }); const trigger = getByTestId("trigger"); return { trigger, getByTestId, queryByTestId, user }; } @@ -126,4 +132,23 @@ describe("link preview", () => { expect(binding).toHaveTextContent("true"); expect(queryByTestId("content")).not.toBeNull(); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({}, LinkPreviewForceMountTest); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup( + { withOpenCheck: true }, + LinkPreviewForceMountTest + ); + expect(queryByTestId("content")).toBeNull(); + await user.hover(trigger); + await waitFor(() => expect(queryByTestId("content")).not.toBeNull()); + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/packages/bits-ui/src/tests/listbox/listbox-force-mount-test.svelte b/packages/bits-ui/src/tests/listbox/listbox-force-mount-test.svelte new file mode 100644 index 000000000..44ffeb79b --- /dev/null +++ b/packages/bits-ui/src/tests/listbox/listbox-force-mount-test.svelte @@ -0,0 +1,120 @@ + + + + +
+ + + {#if selectedLabel} + {selectedLabel} + {:else} + Open combobox + {/if} + + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ + Options + {#each filteredItems as { value, label, disabled }} + + {#snippet children({ selected })} + {#if selected} + x + {/if} + {label} + {/snippet} + + {/each} + +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ + Options + {#each filteredItems as { value, label, disabled }} + + {#snippet children({ selected })} + {#if selected} + x + {/if} + {label} + {/snippet} + + {/each} + +
+ {/snippet} +
+ {/if} +
+
+
+ + +
+
diff --git a/packages/bits-ui/src/tests/listbox/listbox-multi-test.svelte b/packages/bits-ui/src/tests/listbox/listbox-multi-test.svelte index 9874c119b..5024684cb 100644 --- a/packages/bits-ui/src/tests/listbox/listbox-multi-test.svelte +++ b/packages/bits-ui/src/tests/listbox/listbox-multi-test.svelte @@ -43,7 +43,7 @@
- + {#if selectedLabels && selectedLabels.length} {selectedLabels.join(", ")} diff --git a/packages/bits-ui/src/tests/listbox/listbox-test.svelte b/packages/bits-ui/src/tests/listbox/listbox-test.svelte index e9dccd3f3..fe1ed53f0 100644 --- a/packages/bits-ui/src/tests/listbox/listbox-test.svelte +++ b/packages/bits-ui/src/tests/listbox/listbox-test.svelte @@ -33,20 +33,18 @@ const filteredItems = $derived( searchValue === "" ? items - : items.filter((item) => item.label.includes(searchValue.toLowerCase())) + : items.filter((item) => item.label.toLowerCase().includes(searchValue.toLowerCase())) ); - const selectedLabel = $derived(filteredItems.find((item) => item.value === value)?.label); + const selectedLabel = $derived( + value ? items.find((item) => item.value === value)?.label : "Open Listbox" + );
- + - {#if selectedLabel} - {selectedLabel} - {:else} - Open combobox - {/if} + {selectedLabel} diff --git a/packages/bits-ui/src/tests/listbox/listbox.test.ts b/packages/bits-ui/src/tests/listbox/listbox.test.ts index cf2a2d375..355e80881 100644 --- a/packages/bits-ui/src/tests/listbox/listbox.test.ts +++ b/packages/bits-ui/src/tests/listbox/listbox.test.ts @@ -1,12 +1,14 @@ import { render, waitFor } from "@testing-library/svelte"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; -import { tick } from "svelte"; +import { type Component, tick } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import ListboxTest from "./listbox-test.svelte"; import type { Item, ListboxSingleTestProps } from "./listbox-test.svelte"; import type { ListboxMultipleTestProps } from "./listbox-multi-test.svelte"; import ListboxMultiTest from "./listbox-multi-test.svelte"; +import type { ListboxForceMountTestProps } from "./listbox-force-mount-test.svelte"; +import ListboxForceMountTest from "./listbox-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; import type { AnyFn } from "$lib/internal/types.js"; @@ -31,10 +33,15 @@ const testItems: Item[] = [ }, ]; -function setupSingle(props: Partial = {}, items: Item[] = testItems) { +function setupSingle( + props: Partial = {}, + items: Item[] = testItems, + // eslint-disable-next-line ts/no-explicit-any + component: Component = ListboxTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(ListboxTest, { name: "test", ...props, items }); + const returned = render(component, { name: "test", ...props, items }); const trigger = returned.getByTestId("trigger"); const openBinding = returned.getByTestId("open-binding"); const valueBinding = returned.getByTestId("value-binding"); @@ -140,7 +147,7 @@ async function openMultiple( const OPEN_KEYS = [kbd.ARROW_DOWN, kbd.ARROW_UP]; -describe.skip("listbox - single", () => { +describe("listbox - single", () => { it("should have noaccessibility violations", async () => { // @ts-expect-error - testing lib needs to update their generic types const { container } = render(ListboxTest); @@ -322,6 +329,7 @@ describe.skip("listbox - single", () => { await user.keyboard(kbd.ARROW_DOWN); await user.keyboard(kbd.ARROW_DOWN); await user.keyboard(kbd.ENTER); + await sleep(100); expect(getByTestId("trigger")).toHaveTextContent("D"); expect(getHiddenInput()).toHaveValue("4"); await user.click(trigger); @@ -339,7 +347,7 @@ describe.skip("listbox - single", () => { expectNotHighlighted(item1!); }); - it("should start keyboard navigation at the highlighted item even if hovered with mouse", async () => { + it.skip("should start keyboard navigation at the highlighted item even if hovered with mouse", async () => { const { getByTestId, user, trigger } = await openSingle({}, kbd.ARROW_DOWN); const [item1, item2, item3] = getItems(getByTestId); await user.click(trigger); @@ -386,6 +394,28 @@ describe.skip("listbox - single", () => { await user.keyboard(kbd.ARROW_DOWN); expectHighlighted(i1!); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setupSingle({}, [], ListboxForceMountTest); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setupSingle( + { withOpenCheck: true }, + [], + ListboxForceMountTest + ); + + expect(queryByTestId("content")).toBeNull(); + + await user.click(trigger); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); //////////////////////////////////// diff --git a/packages/bits-ui/src/tests/popover/popover-force-mount-test.svelte b/packages/bits-ui/src/tests/popover/popover-force-mount-test.svelte new file mode 100644 index 000000000..bc0d7d85c --- /dev/null +++ b/packages/bits-ui/src/tests/popover/popover-force-mount-test.svelte @@ -0,0 +1,53 @@ + + + + +
+ + trigger + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
+ content + close + +
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
+ content + close + +
+ {/snippet} +
+ {/if} +
+
+ + +
outside
+
+
diff --git a/packages/bits-ui/src/tests/popover/popover.test.ts b/packages/bits-ui/src/tests/popover/popover.test.ts index 0bd40eff8..4f651e205 100644 --- a/packages/bits-ui/src/tests/popover/popover.test.ts +++ b/packages/bits-ui/src/tests/popover/popover.test.ts @@ -1,16 +1,23 @@ import { render, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; +import type { Component } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import PopoverTest, { type PopoverTestProps } from "./popover-test.svelte"; +import PopoverForceMountTest, { + type PopoverForceMountTestProps, +} from "./popover-force-mount-test.svelte"; const kbd = getTestKbd(); -function setup(props: PopoverTestProps = {}) { +function setup( + props: PopoverTestProps | PopoverForceMountTestProps = {}, + component: Component = PopoverTest +) { const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(PopoverTest, { ...props }); + const returned = render(component, { ...props }); const { getByTestId, queryByTestId } = returned; const trigger = getByTestId("trigger"); function getContent() { @@ -161,4 +168,22 @@ describe("popover", () => { expect(binding).toHaveTextContent("true"); expect(getContent()).not.toBeNull(); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({}, PopoverForceMountTest); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup( + { withOpenCheck: true }, + PopoverForceMountTest + ); + expect(queryByTestId("content")).toBeNull(); + await user.click(trigger); + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/packages/bits-ui/src/tests/tooltip/tooltip-force-mount-test.svelte b/packages/bits-ui/src/tests/tooltip/tooltip-force-mount-test.svelte new file mode 100644 index 000000000..35651ba0c --- /dev/null +++ b/packages/bits-ui/src/tests/tooltip/tooltip-force-mount-test.svelte @@ -0,0 +1,58 @@ + + + + +
+ + + @sveltejs + + {#if withOpenCheck} + + {#snippet child({ props, open })} + {#if open} +
Content
+ {/if} + {/snippet} +
+ {:else} + + {#snippet child({ props })} +
Content
+ {/snippet} +
+ {/if} +
+
+
+ +
+
outside
+
+
diff --git a/packages/bits-ui/src/tests/tooltip/tooltip.test.ts b/packages/bits-ui/src/tests/tooltip/tooltip.test.ts index 96d8a1387..98c33af84 100644 --- a/packages/bits-ui/src/tests/tooltip/tooltip.test.ts +++ b/packages/bits-ui/src/tests/tooltip/tooltip.test.ts @@ -1,17 +1,22 @@ import { render, waitFor } from "@testing-library/svelte/svelte5"; -import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; -import { getTestKbd } from "../utils.js"; +import type { Component } from "svelte"; +import { getTestKbd, setupUserEvents } from "../utils.js"; import TooltipTest, { type TooltipTestProps } from "./tooltip-test.svelte"; +import type { TooltipForceMountTestProps } from "./tooltip-force-mount-test.svelte"; +import TooltipForceMountTest from "./tooltip-force-mount-test.svelte"; import { sleep } from "$lib/internal/sleep.js"; const kbd = getTestKbd(); -function setup(props: Partial = {}) { - const user = userEvent.setup({ pointerEventsCheck: 0 }); +function setup( + props: Partial = {}, + component: Component = TooltipTest +) { + const user = setupUserEvents(); // @ts-expect-error - testing lib needs to update their generic types - const returned = render(TooltipTest, { ...props }); + const returned = render(component, { ...props }); const trigger = returned.getByTestId("trigger"); return { ...returned, trigger, user }; } @@ -19,7 +24,7 @@ function setup(props: Partial = {}) { async function open(props: Partial = {}) { const returned = setup(props); expect(returned.queryByTestId("content")).toBeNull(); - returned.user.hover(returned.trigger); + await returned.user.hover(returned.trigger); await waitFor(() => expect(returned.queryByTestId("content")).not.toBeNull()); const content = returned.getByTestId("content"); return { ...returned, content }; @@ -42,7 +47,7 @@ describe("tooltip", () => { }); it("should use provider delay duration if provided and the tooltip.root did not provide one", async () => { - const { user, trigger } = setup(); + const { trigger } = setup(); expect(trigger).toHaveAttribute("data-delay-duration", "0"); }); @@ -133,4 +138,23 @@ describe("tooltip", () => { await waitFor(() => expect(binding).toHaveTextContent("true")); expect(queryByTestId("content")).not.toBeNull(); }); + + it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({}, TooltipForceMountTest); + + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); + + it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup( + { withOpenCheck: true }, + TooltipForceMountTest + ); + expect(queryByTestId("content")).toBeNull(); + await user.hover(trigger); + await waitFor(() => expect(queryByTestId("content")).not.toBeNull()); + const content = getByTestId("content"); + expect(content).toBeVisible(); + }); }); diff --git a/sites/docs/content/components/accordion.md b/sites/docs/content/components/accordion.md index f63d7c397..0758fcd56 100644 --- a/sites/docs/content/components/accordion.md +++ b/sites/docs/content/components/accordion.md @@ -227,7 +227,7 @@ To implement controlled state: let myValue = $state(""); - (myValue = v)}> + (myValue = v)}> ``` diff --git a/sites/docs/content/components/aspect-ratio.md b/sites/docs/content/components/aspect-ratio.md index ae3c5c115..4ee5f060f 100644 --- a/sites/docs/content/components/aspect-ratio.md +++ b/sites/docs/content/components/aspect-ratio.md @@ -8,7 +8,7 @@ description: Displays content while maintaining a specified aspect ratio, ensuri export let schemas; - + {#snippet preview()} diff --git a/sites/docs/content/components/button.md b/sites/docs/content/components/button.md index d2c6e8d67..03b883ac6 100644 --- a/sites/docs/content/components/button.md +++ b/sites/docs/content/components/button.md @@ -8,7 +8,7 @@ description: A component that if passed a `href` prop will render an anchor elem export let schemas; - + {#snippet preview()} diff --git a/sites/docs/content/components/date-range-field.md b/sites/docs/content/components/date-range-field.md index 1cc75d4fb..02656bfc5 100644 --- a/sites/docs/content/components/date-range-field.md +++ b/sites/docs/content/components/date-range-field.md @@ -106,7 +106,6 @@ To implement controlled state: 1. Set the `controlledPlaceholder` prop to `true` on the `DateRangeField.Root` component. 2. Provide a `placeholder` prop to `DateRangeField.Root`, which should be a variable holding the current state. 3. Implement an `onPlaceholderChange` handler to update the state when the internal state changes. -4. ```svelte @@ -31,61 +31,74 @@ description: Organizes content into distinct sections, allowing users to switch ``` -## Value State +## Managing Value State -The `value` represents the currently selected tab within the `Tabs` component. +Bits UI offers several approaches to manage and synchronize the component's value state, catering to different levels of control and integration needs. -Bits UI provides flexible options for controlling and synchronizing the `Tabs` component's value state. +### 1. Two-Way Binding -### Two-Way Binding +For seamless state synchronization, use Svelte's `bind:value` directive. This method automatically keeps your local state in sync with the component's internal state. -Use the `bind:value` directive for effortless two-way synchronization between your local state and the `Tabs` component's value. - -```svelte {3,6,8} +```svelte - - - + + + + ``` -This setup enables changing the `Tabs` component's value via the custom button and ensures the local `value` state is synchronized with the `Tabs` component's value. +#### Key Benefits + +- Simplifies state management +- Automatically updates `myValue` when the internal state changes (e.g., via clicking on an tab's trigger) +- Allows external control (e.g., switching tabs via a separate button) -### Change Handler +### 2. Change Handler -You can also use the `onValueChange` prop to update local state when the `Tabs` component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `Tabs` component's value changes. +For more granular control or to perform additional logic on state changes, use the `onValueChange` prop. This approach is useful when you need to execute custom logic alongside state updates. -```svelte {3,7-11} +```svelte { - value = v; + myValue = v; + // additional logic here. }} > ``` -### Controlled +#### Use Cases -Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. +- Implementing custom behaviors on value change +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) -You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `Tabs.Root` component. +### 3. Fully Controlled + +For complete control over the component's value state, use the `controlledValue` prop. This approach requires you to manually manage the value state, giving you full control over when and how the component responds to value change events. + +To implement controlled state: + +1. Set the `controlledValue` prop to `true` on the `Tabs.Root` component. +2. Provide a `value` prop to `Tabs.Root`, which should be a variable holding the current state. +3. Implement an `onValueChange` handler to update the state when the internal state changes. ```svelte (myValue = v)}> @@ -93,7 +106,19 @@ You will then be responsible for updating a local value state variable that is p ``` -See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. +#### When to Use + +- Implementing complex logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + ## Orientation @@ -111,4 +136,16 @@ When the `orientation` is set to `'horizontal'`, the `ArrowLeft` and `ArrowRight ``` +## Activation Mode + +By default, the `Tabs` component will automatically activate the tab associated with a trigger when that trigger is focused. This behavior can be disabled by setting the `activationMode` prop to `'manual'`. + +When set to `'manual'`, the user will need to activate the tab by pressing the trigger. + +```svelte /activationMode="manual"/ + + + +``` + diff --git a/sites/docs/content/components/toggle-group.md b/sites/docs/content/components/toggle-group.md index f135c937a..971b68bbc 100644 --- a/sites/docs/content/components/toggle-group.md +++ b/sites/docs/content/components/toggle-group.md @@ -4,7 +4,7 @@ description: Groups multiple toggle controls, allowing users to enable one or mu --- @@ -35,43 +35,48 @@ The `ToggleGroup` component supports two `type` props, `'single'` and `'multiple When the `type` is set to `'multiple'`, the `ToggleGroup` will allow multiple items to be selected at a time, and the type of the `value` prop will be an array of strings. -## Value State +## Managing Value State -The `value` prop is used to determine which item(s) are currently "on". Bits UI provides flexible options for controlling and synchronizing the value. +Bits UI offers several approaches to manage and synchronize the component's value state, catering to different levels of control and integration needs. -### Two-Way Binding +### 1. Two-Way Binding -Use the `bind:value` directive for effortless two-way synchronization between your local state and the Toggle Group's internal state. +For seamless state synchronization, use Svelte's `bind:value` directive. This method automatically keeps your local state in sync with the component's internal state. -```svelte /bind:value={myValue}/ +```svelte - + - - + + ``` -This setup enables toggling the Toggle Group's value to "apple" via the custom button and ensures the local `myValue` state updates when the state changes through any internal means (e.g., clicking on an item). +#### Key Benefits -### Change Handler +- Simplifies state management +- Automatically updates `myValue` when the internal state changes (e.g., via clicking on an item) +- Allows external control (e.g., toggling an item via a separate button) -You can also use the `onValueChange` prop to update local state when the Toggle Group's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Toggle Group changes. +### 2. Change Handler -```svelte /onValueChange/ +For more granular control or to perform additional logic on state changes, use the `onValueChange` prop. This approach is useful when you need to execute custom logic alongside state updates. + +```svelte { - myValue = value; + onValueChange={(v) => { + myValue = v; // additional logic here. }} > @@ -79,24 +84,50 @@ You can also use the `onValueChange` prop to update local state when the Toggle ``` -### Controlled +#### Use Cases -Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. +- Implementing custom behaviors on value change +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) -You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `ToggleGroup.Root` component. +### 3. Fully Controlled + +For complete control over the component's value state, use the `controlledValue` prop. This approach requires you to manually manage the value state, giving you full control over when and how the component responds to value change events. + +To implement controlled state: + +1. Set the `controlledValue` prop to `true` on the `ToggleGroup.Root` component. +2. Provide a `value` prop to `ToggleGroup.Root`, which should be a variable holding the current state. +3. Implement an `onValueChange` handler to update the state when the internal state changes. ```svelte - (myValue = v)}> + (myValue = v)} +> ``` -See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. +#### When to Use + +- Implementing complex logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + diff --git a/sites/docs/content/components/toolbar.md b/sites/docs/content/components/toolbar.md index b55fd08e4..fce7af7cc 100644 --- a/sites/docs/content/components/toolbar.md +++ b/sites/docs/content/components/toolbar.md @@ -4,7 +4,7 @@ description: Displays frequently used actions or tools in a compact, easily acce --- @@ -32,4 +32,108 @@ description: Displays frequently used actions or tools in a compact, easily acce ``` +## Managing Value State + +Bits UI offers several approaches to manage and synchronize the component's value state, catering to different levels of control and integration needs. + +### 1. Two-Way Binding + +For seamless state synchronization, use Svelte's `bind:value` directive. This method automatically keeps your local state in sync with the component's internal state. + +```svelte + + + + + + + + + +``` + +#### Key Benefits + +- Simplifies state management +- Automatically updates `myValue` when the internal state changes (e.g., via clicking on an item) +- Allows external control (e.g., toggling an item via a separate button) + +### 2. Change Handler + +For more granular control or to perform additional logic on state changes, use the `onValueChange` prop. This approach is useful when you need to execute custom logic alongside state updates. + +```svelte + + + + { + myValue = v; + // additional logic here. + }} + > + + + +``` + +#### Use Cases + +- Implementing custom behaviors on value change +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) + +### 3. Fully Controlled + +For complete control over the component's value state, use the `controlledValue` prop. This approach requires you to manually manage the value state, giving you full control over when and how the component responds to value change events. + +To implement controlled state: + +1. Set the `controlledValue` prop to `true` on the `Toolbar.Group` component. +2. Provide a `value` prop to `Toolbar.Group`, which should be a variable holding the current state. +3. Implement an `onValueChange` handler to update the state when the internal state changes. + +```svelte + + + + { + myValue = v; + // additional logic here. + }} + > + + + +``` + +#### When to Use + +- Implementing complex logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + + diff --git a/sites/docs/src/lib/components/component-preview-v2.svelte b/sites/docs/src/lib/components/component-preview-v2.svelte index 50a9a596d..2db7b9371 100644 --- a/sites/docs/src/lib/components/component-preview-v2.svelte +++ b/sites/docs/src/lib/components/component-preview-v2.svelte @@ -10,14 +10,23 @@ class?: string; containerClass?: string; size?: "xs" | "sm" | "default" | "lg"; + nonExpandableItems?: string[]; }; - let { preview, children, fileName, class: className, containerClass, size }: Props = $props(); + let { + preview, + children, + fileName, + class: className, + containerClass, + size, + nonExpandableItems = [], + }: Props = $props(); {@render preview()} - + {@render children()} diff --git a/sites/docs/src/lib/components/demo-code-container.svelte b/sites/docs/src/lib/components/demo-code-container.svelte index 670577d7d..836723ccb 100644 --- a/sites/docs/src/lib/components/demo-code-container.svelte +++ b/sites/docs/src/lib/components/demo-code-container.svelte @@ -10,8 +10,14 @@ fileName?: string; children: Snippet; class?: string; + nonExpandableItems?: string[]; }; - let { children, fileName = "App.svelte", class: className }: Props = $props(); + let { + children, + fileName = "App.svelte", + class: className, + nonExpandableItems = [], + }: Props = $props(); const items = $derived([ { @@ -29,9 +35,20 @@ ]); let open = $state(false); + let activeValue = $state(fileName); + + const expandable = $derived(!nonExpandableItems.includes(activeValue)); - + { + activeValue = v; + }} + bind:open + {expandable} +> {#each items as item (item.value)} void; items: { value: string; label: string }[]; children: Snippet; open: boolean; + expandable?: boolean; }; - let { value = $bindable(), open = $bindable(), items, children }: Props = $props(); + let { + value = $bindable(), + open = $bindable(), + onValueChange = noop, + items, + expandable = true, + children, + }: Props = $props(); - +
{#each items as item} @@ -29,15 +39,17 @@ {/each}
- + {#if expandable} + + {/if}