From 1f02d0aeb39e339ba90cc14cffa89083853a31ac Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:35:32 -0400 Subject: [PATCH] next: Controlled State (#676) --- .../accordion/components/accordion.svelte | 12 +++-- .../bits-ui/src/lib/bits/accordion/types.ts | 10 ++++ .../lib/bits/button/components/button.svelte | 5 +- packages/bits-ui/src/lib/bits/button/index.ts | 3 +- packages/bits-ui/src/lib/bits/button/types.ts | 16 +++--- .../bits/calendar/components/calendar.svelte | 14 ++++-- .../bits-ui/src/lib/bits/calendar/types.ts | 20 ++++++++ .../bits/checkbox/components/checkbox.svelte | 7 ++- .../bits-ui/src/lib/bits/checkbox/types.ts | 10 ++++ .../collapsible/components/collapsible.svelte | 12 +++-- .../bits-ui/src/lib/bits/collapsible/types.ts | 9 ++++ .../bits/combobox/components/combobox.svelte | 27 ++++++++-- .../bits/command/components/command.svelte | 5 +- .../bits-ui/src/lib/bits/command/types.ts | 11 +++- .../date-field/components/date-field.svelte | 18 +++++-- .../bits-ui/src/lib/bits/date-field/types.ts | 25 ++++++++-- .../date-picker/components/date-picker.svelte | 29 +++++++++-- .../bits-ui/src/lib/bits/date-picker/types.ts | 29 +++++++++++ .../components/date-range-field.svelte | 25 ++++++++-- .../src/lib/bits/date-range-field/types.ts | 19 +++++++ .../components/date-range-picker.svelte | 38 +++++++++++--- .../src/lib/bits/date-range-picker/types.ts | 29 +++++++++++ .../lib/bits/dialog/components/dialog.svelte | 14 ++++-- packages/bits-ui/src/lib/bits/dialog/types.ts | 9 ++++ .../components/link-preview.svelte | 5 +- .../src/lib/bits/link-preview/types.ts | 9 ++++ .../bits/listbox/components/listbox.svelte | 27 ++++++++-- .../bits-ui/src/lib/bits/listbox/types.ts | 18 +++++++ .../menu/components/menu-checkbox-item.svelte | 5 +- .../menu/components/menu-radio-group.svelte | 5 +- .../lib/bits/menu/components/menu-sub.svelte | 12 ++++- .../src/lib/bits/menu/components/menu.svelte | 23 +++++++-- packages/bits-ui/src/lib/bits/menu/types.ts | 37 ++++++++++++++ .../bits/menubar/components/menubar.svelte | 8 ++- .../bits-ui/src/lib/bits/menubar/types.ts | 9 ++++ .../pagination/components/pagination.svelte | 12 +++-- .../bits-ui/src/lib/bits/pagination/types.ts | 11 +++- .../pin-input/components/pin-input.svelte | 5 +- .../bits-ui/src/lib/bits/pin-input/types.ts | 10 ++++ .../bits/popover/components/popover.svelte | 14 ++++-- .../bits-ui/src/lib/bits/popover/types.ts | 9 ++++ .../radio-group/components/radio-group.svelte | 12 +++-- .../bits-ui/src/lib/bits/radio-group/types.ts | 9 ++++ .../components/range-calendar.svelte | 50 +++++++++++-------- .../src/lib/bits/range-calendar/types.ts | 27 ++++++++-- ...bel.svelte => select-group-heading.svelte} | 0 .../lib/bits/select/components/select.svelte | 15 ++++-- packages/bits-ui/src/lib/bits/select/index.ts | 2 +- packages/bits-ui/src/lib/bits/select/types.ts | 18 +++++++ .../lib/bits/slider/components/slider.svelte | 5 +- packages/bits-ui/src/lib/bits/slider/types.ts | 12 ++++- .../lib/bits/switch/components/switch.svelte | 12 +++-- packages/bits-ui/src/lib/bits/switch/types.ts | 10 ++++ .../src/lib/bits/tabs/components/tabs.svelte | 16 +++--- packages/bits-ui/src/lib/bits/tabs/types.ts | 9 ++++ .../components/toggle-group.svelte | 27 +++++++--- .../src/lib/bits/toggle-group/types.ts | 9 ++++ .../lib/bits/toggle/components/toggle.svelte | 14 ++++-- packages/bits-ui/src/lib/bits/toggle/types.ts | 10 ++++ .../toolbar/components/toolbar-group.svelte | 27 +++++++--- .../bits/toolbar/components/toolbar.svelte | 4 +- .../bits/tooltip/components/tooltip.svelte | 10 ++-- .../bits-ui/src/lib/bits/tooltip/types.ts | 9 ++++ .../{arrays.spec.ts => arrays.test.ts} | 0 .../{callbacks.spec.ts => callbacks.test.ts} | 0 .../internal/{clamp.spec.ts => clamp.test.ts} | 0 ...ndlers.spec.ts => composeHandlers.test.ts} | 0 ...StyleObj.spec.ts => cssToStyleObj.test.ts} | 0 .../{debounce.spec.ts => debounce.test.ts} | 0 ...eys.spec.ts => getDirectionalKeys.test.ts} | 0 .../lib/internal/{is.spec.ts => is.test.ts} | 0 .../internal/{math.spec.ts => math.test.ts} | 0 ...{mergeProps.spec.ts => mergeProps.test.ts} | 0 .../{polygon.spec.ts => polygon.test.ts} | 0 .../{accordion.spec.ts => accordion.test.ts} | 0 ...rt-dialog.spec.ts => alert-dialog.test.ts} | 0 .../avatar/{avatar.spec.ts => avatar.test.ts} | 0 .../{calendar.spec.ts => calendar.test.ts} | 0 .../{checkbox.spec.ts => checkbox.test.ts} | 0 ...ollapsible.spec.ts => collapsible.test.ts} | 0 .../{combobox.spec.ts => combobox.test.ts} | 0 ...text-menu.spec.ts => context-menu.test.ts} | 0 ...{date-field.spec.ts => date-field.test.ts} | 0 ...field.spec.ts => date-range-field.test.ts} | 0 .../dialog/{dialog.spec.ts => dialog.test.ts} | 0 ...own-menu.spec.ts => dropdown-menu.test.ts} | 0 .../label/{label.spec.ts => label.test.ts} | 0 ...k-preview.spec.ts => link-preview.test.ts} | 0 .../{listbox.spec.ts => listbox.test.ts} | 0 .../{menubar.spec.ts => menubar.test.ts} | 0 ...{pagination.spec.ts => pagination.test.ts} | 0 .../{pin-input.spec.ts => pin-input.test.ts} | 0 .../{popover.spec.ts => popover.test.ts} | 0 .../{progress.spec.ts => progress.test.ts} | 0 ...adio-group.spec.ts => radio-group.test.ts} | 0 ...alendar.spec.ts => range-calendar.test.ts} | 0 ...croll-area.spec.ts => scroll-area.test.ts} | 0 .../select/{select.spec.ts => select.test.ts} | 0 .../{separator.spec.ts => separator.test.ts} | 0 .../slider/{slider.spec.ts => slider.test.ts} | 0 .../switch/{switch.spec.ts => switch.test.ts} | 0 .../tests/tabs/{tabs.spec.ts => tabs.test.ts} | 0 ...gle-group.spec.ts => toggle-group.test.ts} | 0 .../toggle/{toggle.spec.ts => toggle.test.ts} | 0 .../{toolbar.spec.ts => toolbar.test.ts} | 0 .../{tooltip.spec.ts => tooltip.test.ts} | 0 sites/docs/content/components/accordion.md | 20 ++++++++ sites/docs/content/components/alert-dialog.md | 20 ++++++++ sites/docs/content/components/collapsible.md | 18 +++++++ sites/docs/content/components/context-menu.md | 20 +++++++- sites/docs/content/components/dialog.md | 18 +++++++ .../docs/content/components/dropdown-menu.md | 20 +++++++- sites/docs/content/controlled-state.md | 38 ++++++++++++++ .../components/api-ref/css-vars-table.svelte | 2 +- .../api-ref/data-attr-value-content.svelte | 2 +- .../api-ref/data-attrs-table.svelte | 2 +- .../lib/components/api-ref/prop-copy.svelte | 2 +- .../api-ref/prop-type-content.svelte | 2 +- .../lib/components/api-ref/props-table.svelte | 2 +- .../src/lib/components/api-section.svelte | 4 +- .../navigation/sidebar-nav-main-items.svelte | 29 +---------- .../components/navigation/sidebar-nav.svelte | 3 +- .../src/lib/components/site-header.svelte | 2 +- sites/docs/src/lib/config/navigation.ts | 23 +++++++++ .../lib/content/api-reference/accordion.ts | 2 + .../lib/content/api-reference/alert-dialog.ts | 2 + .../src/lib/content/api-reference/button.ts | 5 +- .../src/lib/content/api-reference/calendar.ts | 4 ++ .../src/lib/content/api-reference/checkbox.ts | 2 + .../lib/content/api-reference/collapsible.ts | 11 ++-- .../src/lib/content/api-reference/combobox.ts | 8 ++- .../src/lib/content/api-reference/command.ts | 2 + .../lib/content/api-reference/date-field.ts | 5 +- .../lib/content/api-reference/date-picker.ts | 11 ++-- .../content/api-reference/date-range-field.ts | 4 ++ .../api-reference/date-range-picker.ts | 7 ++- .../src/lib/content/api-reference/dialog.ts | 2 + .../src/lib/content/api-reference/helpers.ts | 36 +++++++++++++ .../lib/content/api-reference/link-preview.ts | 2 + .../src/lib/content/api-reference/listbox.ts | 10 ++-- .../src/lib/content/api-reference/menu.ts | 9 +++- .../src/lib/content/api-reference/menubar.ts | 2 + .../lib/content/api-reference/pagination.ts | 21 ++++---- .../lib/content/api-reference/pin-input.ts | 2 + .../src/lib/content/api-reference/popover.ts | 5 +- .../lib/content/api-reference/radio-group.ts | 2 + .../content/api-reference/range-calendar.ts | 2 + .../src/lib/content/api-reference/select.ts | 4 ++ .../src/lib/content/api-reference/slider.ts | 2 + .../src/lib/content/api-reference/switch.ts | 2 + .../src/lib/content/api-reference/tabs.ts | 2 + .../lib/content/api-reference/toggle-group.ts | 2 + .../src/lib/content/api-reference/toggle.ts | 2 + .../src/lib/content/api-reference/toolbar.ts | 2 + .../src/lib/content/api-reference/tooltip.ts | 2 + sites/docs/src/routes/(main)/+layout.svelte | 12 ++--- sites/docs/src/routes/(main)/+page.svelte | 4 +- .../routes/(main)/docs/[...slug]/+page.svelte | 4 +- .../docs/components/[name]/+page.svelte | 4 +- 159 files changed, 1123 insertions(+), 224 deletions(-) rename packages/bits-ui/src/lib/bits/select/components/{select-group-label.svelte => select-group-heading.svelte} (100%) rename packages/bits-ui/src/lib/internal/{arrays.spec.ts => arrays.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{callbacks.spec.ts => callbacks.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{clamp.spec.ts => clamp.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{composeHandlers.spec.ts => composeHandlers.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{cssToStyleObj.spec.ts => cssToStyleObj.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{debounce.spec.ts => debounce.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{getDirectionalKeys.spec.ts => getDirectionalKeys.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{is.spec.ts => is.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{math.spec.ts => math.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{mergeProps.spec.ts => mergeProps.test.ts} (100%) rename packages/bits-ui/src/lib/internal/{polygon.spec.ts => polygon.test.ts} (100%) rename packages/bits-ui/src/tests/accordion/{accordion.spec.ts => accordion.test.ts} (100%) rename packages/bits-ui/src/tests/alert-dialog/{alert-dialog.spec.ts => alert-dialog.test.ts} (100%) rename packages/bits-ui/src/tests/avatar/{avatar.spec.ts => avatar.test.ts} (100%) rename packages/bits-ui/src/tests/calendar/{calendar.spec.ts => calendar.test.ts} (100%) rename packages/bits-ui/src/tests/checkbox/{checkbox.spec.ts => checkbox.test.ts} (100%) rename packages/bits-ui/src/tests/collapsible/{collapsible.spec.ts => collapsible.test.ts} (100%) rename packages/bits-ui/src/tests/combobox/{combobox.spec.ts => combobox.test.ts} (100%) rename packages/bits-ui/src/tests/context-menu/{context-menu.spec.ts => context-menu.test.ts} (100%) rename packages/bits-ui/src/tests/date-field/{date-field.spec.ts => date-field.test.ts} (100%) rename packages/bits-ui/src/tests/date-range-field/{date-range-field.spec.ts => date-range-field.test.ts} (100%) rename packages/bits-ui/src/tests/dialog/{dialog.spec.ts => dialog.test.ts} (100%) rename packages/bits-ui/src/tests/dropdown-menu/{dropdown-menu.spec.ts => dropdown-menu.test.ts} (100%) rename packages/bits-ui/src/tests/label/{label.spec.ts => label.test.ts} (100%) rename packages/bits-ui/src/tests/link-preview/{link-preview.spec.ts => link-preview.test.ts} (100%) rename packages/bits-ui/src/tests/listbox/{listbox.spec.ts => listbox.test.ts} (100%) rename packages/bits-ui/src/tests/menubar/{menubar.spec.ts => menubar.test.ts} (100%) rename packages/bits-ui/src/tests/pagination/{pagination.spec.ts => pagination.test.ts} (100%) rename packages/bits-ui/src/tests/pin-input/{pin-input.spec.ts => pin-input.test.ts} (100%) rename packages/bits-ui/src/tests/popover/{popover.spec.ts => popover.test.ts} (100%) rename packages/bits-ui/src/tests/progress/{progress.spec.ts => progress.test.ts} (100%) rename packages/bits-ui/src/tests/radio-group/{radio-group.spec.ts => radio-group.test.ts} (100%) rename packages/bits-ui/src/tests/range-calendar/{range-calendar.spec.ts => range-calendar.test.ts} (100%) rename packages/bits-ui/src/tests/scroll-area/{scroll-area.spec.ts => scroll-area.test.ts} (100%) rename packages/bits-ui/src/tests/select/{select.spec.ts => select.test.ts} (100%) rename packages/bits-ui/src/tests/separator/{separator.spec.ts => separator.test.ts} (100%) rename packages/bits-ui/src/tests/slider/{slider.spec.ts => slider.test.ts} (100%) rename packages/bits-ui/src/tests/switch/{switch.spec.ts => switch.test.ts} (100%) rename packages/bits-ui/src/tests/tabs/{tabs.spec.ts => tabs.test.ts} (100%) rename packages/bits-ui/src/tests/toggle-group/{toggle-group.spec.ts => toggle-group.test.ts} (100%) rename packages/bits-ui/src/tests/toggle/{toggle.spec.ts => toggle.test.ts} (100%) rename packages/bits-ui/src/tests/toolbar/{toolbar.spec.ts => toolbar.test.ts} (100%) rename packages/bits-ui/src/tests/tooltip/{tooltip.spec.ts => tooltip.test.ts} (100%) create mode 100644 sites/docs/content/controlled-state.md diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte index d1e951d8b..feb32eaa2 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte @@ -4,6 +4,7 @@ import type { RootProps } from "../index.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { disabled = false, @@ -13,9 +14,10 @@ value = $bindable(), ref = $bindable(null), id = useId(), - onValueChange, + onValueChange = noop, loop = true, orientation = "vertical", + controlledValue = false, ...restProps }: RootProps = $props(); @@ -26,8 +28,12 @@ value: box.with( () => value!, (v) => { - value = v; - onValueChange?.(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange(v as any); + } } ) as WritableBox | WritableBox, id: box.with(() => id), diff --git a/packages/bits-ui/src/lib/bits/accordion/types.ts b/packages/bits-ui/src/lib/bits/accordion/types.ts index f7e0db500..7457fd117 100644 --- a/packages/bits-ui/src/lib/bits/accordion/types.ts +++ b/packages/bits-ui/src/lib/bits/accordion/types.ts @@ -24,6 +24,16 @@ export type BaseAccordionRootPropsWithoutHTML = { * @defaultValue "vertical" */ orientation?: Orientation; + + /** + * Whether the value of the accordion is controlled or not. + * If `true`, the accordion will not update the value internally, instead + * it will call `onValueChange` when it would have otherwise, and it is up to you + * to update the `value` prop. + * + * @defaultValue false + */ + controlledValue?: boolean; }; export type AccordionRootSinglePropsWithoutHTML = BaseAccordionRootPropsWithoutHTML & { diff --git a/packages/bits-ui/src/lib/bits/button/components/button.svelte b/packages/bits-ui/src/lib/bits/button/components/button.svelte index 595fbf4bc..4c1136c40 100644 --- a/packages/bits-ui/src/lib/bits/button/components/button.svelte +++ b/packages/bits-ui/src/lib/bits/button/components/button.svelte @@ -1,7 +1,7 @@ {@render children?.()} diff --git a/packages/bits-ui/src/lib/bits/button/index.ts b/packages/bits-ui/src/lib/bits/button/index.ts index 05ab915fb..481d8595e 100644 --- a/packages/bits-ui/src/lib/bits/button/index.ts +++ b/packages/bits-ui/src/lib/bits/button/index.ts @@ -1,3 +1,2 @@ export { default as Root } from "./components/button.svelte"; - -export type { ButtonProps as Props } from "./types.js"; +export type { ButtonProps as RootProps } from "./types.js"; diff --git a/packages/bits-ui/src/lib/bits/button/types.ts b/packages/bits-ui/src/lib/bits/button/types.ts index df8dd782b..8c499d5d8 100644 --- a/packages/bits-ui/src/lib/bits/button/types.ts +++ b/packages/bits-ui/src/lib/bits/button/types.ts @@ -1,23 +1,21 @@ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements"; +import type { WithoutChildren } from "svelte-toolbelt"; +import type { WithChildren } from "$lib/shared/index.js"; -export type ButtonPropsWithoutHTML = { +export type ButtonPropsWithoutHTML = WithChildren<{ ref?: HTMLElement | null; -}; +}>; type AnchorElement = ButtonPropsWithoutHTML & - HTMLAnchorAttributes & { - href?: HTMLAnchorAttributes["href"]; + WithoutChildren> & { + href: HTMLAnchorAttributes["href"]; type?: never; }; type ButtonElement = ButtonPropsWithoutHTML & - HTMLButtonAttributes & { + WithoutChildren> & { type?: HTMLButtonAttributes["type"]; href?: never; }; export type ButtonProps = AnchorElement | ButtonElement; - -export type ButtonEventHandler = T & { - currentTarget: EventTarget & HTMLButtonElement; -}; diff --git a/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte b/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte index 227486743..a3ddfb789 100644 --- a/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte +++ b/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte @@ -34,6 +34,8 @@ type, disableDaysOutsideMonth = true, initialFocus = false, + controlledValue = false, + controlledPlaceholder = false, ...restProps }: RootProps = $props(); @@ -70,7 +72,9 @@ placeholder: box.with( () => placeholder as DateValue, (v) => { - if (placeholder !== v) { + if (controlledPlaceholder) { + onPlaceholderChange(v as DateValue); + } else { placeholder = v; onPlaceholderChange(v as DateValue); } @@ -80,8 +84,12 @@ value: box.with( () => value, (v) => { - value = v; - onValueChange(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange(v as any); + } } ), type: box.with(() => type), diff --git a/packages/bits-ui/src/lib/bits/calendar/types.ts b/packages/bits-ui/src/lib/bits/calendar/types.ts index c0023da0c..b0fe39e1e 100644 --- a/packages/bits-ui/src/lib/bits/calendar/types.ts +++ b/packages/bits-ui/src/lib/bits/calendar/types.ts @@ -188,6 +188,26 @@ type CalendarBaseRootPropsWithoutHTML = { * @defaultValue false */ disableDaysOutsideMonth?: boolean; + + /** + * Whether or not the calendar is controlled or not. If `true`, the calendar will not update + * the value internally, instead it will call `onValueChange` when it would have otherwise, + * and it is up to you to update the `value` prop that is passed to the `Calendar.Root` + * component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the calendar placeholder is controlled or not. If `true`, the calendar will + * not update the placeholder internally, instead it will call `onPlaceholderChange` when it + * would have otherwise, and it is up to you to update the `placeholder` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; }; export type CalendarSingleRootPropsWithoutHTML = { diff --git a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte index 99098081e..afcdc3c5b 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte +++ b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte @@ -8,6 +8,7 @@ let { checked = $bindable(false), + ref = $bindable(null), onCheckedChange, children, disabled = false, @@ -15,7 +16,7 @@ name = undefined, value = "on", id = useId(), - ref = $bindable(null), + controlledChecked = false, child, ...restProps }: RootProps = $props(); @@ -24,7 +25,9 @@ checked: box.with( () => checked, (v) => { - if (checked !== v) { + if (controlledChecked) { + onCheckedChange?.(v); + } else { checked = v; onCheckedChange?.(v); } diff --git a/packages/bits-ui/src/lib/bits/checkbox/types.ts b/packages/bits-ui/src/lib/bits/checkbox/types.ts index 4fd8a250b..df8540520 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/types.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/types.ts @@ -49,6 +49,16 @@ export type CheckboxRootPropsWithoutHTML = WithChild< * A callback function called when the checked state changes. */ onCheckedChange?: OnChangeFn; + + /** + * Whether or not the checkbox is controlled or not. If `true`, the checkbox will not update + * the checked state internally, instead it will call `onCheckedChange` when it would have + * otherwise, and it is up to you to update the `checked` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledChecked?: boolean; }, CheckboxRootSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte index 26bbb74da..444f22583 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible.svelte @@ -4,6 +4,7 @@ import { useCollapsibleRoot } from "../collapsible.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { children, @@ -12,7 +13,8 @@ ref = $bindable(null), open = $bindable(false), disabled = false, - onOpenChange, + controlledOpen = false, + onOpenChange = noop, ...restProps }: RootProps = $props(); @@ -20,8 +22,12 @@ open: box.with( () => open, (v) => { - open = v; - onOpenChange?.(v); + if (controlledOpen) { + onOpenChange(v); + } else { + open = v; + onOpenChange(v); + } } ), disabled: box.with(() => disabled), diff --git a/packages/bits-ui/src/lib/bits/collapsible/types.ts b/packages/bits-ui/src/lib/bits/collapsible/types.ts index 7b08788ff..0037d87d9 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/types.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/types.ts @@ -20,6 +20,15 @@ export type CollapsibleRootPropsWithoutHTML = WithChild<{ * A callback function called when the open state changes. */ onOpenChange?: OnChangeFn; + + /** + * Whether or not the collapsible is controlled or not. If `true`, the collapsible will not + * update the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type CollapsibleRootProps = CollapsibleRootPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte b/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte index 711b3116c..3e48c3446 100644 --- a/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte +++ b/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte @@ -17,18 +17,31 @@ loop = false, scrollAlignment = "nearest", required = false, + controlledOpen = false, + controlledValue = false, children, }: RootProps = $props(); - value === undefined && (value = type === "single" ? "" : []); + if (value === undefined) { + const defaultValue = type === "single" ? "" : []; + if (controlledValue) { + onValueChange(defaultValue as any); + } else { + value = defaultValue; + } + } useListboxRoot({ type, value: box.with( () => value!, (v) => { - value = v; - onValueChange(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange(v as any); + } } ) as WritableBox | WritableBox, disabled: box.with(() => disabled), @@ -36,8 +49,12 @@ open: box.with( () => open, (v) => { - open = v; - onOpenChange(v); + if (controlledOpen) { + onOpenChange(v); + } else { + open = v; + onOpenChange(v); + } } ), loop: box.with(() => loop), diff --git a/packages/bits-ui/src/lib/bits/command/components/command.svelte b/packages/bits-ui/src/lib/bits/command/components/command.svelte index fd68941ff..1d6887f14 100644 --- a/packages/bits-ui/src/lib/bits/command/components/command.svelte +++ b/packages/bits-ui/src/lib/bits/command/components/command.svelte @@ -18,6 +18,7 @@ label = "", vimBindings = true, disablePointerSelection = false, + controlledValue = false, children, child, ...restProps @@ -35,7 +36,9 @@ value: box.with( () => value, (v) => { - if (v !== value) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } diff --git a/packages/bits-ui/src/lib/bits/command/types.ts b/packages/bits-ui/src/lib/bits/command/types.ts index 307b11554..330b8559c 100644 --- a/packages/bits-ui/src/lib/bits/command/types.ts +++ b/packages/bits-ui/src/lib/bits/command/types.ts @@ -69,11 +69,20 @@ export type CommandRootPropsWithoutHTML = WithChild<{ disablePointerSelection?: boolean; /** - * Set the `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation. + * Set this prop to `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation. * * @defaultValue true */ vimBindings?: boolean; + + /** + * Whether or not the command is controlled or not. If `true`, the command will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type CommandRootProps = CommandRootPropsWithoutHTML & 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 c53b77236..a4a7af0a3 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 @@ -22,22 +22,32 @@ readonly = false, readonlySegments = [], required = false, + controlledPlaceholder = false, + controlledValue = false, children, }: RootProps = $props(); if (placeholder === undefined) { - placeholder = getDefaultDate({ + const defaultPlaceholder = getDefaultDate({ granularity, defaultPlaceholder: undefined, defaultValue: value, }); + + if (controlledPlaceholder) { + onPlaceholderChange(defaultPlaceholder); + } else { + placeholder = defaultPlaceholder; + } } useDateFieldRoot({ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } @@ -46,7 +56,9 @@ placeholder: box.with( () => placeholder as DateValue, (v) => { - if (placeholder !== v) { + if (controlledPlaceholder) { + onPlaceholderChange(v); + } else { placeholder = v; onPlaceholderChange(v); } 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 b69aec33f..40036d923 100644 --- a/packages/bits-ui/src/lib/bits/date-field/types.ts +++ b/packages/bits-ui/src/lib/bits/date-field/types.ts @@ -1,12 +1,12 @@ import type { DateValue } from "@internationalized/date"; import type { Snippet } from "svelte"; -import type { SegmentPart } from "$lib/shared/index.js"; +import type { SegmentPart, WithChildren } from "$lib/shared/index.js"; import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { PrimitiveDivAttributes, PrimitiveSpanAttributes } from "$lib/shared/attributes.js"; import type { EditableSegmentPart } from "$lib/shared/date/field/types.js"; import type { DateMatcher, Granularity } from "$lib/shared/date/types.js"; -export type DateFieldRootPropsWithoutHTML = { +export type DateFieldRootPropsWithoutHTML = WithChildren<{ /** * The value of the date field. * @@ -123,8 +123,25 @@ export type DateFieldRootPropsWithoutHTML = { */ required?: boolean; - children?: Snippet; -}; + /** + * Whether or not the value is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the placeholder is controlled or not. If `true`, the component will not update + * the placeholder state internally, instead it will call `onPlaceholderChange` when it would + * have otherwise, and it is up to you to update the `value` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; +}>; export type DateFieldRootProps = DateFieldRootPropsWithoutHTML; 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 b6390b5f6..bdda47ce2 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 @@ -39,20 +39,33 @@ numberOfMonths = 1, closeOnDateSelect = true, initialFocus = false, + controlledPlaceholder = false, + controlledValue = false, + controlledOpen = false, children, }: RootProps = $props(); if (placeholder === undefined) { - placeholder = getDefaultDate({ + const defaultPlaceholder = getDefaultDate({ granularity, defaultPlaceholder: undefined, defaultValue: value, }); + + if (controlledPlaceholder) { + onPlaceholderChange(defaultPlaceholder); + } else { + placeholder = defaultPlaceholder; + } } function onDateSelect() { if (closeOnDateSelect) { - open = false; + if (controlledOpen) { + onOpenChange(false); + } else { + open = false; + } } } @@ -60,7 +73,9 @@ open: box.with( () => open, (v) => { - if (open !== v) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; onOpenChange(v); } @@ -69,7 +84,9 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } @@ -78,7 +95,9 @@ placeholder: box.with( () => placeholder as DateValue, (v) => { - if (placeholder !== v) { + if (controlledPlaceholder) { + onPlaceholderChange(v as DateValue); + } else { placeholder = v; onPlaceholderChange(v as DateValue); } diff --git a/packages/bits-ui/src/lib/bits/date-picker/types.ts b/packages/bits-ui/src/lib/bits/date-picker/types.ts index 17d0ca633..2c4266547 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/types.ts +++ b/packages/bits-ui/src/lib/bits/date-picker/types.ts @@ -238,6 +238,35 @@ export type DatePickerRootPropsWithoutHTML = WithChildren<{ * Whether to focus a date when the picker is first opened. */ initialFocus?: boolean; + + /** + * Whether or not the value is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the placeholder is controlled or not. If `true`, the component will not update + * the placeholder state internally, instead it will call `onPlaceholderChange` when it would + * have otherwise, and it is up to you to update the `placeholder` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would + * have otherwise, and it is up to you to update the `open` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type DatePickerRootProps = DatePickerRootPropsWithoutHTML; 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 c1a11a616..efbe96672 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 @@ -31,6 +31,8 @@ child, onStartValueChange = noop, onEndValueChange = noop, + controlledPlaceholder = false, + controlledValue = false, ...restProps }: RootProps = $props(); @@ -38,15 +40,26 @@ let endValue = $state(value?.end); if (placeholder === undefined) { - placeholder = getDefaultDate({ + const defaultPlaceholder = getDefaultDate({ granularity, defaultPlaceholder: undefined, defaultValue: value?.start, }); + + if (controlledPlaceholder) { + onPlaceholderChange(defaultPlaceholder); + } else { + placeholder = defaultPlaceholder; + } } if (value === undefined) { - value = { start: undefined, end: undefined }; + const defaultValue = { start: undefined, end: undefined }; + if (controlledValue) { + onValueChange(defaultValue); + } else { + value = defaultValue; + } } const rootState = useDateRangeFieldRoot({ @@ -68,7 +81,9 @@ placeholder: box.with( () => placeholder as DateValue, (v) => { - if (v !== placeholder) { + if (controlledPlaceholder) { + onPlaceholderChange(v); + } else { placeholder = v; onPlaceholderChange(v); } @@ -78,7 +93,9 @@ value: box.with( () => value as DateRange, (v) => { - if (v !== value) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } 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 4eb82035f..2d3012dc7 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 @@ -135,6 +135,25 @@ export type DateRangeFieldRootPropsWithoutHTML = WithChild<{ * only part of the value is changed/completed. */ onEndValueChange?: OnChangeFn; + + /** + * Whether or not the value is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the placeholder is controlled or not. If `true`, the component will not update + * the placeholder state internally, instead it will call `onPlaceholderChange` when it would + * have otherwise, and it is up to you to update the `value` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; }>; export type DateRangeFieldRootProps = DateRangeFieldRootPropsWithoutHTML & 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 13de56dab..6f7a14dfb 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 @@ -44,6 +44,9 @@ closeOnRangeSelect = true, onStartValueChange = noop, onEndValueChange = noop, + controlledValue = false, + controlledPlaceholder = false, + controlledOpen = false, child, children, ...restProps @@ -53,20 +56,33 @@ let endValue = $state(value?.end); if (value === undefined) { - value = { start: undefined, end: undefined }; + if (controlledValue) { + onValueChange({ start: undefined, end: undefined }); + } else { + value = { start: undefined, end: undefined }; + } } if (placeholder === undefined) { - placeholder = getDefaultDate({ + const defaultPlaceholder = getDefaultDate({ granularity, defaultPlaceholder: undefined, defaultValue: value?.start, }); + if (controlledPlaceholder) { + onPlaceholderChange(defaultPlaceholder); + } else { + placeholder = defaultPlaceholder; + } } function onRangeSelect() { if (closeOnRangeSelect) { - open = false; + if (controlledOpen) { + onOpenChange(false); + } else { + open = false; + } } } @@ -74,7 +90,9 @@ open: box.with( () => open, (v) => { - if (open !== v) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; onOpenChange(v); } @@ -83,14 +101,20 @@ value: box.with( () => value as DateRange, (v) => { - value = v; - onValueChange(v); + if (controlledValue) { + onValueChange(v); + } else { + value = v; + onValueChange(v); + } } ), placeholder: box.with( () => placeholder as DateValue, (v) => { - if (placeholder !== v) { + if (controlledPlaceholder) { + onPlaceholderChange(v as DateValue); + } else { placeholder = v; onPlaceholderChange(v as DateValue); } diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/types.ts b/packages/bits-ui/src/lib/bits/date-range-picker/types.ts index 9bdda241d..70f624d58 100644 --- a/packages/bits-ui/src/lib/bits/date-range-picker/types.ts +++ b/packages/bits-ui/src/lib/bits/date-range-picker/types.ts @@ -248,6 +248,35 @@ export type DateRangePickerRootPropsWithoutHTML = WithChild<{ * only part of the value is changed/completed. */ onEndValueChange?: OnChangeFn; + + /** + * Whether or not the value is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the placeholder is controlled or not. If `true`, the component will not update + * the placeholder state internally, instead it will call `onPlaceholderChange` when it would + * have otherwise, and it is up to you to update the `placeholder` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would + * have otherwise, and it is up to you to update the `open` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type DateRangePickerRootProps = DateRangePickerRootPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte index d7da91885..cc1dc0896 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte @@ -2,16 +2,24 @@ import { box } from "svelte-toolbelt"; import { useDialogRoot } from "../dialog.svelte.js"; import type { RootProps } from "../index.js"; + import { noop } from "$lib/internal/callbacks.js"; - let { open = $bindable(false), onOpenChange, children }: RootProps = $props(); + let { + open = $bindable(false), + onOpenChange = noop, + controlledOpen = false, + children, + }: RootProps = $props(); useDialogRoot({ open: box.with( () => open, (v) => { - if (v !== open) { - onOpenChange?.(v); + if (controlledOpen) { + onOpenChange(v); + } else { open = v; + onOpenChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/dialog/types.ts b/packages/bits-ui/src/lib/bits/dialog/types.ts index 8aa09b9a8..f0b1a6bc6 100644 --- a/packages/bits-ui/src/lib/bits/dialog/types.ts +++ b/packages/bits-ui/src/lib/bits/dialog/types.ts @@ -17,6 +17,15 @@ export type DialogRootPropsWithoutHTML = WithChildren<{ * A callback that is called when the popover's open state changes. */ onOpenChange?: OnChangeFn; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type DialogRootProps = DialogRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview.svelte b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview.svelte index 8a1c86681..04147f66c 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview.svelte +++ b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview.svelte @@ -10,6 +10,7 @@ onOpenChange = noop, openDelay = 700, closeDelay = 300, + controlledOpen = false, children, }: RootProps = $props(); @@ -17,7 +18,9 @@ open: box.with( () => open, (v) => { - if (open !== v) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; onOpenChange(v); } 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 9a7481332..6fcb782cd 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/types.ts +++ b/packages/bits-ui/src/lib/bits/link-preview/types.ts @@ -47,6 +47,15 @@ export type LinkPreviewRootPropsWithoutHTML = WithChildren<{ * @defaultValue false */ ignoreNonKeyboardFocus?: boolean; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type LinkPreviewRootProps = LinkPreviewRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/bits/listbox/components/listbox.svelte b/packages/bits-ui/src/lib/bits/listbox/components/listbox.svelte index 6becc571b..19cf340bc 100644 --- a/packages/bits-ui/src/lib/bits/listbox/components/listbox.svelte +++ b/packages/bits-ui/src/lib/bits/listbox/components/listbox.svelte @@ -17,9 +17,20 @@ loop = false, scrollAlignment = "nearest", required = false, + controlledOpen = false, + controlledValue = false, children, }: RootProps = $props(); + if (value === undefined) { + const defaultValue = type === "single" ? "" : []; + if (controlledValue) { + onValueChange(defaultValue as any); + } else { + value = defaultValue; + } + } + value === undefined && (value = type === "single" ? "" : []); useListboxRoot({ @@ -27,8 +38,12 @@ value: box.with( () => value!, (v) => { - value = v; - onValueChange(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange(v as any); + } } ) as WritableBox | WritableBox, disabled: box.with(() => disabled), @@ -36,8 +51,12 @@ open: box.with( () => open, (v) => { - open = v; - onOpenChange(v); + if (controlledOpen) { + onOpenChange(v); + } else { + open = v; + onOpenChange(v); + } } ), loop: box.with(() => loop), diff --git a/packages/bits-ui/src/lib/bits/listbox/types.ts b/packages/bits-ui/src/lib/bits/listbox/types.ts index d994bb09b..89c5062d2 100644 --- a/packages/bits-ui/src/lib/bits/listbox/types.ts +++ b/packages/bits-ui/src/lib/bits/listbox/types.ts @@ -52,6 +52,24 @@ export type ListboxBaseRootPropsWithoutHTML = WithChildren<{ * @defaultValue `"nearest"` */ scrollAlignment?: "nearest" | "center"; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type ListboxSingleRootPropsWithoutHTML = { diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte index d502f5e12..c8d488dac 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte @@ -15,6 +15,7 @@ onCheckedChange = noop, disabled = false, onSelect = noop, + controlledChecked = false, ...restProps }: CheckboxItemProps = $props(); @@ -22,7 +23,9 @@ checked: box.with( () => checked, (v) => { - if (checked !== v) { + if (controlledChecked) { + onCheckedChange(v); + } else { checked = v; onCheckedChange(v); } diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte index efdd13ca7..66cdab49e 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte @@ -13,6 +13,7 @@ ref = $bindable(null), value = $bindable(""), onValueChange = noop, + controlledValue = false, ...restProps }: RadioGroupProps = $props(); @@ -20,7 +21,9 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub.svelte index 15815a71f..e2b48f20e 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub.svelte @@ -3,14 +3,22 @@ import type { SubProps } from "../index.js"; import { useMenuSubmenu } from "../menu.svelte.js"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; + import { noop } from "$lib/internal/callbacks.js"; - let { open = $bindable(false), onOpenChange, children }: SubProps = $props(); + let { + open = $bindable(false), + onOpenChange = noop, + controlledOpen = false, + children, + }: SubProps = $props(); useMenuSubmenu({ open: box.with( () => open, (v) => { - if (v !== open) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; onOpenChange?.(v); } diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu.svelte index a2b4df0b0..f0926ac7a 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu.svelte @@ -3,14 +3,25 @@ import type { RootProps } from "../index.js"; import { useMenuMenu, useMenuRoot } from "../menu.svelte.js"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; + import { noop } from "$lib/internal/callbacks.js"; - let { open = $bindable(false), dir = "ltr", onOpenChange, children }: RootProps = $props(); + let { + open = $bindable(false), + dir = "ltr", + onOpenChange = noop, + controlledOpen = false, + children, + }: RootProps = $props(); const root = useMenuRoot({ dir: box.with(() => dir), onClose: () => { - open = false; - onOpenChange?.(false); + if (controlledOpen) { + onOpenChange(false); + } else { + open = false; + onOpenChange?.(false); + } }, }); @@ -18,9 +29,11 @@ open: box.with( () => open, (v) => { - if (v !== open) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; - onOpenChange?.(v); + onOpenChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/menu/types.ts b/packages/bits-ui/src/lib/bits/menu/types.ts index dee7c2bf5..b25becc59 100644 --- a/packages/bits-ui/src/lib/bits/menu/types.ts +++ b/packages/bits-ui/src/lib/bits/menu/types.ts @@ -23,6 +23,15 @@ export type MenuRootPropsWithoutHTML = WithChildren<{ * @defaultValue "ltr" */ dir?: Direction; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type MenuRootProps = MenuRootPropsWithoutHTML; @@ -97,6 +106,16 @@ export type MenuCheckboxItemPropsWithoutHTML = * A callback that is fired when the checked state changes. */ onCheckedChange?: OnChangeFn; + + /** + * Whether or not the checked state is controlled or not. If `true`, the component will not + * update the checked state internally, instead it will call `onCheckedChange` when it + * would have otherwise, and it is up to you to update the `checked` prop that is passed + * to the component. + * + * @defaultValue false + */ + controlledChecked?: boolean; }; export type MenuCheckboxItemProps = MenuCheckboxItemPropsWithoutHTML & @@ -124,6 +143,15 @@ export type MenuSubPropsWithoutHTML = WithChildren<{ * A callback that is called when the menu is opened or closed. */ onOpenChange?: OnChangeFn; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type MenuSubContentPropsWithoutHTML = Expand< @@ -163,6 +191,15 @@ export type MenuRadioGroupPropsWithoutHTML = WithChild<{ * A callback that is fired when the selected radio item changes. */ onValueChange?: OnChangeFn; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type MenuRadioGroupProps = MenuRadioGroupPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/menubar/components/menubar.svelte b/packages/bits-ui/src/lib/bits/menubar/components/menubar.svelte index e65db77bd..ddec40cc2 100644 --- a/packages/bits-ui/src/lib/bits/menubar/components/menubar.svelte +++ b/packages/bits-ui/src/lib/bits/menubar/components/menubar.svelte @@ -4,6 +4,7 @@ import { useMenubarRoot } from "../menubar.svelte.js"; import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { noop } from "$lib/internal/callbacks.js"; let { id = useId(), @@ -13,7 +14,8 @@ value = "", dir = "ltr", loop = true, - onValueChange, + onValueChange = noop, + controlledValue = false, ...restProps }: RootProps = $props(); @@ -22,7 +24,9 @@ value: box.with( () => value, (v) => { - if (v !== value) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange?.(v); } diff --git a/packages/bits-ui/src/lib/bits/menubar/types.ts b/packages/bits-ui/src/lib/bits/menubar/types.ts index d7e89f7cf..c7722387c 100644 --- a/packages/bits-ui/src/lib/bits/menubar/types.ts +++ b/packages/bits-ui/src/lib/bits/menubar/types.ts @@ -25,6 +25,15 @@ export type MenubarRootPropsWithoutHTML = WithChild<{ * A callback that is called when the active menu changes. */ onValueChange?: OnChangeFn; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type MenubarRootProps = MenubarRootPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/pagination/components/pagination.svelte b/packages/bits-ui/src/lib/bits/pagination/components/pagination.svelte index 4274f4750..ff8e392cd 100644 --- a/packages/bits-ui/src/lib/bits/pagination/components/pagination.svelte +++ b/packages/bits-ui/src/lib/bits/pagination/components/pagination.svelte @@ -4,6 +4,7 @@ import { usePaginationRoot } from "../pagination.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { id = useId(), @@ -12,9 +13,10 @@ page = $bindable(1), ref = $bindable(null), siblingCount = 1, - onPageChange, + onPageChange = noop, loop = false, orientation = "horizontal", + controlledPage = false, child, children, ...restProps @@ -27,8 +29,12 @@ page: box.with( () => page, (v) => { - page = v; - onPageChange?.(v); + if (controlledPage) { + onPageChange(v); + } else { + page = v; + onPageChange?.(v); + } } ), loop: box.with(() => loop), diff --git a/packages/bits-ui/src/lib/bits/pagination/types.ts b/packages/bits-ui/src/lib/bits/pagination/types.ts index 65476264d..ab06e9400 100644 --- a/packages/bits-ui/src/lib/bits/pagination/types.ts +++ b/packages/bits-ui/src/lib/bits/pagination/types.ts @@ -32,7 +32,7 @@ export type PaginationRootPropsWithoutHTML = WithChild< * * @defaultValue 1 */ - page?: number | undefined; + page?: number; /** * A callback function called when the page changes. @@ -55,6 +55,15 @@ export type PaginationRootPropsWithoutHTML = WithChild< * @defaultValue "horizontal" */ orientation?: "horizontal" | "vertical"; + + /** + * Whether or not the page state is controlled or not. If `true`, the component will not update + * the page state internally, instead it will call `onPageChange` when it would have + * otherwise, and it is up to you to update the `page` prop that is passed to the component. + * + * @defaultValue false + */ + controlledPage?: boolean; }, PaginationSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/pin-input/components/pin-input.svelte b/packages/bits-ui/src/lib/bits/pin-input/components/pin-input.svelte index 0e74c2878..60bf9fee6 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/components/pin-input.svelte +++ b/packages/bits-ui/src/lib/bits/pin-input/components/pin-input.svelte @@ -22,6 +22,7 @@ disabled = false, value = $bindable(""), onValueChange = noop, + controlledValue = false, ...restProps }: RootProps = $props(); @@ -42,7 +43,9 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } diff --git a/packages/bits-ui/src/lib/bits/pin-input/types.ts b/packages/bits-ui/src/lib/bits/pin-input/types.ts index 820bf7871..2a4902e1b 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/types.ts +++ b/packages/bits-ui/src/lib/bits/pin-input/types.ts @@ -57,6 +57,16 @@ export type PinInputRootPropsWithoutHTML = Omit< */ inputId?: string; + /** + * Whether or not the value state is controlled or not. If `true`, the component will + * not update the value state internally, instead it will call `onValueChange` when + * it would have otherwise, and it is up to you to update the `value` prop that is + * passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + /** * The children snippet used to render the individual cells. */ diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover.svelte index ae481bc9b..f75528788 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover.svelte @@ -3,16 +3,24 @@ import type { RootProps } from "../index.js"; import { usePopoverRoot } from "../popover.svelte.js"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; + import { noop } from "$lib/internal/callbacks.js"; - let { open = $bindable(false), children, onOpenChange }: RootProps = $props(); + let { + open = $bindable(false), + onOpenChange = noop, + controlledOpen = false, + children, + }: RootProps = $props(); usePopoverRoot({ open: box.with( () => open, (v) => { - if (open !== v) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; - onOpenChange?.(v); + onOpenChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/popover/types.ts b/packages/bits-ui/src/lib/bits/popover/types.ts index a5ea82431..1ea881d4a 100644 --- a/packages/bits-ui/src/lib/bits/popover/types.ts +++ b/packages/bits-ui/src/lib/bits/popover/types.ts @@ -13,6 +13,15 @@ export type PopoverRootPropsWithoutHTML = WithChildren<{ * A callback that is called when the popover's open state changes. */ onOpenChange?: OnChangeFn; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type PopoverRootProps = PopoverRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte b/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte index 41c44a8d3..059e0e23a 100644 --- a/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte +++ b/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte @@ -5,6 +5,7 @@ import RadioGroupInput from "./radio-group-input.svelte"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { disabled = false, @@ -17,7 +18,8 @@ name = undefined, required = false, id = useId(), - onValueChange, + onValueChange = noop, + controlledValue = false, ...restProps }: RootProps = $props(); @@ -31,8 +33,12 @@ value: box.with( () => value, (v) => { - value = v; - onValueChange?.(v); + if (controlledValue) { + onValueChange(v); + } else { + value = v; + onValueChange?.(v); + } } ), ref: box.with( diff --git a/packages/bits-ui/src/lib/bits/radio-group/types.ts b/packages/bits-ui/src/lib/bits/radio-group/types.ts index a7a078af1..1d4b2622a 100644 --- a/packages/bits-ui/src/lib/bits/radio-group/types.ts +++ b/packages/bits-ui/src/lib/bits/radio-group/types.ts @@ -53,6 +53,15 @@ export type RadioGroupRootPropsWithoutHTML = WithChild<{ * input is rendered. */ required?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type RadioGroupRootProps = RadioGroupRootPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte b/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte index 03be8fd19..220c24e7e 100644 --- a/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte +++ b/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte @@ -34,6 +34,8 @@ disableDaysOutsideMonth = true, onStartValueChange = noop, onEndValueChange = noop, + controlledPlaceholder = false, + controlledValue = false, ...restProps }: RootProps = $props(); @@ -41,10 +43,25 @@ let endValue = $state(value?.end); if (placeholder === undefined) { - placeholder = getDefaultDate({ + const defaultPlaceholder = getDefaultDate({ defaultPlaceholder: undefined, defaultValue: value?.start, }); + + if (controlledPlaceholder) { + onPlaceholderChange(defaultPlaceholder); + } else { + placeholder = defaultPlaceholder; + } + } + + if (value === undefined) { + const defaultValue = { start: undefined, end: undefined }; + if (controlledValue) { + onValueChange(defaultValue); + } else { + value = defaultValue; + } } value === undefined && (value = { start: undefined, end: undefined }); @@ -56,31 +73,24 @@ (v) => (ref = v) ), value: box.with( - () => (value === undefined ? { start: undefined, end: undefined } : value), + () => value!, (v) => { - value = v; - onValueChange(v as any); + if (controlledValue) { + onValueChange(v); + } else { + value = v; + onValueChange(v); + } } ), placeholder: box.with( - () => - placeholder === undefined - ? getDefaultDate({ - defaultPlaceholder: undefined, - defaultValue: value?.start, - }) - : placeholder, + () => placeholder!, (v) => { - if (placeholder === undefined) { - placeholder = getDefaultDate({ - defaultPlaceholder: undefined, - defaultValue: value?.start, - }); - onPlaceholderChange(placeholder); - } - if (v !== placeholder) { + if (controlledPlaceholder) { + onPlaceholderChange(v); + } else { placeholder = v; - onPlaceholderChange(v as any); + onPlaceholderChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/range-calendar/types.ts b/packages/bits-ui/src/lib/bits/range-calendar/types.ts index 4efacea6e..74d3fa780 100644 --- a/packages/bits-ui/src/lib/bits/range-calendar/types.ts +++ b/packages/bits-ui/src/lib/bits/range-calendar/types.ts @@ -15,12 +15,12 @@ export type RangeCalendarRootPropsWithoutHTML = WithChild< * The value of the selected date range. * @bindable */ - value?: DateRange | undefined; + value?: DateRange; /** * A callback function called when the value changes. */ - onValueChange?: OnChangeFn; + onValueChange?: OnChangeFn; /** * The placeholder date, used to control the view of the @@ -28,13 +28,13 @@ export type RangeCalendarRootPropsWithoutHTML = WithChild< * * @defaultValue the current date */ - placeholder?: DateValue | undefined; + placeholder?: DateValue; /** * A callback function called when the placeholder value * changes. */ - onPlaceholderChange?: OnChangeFn | undefined; + onPlaceholderChange?: OnChangeFn; /** * Whether or not users can deselect a date once selected @@ -199,6 +199,25 @@ export type RangeCalendarRootPropsWithoutHTML = WithChild< * only part of the value is changed/completed. */ onEndValueChange?: OnChangeFn; + + /** + * Whether or not the value is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; + + /** + * Whether or not the placeholder is controlled or not. If `true`, the component will not + * update the placeholder state internally, instead it will call `onPlaceholderChange` when + * it would have otherwise, and it is up to you to update the `value` prop that is passed + * to the component. + * + * @defaultValue false + */ + controlledPlaceholder?: boolean; }, RangeCalendarRootSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/select/components/select-group-label.svelte b/packages/bits-ui/src/lib/bits/select/components/select-group-heading.svelte similarity index 100% rename from packages/bits-ui/src/lib/bits/select/components/select-group-label.svelte rename to packages/bits-ui/src/lib/bits/select/components/select-group-heading.svelte diff --git a/packages/bits-ui/src/lib/bits/select/components/select.svelte b/packages/bits-ui/src/lib/bits/select/components/select.svelte index df6fa5e5c..0f4879135 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select.svelte @@ -4,18 +4,21 @@ import { useSelectRoot } from "../select.svelte.js"; import SelectNative from "./select-native.svelte"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; + import { noop } from "$lib/internal/callbacks.js"; let { open = $bindable(false), value = $bindable(""), children, - onOpenChange, - onValueChange, + onOpenChange = noop, + onValueChange = noop, name = undefined, required = false, disabled = false, autocomplete = undefined, dir = "ltr", + controlledOpen = false, + controlledValue = false, form, }: RootProps = $props(); @@ -23,7 +26,9 @@ open: box.with( () => open, (v) => { - if (open !== v) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; onOpenChange?.(v); } @@ -32,7 +37,9 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange?.(v); } diff --git a/packages/bits-ui/src/lib/bits/select/index.ts b/packages/bits-ui/src/lib/bits/select/index.ts index b43513856..91123efbb 100644 --- a/packages/bits-ui/src/lib/bits/select/index.ts +++ b/packages/bits-ui/src/lib/bits/select/index.ts @@ -2,7 +2,7 @@ export { default as Root } from "./components/select.svelte"; export { default as Arrow } from "./components/select-arrow.svelte"; export { default as Content } from "./components/select-content.svelte"; export { default as Group } from "./components/select-group.svelte"; -export { default as GroupHeading } from "./components/select-group-label.svelte"; +export { default as GroupHeading } from "./components/select-group-heading.svelte"; export { default as Item } from "./components/select-item.svelte"; export { default as ItemText } from "./components/select-item-text.svelte"; export { default as Separator } from "./components/select-separator.svelte"; diff --git a/packages/bits-ui/src/lib/bits/select/types.ts b/packages/bits-ui/src/lib/bits/select/types.ts index c37566ebd..8a185638d 100644 --- a/packages/bits-ui/src/lib/bits/select/types.ts +++ b/packages/bits-ui/src/lib/bits/select/types.ts @@ -61,6 +61,24 @@ export type SelectRootPropsWithoutHTML = WithChildren<{ * Whether the select is required. */ required?: boolean; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type SelectRootProps = SelectRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/bits/slider/components/slider.svelte b/packages/bits-ui/src/lib/bits/slider/components/slider.svelte index c94e8145c..7f6f65ae4 100644 --- a/packages/bits-ui/src/lib/bits/slider/components/slider.svelte +++ b/packages/bits-ui/src/lib/bits/slider/components/slider.svelte @@ -21,6 +21,7 @@ dir = "ltr", autoSort = true, orientation = "horizontal", + controlledValue = false, ...restProps }: RootProps = $props(); @@ -33,7 +34,9 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; onValueChange(v); } diff --git a/packages/bits-ui/src/lib/bits/slider/types.ts b/packages/bits-ui/src/lib/bits/slider/types.ts index b345d1121..3008dc2eb 100644 --- a/packages/bits-ui/src/lib/bits/slider/types.ts +++ b/packages/bits-ui/src/lib/bits/slider/types.ts @@ -13,7 +13,7 @@ export type SliderRootPropsWithoutHTML = WithChild< * The value of the slider. * @bindable */ - value?: number[] | undefined; + value?: number[]; /** * A callback function called when the value changes. @@ -78,6 +78,16 @@ export type SliderRootPropsWithoutHTML = WithChild< * @defaultValue false */ disabled?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will + * not update the value state internally, instead it will call `onValueChange` when it + * would have otherwise, and it is up to you to update the `value` prop that is passed + * to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }, SliderRootSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/switch/components/switch.svelte b/packages/bits-ui/src/lib/bits/switch/components/switch.svelte index e9bef6d74..830f25c3a 100644 --- a/packages/bits-ui/src/lib/bits/switch/components/switch.svelte +++ b/packages/bits-ui/src/lib/bits/switch/components/switch.svelte @@ -5,6 +5,7 @@ import SwitchInput from "./switch-input.svelte"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { child, @@ -17,7 +18,8 @@ value = "on", name = undefined, type = "button", - onCheckedChange, + onCheckedChange = noop, + controlledChecked = false, ...restProps }: RootProps = $props(); @@ -25,8 +27,12 @@ checked: box.with( () => checked, (v) => { - checked = v; - onCheckedChange?.(v); + if (controlledChecked) { + onCheckedChange(v); + } else { + checked = v; + onCheckedChange?.(v); + } } ), disabled: box.with(() => disabled ?? false), diff --git a/packages/bits-ui/src/lib/bits/switch/types.ts b/packages/bits-ui/src/lib/bits/switch/types.ts index cf1640f78..cf8f6216e 100644 --- a/packages/bits-ui/src/lib/bits/switch/types.ts +++ b/packages/bits-ui/src/lib/bits/switch/types.ts @@ -48,6 +48,16 @@ export type SwitchRootPropsWithoutHTML = WithChild< * A callback function called when the checked state changes. */ onCheckedChange?: OnChangeFn; + + /** + * Whether or not the checkbox is controlled or not. If `true`, the checkbox will not update + * the checked state internally, instead it will call `onCheckedChange` when it would have + * otherwise, and it is up to you to update the `checked` prop that is passed to the + * component. + * + * @defaultValue false + */ + controlledChecked?: boolean; }, SwitchRootSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/tabs/components/tabs.svelte b/packages/bits-ui/src/lib/bits/tabs/components/tabs.svelte index 68a006f1a..7f3c3f098 100644 --- a/packages/bits-ui/src/lib/bits/tabs/components/tabs.svelte +++ b/packages/bits-ui/src/lib/bits/tabs/components/tabs.svelte @@ -4,18 +4,20 @@ import { useTabsRoot } from "../tabs.svelte.js"; import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { noop } from "$lib/internal/callbacks.js"; let { - children, - child, - ref = $bindable(null), id = useId(), + ref = $bindable(null), value = $bindable(""), - onValueChange, + onValueChange = noop, orientation = "horizontal", loop = true, activationMode = "automatic", disabled = false, + controlledValue = false, + children, + child, ...restProps }: RootProps = $props(); @@ -24,9 +26,11 @@ value: box.with( () => value, (v) => { - if (value !== v) { + if (controlledValue) { + onValueChange(v); + } else { value = v; - onValueChange?.(v); + onValueChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/tabs/types.ts b/packages/bits-ui/src/lib/bits/tabs/types.ts index 8d0f3aedc..2c6da131c 100644 --- a/packages/bits-ui/src/lib/bits/tabs/types.ts +++ b/packages/bits-ui/src/lib/bits/tabs/types.ts @@ -45,6 +45,15 @@ export type TabsRootPropsWithoutHTML = WithChild<{ * @defaultValue false */ disabled?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }>; export type TabsRootProps = TabsRootPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte b/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte index 96a81e51e..67c3ef6cc 100644 --- a/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte +++ b/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte @@ -4,31 +4,44 @@ import { useToggleGroupRoot } from "../toggle-group.svelte.js"; import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { noop } from "$lib/internal/callbacks.js"; let { - child, - children, - ref = $bindable(null), id = useId(), + ref = $bindable(null), value = $bindable(), - onValueChange, + onValueChange = noop, type, disabled = false, loop = true, orientation = "horizontal", rovingFocus = true, + controlledValue = false, + child, + children, ...restProps }: RootProps = $props(); - value === undefined && (value = type === "single" ? "" : []); + if (value === undefined) { + const defaultValue = type === "single" ? "" : []; + if (controlledValue) { + onValueChange(defaultValue as any); + } else { + value = defaultValue; + } + } const rootState = useToggleGroupRoot({ id: box.with(() => id), value: box.with( () => value!, (v) => { - value = v; - onValueChange?.(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange?.(v as any); + } } ) as WritableBox | WritableBox, disabled: box.with(() => disabled), diff --git a/packages/bits-ui/src/lib/bits/toggle-group/types.ts b/packages/bits-ui/src/lib/bits/toggle-group/types.ts index 97dc71c84..971cb9af5 100644 --- a/packages/bits-ui/src/lib/bits/toggle-group/types.ts +++ b/packages/bits-ui/src/lib/bits/toggle-group/types.ts @@ -32,6 +32,15 @@ export type BaseToggleGroupRootProps = { * users navigate between the items using the tab key. */ rovingFocus?: boolean; + + /** + * Whether or not the value state is controlled or not. If `true`, the component will not update + * the value state internally, instead it will call `onValueChange` when it would have + * otherwise, and it is up to you to update the `value` prop that is passed to the component. + * + * @defaultValue false + */ + controlledValue?: boolean; }; export type SingleToggleGroupRootPropsWithoutHTML = WithChild< diff --git a/packages/bits-ui/src/lib/bits/toggle/components/toggle.svelte b/packages/bits-ui/src/lib/bits/toggle/components/toggle.svelte index 61a83a4a6..155534e1f 100644 --- a/packages/bits-ui/src/lib/bits/toggle/components/toggle.svelte +++ b/packages/bits-ui/src/lib/bits/toggle/components/toggle.svelte @@ -4,16 +4,18 @@ import { useToggleRoot } from "../toggle.svelte.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; + import { noop } from "$lib/internal/callbacks.js"; let { - child, - children, ref = $bindable(null), id = useId(), pressed = $bindable(false), - onPressedChange, + onPressedChange = noop, disabled = false, type = "button", + controlledPressed = false, + children, + child, ...restProps }: RootProps = $props(); @@ -21,9 +23,11 @@ pressed: box.with( () => pressed, (v) => { - if (pressed !== v) { + if (controlledPressed) { + onPressedChange(v); + } else { pressed = v; - onPressedChange?.(v); + onPressedChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/toggle/types.ts b/packages/bits-ui/src/lib/bits/toggle/types.ts index fd9853712..14ff77317 100644 --- a/packages/bits-ui/src/lib/bits/toggle/types.ts +++ b/packages/bits-ui/src/lib/bits/toggle/types.ts @@ -25,6 +25,16 @@ export type ToggleRootPropsWithoutHTML = WithChild< * @defaultValue false */ disabled?: boolean | null | undefined; + + /** + * Whether or not the pressed state is controlled or not. If `true`, the component will not + * update the pressed state internally, instead it will call `onPressedChange` when it + * would have otherwise, and it is up to you to update the `pressed` prop that is passed to + * the component. + * + * @defaultValue false + */ + controlledPressed?: boolean; }, ToggleRootSnippetProps >; diff --git a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte index cfa5284c2..4ea792b63 100644 --- a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte +++ b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte @@ -4,20 +4,29 @@ import { useToolbarGroup } from "../toolbar.svelte.js"; import { useId } from "$lib/internal/useId.js"; import { mergeProps } from "$lib/internal/mergeProps.js"; + import { noop } from "$lib/internal/callbacks.js"; let { - child, - children, - ref = $bindable(null), id = useId(), + ref = $bindable(null), value = $bindable(), - onValueChange, + onValueChange = noop, type, disabled = false, + controlledValue = false, + child, + children, ...restProps }: GroupProps = $props(); - value === undefined && (value = type === "single" ? "" : []); + if (value === undefined) { + const defaultValue = type === "single" ? "" : []; + if (controlledValue) { + onValueChange(defaultValue as any); + } else { + value = defaultValue; + } + } const groupState = useToolbarGroup({ id: box.with(() => id), @@ -26,8 +35,12 @@ value: box.with( () => value!, (v) => { - value = v; - onValueChange?.(v as any); + if (controlledValue) { + onValueChange(v as any); + } else { + value = v; + onValueChange(v as any); + } } ) as WritableBox | WritableBox, ref: box.with( diff --git a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte index edd68113e..6902454ca 100644 --- a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte +++ b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte @@ -6,12 +6,12 @@ import { useId } from "$lib/internal/useId.js"; let { - child, - children, ref = $bindable(null), id = useId(), orientation = "horizontal", loop = true, + child, + children, ...restProps }: RootProps = $props(); diff --git a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip.svelte b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip.svelte index 2c9823140..37b83fb95 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip.svelte +++ b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip.svelte @@ -3,15 +3,17 @@ import type { RootProps } from "../index.js"; import { useTooltipRoot } from "../tooltip.svelte.js"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; + import { noop } from "$lib/internal/callbacks.js"; let { open = $bindable(false), - onOpenChange, + onOpenChange = noop, disabled = false, delayDuration, disableCloseOnTriggerClick = false, disableHoverableContent, ignoreNonKeyboardFocus = false, + controlledOpen = false, children, }: RootProps = $props(); @@ -19,9 +21,11 @@ open: box.with( () => open, (v) => { - if (v !== open) { + if (controlledOpen) { + onOpenChange(v); + } else { open = v; - onOpenChange?.(v); + onOpenChange(v); } } ), diff --git a/packages/bits-ui/src/lib/bits/tooltip/types.ts b/packages/bits-ui/src/lib/bits/tooltip/types.ts index 11f907eb3..47e1059dd 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/types.ts +++ b/packages/bits-ui/src/lib/bits/tooltip/types.ts @@ -101,6 +101,15 @@ export type TooltipRootPropsWithoutHTML = WithChildren<{ * @defaultValue false */ ignoreNonKeyboardFocus?: boolean; + + /** + * Whether or not the open state is controlled or not. If `true`, the component will not update + * the open state internally, instead it will call `onOpenChange` when it would have + * otherwise, and it is up to you to update the `open` prop that is passed to the component. + * + * @defaultValue false + */ + controlledOpen?: boolean; }>; export type TooltipRootProps = TooltipRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/internal/arrays.spec.ts b/packages/bits-ui/src/lib/internal/arrays.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/arrays.spec.ts rename to packages/bits-ui/src/lib/internal/arrays.test.ts diff --git a/packages/bits-ui/src/lib/internal/callbacks.spec.ts b/packages/bits-ui/src/lib/internal/callbacks.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/callbacks.spec.ts rename to packages/bits-ui/src/lib/internal/callbacks.test.ts diff --git a/packages/bits-ui/src/lib/internal/clamp.spec.ts b/packages/bits-ui/src/lib/internal/clamp.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/clamp.spec.ts rename to packages/bits-ui/src/lib/internal/clamp.test.ts diff --git a/packages/bits-ui/src/lib/internal/composeHandlers.spec.ts b/packages/bits-ui/src/lib/internal/composeHandlers.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/composeHandlers.spec.ts rename to packages/bits-ui/src/lib/internal/composeHandlers.test.ts diff --git a/packages/bits-ui/src/lib/internal/cssToStyleObj.spec.ts b/packages/bits-ui/src/lib/internal/cssToStyleObj.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/cssToStyleObj.spec.ts rename to packages/bits-ui/src/lib/internal/cssToStyleObj.test.ts diff --git a/packages/bits-ui/src/lib/internal/debounce.spec.ts b/packages/bits-ui/src/lib/internal/debounce.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/debounce.spec.ts rename to packages/bits-ui/src/lib/internal/debounce.test.ts diff --git a/packages/bits-ui/src/lib/internal/getDirectionalKeys.spec.ts b/packages/bits-ui/src/lib/internal/getDirectionalKeys.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/getDirectionalKeys.spec.ts rename to packages/bits-ui/src/lib/internal/getDirectionalKeys.test.ts diff --git a/packages/bits-ui/src/lib/internal/is.spec.ts b/packages/bits-ui/src/lib/internal/is.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/is.spec.ts rename to packages/bits-ui/src/lib/internal/is.test.ts diff --git a/packages/bits-ui/src/lib/internal/math.spec.ts b/packages/bits-ui/src/lib/internal/math.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/math.spec.ts rename to packages/bits-ui/src/lib/internal/math.test.ts diff --git a/packages/bits-ui/src/lib/internal/mergeProps.spec.ts b/packages/bits-ui/src/lib/internal/mergeProps.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/mergeProps.spec.ts rename to packages/bits-ui/src/lib/internal/mergeProps.test.ts diff --git a/packages/bits-ui/src/lib/internal/polygon.spec.ts b/packages/bits-ui/src/lib/internal/polygon.test.ts similarity index 100% rename from packages/bits-ui/src/lib/internal/polygon.spec.ts rename to packages/bits-ui/src/lib/internal/polygon.test.ts diff --git a/packages/bits-ui/src/tests/accordion/accordion.spec.ts b/packages/bits-ui/src/tests/accordion/accordion.test.ts similarity index 100% rename from packages/bits-ui/src/tests/accordion/accordion.spec.ts rename to packages/bits-ui/src/tests/accordion/accordion.test.ts diff --git a/packages/bits-ui/src/tests/alert-dialog/alert-dialog.spec.ts b/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts similarity index 100% rename from packages/bits-ui/src/tests/alert-dialog/alert-dialog.spec.ts rename to packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts diff --git a/packages/bits-ui/src/tests/avatar/avatar.spec.ts b/packages/bits-ui/src/tests/avatar/avatar.test.ts similarity index 100% rename from packages/bits-ui/src/tests/avatar/avatar.spec.ts rename to packages/bits-ui/src/tests/avatar/avatar.test.ts diff --git a/packages/bits-ui/src/tests/calendar/calendar.spec.ts b/packages/bits-ui/src/tests/calendar/calendar.test.ts similarity index 100% rename from packages/bits-ui/src/tests/calendar/calendar.spec.ts rename to packages/bits-ui/src/tests/calendar/calendar.test.ts diff --git a/packages/bits-ui/src/tests/checkbox/checkbox.spec.ts b/packages/bits-ui/src/tests/checkbox/checkbox.test.ts similarity index 100% rename from packages/bits-ui/src/tests/checkbox/checkbox.spec.ts rename to packages/bits-ui/src/tests/checkbox/checkbox.test.ts diff --git a/packages/bits-ui/src/tests/collapsible/collapsible.spec.ts b/packages/bits-ui/src/tests/collapsible/collapsible.test.ts similarity index 100% rename from packages/bits-ui/src/tests/collapsible/collapsible.spec.ts rename to packages/bits-ui/src/tests/collapsible/collapsible.test.ts diff --git a/packages/bits-ui/src/tests/combobox/combobox.spec.ts b/packages/bits-ui/src/tests/combobox/combobox.test.ts similarity index 100% rename from packages/bits-ui/src/tests/combobox/combobox.spec.ts rename to packages/bits-ui/src/tests/combobox/combobox.test.ts diff --git a/packages/bits-ui/src/tests/context-menu/context-menu.spec.ts b/packages/bits-ui/src/tests/context-menu/context-menu.test.ts similarity index 100% rename from packages/bits-ui/src/tests/context-menu/context-menu.spec.ts rename to packages/bits-ui/src/tests/context-menu/context-menu.test.ts diff --git a/packages/bits-ui/src/tests/date-field/date-field.spec.ts b/packages/bits-ui/src/tests/date-field/date-field.test.ts similarity index 100% rename from packages/bits-ui/src/tests/date-field/date-field.spec.ts rename to packages/bits-ui/src/tests/date-field/date-field.test.ts diff --git a/packages/bits-ui/src/tests/date-range-field/date-range-field.spec.ts b/packages/bits-ui/src/tests/date-range-field/date-range-field.test.ts similarity index 100% rename from packages/bits-ui/src/tests/date-range-field/date-range-field.spec.ts rename to packages/bits-ui/src/tests/date-range-field/date-range-field.test.ts diff --git a/packages/bits-ui/src/tests/dialog/dialog.spec.ts b/packages/bits-ui/src/tests/dialog/dialog.test.ts similarity index 100% rename from packages/bits-ui/src/tests/dialog/dialog.spec.ts rename to packages/bits-ui/src/tests/dialog/dialog.test.ts diff --git a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.spec.ts b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts similarity index 100% rename from packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.spec.ts rename to packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts diff --git a/packages/bits-ui/src/tests/label/label.spec.ts b/packages/bits-ui/src/tests/label/label.test.ts similarity index 100% rename from packages/bits-ui/src/tests/label/label.spec.ts rename to packages/bits-ui/src/tests/label/label.test.ts diff --git a/packages/bits-ui/src/tests/link-preview/link-preview.spec.ts b/packages/bits-ui/src/tests/link-preview/link-preview.test.ts similarity index 100% rename from packages/bits-ui/src/tests/link-preview/link-preview.spec.ts rename to packages/bits-ui/src/tests/link-preview/link-preview.test.ts diff --git a/packages/bits-ui/src/tests/listbox/listbox.spec.ts b/packages/bits-ui/src/tests/listbox/listbox.test.ts similarity index 100% rename from packages/bits-ui/src/tests/listbox/listbox.spec.ts rename to packages/bits-ui/src/tests/listbox/listbox.test.ts diff --git a/packages/bits-ui/src/tests/menubar/menubar.spec.ts b/packages/bits-ui/src/tests/menubar/menubar.test.ts similarity index 100% rename from packages/bits-ui/src/tests/menubar/menubar.spec.ts rename to packages/bits-ui/src/tests/menubar/menubar.test.ts diff --git a/packages/bits-ui/src/tests/pagination/pagination.spec.ts b/packages/bits-ui/src/tests/pagination/pagination.test.ts similarity index 100% rename from packages/bits-ui/src/tests/pagination/pagination.spec.ts rename to packages/bits-ui/src/tests/pagination/pagination.test.ts diff --git a/packages/bits-ui/src/tests/pin-input/pin-input.spec.ts b/packages/bits-ui/src/tests/pin-input/pin-input.test.ts similarity index 100% rename from packages/bits-ui/src/tests/pin-input/pin-input.spec.ts rename to packages/bits-ui/src/tests/pin-input/pin-input.test.ts diff --git a/packages/bits-ui/src/tests/popover/popover.spec.ts b/packages/bits-ui/src/tests/popover/popover.test.ts similarity index 100% rename from packages/bits-ui/src/tests/popover/popover.spec.ts rename to packages/bits-ui/src/tests/popover/popover.test.ts diff --git a/packages/bits-ui/src/tests/progress/progress.spec.ts b/packages/bits-ui/src/tests/progress/progress.test.ts similarity index 100% rename from packages/bits-ui/src/tests/progress/progress.spec.ts rename to packages/bits-ui/src/tests/progress/progress.test.ts diff --git a/packages/bits-ui/src/tests/radio-group/radio-group.spec.ts b/packages/bits-ui/src/tests/radio-group/radio-group.test.ts similarity index 100% rename from packages/bits-ui/src/tests/radio-group/radio-group.spec.ts rename to packages/bits-ui/src/tests/radio-group/radio-group.test.ts diff --git a/packages/bits-ui/src/tests/range-calendar/range-calendar.spec.ts b/packages/bits-ui/src/tests/range-calendar/range-calendar.test.ts similarity index 100% rename from packages/bits-ui/src/tests/range-calendar/range-calendar.spec.ts rename to packages/bits-ui/src/tests/range-calendar/range-calendar.test.ts diff --git a/packages/bits-ui/src/tests/scroll-area/scroll-area.spec.ts b/packages/bits-ui/src/tests/scroll-area/scroll-area.test.ts similarity index 100% rename from packages/bits-ui/src/tests/scroll-area/scroll-area.spec.ts rename to packages/bits-ui/src/tests/scroll-area/scroll-area.test.ts diff --git a/packages/bits-ui/src/tests/select/select.spec.ts b/packages/bits-ui/src/tests/select/select.test.ts similarity index 100% rename from packages/bits-ui/src/tests/select/select.spec.ts rename to packages/bits-ui/src/tests/select/select.test.ts diff --git a/packages/bits-ui/src/tests/separator/separator.spec.ts b/packages/bits-ui/src/tests/separator/separator.test.ts similarity index 100% rename from packages/bits-ui/src/tests/separator/separator.spec.ts rename to packages/bits-ui/src/tests/separator/separator.test.ts diff --git a/packages/bits-ui/src/tests/slider/slider.spec.ts b/packages/bits-ui/src/tests/slider/slider.test.ts similarity index 100% rename from packages/bits-ui/src/tests/slider/slider.spec.ts rename to packages/bits-ui/src/tests/slider/slider.test.ts diff --git a/packages/bits-ui/src/tests/switch/switch.spec.ts b/packages/bits-ui/src/tests/switch/switch.test.ts similarity index 100% rename from packages/bits-ui/src/tests/switch/switch.spec.ts rename to packages/bits-ui/src/tests/switch/switch.test.ts diff --git a/packages/bits-ui/src/tests/tabs/tabs.spec.ts b/packages/bits-ui/src/tests/tabs/tabs.test.ts similarity index 100% rename from packages/bits-ui/src/tests/tabs/tabs.spec.ts rename to packages/bits-ui/src/tests/tabs/tabs.test.ts diff --git a/packages/bits-ui/src/tests/toggle-group/toggle-group.spec.ts b/packages/bits-ui/src/tests/toggle-group/toggle-group.test.ts similarity index 100% rename from packages/bits-ui/src/tests/toggle-group/toggle-group.spec.ts rename to packages/bits-ui/src/tests/toggle-group/toggle-group.test.ts diff --git a/packages/bits-ui/src/tests/toggle/toggle.spec.ts b/packages/bits-ui/src/tests/toggle/toggle.test.ts similarity index 100% rename from packages/bits-ui/src/tests/toggle/toggle.spec.ts rename to packages/bits-ui/src/tests/toggle/toggle.test.ts diff --git a/packages/bits-ui/src/tests/toolbar/toolbar.spec.ts b/packages/bits-ui/src/tests/toolbar/toolbar.test.ts similarity index 100% rename from packages/bits-ui/src/tests/toolbar/toolbar.spec.ts rename to packages/bits-ui/src/tests/toolbar/toolbar.test.ts diff --git a/packages/bits-ui/src/tests/tooltip/tooltip.spec.ts b/packages/bits-ui/src/tests/tooltip/tooltip.test.ts similarity index 100% rename from packages/bits-ui/src/tests/tooltip/tooltip.spec.ts rename to packages/bits-ui/src/tests/tooltip/tooltip.test.ts diff --git a/sites/docs/content/components/accordion.md b/sites/docs/content/components/accordion.md index a75f5570e..9d9f93b16 100644 --- a/sites/docs/content/components/accordion.md +++ b/sites/docs/content/components/accordion.md @@ -179,6 +179,26 @@ You can also use the `onValueChange` prop to update local state when the Accordi ``` +### Controlled + +Sometimes, you may want complete control over the accordion's value state, meaning you will be "kept in the loop" and be required to apply the value state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `Accordion.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + ## Single Type Set the `type` prop to `"single"` to allow only one accordion item to be open at a time. diff --git a/sites/docs/content/components/alert-dialog.md b/sites/docs/content/components/alert-dialog.md index ce3e96b10..dc6358870 100644 --- a/sites/docs/content/components/alert-dialog.md +++ b/sites/docs/content/components/alert-dialog.md @@ -170,6 +170,26 @@ You can also use the `onOpenChange` prop to update local state when the dialog's ``` +### Controlled + +Sometimes, you may want complete control over the dialog's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you'll rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `open` prop to the `AlertDialog.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + ## Managing Focus ### Focus Trap diff --git a/sites/docs/content/components/collapsible.md b/sites/docs/content/components/collapsible.md index f81815029..4130bf8e0 100644 --- a/sites/docs/content/components/collapsible.md +++ b/sites/docs/content/components/collapsible.md @@ -112,6 +112,24 @@ You can also use the `onOpenChange` prop to update local state when the Collapsi ``` +### Controlled + +Sometimes, you may want complete control over the component's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you'll rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `open` prop to the `Collapsible.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + ## Svelte Transitions You can use the `forceMount` prop on the `Collapsible.Content` component to forcefully mount the content regardless of whether the collapsible is opened or not. This is useful when you want more control over the transitions when the collapsible opens and closes using something like [Svelte Transitions](https://svelte.dev/docs#transition). diff --git a/sites/docs/content/components/context-menu.md b/sites/docs/content/components/context-menu.md index 9fd00c495..baaaee71e 100644 --- a/sites/docs/content/components/context-menu.md +++ b/sites/docs/content/components/context-menu.md @@ -184,6 +184,24 @@ You can also use the `onOpenChange` prop to update local state when the menu's ` ``` +### Controlled + +Sometimes, you may want complete control over the component's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you'll rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `open` prop to the `ContextMenu.Root` or `ContextMenu.Sub` component(s). + +```svelte + + + (myOpen = o)}> + + +``` + ## Checkbox Items You can use the `ContextMenu.CheckboxItem` component to create a `menuitemcheckbox` element to add checkbox functionality to menu items. @@ -233,7 +251,7 @@ You can combine the `ContextMenu.RadioGroup` and `ContextMenu.RadioItem` compone ``` -See the [RadioGroup](#contextmenuradiogroup) and [RadioItem](#contextmenuradiaitem) APIs for more information. +See the [RadioGroup](#contextmenuradiogroup) and [RadioItem](#contextmenuradioitem) APIs for more information. ## Nested Menus diff --git a/sites/docs/content/components/dialog.md b/sites/docs/content/components/dialog.md index afae2dd89..0b6517472 100644 --- a/sites/docs/content/components/dialog.md +++ b/sites/docs/content/components/dialog.md @@ -170,6 +170,24 @@ You can also use the `onOpenChange` prop to update local state when the Dialog's ``` +### Controlled + +Sometimes, you may want complete control over the dialog's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you'll rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `open` prop to the `Dialog.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + ## Managing Focus ### Focus Trap diff --git a/sites/docs/content/components/dropdown-menu.md b/sites/docs/content/components/dropdown-menu.md index 5ba8b6ddf..169ba32db 100644 --- a/sites/docs/content/components/dropdown-menu.md +++ b/sites/docs/content/components/dropdown-menu.md @@ -160,6 +160,24 @@ You can also use the `onOpenChange` prop to update local state when the menu's ` ``` +### Controlled + +Sometimes, you may want complete control over the component's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you'll rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `open` prop to the `DropdownMenu.Root` or `DropdownMenu.Sub` component(s). + +```svelte + + + (myOpen = o)}> + + +``` + ## Groups To group related menu items, you can use the `DropdownMenu.Group` component along with either a `DropdownMenu.GroupHeading` or an `aria-label` attribute on the `DropdownMenu.Group` component. @@ -181,7 +199,7 @@ To group related menu items, you can use the `DropdownMenu.Group` component alon ``` -### Group Label +### Group Heading The `DropdownMenu.GroupHeading` component must be a child of either a `DropdownMenu.Group` or `DropdownMenu.RadioGroup` component. If used on its own, an error will be thrown during development. diff --git a/sites/docs/content/controlled-state.md b/sites/docs/content/controlled-state.md new file mode 100644 index 000000000..c2978b9d0 --- /dev/null +++ b/sites/docs/content/controlled-state.md @@ -0,0 +1,38 @@ +--- +title: Controlled State +description: Learn how to use controlled state in Bits UI components. +--- + +Sometimes, Bits UI doesn't know what's best for your specific use case. In these cases, you can use controlled state to ensure the component remains in a specific state depending on your needs. + +## Uncontrolled State + +By default, Bits UI components are uncontrolled. This means that the component is responsible for managing its own state. You can `bind:` to that state for a reference to it, but the component decides when and how to update that state. + +For example, the `Accordion.Root` component manages its own `value` state. When you click or press on any of the triggers, the component will update the `value` state to the value of the trigger that was clicked. + +You can update the `value` state of the `Accordion.Root` component yourself from the outside, but you can't prevent the component from updating it. Preventing the component from updating the state is where controlled state comes in. + +## Controlled State + +Controlled state is when you, as the user, are responsible for updating the state of the component. The component will let you know when it thinks it needs to update the state, but you'll be responsible for whether that update happens. + +This is useful when you have specific conditions that should be met before the component can update, or anything else your requirements dictate. + +To effectively use controlled state, you'll need to set the `controlled` prop to `true` on the component, and you'll also need to pass a local state variable to the component that you'll update yourself. You'll use the `onChange` callback to update the local state variable. + +Here's an example of how you might use controlled state with the `Accordion` component: + +```svelte + + + (myValue = v)}> + + +``` + +In the example above, we're using the `controlledValue` prop to tell the `Accordion.Root` component that it should be in controlled state. If we were to remove the `onValueChange` callback, the component wouldn't respond to user interactions and wouldn't update the `value` state. diff --git a/sites/docs/src/lib/components/api-ref/css-vars-table.svelte b/sites/docs/src/lib/components/api-ref/css-vars-table.svelte index 648146f63..299eac81d 100644 --- a/sites/docs/src/lib/components/api-ref/css-vars-table.svelte +++ b/sites/docs/src/lib/components/api-ref/css-vars-table.svelte @@ -1,5 +1,5 @@ diff --git a/sites/docs/src/lib/components/api-ref/prop-type-content.svelte b/sites/docs/src/lib/components/api-ref/prop-type-content.svelte index 582e90221..df766436f 100644 --- a/sites/docs/src/lib/components/api-ref/prop-type-content.svelte +++ b/sites/docs/src/lib/components/api-ref/prop-type-content.svelte @@ -2,7 +2,7 @@ import { Popover, Tooltip } from "bits-ui"; import Info from "phosphor-svelte/lib/Info"; import type { Component } from "svelte"; - import { Code } from "$lib/components/index.js"; + import Code from "$lib/components/markdown/code.svelte"; import type { PropType } from "$lib/types/index.js"; import { parseTypeDef } from "$lib/utils/index.js"; diff --git a/sites/docs/src/lib/components/api-ref/props-table.svelte b/sites/docs/src/lib/components/api-ref/props-table.svelte index 086d7b096..121851fc0 100644 --- a/sites/docs/src/lib/components/api-ref/props-table.svelte +++ b/sites/docs/src/lib/components/api-ref/props-table.svelte @@ -2,7 +2,7 @@ import { Tooltip } from "bits-ui"; import Badge from "../ui/badge.svelte"; import PropTypeContent from "./prop-type-content.svelte"; - import { Code } from "$lib/components/index.js"; + import Code from "$lib/components/markdown/code.svelte"; import * as Table from "$lib/components/ui/table/index.js"; import type { PropObj } from "$lib/types/index.js"; import { parseMarkdown } from "$lib/utils/index.js"; diff --git a/sites/docs/src/lib/components/api-section.svelte b/sites/docs/src/lib/components/api-section.svelte index 5588917b3..f577aa609 100644 --- a/sites/docs/src/lib/components/api-section.svelte +++ b/sites/docs/src/lib/components/api-section.svelte @@ -1,6 +1,8 @@ {#if items.length}
{#each items as item, index (index)} {#if item.href} + {@const Icon = item.icon} - {#if isIconMapKey(item.title)} - {@const Icon = iconMap[item.title]} - - {/if} + {item.title} {#if item.label} - import { SidebarNavItems, SidebarNavMainItems } from "$lib/components/index.js"; + import SidebarNavItems from "$lib/components/navigation/sidebar-nav-items.svelte"; + import SidebarNavMainItems from "$lib/components/navigation/sidebar-nav-main-items.svelte"; import type { SidebarNavItem } from "$lib/config/index.js"; let { items = [] }: { items: SidebarNavItem[] } = $props(); diff --git a/sites/docs/src/lib/components/site-header.svelte b/sites/docs/src/lib/components/site-header.svelte index b7dc5c1a6..562afa17c 100644 --- a/sites/docs/src/lib/components/site-header.svelte +++ b/sites/docs/src/lib/components/site-header.svelte @@ -1,7 +1,7 @@ diff --git a/sites/docs/src/lib/config/navigation.ts b/sites/docs/src/lib/config/navigation.ts index cab281371..59698890e 100644 --- a/sites/docs/src/lib/config/navigation.ts +++ b/sites/docs/src/lib/config/navigation.ts @@ -1,3 +1,12 @@ +import type { Component } from "svelte"; +import Sticker from "phosphor-svelte/lib/Sticker"; +import CodeBlock from "phosphor-svelte/lib/CodeBlock"; +import Compass from "phosphor-svelte/lib/Compass"; +import Palette from "phosphor-svelte/lib/Palette"; +import CalendarBlank from "phosphor-svelte/lib/CalendarBlank"; +import CableCar from "phosphor-svelte/lib/CableCar"; +import Leaf from "phosphor-svelte/lib/Leaf"; +import Joystick from "phosphor-svelte/lib/Joystick"; import { allComponentDocs, allTypeHelperDocs, @@ -10,6 +19,7 @@ export type NavItem = { disabled?: boolean; external?: boolean; label?: string; + icon?: Component; }; export type SidebarNavItem = NavItem & { @@ -97,36 +107,49 @@ export const navigation: Navigation = { title: "Introduction", href: "/docs/introduction", items: [], + icon: Sticker, }, { title: "Getting Started", href: "/docs/getting-started", items: [], + icon: Compass, }, { title: "Delegation", href: "/docs/delegation", items: [], + icon: CodeBlock, }, { title: "Ref", href: "/docs/ref", items: [], + icon: Leaf, }, { title: "Transitions", href: "/docs/transitions", items: [], + icon: CableCar, }, { title: "Styling", href: "/docs/styling", items: [], + icon: Palette, }, { title: "Dates", href: "/docs/dates", items: [], + icon: CalendarBlank, + }, + { + title: "Controlled State", + href: "/docs/controlled-state", + items: [], + icon: Joystick, }, ], }, diff --git a/sites/docs/src/lib/content/api-reference/accordion.ts b/sites/docs/src/lib/content/api-reference/accordion.ts index a8433e5c9..e231ecad5 100644 --- a/sites/docs/src/lib/content/api-reference/accordion.ts +++ b/sites/docs/src/lib/content/api-reference/accordion.ts @@ -6,6 +6,7 @@ import type { AccordionTriggerPropsWithoutHTML, } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createCSSVarSchema, @@ -64,6 +65,7 @@ const root = createApiSchema({ description: "A callback function called when the active accordion item value changes. If the `type` is `'single'`, the argument will be a string. If `type` is `'multiple'`, the argument will be an array of strings.", }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ description: "Whether or not the accordion is disabled. When disabled, the accordion cannot be interacted with.", diff --git a/sites/docs/src/lib/content/api-reference/alert-dialog.ts b/sites/docs/src/lib/content/api-reference/alert-dialog.ts index 2055854ba..e2f1485d2 100644 --- a/sites/docs/src/lib/content/api-reference/alert-dialog.ts +++ b/sites/docs/src/lib/content/api-reference/alert-dialog.ts @@ -19,6 +19,7 @@ import { import { childrenSnippet, + controlledOpenProp, createApiSchema, createBooleanProp, createFunctionProp, @@ -47,6 +48,7 @@ const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback function called when the open state changes.", }), + controlledOpen: controlledOpenProp, children: childrenSnippet(), }, }); diff --git a/sites/docs/src/lib/content/api-reference/button.ts b/sites/docs/src/lib/content/api-reference/button.ts index 30fe45878..ff93a74c0 100644 --- a/sites/docs/src/lib/content/api-reference/button.ts +++ b/sites/docs/src/lib/content/api-reference/button.ts @@ -1,11 +1,11 @@ import type { ButtonPropsWithoutHTML } from "bits-ui"; -import { createApiSchema, refProp } from "./helpers.js"; +import { childrenSnippet, createApiSchema, refProp } from "./helpers.js"; import * as C from "$lib/content/constants.js"; export const root = createApiSchema({ title: "Root", description: - "A special button component that can receive Melt UI builders for use with the `asChild` prop.", + "A component that can switch between a button and an anchor tag based on the `href`/`type` props.", props: { href: { type: C.STRING, @@ -13,6 +13,7 @@ export const root = createApiSchema({ "An optional prop that when passed converts the button into an anchor tag.", }, ref: refProp({ elType: "HTMLButtonElement" }), + children: childrenSnippet(), }, dataAttributes: [ { diff --git a/sites/docs/src/lib/content/api-reference/calendar.ts b/sites/docs/src/lib/content/api-reference/calendar.ts index c987f099d..e4b0fa1f0 100644 --- a/sites/docs/src/lib/content/api-reference/calendar.ts +++ b/sites/docs/src/lib/content/api-reference/calendar.ts @@ -22,6 +22,8 @@ import { } from "./extended-types/shared/index.js"; import { + controlledPlaceholderProp, + controlledValueProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -91,6 +93,7 @@ export const root = createApiSchema({ definition: CalendarOnValueChangeProp, description: "A function that is called when the selected date changes.", }), + controlledValue: controlledValueProp, placeholder: { type: dateValueProp, description: @@ -100,6 +103,7 @@ export const root = createApiSchema({ definition: OnPlaceholderChangeProp, description: "A function that is called when the placeholder date changes.", }), + controlledPlaceholder: controlledPlaceholderProp, pagedNavigation: createBooleanProp({ description: "Whether or not to use paged navigation for the calendar. Paged navigation causes the previous and next buttons to navigate by the number of months displayed at once, rather than by one month.", diff --git a/sites/docs/src/lib/content/api-reference/checkbox.ts b/sites/docs/src/lib/content/api-reference/checkbox.ts index b16e9fd60..72390718e 100644 --- a/sites/docs/src/lib/content/api-reference/checkbox.ts +++ b/sites/docs/src/lib/content/api-reference/checkbox.ts @@ -1,5 +1,6 @@ import type { CheckboxRootPropsWithoutHTML } from "bits-ui"; import { + controlledCheckedProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -34,6 +35,7 @@ export const root = createApiSchema({ description: "A callback that is fired when the checkbox button's checked state changes.", }), + controlledChecked: controlledCheckedProp, disabled: createBooleanProp({ default: C.FALSE, description: diff --git a/sites/docs/src/lib/content/api-reference/collapsible.ts b/sites/docs/src/lib/content/api-reference/collapsible.ts index ab69dae34..8cc750a84 100644 --- a/sites/docs/src/lib/content/api-reference/collapsible.ts +++ b/sites/docs/src/lib/content/api-reference/collapsible.ts @@ -4,6 +4,7 @@ import type { CollapsibleTriggerPropsWithoutHTML, } from "bits-ui"; import { + controlledOpenProp, createApiSchema, createBooleanProp, createCSSVarSchema, @@ -30,15 +31,17 @@ export const root = createApiSchema({ "The open state of the collapsible. The content will be visible when this is true, and hidden when it's false.", bindable: true, }), + onOpenChange: createFunctionProp({ + definition: OnOpenChangeProp, + description: "A callback that is fired when the collapsible's open state changes.", + }), + controlledOpen: controlledOpenProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the collapsible is disabled. This prevents the user from interacting with it.", }), - onOpenChange: createFunctionProp({ - definition: OnOpenChangeProp, - description: "A callback that is fired when the collapsible's open state changes.", - }), + ...withChildProps({ elType: "HTMLDivElement" }), }, dataAttributes: [ diff --git a/sites/docs/src/lib/content/api-reference/combobox.ts b/sites/docs/src/lib/content/api-reference/combobox.ts index c1d7bb3c6..5c215e878 100644 --- a/sites/docs/src/lib/content/api-reference/combobox.ts +++ b/sites/docs/src/lib/content/api-reference/combobox.ts @@ -23,6 +23,8 @@ import { ComboboxScrollAlignmentProp } from "./extended-types/combobox/index.js" import { arrowProps, childrenSnippet, + controlledOpenProp, + controlledValueProp, createApiSchema, createBooleanProp, createCSSVarSchema, @@ -75,6 +77,7 @@ export const root = createApiSchema({ description: "A callback that is fired when the combobox value changes. When the type is `'single'`, the argument will be a string. When the type is `'multiple'`, the argument will be an array of strings.", }), + controlledValue: controlledValueProp, open: createBooleanProp({ default: C.FALSE, description: "The open state of the combobox menu.", @@ -84,6 +87,7 @@ export const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback that is fired when the combobox menu's open state changes.", }), + controlledOpen: controlledOpenProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the combobox component is disabled.", @@ -315,8 +319,8 @@ export const groupHeading = createApiSchema({ definition: OnStringValueChangeProp, description: "A callback that is fired when the command value changes.", }), + controlledValue: controlledValueProp, label: createStringProp({ description: "An accessible label for the command menu. This is not visible and is only used for screen readers.", diff --git a/sites/docs/src/lib/content/api-reference/date-field.ts b/sites/docs/src/lib/content/api-reference/date-field.ts index 4f7114149..b99ba7c5e 100644 --- a/sites/docs/src/lib/content/api-reference/date-field.ts +++ b/sites/docs/src/lib/content/api-reference/date-field.ts @@ -6,6 +6,8 @@ import type { } from "bits-ui"; import { childrenSnippet, + controlledPlaceholderProp, + controlledValueProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -43,6 +45,7 @@ export const root = createApiSchema({ definition: OnDateValueChangeProp, description: "A function that is called when the selected date changes.", }), + controlledValue: controlledValueProp, placeholder: { type: dateValueProp, description: @@ -53,7 +56,7 @@ export const root = createApiSchema({ definition: OnPlaceholderChangeProp, description: "A function that is called when the placeholder date changes.", }), - + controlledPlaceholder: controlledPlaceholderProp, required: createBooleanProp({ description: "Whether or not the date field is required.", default: C.FALSE, diff --git a/sites/docs/src/lib/content/api-reference/date-picker.ts b/sites/docs/src/lib/content/api-reference/date-picker.ts index 4ace0898e..319acc493 100644 --- a/sites/docs/src/lib/content/api-reference/date-picker.ts +++ b/sites/docs/src/lib/content/api-reference/date-picker.ts @@ -20,6 +20,9 @@ import { import { input as dateFieldInput, root as dateFieldRoot, label, segment } from "./date-field.js"; import { childrenSnippet, + controlledOpenProp, + controlledPlaceholderProp, + controlledValueProp, createApiSchema, createBooleanProp, createFunctionProp, @@ -34,6 +37,7 @@ export const root = createApiSchema({ props: { value: calendarRoot.props!.value, onValueChange: calendarRoot.props!.onValueChange, + controlledValue: controlledValueProp, open: createBooleanProp({ default: C.FALSE, description: "The open state of the popover content.", @@ -43,7 +47,10 @@ export const root = createApiSchema({ definition: "(open: boolean) => void", description: "A callback that fires when the open state changes.", }), - + controlledOpen: controlledOpenProp, + placeholder: calendarRoot.props!.placeholder, + onPlaceholderChange: calendarRoot.props!.onPlaceholderChange, + controlledPlaceholder: controlledPlaceholderProp, isDateUnavailable: dateFieldRoot.props!.isDateUnavailable, isDateDisabled: calendarRoot.props!.isDateDisabled, required: dateFieldRoot.props!.required, @@ -54,8 +61,6 @@ export const root = createApiSchema({ default: C.TRUE, description: "Whether or not to close the popover when a date is selected.", }, - placeholder: calendarRoot.props!.placeholder, - onPlaceholderChange: calendarRoot.props!.onPlaceholderChange, pagedNavigation: calendarRoot.props!.pagedNavigation, preventDeselect: calendarRoot.props!.preventDeselect, weekStartsOn: calendarRoot.props!.weekStartsOn, diff --git a/sites/docs/src/lib/content/api-reference/date-range-field.ts b/sites/docs/src/lib/content/api-reference/date-range-field.ts index da8bafb66..4fba6fddd 100644 --- a/sites/docs/src/lib/content/api-reference/date-range-field.ts +++ b/sites/docs/src/lib/content/api-reference/date-range-field.ts @@ -5,6 +5,8 @@ import type { DateRangeFieldSegmentPropsWithoutHTML, } from "bits-ui"; import { + controlledPlaceholderProp, + controlledValueProp, createApiSchema, createDataAttrSchema, createEnumProp, @@ -36,8 +38,10 @@ export const root = createApiSchema({ definition: DateOnRangeChangeProp, description: "A function that is called when the selected date changes.", }), + controlledValue: dateFieldRoot.props!.controlledValue, placeholder: dateFieldRoot.props!.placeholder, onPlaceholderChange: dateFieldRoot.props!.onPlaceholderChange, + controlledPlaceholder: dateFieldRoot.props!.controlledPlaceholder, isDateUnavailable: dateFieldRoot.props!.isDateUnavailable, minValue: dateFieldRoot.props!.minValue, maxValue: dateFieldRoot.props!.maxValue, diff --git a/sites/docs/src/lib/content/api-reference/date-range-picker.ts b/sites/docs/src/lib/content/api-reference/date-range-picker.ts index 55bd935de..cb41d369b 100644 --- a/sites/docs/src/lib/content/api-reference/date-range-picker.ts +++ b/sites/docs/src/lib/content/api-reference/date-range-picker.ts @@ -30,6 +30,10 @@ const root = createApiSchema({ props: { value: rangeFieldRoot.props!.value, onValueChange: rangeFieldRoot.props!.onValueChange, + controlledValue: rangeFieldRoot.props!.controlledValue, + placeholder: rangeFieldRoot.props!.placeholder, + onPlaceholderChange: rangeFieldRoot.props!.onPlaceholderChange, + controlledPlaceholder: rangeFieldRoot.props!.controlledPlaceholder, readonlySegments: rangeFieldRoot.props!.readonlySegments, isDateUnavailable: rangeFieldRoot.props!.isDateUnavailable, minValue: rangeFieldRoot.props!.minValue, @@ -41,8 +45,6 @@ const root = createApiSchema({ disabled: rangeFieldRoot.props!.disabled, readonly: rangeFieldRoot.props!.readonly, required: rangeFieldRoot.props!.required, - placeholder: rangeFieldRoot.props!.placeholder, - onPlaceholderChange: rangeFieldRoot.props!.onPlaceholderChange, closeOnRangeSelect: createBooleanProp({ default: C.TRUE, description: "Whether or not to close the popover when a date range is selected.", @@ -58,6 +60,7 @@ const root = createApiSchema({ numberOfMonths: rangeCalendarRoot.props!.numberOfMonths, open: datePickerRoot.props!.open, onOpenChange: datePickerRoot.props!.onOpenChange, + controlledOpen: datePickerRoot.props!.controlledOpen, onEndValueChange: rangeFieldRoot.props!.onEndValueChange, onStartValueChange: rangeFieldRoot.props!.onStartValueChange, ...withChildProps({ elType: "HTMLDivElement" }), diff --git a/sites/docs/src/lib/content/api-reference/dialog.ts b/sites/docs/src/lib/content/api-reference/dialog.ts index 4b06b4130..205164cab 100644 --- a/sites/docs/src/lib/content/api-reference/dialog.ts +++ b/sites/docs/src/lib/content/api-reference/dialog.ts @@ -10,6 +10,7 @@ import type { } from "bits-ui"; import { childrenSnippet, + controlledOpenProp, createApiSchema, createBooleanProp, createFunctionProp, @@ -50,6 +51,7 @@ export const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback function called when the open state changes.", }), + controlledOpen: controlledOpenProp, children: childrenSnippet(), }, }); diff --git a/sites/docs/src/lib/content/api-reference/helpers.ts b/sites/docs/src/lib/content/api-reference/helpers.ts index 94ee8afc3..dac1d5eb1 100644 --- a/sites/docs/src/lib/content/api-reference/helpers.ts +++ b/sites/docs/src/lib/content/api-reference/helpers.ts @@ -571,3 +571,39 @@ export const valueDateRangeChangeFn: PropSchema = createFunctionProp({ definition: DateOnRangeChangeProp, description: "A function that is called when the selected date range changes.", }); + +export const controlledValueProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the value is controlled or not. If `true`, the component will not update the value state internally, instead it will call `onValueChange` when it would have otherwise, and it is up to you to update the `value` prop that is passed to the component.", +}); + +export const controlledPlaceholderProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the placeholder is controlled or not. If `true`, the component will not update the placeholder state internally, instead it will call `onPlaceholderChange` when it would have otherwise, and it is up to you to update the `value` prop that is passed to the component.", +}); + +export const controlledOpenProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the open state is controlled or not. If `true`, the component will not update the open state internally, instead it will call `onOpenChange` when it would have otherwise, and it is up to you to update the `open` prop that is passed to the component.", +}); + +export const controlledCheckedProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the checked state is controlled or not. If `true`, the component will not update the checked state internally, instead it will call `onCheckedChange` when it would have otherwise, and it is up to you to update the `checked` prop that is passed to the component.", +}); + +export const controlledPressedProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the pressed state is controlled or not. If `true`, the component will not update the pressed state internally, instead it will call `onPressedChange` when it would have otherwise, and it is up to you to update the `pressed` prop that is passed to the component.", +}); + +export const controlledPageProp = createBooleanProp({ + default: C.FALSE, + description: + "Whether or not the page state is controlled or not. If `true`, the component will not update the page state internally, instead it will call `onPageChange` when it would have otherwise, and it is up to you to update the `page` prop that is passed to the component.", +}); diff --git a/sites/docs/src/lib/content/api-reference/link-preview.ts b/sites/docs/src/lib/content/api-reference/link-preview.ts index 71c61e67e..a833096ce 100644 --- a/sites/docs/src/lib/content/api-reference/link-preview.ts +++ b/sites/docs/src/lib/content/api-reference/link-preview.ts @@ -8,6 +8,7 @@ import type { import { arrowProps, childrenSnippet, + controlledOpenProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -43,6 +44,7 @@ export const root = createApiSchema({ definition: "(open: boolean) => void", description: "A callback that fires when the open state changes.", }), + controlledOpen: controlledOpenProp, openDelay: createNumberProp({ default: "700", description: diff --git a/sites/docs/src/lib/content/api-reference/listbox.ts b/sites/docs/src/lib/content/api-reference/listbox.ts index 5f61f1483..dcb253ea2 100644 --- a/sites/docs/src/lib/content/api-reference/listbox.ts +++ b/sites/docs/src/lib/content/api-reference/listbox.ts @@ -25,6 +25,8 @@ import { ComboboxScrollAlignmentProp } from "./extended-types/combobox/index.js" import { arrowProps, childrenSnippet, + controlledOpenProp, + controlledValueProp, createApiSchema, createBooleanProp, createCSSVarSchema, @@ -77,6 +79,7 @@ export const root = createApiSchema({ description: "A callback that is fired when the listbox value changes. When the type is `'single'`, the argument will be a string. When the type is `'multiple'`, the argument will be an array of strings.", }), + controlledValue: controlledValueProp, open: createBooleanProp({ default: C.FALSE, description: "The open state of the listbox menu.", @@ -86,6 +89,7 @@ export const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback that is fired when the listbox menu's open state changes.", }), + controlledOpen: controlledOpenProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the listbox component is disabled.", @@ -329,12 +333,12 @@ export const group = createApiSchema({ export const groupHeading = createApiSchema({ title: "GroupHeading", description: - "A label for the parent listbox group. This is used to describe a group of related listbox items.", + "A heading for the parent listbox group. This is used to describe a group of related listbox items.", props: withChildProps({ elType: "HTMLDivElement" }), dataAttributes: [ createDataAttrSchema({ - name: "listbox-group-label", - description: "Present on the group label element.", + name: "listbox-group-heading", + description: "Present on the group heading element.", }), ], }); diff --git a/sites/docs/src/lib/content/api-reference/menu.ts b/sites/docs/src/lib/content/api-reference/menu.ts index 39a49558f..3de81369e 100644 --- a/sites/docs/src/lib/content/api-reference/menu.ts +++ b/sites/docs/src/lib/content/api-reference/menu.ts @@ -17,6 +17,9 @@ import type { import { arrowProps, childrenSnippet, + controlledCheckedProp, + controlledOpenProp, + controlledValueProp, createBooleanProp, createFunctionProp, createStringProp, @@ -72,6 +75,7 @@ const props = { definition: OnOpenChangeProp, description: "A callback that is fired when the menu's open state changes.", }), + controlledOpen: controlledOpenProp, dir: dirProp, children: childrenSnippet(), } satisfies PropObj; @@ -86,6 +90,7 @@ const subProps = { definition: OnOpenChangeProp, description: "A callback that is fired when the submenu's open state changes.", }), + controlledOpen: controlledOpenProp, children: childrenSnippet(), } satisfies PropObj; @@ -151,6 +156,7 @@ const checkboxItemProps = { description: "A callback that is fired when the checkbox menu item's checked state changes.", }), + controlledChecked: controlledCheckedProp, ...omit(sharedItemProps, "child", "children"), ...withChildProps({ elType: "HTMLDivElement", @@ -168,6 +174,7 @@ const radioGroupProps = { definition: OnStringValueChangeProp, description: "A callback that is fired when the radio group's value changes.", }), + controlledValue: controlledValueProp, ...withChildProps({ elType: "HTMLDivElement" }), } satisfies PropObj; @@ -293,7 +300,7 @@ const groupAttrs: DataAttrs = [ const labelAttrs: DataAttrs = [ { name: "menu-group-heading", - description: "Present on the group label element.", + description: "Present on the group heading element.", }, ]; diff --git a/sites/docs/src/lib/content/api-reference/menubar.ts b/sites/docs/src/lib/content/api-reference/menubar.ts index 8fd5494f3..1ac3c2f1d 100644 --- a/sites/docs/src/lib/content/api-reference/menubar.ts +++ b/sites/docs/src/lib/content/api-reference/menubar.ts @@ -18,6 +18,7 @@ import type { MenubarTriggerPropsWithoutHTML, } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createFunctionProp, @@ -41,6 +42,7 @@ export const root = createApiSchema({ definition: OnStringValueChangeProp, description: "A callback function called when the active menu value changes.", }), + controlledValue: controlledValueProp, dir: dirProp, loop: createBooleanProp({ default: C.TRUE, diff --git a/sites/docs/src/lib/content/api-reference/pagination.ts b/sites/docs/src/lib/content/api-reference/pagination.ts index 283f58914..2554b8361 100644 --- a/sites/docs/src/lib/content/api-reference/pagination.ts +++ b/sites/docs/src/lib/content/api-reference/pagination.ts @@ -6,6 +6,7 @@ import type { } from "bits-ui"; import { pageItemProp } from "./extended-types/index.js"; import { + controlledPageProp, createApiSchema, createBooleanProp, createEnumProp, @@ -19,6 +20,16 @@ export const root = createApiSchema({ title: "Root", description: "The root pagination component which contains all other pagination components.", props: { + page: createNumberProp({ + description: + "The selected page. You can bind this to a variable to control the selected page from outside the component.", + bindable: true, + }), + onPageChange: createFunctionProp({ + definition: "(page: number) => void", + description: "A function called when the selected page changes.", + }), + controlledPage: controlledPageProp, count: createNumberProp({ description: "The total number of items.", required: true, @@ -31,15 +42,7 @@ export const root = createApiSchema({ description: "The number of page triggers to show on either side of the current page.", default: "1", }), - page: createNumberProp({ - description: - "The selected page. You can bind this to a variable to control the selected page from outside the component.", - bindable: true, - }), - onPageChange: createFunctionProp({ - definition: "(page: number) => void", - description: "A function called when the selected page changes.", - }), + loop: createBooleanProp({ default: C.FALSE, description: diff --git a/sites/docs/src/lib/content/api-reference/pin-input.ts b/sites/docs/src/lib/content/api-reference/pin-input.ts index 71a825996..41791c2c5 100644 --- a/sites/docs/src/lib/content/api-reference/pin-input.ts +++ b/sites/docs/src/lib/content/api-reference/pin-input.ts @@ -9,6 +9,7 @@ import { } from "./extended-types/pin-input/index.js"; import { OnStringValueChangeProp } from "./extended-types/shared/index.js"; import { + controlledValueProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -33,6 +34,7 @@ const root = createApiSchema({ description: "A callback function that is called when the value of the input changes.", definition: OnStringValueChangeProp, }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the pin input is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/popover.ts b/sites/docs/src/lib/content/api-reference/popover.ts index e203facf9..5255dd4f7 100644 --- a/sites/docs/src/lib/content/api-reference/popover.ts +++ b/sites/docs/src/lib/content/api-reference/popover.ts @@ -6,9 +6,11 @@ import type { PopoverRootPropsWithoutHTML, PopoverTriggerPropsWithoutHTML, } from "bits-ui"; +import { OnOpenChangeProp } from "./extended-types/shared/index.js"; import { arrowProps, childrenSnippet, + controlledOpenProp, createApiSchema, createBooleanProp, createEnumDataAttr, @@ -41,9 +43,10 @@ export const root = createApiSchema({ bindable: true, }), onOpenChange: createFunctionProp({ - definition: "(open: boolean) => void", + definition: OnOpenChangeProp, description: "A callback that fires when the open state changes.", }), + controlledOpen: controlledOpenProp, children: childrenSnippet(), }, }); diff --git a/sites/docs/src/lib/content/api-reference/radio-group.ts b/sites/docs/src/lib/content/api-reference/radio-group.ts index 01a629fcb..b44a08bcc 100644 --- a/sites/docs/src/lib/content/api-reference/radio-group.ts +++ b/sites/docs/src/lib/content/api-reference/radio-group.ts @@ -1,5 +1,6 @@ import type { RadioGroupItemPropsWithoutHTML, RadioGroupRootPropsWithoutHTML } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createEnumProp, @@ -24,6 +25,7 @@ export const root = createApiSchema({ definition: "(value: string | undefined) => void", description: "A callback that is fired when the radio group's value changes.", }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ default: C.FALSE, description: diff --git a/sites/docs/src/lib/content/api-reference/range-calendar.ts b/sites/docs/src/lib/content/api-reference/range-calendar.ts index 5558fc383..cd04b6de0 100644 --- a/sites/docs/src/lib/content/api-reference/range-calendar.ts +++ b/sites/docs/src/lib/content/api-reference/range-calendar.ts @@ -32,8 +32,10 @@ export const root = createApiSchema({ props: { value: valueDateRangeProp, onValueChange: valueDateRangeChangeFn, + controlledValue: calendarRoot.props!.controlledValue, placeholder: calendarRoot.props!.placeholder, onPlaceholderChange: calendarRoot.props!.onPlaceholderChange, + controlledPlaceholder: calendarRoot.props!.controlledPlaceholder, pagedNavigation: calendarRoot.props!.pagedNavigation, preventDeselect: calendarRoot.props!.preventDeselect, weekdayFormat: calendarRoot.props!.weekdayFormat, diff --git a/sites/docs/src/lib/content/api-reference/select.ts b/sites/docs/src/lib/content/api-reference/select.ts index c13c5b5dd..ecea760b8 100644 --- a/sites/docs/src/lib/content/api-reference/select.ts +++ b/sites/docs/src/lib/content/api-reference/select.ts @@ -21,6 +21,8 @@ import { SelectPositionProp } from "./extended-types/select/index.js"; import { arrowProps, childrenSnippet, + controlledOpenProp, + controlledValueProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -53,6 +55,7 @@ export const root = createApiSchema({ definition: OnStringValueChangeProp, description: "A callback that is fired when the select menu's value changes.", }), + controlledValue: controlledValueProp, open: createBooleanProp({ default: C.FALSE, description: "The open state of the select menu.", @@ -62,6 +65,7 @@ export const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback that is fired when the select menu's open state changes.", }), + controlledOpen: controlledOpenProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the select menu is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/slider.ts b/sites/docs/src/lib/content/api-reference/slider.ts index 633acb1dc..10476f17e 100644 --- a/sites/docs/src/lib/content/api-reference/slider.ts +++ b/sites/docs/src/lib/content/api-reference/slider.ts @@ -5,6 +5,7 @@ import type { SliderTickPropsWithoutHTML, } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createEnumProp, @@ -35,6 +36,7 @@ const root = createApiSchema({ description: "A callback function called when the user finishes dragging the thumb and the value changes. This is different than the `onValueChange` callback because it waits until the user stops dragging before calling the callback, where the `onValueChange` callback is called immediately after the user starts dragging.", }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the switch is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/switch.ts b/sites/docs/src/lib/content/api-reference/switch.ts index cbf53fd22..c6481f5ae 100644 --- a/sites/docs/src/lib/content/api-reference/switch.ts +++ b/sites/docs/src/lib/content/api-reference/switch.ts @@ -1,5 +1,6 @@ import type { SwitchRootPropsWithoutHTML, SwitchThumbPropsWithoutHTML } from "bits-ui"; import { + controlledCheckedProp, createApiSchema, createBooleanProp, createDataAttrSchema, @@ -33,6 +34,7 @@ const root = createApiSchema({ definition: "(checked: boolean) => void", description: "A callback function called when the checked state of the switch changes.", }), + controlledChecked: controlledCheckedProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the switch is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/tabs.ts b/sites/docs/src/lib/content/api-reference/tabs.ts index 8334cf1f1..d56190d9d 100644 --- a/sites/docs/src/lib/content/api-reference/tabs.ts +++ b/sites/docs/src/lib/content/api-reference/tabs.ts @@ -6,6 +6,7 @@ import type { } from "bits-ui"; import { OnStringValueChangeProp } from "./extended-types/shared/index.js"; import { + controlledValueProp, createApiSchema, createBooleanProp, createEnumProp, @@ -28,6 +29,7 @@ const root = createApiSchema({ definition: OnStringValueChangeProp, description: "A callback function called when the active tab value changes.", }), + controlledValue: controlledValueProp, activationMode: createEnumProp({ options: ["automatic", "manual"], description: diff --git a/sites/docs/src/lib/content/api-reference/toggle-group.ts b/sites/docs/src/lib/content/api-reference/toggle-group.ts index 103b3d726..7dfaadfb4 100644 --- a/sites/docs/src/lib/content/api-reference/toggle-group.ts +++ b/sites/docs/src/lib/content/api-reference/toggle-group.ts @@ -1,5 +1,6 @@ import type { ToggleGroupItemPropsWithoutHTML, ToggleGroupRootPropsWithoutHTML } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createEnumProp, @@ -32,6 +33,7 @@ const root = createApiSchema({ description: "A callback function called when the value of the toggle group changes. The type of the value is dependent on the type of the toggle group.", }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the switch is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/toggle.ts b/sites/docs/src/lib/content/api-reference/toggle.ts index ec8ec4150..ebf87aac2 100644 --- a/sites/docs/src/lib/content/api-reference/toggle.ts +++ b/sites/docs/src/lib/content/api-reference/toggle.ts @@ -1,5 +1,6 @@ import type { ToggleRootPropsWithoutHTML } from "bits-ui"; import { + controlledPressedProp, createApiSchema, createBooleanProp, createFunctionProp, @@ -21,6 +22,7 @@ const root = createApiSchema({ definition: "(checked: boolean) => void", description: "A callback function called when the pressed state of the toggle changes.", }), + controlledPressed: controlledPressedProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the switch is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/toolbar.ts b/sites/docs/src/lib/content/api-reference/toolbar.ts index e5b29ca4a..5d9d729c6 100644 --- a/sites/docs/src/lib/content/api-reference/toolbar.ts +++ b/sites/docs/src/lib/content/api-reference/toolbar.ts @@ -6,6 +6,7 @@ import type { ToolbarRootPropsWithoutHTML, } from "bits-ui"; import { + controlledValueProp, createApiSchema, createBooleanProp, createEnumProp, @@ -95,6 +96,7 @@ const group = createApiSchema({ definition: union("(value: string) => void", "(value: string[]) => void"), description: "A callback function called when the value changes.", }), + controlledValue: controlledValueProp, disabled: createBooleanProp({ default: C.FALSE, description: "Whether or not the switch is disabled.", diff --git a/sites/docs/src/lib/content/api-reference/tooltip.ts b/sites/docs/src/lib/content/api-reference/tooltip.ts index b300a9e1a..4729be3bf 100644 --- a/sites/docs/src/lib/content/api-reference/tooltip.ts +++ b/sites/docs/src/lib/content/api-reference/tooltip.ts @@ -14,6 +14,7 @@ import { import { arrowProps, childrenSnippet, + controlledOpenProp, createApiSchema, createBooleanProp, createEnumDataAttr, @@ -99,6 +100,7 @@ export const root = createApiSchema({ definition: OnOpenChangeProp, description: "A callback that fires when the open state changes.", }), + controlledOpen: controlledOpenProp, disabled, delayDuration, disableHoverableContent, diff --git a/sites/docs/src/routes/(main)/+layout.svelte b/sites/docs/src/routes/(main)/+layout.svelte index 3572fca81..932f32ebd 100644 --- a/sites/docs/src/routes/(main)/+layout.svelte +++ b/sites/docs/src/routes/(main)/+layout.svelte @@ -3,13 +3,11 @@ import { ModeWatcher } from "mode-watcher"; import { dev } from "$app/environment"; import { page } from "$app/stores"; - import { - Metadata, - SidebarNav, - SiteHeader, - TableOfContents, - TailwindIndicator, - } from "$lib/components/index.js"; + import Metadata from "$lib/components/metadata.svelte"; + import SiteHeader from "$lib/components/site-header.svelte"; + import TailwindIndicator from "$lib/components/tailwind-indicator.svelte"; + import TableOfContents from "$lib/components/toc/table-of-contents.svelte"; + import SidebarNav from "$lib/components/navigation/sidebar-nav.svelte"; import { navigation } from "$lib/config/index.js"; import { cn } from "$lib/utils/index.js"; import "$lib/styles/app.postcss"; diff --git a/sites/docs/src/routes/(main)/+page.svelte b/sites/docs/src/routes/(main)/+page.svelte index e502dde10..61a17c4a0 100644 --- a/sites/docs/src/routes/(main)/+page.svelte +++ b/sites/docs/src/routes/(main)/+page.svelte @@ -1,5 +1,7 @@ diff --git a/sites/docs/src/routes/(main)/docs/[...slug]/+page.svelte b/sites/docs/src/routes/(main)/docs/[...slug]/+page.svelte index 5034aa8df..2a095611f 100644 --- a/sites/docs/src/routes/(main)/docs/[...slug]/+page.svelte +++ b/sites/docs/src/routes/(main)/docs/[...slug]/+page.svelte @@ -1,6 +1,8 @@