diff --git a/TODO.md b/TODO.md index cd3a98d70..bdfecaf3c 100644 --- a/TODO.md +++ b/TODO.md @@ -10,3 +10,44 @@ These are outstanding tasks that need to be completed or addressed before the pr - [ ] Determine how we can give users the ability to opt-out of Floating UI styles. - [ ] Now that `Select` is more similar to a native `` element and behavior. I want to have a separate `Listbox` component that doesn't have to be within a "popover-like" component and can just be static on the page, so Listbox isn't a fitting component name. - [ ] Select deselection of items + +## API Reference Audit + +- [ ] Accordion +- [ ] AlertDialog +- [ ] AspectRatio +- [ ] Avatar +- [ ] Button +- [ ] Calendar +- [ ] Checkbox +- [ ] Collapsible +- [ ] Combobox +- [ ] Command +- [ ] ContextMenu +- [ ] DateField +- [ ] DatePicker +- [ ] DateRangeField +- [ ] DateRangePicker +- [ ] Dialog +- [ ] DropdownMenu +- [ ] Label +- [ ] LinkPreview +- [ ] Listbox +- [ ] Menubar +- [ ] NavigationMenu +- [ ] Pagination +- [ ] PinInput +- [ ] Popover +- [ ] Progress +- [ ] RadioGroup +- [ ] RangeCalendar +- [ ] ScrollArea +- [ ] Select +- [ ] Separator +- [ ] Slider +- [ ] Switch +- [ ] Tabs +- [ ] Toggle +- [ ] ToggleGroup +- [ ] Toolbar +- [ ] Tooltip diff --git a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts index 50c12627d..99effd4b9 100644 --- a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts +++ b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts @@ -155,12 +155,13 @@ export class AccordionItemState { useRefById({ id: this.#id, ref: this.#ref, + condition: () => this.isActive, }); } - updateValue() { + updateValue = () => { this.root.toggleItem(this.value.current); - } + }; createTrigger(props: AccordionTriggerStateProps) { return new AccordionTriggerState(props, this); @@ -180,6 +181,7 @@ export class AccordionItemState { id: this.#id.current, "data-state": getDataOpenClosed(this.isActive), "data-disabled": getDataDisabled(this.isDisabled), + "data-orientation": getDataOrientation(this.root.orientation.current), [ACCORDION_ITEM_ATTR]: "", }) as const ); diff --git a/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-action.svelte b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-action.svelte new file mode 100644 index 000000000..68f430448 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-action.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-content.svelte b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-content.svelte index 522b10a1b..ee5d53146 100644 --- a/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-content.svelte +++ b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog-content.svelte @@ -65,7 +65,7 @@ onEscapeKeydown={(e) => { onEscapeKeydown(e); if (e.defaultPrevented) return; - contentState.root.closeDialog(); + contentState.root.handleClose(); }} > { onInteractOutsideStart(e); if (e.defaultPrevented) return; - contentState.root.closeDialog(); + contentState.root.handleClose(); }} > diff --git a/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog.svelte b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog.svelte new file mode 100644 index 000000000..d33aa865c --- /dev/null +++ b/packages/bits-ui/src/lib/bits/alert-dialog/components/alert-dialog.svelte @@ -0,0 +1,30 @@ + + +{@render children?.()} diff --git a/packages/bits-ui/src/lib/bits/alert-dialog/index.ts b/packages/bits-ui/src/lib/bits/alert-dialog/index.ts index a03ecccc7..8aba4d13d 100644 --- a/packages/bits-ui/src/lib/bits/alert-dialog/index.ts +++ b/packages/bits-ui/src/lib/bits/alert-dialog/index.ts @@ -1,6 +1,6 @@ -export { default as Root } from "$lib/bits/dialog/components/dialog.svelte"; +export { default as Root } from "./components/alert-dialog.svelte"; export { default as Title } from "$lib/bits/dialog/components/dialog-title.svelte"; -export { default as Action } from "$lib/bits/dialog/components/dialog-close.svelte"; +export { default as Action } from "./components/alert-dialog-action.svelte"; export { default as Cancel } from "./components/alert-dialog-cancel.svelte"; export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte"; export { default as Content } from "./components/alert-dialog-content.svelte"; diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte new file mode 100644 index 000000000..6f663fc90 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte @@ -0,0 +1,46 @@ + + + + {@render children?.()} + diff --git a/packages/bits-ui/src/lib/bits/context-menu/index.ts b/packages/bits-ui/src/lib/bits/context-menu/index.ts index 7cb79e3fa..8b248d312 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/index.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/index.ts @@ -1,4 +1,4 @@ -export { default as Root } from "$lib/bits/menu/components/menu.svelte"; +export { default as Root } from "./components/context-menu.svelte"; export { default as Sub } from "$lib/bits/menu/components/menu-sub.svelte"; export { default as Item } from "$lib/bits/menu/components/menu-item.svelte"; export { default as Group } from "$lib/bits/menu/components/menu-group.svelte"; @@ -11,6 +11,7 @@ export { default as RadioItem } from "$lib/bits/menu/components/menu-radio-item. export { default as Separator } from "$lib/bits/menu/components/menu-separator.svelte"; export { default as RadioGroup } from "$lib/bits/menu/components/menu-radio-group.svelte"; export { default as SubContent } from "$lib/bits/menu/components/menu-sub-content.svelte"; +export { default as SubContentStatic } from "$lib/bits/menu/components/menu-sub-content-static.svelte"; export { default as SubTrigger } from "$lib/bits/menu/components/menu-sub-trigger.svelte"; export { default as CheckboxItem } from "$lib/bits/menu/components/menu-checkbox-item.svelte"; export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte"; @@ -26,6 +27,7 @@ export type { ContextMenuRadioItemProps as RadioItemProps, ContextMenuSeparatorProps as SeparatorProps, ContextMenuSubContentProps as SubContentProps, + ContextMenuSubContentStaticProps as SubContentStaticProps, ContextMenuSubProps as SubProps, ContextMenuSubTriggerProps as SubTriggerProps, ContextMenuContentProps as ContentProps, diff --git a/packages/bits-ui/src/lib/bits/context-menu/types.ts b/packages/bits-ui/src/lib/bits/context-menu/types.ts index 23544780e..b58e6f3db 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/types.ts @@ -35,6 +35,7 @@ export type { RadioItemProps as ContextMenuRadioItemProps, SeparatorProps as ContextMenuSeparatorProps, SubContentProps as ContextMenuSubContentProps, + SubContentStaticProps as ContextMenuSubContentStaticProps, SubProps as ContextMenuSubProps, SubTriggerProps as ContextMenuSubTriggerProps, PortalProps as ContextMenuPortalProps, @@ -54,5 +55,6 @@ export type { MenuSubPropsWithoutHTML as ContextMenuSubPropsWithoutHTML, MenuSubTriggerPropsWithoutHTML as ContextMenuSubTriggerPropsWithoutHTML, MenuSubContentPropsWithoutHTML as ContextMenuSubContentPropsWithoutHTML, + MenuSubContentStaticPropsWithoutHTML as ContextMenuSubContentStaticPropsWithoutHTML, MenuPortalPropsWithoutHTML as ContextMenuPortalPropsWithoutHTML, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte index be9c6c538..c92484987 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-close.svelte @@ -14,6 +14,7 @@ }: CloseProps = $props(); const closeState = useDialogClose({ + variant: box.with(() => "close"), id: box.with(() => id), ref: box.with( () => ref, diff --git a/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte b/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte index 3c4added5..b19854a9c 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog-content.svelte @@ -57,7 +57,7 @@ onEscapeKeydown={(e) => { onEscapeKeydown(e); if (e.defaultPrevented) return; - contentState.root.closeDialog(); + contentState.root.handleClose(); }} > { onInteractOutside(e); if (e.defaultPrevented) return; - contentState.root.closeDialog(); + contentState.root.handleClose(); }} > 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 cc1dc0896..e0f43f3c8 100644 --- a/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte +++ b/packages/bits-ui/src/lib/bits/dialog/components/dialog.svelte @@ -12,6 +12,7 @@ }: RootProps = $props(); useDialogRoot({ + variant: box.with(() => "dialog"), open: box.with( () => open, (v) => { diff --git a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts index 52f48a868..94df325a2 100644 --- a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts +++ b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts @@ -4,20 +4,31 @@ import { useRefById } from "$lib/internal/useRefById.svelte.js"; import { createContext } from "$lib/internal/createContext.js"; import type { WithRefProps } from "$lib/internal/types.js"; -const CONTENT_ATTR = "data-dialog-content"; -const TITLE_ATTR = "data-dialog-title"; -const TRIGGER_ATTR = "data-dialog-trigger"; -const OVERLAY_ATTR = "data-dialog-overlay"; -const DESCRIPTION_ATTR = "data-dialog-description"; -const CLOSE_ATTR = "data-dialog-close"; -const CANCEL_ATTR = "data-dialog-cancel"; +type DialogVariant = "alert-dialog" | "dialog"; + +function createAttrs(variant: DialogVariant) { + return { + content: `data-${variant}-content`, + trigger: `data-${variant}-trigger`, + overlay: `data-${variant}-overlay`, + title: `data-${variant}-title`, + description: `data-${variant}-description`, + close: `data-${variant}-close`, + cancel: `data-${variant}-cancel`, + action: `data-${variant}-action`, + } as const; +} type DialogRootStateProps = WritableBoxedValues<{ open: boolean; -}>; +}> & + ReadableBoxedValues<{ + variant: DialogVariant; + }>; class DialogRootState { open: DialogRootStateProps["open"]; + variant: DialogRootStateProps["variant"]; triggerNode = $state(null); titleNode = $state(null); contentNode = $state(null); @@ -27,20 +38,22 @@ class DialogRootState { triggerId = $state(undefined); descriptionId = $state(undefined); cancelNode = $state(null); + attrs = $derived.by(() => createAttrs(this.variant.current)); constructor(props: DialogRootStateProps) { this.open = props.open; + this.variant = props.variant; } - openDialog() { + handleOpen = () => { if (this.open.current) return; this.open.current = true; - } + }; - closeDialog() { + handleClose = () => { if (!this.open.current) return; this.open.current = false; - } + }; createTrigger(props: DialogTriggerStateProps) { return new DialogTriggerState(props, this); @@ -101,7 +114,7 @@ class DialogTriggerState { } #onclick = () => { - this.#root.openDialog(); + this.#root.handleOpen(); }; props = $derived.by( @@ -111,23 +124,29 @@ class DialogTriggerState { "aria-haspopup": "dialog", "aria-expanded": getAriaExpanded(this.#root.open.current), "aria-controls": this.#root.contentId, - [TRIGGER_ATTR]: "", + [this.#root.attrs.trigger]: "", onclick: this.#onclick, ...this.#root.sharedProps, }) as const ); } -type DialogCloseStateProps = WithRefProps; +type DialogCloseStateProps = WithRefProps & + ReadableBoxedValues<{ + variant: "action" | "cancel" | "close"; + }>; class DialogCloseState { #id: DialogCloseStateProps["id"]; #ref: DialogCloseStateProps["ref"]; #root: DialogRootState; + #variant: DialogCloseStateProps["variant"]; + #attr = $derived.by(() => this.#root.attrs[this.#variant.current]); constructor(props: DialogCloseStateProps, root: DialogRootState) { this.#root = root; this.#ref = props.ref; this.#id = props.id; + this.#variant = props.variant; useRefById({ id: this.#id, @@ -137,14 +156,14 @@ class DialogCloseState { } #onclick = () => { - this.#root.closeDialog(); + this.#root.handleClose(); }; props = $derived.by( () => ({ id: this.#id.current, - [CLOSE_ATTR]: "", + [this.#attr]: "", onclick: this.#onclick, ...this.#root.sharedProps, }) as const @@ -185,7 +204,7 @@ class DialogTitleState { id: this.#id.current, role: "heading", "aria-level": this.#level.current, - [TITLE_ATTR]: "", + [this.#root.attrs.title]: "", ...this.#root.sharedProps, }) as const ); @@ -218,7 +237,7 @@ class DialogDescriptionState { () => ({ id: this.#id.current, - [DESCRIPTION_ATTR]: "", + [this.#root.attrs.description]: "", ...this.#root.sharedProps, }) as const ); @@ -253,10 +272,10 @@ class DialogContentState { () => ({ id: this.#id.current, - role: "dialog", + role: this.root.variant.current === "alert-dialog" ? "alertdialog" : "dialog", "aria-describedby": this.root.descriptionId, "aria-labelledby": this.root.titleId, - [CONTENT_ATTR]: "", + [this.root.attrs.content]: "", ...this.root.sharedProps, }) as const ); @@ -287,7 +306,7 @@ class DialogOverlayState { () => ({ id: this.#id.current, - [OVERLAY_ATTR]: "", + [this.root.attrs.overlay]: "", ...this.root.sharedProps, }) as const ); @@ -316,14 +335,14 @@ class AlertDialogCancelState { } #onclick = () => { - this.#root.closeDialog(); + this.#root.handleClose(); }; props = $derived.by( () => ({ id: this.#id.current, - [CANCEL_ATTR]: "", + [this.#root.attrs.cancel]: "", onclick: this.#onclick, ...this.#root.sharedProps, }) as const diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts b/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts index 8c76ec14c..ca5ef13d0 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts @@ -11,6 +11,7 @@ export { default as RadioItem } from "$lib/bits/menu/components/menu-radio-item. export { default as Separator } from "$lib/bits/menu/components/menu-separator.svelte"; export { default as RadioGroup } from "$lib/bits/menu/components/menu-radio-group.svelte"; export { default as SubContent } from "$lib/bits/menu/components/menu-sub-content.svelte"; +export { default as SubContentStatic } from "$lib/bits/menu/components/menu-sub-content-static.svelte"; export { default as SubTrigger } from "$lib/bits/menu/components/menu-sub-trigger.svelte"; export { default as CheckboxItem } from "$lib/bits/menu/components/menu-checkbox-item.svelte"; export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte"; @@ -28,6 +29,7 @@ export type { DropdownMenuRadioItemProps as RadioItemProps, DropdownMenuSeparatorProps as SeparatorProps, DropdownMenuSubContentProps as SubContentProps, + DropdownMenuSubContentStaticProps as SubContentStaticProps, DropdownMenuSubProps as SubProps, DropdownMenuSubTriggerProps as SubTriggerProps, DropdownMenuTriggerProps as TriggerProps, diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts b/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts index 0b8df2f24..611e0a0d0 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts @@ -11,6 +11,7 @@ export type { RadioItemProps as DropdownMenuRadioItemProps, SeparatorProps as DropdownMenuSeparatorProps, SubContentProps as DropdownMenuSubContentProps, + SubContentStaticProps as DropdownMenuSubContentStaticProps, SubProps as DropdownMenuSubProps, SubTriggerProps as DropdownMenuSubTriggerProps, TriggerProps as DropdownMenuTriggerProps, @@ -32,6 +33,7 @@ export type { MenuSubPropsWithoutHTML as DropdownMenuSubPropsWithoutHTML, MenuSubTriggerPropsWithoutHTML as DropdownMenuSubTriggerPropsWithoutHTML, MenuSubContentPropsWithoutHTML as DropdownMenuSubContentPropsWithoutHTML, + MenuSubContentStaticPropsWithoutHTML as DropdownMenuSubContentStaticPropsWithoutHTML, MenuTriggerPropsWithoutHTML as DropdownMenuTriggerPropsWithoutHTML, MenuPortalPropsWithoutHTML as DropdownMenuPortalPropsWithoutHTML, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte new file mode 100644 index 000000000..d236d9ffa --- /dev/null +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte @@ -0,0 +1,131 @@ + + + { + onCloseAutoFocusProp(e); + if (e.defaultPrevented) return; + onCloseAutoFocus(e); + }} + onOpenAutoFocus={(e) => { + onOpenAutoFocusProp(e); + if (e.defaultPrevented) return; + onOpenAutoFocus(e); + }} + present={subContentState.parentMenu.open.current || forceMount} + onInteractOutside={(e) => { + onInteractOutside(e); + if (e.defaultPrevented) return; + subContentState.parentMenu.onClose(); + }} + onEscapeKeydown={(e) => { + // TODO: users should be able to cancel this + onEscapeKeydown(e); + if (e.defaultPrevented) return; + subContentState.parentMenu.onClose(); + }} + onFocusOutside={(e) => { + onFocusOutsideProp(e); + if (e.defaultPrevented) return; + // We prevent closing when the trigger is focused to avoid triggering a re-open animation + // on pointer interaction. + if (!isHTMLElement(e.target)) return; + if (e.target.id !== subContentState.parentMenu.triggerNode?.id) { + subContentState.parentMenu.onClose(); + } + }} + preventScroll={false} + {loop} +> + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps)} + {#if child} + {@render child({ props: finalProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte index f95500887..76973c4f5 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte @@ -22,6 +22,9 @@ onEscapeKeydown = noop, interactOutsideBehavior = "defer-otherwise-close", escapeKeydownBehavior = "defer-otherwise-close", + onOpenAutoFocus: onOpenAutoFocusProp = noop, + onCloseAutoFocus: onCloseAutoFocusProp = noop, + onFocusOutside = noop, side = "right", ...restProps }: SubContentProps = $props(); @@ -51,13 +54,13 @@ } } + const dataAttr = $derived(subContentState.parentMenu.root.attrs.subContent); + const mergedProps = $derived( mergeProps(restProps, subContentState.props, { - onOpenAutoFocus, - onCloseAutoFocus, side, onkeydown, - "data-menu-sub-content": "", + [dataAttr]: "", }) ); @@ -80,7 +83,16 @@ {...mergedProps} {interactOutsideBehavior} {escapeKeydownBehavior} - {onOpenAutoFocus} + onCloseAutoFocus={(e) => { + onCloseAutoFocusProp(e); + if (e.defaultPrevented) return; + onCloseAutoFocus(e); + }} + onOpenAutoFocus={(e) => { + onOpenAutoFocusProp(e); + if (e.defaultPrevented) return; + onOpenAutoFocus(e); + }} present={subContentState.parentMenu.open.current || forceMount} onInteractOutside={(e) => { onInteractOutside(e); @@ -94,6 +106,7 @@ subContentState.parentMenu.onClose(); }} onFocusOutside={(e) => { + onFocusOutside(e); if (e.defaultPrevented) return; // We prevent closing when the trigger is focused to avoid triggering a re-open animation // on pointer interaction. 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 f0926ac7a..02f32e218 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu.svelte @@ -10,10 +10,14 @@ dir = "ltr", onOpenChange = noop, controlledOpen = false, + _internal_variant: variant = "dropdown-menu", children, - }: RootProps = $props(); + }: RootProps & { + _internal_variant?: "context-menu" | "dropdown-menu" | "menubar"; + } = $props(); const root = useMenuRoot({ + variant: box.with(() => variant), dir: box.with(() => dir), onClose: () => { if (controlledOpen) { diff --git a/packages/bits-ui/src/lib/bits/menu/index.ts b/packages/bits-ui/src/lib/bits/menu/index.ts index c6367c817..6fe4c9c15 100644 --- a/packages/bits-ui/src/lib/bits/menu/index.ts +++ b/packages/bits-ui/src/lib/bits/menu/index.ts @@ -14,6 +14,7 @@ export { default as Sub } from "./components/menu-sub.svelte"; export { default as SubContent } from "./components/menu-sub-content.svelte"; export { default as SubTrigger } from "./components/menu-sub-trigger.svelte"; export { default as Trigger } from "./components/menu-trigger.svelte"; +export { default as SubContentStatic } from "./components/menu-sub-content-static.svelte"; export type { MenuRootPropsWithoutHTML as RootProps, @@ -23,6 +24,7 @@ export type { MenuTriggerProps as TriggerProps, MenuSubPropsWithoutHTML as SubProps, MenuSubContentProps as SubContentProps, + MenuSubContentStaticProps as SubContentStaticProps, MenuSeparatorProps as SeparatorProps, MenuArrowProps as ArrowProps, MenuCheckboxItemProps as CheckboxItemProps, diff --git a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts index c6934cf8c..5cbdb5a0d 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -37,20 +37,20 @@ import { useRefById } from "$lib/internal/useRefById.svelte.js"; import { isPointerInGraceArea, makeHullFromElements } from "$lib/internal/polygon.js"; import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js"; -const TRIGGER_ATTR = "data-menu-trigger"; -const CONTENT_ATTR = "data-menu-content"; -const ITEM_ATTR = "data-menu-item"; -const SEPARATOR_ATTR = "data-menu-separator"; -const SUB_TRIGGER_ATTR = "data-menu-sub-trigger"; -const CHECKBOX_ITEM_ATTR = "data-menu-checkbox-item"; -const GROUP_ATTR = "data-menu-group"; -const LABEL_ATTR = "data-menu-group-heading"; -const RADIO_GROUP_ATTR = "data-menu-radio-group"; -const RADIO_ITEM_ATTR = "data-menu-radio-item"; -const ARROW_ATTR = "data-menu-arrow"; +// const TRIGGER_ATTR = "data-menu-trigger"; +// const CONTENT_ATTR = "data-menu-content"; +// const ITEM_ATTR = "data-menu-item"; +// const SEPARATOR_ATTR = "data-menu-separator"; +// const SUB_TRIGGER_ATTR = "data-menu-sub-trigger"; +// const CHECKBOX_ITEM_ATTR = "data-menu-checkbox-item"; +// const GROUP_ATTR = "data-menu-group"; +// const LABEL_ATTR = "data-menu-group-heading"; +// const RADIO_GROUP_ATTR = "data-menu-radio-group"; +// const RADIO_ITEM_ATTR = "data-menu-radio-item"; +// const ARROW_ATTR = "data-menu-arrow"; export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger"; -const [setMenuRootContext] = createContext("Menu.Root"); +const [setMenuRootContext, getMenuRootContext] = createContext("Menu.Root"); const [setMenuMenuContext, getMenuMenuContext] = createContext( ["Menu.Root", "Menu.Sub"], @@ -67,20 +67,43 @@ const [setMenuGroupContext, getMenuGroupContext] = createContext< const [setMenuRadioGroupContext, getMenuRadioGroupContext] = createContext("Menu.RadioGroup"); +type MenuVariant = "context-menu" | "dropdown-menu" | "menubar"; + +function createAttrs(variant: MenuVariant) { + return { + trigger: `data-${variant}-trigger`, + content: `data-${variant}-content`, + item: `data-${variant}-item`, + separator: `data-${variant}-separator`, + subTrigger: `data-${variant}-sub-trigger`, + checkboxItem: `data-${variant}-checkbox-item`, + group: `data-${variant}-group`, + groupHeading: `data-${variant}-group-heading`, + radioGroup: `data-${variant}-radio-group`, + radioItem: `data-${variant}-radio-item`, + arrow: `data-${variant}-arrow`, + subContent: `data-${variant}-sub-content`, + } as const; +} + export type MenuRootStateProps = ReadableBoxedValues<{ dir: Direction; + variant: MenuVariant; }> & { onClose: AnyFn; }; class MenuRootState { onClose: MenuRootStateProps["onClose"]; + variant: MenuRootStateProps["variant"]; isUsingKeyboard = box(false); dir: MenuRootStateProps["dir"]; + attrs = $derived.by(() => createAttrs(this.variant.current)); constructor(props: MenuRootStateProps) { this.onClose = props.onClose; this.dir = props.dir; + this.variant = props.variant; $effect(() => { const callbacksToDispose: AnyFn[] = []; @@ -122,6 +145,18 @@ class MenuRootState { createMenu(props: MenuMenuStateProps) { return new MenuMenuState(props, this); } + + createGroup(props: MenuGroupStateProps) { + return new MenuGroupState(props, this); + } + + createSeparator(props: MenuSeparatorStateProps) { + return new MenuSeparatorState(props, this); + } + + createArrow() { + return new MenuArrowState(this); + } } type MenuMenuStateProps = WritableBoxedValues<{ @@ -231,7 +266,7 @@ class MenuContentState { this.#handleTypeaheadSearch = useTypeahead().handleTypeaheadSearch; this.rovingFocusGroup = useRovingFocus({ rootNodeId: this.parentMenu.contentId, - candidateSelector: ITEM_ATTR, + candidateSelector: this.parentMenu.root.attrs.item, loop: this.#loop, orientation: box.with(() => "vertical"), }); @@ -241,7 +276,9 @@ class MenuContentState { const node = this.parentMenu.contentNode; if (!node) return []; const candidates = Array.from( - node.querySelectorAll(`[${ITEM_ATTR}]:not([data-disabled])`) + node.querySelectorAll( + `[${this.parentMenu.root.attrs.item}]:not([data-disabled])` + ) ); return candidates; }; @@ -263,7 +300,8 @@ class MenuContentState { if (!isHTMLElement(target) || !isHTMLElement(currentTarget)) return; const isKeydownInside = - target.closest(`[${CONTENT_ATTR}]`)?.id === this.parentMenu.contentId.current; + target.closest(`[${this.parentMenu.root.attrs.content}]`)?.id === + this.parentMenu.contentId.current; const isModifierKey = e.ctrlKey || e.altKey || e.metaKey; const isCharacterKey = e.key.length === 1; @@ -369,7 +407,7 @@ class MenuContentState { id: this.#id.current, role: "menu", "aria-orientation": getAriaOrientation("vertical"), - [CONTENT_ATTR]: "", + [this.parentMenu.root.attrs.content]: "", "data-state": getDataOpenClosed(this.parentMenu.open.current), onkeydown: this.#onkeydown, onblur: this.#onblur, @@ -475,7 +513,7 @@ class MenuItemSharedState { "aria-disabled": getAriaDisabled(this.disabled.current), "data-disabled": getDataDisabled(this.disabled.current), "data-highlighted": this.#isFocused ? "" : undefined, - [ITEM_ATTR]: "", + [this.content.parentMenu.root.attrs.item]: "", // onpointermove: this.#onpointermove, onpointerleave: this.#onpointerleave, @@ -493,9 +531,11 @@ class MenuItemState { #item: MenuItemSharedState; #onSelect: MenuItemStateProps["onSelect"]; #isPointerDown = $state(false); + root: MenuRootState; constructor(props: MenuItemStateProps, item: MenuItemSharedState) { this.#item = item; + this.root = item.content.parentMenu.root; this.#onSelect = props.onSelect; } @@ -666,7 +706,7 @@ class MenuSubTriggerState { "aria-controls": this.#submenu.open.current ? this.#submenu.contentId.current : undefined, - [SUB_TRIGGER_ATTR]: "", + [this.#submenu.root.attrs.subTrigger]: "", onclick: this.#onclick, onpointermove: this.#onpointermove, onpointerleave: this.#onpointerleave, @@ -707,7 +747,7 @@ class MenuCheckboxItemState { role: "menuitemcheckbox", "aria-checked": getAriaChecked(this.#checked.current), "data-state": getCheckedState(this.#checked.current), - [CHECKBOX_ITEM_ATTR]: "", + [this.#item.root.attrs.checkboxItem]: "", }) as const ); } @@ -718,10 +758,12 @@ class MenuGroupState { #id: MenuGroupStateProps["id"]; #ref: MenuGroupStateProps["ref"]; groupHeadingId = $state(undefined); + root: MenuRootState; - constructor(props: MenuGroupStateProps) { + constructor(props: MenuGroupStateProps, root: MenuRootState) { this.#id = props.id; this.#ref = props.ref; + this.root = root; useRefById({ id: this.#id, @@ -735,7 +777,7 @@ class MenuGroupState { id: this.#id.current, role: "group", "aria-labelledby": this.groupHeadingId, - [GROUP_ATTR]: "", + [this.root.attrs.group]: "", }) as const ); @@ -748,9 +790,9 @@ type MenuGroupHeadingStateProps = WithRefProps; class MenuGroupHeadingState { #id: MenuGroupHeadingStateProps["id"]; #ref: MenuGroupHeadingStateProps["ref"]; - #group: MenuGroupState | MenuRadioGroupState | undefined = undefined; + #group: MenuGroupState | MenuRadioGroupState; - constructor(props: MenuGroupHeadingStateProps, group?: MenuGroupState | MenuRadioGroupState) { + constructor(props: MenuGroupHeadingStateProps, group: MenuGroupState | MenuRadioGroupState) { this.#id = props.id; this.#ref = props.ref; this.#group = group; @@ -759,7 +801,6 @@ class MenuGroupHeadingState { id: this.#id, ref: this.#ref, onRefChange: (node) => { - if (!this.#group) return; this.#group.groupHeadingId = node?.id; }, }); @@ -770,7 +811,7 @@ class MenuGroupHeadingState { ({ id: this.#id.current, role: "group", - [LABEL_ATTR]: "", + [this.#group.root.attrs.groupHeading]: "", }) as const ); } @@ -780,10 +821,12 @@ type MenuSeparatorStateProps = WithRefProps; class MenuSeparatorState { #id: MenuSeparatorStateProps["id"]; #ref: MenuSeparatorStateProps["ref"]; + #root: MenuRootState; - constructor(props: MenuSeparatorStateProps) { + constructor(props: MenuSeparatorStateProps, root: MenuRootState) { this.#id = props.id; this.#ref = props.ref; + this.#root = root; useRefById({ id: this.#id, @@ -796,15 +839,24 @@ class MenuSeparatorState { ({ id: this.#id.current, role: "group", - [SEPARATOR_ATTR]: "", + [this.#root.attrs.separator]: "", }) as const ); } class MenuArrowState { - props = { - [ARROW_ATTR]: "", - } as const; + #root: MenuRootState; + + constructor(root: MenuRootState) { + this.#root = root; + } + + props = $derived.by( + () => + ({ + [this.#root.attrs.arrow]: "", + }) as const + ); } type MenuRadioGroupStateProps = WritableBoxedValues<{ @@ -821,12 +873,14 @@ class MenuRadioGroupState { #ref: MenuRadioGroupStateProps["ref"]; #content: MenuContentState; groupHeadingId = $state(null); + root: MenuRootState; constructor(props: MenuRadioGroupStateProps, content: MenuContentState) { this.value = props.value; this.#id = props.id; this.#ref = props.ref; this.#content = content; + this.root = content.parentMenu.root; useRefById({ id: this.#id, @@ -853,7 +907,7 @@ class MenuRadioGroupState { () => ({ id: this.#id.current, - [RADIO_GROUP_ATTR]: "", + [this.root.attrs.radioGroup]: "", role: "group", "aria-labelledby": this.groupHeadingId, }) as const @@ -896,7 +950,7 @@ class MenuRadioItemState { props = $derived.by( () => ({ - [RADIO_ITEM_ATTR]: "", + [this.#group.root.attrs.radioItem]: "", ...this.#item.props, role: "menuitemradio", "aria-checked": getAriaChecked(this.isChecked), @@ -976,7 +1030,7 @@ class DropdownMenuTriggerState { "aria-controls": this.#ariaControls, "data-disabled": getDataDisabled(this.#disabled.current), "data-state": getDataOpenClosed(this.#parentMenu.open.current), - [TRIGGER_ATTR]: "", + [this.#parentMenu.root.attrs.trigger]: "", // onpointerdown: this.#onpointerdown, onkeydown: this.#onkeydown, @@ -1087,7 +1141,6 @@ class ContextMenuTriggerState { disabled: this.#disabled.current, "data-disabled": getDataDisabled(this.#disabled.current), "data-state": getDataOpenClosed(this.#parentMenu.open.current), - [TRIGGER_ATTR]: "", [CONTEXT_MENU_TRIGGER_ATTR]: "", // onpointerdown: this.#onpointerdown, @@ -1156,19 +1209,17 @@ export function useMenuRadioItem(props: MenuRadioItemStateProps & MenuItemCombin } export function useMenuGroup(props: MenuGroupStateProps) { - return setMenuGroupContext(new MenuGroupState(props)); + return setMenuGroupContext(getMenuRootContext().createGroup(props)); } export function useMenuGroupHeading(props: MenuGroupHeadingStateProps) { - const groupCtx = getMenuGroupContext(null); - if (!groupCtx) return new MenuGroupHeadingState(props); - return groupCtx.createGroupHeading(props); + return getMenuGroupContext().createGroupHeading(props); } export function useMenuSeparator(props: MenuSeparatorStateProps) { - return new MenuSeparatorState(props); + return getMenuRootContext().createSeparator(props); } export function useMenuArrow() { - return new MenuArrowState(); + return getMenuRootContext().createArrow(); } diff --git a/packages/bits-ui/src/lib/bits/menu/types.ts b/packages/bits-ui/src/lib/bits/menu/types.ts index b25becc59..1c7a745ba 100644 --- a/packages/bits-ui/src/lib/bits/menu/types.ts +++ b/packages/bits-ui/src/lib/bits/menu/types.ts @@ -155,12 +155,19 @@ export type MenuSubPropsWithoutHTML = WithChildren<{ }>; export type MenuSubContentPropsWithoutHTML = Expand< - WithChild & _SharedMenuContentProps> + WithChild & _SharedMenuContentProps> >; export type MenuSubContentProps = MenuSubContentPropsWithoutHTML & Without; +export type MenuSubContentStaticPropsWithoutHTML = Expand< + WithChild & _SharedMenuContentProps> +>; + +export type MenuSubContentStaticProps = MenuSubContentStaticPropsWithoutHTML & + Without; + export type MenuSubTriggerPropsWithoutHTML = MenuItemPropsWithoutHTML; export type MenuSubTriggerProps = MenuItemProps; diff --git a/packages/bits-ui/src/lib/bits/menubar/components/menubar-menu.svelte b/packages/bits-ui/src/lib/bits/menubar/components/menubar-menu.svelte index 92c1acfbb..e97ade72e 100644 --- a/packages/bits-ui/src/lib/bits/menubar/components/menubar-menu.svelte +++ b/packages/bits-ui/src/lib/bits/menubar/components/menubar-menu.svelte @@ -18,5 +18,6 @@ if (!open) menuState.root.onMenuClose(); }} dir={menuState.root.dir.current} + _internal_variant="menubar" {...restProps} /> diff --git a/packages/bits-ui/src/lib/bits/menubar/index.ts b/packages/bits-ui/src/lib/bits/menubar/index.ts index 296e3eb89..a2d9d7036 100644 --- a/packages/bits-ui/src/lib/bits/menubar/index.ts +++ b/packages/bits-ui/src/lib/bits/menubar/index.ts @@ -11,6 +11,7 @@ export { default as Arrow } from "$lib/bits/menu/components/menu-arrow.svelte"; export { default as RadioItem } from "$lib/bits/menu/components/menu-radio-item.svelte"; export { default as Separator } from "$lib/bits/menu/components/menu-separator.svelte"; export { default as SubContent } from "$lib/bits/menu/components/menu-sub-content.svelte"; +export { default as SubContentStatic } from "$lib/bits/menu/components/menu-sub-content-static.svelte"; export { default as SubTrigger } from "$lib/bits/menu/components/menu-sub-trigger.svelte"; export { default as RadioGroup } from "$lib/bits/menu/components/menu-radio-group.svelte"; export { default as CheckboxItem } from "$lib/bits/menu/components/menu-checkbox-item.svelte"; @@ -36,4 +37,5 @@ export type { MenuSubTriggerProps as SubTriggerProps, MenuRadioGroupProps as RadioGroupProps, MenuCheckboxItemProps as CheckboxItemProps, + MenuSubContentStaticProps as SubContentStaticProps, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/menubar/types.ts b/packages/bits-ui/src/lib/bits/menubar/types.ts index c7722387c..8e71df01f 100644 --- a/packages/bits-ui/src/lib/bits/menubar/types.ts +++ b/packages/bits-ui/src/lib/bits/menubar/types.ts @@ -82,6 +82,8 @@ export type { MenuSeparatorProps as MenubarSeparatorProps, MenuSubContentPropsWithoutHTML as MenubarSubContentPropsWithoutHTML, MenuSubContentProps as MenubarSubContentProps, + MenuSubContentStaticPropsWithoutHTML as MenubarSubContentStaticPropsWithoutHTML, + MenuSubContentStaticProps as MenubarSubContentStaticProps, MenuSubTriggerPropsWithoutHTML as MenubarSubTriggerPropsWithoutHTML, MenuSubTriggerProps as MenubarSubTriggerProps, MenuSubPropsWithoutHTML as MenubarSubPropsWithoutHTML, diff --git a/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts b/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts index d5f776797..e5922af64 100644 --- a/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts +++ b/packages/bits-ui/src/tests/alert-dialog/alert-dialog.test.ts @@ -64,7 +64,7 @@ describe("alert dialog", () => { for (const part of parts) { const el = getByTestId(part); - expect(el).toHaveAttribute(`data-dialog-${part}`); + expect(el).toHaveAttribute(`data-alert-dialog-${part}`); } }); diff --git a/packages/bits-ui/src/tests/context-menu/context-menu.test.ts b/packages/bits-ui/src/tests/context-menu/context-menu.test.ts index 89497aeb7..90b95fa97 100644 --- a/packages/bits-ui/src/tests/context-menu/context-menu.test.ts +++ b/packages/bits-ui/src/tests/context-menu/context-menu.test.ts @@ -84,13 +84,13 @@ describe("context menu", () => { for (const part of parts) { const el = screen.getByTestId(part); - expect(el).toHaveAttribute(`data-menu-${part}`); + expect(el).toHaveAttribute(`data-context-menu-${part}`); } await user.click(getByTestId("sub-trigger")); const subContent = getByTestId("sub-content"); - expect(subContent).toHaveAttribute(`data-menu-sub-content`); + expect(subContent).toHaveAttribute(`data-context-menu-sub-content`); }); it("should open when right-clicked & respects binding", async () => { diff --git a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts index 3540f4e87..e3ebba542 100644 --- a/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts +++ b/packages/bits-ui/src/tests/dropdown-menu/dropdown-menu.test.ts @@ -91,13 +91,13 @@ describe("dropdown menu", () => { for (const part of parts) { const el = screen.getByTestId(part); - expect(el).toHaveAttribute(`data-menu-${part}`); + expect(el).toHaveAttribute(`data-dropdown-menu-${part}`); } await user.click(getByTestId("sub-trigger")); const subContent = getByTestId("sub-content"); - expect(subContent).toHaveAttribute(`data-menu-sub-content`); + expect(subContent).toHaveAttribute(`data-dropdown-menu-sub-content`); }); it.each(OPEN_KEYS)("should open when %s is pressed & respects binding", async (key) => { diff --git a/sites/docs/content/components/accordion.md b/sites/docs/content/components/accordion.md index 9d9f93b16..563383491 100644 --- a/sites/docs/content/components/accordion.md +++ b/sites/docs/content/components/accordion.md @@ -1,10 +1,10 @@ --- title: Accordion -description: Organizes content into collapsible sections, allowing users to focus on one section at a time. +description: Organizes content into collapsible sections, allowing users to focus on one or more sections at a time. --- @@ -16,7 +16,31 @@ description: Organizes content into collapsible sections, allowing users to focu -## Structure +## Overview + +The Accordion component is a versatile UI element that organizes content into collapsible sections, enabling users to focus on specific information while reducing visual clutter. It's particularly useful for presenting large amounts of related content in a compact, navigable format. + +## Key Features + +- **Customizable Behavior**: Can be configured for single or multiple open sections. +- **Accessibility**: ARIA attributes for screen reader compatibility and keyboard navigation. +- **Transition Support**: CSS variables and data attributes for smooth transitions between states. +- **Flexible State Management**: Supports controlled and uncontrolled state, take control if needed. +- **Compound Component Structure**: Provides a set of subcomponents that work together to create a fully-featured accordion. + +## Component Architecture + +The Accordion component is composed of several subcomponents, each with a specific role: + +- **Root**: The root element that wraps all accordion items and manages the overall state. +- **Item**: Individual sections within the accordion. +- **Trigger**: The button that toggles the visibility of the content. +- **Header**: The title or heading of each item. +- **Content**: The expandable/collapsible body of each item. + +## Component Structure + +Here's an overview of how the Accordion component is structured in code: ```svelte @@ -146,16 +169,19 @@ Use the `bind:value` directive for effortless two-way synchronization between yo ``` -This setup enables opening the Accordion items via the custom button and ensures the local `myValue` state updates when the Accordion closes through any internal means (e.g., clicking on an item's trigger). +#### Key Benefits + +- Simplifies state management +- Automatically updates `myValue` when the accordion changes (e.g., via clicking on an item's trigger) +- Allows external control (e.g., opening an item via a separate button) -### Change Handler +### 2. Change Handler -You can also use the `onValueChange` prop to update local state when the Accordion's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Accordion opens or closes. +For more granular control or to perform additional logic on state changes, use the `onValueChange` prop. This approach is useful when you need to execute custom logic alongside state updates. ```svelte @@ -179,17 +205,26 @@ You can also use the `onValueChange` prop to update local state when the Accordi ``` -### Controlled +#### Use Cases + +- Implementing custom behaviors on value change +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) + +### 3. Fully Controlled -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`. +For complete control over the accordion's value state, use the `controlledValue` prop. This approach requires you to manually manage the value state, giving you full control over when and how the accordion responds to value change events. -You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `Accordion.Root` component. +To implement controlled state: + +1. Set the `controlledValue` prop to `true` on the `Accordion.Root` component. +2. Provide a `value` prop to `Accordion.Root`, which should be a variable holding the current state. +3. Implement an `onValueChange` handler to update the state when the internal state changes. ```svelte (myValue = v)}> @@ -197,7 +232,19 @@ You will then be responsible for updating a local value state variable that is p ``` -See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. +#### When to Use + +- Implementing complex open/close logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + ## Single Type @@ -243,9 +290,11 @@ To disable an individual accordion item, set the `disabled` prop to `true`. This ## Svelte Transitions -You can use the `forceMount` prop on the `Accordion.Content` component to forcefully mount the content regardless of whether the accordion item is open or closed. This is useful when you want more control over the transitions when the accordion item opens and closes using something like [Svelte Transitions](https://svelte.dev/docs/svelte-transition). +The Accordion component can be enhanced with Svelte's built-in transition effects or other animation libraries. + +### Using `forceMount` and `child` Snippets -The `open` snippet prop can be used for conditional rendering of the content based on whether the accordion item is open or closed. +To apply Svelte transitions to Accordion components, use the `forceMount` prop in combination with the `child` snippet. This approach gives you full control over the mounting behavior and animation of the `Accordion.Content`. ```svelte @@ -259,6 +308,13 @@ The `open` snippet prop can be used for conditional rendering of the content bas ``` +In this example: + +- The `forceMount` prop ensures the components are always in the DOM. +- The `child` snippet provides access to the open state and component props. +- Svelte's `#if` block controls when the content is visible. +- Transition directives (`transition:fade` and `transition:fly`) apply the animations. + {#snippet preview()} @@ -267,6 +323,51 @@ The `open` snippet prop can be used for conditional rendering of the content bas -For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. +### Best Practices + +For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. + +```svelte title="MyAccordionContent.svelte" + + + + {#snippet child({ props, open })} + {#if open} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+``` + +You can then use the `MyAccordionContent` component alongside the other `Accordion` primitives throughout your application: + +```svelte + + + + A + + + + + + +``` diff --git a/sites/docs/content/components/alert-dialog.md b/sites/docs/content/components/alert-dialog.md index dc6358870..6c2b697aa 100644 --- a/sites/docs/content/components/alert-dialog.md +++ b/sites/docs/content/components/alert-dialog.md @@ -4,7 +4,7 @@ description: A modal window that alerts users with important information and awa --- @@ -128,11 +128,11 @@ Alternatively, you can define the snippets separately and pass them as props to ## Managing Open State -Bits UI provides flexible options for controlling and synchronizing the Alert Dialog's open state. +Bits UI offers several approaches to manage and synchronize the Alert Dialog's open state, catering to different levels of control and integration needs. -### Two-Way Binding +### 1. Two-Way Binding -Use the `bind:open` directive for effortless two-way synchronization between your local state and the dialog's internal state. +For seamless state synchronization, use Svelte's `bind:open` directive. This method automatically keeps your local state in sync with the dialog's internal state. ```svelte {3,6,8} - + - + ``` -This setup enables opening the dialog via the custom button and ensures the local `isOpen` state updates when the dialog closes through any means (e.g., escape key). +#### Key Benefits + +- Simplifies state management +- Automatically updates isOpen when the dialog closes (e.g., via escape key) +- Allows external control (e.g., opening via a separate button) -### Change Handler +### 2. Change Handler -You can also use the `onOpenChange` prop to update local state when the dialog's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the dialog opens or closes. +For more granular control or to perform additional logic on state changes, use the `onOpenChange` prop. This approach is useful when you need to execute custom logic alongside state updates. ```svelte {3,7-11} (myOpen = o)}> @@ -188,7 +196,19 @@ You will then be responsible for updating a local state variable that is passed ``` -See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. +#### When to Use + +- Implementing complex open/close logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + ## Managing Focus @@ -257,9 +277,17 @@ You'll need to cancel the default behavior of focusing the trigger element by ca ``` -## Scroll Lock +## Advanced Behaviors + +The Alert Dialog component offers several advanced features to customize its behavior and enhance user experience. This section covers scroll locking, escape key handling, and interaction outside the dialog. + +### Scroll Lock -By default, when a dialog is opened, scrolling the body will be disabled, which provides a more native experience for users. If you wish to disable this behavior, you can set the `preventScroll` prop to `false` on the `AlertDialog.Content` component. +By default, when an Alert Dialog opens, scrolling the body is disabled. This provides a more native-like experience, focusing user attention on the dialog content. + +#### Customizing Scroll Behavior + +To allow body scrolling while the dialog is open, use the `preventScroll` prop on `AlertDialog.Content`: ```svelte /preventScroll={false}/ @@ -267,13 +295,26 @@ By default, when a dialog is opened, scrolling the body will be disabled, which ``` -## Escape Keydown + + +Enabling body scroll may affect user focus and accessibility. Use this option judiciously. -By default, when a dialog is open, pressing the `Escape` key will close the dialog. Bits UI provides a couple ways to override this behavior. + -### escapeKeydownBehavior +### Escape Key Handling -You can set the `escapeKeydownBehavior` prop to `'ignore'` on the `AlertDialog.Content` component to prevent the dialog from closing when the `Escape` key is pressed. +By default, pressing the `Escape` key closes an open Alert Dialog. Bits UI provides two methods to customize this behavior. + +#### Method 1: `escapeKeydownBehavior` + +The `escapeKeydownBehavior` prop allows you to customize the behavior taken by the component when the `Escape` key is pressed. It accepts one of the following values: + +- `'close'` (default): Closes the Alert Dialog immediately. +- `'ignore'`: Prevents the Alert Dialog from closing. +- `'defer-otherwise-close'`: If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Alert Dialog will close immediately. +- `'defer-otherwise-ignore'`: If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Alert Dialog will ignore the key press and not close. + +To always prevent the Alert Dialog from closing on Escape key press, set the `escapeKeydownBehavior` prop to `'ignore'` on `Dialog.Content`: ```svelte /escapeKeydownBehavior="ignore"/ @@ -281,78 +322,77 @@ You can set the `escapeKeydownBehavior` prop to `'ignore'` on the `AlertDialog.C ``` -### onEscapeKeydown +#### Method 2: `onEscapeKeydown` -You can also override the default behavior by cancelling the event passed to the `onEscapeKeydown` callback on the `AlertDialog.Content` component. +For more granular control, override the default behavior using the `onEscapeKeydown` prop: -```svelte /onEscapeKeydown={(e) => e.preventDefault()}/ - e.preventDefault()}> +```svelte {2-5} + { + e.preventDefault(); + // do something else instead + }} +> ``` -## Interact Outside +This method allows you to implement custom logic when the `Escape` key is pressed. + +### Interaction Outside -Unlike the regular [Dialog](/docs/components/dialog), the Alert Dialog does not close when the user interacts outside the content. This is because when using an alert dialog, the user is expected to acknowledge the dialog's content before continuing. +By default, interacting outside the Alert Dialog content area closes the Alert Dialog. Bits UI offers two ways to modify this behavior. -If you wish to override this behavior, Bits UI provides a couple ways to do so. +#### Method 1: `interactOutsideBehavior` -### interactOutsideBehavior +The `interactOutsideBehavior` prop allows you to customize the behavior taken by the component when an interaction (touch, mouse, or pointer event) occurs outside the content. It accepts one of the following values: -You can set the `interactOutsideBehavior` prop to `'close'` on the `AlertDialog.Content` component to close the dialog when the user interacts outside the content. +- `'close'` (default): Closes the Alert Dialog immediately. +- `'ignore'`: Prevents the Alert Dialog from closing. +- `'defer-otherwise-close'`: If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Alert Dialog will close immediately. +- `'defer-otherwise-ignore'`: If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Alert Dialog will ignore the event and not close. + +To always prevent the Alert Dialog from closing on Escape key press, set the `escapeKeydownBehavior` prop to `'ignore'` on `Alert.Content`: ```svelte /interactOutsideBehavior="ignore"/ - + - + ``` -### onInteractOutside - -If `interactOutsideBehavior` is set to `'close'`, you can intercept the event passed to the `onInteractOutside` callback on the `AlertDialog.Content` component. +#### Method 2: `onInteractOutside` -If the event is cancelled, the dialog will not close. +For custom handling of outside interactions, you can override the default behavior using the `onInteractOutside` prop: -```svelte /onInteractOutside={(e) => e.preventDefault()}/ - e.preventDefault()}> +```svelte {2-5} + { + e.preventDefault(); + // do something else instead + }} +> ``` -## Nested Dialogs +This approach allows you to implement specific behaviors when users interact outside the Alert Dialog content. -Dialogs can be nested within each other to create more complex layouts. See the [Dialog](/docs/components/dialog) component for more information on nested dialogs. - -## Svelte Transitions +### Best Practices -See the [Dialog](/docs/components/dialog) component for more information on Svelte Transitions with dialog components. +- **Scroll Lock**: Consider your use case carefully before disabling scroll lock. It may be necessary for dialogs with scrollable content or for specific UX requirements. +- **Escape Keydown**: Overriding the default escape key behavior should be done thoughtfully. Users often expect the escape key to close modals. +- **Outside Interactions**: Ignoring outside interactions can be useful for important dialogs or multi-step processes, but be cautious not to trap users unintentionally. +- **Accessibility**: Always ensure that any customizations maintain or enhance the dialog's accessibility. +- **User Expectations**: Try to balance custom behaviors with common UX patterns to avoid confusing users. -## Usage +By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards. -### Controlled Open - -If you want to control or be aware of the `open` state of the dialog from outside of the component, bind to the `open` prop. +## Nested Dialogs -```svelte {3,6,8} - +Dialogs can be nested within each other to create more complex layouts. See the [Dialog](/docs/components/dialog) component for more information on nested dialogs. - +## Svelte Transitions - - - - - - - - - - - - -``` +See the [Dialog](/docs/components/dialog) component for more information on Svelte Transitions with dialog components. diff --git a/sites/docs/content/components/aspect-ratio.md b/sites/docs/content/components/aspect-ratio.md index 41c85b2a6..bfae5c7cf 100644 --- a/sites/docs/content/components/aspect-ratio.md +++ b/sites/docs/content/components/aspect-ratio.md @@ -16,7 +16,13 @@ description: Displays content while maintaining a specified aspect ratio, ensuri -## Structure +## Component Architecture + +- **Root**: The root component which contains the aspect ratio logic + +## Component Structure + +Here's an overview of how the Aspect Ratio component is structured in code: ```svelte - - an abstract painting + + ``` @@ -55,7 +65,7 @@ You can then use the `MyAspectRatio` component in your application like so: import MyAspectRatio from "$lib/components/MyAspectRatio.svelte"; - + ``` ## Custom Ratio diff --git a/sites/docs/content/components/avatar.md b/sites/docs/content/components/avatar.md index 3159b3f2e..c579a82da 100644 --- a/sites/docs/content/components/avatar.md +++ b/sites/docs/content/components/avatar.md @@ -16,7 +16,27 @@ description: Represents a user or entity with a recognizable image or placeholde -## Structure +## Overview + +The Avatar component is designed to represent a user or entity within your application's user interface. It provides a flexible and accessible way to display profile pictures or placeholder images. + +## Key Features + +- **Compound Component Structure**: Offers a set of subcomponents that work together to create a fully-featured avatar. +- **Fallback Mechanism**: Provides a fallback when the primary image is unavailable or loading. +- **Customizable**: Each subcomponent can be styled and configured independently to match your design system. + +## Component Architecture + +The Avatar component is composed of several subcomponents, each with a specific role: + +- **Root**: The main container component that manages the state of the avatar. +- **Image**: The primary image element that displays the user's profile picture or a representative image. +- **Fallback**: The fallback element that displays alternative content when the primary image is unavailable or loading. + +## Component Structure + +Here's an overview of how the Avatar component is structured in code: ```svelte - - - + + + {fallback} @@ -62,7 +89,7 @@ You could then use the `MyAvatar` component in your application like so: import MyAvatar from "$lib/components/MyAvatar.svelte"; - + ``` diff --git a/sites/docs/content/components/button.md b/sites/docs/content/components/button.md index 34b04e8ac..d2c6e8d67 100644 --- a/sites/docs/content/components/button.md +++ b/sites/docs/content/components/button.md @@ -1,6 +1,6 @@ --- title: Button -description: A special button component that can receive Melt UI builders for use with the `asChild` prop. +description: A component that if passed a `href` prop will render an anchor element instead of a button element. --- @@ -16,7 +16,25 @@ description: Allow users to switch between checked, unchecked, and indeterminate -## Structure +## Overview + +The Checkbox component provides a flexible and accessible way to create checkbox inputs in your Svelte applications. It supports three states: checked, unchecked, and indeterminate, allowing for complex form interactions and data representations. + +## Key Features + +- **Tri-State Support**: Handles checked, unchecked, and indeterminate states, providing versatility in form design. +- **Accessibility**: Built with WAI-ARIA guidelines in mind, ensuring keyboard navigation and screen reader support. +- **Flexible State Management**: Supports both controlled and uncontrolled state, allowing for full control over the checkbox's checked state. + +## Component Architecture + +The Checkbox component is composed of the following parts: + +- **Root**: The main component that manages the state and behavior of the checkbox. + +## Component Structure + +Here's an overview of how the Checkbox component is structured in code: ```svelte @@ -65,7 +85,7 @@ It's recommended to use the `Checkbox` primitive to create your own custom check {/if} {/snippet} - + {labelText} ``` @@ -84,11 +104,11 @@ You can then use the `MyCheckbox` component in your application like so: ## Managing Checked State -The `checked` prop is used to determine whether the checkbox is in one of three states: checked, unchecked, or indeterminate. Bits UI provides flexible options for controlling and synchronizing the Checkbox's checked state. +Bits UI offers several approaches to manage and synchronize the Checkbox's checked state, catering to different levels of control and integration needs. -### Two-Way Binding +### 1. Two-Way Binding -Use the `bind:checked` directive for effortless two-way synchronization between your local state and the Checkbox's internal state. +For seamless state synchronization, use Svelte's `bind:checked` directive. This method automatically keeps your local state in sync with the checkbox's internal state. ```svelte + + (myChecked = c)}> + + +``` + +#### When to Use + +- Implementing complex checked/unchecked logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + + ## Disabled State You can disable the checkbox by setting the `disabled` prop to `true`. @@ -155,4 +220,16 @@ For example, if you wanted to submit a string value, you could do the following: ``` +### Required + +If you want to make the checkbox required, you can use the `required` prop. + +```svelte /required/ + + + +``` + +This will apply the `required` attribute to the hidden input element, ensuring that proper form submission is enforced. + diff --git a/sites/docs/content/components/collapsible.md b/sites/docs/content/components/collapsible.md index 4130bf8e0..b1e3e743f 100644 --- a/sites/docs/content/components/collapsible.md +++ b/sites/docs/content/components/collapsible.md @@ -4,7 +4,7 @@ description: Conceals or reveals content sections, enhancing space utilization a --- @@ -16,7 +16,28 @@ description: Conceals or reveals content sections, enhancing space utilization a -## Structure +## Overview + +The Collapsible component enables you to create expandable and collapsible content sections. It provides an efficient way to manage space and organize information in user interfaces, enabling users to show or hide content as needed. + +## Key Features + +- **Accessibility**: ARIA attributes for screen reader compatibility and keyboard navigation. +- **Transition Support**: CSS variables and data attributes for smooth transitions between states. +- **Flexible State Management**: Supports controlled and uncontrolled state, take control if needed. +- **Compound Component Structure**: Provides a set of subcomponents that work together to create a fully-featured collapsible. + +## Component Architecture + +The Accordion component is composed of a few subcomponents, each with a specific role: + +- **Root**: The parent container that manages the state and context for the collapsible functionality. +- **Trigger**: The interactive element (e.g., button) that toggles the expanded/collapsed state of the content. +- **Content**: The container for the content that will be shown or hidden based on the collapsible state. + +## Component Structure + +Here's an overview of how the Collapsible component is structured in code: ```svelte - + - + ``` -This setup enables toggling the Collapsible via the custom button and ensures the local `myOpen` state updates when the Collapsible changes through any internal means (e.g., clicking on the trigger). +#### Key Benefits -### Change Handler +- Simplifies state management +- Automatically updates `isOpen` when the collapsible closes (e.g., via trigger press) +- Allows external control (e.g., opening via a separate button) -You can also use the `onOpenChange` prop to update local state when the Collapsible's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Collapsible changes. +### 2. Change Handler -```svelte +For more granular control or to perform additional logic on state changes, use the `onOpenChange` prop. This approach is useful when you need to execute custom logic alongside state updates. + +```svelte {3,7-11} { - myOpen = open; + isOpen = open; // additional logic here. }} > @@ -112,17 +137,27 @@ You can also use the `onOpenChange` prop to update local state when the Collapsi ``` -### Controlled +#### Use Cases + +- Implementing custom behaviors on open/close +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) -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`. +### 3. Fully Controlled -You will then be responsible for updating a local state variable that is passed as the `open` prop to the `Collapsible.Root` component. +For complete control over the Collapsible's open state, use the `controlledOpen` prop. This approach requires you to manually manage the open state, giving you full control over when and how the collapsible responds to open/close events. + +To implement controlled state: + +1. Set the `controlledOpen` prop to `true` on the `Collapsible.Root` component. +2. Provide an `open` prop to `Collapsible.Root`, which should be a variable holding the current state. +3. Implement an `onOpenChange` handler to update the state when the internal state changes. ```svelte (myOpen = o)}> @@ -130,24 +165,101 @@ You will then be responsible for updating a local state variable that is passed ``` +#### When to Use + +- Implementing complex open/close logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + + ## 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). +The Collapsible component can be enhanced with Svelte's built-in transition effects or other animation libraries. -The `open` snippet prop can be used for conditional rendering of the content based on whether the collapsible is open. +### Using `forceMount` and `child` Snippets -```svelte - +To apply Svelte transitions to Collapsible components, use the `forceMount` prop in combination with the `child` snippet. This approach gives you full control over the mounting behavior and animation of the `Collapsible.Content`. + +```svelte /forceMount/ /transition:fade/ /transition:fly/ + + + + Open + + {#snippet child({ props, open })} + {#if open} +
+ +
+ {/if} + {/snippet} +
+
+``` + +In this example: + +- The `forceMount` prop ensures the content is always in the DOM. +- The `child` snippet provides access to the open state and component props. +- Svelte's `#if` block controls when the content is visible. +- Transition directive (`transition:fade`) apply the animations. + +### Best Practices + +For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. + +```svelte title="MyCollapsibleContent.svelte" + + + {#snippet child({ props, open })} {#if open} -
- This is the collapsible content that will transition in and out. +
+ {@render children?.()}
{/if} {/snippet} ``` -With the amount of boilerplate needed to handle the transitions, it's recommended to componentize your custom implementation of the collapsible content and use that throughout your application. See the [Transitions](/docs/transitions) documentation for more information on using transitions with Bits UI components. +You can then use the `MyCollapsibleContent` component alongside the other `Collapsible` primitives throughout your application: + +```svelte + + + + Open + + + + +``` diff --git a/sites/docs/content/components/combobox.md b/sites/docs/content/components/combobox.md index 6a9f28163..99a474659 100644 --- a/sites/docs/content/components/combobox.md +++ b/sites/docs/content/components/combobox.md @@ -119,4 +119,230 @@ It's recommended to use the `Combobox` primitives to build your own custom combo ``` +## Value State + +The `value` represents the currently selected item/option within the Combobox. Bits UI provides flexible options for controlling and synchronizing the Combobox's `value` state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the Combobox's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables toggling the value via the custom button and ensures the local `myValue` state updates when the Combobox's value changes through any internal means (e.g., clicking on an item). + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the Combobox's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the value changes. + +```svelte {3,7-11} + + + { + myValue = value; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the Combobox'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 `Combobox.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + +## Open State + +The `open` state represents whether or not the Combobox content is open. Bits UI provides flexible options for controlling and synchronizing the Combobox's open state. + +### Two-Way Binding + +Use the `bind:open` directive for effortless two-way synchronization between your local state and the Combobox's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables toggling the Combobox via the custom button and ensures the local `isOpen` state updates when the Combobox's `open` state changes through any internal means e.g. clicking on the trigger or outside the content. + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the Combobox's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Combobox's open state changes. + +```svelte {3,7-11} + + + { + isOpen = open; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the Combobox's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `open` prop to the `Combobox.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + +## Opt-out of Floating UI + +When you use the `Combobox.Content` component, Bits UI uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger, similar to other popover-like components. + +You can opt-out of this behavior by instead using the `Combobox.ContentStatic` component. + +```svelte {4,14} + + + + + + + + + + + + + + + + + +``` + +When using this component, you'll need to handle the positioning of the content yourself. Keep in mind that using `Combobox.Portal` alongside `Combobox.ContentStatic` may result in some unexpected positioning behavior, feel free to not use the portal or work around it. + +## Custom Anchor + +By default, the `Combobox.Content` is anchored to the `Combobox.Trigger` component, which determines where the content is positioned. + +If you wish to instead anchor the content to a different element, you can pass either a selector string or an `HTMLElement` to the `customAnchor` prop of the `Combobox.Content` component. + +```svelte + + +
+ + + + + + + + +``` + +## What is the Viewport? + +The `Combobox.Viewport` component is used to determine the size of the content in order to determine whether or not the scroll up and down buttons should be rendered. + +If you wish to set a minimum/maximum height for the select content, you should apply it to the `Combobox.Viewport` component. + +## Scroll Up/Down Buttons + +The `Combobox.ScrollUpButton` and `Combobox.ScrollDownButton` components are used to render the scroll up and down buttons when the select content is larger than the viewport. + +You must use the `Combobox.Viewport` component when using the scroll buttons. + +## Native Scrolling/Overflow + +If you don't want to use the scroll buttons and prefer to use the standard scrollbar/overflow behavior, you can omit the `Combobox.Scroll[Up|Down]Button` components and the `Combobox.Viewport` component. + +You'll need to set a height on the `Combobox.Content` component and appropriate `overflow` styles to enable scrolling. + +## Scroll Lock + +By default, when a user opens the Combobox, scrolling outside the content will be disabled. You can override this behavior by setting the `preventScroll` prop to `false`. + +```svelte /preventScroll={false}/ + + + +``` + +## Highlighted Items + +The Combobox component follows the [WAI-ARIA descendant pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox) for highlighting items. This means that the `Combobox.Input` retains focus the entire time, even when navigating with the keyboard, and items are highlighted as the user navigates them. + +### Styling Highlighted Items + +You can use the `data-highlighted` attribute on the `Combobox.Item` component to style the item differently when it is highlighted. + +### onHighlight / onUnhighlight + +To trigger side effects when an item is highlighted or unhighlighted, you can use the `onHighlight` and `onUnhighlight` props. + +```svelte + console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> + + +``` + diff --git a/sites/docs/content/components/command.md b/sites/docs/content/components/command.md index d04692924..1c3055370 100644 --- a/sites/docs/content/components/command.md +++ b/sites/docs/content/components/command.md @@ -44,7 +44,72 @@ description: A command menu component that can be used to search, filter, and se ``` -## Within a Dialog +## Value State + +The `value` prop is used to determine which command item is currently selected. Bits UI provides flexible options for controlling and synchronizing the Command's `value` state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the Command's internal state. + +```svelte + + + + + + +``` + +This setup enables setting the Command's value via the custom button and ensures the local `myValue` state updates when the Command updates the value through any internal means (e.g., searching for a command item). + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the Command's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the state changes. + +```svelte + + + { + myValue = value; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the Command'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 `Command.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + +## In a Modal You can combine the `Command` component with the `Dialog` component to display the command menu within a modal. @@ -58,4 +123,58 @@ You can combine the `Command` component with the `Dialog` component to display t +## Filtering + +### Custom Filter + +By default, the `Command` component uses a scoring algorithm to determine how the items should be sorted/filtered. You can provide a custom filter function to override this behavior. + +The function should return a number between `0` and `1`, with `1` being a perfect match, and `0` being no match, resulting in the item being hidden entirely. + +The following example shows how you might implement a strict substring match filter: + +```svelte + + + + + +``` + +### Disable Filtering + +You can disable filtering by setting the `shouldFilter` prop to `false`. + +```svelte + + + +``` + +This is useful when you have a lot of custom logic, need to fetch items asynchronously, or just want to handle filtering yourself. You'll be responsible for iterating over the items and determining which ones should be shown. + +## Item Selection + +You can use the `onSelect` prop to handle the selection of items. + +```svelte + console.log("selected something!")} /> +``` + +## Links + +If you want one of the items to get all the benefits of a link (prefetching, etc.), you should use the `Command.LinkItem` component instead of the `Command.Item` component. The only difference is that the `Command.LinkItem` component will render an `a` element instead of a `div` element. + +```svelte + + + +``` + diff --git a/sites/docs/content/components/context-menu.md b/sites/docs/content/components/context-menu.md index baaaee71e..a911f9c38 100644 --- a/sites/docs/content/components/context-menu.md +++ b/sites/docs/content/components/context-menu.md @@ -141,7 +141,7 @@ Alternatively, you can define the snippet(s) separately and pass them as props t /> ``` -## Managing Open State +## Open State Bits UI provides flexible options for controlling and synchronizing the menu's open state. @@ -194,7 +194,7 @@ You will then be responsible for updating a local state variable that is passed (myOpen = o)}> diff --git a/sites/docs/content/components/date-field.md b/sites/docs/content/components/date-field.md index fc99cfa4d..82a6460c0 100644 --- a/sites/docs/content/components/date-field.md +++ b/sites/docs/content/components/date-field.md @@ -127,7 +127,7 @@ If we're collecting a date from the user where we want the timezone as well, we NOTE: If you're creating a date field for something like a birthday, ensure your placeholder is set in a leap year to ensure users born on a leap year will be able to select the correct date. -## Managing Placeholder State +## Placeholder State Bits UI provides flexible options for controlling and synchronizing the `DateField.Root` component's placeholder state. @@ -168,7 +168,31 @@ You can also use the `onPlaceholderChange` prop to update local state when the ` ``` -## Managing Value State +### Controlled + +Sometimes, you may want complete control over the date field's `placeholder` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledPlaceholder` prop to `true`. + +You will then be responsible for updating a local placeholder state variable that is passed as the `placeholder` prop to the `DateField.Root` component. + +```svelte + + + (myPlaceholder = p)} +> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Value State The `value` represents the currently selected date within the `DateField.Root` component. @@ -212,6 +236,26 @@ You can also use the `onValueChange` prop to update local state when the `DateFi ``` +### Controlled + +Sometimes, you may want complete control over the date field's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `DateField.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + ## Default Value Often, you'll want to start the `DateField.Root` component with a default value. Likely this value will come from a database in the format of an ISO 8601 string. You can use the `parseDate` function from the `@internationalized/date` package to parse the string into a `CalendarDate` object. diff --git a/sites/docs/content/components/date-picker.md b/sites/docs/content/components/date-picker.md index d6e2206ea..8cf186f5d 100644 --- a/sites/docs/content/components/date-picker.md +++ b/sites/docs/content/components/date-picker.md @@ -73,4 +73,195 @@ description: Facilitates the selection of dates through an input and calendar-ba ``` +## Placeholder State + +Bits UI provides flexible options for controlling and synchronizing the `DatePicker` component's placeholder state. + +### Two-Way Binding + +Use the `bind:placeholder` directive for effortless two-way synchronization between your local state and the `DatePicker` component's placeholder. + +```svelte {3,6,8} + + + + + +``` + +This setup enables toggling the `DatePicker` component's placeholder via the custom button and ensures the local `placeholder` state is synchronized with the component's placeholder state. + +### Change Handler + +You can also use the `onPlaceholderChange` prop to update local state when the `DatePicker` component's placeholder changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the component's placeholder changes. + +```svelte {3,7-11} + + + { + placeholder = placeholder.set({ year: 2025 }); + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the `DatePicker`'s `placeholder` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledPlaceholder` prop to `true`. + +You will then be responsible for updating a local placeholder state variable that is passed as the `placeholder` prop to the `DatePicker.Root` component. + +```svelte + + + (myPlaceholder = p)} +> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Value State + +The `value` represents the currently selected date within the `DatePicker` component. + +Bits UI provides flexible options for controlling and synchronizing the `DatePicker` component's value state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the `DateField` component's value. + +```svelte {3,6,8} + + + + + + +``` + +This setup enables toggling the `DatePicker` component's value via the custom button and ensures the local `value` state is synchronized with the `DatePicker` component's value. + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the `DatePicker` component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `DatePicker` component's value changes. + +```svelte {3,7-11} + + + { + value = value.set({ hour: value.hour + 1 }); + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `DatePicker.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Open State + +The `open` prop is used to determine whether the `DatePicker` is open or closed. Bits UI provides flexible options for controlling and synchronizing the open state. + +### Two-Way Binding + +Use the `bind:open` directive for effortless two-way synchronization between your local state and the `DatePicker`'s internal state. + +```svelte + + + + + + + +``` + +This setup enables toggling the `DatePicker` via the custom button and ensures the local `myOpen` state updates when the state changes through any internal means (e.g., clicking on the trigger). + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the `DatePicker`'s `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the state changes. + +```svelte + + + { + myOpen = open; + // additional logic here. + }} +> + + +``` + +### 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 `DatePicker.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + diff --git a/sites/docs/content/components/date-range-field.md b/sites/docs/content/components/date-range-field.md index 90419f130..09fb630fb 100644 --- a/sites/docs/content/components/date-range-field.md +++ b/sites/docs/content/components/date-range-field.md @@ -4,7 +4,7 @@ description: Allows users to input a range of dates within a designated field. --- @@ -39,4 +39,157 @@ description: Allows users to input a range of dates within a designated field. ``` + + +Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the [Dates](/docs/dates) documentation to learn more! + + + +## Placeholder State + +Bits UI provides flexible options for controlling and synchronizing the `DateRangeField` component's `placeholder` state. + +### Two-Way Binding + +Use the `bind:placeholder` directive for effortless two-way synchronization between your local state and the `DateRangeField` component's placeholder. + +```svelte {3,6,8} + + + + + +``` + +This setup enables toggling the `DateRangeField` component's placeholder via the custom button and ensures the local `placeholder` state is synchronized with the `DateRangeField` component's placeholder should it change from within the component. + +### Change Handler + +You can also use the `onPlaceholderChange` prop to update local state when the component's `placeholder` changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `DateRangeField` component's placeholder changes. + +```svelte {3,7-11} + + + { + placeholder = placeholder.set({ year: 2025 }); + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the `placeholder` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledPlaceholder` prop to `true`. + +You will then be responsible for updating a local placeholder state variable that is passed as the `placeholder` prop to the `DateRangeField.Root` component. + +```svelte + + + (myPlaceholder = p)} +> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Value State + +The `value` represents the currently selected date within the `DateRangeField` component. + +Bits UI provides flexible options for controlling and synchronizing the `DateRangeField` component's value state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the `DateRangeField` component's value. + +```svelte {3,6,8} + + + + + + +``` + +This setup enables toggling the component's value via the custom button and ensures the local `value` state is synchronized with the component's value, should it change from within the component. + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the component's value changes. + +```svelte {3,7-11} + + + { + value = { + start: v.start.set({ hour: v.start.hour + 1 }), + end: v.end.set({ hour: v.end.hour + 1 }), + }; + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `DateRangeField.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + diff --git a/sites/docs/content/components/date-range-picker.md b/sites/docs/content/components/date-range-picker.md index 05ed92631..0bc32d12a 100644 --- a/sites/docs/content/components/date-range-picker.md +++ b/sites/docs/content/components/date-range-picker.md @@ -77,4 +77,212 @@ description: Facilitates the selection of date ranges through an input and calen ``` +## Placeholder State + +Bits UI provides flexible options for controlling and synchronizing the `DateRangePicker` component's `placeholder` state. + +### Two-Way Binding + +Use the `bind:placeholder` directive for effortless two-way synchronization between your local state and the `DateRangePicker` component's placeholder. + +```svelte {3,6,8} + + + + + +``` + +This setup enables toggling the `DateRangePicker` component's placeholder via the custom button and ensures the local `placeholder` state is synchronized with the `DateRangePicker` component's placeholder should it change from within the component. + +### Change Handler + +You can also use the `onPlaceholderChange` prop to update local state when the component's `placeholder` changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `DateRangePicker` component's placeholder changes. + +```svelte {3,7-11} + + + { + placeholder = placeholder.set({ year: 2025 }); + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the `placeholder` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledPlaceholder` prop to `true`. + +You will then be responsible for updating a local placeholder state variable that is passed as the `placeholder` prop to the `DateRangePicker.Root` component. + +```svelte + + + (myPlaceholder = p)} +> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Value State + +The `value` represents the currently selected date within the `DateRangePicker` component. + +Bits UI provides flexible options for controlling and synchronizing the `DateRangePicker` component's value state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the `DateRangePicker` component's value. + +```svelte {3,6,8} + + + + + + +``` + +This setup enables toggling the component's value via the custom button and ensures the local `value` state is synchronized with the component's value, should it change from within the component. + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the component's value changes. + +```svelte {3,7-11} + + + { + value = { + start: v.start.set({ hour: v.start.hour + 1 }), + end: v.end.set({ hour: v.end.hour + 1 }), + }; + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `DateRangePicker.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Open State + +The `open` prop is used to determine whether the `DateRangePicker` is open or closed. Bits UI provides flexible options for controlling and synchronizing the open state. + +### Two-Way Binding + +Use the `bind:open` directive for effortless two-way synchronization between your local state and the `DateRangePicker`'s internal state. + +```svelte + + + + + + + +``` + +This setup enables toggling the `DateRangePicker` via the custom button and ensures the local `myOpen` state updates when the state changes through any internal means (e.g., clicking on the trigger). + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the `DateRangePicker`'s `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the state changes. + +```svelte + + + { + myOpen = open; + // additional logic here. + }} +> + + +``` + +### 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 `DateRangePicker.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + diff --git a/sites/docs/content/components/dialog.md b/sites/docs/content/components/dialog.md index 0b6517472..0ae975b75 100644 --- a/sites/docs/content/components/dialog.md +++ b/sites/docs/content/components/dialog.md @@ -4,7 +4,7 @@ description: A modal window presenting content or seeking user input without nav --- @@ -16,7 +16,35 @@ description: A modal window presenting content or seeking user input without nav -## Structure +## Overview + +The Dialog component in Bits UI provides a flexible and accessible way to create modal dialogs in your Svelte applications. It follows a compound component pattern, allowing for fine-grained control over the dialog's structure and behavior while maintaining accessibility and ease of use. + +## Key Features + +- **Compound Component Structure**: Offers a set of subcomponents that work together to create a fully-featured dialog. +- **Accessibility**: Built with WAI-ARIA guidelines in mind, ensuring keyboard navigation and screen reader support. +- **Customizable**: Each subcomponent can be styled and configured independently. +- **Portal Support**: Content can be rendered in a portal, ensuring proper stacking context. +- **Managed Focus**: Automatically manages focus, with the option to take control if needed. +- **Flexible State Management**: Supports both controlled and uncontrolled state, allowing for full control over the dialog's open state. + +## Component Architecture + +The Dialog component is composed of several subcomponents, each with a specific role: + +- **Root**: The main container component that manages the state of the dialog. Provides context for all child components. +- **Trigger**: A button that toggles the dialog's open state. +- **Portal**: Renders its children in a portal, outside the normal DOM hierarchy. +- **Overlay**: A backdrop that sits behind the dialog content. +- **Content**: The main container for the dialog's content. +- **Title**: Renders the dialog's title. +- **Description**: Renders a description or additional context for the dialog. +- **Close**: A button that closes the dialog. + +## Component Structure + +Here's an overview of how the Dialog component is structured in code: ```svelte - + - + ``` -This setup enables opening the Dialog via the custom button and ensures the local `isOpen` state updates when the Dialog closes through any means (e.g., escape key). +#### Key Benefits + +- Simplifies state management +- Automatically updates `isOpen` when the dialog closes (e.g., via escape key) +- Allows external control (e.g., opening via a separate button) -### Change Handler +### 2. Change Handler -You can also use the `onOpenChange` prop to update local state when the Dialog's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Dialog opens or closes. +For more granular control or to perform additional logic on state changes, use the `onOpenChange` prop. This approach is useful when you need to execute custom logic alongside state updates. ```svelte {3,7-11} (myOpen = o)}> @@ -188,13 +239,31 @@ You will then be responsible for updating a local state variable that is passed ``` -## Managing Focus +#### When to Use + +- Implementing complex open/close logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + + +## Focus Management + +Proper focus management is crucial for accessibility and user experience in modal dialogs. Bits UI's Dialog component provides several features to help you manage focus effectively. ### Focus Trap -By default, when a Dialog is opened, focus will be trapped within the Dialog, preventing the user from interacting with the rest of the page. This follows the [WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) for modal dialogs. +By default, the Dialog implements a focus trap, adhering to the WAI-ARIA design pattern for modal dialogs. This ensures that keyboard focus remains within the Dialog while it's open, preventing users from interacting with the rest of the page. + +#### Disabling the Focus Trap -Although it isn't recommended unless absolutely necessary, you can disabled this beahvior by setting the `trapFocus` prop to `false` on the `Dialog.Content` component. +While not recommended, you can disable the focus trap if absolutely necessary: ```svelte /trapFocus={false}/ @@ -202,13 +271,19 @@ Although it isn't recommended unless absolutely necessary, you can disabled this ``` + + +Disabling the focus trap may compromise accessibility. Only do this if you have a specific reason and implement an alternative focus management strategy. + + + ### Open Focus -By default, when a Dialog is opened, focus will be set to the first focusable element with the `Dialog.Content`. This ensures that users navigating my keyboard end up somewhere within the Dialog that they can interact with. +When a Dialog opens, focus is automatically set to the first focusable element within `Dialog.Content`. This ensures keyboard users can immediately interact with the Dialog contents. -You can override this behavior using the `onOpenAutoFocus` prop on the `Dialog.Content` component. It's _highly_ recommended that you use this prop to focus _something_ within the Dialog. +#### Customizing Initial Focus -You'll first need to cancel the default behavior of focusing the first focusable element by cancelling the event passed to the `onOpenAutoFocus` callback. You can then focus whatever you wish. +To specify which element receives focus when the Dialog opens, use the `onOpenAutoFocus` prop on `Dialog.Content`: ```svelte {9-12} + + + {#snippet child({ props, open })} + {#if open} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+``` + +You can then use the `MyDialogOverlay` component alongside the other `Dialog` primitives throughout your application: + +```svelte + + + + Open + + + + + + + +``` diff --git a/sites/docs/content/components/dropdown-menu.md b/sites/docs/content/components/dropdown-menu.md index 169ba32db..b516760a8 100644 --- a/sites/docs/content/components/dropdown-menu.md +++ b/sites/docs/content/components/dropdown-menu.md @@ -116,7 +116,7 @@ You can then use the `MyDropdownMenu` component like this: /> ``` -## Managing Open State +## Open State Bits UI provides flexible options for controlling and synchronizing the menu's open state. @@ -170,7 +170,7 @@ You will then be responsible for updating a local state variable that is passed (myOpen = o)}> diff --git a/sites/docs/content/components/link-preview.md b/sites/docs/content/components/link-preview.md index 17b766e3e..eeea7be33 100644 --- a/sites/docs/content/components/link-preview.md +++ b/sites/docs/content/components/link-preview.md @@ -4,7 +4,7 @@ description: Displays a summarized preview of a linked content's details or info --- @@ -16,6 +16,16 @@ description: Displays a summarized preview of a linked content's details or info +## Overview + +A component that lets users preview a link before they decide to follow it. This is useful for providing non-essential context or additional information about a link without having to navigate away from the current page. + + + +This component is only intended to be used with a mouse or other pointing device. It doesn't respond to touch events, and the preview content cannot be accessed via the keyboard. On touch devices, the link will be followed immediately. As it is not accessible to all users, the preview should not contain vital information. + + + ## Structure ```svelte @@ -29,4 +39,89 @@ description: Displays a summarized preview of a linked content's details or info ``` +## Open State + +Bits UI provides flexible options for controlling and synchronizing the component's open state. + +### Two-Way Binding + +Use the `bind:open` directive for effortless two-way synchronization between your local state and the component's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables opening the Link Preview via the custom button and ensures the local `isOpen` state updates when the state changes through any internal means (e.g., escape key). + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the Link Preview's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Link Preview opens or closes. + +```svelte {3,7-11} + + + { + isOpen = open; + // additional logic here. + }} +> + + +``` + +### 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 `LinkPreview.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Opt-out of Floating UI + +When you use the `LinkPreview.Content` component, Bits UI uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger, similar to other popover-like components. + +You can opt-out of this behavior by instead using the `LinkPreview.ContentStatic` component. This component does not use Floating UI and leaves positioning the content entirely up to you. + +```svelte /LinkPreview.ContentStatic/ + + + + + + +``` + + + +The `LinkPreview.Arrow` component is designed to be used with Floating UI and `LinkPreview.Content`, so you may experience unexpected behavior if you attempt to use it with `LinkPreview.ContentStatic`. + + + diff --git a/sites/docs/content/components/listbox.md b/sites/docs/content/components/listbox.md index 51ea2b634..ad0608baf 100644 --- a/sites/docs/content/components/listbox.md +++ b/sites/docs/content/components/listbox.md @@ -118,7 +118,7 @@ You can then use the `MyListbox` component throughout your application like so: ``` -## Managing Value State +## Value State The `value` represents the currently selected item/option within the listbox. Bits UI provides flexible options for controlling and synchronizing the Listbox's value state. @@ -143,7 +143,7 @@ This setup enables toggling the value via the custom button and ensures the loca ### Change Handler -You can also use the `onValueChange` prop to update local state when the Listbox's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Select changes. +You can also use the `onValueChange` prop to update local state when the Listbox's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the value changes. ```svelte {3,7-11} + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + +## Open State The `open` state represents whether or not the listbox content is open. Bits UI provides flexible options for controlling and synchronizing the Listbox's open state. @@ -206,6 +226,26 @@ You can also use the `onOpenChange` prop to update local state when the Listbox' ``` +### Controlled + +Sometimes, you may want complete control over the listbox's `open` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledOpen` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `open` prop to the `Listbox.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled values. + ## Opt-out of Floating UI When you use the `Listbox.Content` component, Bits UI uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger, similar to other popover-like components. @@ -286,4 +326,22 @@ By default, when a user opens the listbox, scrolling outside the content will be ``` +## Highlighted Items + +The Listbox component follows the [WAI-ARIA descendant pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox) for highlighting items. This means that the `Listbox.Trigger` retains focus the entire time, even when navigating with the keyboard, and items are highlighted as the user navigates them. + +### Styling Highlighted Items + +You can use the `data-highlighted` attribute on the `Listbox.Item` component to style the item differently when it is highlighted. + +### onHighlight / onUnhighlight + +To trigger side effects when an item is highlighted or unhighlighted, you can use the `onHighlight` and `onUnhighlight` props. + +```svelte + console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> + + +``` + diff --git a/sites/docs/content/components/menubar.md b/sites/docs/content/components/menubar.md index 47b10b7ee..cb59694e3 100644 --- a/sites/docs/content/components/menubar.md +++ b/sites/docs/content/components/menubar.md @@ -142,7 +142,7 @@ Now, we can use the `MyMenubarMenu` component within a `Menubar.Root` component ``` -## Managing Value State +## Value State Bits UI provides flexible options for controlling and synchronizing the menubar's active value state. The `value` represents the currently opened menu within the menubar. diff --git a/sites/docs/content/components/pagination.md b/sites/docs/content/components/pagination.md index 06e12e317..362217e19 100644 --- a/sites/docs/content/components/pagination.md +++ b/sites/docs/content/components/pagination.md @@ -32,4 +32,72 @@ description: Facilitates navigation between pages. ``` +## Page State + +Bits UI provides flexible options for controlling and synchronizing the component's `page` state. + +### Two-Way Binding + +Use the `bind:page` directive for effortless two-way synchronization between your local state and the component's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables changing the `page` via the custom button and ensures the local `myPage` state updates when the state updates through any internal means. + +### Change Handler + +You can also use the `onPageChange` prop to update local state when the component's `page` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the page changes. + +```svelte {3,7-11} + + + { + myPage = newPage; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `page` 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 `controlledPage` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `page` prop to the `Pagination.Root` component. + +```svelte + + + (myPage = p)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Ellipsis + +The `pages` snippet prop consists of two types of items: `'page'` and `'ellipsis'`. The `'page'` type represents an actual page number, while the `'ellipsis'` type represents a placeholder for rendering an ellipsis between pages. + diff --git a/sites/docs/content/components/pin-input.md b/sites/docs/content/components/pin-input.md index 56683a533..717684159 100644 --- a/sites/docs/content/components/pin-input.md +++ b/sites/docs/content/components/pin-input.md @@ -4,7 +4,7 @@ description: Allows users to input a sequence of one-character alphanumeric inpu --- @@ -16,7 +16,30 @@ description: Allows users to input a sequence of one-character alphanumeric inpu -This component is derived from and would not have been possible without the work done by [Input OTP](https://github.com/guilhermerodz/input-otp) by [Guilherme Rodz](https://x.com/guilhermerodz). +## Overview + +The PIN Input component provides a customizable solution for One-Time Password (OTP), Two-Factor Authentication (2FA), or Multi-Factor Authentication (MFA) input fields. Due to the lack of a native HTML element for these purposes, developers often resort to either basic input fields or custom implementations. This component offers a robust, accessible, and flexible alternative. + + + +This component is derived from and would not have been possible without the work done by [Guilherme Rodz](https://x.com/guilhermerodz) with [Input OTP](https://github.com/guilhermerodz/input-otp). + + + +## Key Features + +- **Invisible Input Technique**: Utilizes an invisible input element for seamless integration with form submissions and browser autofill functionality. +- **Customizable Appearance**: Allows for custom designs while maintaining core functionality. +- **Accessibility**: Ensures keyboard navigation and screen reader compatibility. +- **Flexible Configuration**: Supports various PIN lengths and input types (numeric, alphanumeric). + +## Component Architecture + +1. **Root Container**: A relatively positioned root element that encapsulates the entire component. +2. **Invisible Input**: A hidden input field that manages the actual value and interacts with the browser's built-in features. +3. **Visual Cells**: Customizable elements representing each character of the PIN, rendered as siblings to the invisible input. + +This structure allows for a seamless user experience while providing developers with full control over the visual representation. ## Structure @@ -28,7 +51,7 @@ This component is derived from and would not have been possible without the work {#snippet children({ cells })} {#each cells as cell} - {cell.char !== null ? cell.char : ""} + {/each} {/snippet} diff --git a/sites/docs/content/components/popover.md b/sites/docs/content/components/popover.md index facd10707..da99b5226 100644 --- a/sites/docs/content/components/popover.md +++ b/sites/docs/content/components/popover.md @@ -32,26 +32,191 @@ description: Display supplementary content or information when users interact wi ``` - +## Open State + +Bits UI provides flexible options for controlling and synchronizing the Popover's open state. + +### Two-Way Binding -## Examples +Use the `bind:open` directive for effortless two-way synchronization between your local state and the Popover's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables opening the `Popover` via the custom button and ensures the local `isOpen` state updates when the Dialog closes through any means (e.g., escape key). + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the `Popover`'s `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `Popover` opens or closes. + +```svelte {3,7-11} + + + { + isOpen = open; + // additional logic here. + }} +> + + +``` ### Controlled -If you want to control or be aware of the `open` state of the popover from outside of the component, bind to the `open` prop. +Sometimes, you may want complete control over the `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 `Popover.Root` component. ```svelte - - - - - - + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Managing Focus + +### Focus Trap + +By default, when a Popover is opened, focus will be trapped within that Popover. You can disabled this beahvior by setting the `trapFocus` prop to `false` on the `Popover.Content` component. + +```svelte /trapFocus={false}/ + + + +``` + +### Open Focus + +By default, when a Popover is opened, focus will be set to the first focusable element with the `Popover.Content`. This ensures that users navigating my keyboard end up somewhere within the Popover that they can interact with. + +You can override this behavior using the `onOpenAutoFocus` prop on the `Popover.Content` component. It's _highly_ recommended that you use this prop to focus _something_ within the Popover's content. + +You'll first need to cancel the default behavior of focusing the first focusable element by cancelling the event passed to the `onOpenAutoFocus` callback. You can then focus whatever you wish. + +```svelte {9-12} + + + + Open Popover + { + e.preventDefault(); + nameInput?.focus(); + }} + > + ``` + +### Close Focus + +By default, when a Popover is closed, focus will be set to the trigger element of the Popover. You can override this behavior using the `onCloseAutoFocus` prop on the `Popover.Content` component. + +You'll need to cancel the default behavior of focusing the trigger element by cancelling the event passed to the `onCloseAutoFocus` callback, and then focus whatever you wish. + +```svelte {9-12} + + + + + Open Popover + { + e.preventDefault(); + nameInput?.focus(); + }} + > + + + +``` + +## Scroll Lock + +By default, when a Popover is opened, users can still scroll the body and interact with content outside of the Popover. If you wish to lock the body scroll and prevent users from interacting with content outside of the Popover, you can set the `preventScroll` prop to `true` on the `Popover.Content` component. + +```svelte /preventScroll={true}/ + + + +``` + +## Escape Keydown + +By default, when a Popover is open, pressing the `Escape` key will close the dialog. Bits UI provides a couple ways to override this behavior. + +### escapeKeydownBehavior + +You can set the `escapeKeydownBehavior` prop to `'ignore'` on the `Popover.Content` component to prevent the dialog from closing when the `Escape` key is pressed. + +```svelte /escapeKeydownBehavior="ignore"/ + + + +``` + +### onEscapeKeydown + +You can also override the default behavior by cancelling the event passed to the `onEscapeKeydown` callback on the `Popover.Content` component. + +```svelte /onEscapeKeydown={(e) => e.preventDefault()}/ + e.preventDefault()}> + + +``` + +## Interact Outside + +By default, when a Popover is open, pointer down events outside the content will close the popover. Bits UI provides a couple ways to override this behavior. + +### interactOutsideBehavior + +You can set the `interactOutsideBehavior` prop to `'ignore'` on the `Popover.Content` component to prevent the dialog from closing when the user interacts outside the content. + +```svelte /interactOutsideBehavior="ignore"/ + + + +``` + +### onInteractOutside + +You can also override the default behavior by cancelling the event passed to the `onInteractOutside` callback on the `Popover.Content` component. + +```svelte /onInteractOutside={(e) => e.preventDefault()}/ + e.preventDefault()}> + + +``` + + diff --git a/sites/docs/content/components/radio-group.md b/sites/docs/content/components/radio-group.md index 4483ef9b8..0691a49ac 100644 --- a/sites/docs/content/components/radio-group.md +++ b/sites/docs/content/components/radio-group.md @@ -90,7 +90,7 @@ You can then use the `MyRadioGroup` component in your application like so: ``` -## Managing Value State +## Value State The `value` prop is used to determine which radio group item(s) are currently checked. Bits UI provides flexible options for controlling and synchronizing the Radio Group's value. @@ -134,6 +134,26 @@ You can also use the `onValueChange` prop to update local state when the Radio G ``` +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `RadioGroup.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + ## HTML Forms If you set the `name` prop on the `RadioGroup.Root` component, a hidden input element will be rendered to submit the value of the radio group to a form. @@ -144,6 +164,16 @@ If you set the `name` prop on the `RadioGroup.Root` component, a hidden input el ``` +### Required + +To make the hidden input element `required` you can set the `required` prop on the `RadioGroup.Root` component. + +```svelte /required/ + + + +``` + ## Disabling Items You can disable a radio group item by setting the `disabled` prop to `true`. @@ -152,4 +182,20 @@ You can disable a radio group item by setting the `disabled` prop to `true`. Apple ``` +## Orientation + +The `orientation` prop is used to determine the orientation of the radio group, which influences how keyboard navigation will work. + +When the `orientation` is set to `'vertical'`, the radio group will navigate through the items using the `ArrowUp` and `ArrowDown` keys. When the `orientation` is set to `'horizontal'`, the radio group will navigate through the items using the `ArrowLeft` and `ArrowRight` keys. + +```svelte /orientation="vertical"/ /orientation="horizontal"/ + + + + + + + +``` + diff --git a/sites/docs/content/components/select.md b/sites/docs/content/components/select.md index 23615785c..34b8bc119 100644 --- a/sites/docs/content/components/select.md +++ b/sites/docs/content/components/select.md @@ -124,7 +124,7 @@ You can then use the `MySelect` component throughout your application like so: ``` -## Managing Value State +## Value State The `value` represents the currently selected item/option within the select menu. Bits UI provides flexible options for controlling and synchronizing the Select's value state. @@ -168,7 +168,7 @@ You can also use the `onValueChange` prop to update local state when the Select' ``` -## Managing Open State +## Open State The `open` state represents whether or not the select content is open. Bits UI provides flexible options for controlling and synchronizing the Select's open state. diff --git a/sites/docs/content/components/slider.md b/sites/docs/content/components/slider.md index 19c683f5a..0ffae603f 100644 --- a/sites/docs/content/components/slider.md +++ b/sites/docs/content/components/slider.md @@ -71,7 +71,7 @@ You can then use the `MySlider` component in your application like so: ``` -## Managing Value State +## Value State The `value` represents the currently selected value(s) of the slider. @@ -110,6 +110,26 @@ Sometimes, you may only want to perform an action or update a state when the use ``` +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `Slider.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + ## Multiple Thumbs and Ticks If the `value` prop has more than one value, the slider will render multiple thumbs. You can also use the `ticks` snippet prop to render ticks at specific intervals diff --git a/sites/docs/content/components/switch.md b/sites/docs/content/components/switch.md index 2118525e4..fca348c43 100644 --- a/sites/docs/content/components/switch.md +++ b/sites/docs/content/components/switch.md @@ -4,7 +4,7 @@ description: A toggle control enabling users to switch between "on" and "off" st --- @@ -16,7 +16,27 @@ description: A toggle control enabling users to switch between "on" and "off" st -## Structure +## Overview + +The Switch component provides an intuitive and accessible toggle control, allowing users to switch between two states, typically "on" and "off". This component is commonly used for enabling or disabling features, toggling settings, or representing boolean values in forms. The Switch offers a more visual and interactive alternative to traditional checkboxes for binary choices. + +## Key Features + +- **Accessibility**: Built with WAI-ARIA guidelines in mind, ensuring keyboard navigation and screen reader support. +- **State Management**: Internally manages the on/off state, with options for controlled and uncontrolled usage. +- **Stylable**: Data attributes allow for smooth transitions between states and custom styles. +- **HTML Forms**: Can render a hidden input element for form submissions. + +## Component Architecture + +The Switch component is composed of two main parts: + +- **Root**: The main container component that manages the state and behavior of the switch. +- **Thumb**: The "movable" part of the switch that indicates the current state. + +## Component Structure + +Here's an overview of how the Switch component is structured in code: ```svelte - let myChecked = $state(false); + - function fetchNotifications() { - // whatever logic would result in the `myChecked` state being updated - myChecked = true; - } - + +``` - +#### Key Benefits - - - +- Simplifies state management +- Automatically updates `myChecked` when the switch changes (e.g., via clicking on the switch) +- Allows external control (e.g., checking via a separate button/programmatically) + +### 2. Change Handler + +For more granular control or to perform additional logic on state changes, use the `onCheckedChange` prop. This approach is useful when you need to execute custom logic alongside state updates. + +```svelte + + + { + myChecked = checked; + // additional logic here. + }} +/> ``` -This setup enables toggling the switch via custom logic and ensures the local `myChecked` state updates when the switch changes through any internal means (e.g. clicking on the switch). +#### Use Cases -### Change Handler +- Implementing custom behaviors on checked/unchecked +- Integrating with external state management solutions +- Triggering side effects (e.g., logging, data fetching) -You can also use the `onCheckedChange` prop to update local state when the switch's checked state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform an additional side-effect when the switch's `checked` state changes. +### 3. Fully Controlled + +For complete control over the switch's checked state, use the `controlledChecked` prop. This approach requires you to manually manage the checked state, giving you full control over when and how the checkbox responds to change events. + +To implement controlled state: + +1. Set the `controlledChecked` prop to `true` on the `Switch.Root` component. +2. Provide a `checked` prop to `Switch.Root`, which should be a variable holding the current state. +3. Implement an `onCheckedChange` handler to update the state when the internal state changes. ```svelte - console.log(checked)}> - + + + (myChecked = c)}> + ``` +#### When to Use + +- Implementing complex checked/unchecked logic +- Coordinating multiple UI elements +- Debugging state-related issues + + + +While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully. + +For more in-depth information on controlled components and advanced state management techniques, refer to our [Controlled State](/docs/controlled-state) documentation. + + + ## Disabled State You can disable the switch by setting the `disabled` prop to `true`. @@ -142,4 +208,16 @@ For example, if you wanted to submit a string value, you could do the following: ``` +### Required + +If you want to make the switch required, you can use the `required` prop. + +```svelte /required/ + + + +``` + +This will apply the `required` attribute to the hidden input element, ensuring that proper form submission is enforced. + diff --git a/sites/docs/content/components/tabs.md b/sites/docs/content/components/tabs.md index af9ba75d2..4e9f09f3a 100644 --- a/sites/docs/content/components/tabs.md +++ b/sites/docs/content/components/tabs.md @@ -31,4 +31,84 @@ description: Organizes content into distinct sections, allowing users to switch ``` +## Value State + +The `value` represents the currently selected tab within the `Tabs` component. + +Bits UI provides flexible options for controlling and synchronizing the `Tabs` component's value state. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the `Tabs` component's value. + +```svelte {3,6,8} + + + + + + +``` + +This setup enables changing the `Tabs` component's value via the custom button and ensures the local `value` state is synchronized with the `Tabs` component's value. + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the `Tabs` component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the `Tabs` component's value changes. + +```svelte {3,7-11} + + + { + value = v; + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `Tabs.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + +## Orientation + +The `orientation` prop is used to determine the orientation of the `Tabs` component, which influences how keyboard navigation will work. + +When the `orientation` is set to `'horizontal'`, the `ArrowLeft` and `ArrowRight` keys will move the focus to the previous and next tab, respectively. When the `orientation` is set to `'vertical'`, the `ArrowUp` and `ArrowDown` keys will move the focus to the previous and next tab, respectively. + +```svelte + + + + + + + +``` + diff --git a/sites/docs/content/components/toggle-group.md b/sites/docs/content/components/toggle-group.md index 6a276e589..f135c937a 100644 --- a/sites/docs/content/components/toggle-group.md +++ b/sites/docs/content/components/toggle-group.md @@ -29,4 +29,74 @@ description: Groups multiple toggle controls, allowing users to enable one or mu ``` +## Single & Multiple + +The `ToggleGroup` component supports two `type` props, `'single'` and `'multiple'`. When the `type` is set to `'single'`, the `ToggleGroup` will only allow a single item to be selected at a time, and the type of the `value` prop will be a string. + +When the `type` is set to `'multiple'`, the `ToggleGroup` will allow multiple items to be selected at a time, and the type of the `value` prop will be an array of strings. + +## Value State + +The `value` prop is used to determine which item(s) are currently "on". Bits UI provides flexible options for controlling and synchronizing the value. + +### Two-Way Binding + +Use the `bind:value` directive for effortless two-way synchronization between your local state and the Toggle Group's internal state. + +```svelte /bind:value={myValue}/ + + + + + + + +``` + +This setup enables toggling the Toggle Group's value to "apple" via the custom button and ensures the local `myValue` state updates when the state changes through any internal means (e.g., clicking on an item). + +### Change Handler + +You can also use the `onValueChange` prop to update local state when the Toggle Group's `value` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Toggle Group changes. + +```svelte /onValueChange/ + + + { + myValue = value; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the component's `value` state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the `controlledValue` prop to `true`. + +You will then be responsible for updating a local value state variable that is passed as the `value` prop to the `ToggleGroup.Root` component. + +```svelte + + + (myValue = v)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + diff --git a/sites/docs/content/components/toggle.md b/sites/docs/content/components/toggle.md index a586fe988..201079e67 100644 --- a/sites/docs/content/components/toggle.md +++ b/sites/docs/content/components/toggle.md @@ -26,4 +26,68 @@ description: A control element that switches between two states, providing a bin ``` +## Pressed State + +Bits UI provides flexible options for controlling and synchronizing the Toggle's pressed state. + +### Two-Way Binding + +Use the `bind:pressd` directive for effortless two-way synchronization between your local state and the Toggle's internal state. + +```svelte {3,6,8} + + + + + + + +``` + +This setup enables toggling the Toggle via the custom button and ensures the local `isPressed` state updates when the Toggle's internal state updates through any means (e.g., pressing the Toggle). + +### Change Handler + +You can also use the `onPressedChange` prop to update local state when the Toggle's `pressed` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Toggle state changes. + +```svelte {3,7-11} + + + { + isPressed = pressed; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the Toggle's `pressed` 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 `controlledPressed` prop to `true`. + +You will then be responsible for updating a local state variable that is passed as the `pressed` prop to the `Toggle.Root` component. + +```svelte + + + (myPressed = p)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + diff --git a/sites/docs/content/components/tooltip.md b/sites/docs/content/components/tooltip.md index 2ef9dbe82..498aaab14 100644 --- a/sites/docs/content/components/tooltip.md +++ b/sites/docs/content/components/tooltip.md @@ -4,7 +4,7 @@ description: Provides additional information or context when users hover over or --- @@ -66,7 +66,7 @@ The `Tooltip.Provider` component is required to be an ancestor of the `Tooltip.R ``` -It also ensures that only a single tooltip within the same provider can be open at a time. It's recommended to wrap your root layout content with the provider component. +It also ensures that only a single tooltip within the same provider can be open at a time. It's recommended to wrap your root layout content with the provider component, setting your sensible default props there. ```svelte title="+layout.svelte" + + + + + + +``` + +This setup enables opening the Tooltip via the custom button and ensures the local `isOpen` state updates when the Tooltip closes through any means (e.g., escape key). + +### Change Handler + +You can also use the `onOpenChange` prop to update local state when the Tooltip's `open` state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Tooltip opens or closes. + +```svelte {3,7-11} + + + { + isOpen = open; + // additional logic here. + }} +> + + +``` + +### Controlled + +Sometimes, you may want complete control over the Tooltip'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 `Tooltip.Root` component. + +```svelte + + + (myOpen = o)}> + + +``` + +See the [Controlled State](/docs/controlled-state) documentation for more information about controlled states. + ## Mobile Tooltips Tooltips are _not_ supported on mobile devices. The intent of a tooltip is to provide a "tip" about a "tool" before the user interacts with that tool (in most cases, a button). @@ -225,4 +289,25 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content components that handles this logic if you intend to use this approach throughout your app. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. +## Opt-out of Floating UI + +When you use the `Tooltip.Content` component, Bits UI uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger, similar to other popover-like components. + +You can opt-out of this behavior by instead using the `Tooltip.ContentStatic` component. This component does not use Floating UI and leaves positioning the content entirely up to you. + +```svelte /Tooltip.ContentStatic/ + + Hello + + + + +``` + + + +When using the `Tooltip.ContentStatic` component, the `Tooltip.Arrow` component will not be rendered relative to it as it is designed to be used with `Tooltip.Content`. + + + 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 df766436f..6f5e7bca3 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 @@ -54,21 +54,25 @@ preventScroll={false} side="top" sideOffset={10} - class="z-50 max-h-[400px] max-w-[400px] overflow-auto rounded-card border border-border bg-background p-4 shadow-popover" + class="z-50 rounded-card border border-border bg-background p-4 shadow-popover" > - - {@html parseTypeDef(typeDef)} - +
+ + {@html parseTypeDef(typeDef)} + +
{:else} {@const TypeDef = typeDef} -
+
diff --git a/sites/docs/src/lib/components/callout.svelte b/sites/docs/src/lib/components/callout.svelte new file mode 100644 index 000000000..877441200 --- /dev/null +++ b/sites/docs/src/lib/components/callout.svelte @@ -0,0 +1,43 @@ + + + + {@const Icon = iconMap[type]} + + + {#if title} + + {title} + + {/if} + + + {@render children?.()} + + diff --git a/sites/docs/src/lib/components/index.ts b/sites/docs/src/lib/components/index.ts index 7fc5c47b4..2908a174e 100644 --- a/sites/docs/src/lib/components/index.ts +++ b/sites/docs/src/lib/components/index.ts @@ -17,3 +17,4 @@ export { default as Metadata } from "./metadata.svelte"; export { default as ComponentPreviewV2 } from "./component-preview-v2.svelte"; export { default as ComponentPreviewMin } from "./component-preview-min.svelte"; export { default as DemoContainer } from "./demo-container.svelte"; +export { default as Callout } from "./callout.svelte"; diff --git a/sites/docs/src/lib/components/markdown/a.svelte b/sites/docs/src/lib/components/markdown/a.svelte index fc4f76403..85795c25f 100644 --- a/sites/docs/src/lib/components/markdown/a.svelte +++ b/sites/docs/src/lib/components/markdown/a.svelte @@ -9,6 +9,6 @@ const target = $derived(!internal ? "_blank" : undefined); - + {@render children?.()} diff --git a/sites/docs/src/lib/components/markdown/h4.svelte b/sites/docs/src/lib/components/markdown/h4.svelte index 2102afbd6..7e2eb7af4 100644 --- a/sites/docs/src/lib/components/markdown/h4.svelte +++ b/sites/docs/src/lib/components/markdown/h4.svelte @@ -5,6 +5,6 @@ let { class: className, children, ...restProps }: HTMLAttributes = $props(); -

+

{@render children?.()}

diff --git a/sites/docs/src/lib/components/markdown/li.svelte b/sites/docs/src/lib/components/markdown/li.svelte index a3fd544d9..5bcde8edf 100644 --- a/sites/docs/src/lib/components/markdown/li.svelte +++ b/sites/docs/src/lib/components/markdown/li.svelte @@ -4,6 +4,6 @@ let { class: className, children, ...restProps }: HTMLAttributes = $props(); -
  • +
  • {@render children?.()}
  • diff --git a/sites/docs/src/lib/components/markdown/ul.svelte b/sites/docs/src/lib/components/markdown/ul.svelte index 7d1105cb4..334244930 100644 --- a/sites/docs/src/lib/components/markdown/ul.svelte +++ b/sites/docs/src/lib/components/markdown/ul.svelte @@ -5,6 +5,6 @@ let { class: className, children, ...restProps }: HTMLAttributes = $props(); -
      +
        {@render children?.()}
      diff --git a/sites/docs/src/lib/components/toc/table-of-contents.svelte b/sites/docs/src/lib/components/toc/table-of-contents.svelte index face7d2bc..0a8cb7a41 100644 --- a/sites/docs/src/lib/components/toc/table-of-contents.svelte +++ b/sites/docs/src/lib/components/toc/table-of-contents.svelte @@ -16,8 +16,8 @@ }); -
      -