diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index 77880796c..6c53b68eb 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -21,7 +21,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: Build site run: pnpm build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f05e6bba1..bc03b7bf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: Run svelte-check run: pnpm check @@ -51,7 +51,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - run: pnpm lint @@ -66,7 +66,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: Build packages run: pnpm build:packages diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 82555e018..cf2b0871c 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -23,7 +23,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: Build site run: pnpm build diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index ae468d686..7079801ca 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -18,7 +18,7 @@ jobs: cache: pnpm - name: install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: build run: pnpm build:packages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d864b6f4b..bb3d7a38c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile --prefer-offline - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/.gitignore b/.gitignore index 9d81dc2ff..0528e139b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,6 @@ web_modules/ .contentlayer docs/.velite docs/static/docs/**/*.txt -docs/static/llms.txt \ No newline at end of file +docs/static/llms.txt +docs/src/routes/api/demos.json/stackblitz-files.json +docs/src/routes/api/demos.json/demos.json \ No newline at end of file diff --git a/docs/content/components/calendar.md b/docs/content/components/calendar.md index d49b87259..201915255 100644 --- a/docs/content/components/calendar.md +++ b/docs/content/components/calendar.md @@ -319,7 +319,9 @@ The calendar will automatically format the content of the calendar according to ### Week Starts On -The calendar will automatically format the content of the calendar according to the `weekStartsOn` prop, which defaults to `0`, but can be changed to any day of the week, where `0` is Sunday and `6` is Saturday. +The calendar will automatically format the content of the calendar according to the `locale`, which will determine what day of the week is the first day of the week. + +You can also override this by setting the `weekStartsOn` prop, where `0` is Sunday and `6` is Saturday to force a consistent first day of the week across all locales. ```svelte diff --git a/docs/content/components/combobox.md b/docs/content/components/combobox.md index a96dc5ddc..c9f73acc7 100644 --- a/docs/content/components/combobox.md +++ b/docs/content/components/combobox.md @@ -4,7 +4,7 @@ description: Enables users to pick from a list of options displayed in a dropdow --- @@ -41,6 +41,9 @@ The Combobox component is composed of several sub-components, each with a specif - **Separator**: A visual separator between items. - **Content**: The dropdown container that displays the items. It uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger. - **ContentStatic**: An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. +- **Viewport**: The visible area of the dropdown content, used to determine the size and scroll behavior. +- **ScrollUpButton**: A button that scrolls the content up when the content is larger than the viewport. +- **ScrollDownButton**: A button that scrolls the content down when the content is larger than the viewport. - **Arrow**: An arrow element that points to the trigger when using the `Combobox.Content` component. ## Structure @@ -75,20 +78,18 @@ It's recommended to use the `Combobox` primitives to build your own custom combo - + + Open {#each filteredItems as item, i (i + item.value)} - + {#snippet children({ selected })} {item.label} {selected ? "✅" : ""} @@ -143,7 +149,7 @@ It's recommended to use the `Combobox` primitives to build your own custom combo ]; - + ``` ## Managing Value State @@ -253,8 +259,8 @@ You can opt-out of this behavior by instead using the `Combobox.ContentStatic` c - + @@ -298,18 +304,32 @@ The `Combobox.ScrollUpButton` and `Combobox.ScrollDownButton` components are use You must use the `Combobox.Viewport` component when using the scroll buttons. +### Custom Scroll Delay + +The initial and subsequent scroll delays can be controlled using the `delay` prop on the buttons. + +For example, we can use the [`cubicOut`](https://svelte.dev/docs/svelte/svelte-easing#cubicOut) easing function from Svelte to create a smooth scrolling effect that speeds up over time. + + + +{#snippet preview()} + +{/snippet} + + + ## 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. +If you don't want to use the [scroll buttons](#scroll-updown-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`. +To prevent the user from scrolling outside of the `Combobox.Content` component when open, you can set the `preventScroll` prop to `true`. -```svelte /preventScroll={false}/ - +```svelte /preventScroll={true}/ + ``` diff --git a/docs/content/components/command.md b/docs/content/components/command.md index 837334817..65f38968b 100644 --- a/docs/content/components/command.md +++ b/docs/content/components/command.md @@ -55,7 +55,7 @@ Here's an overview of how the Command component is structured in code: - + @@ -342,4 +342,19 @@ command.updateSelectedByItem(-1); // previous item ``` +## Common Mistakes + +### Duplicate `value`s + +The value of each `Command.Item` **_must_** be unique. If you have two items with the same value, the component will not be able to determine which one to select, causing unexpected behavior when navigating with the keyboard or hovering with the mouse. + +If the text content of two items are the same for one reason or another, you should use the `value` prop to set a unique value for each item. When a `value` is set, the text content is used for display purposes only. The `value` prop is used for filtering and selection. + +A common pattern is to postfix the `value` with something unique, like an ID or a number so that filtering will still match the value. + +```svelte +My Item +My Item +``` + diff --git a/docs/content/components/context-menu.md b/docs/content/components/context-menu.md index 5b5b83ceb..c34b37468 100644 --- a/docs/content/components/context-menu.md +++ b/docs/content/components/context-menu.md @@ -185,6 +185,34 @@ Use a [Function Binding](https://svelte.dev/docs/svelte/bind#Function-bindings) ``` +## Radio Groups + +You can combine the `ContextMenu.RadioGroup` and `ContextMenu.RadioItem` components to create a radio group within a menu. + +```svelte + + + + {#each values as value} + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + {value} + {/snippet} + + {/each} + +``` + +See the [RadioGroup](#radiogroup) and [RadioItem](#radioitem) APIs for more information. + ## Checkbox Items You can use the `ContextMenu.CheckboxItem` component to create a `menuitemcheckbox` element to add checkbox functionality to menu items. @@ -210,33 +238,47 @@ You can use the `ContextMenu.CheckboxItem` component to create a `menuitemcheckb See the [CheckboxItem API](#checkboxitem) for more information. -## Radio Groups +## Checkbox Groups -You can combine the `ContextMenu.RadioGroup` and `ContextMenu.RadioItem` components to create a radio group within a menu. +You can use the `ContextMenu.CheckboxGroup` component around a set of `ContextMenu.CheckboxItem` components to create a checkbox group within a menu, where the `value` prop is an array of the selected values. ```svelte - - {#each values as value} - - {#snippet children({ checked })} - {#if checked} - ✅ - {/if} - {value} - {/snippet} - - {/each} - + + Favorite color + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Red + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Blue + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Green + {/snippet} + + ``` -See the [RadioGroup](#radiogroup) and [RadioItem](#radioitem) APIs for more information. +The `value` state does not persist between menu open/close cycles. To persist the state, you must store it in a `$state` variable and pass it to the `value` prop. ## Nested Menus diff --git a/docs/content/components/dropdown-menu.md b/docs/content/components/dropdown-menu.md index 2c0e54582..e3e7f1512 100644 --- a/docs/content/components/dropdown-menu.md +++ b/docs/content/components/dropdown-menu.md @@ -199,6 +199,35 @@ The `DropdownMenu.GroupHeading` component must be a child of either a `DropdownM ``` +## Radio Groups + +You can combine the `DropdownMenu.RadioGroup` and `DropdownMenu.RadioItem` components to create a radio group within a menu. + +```svelte + + + + Favorite number + {#each values as value} + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + {value} + {/snippet} + + {/each} + +``` + +The `value` state does not persist between menu open/close cycles. To persist the state, you must store it in a `$state` variable and pass it to the `value` prop. + ## Checkbox Items You can use the `DropdownMenu.CheckboxItem` component to create a `menuitemcheckbox` element to add checkbox functionality to menu items. @@ -224,31 +253,44 @@ You can use the `DropdownMenu.CheckboxItem` component to create a `menuitemcheck The `checked` state does not persist between menu open/close cycles. To persist the state, you must store it in a `$state` variable and pass it to the `checked` prop. -## Radio Groups +## Checkbox Groups -You can combine the `DropdownMenu.RadioGroup` and `DropdownMenu.RadioItem` components to create a radio group within a menu. +You can use the `DropdownMenu.CheckboxGroup` component around a set of `DropdownMenu.CheckboxItem` components to create a checkbox group within a menu, where the `value` prop is an array of the selected values. ```svelte - - Favorite number - {#each values as value} - - {#snippet children({ checked })} - {#if checked} - ✅ - {/if} - {value} - {/snippet} - - {/each} - + + Favorite color + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Red + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Blue + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Green + {/snippet} + + ``` The `value` state does not persist between menu open/close cycles. To persist the state, you must store it in a `$state` variable and pass it to the `value` prop. diff --git a/docs/content/components/menubar.md b/docs/content/components/menubar.md index c0568a5cd..9c8991d39 100644 --- a/docs/content/components/menubar.md +++ b/docs/content/components/menubar.md @@ -195,6 +195,32 @@ Use a [Function Binding](https://svelte.dev/docs/svelte/bind#Function-bindings) ``` +## Radio Groups + +You can combine the `Menubar.RadioGroup` and `Menubar.RadioItem` components to create a radio group within a menu. + +```svelte + + + + {#each values as value} + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + {value} + {/snippet} + + {/each} + +``` + ## Checkbox Items You can use the `Menubar.CheckboxItem` component to create a `menuitemcheckbox` element to add checkbox functionality to menu items. @@ -218,32 +244,48 @@ You can use the `Menubar.CheckboxItem` component to create a `menuitemcheckbox` ``` -## Radio Groups +## Checkbox Groups -You can combine the `Menubar.RadioGroup` and `Menubar.RadioItem` components to create a radio group within a menu. +You can use the `Menubar.CheckboxGroup` component around a set of `Menubar.CheckboxItem` components to create a checkbox group within a menu, where the `value` prop is an array of the selected values. ```svelte - - {#each values as value} - - {#snippet children({ checked })} - {#if checked} - ✅ - {/if} - {value} - {/snippet} - - {/each} - + + Favorite color + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Red + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Blue + {/snippet} + + + {#snippet children({ checked })} + {#if checked} + ✅ + {/if} + Green + {/snippet} + + ``` +The `value` state does not persist between menu open/close cycles. To persist the state, you must store it in a `$state` variable and pass it to the `value` prop. + ## Nested Menus You can create nested menus using the `Menubar.Sub` component to create complex menu structures. diff --git a/docs/content/components/navigation-menu.md b/docs/content/components/navigation-menu.md index 877d182a2..781d75732 100644 --- a/docs/content/components/navigation-menu.md +++ b/docs/content/components/navigation-menu.md @@ -4,7 +4,7 @@ description: A list of links that allow users to navigate between pages of a web --- @@ -113,27 +113,18 @@ You can use the optional `Indicator` component to highlight the currently active ### Submenus You can create a submenu by nesting your navigation menu and using the `Navigation.Sub` component in place of `NavigationMenu.Root`. -Submenus work differently than the `Root` menus and are more similar to [Tabs](/docs/components/tabs) in that one item should always be active, so be sure to assign and pass a `value` prop. ```svelte Item one - Item one content - - - Item two - + - - Sub item one - Sub item one content - - - Sub item two - Sub item two content + + Subitem one + Subitem one content @@ -143,6 +134,51 @@ Submenus work differently than the `Root` menus and are more similar to [Tabs](/ ``` + + +### Submenus with Viewport + +You can use the `NavigationMenu.Viewport` component inside of a `NavigationMenu.Sub` to create a viewport dedicated to that submenu. + +```svelte + + + + Item one + + Item one content + + + + Item two + + Item two content + + + + + +``` + +### No Viewport + +The `NavigationMenu.Viewport` component provides a way to transition between `NavigationMenu.Content` without the need for a full close/open animation between them, however, this is completely optional and you don't need to use it. + + + +{#snippet preview()} + +{/snippet} + + + ### Advanced Animation We expose `--bits-navigation-menu-viewport-[width|height]` and `data-motion['from-start'|'to-start'|'from-end'|'to-end']` to allow you to animate the `NavigationMenu.Viewport` size and `NavigationMenu.Content` position based on the enter/exit direction. @@ -270,4 +306,29 @@ You may wish for the links in the Navigation Menu to persist in the DOM, regardl +### Open on Hover + +By default, the `NavigationMenu.Item` will open its `NavigationMenu.Content` when the `NavigationMenu.Trigger` is hovered. You can disable this by passing `openOnHover={false}` to the `NavigationMenu.Item`. + + + +Unlike the default behavior, when `openOnHover` is `false`, the menu will not close when the pointer moves outside of the `NavigationMenu.Content` and will instead require the user to interact outside of the menu or press escape to close it. + + + +```svelte /openOnHover={false}/ + + Item one + Item one content + +``` + + + +{#snippet preview()} + +{/snippet} + + + diff --git a/docs/content/components/pagination.md b/docs/content/components/pagination.md index 01d7e1a06..2dc63f96a 100644 --- a/docs/content/components/pagination.md +++ b/docs/content/components/pagination.md @@ -48,7 +48,7 @@ Use `bind:page` for simple, automatic state synchronization: - + ``` diff --git a/docs/content/components/select.md b/docs/content/components/select.md index c1aacb42a..0db411b28 100644 --- a/docs/content/components/select.md +++ b/docs/content/components/select.md @@ -4,7 +4,7 @@ description: Enables users to choose from a list of options presented in a dropd --- @@ -42,6 +42,9 @@ The Select component is composed of several sub-components, each with a specific - **Separator**: A visual separator between items. - **Content**: The dropdown container that displays the items. It uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger. - **ContentStatic** (Optional): An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. +- **Viewport**: The visible area of the dropdown content, used to determine the size and scroll behavior. +- **ScrollUpButton**: A button that scrolls the content up when the content is larger than the viewport. +- **ScrollDownButton**: A button that scrolls the content down when the content is larger than the viewport. - **Arrow**: An arrow element that points to the trigger when using the `Combobox.Content` component. ## Structure @@ -317,9 +320,23 @@ The `Select.ScrollUpButton` and `Select.ScrollDownButton` components are used to You must use the `Select.Viewport` component when using the scroll buttons. +### Custom Scroll Delay + +The initial and subsequent scroll delays can be controlled using the `delay` prop on the buttons. + +For example, we can use the [`cubicOut`](https://svelte.dev/docs/svelte/svelte-easing#cubicOut) easing function from Svelte to create a smooth scrolling effect that speeds up over time. + + + +{#snippet preview()} + +{/snippet} + + + ## 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 `Select.Scroll[Up|Down]Button` components and the `Select.Viewport` component. +If you don't want to use the [scroll buttons](#scroll-updown-buttons) and prefer to use the standard scrollbar/overflow behavior, you can omit the `Select.Scroll[Up|Down]Button` components and the `Select.Viewport` component. You'll need to set a height on the `Select.Content` component and appropriate `overflow` styles to enable scrolling. diff --git a/docs/content/components/slider.md b/docs/content/components/slider.md index f4f0d7558..a2f9911a7 100644 --- a/docs/content/components/slider.md +++ b/docs/content/components/slider.md @@ -4,7 +4,7 @@ description: Allows users to select a value from a continuous range by sliding a --- @@ -151,11 +151,11 @@ If the `value` prop has more than one value, the slider will render multiple thu {#snippet children({ ticks, thumbs })} - {#each thumbs as index} + {#each thumbs as index (index)} {/each} - {#each ticks as index} + {#each ticks as index (index)} {/each} {/snippet} @@ -164,9 +164,17 @@ If the `value` prop has more than one value, the slider will render multiple thu To determine the number of ticks that will be rendered, you can simply divide the `max` value by the `step` value. + + +{#snippet preview()} + +{/snippet} + + + ## Single Type -Set the `type` prop to `"single"` to allow only one accordion item to be open at a time. +Set the `type` prop to `"single"` to allow only one slider handle. ```svelte /type="single"/ @@ -182,7 +190,7 @@ Set the `type` prop to `"single"` to allow only one accordion item to be open at ## Multiple Type -Set the `type` prop to `"multiple"` to allow multiple accordion items to be open at the same time. +Set the `type` prop to `"multiple"` to allow multiple slider handles. ```svelte /type="multiple"/ diff --git a/docs/package.json b/docs/package.json index ac6740801..2e670202b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,9 +5,10 @@ "license": "MIT", "private": true, "scripts": { - "dev": "concurrently \"pnpm:dev:content\" \"pnpm:dev:svelte\"", + "dev": "pnpm --reporter append-only --color \"/dev:/\"", "dev:content": "velite dev --watch", "dev:svelte": "vite dev", + "dev:demos": "pnpm build:demos", "build": "pnpm build:content && pnpm build:search && pnpm build:demos && vite build && pnpm build:llms && vite build", "build:llms": "pnpx tsx ./other/build-llms-txt.ts", "build:content": "velite && node ./other/update-velite-output.js", @@ -34,7 +35,6 @@ "@types/unist": "^3.0.3", "autoprefixer": "^10.4.20", "clsx": "^2.1.0", - "concurrently": "^8.2.2", "consola": "^3.4.0", "eruda": "^3.4.1", "jsdom": "^24.1.3", diff --git a/docs/src/lib/components/callout.svelte b/docs/src/lib/components/callout.svelte index 5a997f6db..2f81b08f6 100644 --- a/docs/src/lib/components/callout.svelte +++ b/docs/src/lib/components/callout.svelte @@ -25,7 +25,9 @@ {/if} - + {@render children?.()} diff --git a/docs/src/lib/components/demos/calendar-demo.svelte b/docs/src/lib/components/demos/calendar-demo.svelte index 6256f611e..7e772c7b1 100644 --- a/docs/src/lib/components/demos/calendar-demo.svelte +++ b/docs/src/lib/components/demos/calendar-demo.svelte @@ -4,16 +4,11 @@ import CaretRight from "phosphor-svelte/lib/CaretRight"; import { getLocalTimeZone, today } from "@internationalized/date"; - const isDateUnavailable: Calendar.RootProps["isDateUnavailable"] = (date) => { - return date.day === 17 || date.day === 18; - }; - let value = $state(today(getLocalTimeZone())); + import { Combobox } from "bits-ui"; + import CaretUpDown from "phosphor-svelte/lib/CaretUpDown"; + import Check from "phosphor-svelte/lib/Check"; + import OrangeSlice from "phosphor-svelte/lib/OrangeSlice"; + import CaretDoubleUp from "phosphor-svelte/lib/CaretDoubleUp"; + import CaretDoubleDown from "phosphor-svelte/lib/CaretDoubleDown"; + import { cubicOut } from "svelte/easing"; + + const fruits = [ + { value: "mango", label: "Mango" }, + { value: "watermelon", label: "Watermelon" }, + { value: "apple", label: "Apple" }, + { value: "pineapple", label: "Pineapple" }, + { value: "orange", label: "Orange" }, + { value: "grape", label: "Grape" }, + { value: "strawberry", label: "Strawberry" }, + { value: "banana", label: "Banana" }, + { value: "kiwi", label: "Kiwi" }, + { value: "peach", label: "Peach" }, + { value: "cherry", label: "Cherry" }, + { value: "blueberry", label: "Blueberry" }, + { value: "raspberry", label: "Raspberry" }, + { value: "blackberry", label: "Blackberry" }, + { value: "plum", label: "Plum" }, + { value: "apricot", label: "Apricot" }, + { value: "pear", label: "Pear" }, + { value: "grapefruit", label: "Grapefruit" }, + ]; + + // Duplicate the menu items a couple of times to show off scrolling a big list + const baseFruits = [...fruits]; + for (let i = 0; i < 10; i++) { + for (let baseTheme of baseFruits) { + fruits.push({ ...baseTheme, value: baseTheme.value + i }); + } + } + + let searchValue = $state(""); + + const filteredFruits = $derived( + searchValue === "" + ? fruits + : fruits.filter((fruit) => + fruit.label.toLowerCase().includes(searchValue.toLowerCase()) + ) + ); + + function autoScrollDelay(tick: number) { + const maxDelay = 200; + const minDelay = 25; + const steps = 30; + + const progress = Math.min(tick / steps, 1); + // Use the cubicOut easing function from svelte/easing + return maxDelay - (maxDelay - minDelay) * cubicOut(progress); + } + + + { + if (!o) searchValue = ""; + }} +> +
+ + (searchValue = e.currentTarget.value)} + class="h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm" + placeholder="Search a fruit" + aria-label="Search a fruit" + /> + + + +
+ + + + + + + {#each filteredFruits as fruit, i (i + fruit.value)} + + {#snippet children({ selected })} + {fruit.label} + {#if selected} +
+ +
+ {/if} + {/snippet} +
+ {:else} + + No results found, try again. + + {/each} +
+ + + +
+
+
diff --git a/docs/src/lib/components/demos/index.ts b/docs/src/lib/components/demos/index.ts index e41a189c1..c2dd1bfdd 100644 --- a/docs/src/lib/components/demos/index.ts +++ b/docs/src/lib/components/demos/index.ts @@ -17,6 +17,7 @@ export { default as CheckboxDemoGroup } from "./checkbox-demo-group.svelte"; export { default as CollapsibleDemo } from "./collapsible-demo.svelte"; export { default as CollapsibleDemoTransitions } from "./collapsible-demo-transitions.svelte"; export { default as ComboboxDemo } from "./combobox-demo.svelte"; +export { default as ComboboxDemoAutoScrollDelay } from "./combobox-demo-auto-scroll-delay.svelte"; export { default as ComboboxDemoTransition } from "./combobox-demo-transition.svelte"; export { default as CommandDemo } from "./command-demo.svelte"; export { default as CommandDemoDialog } from "./command-demo-dialog.svelte"; @@ -35,6 +36,7 @@ export { default as LabelDemo } from "./label-demo.svelte"; export { default as LinkPreviewDemo } from "./link-preview-demo.svelte"; export { default as LinkPreviewDemoTransition } from "./link-preview-demo-transition.svelte"; export { default as SelectDemo } from "./select-demo.svelte"; +export { default as SelectDemoAutoScrollDelay } from "./select-demo-auto-scroll-delay.svelte"; export { default as SelectDemoCustom } from "./select-demo-custom.svelte"; export { default as SelectDemoCustomAnchor } from "./select-demo-custom-anchor.svelte"; export { default as SelectDemoMultiple } from "./select-demo-multiple.svelte"; @@ -44,6 +46,10 @@ export { default as MeterDemo } from "./meter-demo.svelte"; export { default as MeterDemoCustom } from "./meter-demo-custom.svelte"; export { default as NavigationMenuDemo } from "./navigation-menu-demo.svelte"; export { default as NavigationMenuDemoForceMount } from "./navigation-menu-demo-force-mount.svelte"; +export { default as NavigationMenuDemoNoHover } from "./navigation-menu-no-hover-demo.svelte"; +export { default as NavigationMenuDemoSubmenu } from "./navigation-menu-submenu-demo.svelte"; +export { default as NavigationMenuDemoSubmenuViewport } from "./navigation-menu-submenu-viewport-demo.svelte"; +export { default as NavigationMenuDemoNoViewport } from "./navigation-menu-no-viewport-demo.svelte"; export { default as PaginationDemo } from "./pagination-demo.svelte"; export { default as PinInputDemo } from "./pin-input-demo.svelte"; export { default as PopoverDemo } from "./popover-demo.svelte"; @@ -57,6 +63,7 @@ export { default as ScrollAreaDemoCustom } from "./scroll-area-demo-custom.svelt export { default as SeparatorDemo } from "./separator-demo.svelte"; export { default as SliderDemo } from "./slider-demo.svelte"; export { default as SliderDemoMultiple } from "./slider-demo-multiple.svelte"; +export { default as SliderDemoTicks } from "./slider-demo-ticks.svelte"; export { default as SwitchDemo } from "./switch-demo.svelte"; export { default as SwitchDemoCustom } from "./switch-demo-custom.svelte"; export { default as TabsDemo } from "./tabs-demo.svelte"; @@ -68,4 +75,5 @@ export { default as TooltipDemoCustom } from "./tooltip-demo-custom.svelte"; export { default as TooltipDemoCustomAnchor } from "./tooltip-demo-custom-anchor.svelte"; export { default as TooltipDemoDelayDuration } from "./tooltip-demo-delay-duration.svelte"; export { default as TooltipDemoTransition } from "./tooltip-demo-transition.svelte"; +export { default as TooltipDemoGroup } from "./tooltip-demo-group.svelte"; export { default as DateFieldDemoCustom } from "./date-field-demo-custom.svelte"; diff --git a/docs/src/lib/components/demos/navigation-menu-demo.svelte b/docs/src/lib/components/demos/navigation-menu-demo.svelte index ac341c33d..d1b1edf42 100644 --- a/docs/src/lib/components/demos/navigation-menu-demo.svelte +++ b/docs/src/lib/components/demos/navigation-menu-demo.svelte @@ -69,7 +69,7 @@ Getting started Components + import { NavigationMenu } from "bits-ui"; + import CaretDown from "phosphor-svelte/lib/CaretDown"; + import { cn } from "$lib/utils/styles.js"; + + const components: { title: string; href: string; description: string }[] = [ + { + title: "Alert Dialog", + href: "/docs/components/alert-dialog", + description: + "A modal dialog that interrupts the user with important content and expects a response.", + }, + { + title: "Link Preview", + href: "/docs/components/link-preview", + description: "For sighted users to preview content available behind a link.", + }, + { + title: "Progress", + href: "/docs/components/progress", + description: + "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.", + }, + { + title: "Scroll Area", + href: "/docs/components/scroll-area", + description: "Visually or semantically separates content.", + }, + { + title: "Tabs", + href: "/docs/components/tabs", + description: + "A set of layered sections of content—known as tab panels—that are displayed one at a time.", + }, + { + title: "Tooltip", + href: "/docs/components/tooltip", + description: + "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.", + }, + ]; + + type ListItemProps = { + className?: string; + title: string; + href: string; + content: string; + }; + + +{#snippet ListItem({ className, title, content, href }: ListItemProps)} +
  • + +
    {title}
    +

    + {content} +

    +
    +
  • +{/snippet} + + + + + + Getting started + + +
      +
    • + + +
      Bits UI
      +

      + The headless components for Svelte. +

      +
      +
    • + + {@render ListItem({ + href: "/docs", + title: "Introduction", + content: "Headless components for Svelte and SvelteKit", + })} + {@render ListItem({ + href: "/docs/getting-started", + title: "Getting Started", + content: "How to install and use Bits UI", + })} + {@render ListItem({ + href: "/docs/styling", + title: "Styling", + content: "How to style Bits UI components", + })} +
    +
    +
    + + + Components + + +
      + {#each components as component (component.title)} + {@render ListItem({ + href: component.href, + title: component.title, + content: component.description, + })} + {/each} +
    +
    +
    + + + + Docs + + + +
    +
    +
    +
    + +
    +
    diff --git a/docs/src/lib/components/demos/navigation-menu-no-viewport-demo.svelte b/docs/src/lib/components/demos/navigation-menu-no-viewport-demo.svelte new file mode 100644 index 000000000..ed0d05a14 --- /dev/null +++ b/docs/src/lib/components/demos/navigation-menu-no-viewport-demo.svelte @@ -0,0 +1,153 @@ + + +{#snippet ListItem({ className, title, content, href }: ListItemProps)} +
  • + +
    {title}
    +

    + {content} +

    +
    +
  • +{/snippet} + + + + + + Getting started + + +
      +
    • + + +
      Bits UI
      +

      + The headless components for Svelte. +

      +
      +
    • + + {@render ListItem({ + href: "/docs", + title: "Introduction", + content: "Headless components for Svelte and SvelteKit", + })} + {@render ListItem({ + href: "/docs/getting-started", + title: "Getting Started", + content: "How to install and use Bits UI", + })} + {@render ListItem({ + href: "/docs/styling", + title: "Styling", + content: "How to style Bits UI components", + })} +
    +
    +
    + + + Components + + +
      + {#each components as component (component.title)} + {@render ListItem({ + href: component.href, + title: component.title, + content: component.description, + })} + {/each} +
    +
    +
    + + + + Docs + + +
    +
    diff --git a/docs/src/lib/components/demos/navigation-menu-submenu-demo.svelte b/docs/src/lib/components/demos/navigation-menu-submenu-demo.svelte new file mode 100644 index 000000000..b4f24efa4 --- /dev/null +++ b/docs/src/lib/components/demos/navigation-menu-submenu-demo.svelte @@ -0,0 +1,230 @@ + + +{#snippet ListItem({ className, title, content, href }: ListItemProps)} + +
    {title}
    +

    + {content} +

    +
    +{/snippet} + +{#snippet SubmenuItem({ className, title, value, items }: SubmenuItemProps)} + + +
    {title}
    +
    + +
      + {#each items as item (item.title)} + {@render ListItem({ + href: item.href, + title: item.title, + content: item.description, + })} + {/each} +
    +
    +
    +{/snippet} + + + + + + Getting started + + +
      +
    • + + +
      Bits UI
      +

      + The headless components for Svelte. +

      +
      +
    • + + {@render ListItem({ + href: "/docs", + title: "Introduction", + content: "Headless components for Svelte and SvelteKit", + })} + {@render ListItem({ + href: "/docs/getting-started", + title: "Getting Started", + content: "How to install and use Bits UI", + })} + {@render ListItem({ + href: "/docs/styling", + title: "Styling", + content: "How to style Bits UI components", + })} +
    +
    +
    + + + Features + + +
    + + +
  • +
    + Components +
    +
  • + + {#each components as component (component.title)} + + {@render ListItem({ + href: component.href, + title: component.title, + content: component.description, + })} + + {/each} +
    + {@render SubmenuItem({ + title: "Utilities", + value: "utilities", + items: utilities, + })} + + {@render SubmenuItem({ + title: "Type Helpers", + value: "type-helpers", + items: typeHelpers, + })} +
    +
    +
    +
    +
    +
    + + + + Docs + + +
    +
    diff --git a/docs/src/lib/components/demos/navigation-menu-submenu-viewport-demo.svelte b/docs/src/lib/components/demos/navigation-menu-submenu-viewport-demo.svelte new file mode 100644 index 000000000..e036f51a2 --- /dev/null +++ b/docs/src/lib/components/demos/navigation-menu-submenu-viewport-demo.svelte @@ -0,0 +1,229 @@ + + +{#snippet ListItem({ className, title, content, href }: ListItemProps)} + +
    {title}
    +

    + {content} +

    +
    +{/snippet} + +{#snippet SubmenuItem({ className, title, value, items }: SubmenuItemProps)} + + +
    {title}
    +
    + +
      + {#each items as item (item.title)} + {@render ListItem({ + href: item.href, + title: item.title, + content: item.description, + })} + {/each} +
    +
    +
    +{/snippet} + + + + + + Getting started + + +
      +
    • + + +
      Bits UI
      +

      + The headless components for Svelte. +

      +
      +
    • + + {@render ListItem({ + href: "/docs", + title: "Introduction", + content: "Headless components for Svelte and SvelteKit", + })} + {@render ListItem({ + href: "/docs/getting-started", + title: "Getting Started", + content: "How to install and use Bits UI", + })} + {@render ListItem({ + href: "/docs/styling", + title: "Styling", + content: "How to style Bits UI components", + })} +
    +
    +
    + + + Features + + +
    + + + +
    + {@render SubmenuItem({ + title: "Utilities", + value: "utilities", + items: utilities, + })} + + {@render SubmenuItem({ + title: "Type Helpers", + value: "type-helpers", + items: typeHelpers, + })} +
    +
    +
    + +
    +
    +
    +
    +
    + + + + Docs + + + +
    +
    +
    +
    + +
    +
    diff --git a/docs/src/lib/components/demos/select-demo-auto-scroll-delay.svelte b/docs/src/lib/components/demos/select-demo-auto-scroll-delay.svelte new file mode 100644 index 000000000..4a3538bf2 --- /dev/null +++ b/docs/src/lib/components/demos/select-demo-auto-scroll-delay.svelte @@ -0,0 +1,105 @@ + + + (value = v)} items={themes}> + + + {selectedLabel} + + + + + + + + + {#each themes as theme, i (i + theme.value)} + + {#snippet children({ selected })} + {theme.label} + {#if selected} +
    + +
    + {/if} + {/snippet} +
    + {/each} +
    + + + +
    +
    +
    diff --git a/docs/src/lib/components/demos/select-demo.svelte b/docs/src/lib/components/demos/select-demo.svelte index dd3e5a540..0f06705cd 100644 --- a/docs/src/lib/components/demos/select-demo.svelte +++ b/docs/src/lib/components/demos/select-demo.svelte @@ -56,7 +56,7 @@ {#each themes as theme, i (i + theme.value)} - + {/if} {/snippet} diff --git a/docs/src/lib/components/demos/slider-demo-multiple.svelte b/docs/src/lib/components/demos/slider-demo-multiple.svelte index 29c1ffe55..205e74f8e 100644 --- a/docs/src/lib/components/demos/slider-demo-multiple.svelte +++ b/docs/src/lib/components/demos/slider-demo-multiple.svelte @@ -21,7 +21,7 @@ {/each} diff --git a/docs/src/lib/components/demos/slider-demo-ticks.svelte b/docs/src/lib/components/demos/slider-demo-ticks.svelte new file mode 100644 index 000000000..062a4102d --- /dev/null +++ b/docs/src/lib/components/demos/slider-demo-ticks.svelte @@ -0,0 +1,36 @@ + + +
    + + {#snippet children({ ticks, thumbs })} + + + + {#each thumbs as thumb} + + {/each} + {#each ticks as tick} + + {/each} + {/snippet} + +
    diff --git a/docs/src/lib/components/demos/slider-demo.svelte b/docs/src/lib/components/demos/slider-demo.svelte index 4da3fbba7..3da94dadc 100644 --- a/docs/src/lib/components/demos/slider-demo.svelte +++ b/docs/src/lib/components/demos/slider-demo.svelte @@ -20,7 +20,7 @@ {/snippet} diff --git a/docs/src/lib/components/demos/tooltip-demo-group.svelte b/docs/src/lib/components/demos/tooltip-demo-group.svelte new file mode 100644 index 000000000..3972e42ac --- /dev/null +++ b/docs/src/lib/components/demos/tooltip-demo-group.svelte @@ -0,0 +1,118 @@ + + +{#snippet tooltipContent({ content }: { content: string })} + + {content} + +{/snippet} + + + + + + + {#snippet child({ props })} + {@const { "data-state": _state, ...rest } = props} + + + + {/snippet} + + {@render tooltipContent({ content: "Bold" })} + + + + + {#snippet child({ props })} + {@const { "data-state": _state, ...rest } = props} + + + + {/snippet} + + {@render tooltipContent({ content: "Italic" })} + + + + + {#snippet child({ props })} + {@const { "data-state": _state, ...rest } = props} + + + + {/snippet} + + {@render tooltipContent({ content: "Strikethrough" })} + + + + + + + + + + + + + + + + + + + +
    + + + Ask AI + +
    +
    +
    diff --git a/docs/src/lib/components/homepage/card-air.svelte b/docs/src/lib/components/homepage/card-air.svelte index 19e9ef4b3..d40f8d7f5 100644 --- a/docs/src/lib/components/homepage/card-air.svelte +++ b/docs/src/lib/components/homepage/card-air.svelte @@ -30,12 +30,12 @@ °C °F diff --git a/docs/src/lib/components/homepage/card-toggle.svelte b/docs/src/lib/components/homepage/card-toggle.svelte index 23885db8b..a933ecfe3 100644 --- a/docs/src/lib/components/homepage/card-toggle.svelte +++ b/docs/src/lib/components/homepage/card-toggle.svelte @@ -38,12 +38,12 @@ > Follow Other @@ -125,7 +125,7 @@ diff --git a/docs/src/lib/components/homepage/home-select.svelte b/docs/src/lib/components/homepage/home-select.svelte index 09b51fd4a..0b58b683e 100644 --- a/docs/src/lib/components/homepage/home-select.svelte +++ b/docs/src/lib/components/homepage/home-select.svelte @@ -20,11 +20,13 @@ {selectedLabel} - + {/each} diff --git a/docs/src/lib/components/homepage/home-switch.svelte b/docs/src/lib/components/homepage/home-switch.svelte index eb5877115..dbaf78f33 100644 --- a/docs/src/lib/components/homepage/home-switch.svelte +++ b/docs/src/lib/components/homepage/home-switch.svelte @@ -21,7 +21,7 @@ {...restProps} > diff --git a/docs/src/lib/components/light-switch.svelte b/docs/src/lib/components/light-switch.svelte index fff8ad644..b52fc11c1 100644 --- a/docs/src/lib/components/light-switch.svelte +++ b/docs/src/lib/components/light-switch.svelte @@ -13,7 +13,7 @@ aria-label="Light Switch" aria-checked={$mode === "light"} title="Toggle {$mode === 'dark' ? 'Dark' : 'Light'} Mode" - class="rounded-input hover:bg-dark-10 focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden relative inline-flex h-10 w-10 items-center justify-center px-2 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2" + class="rounded-input hover:bg-dark-10 focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden relative inline-flex h-10 w-10 cursor-pointer items-center justify-center px-2 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2" > {#if $mode === "light"}

    {@render children?.()} diff --git a/docs/src/lib/components/navigation/sidebar-nav.svelte b/docs/src/lib/components/navigation/sidebar-nav.svelte index 61ff10556..7757599a7 100644 --- a/docs/src/lib/components/navigation/sidebar-nav.svelte +++ b/docs/src/lib/components/navigation/sidebar-nav.svelte @@ -9,7 +9,7 @@ {#if items.length}

    \n\t{/snippet}\n
    \n", - "checkbox-demo-custom": "\n\n\n\t
    \n\t\t\n\t\t\t{#snippet children({ checked, indeterminate })}\n\t\t\t\t
    \n\t\t\t\t\t{#if indeterminate}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else if checked}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
    \n\t\t\t{/snippet}\n\t\t\n\t\t\n\t\t\t{labelText}\n\t\t\n\t
    \n
    \n", - "checkbox-demo-group": "\n\n\n\t\n\t\tNotifications\n\t\n\t
    \n\t\t{@render MyCheckbox({ label: \"Marketing\", value: \"marketing\" })}\n\t\t{@render MyCheckbox({ label: \"Promotions\", value: \"promotions\" })}\n\t\t{@render MyCheckbox({ label: \"News\", value: \"news\" })}\n\t\t{@render MyCheckbox({ label: \"Updates\", value: \"updates\" })}\n\t
    \n
    \n\n{#snippet MyCheckbox({ value, label }: { value: string; label: string })}\n\t{@const id = useId()}\n\t
    \n\t\t\n\t\t\t{#snippet children({ checked, indeterminate })}\n\t\t\t\t
    \n\t\t\t\t\t{#if indeterminate}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else if checked}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
    \n\t\t\t{/snippet}\n\t\t\n\t\t\n\t\t\t{label}\n\t\t\n\t
    \n{/snippet}\n", - "checkbox-demo": "\n\n
    \n\t\n\t\t{#snippet children({ checked, indeterminate })}\n\t\t\t
    \n\t\t\t\t{#if indeterminate}\n\t\t\t\t\t\n\t\t\t\t{:else if checked}\n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t
    \n\t\t{/snippet}\n\t\n\t\n\t\tAccept terms and conditions\n\t\n
    \n", - "collapsible-demo-transitions": "\n\n\n\t
    \n\t\t

    @huntabyte starred 3 repositories

    \n\t\t\n\t\t\t\n\t\t\tToggle\n\t\t\n\t
    \n\n\t\n\t\t{#snippet child({ props, open })}\n\t\t\t{#if open}\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t@huntabyte/bits-ui\n\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t@huntabyte/shadcn-svelte\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t@svecosystem/runed\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t{/if}\n\t\t{/snippet}\n\t
    \n
    \n", - "collapsible-demo": "\n\n\n\t
    \n\t\t

    @huntabyte starred 3 repositories

    \n\t\t\n\t\t\t\n\t\t\tToggle\n\t\t\n\t
    \n\n\t\n\t\t
    \n\t\t\t@huntabyte/bits-ui\n\t\t
    \n\t\t
    \n\t\t\t@huntabyte/shadcn-svelte\n\t\t
    \n\t\t
    \n\t\t\t@svecosystem/runed\n\t\t
    \n\t
    \n
    \n", - "combobox-demo-transition": "\n\n {\n\t\tif (!o) searchValue = \"\";\n\t}}\n>\n\t
    \n\t\t\n\t\t (searchValue = e.currentTarget.value)}\n\t\t\tclass=\"h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm\"\n\t\t\tplaceholder=\"Search a fruit\"\n\t\t\taria-label=\"Search a fruit\"\n\t\t/>\n\t\t\n\t\t\t\n\t\t\n\t
    \n\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#each filteredFruits as fruit, i (i + fruit.value)}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t\t\t\t\t{fruit.label}\n\t\t\t\t\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tNo results found, try again.\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t
    \n\t\n
    \n", - "combobox-demo": "\n\n {\n\t\tif (!o) searchValue = \"\";\n\t}}\n>\n\t
    \n\t\t\n\t\t (searchValue = e.currentTarget.value)}\n\t\t\tclass=\"h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm\"\n\t\t\tplaceholder=\"Search a fruit\"\n\t\t\taria-label=\"Search a fruit\"\n\t\t/>\n\t\t\n\t\t\t\n\t\t\n\t
    \n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{#each filteredFruits as fruit, i (i + fruit.value)}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t{fruit.label}\n\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{:else}\n\t\t\t\t\t\n\t\t\t\t\t\tNo results found, try again.\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
    \n\n", - "command-demo-dialog": "\n\n\n\n\n\t\n\t\tOpen Command Menu ⌘J\n\t\n\t\n\t\t\n\t\t\n\t\t\tCommand Menu\n\t\t\t\n\t\t\t\tThis is the command menu. Use the arrow keys to navigate and press ⌘K to open the\n\t\t\t\tsearch bar.\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tNo results found.\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tSuggestions\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tIntroduction\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tDelegation\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tStyling\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tComponents\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tCalendar\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tRadio Group\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tCombobox\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n", - "command-demo": "\n\n\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\tNo results found.\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tSuggestions\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tIntroduction\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelegation\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tStyling\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tComponents\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tCalendar\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tRadio Group\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tCombobox\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n", - "context-menu-demo-transition": "\n\n\n\t\n\t\t
    \n\t\t\t\n\t\t\tRight click me\n\t\t
    \n\t\n\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tEdit\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tE\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tAdd\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\tN\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tHeader\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tParagraph\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tCodeblock\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tList\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tTask\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tDuplicate\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tD\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "context-menu-demo": "\n\n\n\t\n\t\t
    \n\t\t\t\n\t\t\tRight click me\n\t\t
    \n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tEdit\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t⌘\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tE\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\tAdd\n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tN\n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tHeader\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tParagraph\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tCodeblock\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tList\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tTask\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
    \n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tDuplicate\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t⌘\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tD\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tDelete\n\t\t\t\t
    \n\t\t\t\n\t\t\n\t
    \n
    \n", - "date-field-demo-custom": "\n\n\n\t
    \n\t\t{labelText}\n\t\t\n\t\t\t{#snippet children({ segments })}\n\t\t\t\t{#each segments as { part, value }}\n\t\t\t\t\t
    \n\t\t\t\t\t\t{#if part === \"literal\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
    \n\t\t\t\t{/each}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "date-field-demo": "\n\n\n\t
    \n\t\tBirthday\n\t\t\n\t\t\t{#snippet children({ segments })}\n\t\t\t\t{#each segments as { part, value }}\n\t\t\t\t\t
    \n\t\t\t\t\t\t{#if part === \"literal\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
    \n\t\t\t\t{/each}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "date-picker-demo": "\n\n\n\t
    \n\t\tBirthday\n\t\t\n\t\t\t{#snippet children({ segments })}\n\t\t\t\t{#each segments as { part, value }}\n\t\t\t\t\t
    \n\t\t\t\t\t\t{#if part === \"literal\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
    \n\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t{/snippet}\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t{#snippet children({ months, weekdays })}\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\t\t{#each months as month}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#each weekdays as day}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t
    {day.slice(0, 2)}
    \n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each month.weeks as weekDates}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#each weekDates as date}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{date.day}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t
    \n\t\t\t\t{/snippet}\n\t\t\t\n\t\t\n\t\n
    \n", - "date-range-field-demo": "\n\n\n\t\n\t\tHotel dates\n\t\n\t\n\t\t{#each [\"start\", \"end\"] as const as type}\n\t\t\t\n\t\t\t\t{#snippet children({ segments })}\n\t\t\t\t\t{#each segments as { part, value }}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{#if part === \"literal\"}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
    \n\t\t\t\t\t{/each}\n\t\t\t\t{/snippet}\n\t\t\t
    \n\t\t\t{#if type === \"start\"}\n\t\t\t\t
    –⁠⁠⁠⁠⁠
    \n\t\t\t{/if}\n\t\t{/each}\n\t\n
    \n", - "date-range-picker-demo": "\n\n\n\tRental Days\n\t\n\t\t{#each [\"start\", \"end\"] as const as type}\n\t\t\t\n\t\t\t\t{#snippet children({ segments })}\n\t\t\t\t\t{#each segments as { part, value }}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{#if part === \"literal\"}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
    \n\t\t\t\t\t{/each}\n\t\t\t\t{/snippet}\n\t\t\t
    \n\t\t\t{#if type === \"start\"}\n\t\t\t\t
    –⁠⁠⁠⁠⁠
    \n\t\t\t{/if}\n\t\t{/each}\n\n\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t{#snippet children({ months, weekdays })}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t{#each months as month}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each weekdays as day}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t
    {day.slice(0, 2)}
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#each month.weeks as weekDates}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#each weekDates as date}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\t\t{date.day}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t{/snippet}\n\t\t\n\t
    \n\n", - "dialog-demo-custom": "\n\n\n\t\n\t\t{buttonText}\n\t\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t{@render title()}\n\t\t\t\n\t\t\t\n\t\t\t\t{@render description()}\n\t\t\t\n\t\t\t
    \n\t\t\t\t{@render children?.()}\n\t\t\t
    \n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tClose\n\t\t\t\t
    \n\t\t\t\n\t\t\n\t
    \n
    \n", - "dialog-demo-nested": "\n\n\n\t\n\t\t{#snippet title()}\n\t\t\tFirst Dialog\n\t\t{/snippet}\n\t\t{#snippet description()}\n\t\t\tThis is the first dialog.\n\t\t{/snippet}\n\t\t\n\t\t\t{#snippet title()}\n\t\t\t\tSecond Dialog\n\t\t\t{/snippet}\n\t\t\t{#snippet description()}\n\t\t\t\tThis is the second dialog.\n\t\t\t{/snippet}\n\t\t\n\t\n\n", - "dialog-demo": "\n\n\n\t\n\t\tNew API key\n\t\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tCreate API key\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\tCreate and manage API keys. You can create multiple keys to organize your\n\t\t\t\tapplications.\n\t\t\t\n\t\t\t
    \n\t\t\t\tAPI Key\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t
    \n\t\t\t
    \n\t\t\t\t\n\t\t\t\t\tSave\n\t\t\t\t\n\t\t\t
    \n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tClose\n\t\t\t\t
    \n\t\t\t\n\t\t\n\t
    \n
    \n", - "dropdown-menu-demo-transition": "\n\n\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tProfile\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tP\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tBilling\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tB\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tSettings\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t⌘\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tS\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tNotifications\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tWorkspace\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tHJ\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t@huntabyte\n\t\t\t\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tPS\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t@pavel_stianko\n\t\t\t\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tCK\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t@cokakoala_\n\t\t\t\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTL\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t@thomasglopes\n\t\t\t\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "dropdown-menu-demo": "\n\n\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tProfile\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t⌘\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tP\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tBilling\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t⌘\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tB\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\tSettings\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t⌘\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tS\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t\t\n\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\tNotifications\n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
    \n\t\t\t\t{/snippet}\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\tWorkspace\n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tHJ\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t@huntabyte\n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tPS\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t@pavel_stianko\n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tCK\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t@cokakoala_\n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tTL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t@thomasglopes\n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
    \n\t\t\n\t
    \n
    \n", - "index.ts": "export { default as AccordionDemo } from \"./accordion-demo.svelte\";\nexport { default as AccordionDemoCustom } from \"./accordion-demo-custom.svelte\";\nexport { default as AccordionDemoTransitions } from \"./accordion-demo-transitions.svelte\";\nexport { default as AlertDialogDemo } from \"./alert-dialog-demo.svelte\";\nexport { default as AspectRatioDemo } from \"./aspect-ratio-demo.svelte\";\nexport { default as AvatarDemo } from \"./avatar-demo.svelte\";\nexport { default as ButtonDemo } from \"./button-demo.svelte\";\nexport { default as CalendarDemo } from \"./calendar-demo.svelte\";\nexport { default as CheckboxDemo } from \"./checkbox-demo.svelte\";\nexport { default as CheckboxDemoCustom } from \"./checkbox-demo-custom.svelte\";\nexport { default as CheckboxDemoGroup } from \"./checkbox-demo-group.svelte\";\nexport { default as CollapsibleDemo } from \"./collapsible-demo.svelte\";\nexport { default as CollapsibleDemoTransitions } from \"./collapsible-demo-transitions.svelte\";\nexport { default as ComboboxDemo } from \"./combobox-demo.svelte\";\nexport { default as ComboboxDemoTransition } from \"./combobox-demo-transition.svelte\";\nexport { default as CommandDemo } from \"./command-demo.svelte\";\nexport { default as CommandDemoDialog } from \"./command-demo-dialog.svelte\";\nexport { default as ContextMenuDemo } from \"./context-menu-demo.svelte\";\nexport { default as ContextMenuDemoTransition } from \"./context-menu-demo-transition.svelte\";\nexport { default as DateFieldDemo } from \"./date-field-demo.svelte\";\nexport { default as DateRangeFieldDemo } from \"./date-range-field-demo.svelte\";\nexport { default as DatePickerDemo } from \"./date-picker-demo.svelte\";\nexport { default as DateRangePickerDemo } from \"./date-range-picker-demo.svelte\";\nexport { default as DialogDemo } from \"./dialog-demo.svelte\";\nexport { default as DialogDemoCustom } from \"./dialog-demo-custom.svelte\";\nexport { default as DialogDemoNested } from \"./dialog-demo-nested.svelte\";\nexport { default as DropdownMenuDemo } from \"./dropdown-menu-demo.svelte\";\nexport { default as DropdownMenuDemoTransition } from \"./dropdown-menu-demo-transition.svelte\";\nexport { default as LabelDemo } from \"./label-demo.svelte\";\nexport { default as LinkPreviewDemo } from \"./link-preview-demo.svelte\";\nexport { default as LinkPreviewDemoTransition } from \"./link-preview-demo-transition.svelte\";\nexport { default as SelectDemo } from \"./select-demo.svelte\";\nexport { default as SelectDemoCustom } from \"./select-demo-custom.svelte\";\nexport { default as SelectDemoCustomAnchor } from \"./select-demo-custom-anchor.svelte\";\nexport { default as SelectDemoMultiple } from \"./select-demo-multiple.svelte\";\nexport { default as SelectDemoTransition } from \"./select-demo-transition.svelte\";\nexport { default as MenubarDemo } from \"./menubar-demo.svelte\";\nexport { default as MeterDemo } from \"./meter-demo.svelte\";\nexport { default as MeterDemoCustom } from \"./meter-demo-custom.svelte\";\nexport { default as NavigationMenuDemo } from \"./navigation-menu-demo.svelte\";\nexport { default as NavigationMenuDemoForceMount } from \"./navigation-menu-demo-force-mount.svelte\";\nexport { default as PaginationDemo } from \"./pagination-demo.svelte\";\nexport { default as PinInputDemo } from \"./pin-input-demo.svelte\";\nexport { default as PopoverDemo } from \"./popover-demo.svelte\";\nexport { default as PopoverDemoTransition } from \"./popover-demo-transition.svelte\";\nexport { default as ProgressDemo } from \"./progress-demo.svelte\";\nexport { default as ProgressDemoCustom } from \"./progress-demo-custom.svelte\";\nexport { default as RadioGroupDemo } from \"./radio-group-demo.svelte\";\nexport { default as RangeCalendarDemo } from \"./range-calendar-demo.svelte\";\nexport { default as ScrollAreaDemo } from \"./scroll-area-demo.svelte\";\nexport { default as ScrollAreaDemoCustom } from \"./scroll-area-demo-custom.svelte\";\nexport { default as SeparatorDemo } from \"./separator-demo.svelte\";\nexport { default as SliderDemo } from \"./slider-demo.svelte\";\nexport { default as SliderDemoMultiple } from \"./slider-demo-multiple.svelte\";\nexport { default as SwitchDemo } from \"./switch-demo.svelte\";\nexport { default as SwitchDemoCustom } from \"./switch-demo-custom.svelte\";\nexport { default as TabsDemo } from \"./tabs-demo.svelte\";\nexport { default as ToggleDemo } from \"./toggle-demo.svelte\";\nexport { default as ToggleGroupDemo } from \"./toggle-group-demo.svelte\";\nexport { default as ToolbarDemo } from \"./toolbar-demo.svelte\";\nexport { default as TooltipDemo } from \"./tooltip-demo.svelte\";\nexport { default as TooltipDemoCustom } from \"./tooltip-demo-custom.svelte\";\nexport { default as TooltipDemoDelayDuration } from \"./tooltip-demo-delay-duration.svelte\";\nexport { default as TooltipDemoTransition } from \"./tooltip-demo-transition.svelte\";\nexport { default as DateFieldDemoCustom } from \"./date-field-demo-custom.svelte\";\n", - "label-demo": "\n\n
    \n\t\n\t\t{#snippet children({ checked, indeterminate })}\n\t\t\t
    \n\t\t\t\t{#if indeterminate}\n\t\t\t\t\t\n\t\t\t\t{:else if checked}\n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t
    \n\t\t{/snippet}\n\t\n\t\n\t\tAccept terms and conditions\n\t\n
    \n", - "link-preview-demo-transition": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\tHB\n\t\t\t\n\t\t\n\t\n\t\n\t\t{#snippet child({ open, props, wrapperProps })}\n\t\t\t{#if open}\n\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tHB\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t

    @huntabyte

    \n\t\t\t\t\t\t\t\t

    I do things on the internet.

    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t FL, USA \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t Joined May 2020\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\n\t\t\t{/if}\n\t\t{/snippet}\n\t\n
    \n", - "link-preview-demo": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\tHB\n\t\t\t\n\t\t\n\t\n\t\n\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tHB\n\t\t\t\t
    \n\t\t\t\n\t\t\t
    \n\t\t\t\t

    @huntabyte

    \n\t\t\t\t

    I do things on the internet.

    \n\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\t FL, USA \n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t\t Joined May 2020\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t
    \n\t\t\n\t\n
    \n", - "menubar-demo": "\n\n\n\t
    \n\t\t\n\t
    \n\t\n\t\t\n\t\t\tFile\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t{#each grids as grid}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t{grid.label} grid\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each views as view}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t{view.label}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t
    \n\t\t\t\n\t\t
    \n\t
    \n\n\t\n\t\t\n\t\t\tEdit\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tUndo\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tRedo\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tFind\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tSearch the web\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tFind...\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tFind Next\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tFind Previous\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tCut\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tCopy\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tPaste\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t
    \n\t\n\t\t\n\t\t\tView\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t{#each showConfigs as config}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t{config.label}\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t{@render SwitchOn()}\n\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t{@render SwitchOff()}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tReload\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tForce Reload\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tToggle Fullscreen\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tHide Sidebar\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t
    \n\t\n\t\t\n\t\t\tProfiles\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each profiles as profile}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#snippet children({ checked })}\n\t\t\t\t\t\t\t\t{profile.label}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t{#if checked}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t
    \n\t\t\t\t\n\t\t\t\tEdit...\n\t\t\t\t\n\t\t\t\tAdd Profile...\n\t\t\t\n\t\t
    \n\t
    \n\n\n{#snippet SwitchOn()}\n\t\n\t\t\n\t\n{/snippet}\n\n{#snippet SwitchOff()}\n\t\n\t\t\n\t\n{/snippet}\n", - "meter-demo-custom": "\n\n\n\t
    \n\t\t
    \n\t\t\t {label} \n\t\t\t{valueLabel}\n\t\t
    \n\t\t\n\t\t\t
    \n\t\t\n\t\n
    \n", - "meter-demo": "\n\n
    \n\t
    \n\t\t Tokens used \n\t\t{value} / {max}\n\t
    \n\t\n\t\t
    \n\t\n\n", - "navigation-menu-demo-force-mount": "\n\n{#snippet ListItem({ className, title, content, href }: ListItemProps)}\n\t
  • \n\t\t\n\t\t\t
    {title}
    \n\t\t\t

    \n\t\t\t\t{content}\n\t\t\t

    \n\t\t\n\t
  • \n{/snippet}\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\tGetting started\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
  • \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    Bits UI
    \n\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\t\tThe headless components for Svelte.\n\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\n\t\t\t\t\t
  • \n\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs\",\n\t\t\t\t\t\ttitle: \"Introduction\",\n\t\t\t\t\t\tcontent: \"Headless components for Svelte and SvelteKit\",\n\t\t\t\t\t})}\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs/getting-started\",\n\t\t\t\t\t\ttitle: \"Getting Started\",\n\t\t\t\t\t\tcontent: \"How to install and use Bits UI\",\n\t\t\t\t\t})}\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs/styling\",\n\t\t\t\t\t\ttitle: \"Styling\",\n\t\t\t\t\t\tcontent: \"How to style Bits UI components\",\n\t\t\t\t\t})}\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t\t\n\t\t\t\n\t\t\t\tComponents\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each components as component (component.title)}\n\t\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\t\thref: component.href,\n\t\t\t\t\t\t\ttitle: component.title,\n\t\t\t\t\t\t\tcontent: component.description,\n\t\t\t\t\t\t})}\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t Documentation \n\t\t\t\t Docs \n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t
    \n\t
    \n\t\t\n\t
    \n
    \n", - "navigation-menu-demo": "\n\n{#snippet ListItem({ className, title, content, href }: ListItemProps)}\n\t
  • \n\t\t\n\t\t\t
    {title}
    \n\t\t\t

    \n\t\t\t\t{content}\n\t\t\t

    \n\t\t\n\t
  • \n{/snippet}\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\tGetting started\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
  • \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    Bits UI
    \n\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\t\tThe headless components for Svelte.\n\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\n\t\t\t\t\t
  • \n\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs\",\n\t\t\t\t\t\ttitle: \"Introduction\",\n\t\t\t\t\t\tcontent: \"Headless components for Svelte and SvelteKit\",\n\t\t\t\t\t})}\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs/getting-started\",\n\t\t\t\t\t\ttitle: \"Getting Started\",\n\t\t\t\t\t\tcontent: \"How to install and use Bits UI\",\n\t\t\t\t\t})}\n\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\thref: \"/docs/styling\",\n\t\t\t\t\t\ttitle: \"Styling\",\n\t\t\t\t\t\tcontent: \"How to style Bits UI components\",\n\t\t\t\t\t})}\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t\t\n\t\t\t\n\t\t\t\tComponents\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each components as component (component.title)}\n\t\t\t\t\t\t{@render ListItem({\n\t\t\t\t\t\t\thref: component.href,\n\t\t\t\t\t\t\ttitle: component.title,\n\t\t\t\t\t\t\tcontent: component.description,\n\t\t\t\t\t\t})}\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t Documentation \n\t\t\t\t Docs \n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t
    \n\t
    \n\t\t\n\t
    \n
    \n", - "pagination-demo": "\n\n\n\t{#snippet children({ pages, range })}\n\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t
    \n\t\t\t\t{#each pages as page (page.key)}\n\t\t\t\t\t{#if page.type === \"ellipsis\"}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t...\n\t\t\t\t\t\t
    \n\t\t\t\t\t{:else}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{page.value}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t{/each}\n\t\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t\t

    \n\t\t\tShowing {range.start} - {range.end}\n\t\t

    \n\t{/snippet}\n
    \n", - "pin-input-demo": "\n\n\n\t{#snippet children({ cells })}\n\t\t
    \n\t\t\t{#each cells.slice(0, 3) as cell}\n\t\t\t\t{@render Cell(cell)}\n\t\t\t{/each}\n\t\t
    \n\n\t\t
    \n\t\t\t
    \n\t\t
    \n\n\t\t
    \n\t\t\t{#each cells.slice(3, 6) as cell}\n\t\t\t\t{@render Cell(cell)}\n\t\t\t{/each}\n\t\t
    \n\t{/snippet}\n\n\n{#snippet Cell(cell: CellProps)}\n\t\n\t\t{#if cell.char !== null}\n\t\t\t
    \n\t\t\t\t{cell.char}\n\t\t\t
    \n\t\t{/if}\n\t\t{#if cell.hasFakeCaret}\n\t\t\t\n\t\t\t\t
    \n\t\t\t\n\t\t{/if}\n\t\n{/snippet}\n", - "popover-demo-transition": "\n\n\n\t\n\t\tResize\n\t\n\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tResize image\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\t\t\t\tResize your photos easily\n\t\t\t\t\t\t\t\t\t

    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\tWidth\n\t\t\t\t\t\t\t\t\t\tW\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\tHeight\n\t\t\t\t\t\t\t\t\t\tH\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "popover-demo": "\n\n\n\t\n\t\tResize\n\t\n\t\n\t\t\n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    \n\t\t\t\t\t\tResize image\n\t\t\t\t\t

    \n\t\t\t\t\t

    \n\t\t\t\t\t\tResize your photos easily\n\t\t\t\t\t

    \n\t\t\t\t
    \n\t\t\t
    \n\t\t\t\n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\tWidth\n\t\t\t\t\t\tW\n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\tHeight\n\t\t\t\t\t\tH\n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
    \n\t\t\n\t
    \n
    \n", - "progress-demo-custom": "\n\n\n\t
    \n\t\t
    \n\t\t\t {label} \n\t\t\t{valueLabel}\n\t\t
    \n\t\t\n\t\t\t
    \n\t\t\n\t\n
    \n", - "progress-demo": "\n\n
    \n\t
    \n\t\t Uploading file... \n\t\t{value}%\n\t
    \n\t\n\t\t
    \n\t\n\n", - "radio-group-demo": "\n\n\n\t
    \n\t\t\n\t\tAmazing\n\t
    \n\t
    \n\t\t\n\t\tAverage\n\t
    \n\t
    \n\t\t\n\t\tTerrible\n\t
    \n
    \n", - "range-calendar-demo": "\n\n\n\t{#snippet children({ months, weekdays })}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t
    \n\t\t\t{#each months as month}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each weekdays as day}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    {day.slice(0, 2)}
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\t{#each month.weeks as weekDates}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#each weekDates as date}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t{date.day}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t{/each}\n\t\t\n\t{/snippet}\n\n", - "scroll-area-demo-custom": "\n\n{#snippet Scrollbar({ orientation }: { orientation: \"vertical\" | \"horizontal\" })}\n\t{#if orientation === \"vertical\"}\n\t\t\n\t\t\t\n\t\t\n\t{:else}\n\t\t\n\t\t\t\n\t\t\n\t{/if}\n{/snippet}\n\n\n\t\n\t\t\n\t\t\t{#if children}\n\t\t\t\t{@render children?.()}\n\t\t\t{:else}\n\t\t\t\t\n\t\t\t\t\tScroll Area\n\t\t\t\t\n\t\t\t\t

    \n\t\t\t\t\tLorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit\n\t\t\t\t\trem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea\n\t\t\t\t\tdeleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem\n\t\t\t\t\tipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error\n\t\t\t\t\tminima eos fugit voluptate excepturi eveniet dolore et, ratione impedit\n\t\t\t\t\tconsequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum.\n\t\t\t\t

    \n\t\t\t{/if}\n\t\t\n\t\t{#if orientation === \"vertical\" || orientation === \"both\"}\n\t\t\t{@render Scrollbar({ orientation: \"vertical\" })}\n\t\t{/if}\n\t\t{#if orientation === \"horizontal\" || orientation === \"both\"}\n\t\t\t{@render Scrollbar({ orientation: \"horizontal\" })}\n\t\t{/if}\n\t\t\n\t\n
    \n", - "scroll-area-demo": "\n\n\n\t\n\t\t

    \n\t\t\tScroll Area\n\t\t

    \n\t\t

    \n\t\t\tLorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem,\n\t\t\trepellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti eveniet\n\t\t\tincidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor sit, amet\n\t\t\tconsectetur adipisicing elit. Blanditiis officiis error minima eos fugit voluptate\n\t\t\texcepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae corrupti\n\t\t\tautem? Dolorem, sit voluptatum.\n\t\t

    \n\t
    \n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\n", - "select-demo-custom-anchor": "\n\n\n\t
    \n\t\t
    Custom Anchor
    \n\t\t\n\t
    \n
    \n", - "select-demo-custom": "\n\n\n\t\n\t\t\n\t\t{selectedLabel}\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{#each themes as theme, i (i + theme.value)}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t{theme.label}\n\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
    \n
    \n", - "select-demo-multiple": "\n\n\n\t\n\t\t\n\t\t\n\t\t\t{selectedLabel}\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{#each themes as theme, i (i + theme.value)}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t{theme.label}\n\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
    \n
    \n", - "select-demo-transition": "\n\n\n\t\n\t\t\n\t\t{selectedLabel}\n\t\t\n\t\n\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#each themes as theme, i (i + theme.value)}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t\t\t\t\t{theme.label}\n\t\t\t\t\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t\n\t
    \n
    \n", - "select-demo": "\n\n (value = v)}>\n\t\n\t\t\n\t\t{selectedLabel}\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{#each themes as theme, i (i + theme.value)}\n\t\t\t\t\t\n\t\t\t\t\t\t{#snippet children({ selected })}\n\t\t\t\t\t\t\t{theme.label}\n\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\n\t\t\t\t{/each}\n\t\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t
    \n
    \n", - "separator-demo": "\n\n
    \n\t
    \n\t\t

    Bits UI

    \n\t\t

    Headless UI components for Svelte.

    \n\t
    \n\t\n\t
    \n\t\t
    Blog
    \n\t\t\n\t\t
    Docs
    \n\t\t\n\t\t
    Source
    \n\t
    \n
    \n", - "slider-demo-multiple": "\n\n
    \n\t\n\t\t{#snippet children({ thumbs })}\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t{#each thumbs as index}\n\t\t\t\t\n\t\t\t{/each}\n\t\t{/snippet}\n\t\n
    \n", - "slider-demo": "\n\n
    \n\t\n\t\t{#snippet children()}\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t{/snippet}\n\t\n
    \n", - "switch-demo-custom": "\n\n\n\t
    \n\t\t\n\t\t\t\n\t\t\n\t\t{labelText}\n\t
    \n
    \n", - "switch-demo": "\n\n
    \n\t\n\t\t\n\t\n\tDo not disturb\n
    \n", - "tabs-demo": "\n\n
    \n\t\n\t\t\n\t\t\tOutbound\n\t\t\tInbound\n\t\t\n\t\t\n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    \n\t\t\t\t\t\tPrague\n\t\t\t\t\t

    \n\t\t\t\t\t

    06:05

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    3h 30m

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    \n\t\t\t\t\t\tMalaga\n\t\t\t\t\t

    \n\t\t\t\t\t

    06:05

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t
    \n\n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t\t\n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    \n\t\t\t\t\t\tMalaga\n\t\t\t\t\t

    \n\t\t\t\t\t

    07:25

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    3h 20m

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    \n\t\t\t\t\t\tPrague\n\t\t\t\t\t

    \n\t\t\t\t\t

    10:45

    \n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t
    \n\n\t\t\t\t\t
    \n\t\t\t\t\t\t\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t\n
    \n", - "toggle-demo": "\n\n\n\t\n\t\t{code}\n\t\n\t\n\t\t\n\t\n\n", - "toggle-group-demo": "\n\n\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\n", - "toolbar-demo": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\n\n\t\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\n\n\t\n\n\t
    \n\t\t\n\t\t\t\n\t\t\t Ask AI \n\t\t\n\t
    \n\n", - "tooltip-demo-custom": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t
    \n\t\t\t\t\n\t\t\t
    \n\t\t\t\n\t\t\t\tMake some magic!\n\t\t\t\n\t\t
    \n\t
    \n
    \n", - "tooltip-demo-delay-duration": "\n\n\n\t
    \n\t\t{#each durations as duration (duration)}\n\t\t\t
    \n\t\t\t\t\n\t\t\t\t
    delayDuration={duration}
    \n\t\t\t
    \n\t\t{/each}\n\t
    \n
    \n", - "tooltip-demo-transition": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t{#snippet child({ wrapperProps, props, open })}\n\t\t\t\t{#if open}\n\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tMake some magic!\n\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t{/snippet}\n\t\t
    \n\t
    \n
    \n", - "tooltip-demo": "\n\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tMake some magic!\n\t\t\t\n\t\t\n\t\n\n" -} \ No newline at end of file diff --git a/docs/src/routes/api/demos.json/stackblitz-files.json b/docs/src/routes/api/demos.json/stackblitz-files.json deleted file mode 100644 index d3bb9ec12..000000000 --- a/docs/src/routes/api/demos.json/stackblitz-files.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - ".gitignore": "node_modules\n\n# Output\n.output\n.vercel\n.netlify\n.wrangler\n/.svelte-kit\n/build\n\n# OS\n.DS_Store\nThumbs.db\n\n# Env\n.env\n.env.*\n!.env.example\n!.env.test\n\n# Vite\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n", - ".npmrc": "engine-strict=true\n", - "README.md": "# sv\n\nEverything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```bash\n# create a new project in the current directory\nnpx sv create\n\n# create a new project in my-app\nnpx sv create my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```bash\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\n## Building\n\nTo create a production version of your app:\n\n```bash\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.\n", - "package.json": "{\n \"name\": \"svelte-project-1741139993954\",\n \"private\": true,\n \"version\": \"0.0.1\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite dev\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"prepare\": \"svelte-kit sync || echo ''\",\n \"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n \"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n \"start\": \"vite\"\n },\n \"devDependencies\": {\n \"@sveltejs/adapter-auto\": \"^4.0.0\",\n \"@sveltejs/kit\": \"^2.16.0\",\n \"@sveltejs/vite-plugin-svelte\": \"^5.0.0\",\n \"svelte\": \"^5.0.0\",\n \"svelte-check\": \"^4.0.0\",\n \"typescript\": \"^5.0.0\",\n \"vite\": \"^6.0.0\",\n \"bits-ui\": \"1.3.5\",\n \"@internationalized/date\": \"^3.5.6\",\n \"phosphor-svelte\": \"^2.0.1\",\n \"clsx\": \"^2.1.0\",\n \"tailwind-merge\": \"^2.5.4\"\n }\n}", - "src/app.d.ts": "// See https://svelte.dev/docs/kit/types#app.d.ts\n// for information about these interfaces\ndeclare global {\n\tnamespace App {\n\t\t// interface Error {}\n\t\t// interface Locals {}\n\t\t// interface PageData {}\n\t\t// interface PageState {}\n\t\t// interface Platform {}\n\t}\n}\n\nexport {};\n", - "src/app.html": "\n \n\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t %sveltekit.head%\n\t \n\n\t \n\t\t
    %sveltekit.body%
    \n\t \n \n ", - "src/lib/index.ts": "// place files you want to import through the `$lib` alias in this folder.\n", - "src/routes/+page.svelte": "

    Welcome to SvelteKit

    \n

    Visit svelte.dev/docs/kit to read the documentation

    \n", - "svelte.config.js": "import adapter from '@sveltejs/adapter-auto';\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\t// Consult https://svelte.dev/docs/kit/integrations\n\t// for more information about preprocessors\n\tpreprocess: vitePreprocess(),\n\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n", - "tsconfig.json": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n\t// from the referenced tsconfig.json - TypeScript does not merge them in\n}\n", - "vite.config.ts": "import { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n\tplugins: [sveltekit()]\n});\n", - "src/lib/utils/styles.ts": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n", - "src/routes/+layout.svelte": "\n\n
    \n\t
    \n\t\t
    \n\t\t\t{@render Logo()}\n\t\t\t{@render ThemeToggle()}\n\t\t
    \n\n\t\t
    \n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{componentName}\n\t\t\t\t
    \n\t\t\t\t{@render children?.()}\n\t\t\t
    \n\t\t\n\t\n\n\n{#snippet ThemeToggle()}\n\t\n\t\t{#if theme === \"light\"}\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{:else}\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{/if}\n\t\n{/snippet}\n\n{#snippet Logo()}\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n{/snippet}\n" -} \ No newline at end of file diff --git a/docs/src/routes/api/search.json/search.json b/docs/src/routes/api/search.json/search.json index 858c13062..9b6c0eef5 100644 --- a/docs/src/routes/api/search.json/search.json +++ b/docs/src/routes/api/search.json/search.json @@ -1 +1 @@ -[{"title":"Accordion","content":" import { APISection, ComponentPreviewV2, AccordionDemo, AccordionDemoTransitions, AccordionDemoCustom, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Accordion component is a versatile UI element designed to manage large amounts of content by organizing it into collapsible sections. It's ideal for FAQs, settings panels, or any interface where users need to focus on specific information without being overwhelmed by visual clutter. Key Features Single or Multiple Mode**: Toggle between allowing one open section or multiple sections at once. Accessible by Default**: Built-in ARIA attributes and keyboard navigation support. Smooth Transitions**: Leverage CSS variables or Svelte transitions for animated expansions. Flexible State**: Use uncontrolled defaults or take full control with bound values. Structure The Accordion is a compound component made up of several sub-components: Root**: Wraps all items and manages state. Item**: Represents a single collapsible section. Header**: Displays the title or label. Trigger**: The clickable element that toggles the content. Content**: The collapsible body of each item. Here's a basic example: import { Accordion } from \"bits-ui\"; Section 1 Content for section 1 goes here. Reusable Components To streamline usage in larger applications, create custom wrapper components for repeated patterns. Below is an example of a reusable MyAccordionItem and MyAccordion. Item Wrapper Combines Item, Header, Trigger, and Content into a single component: import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { title: string; content: string; }; let { title, content, ...restProps }: Props = $props(); {item.title} {content} Accordion Wrapper Wraps Root and renders multiple MyAccordionItem components: import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; import MyAccordionItem from \"$lib/components/MyAccordionItem.svelte\"; type Item = { value?: string; title: string; content: string; disabled?: boolean; }; let { value = $bindable(), ref = $bindable(null), ...restProps }: WithoutChildrenOrChild & { items: Item[]; } = $props(); {#each items as item, i (item.title + i)} {/each} Usage Example import MyAccordion from \"$lib/components/MyAccordion.svelte\"; const items = [ { title: \"Item 1\", content: \"Content 1\" }, { title: \"Item 2\", content: \"Content 2\" }, ]; Use unique value props for each Item if you plan to control the state programatically. Managing Value State This section covers how to manage the value state of the Accordion. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Accordion } from \"bits-ui\"; let myValue = $state([]); const numberOfItemsOpen = $derived(myValue.length); { myValue = [\"item-1\", \"item-2\"]; }} Open Items 1 and 2 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Accordion } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } See the $2 documentation for more information. Customization Single vs. Multiple Set the type prop to \"single\" to allow only one accordion item to be open at a time. Set the type prop to \"multiple\" to allow multiple accordion items to be open at the same time. Default Open Items Set the value prop to pre-open items: Disable Items Disable specific items with the disabled prop: Svelte Transitions The Accordion component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets 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. {#snippet child({ props, open })} {#if open} This is the accordion content that will transition in and out. {/if} {/snippet} 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()} {/snippet} Best Practices For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; import type { Snippet } from \"svelte\"; import { fade } from \"svelte/transition\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children: Snippet; } = $props(); {#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: A ","description":"Organizes content into collapsible sections, allowing users to focus on one or more sections at a time.","href":"/docs/components/accordion"},{"title":"Alert Dialog","content":" import { APISection, ComponentPreviewV2, AlertDialogDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Key Features Compound Component Structure**: Build flexible, customizable alert dialogs using sub-components. Accessibility**: ARIA-compliant with full keyboard navigation support. Portal Support**: Render content outside the normal DOM hierarchy for proper stacking. Managed Focus**: Automatically traps focus with customization options. Flexible State**: Supports both controlled and uncontrolled open states. Structure The Alert Dialog is built from sub-components, each with a specific purpose: Root**: Manages state and provides context to child components. Trigger**: Toggles the dialog's open/closed state. Portal**: Renders its children in a portal, outside the normal DOM hierarchy. Overlay**: Displays a backdrop behind the dialog. Content**: Holds the dialog's main content. Title**: Displays the dialog's title. Description**: Displays a description or additional context for the dialog. Cancel**: Closes the dialog without action. Action**: Confirms the dialog's action. Here's a simple example of an Alert Dialog: import { AlertDialog } from \"bits-ui\"; Open Dialog Confirm Action Are you sure? Cancel Confirm Reusable Components For consistency across your app, create a reusable Alert Dialog component. Here's an example: import type { Snippet } from \"svelte\"; import { AlertDialog, type WithoutChild } from \"bits-ui\"; type Props = AlertDialog.RootProps & { buttonText: string; title: Snippet; description: Snippet; contentProps?: WithoutChild; // ...other component props if you wish to pass them }; let { open = $bindable(false), children, buttonText, contentProps, title, description, ...restProps }: Props = $props(); {buttonText} {@render title()} {@render description()} {@render children?.()} Cancel Confirm You can then use the MyAlertDialog component in your application like so: import MyAlertDialog from \"$lib/components/MyAlertDialog.svelte\"; {#snippet title()} Delete your account {/snippet} {#snippet description()} This action cannot be undone. {/snippet} Alternatively, you can define the snippets separately and pass them as props to the component: import MyAlertDialog from \"$lib/components/MyAlertDialog.svelte\"; {#snippet title()} Delete your account {/snippet} {#snippet description()} This action cannot be undone. {/snippet} Use string props for simplicity or snippets for dynamic content. Managing Open State This section covers how to manage the open state of the Alert Dialog. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { AlertDialog } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Dialog Fully Controlled Use a $2 for total control: import { AlertDialog } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } See the $2 documentation for more information. Focus Management Focus Trap Focus is trapped within the dialog by default. To disable: Disabling focus trap may reduce accessibility. Use with caution. Open Focus By default, when a dialog is opened, focus will be set to the AlertDialog.Cancel button if it exists, or the first focusable element within the AlertDialog.Content. This ensures that users navigating my keyboard end up somewhere within the Dialog that they can interact with. You can override this behavior using the onOpenAutoFocus prop on the AlertDialog.Content component. It's highly recommended that you use this prop to focus something within the Dialog. 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. import { AlertDialog } from \"bits-ui\"; let nameInput = $state(); Open AlertDialog { e.preventDefault(); nameInput?.focus(); }} Close Focus By default, when a dialog is closed, focus will be set to the trigger element of the dialog. You can override this behavior using the onCloseAutoFocus prop on the AlertDialog.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. import { AlertDialog } from \"bits-ui\"; let nameInput = $state(); Open AlertDialog { e.preventDefault(); nameInput?.focus(); }} 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 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: Enabling body scroll may affect user focus and accessibility. Use this option judiciously. Escape Key Handling 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: Method 2: onEscapeKeydown For more granular control, override the default behavior using the onEscapeKeydown prop: { e.preventDefault(); // do something else instead }} This method allows you to implement custom logic when the Escape key is pressed. Interaction Outside By default, interacting outside the Alert Dialog content area does not close the dialog. Bits UI offers two ways to modify this behavior. Method 1: 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: 'ignore' (default): Prevents the Alert Dialog from closing. 'close': Closes the Alert Dialog immediately. '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 make the Alert Dialog close when an interaction occurs outside the content, set the interactOutsideBehavior prop to 'close' on AlertDialog.Content: Method 2: onInteractOutside For custom handling of outside interactions, you can override the default behavior using the onInteractOutside prop: { e.preventDefault(); // do something else instead }} This approach allows you to implement specific behaviors when users interact outside the Alert Dialog content. Best Practices 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. By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards. Nested Dialogs Dialogs can be nested within each other to create more complex layouts. See the $2 component for more information on nested dialogs. Svelte Transitions See the $2 component for more information on Svelte Transitions with dialog components. Working with Forms Form Submission When using the AlertDialog component, often you'll want to submit a form or perform an asynchronous action when the user clicks the Action button. This can be done by waiting for the asynchronous action to complete, then programmatically closing the dialog. import { AlertDialog } from \"bits-ui\"; function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } let open = $state(false); Confirm your action Are you sure you want to do this? { wait(1000).then(() => (open = false)); }} No, cancel (close dialog) Yes (submit form) Inside a Form If you're using an AlertDialog within a form, you'll need to ensure that the Portal is disabled or not included in the AlertDialog structure. This is because the Portal will render the dialog content outside of the form, which will prevent the form from being submitted correctly. ","description":"A modal window that alerts users with important information and awaits their acknowledgment or action.","href":"/docs/components/alert-dialog"},{"title":"Aspect Ratio","content":" import { APISection, ComponentPreviewV2, AspectRatioDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Architecture Root**: The root component which contains the aspect ratio logic Structure Here's an overview of how the Aspect Ratio component is structured in code: import { AspectRatio } from \"bits-ui\"; Reusable Component If you plan on using a lot of AspectRatio components throughout your application, you can create a reusable component that combines the AspectRatio.Root and whatever other elements you'd like to render within it. In the following example, we're creating a reusable MyAspectRatio component that takes in a src prop and renders an img element with the src prop. import { AspectRatio, type WithoutChildrenOrChild } from \"bits-ui\"; let { src, alt, ref = $bindable(null), imageRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { src: string; alt: string; imageRef?: HTMLImageElement | null; } = $props(); You can then use the MyAspectRatio component in your application like so: import MyAspectRatio from \"$lib/components/MyAspectRatio.svelte\"; Custom Ratio Use the ratio prop to set a custom aspect ratio for the image. ","description":"Displays content while maintaining a specified aspect ratio, ensuring consistent visual proportions.","href":"/docs/components/aspect-ratio"},{"title":"Avatar","content":" import { APISection, ComponentPreviewV2, AvatarDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 sub-components that work together to create a fully-featured avatar. Fallback Mechanism**: Provides a fallback when the primary image is unavailable or loading. Customizable**: Each sub-component can be styled and configured independently to match your design system. Architecture The Avatar component is composed of several sub-components, 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. Structure Here's an overview of how the Avatar component is structured in code: import { Avatar } from \"bits-ui\"; Reusable Components You can create your own reusable components that combine the Avatar primitives and simplify the usage throughout your application. In the following example, we're creating a reusable MyAvatar component that takes in a src and fallback prop and renders an Avatar.Root component with an Avatar.Image and Avatar.Fallback component. import { Avatar, type WithoutChildrenOrChild } from \"bits-ui\"; let { src, alt, fallback, ref = $bindable(null), imageRef = $bindable(null), fallbackRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { src: string; alt: string; fallback: string; imageRef?: HTMLImageElement | null; fallbackRef?: HTMLElement | null; } = $props(); {fallback} You could then use the MyAvatar component in your application like so: import MyAvatar from \"$lib/components/MyAvatar.svelte\"; ","description":"Represents a user or entity with a recognizable image or placeholder in UI elements.","href":"/docs/components/avatar"},{"title":"Button","content":" import { APISection, ComponentPreviewV2, ButtonDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Button } from \"bits-ui\"; ","description":"A component that if passed a `href` prop will render an anchor element instead of a button element.","href":"/docs/components/button"},{"title":"Calendar","content":" import { APISection, ComponentPreviewV2, CalendarDemo, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { Calendar } from \"bits-ui\"; {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} Placeholder The placeholder prop for the Calendar.Root component determines what date our calendar should start with when the user hasn't selected a date yet. It also determines the current \"view\" of the calendar. As the user navigates through the calendar, the placeholder will be updated to reflect the currently focused date in that view. By default, the placeholder will be set to the current date, and be of type CalendarDate. Managing Placeholder State This section covers how to manage the placeholder state of the Calendar. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { Calendar } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myPlaceholder = new CalendarDate(2024, 8, 3))}> Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Calendar } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } See the $2 documentation for more information. Managing Value State This section covers how to manage the value state of the Calendar. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Calendar } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Calendar } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } See the $2 documentation for more information. Default Value Often, you'll want to start the Calendar.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. import { Calendar } from \"bits-ui\"; import { parseDate } from \"@internationalized/date\"; // this came from a database/API call const date = \"2024-08-03\"; let value = $state(parseDate(date)); Validation Minimum Value You can set a minimum value for the calendar by using the minValue prop on Calendar.Root. If a user selects a date that is less than the minimum value, the calendar will be marked as invalid. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const yesterday = todayDate.subtract({ days: 1 }); Maximum Value You can set a maximum value for the calendar by using the maxValue prop on Calendar.Root. If a user selects a date that is greater than the maximum value, the calendar will be marked as invalid. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); Unavailable Dates You can specify specific dates that are unavailable for selection by using the isDateUnavailable prop. This prop accepts a function that returns a boolean value indicating whether a date is unavailable or not. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, isNotNull } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); function isDateUnavailable(date: DateValue) { return date.day === 1; } Disabled Dates You can specify specific dates that are disabled for selection by using the isDateDisabled prop. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, isNotNull } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); function isDateDisabled(date: DateValue) { return date.day === 1; } Appearance & Behavior Fixed Weeks You can use the fixedWeeks prop to ensure that the calendar renders a fixed number of weeks, regardless of the number of days in the month. This is useful to keep the calendar visually consistent when the number of days in the month changes. Multiple Months You can use the numberOfMonths prop to render multiple months at once. Paged Navigation By default, when the calendar has more than one month, the previous and next buttons will shift the calendar forward or backward by one month. However, you can change this behavior by setting the pagedNavigation prop to true, which will shift the calendar forward or backward by the number of months being displayed. Localization The calendar will automatically format the content of the calendar according to the locale prop, which defaults to 'en-US', but can be changed to any locale supported by the $2 API. Week Starts On The calendar will automatically format the content of the calendar according to the weekStartsOn prop, which defaults to 0, but can be changed to any day of the week, where 0 is Sunday and 6 is Saturday. Multiple Selection You can set the type prop to 'multiple' to allow users to select multiple dates at once. Custom Composition Month Selector The Calendar component includes a PrevButton and NextButton component to allow users to navigate between months. This is useful, but sometimes you may want to allow the user to select a specific month from a list of months, rather than having to navigate one at a time. To achieve this, you can use the placeholder prop to set the month of the the calendar view programmatically. import { Calendar } from \"bits-ui\"; import { CalendarDate } from \"@internationalized/date\"; let placeholder = $state(new CalendarDate(2024, 8, 3)); { placeholder = placeholder.set({ month: 8 }); }} Set month to August Updating the placeholder will update the calendar view to reflect the new month. ","description":"Displays dates and days of the week, facilitating date-related interactions.","href":"/docs/components/calendar"},{"title":"Checkbox","content":" import { APISection, ComponentPreviewV2, CheckboxDemo, CheckboxDemoCustom, CheckboxDemoGroup, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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. Architecture The Checkbox component is composed of the following parts: Root**: The main component that manages the state and behavior of the checkbox. Structure Here's an overview of how the Checkbox component is structured in code: import { Checkbox } from \"bits-ui\"; {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {:else} ❌ {/if} {/snippet} Reusable Components It's recommended to use the Checkbox primitive to create your own custom checkbox component that can be used throughout your application. In the example below, we're using the Checkbox and $2 components to create a custom checkbox component. import { Checkbox, Label, useId, type WithoutChildrenOrChild } from \"bits-ui\"; let { id = useId(), checked = $bindable(false), ref = $bindable(null), labelRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { labelText: string; labelRef?: HTMLLabelElement | null; } = $props(); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {:else} ❌ {/if} {/snippet} {labelText} You can then use the MyCheckbox component in your application like so: import MyCheckbox from \"$lib/components/MyCheckbox.svelte\"; Managing Checked State This section covers how to manage the checked state of the Checkbox. Two-Way Binding Use bind:checked for simple, automatic state synchronization: import { Checkbox } from \"bits-ui\"; let myChecked = $state(false); (myChecked = false)}> uncheck Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myChecked = $state(false); function getChecked() { return myChecked; } function setChecked(newChecked: boolean) { myChecked = newChecked; } Managing Indeterminate State This section covers how to manage the indeterminate state of the Checkbox. Two-Way Binding Use bind:indeterminate for simple, automatic state synchronization: import MyCheckbox from \"$lib/components/MyCheckbox.svelte\"; let myIndeterminate = $state(true); (myIndeterminate = false)}> clear indeterminate Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myIndeterminate = $state(true); function getIndeterminate() { return myIndeterminate; } function setIndeterminate(newIndeterminate: boolean) { myIndeterminate = newIndeterminate; } Disabled State You can disable the checkbox by setting the disabled prop to true. HTML Forms If you set the name prop, a hidden checkbox input will be rendered to submit the value of the checkbox with a form. By default, the checkbox will be submitted with default checkbox value of 'on' if the checked prop is true. Custom Input Value If you'd prefer to submit a different value, you can use the value prop to set the value of the hidden input. 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. This will apply the required attribute to the hidden input element, ensuring that proper form submission is enforced. Checkbox Groups You can use the Checkbox.Group component to create a checkbox group. import { Checkbox } from \"bits-ui\"; Notifications {#snippet preview()} {/snippet} Managing Value State This section covers how to manage the value state of a Checkbox Group. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Checkbox } from \"bits-ui\"; let myValue = $state([]); { myValue = [\"item-1\", \"item-2\"]; }} Open Items 1 and 2 Items Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myValue = $state([]); function getValue() { return myValue; } function setValue(newValue: string[]) { myValue = newValue; } HTML Forms To render hidden ` elements for the various checkboxes within a group, pass a name to Checkbox.Group`. All descendent checkboxes will then render hidden inputs with the same name. When a Checkbox.Group component is used, its descendent Checkbox.Root components will use certain properties from the group, such as the name, required, and disabled. ","description":"Allow users to switch between checked, unchecked, and indeterminate states.","href":"/docs/components/checkbox"},{"title":"Collapsible","content":" import { APISection, ComponentPreviewV2, CollapsibleDemo, CollapsibleDemoTransitions, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 sub-components that work together to create a fully-featured collapsible. Architecture The Collapsible component is composed of a few sub-components, 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. Structure Here's an overview of how the Collapsible component is structured in code: import { Collapsible } from \"bits-ui\"; Reusable Components It's recommended to use the Collapsible primitives to create your own custom collapsible component that can be used throughout your application. import { Collapsible, type WithoutChild } from \"bits-ui\"; type Props = WithoutChild & { buttonText: string; }; let { open = $bindable(false), ref = $bindable(null), buttonText, children, ...restProps }: Props = $props(); {buttonText} {@render children?.()} You can then use the MyCollapsible component in your application like so: import MyCollapsible from \"$lib/components/MyCollapsible.svelte\"; Here is my collapsible content. Managing Open State This section covers how to manage the open state of the Collapsible. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Collapsible } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Collapsible Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Collapsible } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Svelte Transitions The Collapsible component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets 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. import { Collapsible } from \"bits-ui\"; import { fade } from \"svelte/transition\"; 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. import { Collapsible, type WithoutChildrenOrChild } from \"bits-ui\"; import { fade } from \"svelte/transition\"; import type { Snippet } from \"svelte\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children?: Snippet; } = $props(); {#snippet child({ props, open })} {#if open} {@render children?.()} {/if} {/snippet} You can then use the MyCollapsibleContent component alongside the other Collapsible primitives throughout your application: import { Collapsible } from \"bits-ui\"; import { MyCollapsibleContent } from \"$lib/components\"; Open ","description":"Conceals or reveals content sections, enhancing space utilization and organization.","href":"/docs/components/collapsible"},{"title":"Combobox","content":" import { APISection, ComponentPreviewV2, ComboboxDemo, ComboboxDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Combobox component combines the functionality of an input field with a dropdown list of selectable options. It provides users with the ability to search, filter, and select from a predefined set of choices. Key Features Keyboard Navigation**: Full support for keyboard interactions, allowing users to navigate and select options without using a mouse. Customizable Rendering**: Flexible architecture for rendering options, including support for grouped items. Accessibility**: Built with ARIA attributes and keyboard interactions to ensure screen reader compatibility and accessibility standards. Portal Support**: Ability to render the dropdown content in a portal, preventing layout issues in complex UI structures. Architecture The Combobox component is composed of several sub-components, each with a specific role: Root**: The main container component that manages the state and context for the combobox. Input**: The input field that allows users to enter search queries. Trigger**: The button or element that opens the dropdown list. Portal**: Responsible for portalling the dropdown content to the body or a custom target. Group**: A container for grouped items, used to group related items. GroupHeading**: A heading for a group of items, providing a descriptive label for the group. Item**: An individual item within the list. Separator**: A visual separator between items. Content**: The dropdown container that displays the items. It uses $2 to position the content relative to the trigger. ContentStatic**: An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. Arrow**: An arrow element that points to the trigger when using the Combobox.Content component. Structure Here's an overview of how the Combobox component is structured in code: import { Combobox } from \"bits-ui\"; Reusable Components It's recommended to use the Combobox primitives to build your own custom combobox component that can be reused throughout your application. import { Combobox, type WithoutChildrenOrChild, mergeProps } from \"bits-ui\"; type Item = { value: string; label: string }; type Props = Combobox.RootProps & { items: Item[]; inputProps?: WithoutChildrenOrChild; contentProps?: WithoutChildrenOrChild; }; let { items, value = $bindable(), open = $bindable(false), inputProps, contentProps, ...restProps }: Props = $props(); let searchValue = $state(\"\"); const filteredItems = $derived.by(() => { if (searchValue === \"\") return items; return items.filter((item) => item.label.toLowerCase().includes(searchValue.toLowerCase())); }); function handleInput(e: Event & { currentTarget: HTMLInputElement }) { searchValue = e.currentTarget.value; } function handleOpenChange(newOpen: boolean) { if (!newOpen) searchValue = \"\"; } const mergedRootProps = $derived(mergeProps(restProps, { onOpenChange: handleOpenChange })); const mergedInputProps = $derived(mergeProps(inputProps, { oninput: handleInput })); Open {#each filteredItems as item, i (i + item.value)} {#snippet children({ selected })} {item.label} {selected ? \"✅\" : \"\"} {/snippet} {:else} No results found {/each} import { CustomCombobox } from \"$lib/components\"; const items = [ { value: \"mango\", label: \"Mango\" }, { value: \"watermelon\", label: \"Watermelon\" }, { value: \"apple\", label: \"Apple\" }, // ... ]; Managing Value State This section covers how to manage the value state of the Combobox. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Combobox } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Combobox } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the Combobox. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Combobox } from \"bits-ui\"; let myOpen = $state(false); (myOpen = true)}> Open Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Combobox } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Opt-out of Floating UI When you use the Combobox.Content component, Bits UI uses $2 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. 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.Input 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. import { Combobox } from \"bits-ui\"; let customAnchor = $state(null!); 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. Highlighted Items The Combobox component follows the $2 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. console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Combobox.Content component to use Svelte Transitions or another animation library that requires more control. import { Combobox } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Enables users to pick from a list of options displayed in a dropdown.","href":"/docs/components/combobox"},{"title":"Command","content":" import { APISection, ComponentPreviewV2, CommandDemo, CommandDemoDialog, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Command component, also known as a command menu, is designed to provide users with a quick and efficient way to search, filter, and select items within an application. It combines the functionality of a search input with a dynamic, filterable list of commands or options, making it ideal for applications that require fast navigation or action execution. Key Features Dynamic Filtering**: As users type in the input field, the list of commands or items is instantly filtered and sorted based on an (overridable) scoring algorithm. Keyboard Navigation**: Full support for keyboard interactions, allowing users to quickly navigate and select items without using a mouse. Grouped Commands**: Ability to organize commands into logical groups, enhancing readability and organization. Empty and Loading States**: Built-in components to handle scenarios where no results are found or when results are being loaded. Accessibility**: Designed with ARIA attributes and keyboard interactions to ensure screen reader compatibility and accessibility standards. Architecture The Command component is composed of several sub-components, each with a specific role: Root**: The main container that manages the overall state and context of the command menu. Input**: The text input field where users can type to search or filter commands. List**: The container for the list of commands or items. Viewport**: The visible area of the command list, which applies CSS variables to handle dynamic resizing/animations based on the height of the list. Empty**: A component to display when no results are found. Loading**: A component to display while results are being fetched or processed. Group**: A container for a group of items within the command menu. GroupHeading**: A header element to provide an accessible label for a group of items. GroupItems**: A container for the items within a group. Item**: Individual selectable command or item. LinkItem**: A variant of Command.Item specifically for link-based actions. Separator**: A visual separator to divide different sections of the command list. Structure Here's an overview of how the Command component is structured in code: import { Command } from \"bits-ui\"; Managing Value State Bits UI offers several approaches to manage and synchronize the Command's value state, catering to different levels of control and integration needs. 1. Two-Way Binding For seamless state synchronization, use Svelte's bind:value directive. This method automatically keeps your local state in sync with the component's internal state. import { Command } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Key Benefits Simplifies state management Automatically updates myValue when the internal state changes (e.g., via clicking on an item) Allows external control (e.g., selecting an item via a separate button) 2. Change Handler To perform additional logic on state changes, use the onValueChange prop. This approach is useful when you need to execute side effects when the value changes. import { Command } from \"bits-ui\"; { // do something with the new value console.log(value); }} Use Cases Implementing custom behaviors on value change Integrating with external state management solutions Triggering side effects (e.g., logging, data fetching) 3. Fully Controlled For complete control over the component's state, use a $2 to manage the value state externally. You pass a getter function and a setter function to the bind:value directive, giving you full control over how the value is updated/retrieved. import { Command } from \"bits-ui\"; let myValue = $state(\"\"); myValue, (newValue) => (myValue = newValue)}> When to Use Implementing complex value change 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 $2 documentation. In a Modal You can combine the Command component with the Dialog component to display the command menu within a modal. {#snippet preview()} {/snippet} 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: import { Command } from \"bits-ui\"; function customFilter( commandValue: string, search: string, commandKeywords?: string[] ): number { return commandValue.includes(search) ? 1 : 0; } Extend Default Filter By default, the Command component uses the computeCommandScore function to determine the score of each item and filters/sorts them accordingly. This function is exported for you to use and extend as needed. import { Command, computeCommandScore } from \"bits-ui\"; function customFilter( commandValue: string, search: string, commandKeywords?: string[] ): number { const score = computeCommandScore(commandValue, search, commandKeywords); // Add custom logic here return score; } Disable Filtering You can disable filtering by setting the shouldFilter prop to false. 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. 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. Imperative API For more advanced use cases, such as custom keybindings, the Command.Root component exposes several methods for programmatic control. Access these by binding to the component: import { Command } from \"bits-ui\"; let command: typeof Command.Root; Methods getValidItems() Returns an array of valid (non-disabled, visible) command items. Useful for checking bounds before operations. const items = command.getValidItems(); console.log(items.length); // number of selectable items updateSelectedToIndex(index: number) Sets selection to item at specified index. No-op if index is invalid. // select third item (if it exists) command.updateSelectedToIndex(2); // with bounds check const items = command.getValidItems(); if (index import { Command } from \"bits-ui\"; let command: typeof Command.Root; function jumpToLastItem() { if (!command) return; const items = command.getValidItems(); if (!items.length) return; command.updateSelectedToIndex(items.length - 1); } { if (e.key === \"o\") { jumpToLastItem(); } }} /> ","description":"A command menu component that can be used to search, filter, and select items.","href":"/docs/components/command"},{"title":"Context Menu","content":" import { APISection, ComponentPreviewV2, ContextMenuDemo, ContextMenuDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ContextMenu } from \"bits-ui\"; {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} Reusable Components If you're planning to use Context Menu in multiple places, you can create a reusable component that wraps the Context Menu component. This example shows you how to create a Context Menu component that accepts a few custom props that make it more capable. import type { Snippet } from \"svelte\"; import { ContextMenu, type WithoutChild } from \"bits-ui\"; type Props = ContextMenu.Props & { trigger: Snippet; items: string[]; contentProps?: WithoutChild; // other component props if needed }; let { open = $bindable(false), children, trigger, items, contentProps, ...restProps }: Props = $props(); {@render trigger()} Select an Office {#each items as item} {item} {/each} You can then use the CustomContextMenu component like this: import CustomContextMenu from \"./CustomContextMenu.svelte\"; {#snippet triggerArea()} Right-click me {/snippet} Alternatively, you can define the snippet(s) separately and pass them as props to the component: import CustomContextMenu from \"./CustomContextMenu.svelte\"; {#snippet triggerArea()} Right-click me {/snippet} Managing Open State This section covers how to manage the open state of the menu. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { ContextMenu } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Context Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { ContextMenu } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newopen; } Checkbox Items You can use the ContextMenu.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { ContextMenu } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} See the $2 for more information. Radio Groups You can combine the ContextMenu.RadioGroup and ContextMenu.RadioItem components to create a radio group within a menu. import { ContextMenu } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} See the $2 and $2 APIs for more information. Nested Menus You can create nested menus using the ContextMenu.Sub component to create complex menu structures. import { ContextMenu } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the ContextMenu.Content component to use Svelte Transitions or another animation library that requires more control. import { ContextMenu } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Displays options or actions relevant to a specific context or selected item, triggered by a right-click.","href":"/docs/components/context-menu"},{"title":"Date Field","content":" import { CalendarDateTime, CalendarDate, now, getLocalTimeZone, parseDate, today } from \"@internationalized/date\"; import { APISection, ComponentPreviewV2, DateFieldDemo, DateFieldDemoCustom, DemoContainer, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Overview The DateField component is an alternative to the native `` element. It provides a more flexible and customizable way to select dates within a designated field. Structure import { DateField } from \"$lib\"; Check-in date {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} Reusable Components It's recommended to use the DateField primitives to build your own custom date field component that can be used throughout your application. The following example shows how you might create a reusable MyDateField component that can be used throughout your application. For style inspiration, reference the featured demo at the top of this page. import { DateField, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { labelText: string; }; let { value, placeholder, name, ...restProps }: Props = $props(); {labelText} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {#snippet preview()} {/snippet} We'll be using this newly created MyDateField component in the following demos and examples to prevent repeating the same code, so be sure to reference it as you go through the documentation. Segments A segment of the DateField represents a not only a specific part of the date, such as the day, month, year, hour, but can also reference a \"literal\" which is typically a separator between the different parts of the date, and varies based on the locale. Notice that in the MyDateField component we created, we're styling the DateField.Segment components differently based on whether they are a \"literal\" or not. Placeholder The placeholder prop for the DateField.Root component isn't what is displayed when the field is empty, but rather what date our field should start with when the user attempts to cycle through the segments. The placeholder can also be used to set a granularity for the date field, which will determine which type of DateValue object is used for the value. By default, the placeholder will be set to the current date, and be of type CalendarDate. However, if we wanted this date field to also allow for selecting a time, we could set the placeholder to a CalendarDateTime object. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDateTime } from \"@internationalized/date\"; If we're collecting a date from the user where we want the timezone as well, we can use a ZonedDateTime object instead. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { now, getLocalTimeZone } from \"@internationalized/date\"; 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 This section covers how to manage the placeholder state of the Date Field. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myPlaceholder = new CalendarDate(2024, 8, 3))}> Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateField } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the Date Field. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled For complete control over the component's state, use a $2 to manage the value state externally. import { DateField } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } 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. import { DateField } from \"bits-ui\"; import { parseDate } from \"@internationalized/date\"; // this came from a database/API call const date = \"2024-08-03\"; let value = $state(parseDate(date)); Now our input is populated with the default value. In addition to the parseDate function, you can also use parseDateTime or parseZonedDateTime to parse the string into a CalendarDateTime or ZonedDateTime object respectively. Validation Minimum Value You can set a minimum value for the DateField.Root component by using the minValue prop. If a user selects a date that is less than the minimum value, the date field will be marked as invalid. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const yesterday = todayDate.subtract({ days: 1 }); In the example above, we're setting the minimum value to today, and the default value to yesterday. This causes the date field to be marked as invalid, and we can style it accordingly. If you adjust the date to be greater than the minimum value, the invalid state will be cleared. Maximum Value You can set a maximum value for the DateField.Root component by using the maxValue prop. If a user selects a date that is greater than the maximum value, the date field will be marked as invalid. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); In the example above, we're setting the maximum value to today, and the default value to tomorrow. This causes the date field to be marked as invalid, and we can style it accordingly. If you adjust the date to be less than the maximum value, the invalid state will be cleared. Custom Validation You can use the validate prop to provide a custom validation function for the date field. This function should return a string or array of strings as validation errors if the date is invalid, or undefined/nothing if the date is valid. The strings are then passed to the onInvalid callback, which you can use to display an error message to the user. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDate, type DateValue } from \"@internationalized/date\"; const value = new CalendarDate(2024, 8, 2); function validate(date: DateValue) { return date.day === 1 ? \"Date cannot be the first day of the month\" : undefined; } function onInvalid(reason: \"min\" | \"max\" | \"custom\", msg?: string | string[]) { if (reason === \"custom\") { if (typeof msg === \"string\") { // do something with the error message console.log(msg); return; } else if (Array.isArray(msg)) { // do something with the error messages console.log(msg); return; } console.log(\"The date is invalid\"); } else if (reason === \"min\") { // let the user know that the date is too early. console.log(\"The date is too early.\"); } else if (reason === \"max\") { // let the user know that the date is too late. console.log(\"The date is too late.\"); } } date.day === 1 ? \"Date cannot be the first day of the month\" : undefined} value={new CalendarDate(2024, 8, 2)} onInvalid={(reason, msg) => { if (reason === \"custom\") { if (typeof msg === \"string\") { // do something with the error message console.log(msg); return; } else if (Array.isArray(msg)) { // do something with the error messages console.log(msg); return; } console.log(\"The date is invalid\"); } else if (reason === \"min\") { // let the user know that the date is too early. console.log(\"The date is too early.\"); } else if (reason === \"max\") { // let the user know that the date is too late. console.log(\"The date is too late.\"); } }} /> In the example above, we're setting the isDateUnavailable prop to a function that returns true for the first day of the month. Try selecting a date that is the first day of the month to see the date field marked as invalid. Granularity The granularity prop sets the granularity of the date field, which determines which segments are rendered in the date field. The granularity can be set to either 'day', 'hour', 'minute', or 'second'. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDateTime } from \"@internationalized/date\"; const value = new CalendarDateTime(2024, 8, 2, 12, 30); In the example above, we're setting the granularity to 'second', which means that the date field will include an additional segment for the seconds. Localization You can use the locale prop to set the locale of the date field. This will affect the formatting of the date field's segments and placeholders. import MyDateField from \"$lib/components/MyDateField.svelte\"; ","description":"Enables users to input specific dates within a designated field.","href":"/docs/components/date-field"},{"title":"Date Picker","content":" import { APISection, ComponentPreviewV2, DatePickerDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { DatePicker } from \"bits-ui\"; {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(); { myPlaceholder = new CalendarDateTime(2024, 8, 3, 12, 30); }} Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open DatePicker Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Customization The DatePicker component is made up of three other Bits UI components: $2, $2, and $2. You can check out the documentation for each of these components to learn more about their customization options, each of which can be used to customize the DatePicker component. ","description":"Facilitates the selection of dates through an input and calendar-based interface.","href":"/docs/components/date-picker"},{"title":"Date Range Field","content":" import { APISection, ComponentPreviewV2, DateRangeFieldDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Overview The DateRangeField component combines two $2 components to create a date range field. Check out the $2 component documentation for information on how to customize this component. Structure import { DateRangeField } from \"$lib\"; Check-in date {#each [\"start\", \"end\"] as const as type} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {/each} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateRangeField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangeField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: CalendarDateTime) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateRangeField, type DateRange } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state({ start: new CalendarDateTime(2024, 8, 3, 12, 30), end: new CalendarDateTime(2024, 8, 4, 12, 30), }); { value = { start: value.start.add({ days: 1 }), end: value.end.add({ days: 1 }), }; }} Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangeField } from \"bits-ui\"; let myValue = $state({ start: undefined, end: undefined, }); function getValue() { return myValue; } function setValue(newValue: DateRange) { myValue = newValue; } ","description":"Allows users to input a range of dates within a designated field.","href":"/docs/components/date-range-field"},{"title":"Date Range Picker","content":" import { APISection, ComponentPreviewV2, DateRangePickerDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { DateRangePicker } from \"bits-ui\"; {#each [\"start\", \"end\"] as const as type} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {/each} {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {date.day} {/each} {/each} {/each} {/snippet} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state({ start: new CalendarDateTime(2024, 8, 3, 12, 30), end: new CalendarDateTime(2024, 8, 4, 12, 30), }); { value = { start: value.start.add({ days: 1 }), end: value.end.add({ days: 1 }), }; }} Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker, type DateRange } from \"bits-ui\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateRange) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open DateRangePicker Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Customization The DateRangePicker component is made up of three other Bits UI components: $2, $2, and $2. You can check out the documentation for each of these components to learn more about their customization options, each of which can be used to customize the DateRangePicker component. ","description":"Facilitates the selection of date ranges through an input and calendar-based interface.","href":"/docs/components/date-range-picker"},{"title":"Dialog","content":" import { APISection, ComponentPreviewV2, DialogDemo, DialogDemoCustom, DialogDemoNested, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 sub-components 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 sub-component 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. Architecture The Dialog component is composed of several sub-components, 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. Structure Here's an overview of how the Dialog component is structured in code: import { Dialog } from \"bits-ui\"; Reusable Components Bits UI provides a comprehensive set of Dialog components that serve as building blocks for creating customized, reusable Dialog implementations. This approach offers flexibility in design while maintaining consistency and accessibility across your application. Building a Reusable Dialog The following example demonstrates how to create a versatile, reusable Dialog component using Bits UI building blocks. This implementation showcases the flexibility of the component API by combining props and snippets. import type { Snippet } from \"svelte\"; import { Dialog, type WithoutChild } from \"bits-ui\"; type Props = Dialog.RootProps & { buttonText: string; title: Snippet; description: Snippet; contentProps?: WithoutChild; // ...other component props if you wish to pass them }; let { open = $bindable(false), children, buttonText, contentProps, title, description, ...restProps }: Props = $props(); {buttonText} {@render title()} {@render description()} {@render children?.()} Close Dialog Usage with Inline Snippets import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} Account settings {/snippet} {#snippet description()} Manage your account settings and preferences. {/snippet} Usage with Separate Snippets import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} Account settings {/snippet} {#snippet description()} Manage your account settings and preferences. {/snippet} Best Practices Prop Flexibility**: Design your component to accept props for any nested components for maximum flexibility Styling Options**: Use tools like clsx to merge class overrides Binding Props**: Use bind: and expose $bindable props to provide consumers with full control Type Safety**: Use the exported types from Bits UI to type your component props Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Dialog } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Dialog Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Dialog } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } 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, 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 While not recommended, you can disable the focus trap if absolutely necessary: 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 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. Customizing Initial Focus To specify which element receives focus when the Dialog opens, use the onOpenAutoFocus prop on Dialog.Content: import { Dialog } from \"bits-ui\"; let nameInput = $state(); Open Dialog { e.preventDefault(); nameInput?.focus(); }} Always ensure that something within the Dialog receives focus when it opens. This is crucial for maintaining keyboard navigation context and makes your users happy. Close Focus When a Dialog closes, focus returns to the element that triggered its opening (typically the Dialog.Trigger). Customizing Close Focus To change which element receives focus when the Dialog closes, use the onCloseAutoFocus prop on Dialog.Content: import { Dialog } from \"bits-ui\"; let nameInput = $state(); Open Dialog { e.preventDefault(); nameInput?.focus(); }} Best Practices Always maintain a clear focus management strategy for your Dialogs. Ensure that focus is predictable and logical for keyboard users. Test your focus management with keyboard navigation to verify its effectiveness. Advanced Behaviors Bits UI's 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 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 Dialog.Content: Enabling body scroll may affect user focus and accessibility. Use this option judiciously. Escape Key Handling By default, pressing the Escape key closes an open 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 Dialog immediately. 'ignore': Prevents the 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 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 Dialog will ignore the key press and not close. To always prevent the Dialog from closing on Escape key press, set the escapeKeydownBehavior prop to 'ignore' on Dialog.Content: Method 2: onEscapeKeydown For more granular control, override the default behavior using the onEscapeKeydown prop: { e.preventDefault(); // do something else instead }} This method allows you to implement custom logic when the Escape key is pressed. Interaction Outside By default, interacting outside the Dialog content area closes the Dialog. Bits UI offers two ways to modify this behavior. Method 1: 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: 'close' (default): Closes the Dialog immediately. 'ignore': Prevents the 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 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 Dialog will ignore the event and not close. To always prevent the Dialog from closing when an interaction occurs outside the content, set the interactOutsideBehavior prop to 'ignore' on Dialog.Content: Method 2: onInteractOutside For custom handling of outside interactions, you can override the default behavior using the onInteractOutside prop: { e.preventDefault(); // do something else instead }} This approach allows you to implement specific behaviors when users interact outside the Dialog content. Best Practices 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. By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards. Nested Dialogs Dialogs can be nested within each other to create more complex user interfaces: import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} First Dialog {/snippet} {#snippet description()} This is the first dialog. {/snippet} {#snippet title()} Second Dialog {/snippet} {#snippet description()} This is the second dialog. {/snippet} Svelte Transitions The Dialog component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets To apply Svelte transitions to Dialog components, use the forceMount prop in combination with the child snippet. This approach gives you full control over the mounting behavior and animation of Dialog.Content and Dialog.Overlay. import { Dialog } from \"bits-ui\"; import { fly, fade } from \"svelte/transition\"; {#snippet child({ props, open })} {#if open} {/if} {/snippet} {#snippet child({ props, open })} {#if open} {/if} {/snippet} 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. Best Practices For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. import { Dialog, type WithoutChildrenOrChild } from \"bits-ui\"; import { fade } from \"svelte/transition\"; import type { Snippet } from \"svelte\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children?: Snippet; } = $props(); {#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: import { Dialog } from \"bits-ui\"; import { MyDialogOverlay } from \"$lib/components\"; Open Working with Forms Form Submission When using the Dialog component, often you'll want to submit a form or perform an asynchronous action and then close the dialog. This can be done by waiting for the asynchronous action to complete, then programmatically closing the dialog. import { Dialog } from \"bits-ui\"; function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } let open = $state(false); Confirm your action Are you sure you want to do this? { wait(1000).then(() => (open = false)); }} Submit form Inside a Form If you're using a Dialog within a form, you'll need to ensure that the Portal is disabled or not included in the Dialog structure. This is because the Portal will render the dialog content outside of the form, which will prevent the form from being submitted correctly. ","description":"A modal window presenting content or seeking user input without navigating away from the current context.","href":"/docs/components/dialog"},{"title":"Dropdown Menu","content":" import { APISection, ComponentPreviewV2, DropdownMenuDemo, DropdownMenuDemoTransition, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { DropdownMenu } from \"bits-ui\"; Reusable Components If you're planning to use Dropdown Menu in multiple places, you can create a reusable component that wraps the Dropdown Menu component. This example shows you how to create a Dropdown Menu component that accepts a few custom props that make it more capable. import type { Snippet } from \"svelte\"; import { DropdownMenu, type WithoutChild } from \"bits-ui\"; type Props = DropdownMenu.Props & { buttonText: string; items: string[]; contentProps?: WithoutChild; // other component props if needed }; let { open = $bindable(false), children, buttonText, items, contentProps, ...restProps }: Props = $props(); {buttonText} {#each items as item} {item} {/each} You can then use the MyDropdownMenu component like this: import MyDropdownMenu from \"./MyDropdownMenu.svelte\"; Managing Open State This section covers how to manage the open state of the menu. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DropdownMenu } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Context Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DropdownMenu } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Groups To group related menu items, you can use the DropdownMenu.Group component along with either a DropdownMenu.GroupHeading or an aria-label attribute on the DropdownMenu.Group component. File New Open Save Save As New Open Save Save As Group Heading The DropdownMenu.GroupHeading component must be a child of either a DropdownMenu.Group or DropdownMenu.RadioGroup component. If used on its own, an error will be thrown during development. File Favorite color Checkbox Items You can use the DropdownMenu.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { DropdownMenu } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} The checked state does not persist between menu open/close cycles. To persist the state, you must store it in a $state variable and pass it to the checked prop. Radio Groups You can combine the DropdownMenu.RadioGroup and DropdownMenu.RadioItem components to create a radio group within a menu. import { DropdownMenu } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); Favorite number {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} The value state does not persist between menu open/close cycles. To persist the state, you must store it in a $state variable and pass it to the value prop. Nested Menus You can create nested menus using the DropdownMenu.Sub component to create complex menu structures. import { DropdownMenu } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 --> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the DropdownMenu.Content component to use Svelte Transitions or another animation library that requires more control. import { DropdownMenu } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} Custom Anchor By default, the DropdownMenu.Content is anchored to the DropdownMenu.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 DropdownMenu.Content component. import { DropdownMenu } from \"bits-ui\"; let customAnchor = $state(null!); ","description":"Displays a menu of items that users can select from when triggered.","href":"/docs/components/dropdown-menu"},{"title":"Label","content":" import { APISection, ComponentPreviewV2, LabelDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Label } from \"bits-ui\"; ","description":"Identifies or describes associated UI elements.","href":"/docs/components/label"},{"title":"Link Preview","content":" import { APISection, ComponentPreviewV2, LinkPreviewDemo, LinkPreviewDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 import { LinkPreview } from \"bits-ui\"; Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { LinkPreview } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Link Preview Fully Controlled Use a $2 for complete control over the state's reads and writes. import { LinkPreview } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Opt-out of Floating UI When you use the LinkPreview.Content component, Bits UI uses $2 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. 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. Custom Anchor By default, the LinkPreview.Content is anchored to the LinkPreview.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 LinkPreview.Content component. import { LinkPreview } from \"bits-ui\"; let customAnchor = $state(null!); Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the LinkPreview.Content component to use Svelte Transitions or another animation library that requires more control. import { LinkPreview } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Displays a summarized preview of a linked content's details or information.","href":"/docs/components/link-preview"},{"title":"Menubar","content":" import { APISection, ComponentPreviewV2, MenubarDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Menubar } from \"bits-ui\"; {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} Reusable Components If you're planning to use Menubar in multiple places, you can create reusable components that wrap the different parts of the Menubar. In the following example, we're creating a reusable MyMenubarMenu component that contains the trigger, content, and items of a menu. import { Menubar, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { triggerText: string; items: { label: string; value: string; onSelect?: () => void }[]; contentProps?: WithoutChildrenOrChild; // other component props if needed }; let { triggerText, items, contentProps, ...restProps }: Props = $props(); {triggerText} {#each items as item} {item.label} {/each} Now, we can use the MyMenubarMenu component within a Menubar.Root component to render out the various menus. import { Menubar } from \"bits-ui\"; import MyMenubarMenu from \"./MyMenubarMenu.svelte\"; const sales = [ { label: \"Michael Scott\", value: \"michael\" }, { label: \"Dwight Schrute\", value: \"dwight\" }, { label: \"Jim Halpert\", value: \"jim\" }, { label: \"Stanley Hudson\", value: \"stanley\" }, { label: \"Phyllis Vance\", value: \"phyllis\" }, { label: \"Pam Beesly\", value: \"pam\" }, { label: \"Andy Bernard\", value: \"andy\" }, ]; const hr = [ { label: \"Toby Flenderson\", value: \"toby\" }, { label: \"Holly Flax\", value: \"holly\" }, { label: \"Jan Levinson\", value: \"jan\" }, ]; const accounting = [ { label: \"Angela Martin\", value: \"angela\" }, { label: \"Kevin Malone\", value: \"kevin\" }, { label: \"Oscar Martinez\", value: \"oscar\" }, ]; const menubarMenus = [ { title: \"Sales\", items: sales }, { title: \"HR\", items: hr }, { title: \"Accounting\", items: accounting }, ]; {#each menubarMenus as { title, items }} {/each} Managing Value State This section covers how to manage the value state of the menubar. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Menubar } from \"bits-ui\"; let activeValue = $state(\"\"); (activeValue = \"menu-1\")}>Open Menubar Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Menubar } from \"bits-ui\"; let activeValue = $state(\"\"); function getValue() { return activeValue; } function setValue(newValue: string) { activeValue = newValue; } Checkbox Items You can use the Menubar.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { Menubar } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} Radio Groups You can combine the Menubar.RadioGroup and Menubar.RadioItem components to create a radio group within a menu. import { Menubar } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} Nested Menus You can create nested menus using the Menubar.Sub component to create complex menu structures. import { Menubar } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Menubar.Content component to use Svelte Transitions or another animation library that requires more control. import { Menubar } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. ","description":"Organizes and presents a collection of menu options or actions within a horizontal bar.","href":"/docs/components/menubar"},{"title":"Meter","content":" import { APISection, ComponentPreviewV2, MeterDemo, DemoCodeContainer, MeterDemoCustom } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} While often visually similar, meters and $2 bars serve distinct purposes: Meter: Displays a static measurement within a known range (0-100) Value can fluctuate up/down based on real-time measurements Examples: CPU usage, battery level, sound volume Use when showing current state relative to capacity Progress bar: Shows completion status of a task Value only increases as task progresses Examples: File upload, installation status, form completion Use when tracking advancement toward completion If a progress bar better fits your requirements, check out the $2 component. Structure import { Meter } from \"bits-ui\"; Reusable Components It's recommended to use the Meter primitive to create your own custom meter component that can be used throughout your application. In the example below, we're using the Meter primitive to create a more comprehensive meter component. import { Meter, useId } from \"bits-ui\"; import type { ComponentProps } from \"svelte\"; let { max = 100, value = 0, min = 0, label, valueLabel, }: ComponentProps & { label: string; valueLabel: string; } = $props(); const labelId = useId(); {label} {valueLabel} You can then use the MyMeter component in your application like so: import MyMeter from \"$lib/components/MyMeter.svelte\"; let value = $state(3000); const max = 4000; Of course, you'd want to apply your own styles and other customizations to the MyMeter component to fit your application's design. Accessibility If a visual label is used, the ID of the label element should be pass via the aria-labelledby prop to Meter.Root. If no visual label is used, the aria-label prop should be used to provide a text description of the progress bar. Assistive technologies often present aria-valuenow as a percentage. If conveying the value of the meter only in terms of a percentage would not be user friendly, the aria-valuetext property should be set to a string that makes the meter value understandable. For example, a battery meter value might be conveyed as aria-valuetext=\"50% (6 hours) remaining\". $2] ","description":"Display real-time measurements within a defined range.","href":"/docs/components/meter"},{"title":"Navigation Menu","content":" import { APISection, ComponentPreviewV2, NavigationMenuDemo, Callout, NavigationMenuDemoForceMount } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { NavigationMenu } from \"bits-ui\"; Usage Vertical You can create a vertical menu by using the orientation prop. Flexible Layouts Use the Viewport component when you need extra control over where Content is rendered. This can be useful when your design requires an adjusted DOM structure or if you need flexibility to achieve advanced animations. Tab focus will be managed automatically. Item one Item one content Item two Item two content With Indicator You can use the optional Indicator component to highlight the currently active Trigger, which is useful when you want to provide an animated visual cue such as an arrow or highlight to accompany the Viewport. Item one Item one content Item two Item two content Submenus You can create a submenu by nesting your navigation menu and using the Navigation.Sub component in place of NavigationMenu.Root. Submenus work differently than the Root menus and are more similar to $2 in that one item should always be active, so be sure to assign and pass a value prop. Item one Item one content Item two Sub item one Sub item one content Sub item two Sub item two content Advanced Animation We expose --bits-navigation-menu-viewport-[width|height] and data-motion['from-start'|'to-start'|'from-end'|'to-end'] to allow you to animate the NavigationMenu.Viewport size and NavigationMenu.Content position based on the enter/exit direction. Combining these with position: absolute; allows you to create smooth overlapping animation effects when moving between items. Item one Item one content Item two Item two content /* app.css */ .NavigationMenuContent { position: absolute; top: 0; left: 0; animation-duration: 250ms; animation-timing-function: ease; } .NavigationMenuContent[data-motion=\"from-start\"] { animation-name: enter-from-left; } .NavigationMenuContent[data-motion=\"from-end\"] { animation-name: enter-from-right; } .NavigationMenuContent[data-motion=\"to-start\"] { animation-name: exit-to-left; } .NavigationMenuContent[data-motion=\"to-end\"] { animation-name: exit-to-right; } .NavigationMenuViewport { position: relative; width: var(--bits-navigation-menu-viewport-width); height: var(--bits-navigation-menu-viewport-height); transition: width, height, 250ms ease; } @keyframes enter-from-right { from { opacity: 0; transform: translateX(200px); } to { opacity: 1; transform: translateX(0); } } @keyframes enter-from-left { from { opacity: 0; transform: translateX(-200px); } to { opacity: 1; transform: translateX(0); } } @keyframes exit-to-right { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(200px); } } @keyframes exit-to-left { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-200px); } } Force Mounting You may wish for the links in the Navigation Menu to persist in the DOM, regardless of whether the menu is open or not. This is particularly useful for SEO purposes. You can achieve this by using the forceMount prop on the NavigationMenu.Content and NavigationMenu.Viewport components. Note: Using forceMount requires you to manage the visibility of the elements yourself, using the data-state attributes on the NavigationMenu.Content and NavigationMenu.Viewport components. {#snippet preview()} {/snippet} ","description":"A list of links that allow users to navigate between pages of a website.","href":"/docs/components/navigation-menu"},{"title":"Pagination","content":" import { APISection, ComponentPreviewV2, PaginationDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Pagination } from \"bits-ui\"; {#each pages as page (page.key)} {/each} Managing Page State This section covers how to manage the page state of the component. Two-Way Binding Use bind:page for simple, automatic state synchronization: import { Pagination } from \"bits-ui\"; let myPage = $state(1); (myPage = 2)}> Go to page 2 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Pagination } from \"bits-ui\"; let myPage = $state(1); function getPage() { return myPage; } function setPage(newPage: number) { myPage = newPage; } 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. ","description":"Facilitates navigation between pages.","href":"/docs/components/pagination"},{"title":"PIN Input","content":" import { APISection, ComponentPreviewV2, PinInputDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 $2 with $2. 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). Architecture Root Container: A relatively positioned root element that encapsulates the entire component. Invisible Input: A hidden input field that manages the actual value and interacts with the browser's built-in features. 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 import { PinInput } from \"bits-ui\"; {#snippet children({ cells })} {#each cells as cell} {/each} {/snippet} Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { PinInput } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"123456\")}> Set value to 123456 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { PinInput } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Paste Transformation The pasteTransformer prop allows you to sanitize/transform pasted text. This can be useful for cleaning up pasted text, like removing hyphens or other characters that should not make it into the input. This function should return the sanitized text, which will be used as the new value of the input. import { PinInput } from \"bits-ui\"; text.replace(/-/g, \"\")}> HTML Forms The PinInput.Root component is designed to work seamlessly with HTML forms. Simply pass the name prop to the PinInput.Root component and the input will be submitted with the form. Submit On Complete To submit the form when the input is complete, you can use the onComplete prop. import { PinInput } from \"bits-ui\"; let form = $state(null!); form.submit()}> Patterns You can use the pattern prop to restrict the characters that can be entered or pasted into the input. Client-side validation cannot replace server-side validation. Use this in addition to server-side validation for an improved user experience. Bits UI exports a few common patterns that you can import and use in your application. REGEXP_ONLY_DIGITS - Only allow digits to be entered. REGEXP_ONLY_CHARS - Only allow characters to be entered. REGEXP_ONLY_DIGITS_AND_CHARS - Only allow digits and characters to be entered. import { PinInput, REGEXP_ONLY_DIGITS } from \"bits-ui\"; ","description":"Allows users to input a sequence of one-character alphanumeric inputs.","href":"/docs/components/pin-input"},{"title":"Popover","content":" import { APISection, ComponentPreviewV2, PopoverDemo, PopoverDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Popover } from \"bits-ui\"; Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Popover } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Popover Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Popover } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Managing Focus Focus Trap By default, when a Popover is opened, focus will be trapped within that Popover. You can disable this behavior by setting the trapFocus prop to false on the Popover.Content component. 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. import { Popover } from \"bits-ui\"; let nameInput = $state(); 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. import { Popover } from \"bits-ui\"; let nameInput = $state(); 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. 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. onEscapeKeydown You can also override the default behavior by cancelling the event passed to the onEscapeKeydown callback on the Popover.Content component. 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. onInteractOutside You can also override the default behavior by cancelling the event passed to the onInteractOutside callback on the Popover.Content component. e.preventDefault()}> Custom Anchor By default, the Popover.Content is anchored to the Popover.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 Popover.Content component. import { Popover } from \"bits-ui\"; let customAnchor = $state(null!); Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Popover.Content component to use Svelte Transitions or another animation library that requires more control. import { Popover } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Display supplementary content or information when users interact with specific elements.","href":"/docs/components/popover"},{"title":"Progress","content":" import { APISection, ComponentPreviewV2, ProgressDemo, ProgressDemoCustom } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} While often visually similar, progress bars and $2 serve distinct purposes: Progress: Shows completion status of a task Value only increases as task progresses Examples: File upload, installation status, form completion Use when tracking advancement toward completion Meter: Displays a static measurement within a known range (0-100) Value can fluctuate up/down based on real-time measurements Examples: CPU usage, battery level, sound volume Use when showing current state relative to capacity If a meter better fits your requirements, check out the $2 component. Structure import { Progress } from \"bits-ui\"; Reusable Components It's recommended to use the Progress primitive to create your own custom meter component that can be used throughout your application. In the example below, we're using the Progress primitive to create a more comprehensive meter component. import { Progress, useId } from \"bits-ui\"; import type { ComponentProps } from \"svelte\"; let { max = 100, value = 0, min = 0, label, valueLabel, }: ComponentProps & { label: string; valueLabel: string; } = $props(); const labelId = useId(); {label} {valueLabel} You can then use the MyProgress component in your application like so: import MyProgress from \"$lib/components/MyProgress.svelte\"; let value = $state(50); Of course, you'd want to apply your own styles and other customizations to the MyProgress component to fit your application's design. Accessibility If a visual label is used, the ID of the label element should be pass via the aria-labelledby prop to Progress.Root. If no visual label is used, the aria-label prop should be used to provide a text description of the progress bar. ","description":"Visualizes the progress or completion status of a task or process.","href":"/docs/components/progress"},{"title":"Radio Group","content":" import { APISection, ComponentPreviewV2, RadioGroupDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { RadioGroup } from \"bits-ui\"; {#snippet children({ checked })} {#if checked} ✅ {/if} {/snippet} Reusable Components It's recommended to use the RadioGroup primitives to create your own custom components that can be used throughout your application. In the example below, we're creating a custom MyRadioGroup component that takes in an array of items and renders a radio group with those items along with a $2 component for each item. import { RadioGroup, Label, type WithoutChildrenOrChild, useId } from \"bits-ui\"; type Item = { value: string; label: string; disabled?: boolean; }; type Props = WithoutChildrenOrChild & { items: Item[]; }; let { value = $bindable(\"\"), ref = $bindable(null), items, ...restProps }: Props = $props(); {#each items as item} {@const id = useId()} {#snippet children({ checked })} {#if checked} ✅ {/if} {/snippet} {item.label} {/each} You can then use the MyRadioGroup component in your application like so: import MyRadioGroup from \"$lib/components/MyRadioGroup.svelte\"; const myItems = [ { value: \"apple\", label: \"Apple\" }, { value: \"banana\", label: \"Banana\" }, { value: \"coconut\", label: \"Coconut\", disabled: true }, ]; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { RadioGroup } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { RadioGroup } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } 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. Required To make the hidden input element required you can set the required prop on the RadioGroup.Root component. Disabling Items 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. ","description":"Allows users to select a single option from a list of mutually exclusive choices.","href":"/docs/components/radio-group"},{"title":"Range Calendar","content":" import { APISection, ComponentPreviewV2, RangeCalendarDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { RangeCalendar } from \"bits-ui\"; {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} ","description":"Presents a calendar view tailored for selecting date ranges.","href":"/docs/components/range-calendar"},{"title":"Scroll Area","content":" import { APISection, ComponentPreviewV2, ScrollAreaDemo, ScrollAreaDemoCustom } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ScrollArea } from \"bits-ui\"; Reusable Components If you're planning to use the Scroll Area throughout your application, it's recommended to create a reusable component to reduce the amount of code you need to write each time. This example shows you how to create a Scroll Area component that accepts a few custom props that make it more capable. import { ScrollArea, type WithoutChild } from \"bits-ui\"; type Props = WithoutChild & { orientation: \"vertical\" | \"horizontal\" | \"both\"; viewportClasses?: string; }; let { ref = $bindable(null), orientation = \"vertical\", viewportClasses, children, ...restProps }: Props = $props(); {#snippet Scrollbar({ orientation }: { orientation: \"vertical\" | \"horizontal\" })} {/snippet} {@render children?.()} {#if orientation === \"vertical\" || orientation === \"both\"} {@render Scrollbar({ orientation: \"vertical\" })} {/if} {#if orientation === \"horizontal\" || orientation === \"both\"} {@render Scrollbar({ orientation: \"horizontal\" })} {/if} We'll use this custom component in the following examples to demonstrate how to customize the behavior of the Scroll Area. Scroll Area Types Hover The hover type is the default type of the scroll area, demonstrated in the featured example above. It only shows scrollbars when the user hovers over the scroll area and the content is larger than the viewport. Scroll The scroll type displays the scrollbars when the user scrolls the content. This is similar to the behavior of MacOS. Auto The auto type behaves similarly to your typical browser scrollbars. When the content is larger than the viewport, the scrollbars will appear and remain visible at all times. Always The always type behaves as if you set overflow: scroll on the scroll area. Scrollbars will always be visible, even when the content is smaller than the viewport. We've also set the orientation prop on the MyScrollArea to 'both' to ensure both scrollbars are rendered. Customizing the Hide Delay You can customize the hide delay of the scrollbars using the scrollHideDelay prop. ","description":"Provides a consistent scroll area across platforms.","href":"/docs/components/scroll-area"},{"title":"Select","content":" import { APISection, ComponentPreviewV2, SelectDemo, SelectDemoCustomAnchor, SelectDemoMultiple, SelectDemoTransition, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Select component provides users with a selectable list of options. It's designed to offer an enhanced selection experience with features like typeahead search, keyboard navigation, and customizable grouping. This component is particularly useful for scenarios where users need to choose from a predefined set of options, offering more functionality than a standard select element. Key Features Typeahead Search**: Users can quickly find options by typing Keyboard Navigation**: Full support for keyboard interactions, allowing users to navigate through options using arrow keys, enter to select, and more. Grouped Options**: Ability to organize options into logical groups, enhancing readability and organization of large option sets. Scroll Management**: Includes scroll up/down buttons for easy navigation in long lists. Accessibility**: Built-in ARIA attributes and keyboard support ensure compatibility with screen readers and adherence to accessibility standards. Portal Support**: Option to render the select content in a portal, preventing layout issues in complex UI structures. Architecture The Select component is composed of several sub-components, each with a specific role: Root**: The main container component that manages the state and context for the combobox. Trigger**: The button or element that opens the dropdown list. Portal**: Responsible for portalling the dropdown content to the body or a custom target. Group**: A container for grouped items, used to group related items. GroupHeading**: A heading for a group of items, providing a descriptive label for the group. Item**: An individual item within the list. Separator**: A visual separator between items. Content**: The dropdown container that displays the items. It uses $2 to position the content relative to the trigger. ContentStatic** (Optional): An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. Arrow**: An arrow element that points to the trigger when using the Combobox.Content component. Structure Here's an overview of how the Select component is structured in code: import { Select } from \"bits-ui\"; Reusable Components As you can see from the structure above, there are a number of pieces that make up the Select component. These pieces are provided to give you maximum flexibility and customization options, but can be a burden to write out everywhere you need to use a select in your application. To ease this burden, it's recommended to create your own reusable select component that wraps the primitives and provides a more convenient API for your use cases. Here's an example of how you might create a reusable MySelect component that receives a list of options and renders each of them as an item. import { Select, type WithoutChildren } from \"bits-ui\"; type Props = WithoutChildren & { placeholder?: string; items: { value: string; label: string; disabled?: boolean }[]; contentProps?: WithoutChildren; // any other specific component props if needed }; let { value = $bindable(), items, contentProps, placeholder, ...restProps }: Props = $props(); const selectedLabel = $derived(items.find((item) => item.value === value)?.label); {selectedLabel ? selectedLabel : placeholder} up {#each items as { value, label, disabled } (value)} {#snippet children({ selected })} {selected ? \"✅\" : \"\"} {item.label} {/snippet} {/each} down You can then use the MySelect component throughout your application like so: import MySelect from \"$lib/components/MySelect.svelte\"; const items = [ { value: \"apple\", label: \"Apple\" }, { value: \"banana\", label: \"Banana\" }, { value: \"cherry\", label: \"Cherry\" }, ]; let fruit = $state(\"apple\"); Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Select } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Select } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Select } from \"bits-ui\"; let myOpen = $state(false); (myOpen = true)}> Open Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Select } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Multiple Selection The type prop can be set to 'multiple' to allow multiple items to be selected at a time. import { Select } from \"bits-ui\"; let value = $state([]); {#snippet preview()} {/snippet} Opt-out of Floating UI When you use the Select.Content component, Bits UI uses $2 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 Select.ContentStatic component. When using this component, you'll need to handle the positioning of the content yourself. Keep in mind that using Select.Portal alongside Select.ContentStatic may result in some unexpected positioning behavior, feel free to not use the portal or work around it. Custom Anchor By default, the Select.Content is anchored to the Select.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 Select.Content component. import { Select } from \"bits-ui\"; let customAnchor = $state(null!); What is the Viewport? The Select.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 Select.Viewport component. Scroll Up/Down Buttons The Select.ScrollUpButton and Select.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 Select.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 Select.Scroll[Up|Down]Button components and the Select.Viewport component. You'll need to set a height on the Select.Content component and appropriate overflow styles to enable scrolling. Scroll Lock By default, when a user opens the select, scrolling outside the content will not be disabled. You can override this behavior by setting the preventScroll prop to true. Highlighted Items The Select component follows the $2 for highlighting items. This means that the Select.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 Select.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. console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Select.Content component to use Svelte Transitions or another animation library that requires more control. import { Select } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Enables users to choose from a list of options presented in a dropdown.","href":"/docs/components/select"},{"title":"Separator","content":" import { APISection, ComponentPreviewV2, SeparatorDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Separator } from \"bits-ui\"; ","description":"Visually separates content or UI elements for clarity and organization.","href":"/docs/components/separator"},{"title":"Slider","content":" import { APISection, ComponentPreviewV2, SliderDemo, SliderDemoMultiple, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Slider } from \"bits-ui\"; Reusable Components Bits UI provides primitives that enable you to build your own custom slider component that can be reused throughout your application. Here's an example of how you might create a reusable MySlider component. import type { ComponentProps } from \"svelte\"; import { Slider } from \"bits-ui\"; type Props = WithoutChildren>; let { value = $bindable(), ref = $bindable(null), ...restProps }: Props = $props(); {#snippet children({ thumbs, ticks })} {#each thumbs as index} {/each} {#each ticks as index} {/each} {/snippet} You can then use the MySlider component in your application like so: import MySlider from \"$lib/components/MySlider.svelte\"; let multiValue = $state([5, 10]); let singleValue = $state(50); Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Slider } from \"bits-ui\"; let myValue = $state(0); (myValue = 20)}> Set value to 20 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Slider } from \"bits-ui\"; let myValue = $state(0); function getValue() { return myValue; } function setValue(newValue: number) { myValue = newValue; } Value Commit You can use the onValueCommit prop to be notified when the user finishes dragging the thumb and the value changes. This is different than the onValueChange callback because it waits until the user stops dragging before calling the callback, where the onValueChange callback is called as the user dragging. { console.log(\"user is done sliding!\", v); }} /> 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 import { Slider } from \"bits-ui\"; // we have two numbers in the array, so the slider will render two thumbs let value = $state([5, 7]); {#snippet children({ ticks, thumbs })} {#each thumbs as index} {/each} {#each ticks as index} {/each} {/snippet} To determine the number of ticks that will be rendered, you can simply divide the max value by the step value. Single Type Set the type prop to \"single\" to allow only one accordion item to be open at a time. {#snippet preview()} {/snippet} Multiple Type Set the type prop to \"multiple\" to allow multiple accordion items to be open at the same time. {#snippet preview()} {/snippet} Vertical Orientation You can use the orientation prop to change the orientation of the slider, which defaults to \"horizontal\". RTL Support You can use the dir prop to change the reading direction of the slider, which defaults to \"ltr\". Auto Sort By default, the slider will sort the values from smallest to largest, so if you drag a smaller thumb to a larger value, the value of that thumb will be updated to the larger value. You can disable this behavior by setting the autoSort prop to false. HTML Forms Since there is a near endless number of possible values that a user can select, the slider does not render a hidden input element by default. You'll need to determine how you want to submit the value(s) of the slider with a form. Here's an example of how you might do that: import MySlider from \"$lib/components/MySlider.svelte\"; let expectedIncome = $state([50, 100]); let desiredIncome = $state(50); Submit ","description":"Allows users to select a value from a continuous range by sliding a handle.","href":"/docs/components/slider"},{"title":"Switch","content":" import { APISection, ComponentPreviewV2, SwitchDemo, SwitchDemoCustom, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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. Style-able**: Data attributes allow for smooth transitions between states and custom styles. HTML Forms**: Can render a hidden input element for form submissions. 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. Structure Here's an overview of how the Switch component is structured in code: import { Switch } from \"bits-ui\"; Reusable Components It's recommended to use the Switch primitives to create your own custom switch component that can be used throughout your application. In the example below, we're using the Checkbox and $2 components to create a custom switch component. import { Switch, Label, useId, type WithoutChildrenOrChild } from \"bits-ui\"; let { id = useId(), checked = $bindable(false), ref = $bindable(null), ...restProps }: WithoutChildrenOrChild & { labelText: string; } = $props(); {labelText} You can then use the MySwitch component in your application like so: import MySwitch from \"$lib/components/MySwitch.svelte\"; let notifications = $state(true); Managing Checked State This section covers how to manage the checked state of the component. Two-Way Binding Use bind:checked for simple, automatic state synchronization: import { Switch } from \"bits-ui\"; let myChecked = $state(true); (myChecked = false)}> uncheck Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Switch } from \"bits-ui\"; let myChecked = $state(false); function getChecked() { return myChecked; } function setChecked(newChecked: boolean) { myChecked = newChecked; } Disabled State You can disable the switch by setting the disabled prop to true. HTML Forms If you pass the name prop to Switch.Root, a hidden input element will be rendered to submit the value of the switch to a form. By default, the input will be submitted with the default checkbox value of 'on' if the switch is checked. Custom Input Value If you'd prefer to submit a different value, you can use the value prop to set the value of the hidden input. 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. This will apply the required attribute to the hidden input element, ensuring that proper form submission is enforced. ","description":"A toggle control enabling users to switch between \"on\" and \"off\" states.","href":"/docs/components/switch"},{"title":"Tabs","content":" import { APISection, ComponentPreviewV2, TabsDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Tabs } from \"bits-ui\"; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Tabs } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"tab-1\")}> Activate tab 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Tabs } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } 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. Activation Mode By default, the Tabs component will automatically activate the tab associated with a trigger when that trigger is focused. This behavior can be disabled by setting the activationMode prop to 'manual'. When set to 'manual', the user will need to activate the tab by pressing the trigger. ","description":"Organizes content into distinct sections, allowing users to switch between them.","href":"/docs/components/tabs"},{"title":"Toggle Group","content":" import { APISection, ComponentPreviewV2, ToggleGroupDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ToggleGroup } from \"bits-ui\"; bold italic 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. Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { ToggleGroup } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"item-1\")}> Press item 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { ToggleGroup } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } ","description":"Groups multiple toggle controls, allowing users to enable one or multiple options.","href":"/docs/components/toggle-group"},{"title":"Toggle","content":" import { APISection, ComponentPreviewV2, ToggleDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Toggle } from \"bits-ui\"; Managing Pressed State This section covers how to manage the pressed state of the component. Two-Way Binding Use bind:pressed for simple, automatic state synchronization: import { Toggle } from \"bits-ui\"; let myPressed = $state(true); (myPressed = false)}> un-press Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Toggle } from \"bits-ui\"; let myPressed = $state(false); function getPressed() { return myPressed; } function setPressed(newPressed: boolean) { myPressed = newPressed; } ","description":"A control element that switches between two states, providing a binary choice.","href":"/docs/components/toggle"},{"title":"Toolbar","content":" import { APISection, ComponentPreviewV2, ToolbarDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Toolbar } from \"bits-ui\"; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Toolbar } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"item-1\")}> Press item 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Toolbar } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } ","description":"Displays frequently used actions or tools in a compact, easily accessible bar.","href":"/docs/components/toolbar"},{"title":"Tooltip","content":" import { ComponentPreviewV2, TooltipDemo, TooltipDemoCustom, TooltipDemoDelayDuration, TooltipDemoTransition, APISection, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Tooltip } from \"bits-ui\"; Provider Component The Tooltip.Provider component is required to be an ancestor of the Tooltip.Root component. It provides shared state for the tooltip components used within it. You can set a single delayDuration or disableHoverableContent prop on the provider component to apply to all the tooltip components within it. import { Tooltip } from \"bits-ui\"; 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. import { Tooltip } from \"bits-ui\"; let { children } = $props(); {@render children()} Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Tooltip } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Tooltip Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Tooltip } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } 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). This is not possible on mobile devices, because there is no sense of hover on mobile. If a user were to press/touch a button with a tooltip, the action that button triggers would be taken before they were even able to see the tooltip, which renders it useless. If you are using a tooltip on a button without an action, you should consider using a $2 instead. If you'd like to learn more about how we came to this decision, here are some useful resources: The tooltip is not the appropriate role for the more information \"i\" icon, ⓘ. A tooltip is directly associated with the owning element. The ⓘ isn't 'described by' detailed information; the tool or control is. $2 Tooltips should only ever contain non-essential content. The best approach to writing tooltip content is to always assume it may never be read. $2 Reusable Components It's recommended to use the Tooltip primitives to build your own custom tooltip component that can be used throughout your application. Below is an example of how you might create a reusable tooltip component that can be used throughout your application. Of course, this isn't the only way to do it, but it should give you a good idea of how to compose the primitives. import { Tooltip } from \"bits-ui\"; import { type Snippet } from \"svelte\"; type Props = Tooltip.RootProps & { trigger: Snippet; triggerProps?: Tooltip.TriggerProps; }; let { open = $bindable(false), children, buttonText, triggerProps = {}, ...restProps }: Tooltip.RootProps = $props(); {@render trigger()} {@render children?.()} You could then use the MyTooltip component in your application like so: import MyTooltip from \"$lib/components/MyTooltip.svelte\"; import BoldIcon from \"..some-icon-library\"; // not real alert(\"changed to bold!\") }}> {#snippet trigger()} {/snippet} Change font to bold Delay Duration You can change how long a user needs to hover over a trigger before the tooltip appears by setting the delayDuration prop on the Tooltip.Root or Tooltip.Provider component. Close on Trigger Click By default, the tooltip will close when the user clicks the trigger. If you want to disable this behavior, you can set the disableCloseOnTriggerClick prop to true. Hoverable Content By default, the tooltip will remain open when the user hovers over the content. If you instead want the tooltip to close as the user moves their mouse towards the content, you can set the disableHoverableContent prop to true. Non-Keyboard Focus If you want to prevent opening the tooltip when the user focuses the trigger without using the keyboard, you can set the ignoreNonKeyboardFocus prop to true. Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Tooltip.Content component to use Svelte Transitions or another animation library that requires more control. import { Tooltip } from \"bits-ui\"; import { fly, fade } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} 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 $2 documentation. {#snippet preview()} {/snippet} Opt-out of Floating UI When you use the Tooltip.Content component, Bits UI uses $2 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. 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. Custom Anchor By default, the Tooltip.Content is anchored to the Tooltip.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 Tooltip.Content component. import { Tooltip } from \"bits-ui\"; let customAnchor = $state(null!); ","description":"Provides additional information or context when users hover over or interact with an element.","href":"/docs/components/tooltip"},{"title":"IsUsingKeyboard","content":"Overview IsUsingKeyboard is a utility component that tracks whether the user is actively using the keyboard or not. This component is used internally by Bits UI components to provide keyboard accessibility features. It provides global state that is shared across all instances of the class to prevent duplicate event listener registration. Usage import { IsUsingKeyboard } from \"bits-ui\"; const isUsingKeyboard = new IsUsingKeyboard(); const shouldShowMenu = $derived(isUsingKeyboard.current); `","description":"A utility to track whether the user is actively using the keyboard or not.","href":"/docs/utilities/is-using-keyboard"},{"title":"mergeProps","content":"Overview mergeProps is a utility function designed to merge multiple props objects. It's particularly useful for composing components with different prop sets or extending the functionality of existing components. It is used internally by Bits UI components to merge the custom restProps you pass to a component with the props that Bits UI provides to the component. Key Features Merges multiple props objects Chains event handlers with cancellation support Combines class names Merges style objects and strings Chains non-event handler functions Detailed Behavior Event Handlers Event handlers are chained in the order they're passed. If a handler calls event.preventDefault(), subsequent handlers in the chain are not executed. const props1 = { onclick: (e: MouseEvent) => console.log(\"First click\") }; const props2 = { onclick: (e: MouseEvent) => console.log(\"Second click\") }; const mergedProps = mergeProps(props1, props2); mergedProps.onclick(new MouseEvent(\"click\")); // Logs: \"First click\" then \"Second click\" If preventDefault() is called: const props1 = { onclick: (e: MouseEvent) => console.log(\"First click\") }; const props2 = { onclick: (e: MouseEvent) => { console.log(\"Second click\"); e.preventDefault(); }, }; const props3 = { onclick: (e: MouseEvent) => console.log(\"Third click\") }; const mergedProps = mergeProps(props1, props2, props3); mergedProps.onclick(new MouseEvent(\"click\")); // Logs: \"First click\" then \"Second click\" only Since props2 called event.preventDefault(), props3's onclick handler will not be called. Non-Event Handler Functions Non-event handler functions are also chained, but without the ability to prevent subsequent functions from executing: const props1 = { doSomething: () => console.log(\"Action 1\") }; const props2 = { doSomething: () => console.log(\"Action 2\") }; const mergedProps = mergeProps(props1, props2); mergedProps.doSomething(); // Logs: \"Action 1\" then \"Action 2\" Classes Class names are merged using $2: const props1 = { class: \"text-lg font-bold\" }; const props2 = { class: [\"bg-blue-500\", \"hover:bg-blue-600\"] }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.class); // \"text-lg font-bold bg-blue-500 hover:bg-blue-600\" Styles Style objects and strings are merged, with later properties overriding earlier ones: const props1 = { style: { color: \"red\", fontSize: \"16px\" } }; const props2 = { style: \"background-color: blue; font-weight: bold;\" }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.style); // \"color: red; font-size: 16px; background-color: blue; font-weight: bold;\" import { mergeProps } from \"bits-ui\"; const props1 = { style: \"--foo: red\" }; const props2 = { style: { \"--foo\": \"green\", color: \"blue\" } }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.style); // \"--foo: green; color: blue;\" `","description":"A utility function to merge props objects.","href":"/docs/utilities/merge-props"},{"title":"Portal","content":"Overview The Portal component is a utility component that renders its children in a portal, preventing layout issues in complex UI structures. This component is used for the various Bits UI component that have a Portal sub-component. Usage Default behavior By default, the Portal component will render its children in the body element. import { Portal } from \"bits-ui\"; This content will be portalled to the body Custom target You can use the to prop to specify a custom target element or selector to render the content to. import { Portal } from \"bits-ui\"; This content will be portalled to the #custom-target element Disable You can use the disabled prop to disable the portal behavior. import { Portal } from \"bits-ui\"; This content will not be portalled `","description":"A component that renders its children in a portal, preventing layout issues in complex UI structures.","href":"/docs/utilities/portal"},{"title":"useId","content":"The useId function is a utility function that can be used to generate unique IDs. This function is used internally by all Bits UI components and is exposed for your convenience. Usage import { useId } from \"bits-ui\"; const id = useId(); Label here `","description":"A utility function to generate unique IDs.","href":"/docs/utilities/use-id"},{"title":"WithElementRef","content":"The WithElementRef type helper is a convenience type that enables you to follow the same $2 prop pattern as used by Bits UI components when crafting your own. type WithElementRef = T & { ref?: U | null }; This type helper is used internally by Bits UI components to enable the ref prop on a component. Usage Example import type { WithElementRef } from \"bits-ui\"; type Props = WithElementRef; let { yourPropA, yourPropB, ref = $bindable(null) }: Props = $props(); `","description":"A type helper to enable the `ref` prop on a component.","href":"/docs/type-helpers/with-element-ref"},{"title":"WithoutChild","content":"The WithoutChild type helper is used to exclude the child snippet prop from a component. This is useful when you're building custom component wrappers that populate the children prop of a component and don't provide a way to pass a custom child snippet. To learn more about the child snippet prop, check out the $2 documentation. import { Accordion, type WithoutChild } from \"bits-ui\"; let { children, ...restProps }: WithoutChild = $props(); {@render children?.()} `","description":"A type helper to exclude the child snippet prop from a component.","href":"/docs/type-helpers/without-child"},{"title":"WithoutChildrenOrChild","content":"The WithoutChildrenOrChild type helper is used to exclude the child and children props from a component. This is useful when you're building custom component wrappers that populate the children prop of a component and don't provide a way to pass a custom children or child snippet. To learn more about the child snippet prop, check out the $2 documentation. import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; let { title, ...restProps }: WithoutChildrenOrChild = $props(); {title} Now, the CustomAccordionTrigger component won't expose children or child props to the user, but will expose the other root component props.","description":"A type helper to exclude the child ad children snippet props from a component.","href":"/docs/type-helpers/without-children-or-child"},{"title":"WithoutChildren","content":"The WithoutChildren type helper is used to exclude the children snippet prop from a component. This is useful when you're building custom component wrappers that populate the children prop of a component. import { Accordion, type WithoutChildren } from \"bits-ui\"; let { value, onValueChange, ...restProps }: WithoutChildren = $props(); In the example above, we're using the WithoutChildren type helper to exclude the children snippet prop from the Accordion.Root component. This ensures our exposed props are consistent with what is being used internally.","description":"A type helper to exclude the children snippet prop from a component.","href":"/docs/type-helpers/without-children"},{"title":"Child Snippet","content":"Usage Many Bits UI components have a default HTML element that wraps their children. For example, Accordion.Trigger typically renders as: {@render children()} While you can set standard button attributes, you might need more control for: Applying Svelte transitions or actions Using custom components Scoped CSS This is where the child snippet comes in. Components supporting render delegation accept an optional child prop, which is a Svelte snippet. When used, the component passes its attributes to this snippet, allowing you to apply them to any element. Let's take a look at an example using the Accordion.Trigger component: {#snippet child({ props })} Open accordion item {/snippet} The props object includes event handlers, ARIA attributes, and any other attributes passed to Accordion.Trigger. Note that when using child, other children outside this snippet are ignored. Custom IDs & Attributes To use custom IDs, event handlers, or other attributes with a custom element, you must pass them to the component first. This is crucial because: Many Bits UI internals rely on specific IDs Props are merged using a $2 function to handle cancelling internal handlers, etc. Correct usage: console.log(\"clicked\")}> {#snippet child({ props })} Open accordion item {/snippet} In this example, my-custom-id, the click event handler, and my-custom-class are properly merged into the props object, ensuring they work alongside Bits UI's internal logic. Behind the scenes, components using the child prop typically implement logic similar to this: // other imports/props/logic omitted for brevity let { child, children, ...restProps } = $props(); const trigger = makeTrigger(); const mergedProps = $derived(mergeProps(restProps, trigger.props)); {#if child} {@render child({ props: mergedProps })} {:else} {@render children?.()} {/if} Floating Content Components Floating content components (tooltips, popovers, dropdowns, etc.) require special handling when used with the child snippet due to their positioning requirements with Floating UI. Implementation Details When implementing floating content, you must: Include a wrapper element within the child snippet Spread the wrapperProps prop to this wrapper element Place your floating content inside this wrapper {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Important Considerations The wrapper element must remain unstyled as its positioning is managed internally by Floating UI The wrapperProps contain computed positioning data essential for proper floating behavior Modifying the wrapper element's styles or structure may break positioning calculations Affected Components The following components require a wrapper element: Combobox.Content DatePicker.Content DateRangePicker.Content DropdownMenu.Content LinkPreview.Content Menubar.Content Popover.Content Select.Content Tooltip.Content","description":"Learn how to use the `child` snippet to render your own elements.","href":"/docs/child-snippet"},{"title":"Dates and Times","content":"The date and time components in Bits UI are built on top of the $2 package, which provides a unified API for working with dates and times in different locales and time zones. It's heavily inspired by the $2 proposal, and intends to back the objects in this package with the Temporal API once it's available. You can install the package using your favorite package manager: npm install @internationalized/date It's highly recommended to familiarize yourself with the package's documentation before diving into the components. We'll cover the basics of how we use the package in Bits UI in the sections below, but their documentation provides much more detail on the various formats and how to work with them. DateValue We use the DateValue objects provided by @internationalized/date to represent dates and times in a consistent way. These objects are immutable and provide information about the type of date they represent. The DateValue is a union of the following three types: CalendarDate - Represents a date with no time component, such as 2024-07-10 CalendarDateTime - Represents a date and time, such as 2024-07-10T12:30:00 ZonedDateTime - Represents a date and time with a time zone, such as 2023-10-11T21:00:00:00-04:00[America/New_York] The benefit of using these objects is that they allow you to be very specific about the type of date you want, and the component will adapt to that type. For example, if you pass a CalendarDate object to a DateField component, it will only display the date portion of the date, without the time. See the $2 component for more information. CalendarDate The CalendarDate object represents a date with no time component, such as 2024-07-10. You can use the CalendarDate constructor to create a new CalendarDate object: import { CalendarDate } from \"@internationalized/date\"; const date = new CalendarDate(2024, 7, 10); You can also use the parseDate function to parse an $2 string into a CalendarDate object: import { parseDate } from \"@internationalized/date\"; const date = parseDate(\"2024-07-10\"); If you want to create a CalendarDate with the current date, you can use the today function. This function requires a timezone identifier as an argument, which can be passed in as a string, or by using getLocalTimeZone which returns the user's current time zone: import { today, getLocalTimeZone } from \"@internationalized/date\"; const losAngelesToday = today(\"America/Los_Angeles\"); const localToday = today(getLocalTimeZone()); See the $2 for more information. CalendarDateTime The CalendarDateTime object represents a date and time, such as 2024-07-10T12:30:00. You can use the CalendarDateTime constructor to create a new CalendarDateTime object: import { CalendarDateTime } from \"@internationalized/date\"; const dateTime = new CalendarDateTime(2024, 7, 10, 12, 30, 0); You can also use the parseDateTime function to parse an $2 string into a CalendarDateTime object: import { parseDateTime } from \"@internationalized/date\"; const dateTime = parseDateTime(\"2024-07-10T12:30:00\"); See the $2 for more information. ZonedDateTime The ZonedDateTime object represents a date and time with a time zone, which represents an exact date and time in a specific time zone. ZonedDateTimes are often used for things such as in person events (concerts, conferences, etc.), where you want to represent a date and time in a specific time zone, rather than a specific date and time in the user's local time zone. You can use the ZonedDateTime constructor to create a new ZonedDateTime object: import { ZonedDateTime } from \"@internationalized/date\"; const date = new ZonedDateTime( // Date 2022, 2, 3, // Time zone and UTC offset \"America/Los_Angeles\", -28800000, // Time 9, 15, 0 ); You can also use one of the following parsing functions to parse an $2 string into a ZonedDateTime object: import { parseZonedDateTime, parseAbsolute, parseAbsoluteToLocal } from \"@internationalized/date\"; const date = parseZonedDateTime(\"2024-07-12T00:45[America/New_York]\"); // or const date = parseAbsolute(\"2024-07-12T07:45:00Z\", \"America/New_York\"); // or const date = parseAbsoluteToLocal(\"2024-07-12T07:45:00Z\"); See the $2 for more information. Date Ranges Bits UI also provides a DateRange type with the following structure: type DateRange = { start: DateValue; end: DateValue; }; This type is used to represent the value of the various date range components in Bits UI, such as the $2, $2, and $2. Placeholder Each of the date/time components in Bits UI has a bindable placeholder prop, which acts as the starting point for the component when no value is present. The placeholder value is used to determine the type of date/time to display, and the component and its value will adapt to that type. For example, if you pass a CalendarDate object to a DateField component, it will only display the date portion of the date, without the time. If you pass a CalendarDateTime object, it will display the date and time. If you pass a ZonedDateTime object, it will display the date and time with the time zone information. In addition to setting the starting point and type of the date/time, the placeholder is also used to control the view of the calendar. For example, if you wanted to give the user the ability to select a specific month to jump to in the calendar, you could simply update the placeholder to a DateValue representing that month. Here's an example of how you might do that: import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, type DateValue } from \"@internationalized/date\"; let placeholder: DateValue = $state(today(getLocalTimeZone())); let selectedMonth: number = $state(placeholder.month); { placeholder = placeholder.set({ month: selectedMonth }); }} bind:value={selectedMonth} January February In the example above, we're using the placeholder value to control the view of the calendar. The user can select a specific month to jump to in the calendar, and the placeholder will be updated to reflect the selected month. When the placeholder is updated, the calendar view will automatically update to reflect that new month. As the user interacts with the calendar, the placeholder will be updated to reflect the currently focused date in the calendar. If a value is selected in the calendar, the placeholder will be updated to reflect that selected value. Updating the placeholder It's important to note that DateValue objects are immutable, so you can't directly update the placeholder value. Instead, you'll need to reassign the value to the placeholder prop for the changes to reflect. @internationalized/date provides a number of methods for updating the DateValue objects, such as set, add, subtract, and cycle, each of which will return a new DateValue object with the updated value. For example, if you wanted to update the placeholder to the next month, you could use the add method to add one month to the current month in the placeholder value: let placeholder = new CalendarDate(2024, 07, 10); console.log(placeholder.add({ months: 1 })); // 2024-08-10 console.log(placeholder); // 2024-07-10 (unchanged) placeholder = placeholder.add({ months: 1 }); console.log(placeholder); // 2024-08-10 (updated) Formatting Dates @internationalized/date provides a $2 class that is a wrapper around the $2 API that fixes various browser bugs, and polyfills new features. It's highly recommended to use this class to format dates and times in your application, as it will ensure that the formatting is accurate for all locales, time zones, and calendars. Parsing Dates Often, you'll want to parse a string from a database or other source into a DateValue object for use with the date/time components in Bits UI. @internationalized/date provides various parsing functions that can be used to parse strings into each of the supported DateValue objects. parseDate The parseDate function is used to parse a string into a CalendarDate object.","description":"How to work with the various date and time components in Bits UI.","href":"/docs/dates"},{"title":"Figma","content":"The Figma UI Kit is open sourced by $2. import { AspectRatio } from \"bits-ui\"; Grab a copy https://www.figma.com/community/file/1430229712135910564/bits-ui-kit","description":"Every component recreated in Figma.","href":"/docs/figma-file"},{"title":"Getting Started","content":"Installation Install bits using your favorite package manager. npm install bits-ui You can then import and start using them in your app. import { Accordion } from \"bits-ui\"; First First accordion content Second Second accordion content Third Third accordion content `","description":"Learn how to get started using Bits in your app.","href":"/docs/getting-started"},{"title":"Introduction","content":"Bits UI is a collection of headless component primitives for Svelte that prioritizes developer experience, accessibility, and flexibility. Our vision is to empower developers to build high-quality, accessible user interfaces without sacrificing creative control or performance. Why Bits UI? Bring Your Own Styles Most components ship with zero styling. Minimal styles are included only when absolutely necessary for core functionality. You maintain complete control over the visual design, applying your own styles through standard Svelte class props or targeting components via data attributes. See our $2 for implementation details. Empowering DX Every component is designed with developer experience in mind: Extensive TypeScript support Predictable behavior and consistent APIs Comprehensive documentation and examples Flexible event override system for custom behavior Sensible defaults Built for Production Strives to follow $2 Built-in keyboard navigation Screen reader optimization Focus management Composable Architecture Components are designed to work independently or together, featuring: $2 for maximum flexibility Chainable events and callbacks Override-friendly defaults Minimal dependencies Community Bits UI is an open-source project built and maintained by $2 with design support from $2 and his team at $2. We always welcome contributions and feedback from the community. Found an accessibility issue or have a suggestion? $2. Acknowledgments Built on the shoulders of giants: $2 - Inspired our internal architecture and powered the first version of Bits UI $2 - Reference for component API design $2 - Inspiration for date/time components","description":"The headless components for Svelte.","href":"/docs/introduction"},{"title":"LLMs","content":"At the top of each documentation page, you'll find a convenient \"Copy Markdown\" button alongside a direct link to the LLM-friendly version of that page (e.g., /llms.txt). These tools make it easy to copy the content in Markdown format or access the machine-readable llms.txt file tailored for that specific page. Bits UI documentation is designed to be accessible not only to humans but also to large language models (LLMs). We've adopted the $2 proposal standard, which provides a structured, machine-readable format optimized for LLMs. This enables developers, researchers, and AI systems to efficiently parse and utilize our documentation. What is llms.txt? The llms.txt standard is an emerging convention for presenting documentation in a simplified, text-based format that's easy for LLMs to process. By following this standard, Bits UI ensures compatibility with AI tools and workflows, allowing seamless integration into LLM-powered applications, research, or automation systems. Accessing LLM-friendly Documentation To access the LLM-friendly version of any supported Bits UI documentation page, simply append /llms.txt to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. Example Standard Page**: The Accordion component documentation is available at $2. LLM-friendly Version**: Append /llms.txt to access it at $2. Root Index To explore all supported pages in LLM-friendly format, visit the root index at $2. This page provides a comprehensive list of available documentation endpoints compatible with the llms.txt standard. Full LLM-friendly Documentation For a complete, consolidated view of the Bits UI documentation in an LLM-friendly format, navigate to $2. This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. Notes Not all pages may support the /llms.txt suffix (those deemed irrelevant to LLMs, such as the Figma page). Check the root $2 page for an up-to-date list of compatible URLs. The \"Copy Markdown\" button at the top of each page provides the same content you'd find in the /llms.txt of that page. By embracing the llms.txt standard, Bits UI empowers both human developers and AI systems to make the most of our documentation. Whether you're building with Bits UI or training an LLM, these tools are designed to enhance your experience.","description":"How to access LLM-friendly versions of Bits UI documentation.","href":"/docs/llms"},{"title":"Migration Guide","content":" import { Callout } from '$lib/components'; Bits UI v1 is a major update that introduces significant improvements, but it also comes with breaking changes. Since anything before v1.0 was a pre-release, backward compatibility was not guaranteed. This guide will help you transition smoothly, though it may not cover every edge case. We highly recommend reviewing the documentation for each component you use, as their APIs may have changed. Looking for the old documentation? You can still access Bits UI v0.x at $2. However, we encourage you to migrate as soon as possible to take advantage of the latest features and improvements. Why Upgrade? Bits UI has been completely rewritten for Svelte 5, bringing several key benefits: Performance improvements** – Faster rendering and reduced overhead. More flexible APIs** – Easier customization and integration. Bug fixes and stability** – Addressing every bug and issue from v0.x. Better developer experience** – Improved consistency and documentation. Once you get familiar with Bits UI v1, we're confident you'll find it to be a more powerful and streamlined headless component library. Shared Changes el prop replaced with ref**: The el prop has been removed across all components that render and HTML element. Use the ref prop instead. See the $2 documentation for more information. asChild prop replaced with child snippet**: Components that previously used asChild now use the child snippet prop. See the $2 documentation. Transition props removed**: Components no longer accept transition props. Instead, use the child snippet along with forceMount to leverage Svelte transitions. More details in the $2 documentation. let: directives replaced with snippet props**: Components that used to expose data via let: directives now provide it through children/child snippet props. Accordion The multiple prop has been removed from the Accordion.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The various transition props have been removed from the Accordion.Content component. See the $2 documentation for more information. See the $2 documentation for more information. Alert Dialog The various transition props have been removed from the AlertDialog.Content and AlertDialog.Overlay components. See the $2 documentation for more information. To render the dialog content in a portal, you now must wrap the AlertDialog.Content in the AlertDialog.Portal component. The AlertDialog.Action component no longer closes the dialog by default, as we learned it was causing more harm than good when attempting to submit a form with the Action button. See the $2 section for more information on how to handle submitting forms before closing the dialog. Button The Button component no longer accepts a builders prop, instead you should use the child snippet on the various components to receive/pass the attributes to the underlying button. See $2 for more information. Checkbox The Checkbox.Indicator component has been removed in favor of using the children snippet prop to get a reference to the checked state and render a custom indicator. See the $2 documentation for more information. The Checkbox.Input component has been removed in favor of automatically rendering a hidden input when the name prop is provided to the Checkbox.Root component. The checked state of the Checkbox component is now of type boolean instead of boolean | 'indeterminate', indeterminate is its own state now and can be managed via the indeterminate prop. A new component, Checkbox.Group has been introduced to support checkbox groups. See the $2 documentation for more information. Combobox The multiple prop has been removed from the Combobox.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The selected prop has been replaced with a value prop, which is limited to a string (or string[] if type=\"multiple\"). The combobox now automatically renders a hidden input when the name prop is provided to the Combobox.Root component. The Combobox.ItemIndicator component has been removed in favor of using the children snippet prop to get a reference to the selected state and render a custom indicator. See the $2 documentation for more information. Combobox.Group and Combobox.GroupHeading have been added to support groups within the combobox. In Bits UI v0, the Combobox.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Combobox.Portal component that you can wrap around the Combobox.Content to portal the content. Combobox.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Context Menu/Dropdown Menu/Menubar Menu The Menu.RadioIndicator and Menu.CheckboxIndicator components have been removed in favor of using the children snippet prop to get a reference to the checked or selected state and render a custom indicator. See the $2, $2, and $2 documentation for more information. The Menu.Label component, which was used as the heading for a group of items has been replaced with the Menu.GroupHeading component. The href prop on the .Item components has been removed in favor of the child snippet and rendering your own anchor element. In Bits UI v0, the Menu.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Menu.Portal component that you can wrap around the Menu.Content to portal the content. Menu.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Pin Input The PinInput component has been completely overhauled to better act as an OTP input component, with code and inspiration taken from $2 by $2. The best way to migrate is to reference the $2 documentation to see how to use the new component. Popover In Bits UI v0, the Popover.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Popover.Portal component that you can wrap around the Popover.Content to portal the content. Popover.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Radio Group RadioGroup.ItemIndicator has been removed in favor of using the children snippet prop to get a reference to the checked state which provides more flexibility to render a custom indicator as needed. See the $2 documentation for more information. Scroll Area ScrollArea.Content has been removed as it is not necessary for functionality in Bits UI v1. Select The multiple prop has been removed from the Select.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The selected prop has been replaced with a value prop, which is limited to a string (or string[] if type=\"multiple\"). The select now automatically renders a hidden input when the name prop is provided to the Select.Root component. The Select.ItemIndicator component has been removed in favor of using the children snippet prop to get a reference to the selected state and render a custom indicator. See the $2 documentation for more information. Select.Group and Select.GroupHeading have been added to support groups within the Select. Select.Value has been removed in favor of enabling developers to use the value prop to render your own custom label in the trigger to represent the value. In Bits UI v0, the Select.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Select.Portal component that you can wrap around the Select.Content to portal the content. Select.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Slider Slider.Root now requires a type prop to be set to either 'single' or 'multiple' to properly type the value as either a number or number[]. A new prop, onValueCommit has been introduced which is called when the user commits a value change (e.g. by releasing the mouse button or pressing Enter). This is useful for scenarios where you want to update the value only when the user has finished interacting with the slider, not for each movement of the thumb. Tooltip A required component necessary to provide context for shared tooltips, Tooltip.Provider has been added. This replaces the group prop on the previous version's Tooltip component. You can wrap your entire app with Tooltip.Provider, or wrap a specific section of your app with it to provide shared context for tooltips.","description":"Learn how to migrate from 0.x to 1.x","href":"/docs/migration-guide"},{"title":"Ref","content":"Bits UI components with underlying HTML elements provide a ref prop for direct element access. For example, Accordion.Trigger's ref gives access to its rendered HTMLButtonElement: import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); } Focus trigger With delegation Bits UI tracks the reference to the underlying element using its id attribute. This means that even if you use a custom element/component with $2, the ref prop will still work. import CustomButton from \"./CustomButton.svelte\"; import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); } {#snippet child({ props })} {/snippet} One caveat is that if you wish to use a custom id on the element, you must pass it to the component first, so it can be registered and associated with the ref prop. The id you pass will be passed down via the props snippet prop on the child snippet. import CustomButton from \"./CustomButton.svelte\"; import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); } const myCustomId = \"my-custom-id\"; {#snippet child({ props })} {/snippet} The following example would not work, as the Accordion.Trigger component has no idea what the id of the CustomButton is. import CustomButton from \"./CustomButton.svelte\"; import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); // will always be undefined } {#snippet child({ props })} {/snippet} Why Possibly null? The ref prop may be null until the element has mounted, especially with the many components that use conditional rendering. This HTMLElement | null type mimics browser DOM methods like getElementById. WithElementRef Bits UI exposes a $2 type which enables you to create your own components following the same ref prop pattern.","description":"Learn about the $bindable ref prop.","href":"/docs/ref"},{"title":"State Management","content":"State management is a critical aspect of modern UI development. Bits UI components support multiple approaches to manage component state, giving you flexibility based on your specific needs. Each component's API reference will highlight what props are bindable. You can replace the value prop used in the examples on this page with any bindable prop. Two-Way Binding The simplest approach is using Svelte's built-in two-way binding with bind:: import { ComponentName } from \"bits-ui\"; let myValue = $state(\"default-value\"); (myValue = \"new-value\")}> Update Value Why Use It? Zero-boilerplate state updates External controls work automatically Great for simple use cases Function Binding For complete control, use a $2 that handles both getting and setting values: import { ComponentName } from \"bits-ui\"; let myValue = $state(\"default-value\"); function getValue() { return myValue; } function setValue(newValue: string) { // Only update during business hours const now = new Date(); const hour = now.getHours(); if (hour >= 9 && hour When the component wants to set the value from an internal action, it will invoke the setter, where you can determine if the setter actually updates the state or not. When to Use Complex state transformation logic Conditional updates Debouncing or throttling state changes Maintaining additional state alongside the primary value Integrating with external state systems","description":"How to manage the state of Bits UI components","href":"/docs/state-management"},{"title":"Styling","content":"We ship almost zero styles with Bits UI. This is intentional. We want to give you the most flexibility possible when it comes to styling your components. For each component that renders an HTML element, we expose a class prop that you can use to apply styles to the component. This is the recommended and most straightforward way to style them. CSS frameworks If you're using a CSS framework like TailwindCSS or UnoCSS, you can simply pass the classes you need to the component, and they will be applied to the underlying HTML element. import { Button } from \"bits-ui\"; Click me Data attributes A data attribute is applied to each element rendered by Bits UI, which you can use to target the component across your entire application. Check out the API reference of the component to determine what those data attributes are. You can then use those data attributes like so: Define global styles [data-button-root] { height: 3rem; width: 100%; background-color: #3182ce; color: #fff; } Import stylesheet import \"../app.pcss\"; let { children } = $props(); {@render children()} Now every `` component will have the styles applied to it. Global classes If you prefer the class approach, you can simply apply your global classes to the component. 1. Define global styles .button { height: 3rem; width: 100%; background-color: #3182ce; color: #fff; } 2. Apply global styles import \"../app.pcss\"; let { children } = $props(); {@render children()} 3. Use with components import { Button } from \"bits-ui\"; Click me Scoped Styles If you wish to use Svelte's scoped styles, you must use the child snippet for the various components that support it. This moves the underlying HTML element out of the Bits UI component scope and into the scope of your component. See the $2 documentation for more information. Style Prop Bits UI components accept a style prop, which can either be a string or an object of CSS properties and values. These are gracefully merged with the component's internal styles to create a single style object using the $2 function.","description":"Learn how to style Bits UI components.","href":"/docs/styling"},{"title":"Transitions","content":" import Callout from '$lib/components/callout.svelte'; Svelte Transitions are one of the awesome features of Svelte. Unfortunately, they don't play very nicely with components, due to the fact that they rely on various directives like in:, out:, and transition:, which aren't supported by components. In previous version of Bits UI, we had a workaround for this by exposing a ton of transition* props on the components that we felt were most likely to be used with transitions. However, this was a bit of a hack and limited us to only Svelte Transitions, and users who wanted to use other libraries or just CSS were left out. With Bits UI for Svelte 5, we've completely removed this workaround and instead exposed props and snippets that allow you to use any animation or transitions library you want. The Defaults By default, Bits UI components handle the mounting and unmounting of specific components for you. They are wrapped in a component that ensures the component waits for transitions to finish before unmounting. You can use any CSS transitions or animations you want with this approach, which is what we're doing in the various example components in this documentation, using $2. Force Mounting On each component that we conditionally render, a forceMount prop is exposed. If set to true, the component will be forced to mount in the DOM and become visible to the user. You can use this prop in conjunction with the $2 child snippet to conditionally render the component and apply Svelte Transitions or another animation library. The child snippet exposes a prop that you can use to conditionally render the element and apply your transitions. import { Dialog } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ props, open })} {#if open} {/if} {/snippet} In the example above, we're using the forceMount prop to tell the component to forcefully mount the Dialog.Content component. We're then using the child snippet to delegate the rendering of the Dialog.Content to a div element which we can apply our props and transitions to. We understand this isn't the prettiest syntax, but it enables us to cover every use case. If you intend to use this approach across your application, it's recommended to create a reusable component that handles this logic, like so: import type { Snippet } from \"svelte\"; import { fly } from \"svelte/transition\"; import { Dialog, type WithoutChildrenOrChild } from \"bits-ui\"; let { ref = $bindable(null), children, ...restProps }: WithoutChildrenOrChild & { children?: Snippet; } = $props(); {#snippet child({ props, open })} {#if open} {@render children?.()} {/if} {/snippet} Which can then be used alongside the other Dialog.* components: import { Dialog } from \"bits-ui\"; import MyDialogContent from \"$lib/components/MyDialogContent.svelte\"; Open Dialog Dialog Title Dialog Description Close Other dialog content Floating Content Components Content components that rely on Floating UI require a slight modification to how the child snippet is used. For example, if we were to use the Popover.Content component, we need to add a wrapper element within the child snippet, and spread the wrapperProps snippet prop to it. import { Popover } from \"bits-ui\"; import { fly } from \"svelte/transition\"; Open Popover {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} `","description":"Learn how to use transitions with Bits UI components.","href":"/docs/transitions"}] \ No newline at end of file +[{"title":"Accordion","content":" import { APISection, ComponentPreviewV2, AccordionDemo, AccordionDemoTransitions, AccordionDemoCustom, AccordionDemoHorizontalCards, Callout, AccordionDemoCheckoutSteps } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Accordion component is a versatile UI element designed to organize content into collapsible sections, helping users focus on specific information without being overwhelmed by visual clutter. Quick Start import { Accordion } from \"bits-ui\"; Item 1 Title This is the collapsible content for this section. Item 2 Title This is the collapsible content for this section. Key Features Single or Multiple Mode**: Toggle between allowing one open section or multiple sections at once. Accessible by Default**: Built-in ARIA attributes and keyboard navigation support. Smooth Transitions**: Leverage CSS variables or Svelte transitions for animated open/close effects. Flexible State**: Use uncontrolled defaults or take full control with bound values. Structure The Accordion is a compound component made up of several parts: Accordion.Root: Container that manages overall state Accordion.Item: Individual collapsible section Accordion.Header: Contains the visible heading Accordion.Trigger: The clickable element that toggles content visibility Accordion.Content: The collapsible body content Reusable Components To streamline usage in larger applications, create custom wrapper components for repeated patterns. Below is an example of a reusable MyAccordionItem and MyAccordion. Item Wrapper Combines Item, Header, Trigger, and Content into a single component: import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { title: string; content: string; }; let { title, content, ...restProps }: Props = $props(); {item.title} {content} Accordion Wrapper Wraps Root and renders multiple MyAccordionItem components: import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; import MyAccordionItem from \"$lib/components/MyAccordionItem.svelte\"; type Item = { value?: string; title: string; content: string; disabled?: boolean; }; let { value = $bindable(), ref = $bindable(null), ...restProps }: WithoutChildrenOrChild & { items: Item[]; } = $props(); {#each items as item, i (item.title + i)} {/each} Usage Example import MyAccordion from \"$lib/components/MyAccordion.svelte\"; const items = [ { title: \"Item 1\", content: \"Content 1\" }, { title: \"Item 2\", content: \"Content 2\" }, ]; Use unique value props for each Item if you plan to control the state programatically. Managing Value State This section covers how to manage the value state of the Accordion. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Accordion } from \"bits-ui\"; let myValue = $state([]); const numberOfItemsOpen = $derived(myValue.length); { myValue = [\"item-1\", \"item-2\"]; }} Open Items 1 and 2 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Accordion } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } See the $2 documentation for more information. Customization Single vs. Multiple Set the type prop to \"single\" to allow only one accordion item to be open at a time. Set the type prop to \"multiple\" to allow multiple accordion items to be open at the same time. Default Open Items Set the value prop to pre-open items: Disable Items Disable specific items with the disabled prop: Svelte Transitions The Accordion component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets 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. {#snippet child({ props, open })} {#if open} This is the accordion content that will transition in and out. {/if} {/snippet} 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()} {/snippet} Best Practices For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; import type { Snippet } from \"svelte\"; import { fade } from \"svelte/transition\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children: Snippet; } = $props(); {#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: A Examples The following examples demonstrate different ways to use the Accordion component. Horizontal Cards Use the Accordion component to create a horizontal card layout with collapsible sections. {#snippet preview()} {/snippet} Checkout Steps Use the Accordion component to create a multi-step checkout process. {#snippet preview()} {/snippet} ","description":"Organizes content into collapsible sections, allowing users to focus on one or more sections at a time.","href":"/docs/components/accordion"},{"title":"Alert Dialog","content":" import { APISection, ComponentPreviewV2, AlertDialogDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Key Features Compound Component Structure**: Build flexible, customizable alert dialogs using sub-components. Accessibility**: ARIA-compliant with full keyboard navigation support. Portal Support**: Render content outside the normal DOM hierarchy for proper stacking. Managed Focus**: Automatically traps focus with customization options. Flexible State**: Supports both controlled and uncontrolled open states. Structure The Alert Dialog is built from sub-components, each with a specific purpose: Root**: Manages state and provides context to child components. Trigger**: Toggles the dialog's open/closed state. Portal**: Renders its children in a portal, outside the normal DOM hierarchy. Overlay**: Displays a backdrop behind the dialog. Content**: Holds the dialog's main content. Title**: Displays the dialog's title. Description**: Displays a description or additional context for the dialog. Cancel**: Closes the dialog without action. Action**: Confirms the dialog's action. Here's a simple example of an Alert Dialog: import { AlertDialog } from \"bits-ui\"; Open Dialog Confirm Action Are you sure? Cancel Confirm Reusable Components For consistency across your app, create a reusable Alert Dialog component. Here's an example: import type { Snippet } from \"svelte\"; import { AlertDialog, type WithoutChild } from \"bits-ui\"; type Props = AlertDialog.RootProps & { buttonText: string; title: Snippet; description: Snippet; contentProps?: WithoutChild; // ...other component props if you wish to pass them }; let { open = $bindable(false), children, buttonText, contentProps, title, description, ...restProps }: Props = $props(); {buttonText} {@render title()} {@render description()} {@render children?.()} Cancel Confirm You can then use the MyAlertDialog component in your application like so: import MyAlertDialog from \"$lib/components/MyAlertDialog.svelte\"; {#snippet title()} Delete your account {/snippet} {#snippet description()} This action cannot be undone. {/snippet} Alternatively, you can define the snippets separately and pass them as props to the component: import MyAlertDialog from \"$lib/components/MyAlertDialog.svelte\"; {#snippet title()} Delete your account {/snippet} {#snippet description()} This action cannot be undone. {/snippet} Use string props for simplicity or snippets for dynamic content. Managing Open State This section covers how to manage the open state of the Alert Dialog. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { AlertDialog } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Dialog Fully Controlled Use a $2 for total control: import { AlertDialog } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } See the $2 documentation for more information. Focus Management Focus Trap Focus is trapped within the dialog by default. To disable: Disabling focus trap may reduce accessibility. Use with caution. Open Focus By default, when a dialog is opened, focus will be set to the AlertDialog.Cancel button if it exists, or the first focusable element within the AlertDialog.Content. This ensures that users navigating my keyboard end up somewhere within the Dialog that they can interact with. You can override this behavior using the onOpenAutoFocus prop on the AlertDialog.Content component. It's highly recommended that you use this prop to focus something within the Dialog. 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. import { AlertDialog } from \"bits-ui\"; let nameInput = $state(); Open AlertDialog { e.preventDefault(); nameInput?.focus(); }} Close Focus By default, when a dialog is closed, focus will be set to the trigger element of the dialog. You can override this behavior using the onCloseAutoFocus prop on the AlertDialog.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. import { AlertDialog } from \"bits-ui\"; let nameInput = $state(); Open AlertDialog { e.preventDefault(); nameInput?.focus(); }} 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 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: Enabling body scroll may affect user focus and accessibility. Use this option judiciously. Escape Key Handling 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: Method 2: onEscapeKeydown For more granular control, override the default behavior using the onEscapeKeydown prop: { e.preventDefault(); // do something else instead }} This method allows you to implement custom logic when the Escape key is pressed. Interaction Outside By default, interacting outside the Alert Dialog content area does not close the dialog. Bits UI offers two ways to modify this behavior. Method 1: 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: 'ignore' (default): Prevents the Alert Dialog from closing. 'close': Closes the Alert Dialog immediately. '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 make the Alert Dialog close when an interaction occurs outside the content, set the interactOutsideBehavior prop to 'close' on AlertDialog.Content: Method 2: onInteractOutside For custom handling of outside interactions, you can override the default behavior using the onInteractOutside prop: { e.preventDefault(); // do something else instead }} This approach allows you to implement specific behaviors when users interact outside the Alert Dialog content. Best Practices 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. By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards. Nested Dialogs Dialogs can be nested within each other to create more complex layouts. See the $2 component for more information on nested dialogs. Svelte Transitions See the $2 component for more information on Svelte Transitions with dialog components. Working with Forms Form Submission When using the AlertDialog component, often you'll want to submit a form or perform an asynchronous action when the user clicks the Action button. This can be done by waiting for the asynchronous action to complete, then programmatically closing the dialog. import { AlertDialog } from \"bits-ui\"; function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } let open = $state(false); Confirm your action Are you sure you want to do this? { wait(1000).then(() => (open = false)); }} No, cancel (close dialog) Yes (submit form) Inside a Form If you're using an AlertDialog within a form, you'll need to ensure that the Portal is disabled or not included in the AlertDialog structure. This is because the Portal will render the dialog content outside of the form, which will prevent the form from being submitted correctly. ","description":"A modal window that alerts users with important information and awaits their acknowledgment or action.","href":"/docs/components/alert-dialog"},{"title":"Aspect Ratio","content":" import { APISection, ComponentPreviewV2, AspectRatioDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Architecture Root**: The root component which contains the aspect ratio logic Structure Here's an overview of how the Aspect Ratio component is structured in code: import { AspectRatio } from \"bits-ui\"; Reusable Component If you plan on using a lot of AspectRatio components throughout your application, you can create a reusable component that combines the AspectRatio.Root and whatever other elements you'd like to render within it. In the following example, we're creating a reusable MyAspectRatio component that takes in a src prop and renders an img element with the src prop. import { AspectRatio, type WithoutChildrenOrChild } from \"bits-ui\"; let { src, alt, ref = $bindable(null), imageRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { src: string; alt: string; imageRef?: HTMLImageElement | null; } = $props(); You can then use the MyAspectRatio component in your application like so: import MyAspectRatio from \"$lib/components/MyAspectRatio.svelte\"; Custom Ratio Use the ratio prop to set a custom aspect ratio for the image. ","description":"Displays content while maintaining a specified aspect ratio, ensuring consistent visual proportions.","href":"/docs/components/aspect-ratio"},{"title":"Avatar","content":" import { APISection, ComponentPreviewV2, AvatarDemo, AvatarDemoLinkPreview } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Avatar component provides a consistent way to display user or entity images throughout your application. It handles image loading states gracefully and offers fallback options when images fail to load, ensuring your UI remains resilient. Features Smart Image Loading**: Automatically detects and handles image loading states Fallback System**: Displays alternatives when images are unavailable or slow to load Compound Structure**: Flexible primitives that can be composed and customized Customizable**: Choose to show the image immediately without a load check when you're certain the image will load. Architecture The Avatar component follows a compound component pattern with three key parts: Avatar.Root**: Container that manages the state of the image and its fallback Avatar.Image**: Displays user or entity image Avatar.Fallback**: Shows when the image is loading or fails to load Quick Start To get started with the Avatar component, you can use the Avatar.Root, Avatar.Image, and Avatar.Fallback primitives to create a basic avatar component: import { Avatar } from \"bits-ui\"; HB Reusable Components You can create your own reusable Avatar component to maintain consistent styling and behavior throughout your application: import { Avatar, type WithoutChildrenOrChild } from \"bits-ui\"; let { src, alt, fallback, ref = $bindable(null), imageRef = $bindable(null), fallbackRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { src: string; alt: string; fallback: string; imageRef?: HTMLImageElement | null; fallbackRef?: HTMLElement | null; } = $props(); {fallback} Then use it throughout your application: import UserAvatar from \"$lib/components/UserAvatar.svelte\"; const users = [ { handle: \"huntabyte\", initials: \"HJ\" }, { handle: \"pavelstianko\", initials: \"PS\" }, { handle: \"adriangonz97\", initials: \"AG\" }, ]; {#each users as user} {/each} Customization Skip Loading Check When you're confident that an image will load (such as local assets), you can bypass the loading check: import { Avatar } from \"bits-ui\"; // local asset that's guaranteed to be available import localAvatar from \"/avatar.png\"; HB Examples Clickable with Link Preview This example demonstrates how to create a clickable avatar composed with a $2: {#snippet preview()} {/snippet} ","description":"Represents a user or entity with a recognizable image or placeholder in UI elements.","href":"/docs/components/avatar"},{"title":"Button","content":" import { APISection, ComponentPreviewV2, ButtonDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Button } from \"bits-ui\"; ","description":"A component that if passed a `href` prop will render an anchor element instead of a button element.","href":"/docs/components/button"},{"title":"Calendar","content":" import { APISection, ComponentPreviewV2, CalendarDemo, CalendarDemoSelects, CalendarDemoPresets, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { Calendar } from \"bits-ui\"; {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} Placeholder The placeholder prop for the Calendar.Root component determines what date our calendar should start with when the user hasn't selected a date yet. It also determines the current \"view\" of the calendar. As the user navigates through the calendar, the placeholder will be updated to reflect the currently focused date in that view. By default, the placeholder will be set to the current date, and be of type CalendarDate. Managing Placeholder State This section covers how to manage the placeholder state of the Calendar. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { Calendar } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myPlaceholder = new CalendarDate(2024, 8, 3))}> Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Calendar } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } See the $2 documentation for more information. Managing Value State This section covers how to manage the value state of the Calendar. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Calendar } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Calendar } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } See the $2 documentation for more information. Default Value Often, you'll want to start the Calendar.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. import { Calendar } from \"bits-ui\"; import { parseDate } from \"@internationalized/date\"; // this came from a database/API call const date = \"2024-08-03\"; let value = $state(parseDate(date)); Validation Minimum Value You can set a minimum value for the calendar by using the minValue prop on Calendar.Root. If a user selects a date that is less than the minimum value, the calendar will be marked as invalid. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const yesterday = todayDate.subtract({ days: 1 }); Maximum Value You can set a maximum value for the calendar by using the maxValue prop on Calendar.Root. If a user selects a date that is greater than the maximum value, the calendar will be marked as invalid. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); Unavailable Dates You can specify specific dates that are unavailable for selection by using the isDateUnavailable prop. This prop accepts a function that returns a boolean value indicating whether a date is unavailable or not. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, isNotNull } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); function isDateUnavailable(date: DateValue) { return date.day === 1; } Disabled Dates You can specify specific dates that are disabled for selection by using the isDateDisabled prop. import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, isNotNull } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); function isDateDisabled(date: DateValue) { return date.day === 1; } Appearance & Behavior Fixed Weeks You can use the fixedWeeks prop to ensure that the calendar renders a fixed number of weeks, regardless of the number of days in the month. This is useful to keep the calendar visually consistent when the number of days in the month changes. Multiple Months You can use the numberOfMonths prop to render multiple months at once. Paged Navigation By default, when the calendar has more than one month, the previous and next buttons will shift the calendar forward or backward by one month. However, you can change this behavior by setting the pagedNavigation prop to true, which will shift the calendar forward or backward by the number of months being displayed. Localization The calendar will automatically format the content of the calendar according to the locale prop, which defaults to 'en-US', but can be changed to any locale supported by the $2 API. Week Starts On The calendar will automatically format the content of the calendar according to the locale, which will determine what day of the week is the first day of the week. You can also override this by setting the weekStartsOn prop, where 0 is Sunday and 6 is Saturday to force a consistent first day of the week across all locales. Multiple Selection You can set the type prop to 'multiple' to allow users to select multiple dates at once. Custom Composition Month Selector The Calendar component includes a PrevButton and NextButton component to allow users to navigate between months. This is useful, but sometimes you may want to allow the user to select a specific month from a list of months, rather than having to navigate one at a time. To achieve this, you can use the placeholder prop to set the month of the the calendar view programmatically. import { Calendar } from \"bits-ui\"; import { CalendarDate } from \"@internationalized/date\"; let placeholder = $state(new CalendarDate(2024, 8, 3)); { placeholder = placeholder.set({ month: 8 }); }} Set month to August Updating the placeholder will update the calendar view to reflect the new month. Examples Month and Year Selects This example demonstrates how to use the placeholder prop to set the month and year of the calendar view programmatically. {#snippet preview()} {/snippet} Preset Dates This example demonstrates how to programatically set the value of the calendar to a specific date when a user presses a button. {#snippet preview()} {/snippet} ","description":"Displays dates and days of the week, facilitating date-related interactions.","href":"/docs/components/calendar"},{"title":"Checkbox","content":" import { APISection, ComponentPreviewV2, CheckboxDemo, CheckboxDemoCustom, CheckboxDemoGroup, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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. Architecture The Checkbox component is composed of the following parts: Root**: The main component that manages the state and behavior of the checkbox. Structure Here's an overview of how the Checkbox component is structured in code: import { Checkbox } from \"bits-ui\"; {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {:else} ❌ {/if} {/snippet} Reusable Components It's recommended to use the Checkbox primitive to create your own custom checkbox component that can be used throughout your application. In the example below, we're using the Checkbox and $2 components to create a custom checkbox component. import { Checkbox, Label, useId, type WithoutChildrenOrChild } from \"bits-ui\"; let { id = useId(), checked = $bindable(false), ref = $bindable(null), labelRef = $bindable(null), ...restProps }: WithoutChildrenOrChild & { labelText: string; labelRef?: HTMLLabelElement | null; } = $props(); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {:else} ❌ {/if} {/snippet} {labelText} You can then use the MyCheckbox component in your application like so: import MyCheckbox from \"$lib/components/MyCheckbox.svelte\"; Managing Checked State This section covers how to manage the checked state of the Checkbox. Two-Way Binding Use bind:checked for simple, automatic state synchronization: import { Checkbox } from \"bits-ui\"; let myChecked = $state(false); (myChecked = false)}> uncheck Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myChecked = $state(false); function getChecked() { return myChecked; } function setChecked(newChecked: boolean) { myChecked = newChecked; } Managing Indeterminate State This section covers how to manage the indeterminate state of the Checkbox. Two-Way Binding Use bind:indeterminate for simple, automatic state synchronization: import MyCheckbox from \"$lib/components/MyCheckbox.svelte\"; let myIndeterminate = $state(true); (myIndeterminate = false)}> clear indeterminate Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myIndeterminate = $state(true); function getIndeterminate() { return myIndeterminate; } function setIndeterminate(newIndeterminate: boolean) { myIndeterminate = newIndeterminate; } Disabled State You can disable the checkbox by setting the disabled prop to true. HTML Forms If you set the name prop, a hidden checkbox input will be rendered to submit the value of the checkbox with a form. By default, the checkbox will be submitted with default checkbox value of 'on' if the checked prop is true. Custom Input Value If you'd prefer to submit a different value, you can use the value prop to set the value of the hidden input. 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. This will apply the required attribute to the hidden input element, ensuring that proper form submission is enforced. Checkbox Groups You can use the Checkbox.Group component to create a checkbox group. import { Checkbox } from \"bits-ui\"; Notifications {#snippet preview()} {/snippet} Managing Value State This section covers how to manage the value state of a Checkbox Group. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Checkbox } from \"bits-ui\"; let myValue = $state([]); { myValue = [\"item-1\", \"item-2\"]; }} Open Items 1 and 2 Items Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Checkbox } from \"bits-ui\"; let myValue = $state([]); function getValue() { return myValue; } function setValue(newValue: string[]) { myValue = newValue; } HTML Forms To render hidden ` elements for the various checkboxes within a group, pass a name to Checkbox.Group`. All descendent checkboxes will then render hidden inputs with the same name. When a Checkbox.Group component is used, its descendent Checkbox.Root components will use certain properties from the group, such as the name, required, and disabled. ","description":"Allow users to switch between checked, unchecked, and indeterminate states.","href":"/docs/components/checkbox"},{"title":"Collapsible","content":" import { APISection, ComponentPreviewV2, CollapsibleDemo, CollapsibleDemoTransitions, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 sub-components that work together to create a fully-featured collapsible. Architecture The Collapsible component is composed of a few sub-components, 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. Structure Here's an overview of how the Collapsible component is structured in code: import { Collapsible } from \"bits-ui\"; Reusable Components It's recommended to use the Collapsible primitives to create your own custom collapsible component that can be used throughout your application. import { Collapsible, type WithoutChild } from \"bits-ui\"; type Props = WithoutChild & { buttonText: string; }; let { open = $bindable(false), ref = $bindable(null), buttonText, children, ...restProps }: Props = $props(); {buttonText} {@render children?.()} You can then use the MyCollapsible component in your application like so: import MyCollapsible from \"$lib/components/MyCollapsible.svelte\"; Here is my collapsible content. Managing Open State This section covers how to manage the open state of the Collapsible. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Collapsible } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Collapsible Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Collapsible } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Svelte Transitions The Collapsible component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets 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. import { Collapsible } from \"bits-ui\"; import { fade } from \"svelte/transition\"; 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. import { Collapsible, type WithoutChildrenOrChild } from \"bits-ui\"; import { fade } from \"svelte/transition\"; import type { Snippet } from \"svelte\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children?: Snippet; } = $props(); {#snippet child({ props, open })} {#if open} {@render children?.()} {/if} {/snippet} You can then use the MyCollapsibleContent component alongside the other Collapsible primitives throughout your application: import { Collapsible } from \"bits-ui\"; import { MyCollapsibleContent } from \"$lib/components\"; Open ","description":"Conceals or reveals content sections, enhancing space utilization and organization.","href":"/docs/components/collapsible"},{"title":"Combobox","content":" import { APISection, ComponentPreviewV2, ComboboxDemo, ComboboxDemoTransition, ComboboxDemoAutoScrollDelay, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Combobox component combines the functionality of an input field with a dropdown list of selectable options. It provides users with the ability to search, filter, and select from a predefined set of choices. Key Features Keyboard Navigation**: Full support for keyboard interactions, allowing users to navigate and select options without using a mouse. Customizable Rendering**: Flexible architecture for rendering options, including support for grouped items. Accessibility**: Built with ARIA attributes and keyboard interactions to ensure screen reader compatibility and accessibility standards. Portal Support**: Ability to render the dropdown content in a portal, preventing layout issues in complex UI structures. Architecture The Combobox component is composed of several sub-components, each with a specific role: Root**: The main container component that manages the state and context for the combobox. Input**: The input field that allows users to enter search queries. Trigger**: The button or element that opens the dropdown list. Portal**: Responsible for portalling the dropdown content to the body or a custom target. Group**: A container for grouped items, used to group related items. GroupHeading**: A heading for a group of items, providing a descriptive label for the group. Item**: An individual item within the list. Separator**: A visual separator between items. Content**: The dropdown container that displays the items. It uses $2 to position the content relative to the trigger. ContentStatic**: An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. Viewport**: The visible area of the dropdown content, used to determine the size and scroll behavior. ScrollUpButton**: A button that scrolls the content up when the content is larger than the viewport. ScrollDownButton**: A button that scrolls the content down when the content is larger than the viewport. Arrow**: An arrow element that points to the trigger when using the Combobox.Content component. Structure Here's an overview of how the Combobox component is structured in code: import { Combobox } from \"bits-ui\"; Reusable Components It's recommended to use the Combobox primitives to build your own custom combobox component that can be reused throughout your application. import { Combobox, type WithoutChildrenOrChild, mergeProps } from \"bits-ui\"; type Props = Combobox.RootProps & { inputProps?: WithoutChildrenOrChild; contentProps?: WithoutChildrenOrChild; }; let { items = [], value = $bindable(), open = $bindable(false), inputProps, contentProps, type, ...restProps }: Props = $props(); let searchValue = $state(\"\"); const filteredItems = $derived.by(() => { if (searchValue === \"\") return items; return items.filter((item) => item.label.toLowerCase().includes(searchValue.toLowerCase())); }); function handleInput(e: Event & { currentTarget: HTMLInputElement }) { searchValue = e.currentTarget.value; } function handleOpenChange(newOpen: boolean) { if (!newOpen) searchValue = \"\"; } const mergedRootProps = $derived(mergeProps(restProps, { onOpenChange: handleOpenChange })); const mergedInputProps = $derived(mergeProps(inputProps, { oninput: handleInput })); Open {#each filteredItems as item, i (i + item.value)} {#snippet children({ selected })} {item.label} {selected ? \"✅\" : \"\"} {/snippet} {:else} No results found {/each} import { CustomCombobox } from \"$lib/components\"; const items = [ { value: \"mango\", label: \"Mango\" }, { value: \"watermelon\", label: \"Watermelon\" }, { value: \"apple\", label: \"Apple\" }, // ... ]; Managing Value State This section covers how to manage the value state of the Combobox. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Combobox } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Combobox } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the Combobox. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Combobox } from \"bits-ui\"; let myOpen = $state(false); (myOpen = true)}> Open Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Combobox } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Opt-out of Floating UI When you use the Combobox.Content component, Bits UI uses $2 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. 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.Input 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. import { Combobox } from \"bits-ui\"; let customAnchor = $state(null!); 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. Custom Scroll Delay The initial and subsequent scroll delays can be controlled using the delay prop on the buttons. For example, we can use the $2 easing function from Svelte to create a smooth scrolling effect that speeds up over time. {#snippet preview()} {/snippet} Native Scrolling/Overflow If you don't want to use the $2 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 To prevent the user from scrolling outside of the Combobox.Content component when open, you can set the preventScroll prop to true. Highlighted Items The Combobox component follows the $2 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. console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Combobox.Content component to use Svelte Transitions or another animation library that requires more control. import { Combobox } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Enables users to pick from a list of options displayed in a dropdown.","href":"/docs/components/combobox"},{"title":"Command","content":" import { APISection, ComponentPreviewV2, CommandDemo, CommandDemoDialog, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Command component, also known as a command menu, is designed to provide users with a quick and efficient way to search, filter, and select items within an application. It combines the functionality of a search input with a dynamic, filterable list of commands or options, making it ideal for applications that require fast navigation or action execution. Key Features Dynamic Filtering**: As users type in the input field, the list of commands or items is instantly filtered and sorted based on an (overridable) scoring algorithm. Keyboard Navigation**: Full support for keyboard interactions, allowing users to quickly navigate and select items without using a mouse. Grouped Commands**: Ability to organize commands into logical groups, enhancing readability and organization. Empty and Loading States**: Built-in components to handle scenarios where no results are found or when results are being loaded. Accessibility**: Designed with ARIA attributes and keyboard interactions to ensure screen reader compatibility and accessibility standards. Architecture The Command component is composed of several sub-components, each with a specific role: Root**: The main container that manages the overall state and context of the command menu. Input**: The text input field where users can type to search or filter commands. List**: The container for the list of commands or items. Viewport**: The visible area of the command list, which applies CSS variables to handle dynamic resizing/animations based on the height of the list. Empty**: A component to display when no results are found. Loading**: A component to display while results are being fetched or processed. Group**: A container for a group of items within the command menu. GroupHeading**: A header element to provide an accessible label for a group of items. GroupItems**: A container for the items within a group. Item**: Individual selectable command or item. LinkItem**: A variant of Command.Item specifically for link-based actions. Separator**: A visual separator to divide different sections of the command list. Structure Here's an overview of how the Command component is structured in code: import { Command } from \"bits-ui\"; Managing Value State Bits UI offers several approaches to manage and synchronize the Command's value state, catering to different levels of control and integration needs. 1. Two-Way Binding For seamless state synchronization, use Svelte's bind:value directive. This method automatically keeps your local state in sync with the component's internal state. import { Command } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Key Benefits Simplifies state management Automatically updates myValue when the internal state changes (e.g., via clicking on an item) Allows external control (e.g., selecting an item via a separate button) 2. Change Handler To perform additional logic on state changes, use the onValueChange prop. This approach is useful when you need to execute side effects when the value changes. import { Command } from \"bits-ui\"; { // do something with the new value console.log(value); }} Use Cases Implementing custom behaviors on value change Integrating with external state management solutions Triggering side effects (e.g., logging, data fetching) 3. Fully Controlled For complete control over the component's state, use a $2 to manage the value state externally. You pass a getter function and a setter function to the bind:value directive, giving you full control over how the value is updated/retrieved. import { Command } from \"bits-ui\"; let myValue = $state(\"\"); myValue, (newValue) => (myValue = newValue)}> When to Use Implementing complex value change 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 $2 documentation. In a Modal You can combine the Command component with the Dialog component to display the command menu within a modal. {#snippet preview()} {/snippet} 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: import { Command } from \"bits-ui\"; function customFilter( commandValue: string, search: string, commandKeywords?: string[] ): number { return commandValue.includes(search) ? 1 : 0; } Extend Default Filter By default, the Command component uses the computeCommandScore function to determine the score of each item and filters/sorts them accordingly. This function is exported for you to use and extend as needed. import { Command, computeCommandScore } from \"bits-ui\"; function customFilter( commandValue: string, search: string, commandKeywords?: string[] ): number { const score = computeCommandScore(commandValue, search, commandKeywords); // Add custom logic here return score; } Disable Filtering You can disable filtering by setting the shouldFilter prop to false. 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. 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. Imperative API For more advanced use cases, such as custom keybindings, the Command.Root component exposes several methods for programmatic control. Access these by binding to the component: import { Command } from \"bits-ui\"; let command: typeof Command.Root; Methods getValidItems() Returns an array of valid (non-disabled, visible) command items. Useful for checking bounds before operations. const items = command.getValidItems(); console.log(items.length); // number of selectable items updateSelectedToIndex(index: number) Sets selection to item at specified index. No-op if index is invalid. // select third item (if it exists) command.updateSelectedToIndex(2); // with bounds check const items = command.getValidItems(); if (index import { Command } from \"bits-ui\"; let command: typeof Command.Root; function jumpToLastItem() { if (!command) return; const items = command.getValidItems(); if (!items.length) return; command.updateSelectedToIndex(items.length - 1); } { if (e.key === \"o\") { jumpToLastItem(); } }} /> Common Mistakes Duplicate values The value of each Command.Item must be unique. If you have two items with the same value, the component will not be able to determine which one to select, causing unexpected behavior when navigating with the keyboard or hovering with the mouse. If the text content of two items are the same for one reason or another, you should use the value prop to set a unique value for each item. When a value is set, the text content is used for display purposes only. The value prop is used for filtering and selection. A common pattern is to postfix the value with something unique, like an ID or a number so that filtering will still match the value. My Item My Item ","description":"A command menu component that can be used to search, filter, and select items.","href":"/docs/components/command"},{"title":"Context Menu","content":" import { APISection, ComponentPreviewV2, ContextMenuDemo, ContextMenuDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ContextMenu } from \"bits-ui\"; {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} Reusable Components If you're planning to use Context Menu in multiple places, you can create a reusable component that wraps the Context Menu component. This example shows you how to create a Context Menu component that accepts a few custom props that make it more capable. import type { Snippet } from \"svelte\"; import { ContextMenu, type WithoutChild } from \"bits-ui\"; type Props = ContextMenu.Props & { trigger: Snippet; items: string[]; contentProps?: WithoutChild; // other component props if needed }; let { open = $bindable(false), children, trigger, items, contentProps, ...restProps }: Props = $props(); {@render trigger()} Select an Office {#each items as item} {item} {/each} You can then use the CustomContextMenu component like this: import CustomContextMenu from \"./CustomContextMenu.svelte\"; {#snippet triggerArea()} Right-click me {/snippet} Alternatively, you can define the snippet(s) separately and pass them as props to the component: import CustomContextMenu from \"./CustomContextMenu.svelte\"; {#snippet triggerArea()} Right-click me {/snippet} Managing Open State This section covers how to manage the open state of the menu. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { ContextMenu } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Context Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { ContextMenu } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newopen; } Checkbox Items You can use the ContextMenu.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { ContextMenu } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} See the $2 for more information. Radio Groups You can combine the ContextMenu.RadioGroup and ContextMenu.RadioItem components to create a radio group within a menu. import { ContextMenu } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} See the $2 and $2 APIs for more information. Nested Menus You can create nested menus using the ContextMenu.Sub component to create complex menu structures. import { ContextMenu } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the ContextMenu.Content component to use Svelte Transitions or another animation library that requires more control. import { ContextMenu } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Displays options or actions relevant to a specific context or selected item, triggered by a right-click.","href":"/docs/components/context-menu"},{"title":"Date Field","content":" import { CalendarDateTime, CalendarDate, now, getLocalTimeZone, parseDate, today } from \"@internationalized/date\"; import { APISection, ComponentPreviewV2, DateFieldDemo, DateFieldDemoCustom, DemoContainer, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Overview The DateField component is an alternative to the native `` element. It provides a more flexible and customizable way to select dates within a designated field. Structure import { DateField } from \"$lib\"; Check-in date {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} Reusable Components It's recommended to use the DateField primitives to build your own custom date field component that can be used throughout your application. The following example shows how you might create a reusable MyDateField component that can be used throughout your application. For style inspiration, reference the featured demo at the top of this page. import { DateField, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { labelText: string; }; let { value, placeholder, name, ...restProps }: Props = $props(); {labelText} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {#snippet preview()} {/snippet} We'll be using this newly created MyDateField component in the following demos and examples to prevent repeating the same code, so be sure to reference it as you go through the documentation. Segments A segment of the DateField represents a not only a specific part of the date, such as the day, month, year, hour, but can also reference a \"literal\" which is typically a separator between the different parts of the date, and varies based on the locale. Notice that in the MyDateField component we created, we're styling the DateField.Segment components differently based on whether they are a \"literal\" or not. Placeholder The placeholder prop for the DateField.Root component isn't what is displayed when the field is empty, but rather what date our field should start with when the user attempts to cycle through the segments. The placeholder can also be used to set a granularity for the date field, which will determine which type of DateValue object is used for the value. By default, the placeholder will be set to the current date, and be of type CalendarDate. However, if we wanted this date field to also allow for selecting a time, we could set the placeholder to a CalendarDateTime object. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDateTime } from \"@internationalized/date\"; If we're collecting a date from the user where we want the timezone as well, we can use a ZonedDateTime object instead. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { now, getLocalTimeZone } from \"@internationalized/date\"; 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 This section covers how to manage the placeholder state of the Date Field. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myPlaceholder = new CalendarDate(2024, 8, 3))}> Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateField } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the Date Field. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled For complete control over the component's state, use a $2 to manage the value state externally. import { DateField } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } 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. import { DateField } from \"bits-ui\"; import { parseDate } from \"@internationalized/date\"; // this came from a database/API call const date = \"2024-08-03\"; let value = $state(parseDate(date)); Now our input is populated with the default value. In addition to the parseDate function, you can also use parseDateTime or parseZonedDateTime to parse the string into a CalendarDateTime or ZonedDateTime object respectively. Validation Minimum Value You can set a minimum value for the DateField.Root component by using the minValue prop. If a user selects a date that is less than the minimum value, the date field will be marked as invalid. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const yesterday = todayDate.subtract({ days: 1 }); In the example above, we're setting the minimum value to today, and the default value to yesterday. This causes the date field to be marked as invalid, and we can style it accordingly. If you adjust the date to be greater than the minimum value, the invalid state will be cleared. Maximum Value You can set a maximum value for the DateField.Root component by using the maxValue prop. If a user selects a date that is greater than the maximum value, the date field will be marked as invalid. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { today, getLocalTimeZone } from \"@internationalized/date\"; const todayDate = today(getLocalTimeZone()); const tomorrow = todayDate.add({ days: 1 }); In the example above, we're setting the maximum value to today, and the default value to tomorrow. This causes the date field to be marked as invalid, and we can style it accordingly. If you adjust the date to be less than the maximum value, the invalid state will be cleared. Custom Validation You can use the validate prop to provide a custom validation function for the date field. This function should return a string or array of strings as validation errors if the date is invalid, or undefined/nothing if the date is valid. The strings are then passed to the onInvalid callback, which you can use to display an error message to the user. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDate, type DateValue } from \"@internationalized/date\"; const value = new CalendarDate(2024, 8, 2); function validate(date: DateValue) { return date.day === 1 ? \"Date cannot be the first day of the month\" : undefined; } function onInvalid(reason: \"min\" | \"max\" | \"custom\", msg?: string | string[]) { if (reason === \"custom\") { if (typeof msg === \"string\") { // do something with the error message console.log(msg); return; } else if (Array.isArray(msg)) { // do something with the error messages console.log(msg); return; } console.log(\"The date is invalid\"); } else if (reason === \"min\") { // let the user know that the date is too early. console.log(\"The date is too early.\"); } else if (reason === \"max\") { // let the user know that the date is too late. console.log(\"The date is too late.\"); } } date.day === 1 ? \"Date cannot be the first day of the month\" : undefined} value={new CalendarDate(2024, 8, 2)} onInvalid={(reason, msg) => { if (reason === \"custom\") { if (typeof msg === \"string\") { // do something with the error message console.log(msg); return; } else if (Array.isArray(msg)) { // do something with the error messages console.log(msg); return; } console.log(\"The date is invalid\"); } else if (reason === \"min\") { // let the user know that the date is too early. console.log(\"The date is too early.\"); } else if (reason === \"max\") { // let the user know that the date is too late. console.log(\"The date is too late.\"); } }} /> In the example above, we're setting the isDateUnavailable prop to a function that returns true for the first day of the month. Try selecting a date that is the first day of the month to see the date field marked as invalid. Granularity The granularity prop sets the granularity of the date field, which determines which segments are rendered in the date field. The granularity can be set to either 'day', 'hour', 'minute', or 'second'. import MyDateField from \"$lib/components/MyDateField.svelte\"; import { CalendarDateTime } from \"@internationalized/date\"; const value = new CalendarDateTime(2024, 8, 2, 12, 30); In the example above, we're setting the granularity to 'second', which means that the date field will include an additional segment for the seconds. Localization You can use the locale prop to set the locale of the date field. This will affect the formatting of the date field's segments and placeholders. import MyDateField from \"$lib/components/MyDateField.svelte\"; ","description":"Enables users to input specific dates within a designated field.","href":"/docs/components/date-field"},{"title":"Date Picker","content":" import { APISection, ComponentPreviewV2, DatePickerDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { DatePicker } from \"bits-ui\"; {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(); { myPlaceholder = new CalendarDateTime(2024, 8, 3, 12, 30); }} Set placeholder to August 3rd, 2024 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); (myValue = myValue.add({ days: 1 }))}> Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateValue) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DatePicker } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open DatePicker Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DatePicker } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Customization The DatePicker component is made up of three other Bits UI components: $2, $2, and $2. You can check out the documentation for each of these components to learn more about their customization options, each of which can be used to customize the DatePicker component. ","description":"Facilitates the selection of dates through an input and calendar-based interface.","href":"/docs/components/date-picker"},{"title":"Date Range Field","content":" import { APISection, ComponentPreviewV2, DateRangeFieldDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Overview The DateRangeField component combines two $2 components to create a date range field. Check out the $2 component documentation for information on how to customize this component. Structure import { DateRangeField } from \"$lib\"; Check-in date {#each [\"start\", \"end\"] as const as type} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {/each} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateRangeField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangeField } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: CalendarDateTime) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateRangeField, type DateRange } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state({ start: new CalendarDateTime(2024, 8, 3, 12, 30), end: new CalendarDateTime(2024, 8, 4, 12, 30), }); { value = { start: value.start.add({ days: 1 }), end: value.end.add({ days: 1 }), }; }} Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangeField } from \"bits-ui\"; let myValue = $state({ start: undefined, end: undefined, }); function getValue() { return myValue; } function setValue(newValue: DateRange) { myValue = newValue; } ","description":"Allows users to input a range of dates within a designated field.","href":"/docs/components/date-range-field"},{"title":"Date Range Picker","content":" import { APISection, ComponentPreviewV2, DateRangePickerDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { DateRangePicker } from \"bits-ui\"; {#each [\"start\", \"end\"] as const as type} {#snippet children({ segments })} {#each segments as { part, value }} {value} {/each} {/snippet} {/each} {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {date.day} {/each} {/each} {/each} {/snippet} Managing Placeholder State This section covers how to manage the placeholder state of the component. Two-Way Binding Use bind:placeholder for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30)); Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker } from \"bits-ui\"; import type { DateValue } from \"@internationalized/date\"; let myPlaceholder = $state(); function getPlaceholder() { return myPlaceholder; } function setPlaceholder(newPlaceholder: DateValue) { myPlaceholder = newPlaceholder; } Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; import { CalendarDateTime } from \"@internationalized/date\"; let myValue = $state({ start: new CalendarDateTime(2024, 8, 3, 12, 30), end: new CalendarDateTime(2024, 8, 4, 12, 30), }); { value = { start: value.start.add({ days: 1 }), end: value.end.add({ days: 1 }), }; }} Add 1 day Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker, type DateRange } from \"bits-ui\"; let myValue = $state(); function getValue() { return myValue; } function setValue(newValue: DateRange) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DateRangePicker } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open DateRangePicker Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DateRangePicker } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Customization The DateRangePicker component is made up of three other Bits UI components: $2, $2, and $2. You can check out the documentation for each of these components to learn more about their customization options, each of which can be used to customize the DateRangePicker component. ","description":"Facilitates the selection of date ranges through an input and calendar-based interface.","href":"/docs/components/date-range-picker"},{"title":"Dialog","content":" import { APISection, ComponentPreviewV2, DialogDemo, DialogDemoCustom, DialogDemoNested, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 sub-components 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 sub-component 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. Architecture The Dialog component is composed of several sub-components, 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. Structure Here's an overview of how the Dialog component is structured in code: import { Dialog } from \"bits-ui\"; Reusable Components Bits UI provides a comprehensive set of Dialog components that serve as building blocks for creating customized, reusable Dialog implementations. This approach offers flexibility in design while maintaining consistency and accessibility across your application. Building a Reusable Dialog The following example demonstrates how to create a versatile, reusable Dialog component using Bits UI building blocks. This implementation showcases the flexibility of the component API by combining props and snippets. import type { Snippet } from \"svelte\"; import { Dialog, type WithoutChild } from \"bits-ui\"; type Props = Dialog.RootProps & { buttonText: string; title: Snippet; description: Snippet; contentProps?: WithoutChild; // ...other component props if you wish to pass them }; let { open = $bindable(false), children, buttonText, contentProps, title, description, ...restProps }: Props = $props(); {buttonText} {@render title()} {@render description()} {@render children?.()} Close Dialog Usage with Inline Snippets import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} Account settings {/snippet} {#snippet description()} Manage your account settings and preferences. {/snippet} Usage with Separate Snippets import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} Account settings {/snippet} {#snippet description()} Manage your account settings and preferences. {/snippet} Best Practices Prop Flexibility**: Design your component to accept props for any nested components for maximum flexibility Styling Options**: Use tools like clsx to merge class overrides Binding Props**: Use bind: and expose $bindable props to provide consumers with full control Type Safety**: Use the exported types from Bits UI to type your component props Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Dialog } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Dialog Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Dialog } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } 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, 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 While not recommended, you can disable the focus trap if absolutely necessary: 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 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. Customizing Initial Focus To specify which element receives focus when the Dialog opens, use the onOpenAutoFocus prop on Dialog.Content: import { Dialog } from \"bits-ui\"; let nameInput = $state(); Open Dialog { e.preventDefault(); nameInput?.focus(); }} Always ensure that something within the Dialog receives focus when it opens. This is crucial for maintaining keyboard navigation context and makes your users happy. Close Focus When a Dialog closes, focus returns to the element that triggered its opening (typically the Dialog.Trigger). Customizing Close Focus To change which element receives focus when the Dialog closes, use the onCloseAutoFocus prop on Dialog.Content: import { Dialog } from \"bits-ui\"; let nameInput = $state(); Open Dialog { e.preventDefault(); nameInput?.focus(); }} Best Practices Always maintain a clear focus management strategy for your Dialogs. Ensure that focus is predictable and logical for keyboard users. Test your focus management with keyboard navigation to verify its effectiveness. Advanced Behaviors Bits UI's 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 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 Dialog.Content: Enabling body scroll may affect user focus and accessibility. Use this option judiciously. Escape Key Handling By default, pressing the Escape key closes an open 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 Dialog immediately. 'ignore': Prevents the 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 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 Dialog will ignore the key press and not close. To always prevent the Dialog from closing on Escape key press, set the escapeKeydownBehavior prop to 'ignore' on Dialog.Content: Method 2: onEscapeKeydown For more granular control, override the default behavior using the onEscapeKeydown prop: { e.preventDefault(); // do something else instead }} This method allows you to implement custom logic when the Escape key is pressed. Interaction Outside By default, interacting outside the Dialog content area closes the Dialog. Bits UI offers two ways to modify this behavior. Method 1: 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: 'close' (default): Closes the Dialog immediately. 'ignore': Prevents the 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 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 Dialog will ignore the event and not close. To always prevent the Dialog from closing when an interaction occurs outside the content, set the interactOutsideBehavior prop to 'ignore' on Dialog.Content: Method 2: onInteractOutside For custom handling of outside interactions, you can override the default behavior using the onInteractOutside prop: { e.preventDefault(); // do something else instead }} This approach allows you to implement specific behaviors when users interact outside the Dialog content. Best Practices 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. By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards. Nested Dialogs Dialogs can be nested within each other to create more complex user interfaces: import MyDialog from \"$lib/components/MyDialog.svelte\"; {#snippet title()} First Dialog {/snippet} {#snippet description()} This is the first dialog. {/snippet} {#snippet title()} Second Dialog {/snippet} {#snippet description()} This is the second dialog. {/snippet} Svelte Transitions The Dialog component can be enhanced with Svelte's built-in transition effects or other animation libraries. Using forceMount and child Snippets To apply Svelte transitions to Dialog components, use the forceMount prop in combination with the child snippet. This approach gives you full control over the mounting behavior and animation of Dialog.Content and Dialog.Overlay. import { Dialog } from \"bits-ui\"; import { fly, fade } from \"svelte/transition\"; {#snippet child({ props, open })} {#if open} {/if} {/snippet} {#snippet child({ props, open })} {#if open} {/if} {/snippet} 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. Best Practices For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic. import { Dialog, type WithoutChildrenOrChild } from \"bits-ui\"; import { fade } from \"svelte/transition\"; import type { Snippet } from \"svelte\"; let { ref = $bindable(null), duration = 200, children, ...restProps }: WithoutChildrenOrChild & { duration?: number; children?: Snippet; } = $props(); {#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: import { Dialog } from \"bits-ui\"; import { MyDialogOverlay } from \"$lib/components\"; Open Working with Forms Form Submission When using the Dialog component, often you'll want to submit a form or perform an asynchronous action and then close the dialog. This can be done by waiting for the asynchronous action to complete, then programmatically closing the dialog. import { Dialog } from \"bits-ui\"; function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } let open = $state(false); Confirm your action Are you sure you want to do this? { wait(1000).then(() => (open = false)); }} Submit form Inside a Form If you're using a Dialog within a form, you'll need to ensure that the Portal is disabled or not included in the Dialog structure. This is because the Portal will render the dialog content outside of the form, which will prevent the form from being submitted correctly. ","description":"A modal window presenting content or seeking user input without navigating away from the current context.","href":"/docs/components/dialog"},{"title":"Dropdown Menu","content":" import { APISection, ComponentPreviewV2, DropdownMenuDemo, DropdownMenuDemoTransition, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { DropdownMenu } from \"bits-ui\"; Reusable Components If you're planning to use Dropdown Menu in multiple places, you can create a reusable component that wraps the Dropdown Menu component. This example shows you how to create a Dropdown Menu component that accepts a few custom props that make it more capable. import type { Snippet } from \"svelte\"; import { DropdownMenu, type WithoutChild } from \"bits-ui\"; type Props = DropdownMenu.Props & { buttonText: string; items: string[]; contentProps?: WithoutChild; // other component props if needed }; let { open = $bindable(false), children, buttonText, items, contentProps, ...restProps }: Props = $props(); {buttonText} {#each items as item} {item} {/each} You can then use the MyDropdownMenu component like this: import MyDropdownMenu from \"./MyDropdownMenu.svelte\"; Managing Open State This section covers how to manage the open state of the menu. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { DropdownMenu } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Context Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { DropdownMenu } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Groups To group related menu items, you can use the DropdownMenu.Group component along with either a DropdownMenu.GroupHeading or an aria-label attribute on the DropdownMenu.Group component. File New Open Save Save As New Open Save Save As Group Heading The DropdownMenu.GroupHeading component must be a child of either a DropdownMenu.Group or DropdownMenu.RadioGroup component. If used on its own, an error will be thrown during development. File Favorite color Checkbox Items You can use the DropdownMenu.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { DropdownMenu } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} The checked state does not persist between menu open/close cycles. To persist the state, you must store it in a $state variable and pass it to the checked prop. Radio Groups You can combine the DropdownMenu.RadioGroup and DropdownMenu.RadioItem components to create a radio group within a menu. import { DropdownMenu } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); Favorite number {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} The value state does not persist between menu open/close cycles. To persist the state, you must store it in a $state variable and pass it to the value prop. Nested Menus You can create nested menus using the DropdownMenu.Sub component to create complex menu structures. import { DropdownMenu } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 --> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the DropdownMenu.Content component to use Svelte Transitions or another animation library that requires more control. import { DropdownMenu } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} Custom Anchor By default, the DropdownMenu.Content is anchored to the DropdownMenu.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 DropdownMenu.Content component. import { DropdownMenu } from \"bits-ui\"; let customAnchor = $state(null!); ","description":"Displays a menu of items that users can select from when triggered.","href":"/docs/components/dropdown-menu"},{"title":"Label","content":" import { APISection, ComponentPreviewV2, LabelDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Label } from \"bits-ui\"; ","description":"Identifies or describes associated UI elements.","href":"/docs/components/label"},{"title":"Link Preview","content":" import { APISection, ComponentPreviewV2, LinkPreviewDemo, LinkPreviewDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 import { LinkPreview } from \"bits-ui\"; Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { LinkPreview } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Link Preview Fully Controlled Use a $2 for complete control over the state's reads and writes. import { LinkPreview } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Opt-out of Floating UI When you use the LinkPreview.Content component, Bits UI uses $2 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. 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. Custom Anchor By default, the LinkPreview.Content is anchored to the LinkPreview.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 LinkPreview.Content component. import { LinkPreview } from \"bits-ui\"; let customAnchor = $state(null!); Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the LinkPreview.Content component to use Svelte Transitions or another animation library that requires more control. import { LinkPreview } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Displays a summarized preview of a linked content's details or information.","href":"/docs/components/link-preview"},{"title":"Menubar","content":" import { APISection, ComponentPreviewV2, MenubarDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Menubar } from \"bits-ui\"; {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} {#snippet children({ checked })} {checked ? \"✅\" : \"\"} {/snippet} Reusable Components If you're planning to use Menubar in multiple places, you can create reusable components that wrap the different parts of the Menubar. In the following example, we're creating a reusable MyMenubarMenu component that contains the trigger, content, and items of a menu. import { Menubar, type WithoutChildrenOrChild } from \"bits-ui\"; type Props = WithoutChildrenOrChild & { triggerText: string; items: { label: string; value: string; onSelect?: () => void }[]; contentProps?: WithoutChildrenOrChild; // other component props if needed }; let { triggerText, items, contentProps, ...restProps }: Props = $props(); {triggerText} {#each items as item} {item.label} {/each} Now, we can use the MyMenubarMenu component within a Menubar.Root component to render out the various menus. import { Menubar } from \"bits-ui\"; import MyMenubarMenu from \"./MyMenubarMenu.svelte\"; const sales = [ { label: \"Michael Scott\", value: \"michael\" }, { label: \"Dwight Schrute\", value: \"dwight\" }, { label: \"Jim Halpert\", value: \"jim\" }, { label: \"Stanley Hudson\", value: \"stanley\" }, { label: \"Phyllis Vance\", value: \"phyllis\" }, { label: \"Pam Beesly\", value: \"pam\" }, { label: \"Andy Bernard\", value: \"andy\" }, ]; const hr = [ { label: \"Toby Flenderson\", value: \"toby\" }, { label: \"Holly Flax\", value: \"holly\" }, { label: \"Jan Levinson\", value: \"jan\" }, ]; const accounting = [ { label: \"Angela Martin\", value: \"angela\" }, { label: \"Kevin Malone\", value: \"kevin\" }, { label: \"Oscar Martinez\", value: \"oscar\" }, ]; const menubarMenus = [ { title: \"Sales\", items: sales }, { title: \"HR\", items: hr }, { title: \"Accounting\", items: accounting }, ]; {#each menubarMenus as { title, items }} {/each} Managing Value State This section covers how to manage the value state of the menubar. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Menubar } from \"bits-ui\"; let activeValue = $state(\"\"); (activeValue = \"menu-1\")}>Open Menubar Menu Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Menubar } from \"bits-ui\"; let activeValue = $state(\"\"); function getValue() { return activeValue; } function setValue(newValue: string) { activeValue = newValue; } Checkbox Items You can use the Menubar.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items. import { Menubar } from \"bits-ui\"; let notifications = $state(true); {#snippet children({ checked, indeterminate })} {#if indeterminate} {:else if checked} ✅ {/if} Notifications {/snippet} Radio Groups You can combine the Menubar.RadioGroup and Menubar.RadioItem components to create a radio group within a menu. import { Menubar } from \"bits-ui\"; const values = [\"one\", \"two\", \"three\"]; let value = $state(\"one\"); {#each values as value} {#snippet children({ checked })} {#if checked} ✅ {/if} {value} {/snippet} {/each} Nested Menus You can create nested menus using the Menubar.Sub component to create complex menu structures. import { Menubar } from \"bits-ui\"; Item 1 Item 2 Open Sub Menu Sub Item 1 Sub Item 2 Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Menubar.Content component to use Svelte Transitions or another animation library that requires more control. import { Menubar } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} Item 1 Item 2 {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. ","description":"Organizes and presents a collection of menu options or actions within a horizontal bar.","href":"/docs/components/menubar"},{"title":"Meter","content":" import { APISection, ComponentPreviewV2, MeterDemo, DemoCodeContainer, MeterDemoCustom } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} While often visually similar, meters and $2 bars serve distinct purposes: Meter: Displays a static measurement within a known range (0-100) Value can fluctuate up/down based on real-time measurements Examples: CPU usage, battery level, sound volume Use when showing current state relative to capacity Progress bar: Shows completion status of a task Value only increases as task progresses Examples: File upload, installation status, form completion Use when tracking advancement toward completion If a progress bar better fits your requirements, check out the $2 component. Structure import { Meter } from \"bits-ui\"; Reusable Components It's recommended to use the Meter primitive to create your own custom meter component that can be used throughout your application. In the example below, we're using the Meter primitive to create a more comprehensive meter component. import { Meter, useId } from \"bits-ui\"; import type { ComponentProps } from \"svelte\"; let { max = 100, value = 0, min = 0, label, valueLabel, }: ComponentProps & { label: string; valueLabel: string; } = $props(); const labelId = useId(); {label} {valueLabel} You can then use the MyMeter component in your application like so: import MyMeter from \"$lib/components/MyMeter.svelte\"; let value = $state(3000); const max = 4000; Of course, you'd want to apply your own styles and other customizations to the MyMeter component to fit your application's design. Accessibility If a visual label is used, the ID of the label element should be pass via the aria-labelledby prop to Meter.Root. If no visual label is used, the aria-label prop should be used to provide a text description of the progress bar. Assistive technologies often present aria-valuenow as a percentage. If conveying the value of the meter only in terms of a percentage would not be user friendly, the aria-valuetext property should be set to a string that makes the meter value understandable. For example, a battery meter value might be conveyed as aria-valuetext=\"50% (6 hours) remaining\". $2] ","description":"Display real-time measurements within a defined range.","href":"/docs/components/meter"},{"title":"Navigation Menu","content":" import { APISection, ComponentPreviewV2, NavigationMenuDemo, Callout, NavigationMenuDemoForceMount } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { NavigationMenu } from \"bits-ui\"; Usage Vertical You can create a vertical menu by using the orientation prop. Flexible Layouts Use the Viewport component when you need extra control over where Content is rendered. This can be useful when your design requires an adjusted DOM structure or if you need flexibility to achieve advanced animations. Tab focus will be managed automatically. Item one Item one content Item two Item two content With Indicator You can use the optional Indicator component to highlight the currently active Trigger, which is useful when you want to provide an animated visual cue such as an arrow or highlight to accompany the Viewport. Item one Item one content Item two Item two content Submenus You can create a submenu by nesting your navigation menu and using the Navigation.Sub component in place of NavigationMenu.Root. Submenus work differently than the Root menus and are more similar to $2 in that one item should always be active, so be sure to assign and pass a value prop. Item one Item one content Item two Sub item one Sub item one content Sub item two Sub item two content Advanced Animation We expose --bits-navigation-menu-viewport-[width|height] and data-motion['from-start'|'to-start'|'from-end'|'to-end'] to allow you to animate the NavigationMenu.Viewport size and NavigationMenu.Content position based on the enter/exit direction. Combining these with position: absolute; allows you to create smooth overlapping animation effects when moving between items. Item one Item one content Item two Item two content /* app.css */ .NavigationMenuContent { position: absolute; top: 0; left: 0; animation-duration: 250ms; animation-timing-function: ease; } .NavigationMenuContent[data-motion=\"from-start\"] { animation-name: enter-from-left; } .NavigationMenuContent[data-motion=\"from-end\"] { animation-name: enter-from-right; } .NavigationMenuContent[data-motion=\"to-start\"] { animation-name: exit-to-left; } .NavigationMenuContent[data-motion=\"to-end\"] { animation-name: exit-to-right; } .NavigationMenuViewport { position: relative; width: var(--bits-navigation-menu-viewport-width); height: var(--bits-navigation-menu-viewport-height); transition: width, height, 250ms ease; } @keyframes enter-from-right { from { opacity: 0; transform: translateX(200px); } to { opacity: 1; transform: translateX(0); } } @keyframes enter-from-left { from { opacity: 0; transform: translateX(-200px); } to { opacity: 1; transform: translateX(0); } } @keyframes exit-to-right { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(200px); } } @keyframes exit-to-left { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-200px); } } Force Mounting You may wish for the links in the Navigation Menu to persist in the DOM, regardless of whether the menu is open or not. This is particularly useful for SEO purposes. You can achieve this by using the forceMount prop on the NavigationMenu.Content and NavigationMenu.Viewport components. Note: Using forceMount requires you to manage the visibility of the elements yourself, using the data-state attributes on the NavigationMenu.Content and NavigationMenu.Viewport components. {#snippet preview()} {/snippet} ","description":"A list of links that allow users to navigate between pages of a website.","href":"/docs/components/navigation-menu"},{"title":"Pagination","content":" import { APISection, ComponentPreviewV2, PaginationDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Pagination } from \"bits-ui\"; {#each pages as page (page.key)} {/each} Managing Page State This section covers how to manage the page state of the component. Two-Way Binding Use bind:page for simple, automatic state synchronization: import { Pagination } from \"bits-ui\"; let myPage = $state(1); (myPage = 2)}> Go to page 2 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Pagination } from \"bits-ui\"; let myPage = $state(1); function getPage() { return myPage; } function setPage(newPage: number) { myPage = newPage; } 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. ","description":"Facilitates navigation between pages.","href":"/docs/components/pagination"},{"title":"PIN Input","content":" import { APISection, ComponentPreviewV2, PinInputDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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 $2 with $2. 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). Architecture Root Container: A relatively positioned root element that encapsulates the entire component. Invisible Input: A hidden input field that manages the actual value and interacts with the browser's built-in features. 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 import { PinInput } from \"bits-ui\"; {#snippet children({ cells })} {#each cells as cell} {/each} {/snippet} Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { PinInput } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"123456\")}> Set value to 123456 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { PinInput } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Paste Transformation The pasteTransformer prop allows you to sanitize/transform pasted text. This can be useful for cleaning up pasted text, like removing hyphens or other characters that should not make it into the input. This function should return the sanitized text, which will be used as the new value of the input. import { PinInput } from \"bits-ui\"; text.replace(/-/g, \"\")}> HTML Forms The PinInput.Root component is designed to work seamlessly with HTML forms. Simply pass the name prop to the PinInput.Root component and the input will be submitted with the form. Submit On Complete To submit the form when the input is complete, you can use the onComplete prop. import { PinInput } from \"bits-ui\"; let form = $state(null!); form.submit()}> Patterns You can use the pattern prop to restrict the characters that can be entered or pasted into the input. Client-side validation cannot replace server-side validation. Use this in addition to server-side validation for an improved user experience. Bits UI exports a few common patterns that you can import and use in your application. REGEXP_ONLY_DIGITS - Only allow digits to be entered. REGEXP_ONLY_CHARS - Only allow characters to be entered. REGEXP_ONLY_DIGITS_AND_CHARS - Only allow digits and characters to be entered. import { PinInput, REGEXP_ONLY_DIGITS } from \"bits-ui\"; ","description":"Allows users to input a sequence of one-character alphanumeric inputs.","href":"/docs/components/pin-input"},{"title":"Popover","content":" import { APISection, ComponentPreviewV2, PopoverDemo, PopoverDemoTransition, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Popover } from \"bits-ui\"; Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Popover } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Popover Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Popover } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Managing Focus Focus Trap By default, when a Popover is opened, focus will be trapped within that Popover. You can disable this behavior by setting the trapFocus prop to false on the Popover.Content component. 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. import { Popover } from \"bits-ui\"; let nameInput = $state(); 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. import { Popover } from \"bits-ui\"; let nameInput = $state(); 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. 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. onEscapeKeydown You can also override the default behavior by cancelling the event passed to the onEscapeKeydown callback on the Popover.Content component. 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. onInteractOutside You can also override the default behavior by cancelling the event passed to the onInteractOutside callback on the Popover.Content component. e.preventDefault()}> Custom Anchor By default, the Popover.Content is anchored to the Popover.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 Popover.Content component. import { Popover } from \"bits-ui\"; let customAnchor = $state(null!); Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Popover.Content component to use Svelte Transitions or another animation library that requires more control. import { Popover } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Display supplementary content or information when users interact with specific elements.","href":"/docs/components/popover"},{"title":"Progress","content":" import { APISection, ComponentPreviewV2, ProgressDemo, ProgressDemoCustom } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} While often visually similar, progress bars and $2 serve distinct purposes: Progress: Shows completion status of a task Value only increases as task progresses Examples: File upload, installation status, form completion Use when tracking advancement toward completion Meter: Displays a static measurement within a known range (0-100) Value can fluctuate up/down based on real-time measurements Examples: CPU usage, battery level, sound volume Use when showing current state relative to capacity If a meter better fits your requirements, check out the $2 component. Structure import { Progress } from \"bits-ui\"; Reusable Components It's recommended to use the Progress primitive to create your own custom meter component that can be used throughout your application. In the example below, we're using the Progress primitive to create a more comprehensive meter component. import { Progress, useId } from \"bits-ui\"; import type { ComponentProps } from \"svelte\"; let { max = 100, value = 0, min = 0, label, valueLabel, }: ComponentProps & { label: string; valueLabel: string; } = $props(); const labelId = useId(); {label} {valueLabel} You can then use the MyProgress component in your application like so: import MyProgress from \"$lib/components/MyProgress.svelte\"; let value = $state(50); Of course, you'd want to apply your own styles and other customizations to the MyProgress component to fit your application's design. Accessibility If a visual label is used, the ID of the label element should be pass via the aria-labelledby prop to Progress.Root. If no visual label is used, the aria-label prop should be used to provide a text description of the progress bar. ","description":"Visualizes the progress or completion status of a task or process.","href":"/docs/components/progress"},{"title":"Radio Group","content":" import { APISection, ComponentPreviewV2, RadioGroupDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { RadioGroup } from \"bits-ui\"; {#snippet children({ checked })} {#if checked} ✅ {/if} {/snippet} Reusable Components It's recommended to use the RadioGroup primitives to create your own custom components that can be used throughout your application. In the example below, we're creating a custom MyRadioGroup component that takes in an array of items and renders a radio group with those items along with a $2 component for each item. import { RadioGroup, Label, type WithoutChildrenOrChild, useId } from \"bits-ui\"; type Item = { value: string; label: string; disabled?: boolean; }; type Props = WithoutChildrenOrChild & { items: Item[]; }; let { value = $bindable(\"\"), ref = $bindable(null), items, ...restProps }: Props = $props(); {#each items as item} {@const id = useId()} {#snippet children({ checked })} {#if checked} ✅ {/if} {/snippet} {item.label} {/each} You can then use the MyRadioGroup component in your application like so: import MyRadioGroup from \"$lib/components/MyRadioGroup.svelte\"; const myItems = [ { value: \"apple\", label: \"Apple\" }, { value: \"banana\", label: \"Banana\" }, { value: \"coconut\", label: \"Coconut\", disabled: true }, ]; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { RadioGroup } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { RadioGroup } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } 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. Required To make the hidden input element required you can set the required prop on the RadioGroup.Root component. Disabling Items 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. ","description":"Allows users to select a single option from a list of mutually exclusive choices.","href":"/docs/components/radio-group"},{"title":"Range Calendar","content":" import { APISection, ComponentPreviewV2, RangeCalendarDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the $2 documentation to learn more! Structure import { RangeCalendar } from \"bits-ui\"; {#snippet children({ months, weekdays })} {#each months as month} {#each weekdays as day} {day} {/each} {#each month.weeks as weekDates} {#each weekDates as date} {/each} {/each} {/each} {/snippet} ","description":"Presents a calendar view tailored for selecting date ranges.","href":"/docs/components/range-calendar"},{"title":"Scroll Area","content":" import { APISection, ComponentPreviewV2, ScrollAreaDemo, ScrollAreaDemoCustom } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ScrollArea } from \"bits-ui\"; Reusable Components If you're planning to use the Scroll Area throughout your application, it's recommended to create a reusable component to reduce the amount of code you need to write each time. This example shows you how to create a Scroll Area component that accepts a few custom props that make it more capable. import { ScrollArea, type WithoutChild } from \"bits-ui\"; type Props = WithoutChild & { orientation: \"vertical\" | \"horizontal\" | \"both\"; viewportClasses?: string; }; let { ref = $bindable(null), orientation = \"vertical\", viewportClasses, children, ...restProps }: Props = $props(); {#snippet Scrollbar({ orientation }: { orientation: \"vertical\" | \"horizontal\" })} {/snippet} {@render children?.()} {#if orientation === \"vertical\" || orientation === \"both\"} {@render Scrollbar({ orientation: \"vertical\" })} {/if} {#if orientation === \"horizontal\" || orientation === \"both\"} {@render Scrollbar({ orientation: \"horizontal\" })} {/if} We'll use this custom component in the following examples to demonstrate how to customize the behavior of the Scroll Area. Scroll Area Types Hover The hover type is the default type of the scroll area, demonstrated in the featured example above. It only shows scrollbars when the user hovers over the scroll area and the content is larger than the viewport. Scroll The scroll type displays the scrollbars when the user scrolls the content. This is similar to the behavior of MacOS. Auto The auto type behaves similarly to your typical browser scrollbars. When the content is larger than the viewport, the scrollbars will appear and remain visible at all times. Always The always type behaves as if you set overflow: scroll on the scroll area. Scrollbars will always be visible, even when the content is smaller than the viewport. We've also set the orientation prop on the MyScrollArea to 'both' to ensure both scrollbars are rendered. Customizing the Hide Delay You can customize the hide delay of the scrollbars using the scrollHideDelay prop. ","description":"Provides a consistent scroll area across platforms.","href":"/docs/components/scroll-area"},{"title":"Select","content":" import { APISection, ComponentPreviewV2, SelectDemo, SelectDemoCustomAnchor, SelectDemoMultiple, SelectDemoTransition, SelectDemoAutoScrollDelay, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Overview The Select component provides users with a selectable list of options. It's designed to offer an enhanced selection experience with features like typeahead search, keyboard navigation, and customizable grouping. This component is particularly useful for scenarios where users need to choose from a predefined set of options, offering more functionality than a standard select element. Key Features Typeahead Search**: Users can quickly find options by typing Keyboard Navigation**: Full support for keyboard interactions, allowing users to navigate through options using arrow keys, enter to select, and more. Grouped Options**: Ability to organize options into logical groups, enhancing readability and organization of large option sets. Scroll Management**: Includes scroll up/down buttons for easy navigation in long lists. Accessibility**: Built-in ARIA attributes and keyboard support ensure compatibility with screen readers and adherence to accessibility standards. Portal Support**: Option to render the select content in a portal, preventing layout issues in complex UI structures. Architecture The Select component is composed of several sub-components, each with a specific role: Root**: The main container component that manages the state and context for the combobox. Trigger**: The button or element that opens the dropdown list. Portal**: Responsible for portalling the dropdown content to the body or a custom target. Group**: A container for grouped items, used to group related items. GroupHeading**: A heading for a group of items, providing a descriptive label for the group. Item**: An individual item within the list. Separator**: A visual separator between items. Content**: The dropdown container that displays the items. It uses $2 to position the content relative to the trigger. ContentStatic** (Optional): An alternative to the Content component, that enables you to opt-out of Floating UI and position the content yourself. Viewport**: The visible area of the dropdown content, used to determine the size and scroll behavior. ScrollUpButton**: A button that scrolls the content up when the content is larger than the viewport. ScrollDownButton**: A button that scrolls the content down when the content is larger than the viewport. Arrow**: An arrow element that points to the trigger when using the Combobox.Content component. Structure Here's an overview of how the Select component is structured in code: import { Select } from \"bits-ui\"; Reusable Components As you can see from the structure above, there are a number of pieces that make up the Select component. These pieces are provided to give you maximum flexibility and customization options, but can be a burden to write out everywhere you need to use a select in your application. To ease this burden, it's recommended to create your own reusable select component that wraps the primitives and provides a more convenient API for your use cases. Here's an example of how you might create a reusable MySelect component that receives a list of options and renders each of them as an item. import { Select, type WithoutChildren } from \"bits-ui\"; type Props = WithoutChildren & { placeholder?: string; items: { value: string; label: string; disabled?: boolean }[]; contentProps?: WithoutChildren; // any other specific component props if needed }; let { value = $bindable(), items, contentProps, placeholder, ...restProps }: Props = $props(); const selectedLabel = $derived(items.find((item) => item.value === value)?.label); {selectedLabel ? selectedLabel : placeholder} up {#each items as { value, label, disabled } (value)} {#snippet children({ selected })} {selected ? \"✅\" : \"\"} {label} {/snippet} {/each} down You can then use the MySelect component throughout your application like so: import MySelect from \"$lib/components/MySelect.svelte\"; const items = [ { value: \"apple\", label: \"Apple\" }, { value: \"banana\", label: \"Banana\" }, { value: \"cherry\", label: \"Cherry\" }, ]; let fruit = $state(\"apple\"); Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Select } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"A\")}> Select A Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Select } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Select } from \"bits-ui\"; let myOpen = $state(false); (myOpen = true)}> Open Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Select } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } Multiple Selection The type prop can be set to 'multiple' to allow multiple items to be selected at a time. import { Select } from \"bits-ui\"; let value = $state([]); {#snippet preview()} {/snippet} Opt-out of Floating UI When you use the Select.Content component, Bits UI uses $2 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 Select.ContentStatic component. When using this component, you'll need to handle the positioning of the content yourself. Keep in mind that using Select.Portal alongside Select.ContentStatic may result in some unexpected positioning behavior, feel free to not use the portal or work around it. Custom Anchor By default, the Select.Content is anchored to the Select.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 Select.Content component. import { Select } from \"bits-ui\"; let customAnchor = $state(null!); What is the Viewport? The Select.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 Select.Viewport component. Scroll Up/Down Buttons The Select.ScrollUpButton and Select.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 Select.Viewport component when using the scroll buttons. Custom Scroll Delay The initial and subsequent scroll delays can be controlled using the delay prop on the buttons. For example, we can use the $2 easing function from Svelte to create a smooth scrolling effect that speeds up over time. {#snippet preview()} {/snippet} Native Scrolling/Overflow If you don't want to use the $2 and prefer to use the standard scrollbar/overflow behavior, you can omit the Select.Scroll[Up|Down]Button components and the Select.Viewport component. You'll need to set a height on the Select.Content component and appropriate overflow styles to enable scrolling. Scroll Lock By default, when a user opens the select, scrolling outside the content will not be disabled. You can override this behavior by setting the preventScroll prop to true. Highlighted Items The Select component follows the $2 for highlighting items. This means that the Select.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 Select.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. console.log('I am highlighted!')} onUnhighlight={() => console.log('I am unhighlighted!')} /> Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Select.Content component to use Svelte Transitions or another animation library that requires more control. import { Select } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the $2 documentation. {#snippet preview()} {/snippet} ","description":"Enables users to choose from a list of options presented in a dropdown.","href":"/docs/components/select"},{"title":"Separator","content":" import { APISection, ComponentPreviewV2, SeparatorDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Separator } from \"bits-ui\"; ","description":"Visually separates content or UI elements for clarity and organization.","href":"/docs/components/separator"},{"title":"Slider","content":" import { APISection, ComponentPreviewV2, SliderDemo, SliderDemoMultiple, SliderDemoTicks, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Slider } from \"bits-ui\"; Reusable Components Bits UI provides primitives that enable you to build your own custom slider component that can be reused throughout your application. Here's an example of how you might create a reusable MySlider component. import type { ComponentProps } from \"svelte\"; import { Slider } from \"bits-ui\"; type Props = WithoutChildren>; let { value = $bindable(), ref = $bindable(null), ...restProps }: Props = $props(); {#snippet children({ thumbs, ticks })} {#each thumbs as index} {/each} {#each ticks as index} {/each} {/snippet} You can then use the MySlider component in your application like so: import MySlider from \"$lib/components/MySlider.svelte\"; let multiValue = $state([5, 10]); let singleValue = $state(50); Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Slider } from \"bits-ui\"; let myValue = $state(0); (myValue = 20)}> Set value to 20 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Slider } from \"bits-ui\"; let myValue = $state(0); function getValue() { return myValue; } function setValue(newValue: number) { myValue = newValue; } Value Commit You can use the onValueCommit prop to be notified when the user finishes dragging the thumb and the value changes. This is different than the onValueChange callback because it waits until the user stops dragging before calling the callback, where the onValueChange callback is called as the user dragging. { console.log(\"user is done sliding!\", v); }} /> 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 import { Slider } from \"bits-ui\"; // we have two numbers in the array, so the slider will render two thumbs let value = $state([5, 7]); {#snippet children({ ticks, thumbs })} {#each thumbs as index (index)} {/each} {#each ticks as index (index)} {/each} {/snippet} To determine the number of ticks that will be rendered, you can simply divide the max value by the step value. {#snippet preview()} {/snippet} Single Type Set the type prop to \"single\" to allow only one slider handle. {#snippet preview()} {/snippet} Multiple Type Set the type prop to \"multiple\" to allow multiple slider handles. {#snippet preview()} {/snippet} Vertical Orientation You can use the orientation prop to change the orientation of the slider, which defaults to \"horizontal\". RTL Support You can use the dir prop to change the reading direction of the slider, which defaults to \"ltr\". Auto Sort By default, the slider will sort the values from smallest to largest, so if you drag a smaller thumb to a larger value, the value of that thumb will be updated to the larger value. You can disable this behavior by setting the autoSort prop to false. HTML Forms Since there is a near endless number of possible values that a user can select, the slider does not render a hidden input element by default. You'll need to determine how you want to submit the value(s) of the slider with a form. Here's an example of how you might do that: import MySlider from \"$lib/components/MySlider.svelte\"; let expectedIncome = $state([50, 100]); let desiredIncome = $state(50); Submit ","description":"Allows users to select a value from a continuous range by sliding a handle.","href":"/docs/components/slider"},{"title":"Switch","content":" import { APISection, ComponentPreviewV2, SwitchDemo, SwitchDemoCustom, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} 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. Style-able**: Data attributes allow for smooth transitions between states and custom styles. HTML Forms**: Can render a hidden input element for form submissions. 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. Structure Here's an overview of how the Switch component is structured in code: import { Switch } from \"bits-ui\"; Reusable Components It's recommended to use the Switch primitives to create your own custom switch component that can be used throughout your application. In the example below, we're using the Checkbox and $2 components to create a custom switch component. import { Switch, Label, useId, type WithoutChildrenOrChild } from \"bits-ui\"; let { id = useId(), checked = $bindable(false), ref = $bindable(null), ...restProps }: WithoutChildrenOrChild & { labelText: string; } = $props(); {labelText} You can then use the MySwitch component in your application like so: import MySwitch from \"$lib/components/MySwitch.svelte\"; let notifications = $state(true); Managing Checked State This section covers how to manage the checked state of the component. Two-Way Binding Use bind:checked for simple, automatic state synchronization: import { Switch } from \"bits-ui\"; let myChecked = $state(true); (myChecked = false)}> uncheck Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Switch } from \"bits-ui\"; let myChecked = $state(false); function getChecked() { return myChecked; } function setChecked(newChecked: boolean) { myChecked = newChecked; } Disabled State You can disable the switch by setting the disabled prop to true. HTML Forms If you pass the name prop to Switch.Root, a hidden input element will be rendered to submit the value of the switch to a form. By default, the input will be submitted with the default checkbox value of 'on' if the switch is checked. Custom Input Value If you'd prefer to submit a different value, you can use the value prop to set the value of the hidden input. 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. This will apply the required attribute to the hidden input element, ensuring that proper form submission is enforced. ","description":"A toggle control enabling users to switch between \"on\" and \"off\" states.","href":"/docs/components/switch"},{"title":"Tabs","content":" import { APISection, ComponentPreviewV2, TabsDemo, Callout } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Tabs } from \"bits-ui\"; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Tabs } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"tab-1\")}> Activate tab 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Tabs } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } 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. Activation Mode By default, the Tabs component will automatically activate the tab associated with a trigger when that trigger is focused. This behavior can be disabled by setting the activationMode prop to 'manual'. When set to 'manual', the user will need to activate the tab by pressing the trigger. ","description":"Organizes content into distinct sections, allowing users to switch between them.","href":"/docs/components/tabs"},{"title":"Toggle Group","content":" import { APISection, ComponentPreviewV2, ToggleGroupDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { ToggleGroup } from \"bits-ui\"; bold italic 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. Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { ToggleGroup } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"item-1\")}> Press item 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { ToggleGroup } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } ","description":"Groups multiple toggle controls, allowing users to enable one or multiple options.","href":"/docs/components/toggle-group"},{"title":"Toggle","content":" import { APISection, ComponentPreviewV2, ToggleDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Toggle } from \"bits-ui\"; Managing Pressed State This section covers how to manage the pressed state of the component. Two-Way Binding Use bind:pressed for simple, automatic state synchronization: import { Toggle } from \"bits-ui\"; let myPressed = $state(true); (myPressed = false)}> un-press Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Toggle } from \"bits-ui\"; let myPressed = $state(false); function getPressed() { return myPressed; } function setPressed(newPressed: boolean) { myPressed = newPressed; } ","description":"A control element that switches between two states, providing a binary choice.","href":"/docs/components/toggle"},{"title":"Toolbar","content":" import { APISection, ComponentPreviewV2, ToolbarDemo } from '$lib/components/index.js' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Toolbar } from \"bits-ui\"; Managing Value State This section covers how to manage the value state of the component. Two-Way Binding Use bind:value for simple, automatic state synchronization: import { Toolbar } from \"bits-ui\"; let myValue = $state(\"\"); (myValue = \"item-1\")}> Press item 1 Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Toolbar } from \"bits-ui\"; let myValue = $state(\"\"); function getValue() { return myValue; } function setValue(newValue: string) { myValue = newValue; } ","description":"Displays frequently used actions or tools in a compact, easily accessible bar.","href":"/docs/components/toolbar"},{"title":"Tooltip","content":" import { ComponentPreviewV2, TooltipDemo, TooltipDemoCustom, TooltipDemoCustomAnchor, TooltipDemoDelayDuration, TooltipDemoTransition, APISection, Callout } from '$lib/components' let { schemas } = $props() {#snippet preview()} {/snippet} Structure import { Tooltip } from \"bits-ui\"; Provider Component The Tooltip.Provider component is required to be an ancestor of the Tooltip.Root component. It provides shared state for the tooltip components used within it. You can set a single delayDuration or disableHoverableContent prop on the provider component to apply to all the tooltip components within it. import { Tooltip } from \"bits-ui\"; 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. import { Tooltip } from \"bits-ui\"; let { children } = $props(); {@render children()} Managing Open State This section covers how to manage the open state of the component. Two-Way Binding Use bind:open for simple, automatic state synchronization: import { Tooltip } from \"bits-ui\"; let isOpen = $state(false); (isOpen = true)}>Open Tooltip Fully Controlled Use a $2 for complete control over the state's reads and writes. import { Tooltip } from \"bits-ui\"; let myOpen = $state(false); function getOpen() { return myOpen; } function setOpen(newOpen: boolean) { myOpen = newOpen; } 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). This is not possible on mobile devices, because there is no sense of hover on mobile. If a user were to press/touch a button with a tooltip, the action that button triggers would be taken before they were even able to see the tooltip, which renders it useless. If you are using a tooltip on a button without an action, you should consider using a $2 instead. If you'd like to learn more about how we came to this decision, here are some useful resources: The tooltip is not the appropriate role for the more information \"i\" icon, ⓘ. A tooltip is directly associated with the owning element. The ⓘ isn't 'described by' detailed information; the tool or control is. $2 Tooltips should only ever contain non-essential content. The best approach to writing tooltip content is to always assume it may never be read. $2 Reusable Components It's recommended to use the Tooltip primitives to build your own custom tooltip component that can be used throughout your application. Below is an example of how you might create a reusable tooltip component that can be used throughout your application. Of course, this isn't the only way to do it, but it should give you a good idea of how to compose the primitives. import { Tooltip } from \"bits-ui\"; import { type Snippet } from \"svelte\"; type Props = Tooltip.RootProps & { trigger: Snippet; triggerProps?: Tooltip.TriggerProps; }; let { open = $bindable(false), children, buttonText, triggerProps = {}, ...restProps }: Tooltip.RootProps = $props(); {@render trigger()} {@render children?.()} You could then use the MyTooltip component in your application like so: import MyTooltip from \"$lib/components/MyTooltip.svelte\"; import BoldIcon from \"..some-icon-library\"; // not real alert(\"changed to bold!\") }}> {#snippet trigger()} {/snippet} Change font to bold Delay Duration You can change how long a user needs to hover over a trigger before the tooltip appears by setting the delayDuration prop on the Tooltip.Root or Tooltip.Provider component. Close on Trigger Click By default, the tooltip will close when the user clicks the trigger. If you want to disable this behavior, you can set the disableCloseOnTriggerClick prop to true. Hoverable Content By default, the tooltip will remain open when the user hovers over the content. If you instead want the tooltip to close as the user moves their mouse towards the content, you can set the disableHoverableContent prop to true. Non-Keyboard Focus If you want to prevent opening the tooltip when the user focuses the trigger without using the keyboard, you can set the ignoreNonKeyboardFocus prop to true. Svelte Transitions You can use the forceMount prop along with the child snippet to forcefully mount the Tooltip.Content component to use Svelte Transitions or another animation library that requires more control. import { Tooltip } from \"bits-ui\"; import { fly, fade } from \"svelte/transition\"; {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} 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 $2 documentation. {#snippet preview()} {/snippet} Opt-out of Floating UI When you use the Tooltip.Content component, Bits UI uses $2 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. 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. Custom Anchor By default, the Tooltip.Content is anchored to the Tooltip.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 Tooltip.Content component. import { Tooltip } from \"bits-ui\"; let customAnchor = $state(null!); {#snippet preview()} {/snippet} ","description":"Provides additional information or context when users hover over or interact with an element.","href":"/docs/components/tooltip"},{"title":"IsUsingKeyboard","content":"Overview IsUsingKeyboard is a utility component that tracks whether the user is actively using the keyboard or not. This component is used internally by Bits UI components to provide keyboard accessibility features. It provides global state that is shared across all instances of the class to prevent duplicate event listener registration. Usage import { IsUsingKeyboard } from \"bits-ui\"; const isUsingKeyboard = new IsUsingKeyboard(); const shouldShowMenu = $derived(isUsingKeyboard.current); `","description":"A utility to track whether the user is actively using the keyboard or not.","href":"/docs/utilities/is-using-keyboard"},{"title":"mergeProps","content":"Overview mergeProps is a utility function designed to merge multiple props objects. It's particularly useful for composing components with different prop sets or extending the functionality of existing components. It is used internally by Bits UI components to merge the custom restProps you pass to a component with the props that Bits UI provides to the component. Key Features Merges multiple props objects Chains event handlers with cancellation support Combines class names Merges style objects and strings Chains non-event handler functions Detailed Behavior Event Handlers Event handlers are chained in the order they're passed. If a handler calls event.preventDefault(), subsequent handlers in the chain are not executed. const props1 = { onclick: (e: MouseEvent) => console.log(\"First click\") }; const props2 = { onclick: (e: MouseEvent) => console.log(\"Second click\") }; const mergedProps = mergeProps(props1, props2); mergedProps.onclick(new MouseEvent(\"click\")); // Logs: \"First click\" then \"Second click\" If preventDefault() is called: const props1 = { onclick: (e: MouseEvent) => console.log(\"First click\") }; const props2 = { onclick: (e: MouseEvent) => { console.log(\"Second click\"); e.preventDefault(); }, }; const props3 = { onclick: (e: MouseEvent) => console.log(\"Third click\") }; const mergedProps = mergeProps(props1, props2, props3); mergedProps.onclick(new MouseEvent(\"click\")); // Logs: \"First click\" then \"Second click\" only Since props2 called event.preventDefault(), props3's onclick handler will not be called. Non-Event Handler Functions Non-event handler functions are also chained, but without the ability to prevent subsequent functions from executing: const props1 = { doSomething: () => console.log(\"Action 1\") }; const props2 = { doSomething: () => console.log(\"Action 2\") }; const mergedProps = mergeProps(props1, props2); mergedProps.doSomething(); // Logs: \"Action 1\" then \"Action 2\" Classes Class names are merged using $2: const props1 = { class: \"text-lg font-bold\" }; const props2 = { class: [\"bg-blue-500\", \"hover:bg-blue-600\"] }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.class); // \"text-lg font-bold bg-blue-500 hover:bg-blue-600\" Styles Style objects and strings are merged, with later properties overriding earlier ones: const props1 = { style: { color: \"red\", fontSize: \"16px\" } }; const props2 = { style: \"background-color: blue; font-weight: bold;\" }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.style); // \"color: red; font-size: 16px; background-color: blue; font-weight: bold;\" import { mergeProps } from \"bits-ui\"; const props1 = { style: \"--foo: red\" }; const props2 = { style: { \"--foo\": \"green\", color: \"blue\" } }; const mergedProps = mergeProps(props1, props2); console.log(mergedProps.style); // \"--foo: green; color: blue;\" `","description":"A utility function to merge props objects.","href":"/docs/utilities/merge-props"},{"title":"Portal","content":"Overview The Portal component is a utility component that renders its children in a portal, preventing layout issues in complex UI structures. This component is used for the various Bits UI component that have a Portal sub-component. Usage Default behavior By default, the Portal component will render its children in the body element. import { Portal } from \"bits-ui\"; This content will be portalled to the body Custom target You can use the to prop to specify a custom target element or selector to render the content to. import { Portal } from \"bits-ui\"; This content will be portalled to the #custom-target element Disable You can use the disabled prop to disable the portal behavior. import { Portal } from \"bits-ui\"; This content will not be portalled `","description":"A component that renders its children in a portal, preventing layout issues in complex UI structures.","href":"/docs/utilities/portal"},{"title":"useId","content":"The useId function is a utility function that can be used to generate unique IDs. This function is used internally by all Bits UI components and is exposed for your convenience. Usage import { useId } from \"bits-ui\"; const id = useId(); Label here `","description":"A utility function to generate unique IDs.","href":"/docs/utilities/use-id"},{"title":"WithElementRef","content":"The WithElementRef type helper is a convenience type that enables you to follow the same $2 prop pattern as used by Bits UI components when crafting your own. type WithElementRef = T & { ref?: U | null }; This type helper is used internally by Bits UI components to enable the ref prop on a component. Usage Example import type { WithElementRef } from \"bits-ui\"; type Props = WithElementRef; let { yourPropA, yourPropB, ref = $bindable(null) }: Props = $props(); `","description":"A type helper to enable the `ref` prop on a component.","href":"/docs/type-helpers/with-element-ref"},{"title":"WithoutChild","content":"The WithoutChild type helper is used to exclude the child snippet prop from a component. This is useful when you're building custom component wrappers that populate the children prop of a component and don't provide a way to pass a custom child snippet. To learn more about the child snippet prop, check out the $2 documentation. import { Accordion, type WithoutChild } from \"bits-ui\"; let { children, ...restProps }: WithoutChild = $props(); {@render children?.()} `","description":"A type helper to exclude the child snippet prop from a component.","href":"/docs/type-helpers/without-child"},{"title":"WithoutChildrenOrChild","content":"The WithoutChildrenOrChild type helper is used to exclude the child and children props from a component. This is useful when you're building custom component wrappers that populate the children prop of a component and don't provide a way to pass a custom children or child snippet. To learn more about the child snippet prop, check out the $2 documentation. import { Accordion, type WithoutChildrenOrChild } from \"bits-ui\"; let { title, ...restProps }: WithoutChildrenOrChild = $props(); {title} Now, the CustomAccordionTrigger component won't expose children or child props to the user, but will expose the other root component props.","description":"A type helper to exclude the child ad children snippet props from a component.","href":"/docs/type-helpers/without-children-or-child"},{"title":"WithoutChildren","content":"The WithoutChildren type helper is used to exclude the children snippet prop from a component. This is useful when you're building custom component wrappers that populate the children prop of a component. import { Accordion, type WithoutChildren } from \"bits-ui\"; let { value, onValueChange, ...restProps }: WithoutChildren = $props(); In the example above, we're using the WithoutChildren type helper to exclude the children snippet prop from the Accordion.Root component. This ensures our exposed props are consistent with what is being used internally.","description":"A type helper to exclude the children snippet prop from a component.","href":"/docs/type-helpers/without-children"},{"title":"Child Snippet","content":"The child snippet is a powerful feature that gives you complete control over the rendered elements in Bits UI components, allowing for customization while maintaining accessibility and functionality. When to Use It You should use the child snippet when you need: Svelte-specific features like transitions, animations, actions, or scoped styles Integration with custom components in your application Precise control over the DOM structure Advanced composition of components Basic Usage Many Bits UI components have default HTML elements that wrap their content. For example, Accordion.Trigger renders a `` element by default: {@render children()} When you need to customize this element, the child snippet lets you take control: import MyCustomButton from \"$lib/components\"; import { Accordion } from \"bits-ui\"; {#snippet child({ props })} Toggle Item {/snippet} {#snippet child({ props })} Toggle Item {/snippet} .scoped-button { background-color: #3182ce; color: #fff; } In this example: The props parameter contains all necessary attributes and event handlers The {...props} spread applies these to your custom element/component You can add scoped styles, transitions, actions, etc. directly to the element How It Works When you use the child snippet: The component passes all internal props and your custom props passed to the component via the props snippet parameter You decide which element receives these props The component's internal logic continues to work correctly Behind the Scenes Components that support the child snippet typically implement logic similar to: // Bits UI component internal logic let { child, children, ...restProps } = $props(); const trigger = makeTrigger(); // Merge internal props with user props const mergedProps = $derived(mergeProps(restProps, trigger.props)); {#if child} {@render child({ props: mergedProps })} {:else} {@render children?.()} {/if} Working with Props Custom IDs & Attributes To use custom IDs, event handlers, or other attributes, pass them to the component first: console.log(\"clicked\")} {#snippet child({ props })} Open accordion item {/snippet} The props object will now include: Your custom ID (id=\"my-custom-id\") Your data attribute (data-testid=\"accordion-trigger\") Your click event handler, properly merged with internal handlers All required ARIA attributes and internal event handlers Combining with Svelte Features You can apply Svelte-specific features to your custom elements, such as transitions, actions, and scoped styles: {#snippet child({ props })} {/snippet} .my-custom-trigger { background-color: #3182ce; color: #fff; } Floating Components Floating content components (tooltips, popovers, dropdowns, etc.) require special handling due to their positioning requirements. Required Structure For floating components, you must use a two-level structure: An outer wrapper element with {...wrapperProps} An inner content element with {...props} {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} Important Rules for Floating Content The wrapper element with {...wrapperProps} must remain unstyled Positioning is handled by the wrapper element; styling goes on the inner content element The open parameter lets you conditionally render the content, triggering Svelte transitions Always maintain this two-level structure to ensure proper positioning and behavior Components Requiring Wrapper Elements The following components require a wrapper element: Combobox.Content DatePicker.Content DateRangePicker.Content DropdownMenu.Content LinkPreview.Content Menubar.Content Popover.Content Select.Content Tooltip.Content Examples Basic Custom Element {#snippet child({ props })} Favorite {/snippet} With Svelte Transitions {#snippet child({ props, open })} {#if open} Dialog content with a scale transition {/if} {/snippet} Floating Element Example {#snippet child({ wrapperProps, props, open })} {#if open} Custom tooltip content {/if} {/snippet} Common Pitfalls Missing props spread**: Always include {...props} on your custom element Styling the wrapper**: Never style the wrapper element in floating components Direct children**: When using child, other children outside the snippet are ignored Missing structure**: For floating elements, forgetting the two-level structure will break positioning Related Resources $2 Utility $2 $2","description":"Learn how to use the `child` snippet to render your own elements.","href":"/docs/child-snippet"},{"title":"Dates and Times","content":"The date and time components in Bits UI leverage the $2 package, providing a unified API for working with dates and times across different locales and time zones. This package is inspired by the $2 proposal and is designed to seamlessly integrate with the Temporal API once it becomes available. Installation You can install the package using your favorite package manager: npm install @internationalized/date It's highly recommended to familiarize yourself with the package's documentation before diving into the components. We'll cover the basics of how we use the package in Bits UI in the sections below, but their documentation provides much more detail on the various formats and how to work with them. DateValue Types Bits UI uses DateValue objects from @internationalized/date to represent dates and times consistently. These immutable objects provide specific information about the type of date they represent: We use the DateValue objects provided by @internationalized/date to represent dates and times in a consistent way. These objects are immutable and provide information about the type of date they represent. The DateValue is a union of the following three types: | Type | Description | Example | | ------------------ | --------------------------- | ------------------------------------------------ | | CalendarDate | Date without time component | 2024-07-10 | | CalendarDateTime | Date with time | 2024-07-10T12:30:00 | | ZonedDateTime | Date with time and timezone | 2024-07-10T21:00:00:00-04:00[America/New_York] | Using these strongly-typed objects allows components to adapt appropriately to the date type you provide. CalendarDate Represents a date without a time component. // Creating a CalendarDate import { CalendarDate, parseDate, today, getLocalTimeZone } from \"@internationalized/date\"; // From year, month, day parameters const date = new CalendarDate(2024, 7, 10); // From ISO 8601 string const parsedDate = parseDate(\"2024-07-10\"); // Current date in specific timezone const losAngelesToday = today(\"America/Los_Angeles\"); // Current date in user's timezone const localToday = today(getLocalTimeZone()); See the $2 for additional methods. CalendarDateTime Represents a date with a time component, but without timezone information. // Creating a CalendarDateTime import { CalendarDateTime, parseDateTime } from \"@internationalized/date\"; // From date and time components const dateTime = new CalendarDateTime(2024, 7, 10, 12, 30, 0); // From ISO 8601 string const parsedDateTime = parseDateTime(\"2024-07-10T12:30:00\"); See the $2 for additional methods. ZonedDateTime Represents a specific date and time in a specific timezone - crucial for events that occur at an exact moment regardless of the user's location (like conferences or live broadcasts). // Creating a ZonedDateTime import { ZonedDateTime, parseZonedDateTime, parseAbsolute, parseAbsoluteToLocal, } from \"@internationalized/date\"; const date = new ZonedDateTime( 2022, 2, 3, // Date (year, month, day) \"America/Los_Angeles\", // Timezone -28800000, // UTC offset in milliseconds 9, 15, 0 // Time (hour, minute, second) ); // From ISO 8601 strings using different parsing functions const date1 = parseZonedDateTime(\"2024-07-12T00:45[America/New_York]\"); const date2 = parseAbsolute(\"2024-07-12T07:45:00Z\", \"America/New_York\"); const date3 = parseAbsoluteToLocal(\"2024-07-12T07:45:00Z\"); See the $2 for more information. Working with Date Ranges For components that require date ranges, Bits UI provides a DateRange type: type DateRange = { start: DateValue; end: DateValue; }; This type is used in components such as: $2 $2 $2 Using the Placeholder Each date/time component in Bits UI has a bindable placeholder prop that serves multiple important functions: Starting Point: Acts as the initial date when no value is selected Type Definition: Determines what type of date/time to display if value is absent Calendar Navigation: Controls the visible date range in calendar views Example: Using Placeholder with Calendar import { Calendar } from \"bits-ui\"; import { today, getLocalTimeZone, type DateValue } from \"@internationalized/date\"; // Initialize placeholder with today's date let placeholder: DateValue = $state(today(getLocalTimeZone())); let selectedMonth: number = $state(placeholder.month); { placeholder = placeholder.set({ month: selectedMonth }); }} bind:value={selectedMonth} January February Updating DateValue Objects Since DateValue objects are immutable, you must create new instances when updating them: // INCORRECT - will not work let placeholder = new CalendarDate(2024, 7, 10); placeholder.month = 8; // Error! DateValue objects are immutable // CORRECT - using methods that return new instances let placeholder = new CalendarDate(2024, 7, 10); // Method 1: Using set() placeholder = placeholder.set({ month: 8 }); // Method 2: Using add() placeholder = placeholder.add({ months: 1 }); // Method 3: Using subtract() placeholder = placeholder.subtract({ days: 5 }); // Method 4: Using cycle() - cycles through valid values placeholder = placeholder.cycle(\"month\", \"forward\", [1, 3, 5, 7, 9, 11]); Formatting and Parsing Formatting Dates for Display For consistent, locale-aware date formatting, use the DateFormatter class: import { DateFormatter } from \"@internationalized/date\"; // Create a formatter for the current locale const formatter = new DateFormatter(\"en-US\", { dateStyle: \"full\", timeStyle: \"short\", }); // Format a DateValue const formattedDate = formatter.format(myDateValue.toDate(\"America/New_York\")); // Example output: \"Wednesday, July 10, 2024 at 12:30 PM\" The DateFormatter wraps the native $2 while fixing browser inconsistencies and polyfilling newer features. Parsing Date Strings When working with date strings from APIs or databases, use the appropriate parsing function for your needs: import { parseDate, // For CalendarDate parseDateTime, // For CalendarDateTime parseZonedDateTime, // For ZonedDateTime with timezone name parseAbsolute, // For ZonedDateTime from UTC string + timezone parseAbsoluteToLocal, // For ZonedDateTime in local timezone } from \"@internationalized/date\"; // Examples const date = parseDate(\"2024-07-10\"); // CalendarDate const dateTime = parseDateTime(\"2024-07-10T12:30:00\"); // CalendarDateTime const zonedDate = parseZonedDateTime(\"2024-07-12T00:45[America/New_York]\"); // ZonedDateTime const absoluteDate = parseAbsolute(\"2024-07-12T07:45:00Z\", \"America/New_York\"); // ZonedDateTime const localDate = parseAbsoluteToLocal(\"2024-07-12T07:45:00Z\"); // ZonedDateTime in user's timezone Common Gotchas and Tips Month Indexing**: Unlike JavaScript's Date object (which is 0-indexed), @internationalized/date uses 1-indexed months (January = 1). Immutability**: Always reassign when modifying date objects: date = date.add({ days: 1 }). Timezone Handling**: Use ZonedDateTime for schedule-critical events like meetings or appointments. Type Consistency**: Match placeholder types to your needs - if you need time selection, use CalendarDateTime not CalendarDate. Performance**: Create DateFormatter instances once and reuse them rather than creating new instances on each render. Related Resources $2 $2 $2 $2 $2 $2 $2","description":"How to work with the various date and time components in Bits UI.","href":"/docs/dates"},{"title":"Figma","content":"The Figma UI Kit is open sourced by $2. import { AspectRatio } from \"bits-ui\"; Grab a copy https://www.figma.com/community/file/1430229712135910564/bits-ui-kit","description":"Every component recreated in Figma.","href":"/docs/figma-file"},{"title":"Getting Started","content":"Welcome to Bits UI, a collection of headless component primitives for Svelte 5 that prioritizes developer experience, accessibility, and flexibility. This guide will help you quickly integrate Bits UI into your Svelte application. Installation Install bits using your preferred package manager. npm install bits-ui Basic Usage After installation, you can import and use Bits UI components in your Svelte files. Here's a simple example using the $2 component. import { Accordion } from \"bits-ui\"; First First accordion content Second Second accordion content Adding Styles Bits UI components are headless by design, meaning they ship with minimal styling. This gives you complete control over the appearance of your components. Each component that renders an HTML element exposes a class prop and style prop that you can use to apply styles to the element. Styling with TailwindCSS or UnoCSS If you're using a CSS framework like TailwindCSS or UnoCSS, you can pass the classes directly to the components: import { Accordion } from \"bits-ui\"; Tailwind-styled Accordion This accordion is styled using Tailwind CSS classes. Styling with Data Attributes Each Bits UI component applies specific data attributes to the underlying HTML elements. You can use these attributes to target components in your global styles: Check the API Reference for each component to determine its data attributes Use those attributes in your CSS selectors import { Button } from \"bits-ui\"; import \"../app.css\"; Click me [data-button-root] { height: 3rem; width: 100%; background-color: #3182ce; color: white; border-radius: 0.375rem; padding: 0.5rem 1rem; font-weight: 500; } [data-button-root]:hover { background-color: #2c5282; } With this approach, every Button.Root component will have these styles applied to it automatically. TypeScript Support Bits UI is built with TypeScript and provides comprehensive type definitions. When using TypeScript, you'll get full type checking and autocompletion: import { Accordion } from \"bits-ui\"; // TypeScript will validate these props const accordionMultipleProps: Accordion.RootProps = { type: \"multiple\", value: [\"item-1\"], // type error if value is not an array }; const accordionSingleProps: Accordion.RootProps = { type: \"single\", value: \"item-1\", // type error if value is an array }; Next Steps Now that you have Bits UI installed and working, you can: Explore the $2 to learn about all available components Learn about render delegation using the $2 for maximum flexibility and customization Learn how Bits UI handles $2 and how you can take more control over your components Resources If you have questions or need help, there are several ways to get support from the Bits UI community: For confirmed bugs, please $2 on GitHub. Have a question or need help? Join our $2 or $2 on GitHub to chat with other developers and the Bits UI team. Have a feature request or idea? $2 on GitHub to share your thoughts. All feature requests start as discussions before formally being moved to issues.","description":"Learn how to get started using Bits in your app.","href":"/docs/getting-started"},{"title":"Introduction","content":"Bits UI is a collection of headless component primitives for Svelte that prioritizes developer experience, accessibility, and flexibility. Our vision is to empower developers to build high-quality, accessible user interfaces without sacrificing creative control or performance. Why Bits UI? Bring Your Own Styles Most components ship with zero styling. Minimal styles are included only when absolutely necessary for core functionality. You maintain complete control over the visual design, applying your own styles through standard Svelte class props or targeting components via data attributes. See our $2 for implementation details. Empowering DX Every component is designed with developer experience in mind: Extensive TypeScript support Predictable behavior and consistent APIs Comprehensive documentation and examples Flexible event override system for custom behavior Sensible defaults Built for Production Strives to follow $2 Built-in keyboard navigation Screen reader optimization Focus management Composable Architecture Components are designed to work independently or together, featuring: $2 for maximum flexibility Chainable events and callbacks Override-friendly defaults Minimal dependencies Community Bits UI is an open-source project built and maintained by $2 with design support from $2 and his team at $2. We always welcome contributions and feedback from the community. Found an accessibility issue or have a suggestion? $2. Acknowledgments Built on the shoulders of giants: $2 - Inspired our internal architecture and powered the first version of Bits UI $2 - Reference for component API design $2 - Inspiration for date/time components","description":"The headless components for Svelte.","href":"/docs/introduction"},{"title":"LLMs","content":"At the top of each documentation page, you'll find a convenient \"Copy Markdown\" button alongside a direct link to the LLM-friendly version of that page (e.g., /llms.txt). These tools make it easy to copy the content in Markdown format or access the machine-readable llms.txt file tailored for that specific page. Bits UI documentation is designed to be accessible not only to humans but also to large language models (LLMs). We've adopted the $2 proposal standard, which provides a structured, machine-readable format optimized for LLMs. This enables developers, researchers, and AI systems to efficiently parse and utilize our documentation. What is llms.txt? The llms.txt standard is an emerging convention for presenting documentation in a simplified, text-based format that's easy for LLMs to process. By following this standard, Bits UI ensures compatibility with AI tools and workflows, allowing seamless integration into LLM-powered applications, research, or automation systems. Accessing LLM-friendly Documentation To access the LLM-friendly version of any supported Bits UI documentation page, simply append /llms.txt to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. Example Standard Page**: The Accordion component documentation is available at $2. LLM-friendly Version**: Append /llms.txt to access it at $2. Root Index To explore all supported pages in LLM-friendly format, visit the root index at $2. This page provides a comprehensive list of available documentation endpoints compatible with the llms.txt standard. Full LLM-friendly Documentation For a complete, consolidated view of the Bits UI documentation in an LLM-friendly format, navigate to $2. This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. Notes Not all pages may support the /llms.txt suffix (those deemed irrelevant to LLMs, such as the Figma page). Check the root $2 page for an up-to-date list of compatible URLs. The \"Copy Markdown\" button at the top of each page provides the same content you'd find in the /llms.txt of that page. By embracing the llms.txt standard, Bits UI empowers both human developers and AI systems to make the most of our documentation. Whether you're building with Bits UI or training an LLM, these tools are designed to enhance your experience.","description":"How to access LLM-friendly versions of Bits UI documentation.","href":"/docs/llms"},{"title":"Migration Guide","content":" import { Callout } from '$lib/components'; Bits UI v1 is a major update that introduces significant improvements, but it also comes with breaking changes. Since anything before v1.0 was a pre-release, backward compatibility was not guaranteed. This guide will help you transition smoothly, though it may not cover every edge case. We highly recommend reviewing the documentation for each component you use, as their APIs may have changed. Looking for the old documentation? You can still access Bits UI v0.x at $2. However, we encourage you to migrate as soon as possible to take advantage of the latest features and improvements. Why Upgrade? Bits UI has been completely rewritten for Svelte 5, bringing several key benefits: Performance improvements** – Faster rendering and reduced overhead. More flexible APIs** – Easier customization and integration. Bug fixes and stability** – Addressing every bug and issue from v0.x. Better developer experience** – Improved consistency and documentation. Once you get familiar with Bits UI v1, we're confident you'll find it to be a more powerful and streamlined headless component library. Shared Changes el prop replaced with ref**: The el prop has been removed across all components that render and HTML element. Use the ref prop instead. See the $2 documentation for more information. asChild prop replaced with child snippet**: Components that previously used asChild now use the child snippet prop. See the $2 documentation. Transition props removed**: Components no longer accept transition props. Instead, use the child snippet along with forceMount to leverage Svelte transitions. More details in the $2 documentation. let: directives replaced with snippet props**: Components that used to expose data via let: directives now provide it through children/child snippet props. Accordion The multiple prop has been removed from the Accordion.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The various transition props have been removed from the Accordion.Content component. See the $2 documentation for more information. See the $2 documentation for more information. Alert Dialog The various transition props have been removed from the AlertDialog.Content and AlertDialog.Overlay components. See the $2 documentation for more information. To render the dialog content in a portal, you now must wrap the AlertDialog.Content in the AlertDialog.Portal component. The AlertDialog.Action component no longer closes the dialog by default, as we learned it was causing more harm than good when attempting to submit a form with the Action button. See the $2 section for more information on how to handle submitting forms before closing the dialog. Button The Button component no longer accepts a builders prop, instead you should use the child snippet on the various components to receive/pass the attributes to the underlying button. See $2 for more information. Checkbox The Checkbox.Indicator component has been removed in favor of using the children snippet prop to get a reference to the checked state and render a custom indicator. See the $2 documentation for more information. The Checkbox.Input component has been removed in favor of automatically rendering a hidden input when the name prop is provided to the Checkbox.Root component. The checked state of the Checkbox component is now of type boolean instead of boolean | 'indeterminate', indeterminate is its own state now and can be managed via the indeterminate prop. A new component, Checkbox.Group has been introduced to support checkbox groups. See the $2 documentation for more information. Combobox The multiple prop has been removed from the Combobox.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The selected prop has been replaced with a value prop, which is limited to a string (or string[] if type=\"multiple\"). The combobox now automatically renders a hidden input when the name prop is provided to the Combobox.Root component. The Combobox.ItemIndicator component has been removed in favor of using the children snippet prop to get a reference to the selected state and render a custom indicator. See the $2 documentation for more information. Combobox.Group and Combobox.GroupHeading have been added to support groups within the combobox. In Bits UI v0, the Combobox.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Combobox.Portal component that you can wrap around the Combobox.Content to portal the content. Combobox.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Context Menu/Dropdown Menu/Menubar Menu The Menu.RadioIndicator and Menu.CheckboxIndicator components have been removed in favor of using the children snippet prop to get a reference to the checked or selected state and render a custom indicator. See the $2, $2, and $2 documentation for more information. The Menu.Label component, which was used as the heading for a group of items has been replaced with the Menu.GroupHeading component. The href prop on the .Item components has been removed in favor of the child snippet and rendering your own anchor element. In Bits UI v0, the Menu.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Menu.Portal component that you can wrap around the Menu.Content to portal the content. Menu.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Pin Input The PinInput component has been completely overhauled to better act as an OTP input component, with code and inspiration taken from $2 by $2. The best way to migrate is to reference the $2 documentation to see how to use the new component. Popover In Bits UI v0, the Popover.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Popover.Portal component that you can wrap around the Popover.Content to portal the content. Popover.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Radio Group RadioGroup.ItemIndicator has been removed in favor of using the children snippet prop to get a reference to the checked state which provides more flexibility to render a custom indicator as needed. See the $2 documentation for more information. Scroll Area ScrollArea.Content has been removed as it is not necessary for functionality in Bits UI v1. Select The multiple prop has been removed from the Select.Root component and replaced with a required type prop which can be set to either 'single' or 'multiple'. This is used as a discriminant to properly type the value prop as either a string or string[]. The selected prop has been replaced with a value prop, which is limited to a string (or string[] if type=\"multiple\"). The select now automatically renders a hidden input when the name prop is provided to the Select.Root component. The Select.ItemIndicator component has been removed in favor of using the children snippet prop to get a reference to the selected state and render a custom indicator. See the $2 documentation for more information. Select.Group and Select.GroupHeading have been added to support groups within the Select. Select.Value has been removed in favor of enabling developers to use the value prop to render your own custom label in the trigger to represent the value. In Bits UI v0, the Select.Content was automatically portalled unless you explicitly set the portal prop to false. In v1, we provide a Select.Portal component that you can wrap around the Select.Content to portal the content. Select.Portal accepts a to prop that can be used to specify the target portal container (defaults to document.body), and a disabled prop that can be used to disable portalling. Slider Slider.Root now requires a type prop to be set to either 'single' or 'multiple' to properly type the value as either a number or number[]. A new prop, onValueCommit has been introduced which is called when the user commits a value change (e.g. by releasing the mouse button or pressing Enter). This is useful for scenarios where you want to update the value only when the user has finished interacting with the slider, not for each movement of the thumb. Tooltip A required component necessary to provide context for shared tooltips, Tooltip.Provider has been added. This replaces the group prop on the previous version's Tooltip component. You can wrap your entire app with Tooltip.Provider, or wrap a specific section of your app with it to provide shared context for tooltips.","description":"Learn how to migrate from 0.x to 1.x","href":"/docs/migration-guide"},{"title":"Ref","content":"The ref prop provides direct access to the underlying HTML elements in Bits UI components, enabling you to manipulate the DOM when necessary. Basic Usage Every Bits UI component that renders an HTML element exposes a ref prop that you can bind to access the rendered element. This is particularly useful for programmatically manipulating the element, such as focusing inputs or measuring dimensions. import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); } Focus trigger Trigger content With Child Snippet Bits UI uses element IDs to track references to underlying elements. This approach ensures that the ref prop works correctly even when using the $2. Simple Delegation Example The ref binding will automatically work with delegated child elements/components. import CustomButton from \"./CustomButton.svelte\"; import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); function focusTrigger() { triggerRef?.focus(); } {#snippet child({ props })} {/snippet} Using Custom IDs When you need to use a custom id on the element, pass it to the parent component first so it can be correctly registered with the ref binding: import CustomButton from \"./CustomButton.svelte\"; import { Accordion } from \"bits-ui\"; let triggerRef = $state(null); const myCustomId = \"my-custom-id\"; {#snippet child({ props })} {/snippet} Common Pitfalls Avoid setting the id directly on the child component/element, as this breaks the connection between the ref binding and the element: {#snippet child({ props })} {/snippet} In this example, the Accordion.Trigger component can't track the element because it doesn't know the custom ID. Why Possibly null? The ref value may be null until the component mounts in the DOM. This behavior is consistent with native DOM methods like getElementById which can return null. Creating Your Own ref Props To implement the same ref pattern in your custom components, Bits UI provides a $2 type helper. This enables you to create type-safe components that follow the same pattern. import { WithElementRef } from \"bits-ui\"; import type { HTMLButtonAttributes } from \"svelte/elements\"; // Define props with the ref type let { ref = $bindable(null), children, ...rest }: WithElementRef = $props(); {@render children?.()} `","description":"Learn about the $bindable ref prop.","href":"/docs/ref"},{"title":"State Management","content":"State management is a critical aspect of modern UI development. Bits UI components support multiple approaches to manage component state, giving you flexibility based on your specific needs. Each component's API reference will highlight what props are bindable. You can replace the value prop used in the examples on this page with any bindable prop. Two-Way Binding The simplest approach is using Svelte's built-in two-way binding with bind:: import { ComponentName } from \"bits-ui\"; let myValue = $state(\"default-value\"); (myValue = \"new-value\")}> Update Value Why Use It? Zero-boilerplate state updates External controls work automatically Great for simple use cases Function Binding For complete control, use a $2 that handles both getting and setting values: import { ComponentName } from \"bits-ui\"; let myValue = $state(\"default-value\"); function getValue() { return myValue; } function setValue(newValue: string) { // Only update during business hours const now = new Date(); const hour = now.getHours(); if (hour >= 9 && hour When the component wants to set the value from an internal action, it will invoke the setter, where you can determine if the setter actually updates the state or not. When to Use Complex state transformation logic Conditional updates Debouncing or throttling state changes Maintaining additional state alongside the primary value Integrating with external state systems","description":"How to manage the state of Bits UI components","href":"/docs/state-management"},{"title":"Styling","content":"We ship almost zero styles with Bits UI by design, giving you complete flexibility when styling your components. For each component that renders an HTML element, we expose the class and style props to apply styles directly to the component. Styling Approaches CSS Frameworks If you're using a CSS framework like $2 or $2, simply pass the classes to the component: import { Accordion } from \"bits-ui\"; Click me Data Attributes Each Bits UI component applies specific data attributes to its rendered elements. These attributes provide reliable selectors for styling across your application. /* Target all Accordion.Trigger components */ [data-accordion-trigger] { height: 3rem; width: 100%; background-color: #3182ce; color: #fff; } Import your stylesheet in your layout component: import \"../app.css\"; let { children } = $props(); {@render children()} Now every Accordion.Trigger component will have the styles applied to it. Global Classes Alternatively, you can use global class names: .accordion-trigger { height: 3rem; width: 100%; background-color: #3182ce; color: #fff; } Import your stylesheet in your layout component: import \"../app.css\"; let { children } = $props(); {@render children()} Use the global class with the component: import { Accordion } from \"bits-ui\"; Click me Scoped Styles To use Svelte's scoped styles, use the child snippet to bring the element into your component's scope. See the $2 documentation for more information. import { Accordion } from \"bits-ui\"; {#snippet child({ props })} Click me! {/snippet} .my-accordion-trigger { height: 3rem; width: 100%; background-color: #3182ce; color: #fff; } Style Prop All Bits UI components that render an element accept a style prop as either a string or an object of CSS properties. These are merged with internal styles using the $2 function. Click me Click me Styling Component States Bits UI components may expose state information through data attributes and CSS variables, allowing you to create dynamic styles based on component state. State Data Attributes Components apply state-specific data attributes that you can target in your CSS: /* Style the Accordion.Trigger when open */ $2 { background-color: #f0f0f0; font-weight: bold; } /* Style the Accordion.Trigger when closed */ $2 { background-color: #ffffff; } /* Style disabled components */ $2 { opacity: 0.5; cursor: not-allowed; } See each component's API reference for its specific data attributes. CSS Variables Bits UI components may expose CSS variables that allow you to access internal component values. For example, to ensure the Select.Content is the same width as the anchor (by default is the Select.Trigger unless using a customAnchor), you can use the --bits-select-anchor-width CSS variable: [data-select-content] { width: var(--bits-select-anchor-width); min-width: var(--bits-select-anchor-width); max-width: var(--bits-select-anchor-width); } See each component's API reference for specific CSS variables it provides. Example: Styling an Accordion Here's an example styling an accordion with different states: import { Accordion } from \"bits-ui\"; Section 1 Content for section 1 Section 2 (Disabled) Content for section 2 /* Base styles */ :global([data-accordion-item]) { border: 1px solid #e2e8f0; border-radius: 0.25rem; margin-bottom: 0.5rem; } /* Trigger styles based on state */ :global([data-accordion-trigger]) { width: 100%; padding: 1rem; display: flex; justify-content: space-between; align-items: center; } :global($2) { background-color: #f7fafc; border-bottom: 1px solid #e2e8f0; } :global($2) { opacity: 0.5; cursor: not-allowed; } /* Content styles */ :global([data-accordion-content]) { padding: 1rem; } Advanced Styling Techniques Combining Data Attributes with CSS Variables You can combine data attributes with CSS variables to create dynamic styles based on component state. Here's how to animate the accordion content using the --bits-accordion-content-height variable and the data-state attribute: /* Basic transition animation */ [data-accordion-content] { overflow: hidden; transition: height 300ms ease-out; height: 0; } $2 { height: var(--bits-accordion-content-height); } $2 { height: 0; } Custom Keyframe Animations For more control, use keyframe animations with the CSS variables: /* Define keyframes for opening animation */ @keyframes accordionOpen { 0% { height: 0; opacity: 0; } 80% { height: var(--bits-accordion-content-height); opacity: 0.8; } 100% { height: var(--bits-accordion-content-height); opacity: 1; } } /* Define keyframes for closing animation */ @keyframes accordionClose { 0% { height: var(--bits-accordion-content-height); opacity: 1; } 20% { height: var(--bits-accordion-content-height); opacity: 0.8; } 100% { height: 0; opacity: 0; } } /* Apply animations based on state */ $2 { animation: accordionOpen 400ms cubic-bezier(0.16, 1, 0.3, 1) forwards; } $2 { animation: accordionClose 300ms cubic-bezier(0.7, 0, 0.84, 0) forwards; } Example: Animated Accordion Here's an example of an accordion with a custom transition: import { Accordion } from \"bits-ui\"; Section 1 Content for section 1 Section 2 Content for section 2 /* Base styles */ :global([data-accordion-item]) { border: 1px solid #e2e8f0; border-radius: 0.25rem; margin-bottom: 0.5rem; } /* Trigger styles based on state */ :global([data-accordion-trigger]) { width: 100%; padding: 1rem; display: flex; justify-content: space-between; align-items: center; } :global($2) { background-color: #f7fafc; border-bottom: 1px solid #e2e8f0; } /* Content styles */ :global([data-accordion-content]) { overflow: hidden; transition: height 300ms ease-out; } /* Define keyframes for opening animation */ @keyframes -global-accordionOpen { 0% { height: 0; opacity: 0; } 80% { height: var(--bits-accordion-content-height); opacity: 0.8; } 100% { height: var(--bits-accordion-content-height); opacity: 1; } } /* Define keyframes for closing animation */ @keyframes -global-accordionClose { 0% { height: var(--bits-accordion-content-height); opacity: 1; } 20% { height: var(--bits-accordion-content-height); opacity: 0.8; } 100% { height: 0; opacity: 0; } } /* Apply animations based on state */ :global($2) { animation: accordionOpen 400ms cubic-bezier(0.16, 1, 0.3, 1) forwards; } :global($2) { animation: accordionClose 300ms cubic-bezier(0.7, 0, 0.84, 0) forwards; } `","description":"Learn how to style Bits UI components.","href":"/docs/styling"},{"title":"Transitions","content":" import Callout from '$lib/components/callout.svelte'; Svelte Transitions are one of the awesome features of Svelte. Unfortunately, they don't play very nicely with components, due to the fact that they rely on various directives like in:, out:, and transition:, which aren't supported by components. In previous version of Bits UI, we had a workaround for this by exposing a ton of transition* props on the components that we felt were most likely to be used with transitions. However, this was a bit of a hack and limited us to only Svelte Transitions, and users who wanted to use other libraries or just CSS were left out. With Bits UI for Svelte 5, we've completely removed this workaround and instead exposed props and snippets that allow you to use any animation or transitions library you want. The Defaults By default, Bits UI components handle the mounting and unmounting of specific components for you. They are wrapped in a component that ensures the component waits for transitions to finish before unmounting. You can use any CSS transitions or animations you want with this approach, which is what we're doing in the various example components in this documentation, using $2. Force Mounting On each component that we conditionally render, a forceMount prop is exposed. If set to true, the component will be forced to mount in the DOM and become visible to the user. You can use this prop in conjunction with the $2 child snippet to conditionally render the component and apply Svelte Transitions or another animation library. The child snippet exposes a prop that you can use to conditionally render the element and apply your transitions. import { Dialog } from \"bits-ui\"; import { fly } from \"svelte/transition\"; {#snippet child({ props, open })} {#if open} {/if} {/snippet} In the example above, we're using the forceMount prop to tell the component to forcefully mount the Dialog.Content component. We're then using the child snippet to delegate the rendering of the Dialog.Content to a div element which we can apply our props and transitions to. We understand this isn't the prettiest syntax, but it enables us to cover every use case. If you intend to use this approach across your application, it's recommended to create a reusable component that handles this logic, like so: import type { Snippet } from \"svelte\"; import { fly } from \"svelte/transition\"; import { Dialog, type WithoutChildrenOrChild } from \"bits-ui\"; let { ref = $bindable(null), children, ...restProps }: WithoutChildrenOrChild & { children?: Snippet; } = $props(); {#snippet child({ props, open })} {#if open} {@render children?.()} {/if} {/snippet} Which can then be used alongside the other Dialog.* components: import { Dialog } from \"bits-ui\"; import MyDialogContent from \"$lib/components/MyDialogContent.svelte\"; Open Dialog Dialog Title Dialog Description Close Other dialog content Floating Content Components Content components that rely on Floating UI require a slight modification to how the child snippet is used. For example, if we were to use the Popover.Content component, we need to add a wrapper element within the child snippet, and spread the wrapperProps snippet prop to it. import { Popover } from \"bits-ui\"; import { fly } from \"svelte/transition\"; Open Popover {#snippet child({ wrapperProps, props, open })} {#if open} {/if} {/snippet} `","description":"Learn how to use transitions with Bits UI components.","href":"/docs/transitions"}] \ No newline at end of file diff --git a/packages/bits-ui/CHANGELOG.md b/packages/bits-ui/CHANGELOG.md index c22c0de20..05a528f40 100644 --- a/packages/bits-ui/CHANGELOG.md +++ b/packages/bits-ui/CHANGELOG.md @@ -1,5 +1,179 @@ # bits-ui +## 1.8.0 + +### Minor Changes + +- feat(Slider): `thumbPositioning` for more granular control of thumb positioning ([#1470](https://github.com/huntabyte/bits-ui/pull/1470)) + +### Patch Changes + +- fix(NavigationMenu): moving from submenu trigger to menu item in the same menu should close the submenu ([#1489](https://github.com/huntabyte/bits-ui/pull/1489)) + +- feat(NavigationMenu): `openOnHover` prop to control whether menu items open on hover or not ([#1491](https://github.com/huntabyte/bits-ui/pull/1491)) + +- fix(NavigationMenu): issues with non-viewport transitions ([#1489](https://github.com/huntabyte/bits-ui/pull/1489)) + +## 1.7.0 + +### Minor Changes + +- feat(DropdownMenu): new `DropdownMenu.CheckboxGroup` component ([#1486](https://github.com/huntabyte/bits-ui/pull/1486)) + +- feat(ContextMenu): new `ContextMenu.CheckboxGroup` component ([#1486](https://github.com/huntabyte/bits-ui/pull/1486)) + +- feat(Menubar): new `Menubar.CheckboxGroup` component ([#1486](https://github.com/huntabyte/bits-ui/pull/1486)) + +### Patch Changes + +- fix(Select): ensure scroll buttons render on subsequent mounts ([#1484](https://github.com/huntabyte/bits-ui/pull/1484)) + +- fix(Combobox): ensure scroll buttons render on subsequent mounts ([#1484](https://github.com/huntabyte/bits-ui/pull/1484)) + +## 1.6.1 + +### Patch Changes + +- fix(Tooltip): ensure only one tooltip within a Provider can be open at a time ([#1481](https://github.com/huntabyte/bits-ui/pull/1481)) + +- fix(Command): replace `encodeURIComponent` with `css.escape` for attribute values ([#1482](https://github.com/huntabyte/bits-ui/pull/1482)) + +## 1.6.0 + +### Minor Changes + +- feat(Slider): expose thumb `active` state ([#1471](https://github.com/huntabyte/bits-ui/pull/1471)) + +### Patch Changes + +- fix(DateRangeField): ensure prepopulated value takes priority over placeholder for validation ([#1479](https://github.com/huntabyte/bits-ui/pull/1479)) + +- fix(NavigationMenu): do not close `Sub` content when clicking the trigger ([#1473](https://github.com/huntabyte/bits-ui/pull/1473)) + +- fix(NavigationMenu): render `Content` without `Viewport` ([#1474](https://github.com/huntabyte/bits-ui/pull/1474)) + +- fix(DateField): ensure prepopulated value takes priority over placeholder for validation ([#1479](https://github.com/huntabyte/bits-ui/pull/1479)) + +## 1.5.3 + +### Patch Changes + +- chore: remove internal uses of parameter properties ([#1466](https://github.com/huntabyte/bits-ui/pull/1466)) + +## 1.5.2 + +### Patch Changes + +- fix(RangeCalendar): ensure `weekStartsOn` is absolute and fallback to locale if not provided ([#1462](https://github.com/huntabyte/bits-ui/pull/1462)) + +- fix(DateRangePicker): use current field to determine max days in month ([#1460](https://github.com/huntabyte/bits-ui/pull/1460)) + +- fix(DateRangePicker): ensure `weekStartsOn` is absolute and fallback to locale if not provided ([#1462](https://github.com/huntabyte/bits-ui/pull/1462)) + +- fix(DatePicker): ensure `weekStartsOn` is absolute and fallback to locale if not provided ([#1462](https://github.com/huntabyte/bits-ui/pull/1462)) + +- fix(Calendar): ensure `weekStartsOn` is absolute and fallback to locale if not provided ([#1462](https://github.com/huntabyte/bits-ui/pull/1462)) + +- fix(DateRangeField): use current field to determine max days in month ([#1460](https://github.com/huntabyte/bits-ui/pull/1460)) + +## 1.5.1 + +### Patch Changes + +- fix(NavigationMenu): allow roving focus to link items ([#1457](https://github.com/huntabyte/bits-ui/pull/1457)) + +## 1.5.0 + +### Minor Changes + +- feat(Menu): add `onSelect` for SubTrigger ([#1454](https://github.com/huntabyte/bits-ui/pull/1454)) + +### Patch Changes + +- fix(ScrollArea): ensure thumb properly restores previous position ([#1455](https://github.com/huntabyte/bits-ui/pull/1455)) + +- fix(DatePicker): export `Portal` parts ([#1451](https://github.com/huntabyte/bits-ui/pull/1451)) + +- fix(Menu): remove unused `closeOnSelect` prop from SubTrigger components ([#1453](https://github.com/huntabyte/bits-ui/pull/1453)) + +## 1.4.8 + +### Patch Changes + +- fix(Checkbox): ensure Checkbox.Group value setter is called ([#1440](https://github.com/huntabyte/bits-ui/pull/1440)) + +## 1.4.7 + +### Patch Changes + +- fix(Multiple): ensure `preventOverflowTextSelection` prop is applied ([#1435](https://github.com/huntabyte/bits-ui/pull/1435)) + +## 1.4.6 + +### Patch Changes + +- fix(Command): fallback to id when no group value ([#1428](https://github.com/huntabyte/bits-ui/pull/1428)) + +## 1.4.5 + +### Patch Changes + +- fix(Command): ensure groups without headings have a fallback ([#1425](https://github.com/huntabyte/bits-ui/pull/1425)) + +## 1.4.4 + +### Patch Changes + +- fix(Dialog): ensure `role="heading"` exists on title ([#1420](https://github.com/huntabyte/bits-ui/pull/1420)) + +## 1.4.3 + +### Patch Changes + +- fix(DateRangeField): do not update start date automatically ([#1406](https://github.com/huntabyte/bits-ui/pull/1406)) + +- fix(DateRangePicker): do not update start date automatically ([#1406](https://github.com/huntabyte/bits-ui/pull/1406)) + +## 1.4.2 + +### Patch Changes + +- fix(Dialog): ensure conditional content doesn't break nested focus ([#1410](https://github.com/huntabyte/bits-ui/pull/1410)) + +- fix(Command): ensure asynchronously loaded items register properly ([#1405](https://github.com/huntabyte/bits-ui/pull/1405)) + +- fix(Command): list restoration after empty state ([#1405](https://github.com/huntabyte/bits-ui/pull/1405)) + +## 1.4.1 + +### Patch Changes + +- fix(Multiple Components): ensure default values are set if entire spread props object is reassigned outside the component ([#1401](https://github.com/huntabyte/bits-ui/pull/1401)) + +- fix(Command): ensure `onValueChange` only fires when the value changes ([#1403](https://github.com/huntabyte/bits-ui/pull/1403)) + +- fix(Select): ensure typeahead ignores leading and trailing spaces ([#1399](https://github.com/huntabyte/bits-ui/pull/1399)) + +- fix(Select): disabled items should not be highlighted ([#1399](https://github.com/huntabyte/bits-ui/pull/1399)) + +## 1.4.0 + +### Minor Changes + +- feat(Combobox): add `delay` prop to scroll buttons for custom scroll delay ([#1395](https://github.com/huntabyte/bits-ui/pull/1395)) + +- feat(Select): add `delay` prop to scroll buttons for custom scroll delay ([#1395](https://github.com/huntabyte/bits-ui/pull/1395)) + +### Patch Changes + +- fix(Slider): update tick position calculation for consistent scaling ([#1375](https://github.com/huntabyte/bits-ui/pull/1375)) + +- chore(Popover): export `PopoverPortalPropsWithoutHTML` from Popover types ([#1397](https://github.com/huntabyte/bits-ui/pull/1397)) + +- fix(FocusScope): safely call onCloseAutoFocus handler if defined ([#1366](https://github.com/huntabyte/bits-ui/pull/1366)) + +- fix(Select): ensure `scrollAlignment` prop is used (if provided) when scrolling ([#1390](https://github.com/huntabyte/bits-ui/pull/1390)) + ## 1.3.19 ### Patch Changes diff --git a/packages/bits-ui/package.json b/packages/bits-ui/package.json index 3684c40d9..266b60909 100644 --- a/packages/bits-ui/package.json +++ b/packages/bits-ui/package.json @@ -1,6 +1,6 @@ { "name": "bits-ui", - "version": "1.3.19", + "version": "1.8.0", "license": "MIT", "repository": "github:huntabyte/bits-ui", "funding": "https://github.com/sponsors/huntabyte", @@ -30,6 +30,7 @@ "@sveltejs/kit": "^2.16.1", "@sveltejs/package": "^2.3.9", "@sveltejs/vite-plugin-svelte": "4.0.0", + "@types/css.escape": "^1.5.2", "@types/node": "^20.17.6", "@types/resize-observer-browser": "^0.1.11", "csstype": "^3.1.3", @@ -39,7 +40,7 @@ "svelte": "^5.22.6", "svelte-check": "^4.1.4", "tslib": "^2.8.1", - "typescript": "^5.6.3", + "typescript": "^5.8.3", "vite": "^5.4.11", "vitest": "^2.1.8" }, @@ -50,6 +51,7 @@ "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", + "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", 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 0dc961d94..06617158d 100644 --- a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts +++ b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts @@ -35,9 +35,12 @@ type AccordionBaseStateProps = WithRefProps< >; class AccordionBaseState { + readonly opts: AccordionBaseStateProps; rovingFocusGroup: UseRovingFocusReturn; - constructor(readonly opts: AccordionBaseStateProps) { + constructor(opts: AccordionBaseStateProps) { + this.opts = opts; + useRefById(this.opts); this.rovingFocusGroup = useRovingFocus({ @@ -66,10 +69,12 @@ class AccordionBaseState { type AccordionSingleStateProps = AccordionBaseStateProps & WritableBoxedValues<{ value: string }>; export class AccordionSingleState extends AccordionBaseState { + readonly opts: AccordionSingleStateProps; isMulti = false as const; - constructor(readonly opts: AccordionSingleStateProps) { + constructor(opts: AccordionSingleStateProps) { super(opts); + this.opts = opts; this.includesItem = this.includesItem.bind(this); this.toggleItem = this.toggleItem.bind(this); } @@ -128,11 +133,13 @@ type AccordionItemStateProps = WithRefProps< >; export class AccordionItemState { + readonly opts: AccordionItemStateProps; root: AccordionState; isActive = $derived.by(() => this.root.includesItem(this.opts.value.current)); isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); - constructor(readonly opts: AccordionItemStateProps) { + constructor(opts: AccordionItemStateProps) { + this.opts = opts; this.root = opts.rootState; this.updateValue = this.updateValue.bind(this); @@ -170,6 +177,8 @@ type AccordionTriggerStateProps = WithRefProps< >; class AccordionTriggerState { + readonly opts: AccordionTriggerStateProps; + readonly itemState: AccordionItemState; #root: AccordionState; #isDisabled = $derived.by( () => @@ -178,10 +187,9 @@ class AccordionTriggerState { this.#root.opts.disabled.current ); - constructor( - readonly opts: AccordionTriggerStateProps, - readonly itemState: AccordionItemState - ) { + constructor(opts: AccordionTriggerStateProps, itemState: AccordionItemState) { + this.opts = opts; + this.itemState = itemState; this.#root = itemState.root; this.onkeydown = this.onkeydown.bind(this); this.onclick = this.onclick.bind(this); @@ -235,6 +243,8 @@ type AccordionContentStateProps = WithRefProps< }> >; class AccordionContentState { + readonly opts: AccordionContentStateProps; + readonly item: AccordionItemState; #originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined; #isMountAnimationPrevented = false; #width = $state(0); @@ -242,10 +252,8 @@ class AccordionContentState { present = $derived.by(() => this.opts.forceMount.current || this.item.isActive); - constructor( - readonly opts: AccordionContentStateProps, - readonly item: AccordionItemState - ) { + constructor(opts: AccordionContentStateProps, item: AccordionItemState) { + this.opts = opts; this.item = item; this.#isMountAnimationPrevented = this.item.isActive; @@ -316,10 +324,13 @@ type AccordionHeaderStateProps = WithRefProps< >; class AccordionHeaderState { - constructor( - readonly opts: AccordionHeaderStateProps, - readonly item: AccordionItemState - ) { + readonly opts: AccordionHeaderStateProps; + readonly item: AccordionItemState; + + constructor(opts: AccordionHeaderStateProps, item: AccordionItemState) { + this.opts = opts; + this.item = item; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte index a5b8f6236..2e40bb706 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion.svelte @@ -4,6 +4,7 @@ import type { AccordionRootProps } from "../types.js"; import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; + import { watch } from "runed"; let { disabled = false, @@ -19,7 +20,20 @@ ...restProps }: AccordionRootProps = $props(); - value === undefined && (value = type === "single" ? "" : []); + function handleDefaultValue() { + if (value !== undefined) return; + value = type === "single" ? "" : []; + } + + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); const rootState = useAccordionRoot({ type, 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 dbc3f582f..38793dc51 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 @@ -54,7 +54,7 @@ trapFocus, open: contentState.root.opts.open.current, })} - {...mergedProps} + {id} onCloseAutoFocus={(e) => { onCloseAutoFocus(e); if (e.defaultPrevented) return; diff --git a/packages/bits-ui/src/lib/bits/aspect-ratio/aspect-ratio.svelte.ts b/packages/bits-ui/src/lib/bits/aspect-ratio/aspect-ratio.svelte.ts index fa03ec440..df7bbe21c 100644 --- a/packages/bits-ui/src/lib/bits/aspect-ratio/aspect-ratio.svelte.ts +++ b/packages/bits-ui/src/lib/bits/aspect-ratio/aspect-ratio.svelte.ts @@ -7,7 +7,11 @@ const ASPECT_RATIO_ROOT_ATTR = "data-aspect-ratio-root"; type AspectRatioRootStateProps = WithRefProps>; class AspectRatioRootState { - constructor(readonly opts: AspectRatioRootStateProps) { + readonly opts: AspectRatioRootStateProps; + + constructor(opts: AspectRatioRootStateProps) { + this.opts = opts; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts b/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts index eaeec6d65..086db86f4 100644 --- a/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts @@ -24,8 +24,12 @@ type AvatarRootStateProps = WithRefProps<{ type AvatarImageSrc = string | null | undefined; class AvatarRootState { - constructor(readonly opts: AvatarRootStateProps) { + readonly opts: AvatarRootStateProps; + + constructor(opts: AvatarRootStateProps) { + this.opts = opts; this.loadImage = this.loadImage.bind(this); + useRefById(opts); } @@ -75,10 +79,13 @@ type AvatarImageStateProps = WithRefProps< >; class AvatarImageState { - constructor( - readonly opts: AvatarImageStateProps, - readonly root: AvatarRootState - ) { + readonly opts: AvatarImageStateProps; + readonly root: AvatarRootState; + + constructor(opts: AvatarImageStateProps, root: AvatarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); $effect.pre(() => { @@ -121,10 +128,13 @@ class AvatarImageState { type AvatarFallbackStateProps = WithRefProps; class AvatarFallbackState { - constructor( - readonly opts: AvatarFallbackStateProps, - readonly root: AvatarRootState - ) { + readonly opts: AvatarFallbackStateProps; + readonly root: AvatarRootState; + + constructor(opts: AvatarFallbackStateProps, root: AvatarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts b/packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts index 1c1de1db2..e1c83787d 100644 --- a/packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts @@ -45,6 +45,7 @@ import { useMonthViewPlaceholderSync, } from "$lib/internal/date-time/calendar-helpers.svelte.js"; import { getDateValueType, isBefore, toDate } from "$lib/internal/date-time/utils.js"; +import type { WeekStartsOn } from "$lib/shared/date/types.js"; type CalendarRootStateProps = WithRefProps< WritableBoxedValues<{ @@ -57,7 +58,7 @@ type CalendarRootStateProps = WithRefProps< maxValue: DateValue | undefined; disabled: boolean; pagedNavigation: boolean; - weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6; + weekStartsOn: WeekStartsOn | undefined; weekdayFormat: Intl.DateTimeFormatOptions["weekday"]; isDateDisabled: DateMatcher; isDateUnavailable: DateMatcher; @@ -80,13 +81,15 @@ type CalendarRootStateProps = WithRefProps< >; export class CalendarRootState { + readonly opts: CalendarRootStateProps; months: Month[] = $state([]); visibleMonths = $derived.by(() => this.months.map((month) => month.value)); announcer: Announcer; formatter: Formatter; accessibleHeadingId = useId(); - constructor(readonly opts: CalendarRootStateProps) { + constructor(opts: CalendarRootStateProps) { + this.opts = opts; this.announcer = getAnnouncer(); this.formatter = createFormatter(this.opts.locale.current); @@ -465,12 +468,14 @@ export class CalendarRootState { export type CalendarHeadingStateProps = WithRefProps; export class CalendarHeadingState { + readonly opts: CalendarHeadingStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; headingValue = $derived.by(() => this.root.headingValue); - constructor( - readonly opts: CalendarHeadingStateProps, - readonly root: CalendarRootState | RangeCalendarRootState - ) { + constructor(opts: CalendarHeadingStateProps, root: CalendarRootState | RangeCalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -494,6 +499,8 @@ type CalendarCellStateProps = WithRefProps< >; class CalendarCellState { + readonly opts: CalendarCellStateProps; + readonly root: CalendarRootState; cellDate = $derived.by(() => toDate(this.opts.date.current)); isDisabled = $derived.by(() => this.root.isDateDisabled(this.opts.date.current)); isUnavailable = $derived.by(() => @@ -519,10 +526,10 @@ class CalendarCellState { }) ); - constructor( - readonly opts: CalendarCellStateProps, - readonly root: CalendarRootState - ) { + constructor(opts: CalendarCellStateProps, root: CalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -574,10 +581,12 @@ class CalendarCellState { type CalendarDayStateProps = WithRefProps; class CalendarDayState { - constructor( - readonly opts: CalendarDayStateProps, - readonly cell: CalendarCellState - ) { + readonly opts: CalendarDayStateProps; + readonly cell: CalendarCellState; + + constructor(opts: CalendarDayStateProps, cell: CalendarCellState) { + this.opts = opts; + this.cell = cell; this.onclick = this.onclick.bind(this); useRefById(opts); @@ -625,12 +634,16 @@ class CalendarDayState { export type CalendarNextButtonStateProps = WithRefProps; export class CalendarNextButtonState { + readonly opts: CalendarNextButtonStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; isDisabled = $derived.by(() => this.root.isNextButtonDisabled); constructor( - readonly opts: CalendarNextButtonStateProps, - readonly root: CalendarRootState | RangeCalendarRootState + opts: CalendarNextButtonStateProps, + root: CalendarRootState | RangeCalendarRootState ) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); useRefById(opts); @@ -661,12 +674,16 @@ export class CalendarNextButtonState { export type CalendarPrevButtonStateProps = WithRefProps; export class CalendarPrevButtonState { + readonly opts: CalendarPrevButtonStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; isDisabled = $derived.by(() => this.root.isPrevButtonDisabled); constructor( - readonly opts: CalendarPrevButtonStateProps, - readonly root: CalendarRootState | RangeCalendarRootState + opts: CalendarPrevButtonStateProps, + root: CalendarRootState | RangeCalendarRootState ) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); useRefById(opts); @@ -697,10 +714,13 @@ export class CalendarPrevButtonState { export type CalendarGridStateProps = WithRefProps; export class CalendarGridState { - constructor( - readonly opts: CalendarGridStateProps, - readonly root: CalendarRootState | RangeCalendarRootState - ) { + readonly opts: CalendarGridStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + + constructor(opts: CalendarGridStateProps, root: CalendarRootState | RangeCalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -722,10 +742,16 @@ export class CalendarGridState { export type CalendarGridBodyStateProps = WithRefProps; export class CalendarGridBodyState { + readonly opts: CalendarGridBodyStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + constructor( - readonly opts: CalendarGridBodyStateProps, - readonly root: CalendarRootState | RangeCalendarRootState + opts: CalendarGridBodyStateProps, + root: CalendarRootState | RangeCalendarRootState ) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -743,10 +769,16 @@ export class CalendarGridBodyState { export type CalendarGridHeadStateProps = WithRefProps; export class CalendarGridHeadState { + readonly opts: CalendarGridHeadStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + constructor( - readonly opts: CalendarGridHeadStateProps, - readonly root: CalendarRootState | RangeCalendarRootState + opts: CalendarGridHeadStateProps, + root: CalendarRootState | RangeCalendarRootState ) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -764,10 +796,13 @@ export class CalendarGridHeadState { export type CalendarGridRowStateProps = WithRefProps; export class CalendarGridRowState { - constructor( - readonly opts: CalendarGridRowStateProps, - readonly root: CalendarRootState | RangeCalendarRootState - ) { + readonly opts: CalendarGridRowStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + + constructor(opts: CalendarGridRowStateProps, root: CalendarRootState | RangeCalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -785,10 +820,16 @@ export class CalendarGridRowState { export type CalendarHeadCellStateProps = WithRefProps; export class CalendarHeadCellState { + readonly opts: CalendarHeadCellStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + constructor( - readonly opts: CalendarHeadCellStateProps, - readonly root: CalendarRootState | RangeCalendarRootState + opts: CalendarHeadCellStateProps, + root: CalendarRootState | RangeCalendarRootState ) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -806,10 +847,13 @@ export class CalendarHeadCellState { export type CalendarHeaderStateProps = WithRefProps; export class CalendarHeaderState { - constructor( - readonly opts: CalendarHeaderStateProps, - readonly root: CalendarRootState | RangeCalendarRootState - ) { + readonly opts: CalendarHeaderStateProps; + readonly root: CalendarRootState | RangeCalendarRootState; + + constructor(opts: CalendarHeaderStateProps, root: CalendarRootState | RangeCalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte b/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte index fb2418c37..0e6e501e0 100644 --- a/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte +++ b/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte @@ -6,6 +6,7 @@ import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { child, @@ -17,7 +18,7 @@ placeholder = $bindable(), onPlaceholderChange = noop, weekdayFormat = "narrow", - weekStartsOn = 0, + weekStartsOn, pagedNavigation = false, isDateDisabled = () => false, isDateUnavailable = () => false, @@ -40,16 +41,36 @@ defaultValue: value, }); - if (placeholder === undefined) { + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; placeholder = defaultPlaceholder; } - if (value === undefined) { - const defaultValue = type === "single" ? undefined : []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = defaultValue as any; + // SSR + handleDefaultPlaceholder(); + + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + + function handleDefaultValue() { + if (value !== undefined) return; + value = type === "single" ? undefined : []; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useCalendarRoot({ id: box.with(() => id), ref: box.with( diff --git a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts index fdd5ce540..d6d50fe46 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts @@ -28,17 +28,21 @@ type CheckboxGroupStateProps = WithRefProps< >; class CheckboxGroupState { + readonly opts: CheckboxGroupStateProps; labelId = $state(undefined); - constructor(readonly opts: CheckboxGroupStateProps) { + constructor(opts: CheckboxGroupStateProps) { + this.opts = opts; + useRefById(opts); } addValue(checkboxValue: string | undefined) { if (!checkboxValue) return; if (!this.opts.value.current.includes(checkboxValue)) { - this.opts.value.current.push(checkboxValue); - this.opts.onValueChange.current(this.opts.value.current); + const newValue = [...$state.snapshot(this.opts.value.current), checkboxValue]; + this.opts.value.current = newValue; + this.opts.onValueChange.current(newValue); } } @@ -46,8 +50,9 @@ class CheckboxGroupState { if (!checkboxValue) return; const index = this.opts.value.current.indexOf(checkboxValue); if (index === -1) return; - this.opts.value.current.splice(index, 1); - this.opts.onValueChange.current(this.opts.value.current); + const newValue = this.opts.value.current.filter((v) => v !== checkboxValue); + this.opts.value.current = newValue; + this.opts.onValueChange.current(newValue); } props = $derived.by( @@ -65,10 +70,13 @@ class CheckboxGroupState { type CheckboxGroupLabelStateProps = WithRefProps; class CheckboxGroupLabelState { - constructor( - readonly opts: CheckboxGroupLabelStateProps, - readonly group: CheckboxGroupState - ) { + readonly opts: CheckboxGroupLabelStateProps; + readonly group: CheckboxGroupState; + + constructor(opts: CheckboxGroupLabelStateProps, group: CheckboxGroupState) { + this.opts = opts; + this.group = group; + useRefById({ ...opts, onRefChange: (node) => { @@ -106,6 +114,8 @@ type CheckboxRootStateProps = WithRefProps< >; class CheckboxRootState { + readonly opts: CheckboxRootStateProps; + readonly group: CheckboxGroupState | null; trueName = $derived.by(() => { if (this.group && this.group.opts.name.current) { return this.group.opts.name.current; @@ -126,10 +136,9 @@ class CheckboxRootState { return this.opts.disabled.current; }); - constructor( - readonly opts: CheckboxRootStateProps, - readonly group: CheckboxGroupState | null = null - ) { + constructor(opts: CheckboxRootStateProps, group: CheckboxGroupState | null = null) { + this.opts = opts; + this.group = group; this.onkeydown = this.onkeydown.bind(this); this.onclick = this.onclick.bind(this); useRefById(opts); @@ -213,6 +222,7 @@ class CheckboxRootState { // class CheckboxInputState { + readonly root: CheckboxRootState; trueChecked = $derived.by(() => { if (this.root.group) { if ( @@ -228,7 +238,9 @@ class CheckboxInputState { shouldRender = $derived.by(() => Boolean(this.root.trueName)); - constructor(readonly root: CheckboxRootState) {} + constructor(root: CheckboxRootState) { + this.root = root; + } props = $derived.by( () => diff --git a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-group.svelte b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-group.svelte index b946ad1dd..44dc3a10c 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-group.svelte +++ b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox-group.svelte @@ -28,9 +28,9 @@ required: box.with(() => Boolean(required)), name: box.with(() => name), value: box.with( - () => value, + () => $state.snapshot(value), (v) => { - value = v; + value = $state.snapshot(v); onValueChange(v); } ), diff --git a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte index b1228596a..4eb5db3c3 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte +++ b/packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte @@ -4,6 +4,7 @@ import { CheckboxGroupContext, useCheckboxRoot } from "../checkbox.svelte.js"; import CheckboxInput from "./checkbox-input.svelte"; import { useId } from "$lib/internal/use-id.js"; + import { watch } from "runed"; let { checked = $bindable(false), @@ -32,6 +33,19 @@ } } + watch.pre( + () => value, + () => { + if (group && value) { + if (group.opts.value.current.includes(value)) { + checked = true; + } else { + checked = false; + } + } + } + ); + const rootState = useCheckboxRoot( { checked: box.with( diff --git a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts index 44d4d6edf..22d6b6416 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts @@ -18,9 +18,11 @@ type CollapsibleRootStateProps = WithRefProps & }>; class CollapsibleRootState { + readonly opts: CollapsibleRootStateProps; contentNode = $state(null); - constructor(readonly opts: CollapsibleRootStateProps) { + constructor(opts: CollapsibleRootStateProps) { + this.opts = opts; this.toggleOpen = this.toggleOpen.bind(this); useRefById(opts); @@ -46,16 +48,17 @@ type CollapsibleContentStateProps = WithRefProps & forceMount: boolean; }>; class CollapsibleContentState { + readonly opts: CollapsibleContentStateProps; + readonly root: CollapsibleRootState; #originalStyles: { transitionDuration: string; animationName: string } | undefined; #isMountAnimationPrevented = $state(false); #width = $state(0); #height = $state(0); present = $derived.by(() => this.opts.forceMount.current || this.root.opts.open.current); - constructor( - readonly opts: CollapsibleContentStateProps, - readonly root: CollapsibleRootState - ) { + constructor(opts: CollapsibleContentStateProps, root: CollapsibleRootState) { + this.opts = opts; + this.root = root; this.#isMountAnimationPrevented = root.opts.open.current; useRefById({ @@ -133,12 +136,13 @@ type CollapsibleTriggerStateProps = WithRefProps & }>; class CollapsibleTriggerState { + readonly opts: CollapsibleTriggerStateProps; + readonly root: CollapsibleRootState; #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); - constructor( - readonly opts: CollapsibleTriggerStateProps, - readonly root: CollapsibleRootState - ) { + constructor(opts: CollapsibleTriggerStateProps, root: CollapsibleRootState) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); diff --git a/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte b/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte index dd023e126..beb3375e3 100644 --- a/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte +++ b/packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte @@ -5,6 +5,7 @@ import FloatingLayer from "$lib/bits/utilities/floating-layer/components/floating-layer.svelte"; import { useSelectRoot } from "$lib/bits/select/select.svelte.js"; import ListboxHiddenInput from "$lib/bits/select/components/select-hidden-input.svelte"; + import { watch } from "runed"; let { value = $bindable(), @@ -27,6 +28,14 @@ value = defaultValue; } + watch.pre( + () => value, + () => { + if (value !== undefined) return; + value = type === "single" ? "" : []; + } + ); + const rootState = useSelectRoot({ type, value: box.with( diff --git a/packages/bits-ui/src/lib/bits/command/command.svelte.ts b/packages/bits-ui/src/lib/bits/command/command.svelte.ts index eb9005b06..a9934f181 100644 --- a/packages/bits-ui/src/lib/bits/command/command.svelte.ts +++ b/packages/bits-ui/src/lib/bits/command/command.svelte.ts @@ -19,7 +19,7 @@ import { } from "$lib/internal/attrs.js"; import { getFirstNonCommentChild } from "$lib/internal/dom.js"; import { computeCommandScore } from "./index.js"; -import { noop } from "$lib/internal/noop.js"; +import cssesc from "css.escape"; // attributes const COMMAND_ROOT_ATTR = "data-command-root"; @@ -77,6 +77,7 @@ const defaultState = { }; class CommandRootState { + readonly opts: CommandRootStateProps; #updateScheduled = false; sortAfterTick = false; sortAndFilterAfterTick = false; @@ -92,8 +93,6 @@ class CommandRootState { commandState = $state.raw(defaultState); // internal state that we mutate in batches and publish to the `state` at once _commandState = $state(defaultState); - // whether the search has had a value other than "" - searchHasHadValue = $state(false); #snapshot() { return $state.snapshot(this._commandState); @@ -125,10 +124,6 @@ class CommandRootState { // Filter synchronously before emitting back to children this.#filterItems(); this.#sort(); - this.#selectFirstItem(); - afterTick(() => { - this.#selectFirstItem(); - }); } else if (key === "value") { // opts is a boolean referring to whether it should NOT be scrolled into view if (!opts) { @@ -140,7 +135,9 @@ class CommandRootState { this.#scheduleUpdate(); } - constructor(readonly opts: CommandRootStateProps) { + constructor(opts: CommandRootStateProps) { + this.opts = opts; + const defaults = { ...this._commandState, value: this.opts.value.current ?? "" }; this._commandState = defaults; @@ -149,12 +146,6 @@ class CommandRootState { useRefById(opts); this.onkeydown = this.onkeydown.bind(this); - - $effect(() => { - if (this._commandState.search !== "") { - this.searchHasHadValue = true; - } - }); } /** @@ -179,7 +170,10 @@ class CommandRootState { #sort(): void { if (!this._commandState.search || this.opts.shouldFilter.current === false) { // If no search and no selection yet, select first item - if (!this.commandState.value) this.#selectFirstItem(); + this.#selectFirstItem(); + // if (!this.commandState.value) { + // this.#selectFirstItem(); + // } return; } @@ -209,8 +203,8 @@ class CommandRootState { const listInsertionElement = this.viewportNode; const sorted = this.getValidItems().sort((a, b) => { - const valueA = a.getAttribute("id"); - const valueB = b.getAttribute("id"); + const valueA = a.getAttribute("data-value"); + const valueB = b.getAttribute("data-value"); const scoresA = scores.get(valueA!) ?? 0; const scoresB = scores.get(valueB!) ?? 0; return scoresB - scoresA; @@ -244,10 +238,12 @@ class CommandRootState { for (const group of sortedGroups) { const element = listInsertionElement?.querySelector( - `${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(group[0])}"]` + `${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssesc(group[0])}"]` ); element?.parentElement?.appendChild(element); } + + this.#selectFirstItem(); } /** @@ -476,10 +472,11 @@ class CommandRootState { * @param keywords - Optional search boost terms * @returns Cleanup function */ - registerValue(id: string, value: string, keywords?: string[]): () => void { - if (value === this.allIds.get(id)?.value) return noop; - this.allIds.set(id, { value, keywords }); - this._commandState.filtered.items.set(id, this.#score(value, keywords)); + registerValue(value: string, keywords?: string[]): () => void { + if (!(value && value === this.allIds.get(value)?.value)) { + this.allIds.set(value, { value, keywords }); + } + this._commandState.filtered.items.set(value, this.#score(value, keywords)); // Schedule sorting to run after this tick when all items are added not each time an item is added if (!this.sortAfterTick) { @@ -491,7 +488,7 @@ class CommandRootState { } return () => { - this.allIds.delete(id); + this.allIds.delete(value); }; } @@ -681,20 +678,20 @@ type CommandEmptyStateProps = WithRefProps & }>; class CommandEmptyState { + readonly opts: CommandEmptyStateProps; + readonly root: CommandRootState; #isInitialRender = true; shouldRender = $derived.by(() => { return ( - (this.root._commandState.filtered.count === 0 && - this.#isInitialRender === false && - this.root.searchHasHadValue) || + (this.root._commandState.filtered.count === 0 && this.#isInitialRender === false) || this.opts.forceMount.current ); }); - constructor( - readonly opts: CommandEmptyStateProps, - readonly root: CommandRootState - ) { + constructor(opts: CommandEmptyStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + $effect.pre(() => { this.#isInitialRender = false; }); @@ -723,21 +720,22 @@ type CommandGroupContainerStateProps = WithRefProps< >; class CommandGroupContainerState { + readonly opts: CommandGroupContainerStateProps; + readonly root: CommandRootState; headingNode = $state(null); + trueValue = $state(""); shouldRender = $derived.by(() => { if (this.opts.forceMount.current) return true; if (this.root.opts.shouldFilter.current === false) return true; if (!this.root.commandState.search) return true; - return this.root.commandState.filtered.groups.has(this.opts.id.current); + return this.root._commandState.filtered.groups.has(this.trueValue); }); - trueValue = $state(""); - constructor( - readonly opts: CommandGroupContainerStateProps, - readonly root: CommandRootState - ) { - this.trueValue = opts.value.current; + constructor(opts: CommandGroupContainerStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + this.trueValue = opts.value.current ?? opts.id.current; useRefById({ ...opts, @@ -745,22 +743,22 @@ class CommandGroupContainerState { }); watch( - () => this.opts.id.current, + () => this.trueValue, () => { - return this.root.registerGroup(this.opts.id.current); + return this.root.registerGroup(this.trueValue); } ); $effect(() => { if (this.opts.value.current) { this.trueValue = this.opts.value.current; - return this.root.registerValue(this.opts.id.current, this.opts.value.current); + return this.root.registerValue(this.opts.value.current); } else if (this.headingNode && this.headingNode.textContent) { this.trueValue = this.headingNode.textContent.trim().toLowerCase(); - return this.root.registerValue(this.opts.id.current, this.trueValue); - } else if (this.opts.ref.current?.textContent) { - this.trueValue = this.opts.ref.current.textContent.trim().toLowerCase(); - return this.root.registerValue(this.opts.id.current, this.trueValue); + return this.root.registerValue(this.trueValue); + } else { + this.trueValue = `-----${this.opts.id.current}`; + return this.root.registerValue(this.trueValue); } }); } @@ -780,10 +778,13 @@ class CommandGroupContainerState { type CommandGroupHeadingStateProps = WithRefProps; class CommandGroupHeadingState { - constructor( - readonly opts: CommandGroupHeadingStateProps, - readonly group: CommandGroupContainerState - ) { + readonly opts: CommandGroupHeadingStateProps; + readonly group: CommandGroupContainerState; + + constructor(opts: CommandGroupHeadingStateProps, group: CommandGroupContainerState) { + this.opts = opts; + this.group = group; + useRefById({ ...opts, onRefChange: (node) => { @@ -804,10 +805,13 @@ class CommandGroupHeadingState { type CommandGroupItemsStateProps = WithRefProps; class CommandGroupItemsState { - constructor( - readonly opts: CommandGroupItemsStateProps, - readonly group: CommandGroupContainerState - ) { + readonly opts: CommandGroupItemsStateProps; + readonly group: CommandGroupContainerState; + + constructor(opts: CommandGroupItemsStateProps, group: CommandGroupContainerState) { + this.opts = opts; + this.group = group; + useRefById(opts); } @@ -832,18 +836,20 @@ type CommandInputStateProps = WithRefProps< >; class CommandInputState { + readonly opts: CommandInputStateProps; + readonly root: CommandRootState; #selectedItemId = $derived.by(() => { const item = this.root.viewportNode?.querySelector( - `${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(this.root.opts.value.current)}"]` + `${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssesc(this.root.opts.value.current)}"]` ); if (!item) return; return item?.getAttribute("id") ?? undefined; }); - constructor( - readonly opts: CommandInputStateProps, - readonly root: CommandRootState - ) { + constructor(opts: CommandInputStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -903,6 +909,8 @@ type CommandItemStateProps = WithRefProps< >; class CommandItemState { + readonly opts: CommandItemStateProps; + readonly root: CommandRootState; #group: CommandGroupContainerState | null = null; #trueForceMount = $derived.by(() => { return this.opts.forceMount.current || this.#group?.opts.forceMount.current === true; @@ -917,7 +925,7 @@ class CommandItemState { ) { return true; } - const currentScore = this.root.commandState.filtered.items.get(this.opts.id.current); + const currentScore = this.root.commandState.filtered.items.get(this.trueValue); if (currentScore === undefined) return false; return currentScore > 0; }); @@ -926,10 +934,9 @@ class CommandItemState { () => this.root.opts.value.current === this.trueValue && this.trueValue !== "" ); - constructor( - readonly opts: CommandItemStateProps, - readonly root: CommandRootState - ) { + constructor(opts: CommandItemStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; this.#group = CommandGroupContainerContext.getOr(null); this.trueValue = opts.value.current; @@ -940,29 +947,26 @@ class CommandItemState { watch( [ - () => this.opts.id.current, - () => this.#group?.opts.id.current, + () => this.trueValue, + () => this.#group?.trueValue, () => this.opts.forceMount.current, - () => this.opts.ref.current, ], () => { if (this.opts.forceMount.current) return; - return this.root.registerItem(this.opts.id.current, this.#group?.opts.id.current); + return this.root.registerItem(this.trueValue, this.#group?.trueValue); } ); watch([() => this.opts.value.current, () => this.opts.ref.current], () => { - if (!this.opts.ref.current) return; - if (!this.opts.value.current && this.opts.ref.current.textContent) { + if (!this.opts.value.current && this.opts.ref.current?.textContent) { this.trueValue = this.opts.ref.current.textContent.trim(); } this.root.registerValue( - this.opts.id.current, this.trueValue, opts.keywords.current.map((kw) => kw.trim()) ); - this.opts.ref.current.setAttribute(COMMAND_VALUE_ATTR, this.trueValue); + this.opts.ref.current?.setAttribute(COMMAND_VALUE_ATTR, this.trueValue); }); // bindings @@ -1015,7 +1019,11 @@ type CommandLoadingStateProps = WithRefProps< >; class CommandLoadingState { - constructor(readonly opts: CommandLoadingStateProps) { + readonly opts: CommandLoadingStateProps; + + constructor(opts: CommandLoadingStateProps) { + this.opts = opts; + useRefById(opts); } @@ -1039,14 +1047,16 @@ type CommandSeparatorStateProps = WithRefProps & }>; class CommandSeparatorState { + readonly opts: CommandSeparatorStateProps; + readonly root: CommandRootState; shouldRender = $derived.by( () => !this.root._commandState.search || this.opts.forceMount.current ); - constructor( - readonly opts: CommandSeparatorStateProps, - readonly root: CommandRootState - ) { + constructor(opts: CommandSeparatorStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.shouldRender, @@ -1070,10 +1080,13 @@ type CommandListStateProps = WithRefProps & }>; class CommandListState { - constructor( - readonly opts: CommandListStateProps, - readonly root: CommandRootState - ) { + readonly opts: CommandListStateProps; + readonly root: CommandRootState; + + constructor(opts: CommandListStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -1091,10 +1104,13 @@ class CommandListState { type CommandLabelStateProps = WithRefProps>; class CommandLabelState { - constructor( - readonly opts: CommandLabelStateProps, - readonly root: CommandRootState - ) { + readonly opts: CommandLabelStateProps; + readonly root: CommandRootState; + + constructor(opts: CommandLabelStateProps, root: CommandRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -1117,10 +1133,13 @@ class CommandLabelState { type CommandViewportStateProps = WithRefProps; class CommandViewportState { - constructor( - readonly opts: CommandViewportStateProps, - readonly list: CommandListState - ) { + readonly opts: CommandViewportStateProps; + readonly list: CommandListState; + + constructor(opts: CommandViewportStateProps, list: CommandListState) { + this.opts = opts; + this.list = list; + useRefById({ ...opts, onRefChange: (node) => { diff --git a/packages/bits-ui/src/lib/bits/command/components/command.svelte b/packages/bits-ui/src/lib/bits/command/components/command.svelte index 0802f471a..e43a9e551 100644 --- a/packages/bits-ui/src/lib/bits/command/components/command.svelte +++ b/packages/bits-ui/src/lib/bits/command/components/command.svelte @@ -36,8 +36,10 @@ value: box.with( () => value, (v) => { - value = v; - onValueChange(v); + if (value !== v) { + value = v; + onValueChange(v); + } } ), vimBindings: box.with(() => vimBindings), diff --git a/packages/bits-ui/src/lib/bits/command/types.ts b/packages/bits-ui/src/lib/bits/command/types.ts index a7534766e..8ad9b34bc 100644 --- a/packages/bits-ui/src/lib/bits/command/types.ts +++ b/packages/bits-ui/src/lib/bits/command/types.ts @@ -15,7 +15,7 @@ export type CommandState = { filtered: { /** The count of all visible items. */ count: number; - /** Map from visible item id to its search store. */ + /** Map from visible item value to its search store. */ items: Map; /** Set of groups with at least one visible item. */ groups: Set; diff --git a/packages/bits-ui/src/lib/bits/context-menu/exports.ts b/packages/bits-ui/src/lib/bits/context-menu/exports.ts index 8b248d312..01692dcc1 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/exports.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/exports.ts @@ -15,6 +15,7 @@ export { default as SubContentStatic } from "$lib/bits/menu/components/menu-sub- 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"; +export { default as CheckboxGroup } from "$lib/bits/menu/components/menu-checkbox-group.svelte"; export type { ContextMenuArrowProps as ArrowProps, @@ -34,4 +35,5 @@ export type { ContextMenuContentStaticProps as ContentStaticProps, ContextMenuTriggerProps as TriggerProps, ContextMenuPortalProps as PortalProps, + ContextMenuCheckboxGroupProps as CheckboxGroupProps, } from "./types.js"; 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 847c11267..e0bd15703 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/types.ts @@ -36,6 +36,7 @@ export type { MenuSubProps as ContextMenuSubProps, MenuSubTriggerProps as ContextMenuSubTriggerProps, MenuPortalProps as ContextMenuPortalProps, + MenuCheckboxGroupProps as ContextMenuCheckboxGroupProps, } from "$lib/bits/menu/types.js"; export type { @@ -54,4 +55,5 @@ export type { MenuSubContentPropsWithoutHTML as ContextMenuSubContentPropsWithoutHTML, MenuSubContentStaticPropsWithoutHTML as ContextMenuSubContentStaticPropsWithoutHTML, MenuPortalPropsWithoutHTML as ContextMenuPortalPropsWithoutHTML, + MenuCheckboxGroupPropsWithoutHTML as ContextMenuCheckboxGroupPropsWithoutHTML, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte index 140c774da..4319b5a2a 100644 --- a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte +++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte @@ -5,6 +5,7 @@ import type { DateFieldRootProps } from "../types.js"; import { noop } from "$lib/internal/noop.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { disabled = false, @@ -27,7 +28,9 @@ children, }: DateFieldRootProps = $props(); - if (placeholder === undefined) { + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; + const defaultPlaceholder = getDefaultDate({ granularity, defaultValue: value, @@ -36,6 +39,21 @@ placeholder = defaultPlaceholder; } + // SSR + handleDefaultPlaceholder(); + + /** + * Covers an edge case where when a spread props object is reassigned, + * the props are reset to their default values, which would make placeholder + * undefined which causes errors to be thrown. + */ + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + useDateFieldRoot({ value: box.with( () => value, diff --git a/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts index 5c649bf05..07dcf7815 100644 --- a/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts @@ -65,6 +65,80 @@ import { export const DATE_FIELD_INPUT_ATTR = "data-date-field-input"; const DATE_FIELD_LABEL_ATTR = "data-date-field-label"; +// Common segment configuration +type SegmentConfig = { + min: number | ((root: DateFieldRootState) => number); + max: number | ((root: DateFieldRootState) => number); + cycle: number; + canBeZero?: boolean; + padZero?: boolean; + getAnnouncement?: (value: number, root: DateFieldRootState) => string | number; + updateLogic?: (props: { + root: DateFieldRootState; + prev: string | null; + num: number; + moveToNext: { value: boolean }; + }) => string | null; +}; + +const SEGMENT_CONFIGS: Record< + "day" | "month" | "year" | "hour" | "minute" | "second", + SegmentConfig +> = { + day: { + min: 1, + max: (root) => { + const segmentMonthValue = root.segmentValues.month; + const placeholder = root.value.current ?? root.placeholder.current; + return segmentMonthValue + ? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) })) + : getDaysInMonth(placeholder); + }, + cycle: 1, + padZero: true, + }, + month: { + min: 1, + max: 12, + cycle: 1, + padZero: true, + getAnnouncement: (month, root) => + `${month} - ${root.formatter.fullMonth(toDate(root.placeholder.current.set({ month })))}`, + }, + year: { + min: 1, + max: 9999, + cycle: 1, + padZero: false, + }, + hour: { + min: (root) => (root.hourCycle.current === 12 ? 1 : 0), + max: (root) => { + if (root.hourCycle.current === 24) return 23; + if ("dayPeriod" in root.segmentValues && root.segmentValues.dayPeriod !== null) + return 12; + return 23; + }, + cycle: 1, + canBeZero: true, + padZero: true, + }, + minute: { + min: 0, + max: 59, + cycle: 1, + canBeZero: true, + padZero: true, + }, + second: { + min: 0, + max: 59, + cycle: 1, + canBeZero: true, + padZero: true, + }, +}; + export type DateFieldRootStateProps = WritableBoxedValues<{ value: DateValue | undefined; placeholder: DateValue; @@ -418,13 +492,15 @@ export class DateFieldRootState { return inferred; }); + dateRef = $derived.by(() => this.value.current ?? this.placeholder.current); + allSegmentContent = $derived.by(() => createContent({ segmentValues: this.segmentValues, formatter: this.formatter, locale: this.locale.current, granularity: this.inferredGranularity, - dateRef: this.placeholder.current, + dateRef: this.dateRef, hideTimeZone: this.hideTimeZone.current, hourCycle: this.hourCycle.current, }) @@ -621,10 +697,13 @@ type DateFieldInputStateProps = WithRefProps & }>; export class DateFieldInputState { - constructor( - readonly opts: DateFieldInputStateProps, - readonly root: DateFieldRootState - ) { + readonly opts: DateFieldInputStateProps; + readonly root: DateFieldRootState; + + constructor(opts: DateFieldInputStateProps, root: DateFieldRootState) { + this.opts = opts; + this.root = root; + $effect(() => { this.root.setName(this.opts.name.current); }); @@ -658,14 +737,16 @@ export class DateFieldInputState { }) as const ); } - class DateFieldHiddenInputState { + readonly root: DateFieldRootState; shouldRender = $derived.by(() => this.root.name !== ""); isoValue = $derived.by(() => this.root.value.current ? this.root.value.current.toString() : "" ); - constructor(readonly root: DateFieldRootState) {} + constructor(root: DateFieldRootState) { + this.root = root; + } props = $derived.by(() => { return { @@ -679,10 +760,12 @@ class DateFieldHiddenInputState { type DateFieldLabelStateProps = WithRefProps; class DateFieldLabelState { - constructor( - readonly opts: DateFieldLabelStateProps, - readonly root: DateFieldRootState - ) { + readonly opts: DateFieldLabelStateProps; + readonly root: DateFieldRootState; + + constructor(opts: DateFieldLabelStateProps, root: DateFieldRootState) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); useRefById({ @@ -712,263 +795,327 @@ class DateFieldLabelState { ); } -type DateFieldDaySegmentStateProps = WithRefProps; - -class DateFieldDaySegmentState { - #announcer: Announcer; - - constructor( - readonly opts: DateFieldDaySegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; +// Base class for numeric segments +abstract class BaseNumericSegmentState { + readonly opts: WithRefProps; + readonly root: DateFieldRootState; + readonly announcer: Announcer; + readonly part: string; + readonly config: SegmentConfig; + + constructor(opts: WithRefProps, root: DateFieldRootState, part: string, config: SegmentConfig) { + this.opts = opts; + this.root = root; + this.part = part; + this.config = config; + this.announcer = root.announcer; this.onkeydown = this.onkeydown.bind(this); this.onfocusout = this.onfocusout.bind(this); useRefById(opts); } + #getMax(): number { + return typeof this.config.max === "function" ? this.config.max(this.root) : this.config.max; + } + + #getMin(): number { + return typeof this.config.min === "function" ? this.config.min(this.root) : this.config.min; + } + + #getAnnouncement(value: number): string | number { + if (this.config.getAnnouncement) { + return this.config.getAnnouncement(value, this.root); + } + return value; + } + + #formatValue(value: number, forDisplay = true): string { + const str = String(value); + if (forDisplay && this.config.padZero && str.length === 1) { + return `0${value}`; + } + return str; + } + onkeydown(e: BitsKeyboardEvent) { - const placeholder = this.root.placeholder.current; + const placeholder = this.root.value.current ?? this.root.placeholder.current; if (e.ctrlKey || e.metaKey || this.root.disabled.current) return; - if (e.key !== kbd.TAB) e.preventDefault(); - if (!isAcceptableSegmentKey(e.key)) return; - const segmentMonthValue = this.root.segmentValues.month; + // Special check for time segments + if ( + (this.part === "hour" || this.part === "minute" || this.part === "second") && + !(this.part in placeholder) + ) + return; - const daysInMonth = segmentMonthValue - ? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) })) - : getDaysInMonth(placeholder); + if (e.key !== kbd.TAB) e.preventDefault(); + if (!isAcceptableSegmentKey(e.key)) return; if (isArrowUp(e.key)) { - this.root.updateSegment("day", (prev) => { - if (prev === null) { - const next = placeholder.day; - this.#announcer.announce(next); - if (next < 10) return `0${next}`; - return `${next}`; - } - const next = placeholder.set({ day: Number.parseInt(prev) }).cycle("day", 1).day; - this.#announcer.announce(next); - if (next < 10) return `0${next}`; - return `${next}`; - }); + this.#handleArrowUp(placeholder); return; } + if (isArrowDown(e.key)) { - this.root.updateSegment("day", (prev) => { - if (prev === null) { - const next = placeholder.day; - this.#announcer.announce(next); - if (next < 10) return `0${next}`; - return `${next}`; - } - const next = placeholder.set({ day: Number.parseInt(prev) }).cycle("day", -1).day; + this.#handleArrowDown(placeholder); + return; + } - this.#announcer.announce(next); - if (next < 10) return `0${next}`; - return `${next}`; - }); + if (isNumberString(e.key)) { + this.#handleNumberKey(e); return; } - const fieldNode = this.root.getFieldNode(); + if (isBackspace(e.key)) { + this.#handleBackspace(e); + return; + } - if (isNumberString(e.key)) { - const num = Number.parseInt(e.key); - let moveToNext = false; - this.root.updateSegment("day", (prev) => { - const max = daysInMonth; - const maxStart = Math.floor(max / 10); - const numIsZero = num === 0; - - /** - * If the user has left the segment, we want to reset the - * `prev` value so that we can start the segment over again - * when the user types a number. - */ - if (this.root.states.day.hasLeftFocus) { - prev = null; - this.root.states.day.hasLeftFocus = false; - } + if (isSegmentNavigationKey(e.key)) { + handleSegmentNavigation(e, this.root.getFieldNode()); + } + } - /** - * We are starting over in the segment if prev is null, which could - * happen in one of two scenarios: - * - the user has left the segment and then comes back to it - * - the segment was empty and the user begins typing a number - */ - if (prev === null) { - /** - * If the user types a 0 as the first number, we want - * to keep track of that so that when they type the next - * number, we can move to the next segment. - */ - if (numIsZero) { - this.root.states.day.lastKeyZero = true; - this.#announcer.announce("0"); - return "0"; - } + #handleArrowUp(placeholder: DateValue) { + const stateKey = this.part as keyof typeof this.root.states; + if (stateKey in this.root.states) { + this.root.states[stateKey].hasLeftFocus = false; + } - /////////////////////////// + // @ts-expect-error this is a part + this.root.updateSegment(this.part, (prev: string | null) => { + if (prev === null) { + const next = placeholder[this.part as keyof DateValue]; + this.announcer.announce(this.#getAnnouncement(next as number)); + return this.#formatValue(next as number); + } + const current = placeholder.set({ + [this.part]: Number.parseInt(prev), + }); + // @ts-expect-error this is a part + const next = current.cycle(this.part, this.config.cycle)[this.part as keyof DateValue]; + this.announcer.announce(this.#getAnnouncement(next as number)); + return this.#formatValue(next as number); + }); + } - /** - * If the last key was a 0, or if the first number is - * greater than the max start digit (0-3 in most cases), then - * we want to move to the next segment, since it's not possible - * to continue typing a valid number in this segment. - */ - if (this.root.states.day.lastKeyZero || num > maxStart) { - moveToNext = true; - } + #handleArrowDown(placeholder: DateValue) { + const stateKey = this.part as keyof typeof this.root.states; + if (stateKey in this.root.states) { + this.root.states[stateKey].hasLeftFocus = false; + } - this.root.states.day.lastKeyZero = false; + // @ts-expect-error this is a part + this.root.updateSegment(this.part, (prev: string | null) => { + if (prev === null) { + const next = placeholder[this.part as keyof DateValue]; + this.announcer.announce(this.#getAnnouncement(next as number)); + return this.#formatValue(next as number); + } + const current = placeholder.set({ + [this.part]: Number.parseInt(prev), + }); + // @ts-expect-error this is a part + const next = current.cycle(this.part, -this.config.cycle)[this.part as keyof DateValue]; + this.announcer.announce(this.#getAnnouncement(next as number)); + return this.#formatValue(next as number); + }); + } - /** - * If we're moving to the next segment and the number is less than - * two digits, we want to announce the number and return it with a - * leading zero to follow the placeholder format of `MM/DD/YYYY`. - */ - if (moveToNext && String(num).length === 1) { - this.#announcer.announce(num); - return `0${num}`; + #handleNumberKey(e: BitsKeyboardEvent) { + const num = Number.parseInt(e.key); + let moveToNext = false; + const max = this.#getMax(); + const maxStart = Math.floor(max / 10); + const numIsZero = num === 0; + const stateKey = this.part as keyof typeof this.root.states; + + // @ts-expect-error this is a part + this.root.updateSegment(this.part, (prev: string | null) => { + // Check if user has left focus + if (stateKey in this.root.states && this.root.states[stateKey].hasLeftFocus) { + prev = null; + this.root.states[stateKey].hasLeftFocus = false; + } + + // Starting fresh + if (prev === null) { + if (numIsZero) { + if (stateKey in this.root.states) { + this.root.states[stateKey].lastKeyZero = true; } + this.announcer.announce("0"); + return "0"; + } - /** - * If none of the above conditions are met, then we can just - * return the number as the segment value and continue typing - * in this segment. - */ - return `${num}`; + if ( + stateKey in this.root.states && + (this.root.states[stateKey].lastKeyZero || num > maxStart) + ) { + moveToNext = true; } - /** - * If the number of digits is 2, or if the total with the existing digit - * and the pressed digit is greater than the maximum value for this - * month, then we will reset the segment as if the user had pressed the - * backspace key and then typed the number. - */ - const total = Number.parseInt(prev + num.toString()); - - if (this.root.states.day.lastKeyZero) { - /** - * If the new number is not 0, then we reset the lastKeyZero state and - * move to the next segment, returning the new number with a leading 0. - */ - if (num !== 0) { - moveToNext = true; - this.root.states.day.lastKeyZero = false; - return `0${num}`; - } + if (stateKey in this.root.states) { + this.root.states[stateKey].lastKeyZero = false; + } - /** - * If the new number is 0, then we simply return the previous value, since - * they didn't actually type a new number. - */ - return prev; + if (moveToNext && String(num).length === 1) { + this.announcer.announce(num); + return `0${num}`; } - /** - * If the total is greater than the max day value possible for this month, then - * we want to move to the next segment, trimming the first digit from the total, - * replacing it with a 0. - */ - if (total > max) { + return `${num}`; + } + + // Handle special cases for segments with lastKeyZero tracking + if (stateKey in this.root.states && this.root.states[stateKey].lastKeyZero) { + if (num !== 0) { moveToNext = true; + this.root.states[stateKey].lastKeyZero = false; return `0${num}`; } - /** - * If the total has two digits and is less than or equal to the max day value, - * we will move to the next segment and return the total as the segment value. - */ + // Special handling for hour segment with 24-hour cycle + if (this.part === "hour" && num === 0 && this.root.hourCycle.current === 24) { + moveToNext = true; + this.root.states[stateKey].lastKeyZero = false; + return `00`; + } + + // Special handling for minute/second segments + if ((this.part === "minute" || this.part === "second") && num === 0) { + moveToNext = true; + this.root.states[stateKey].lastKeyZero = false; + return "00"; + } + + return prev; + } + + const total = Number.parseInt(prev + num.toString()); + + if (total > max) { moveToNext = true; - return `${total}`; - }); + return `0${num}`; + } + + moveToNext = true; + return `${total}`; + }); + + if (moveToNext) { + moveToNextSegment(e, this.root.getFieldNode()); + } + } - if (moveToNext) { - moveToNextSegment(e, fieldNode); + #handleBackspace(e: BitsKeyboardEvent) { + const stateKey = this.part as keyof typeof this.root.states; + if (stateKey in this.root.states) { + this.root.states[stateKey].hasLeftFocus = false; + } + + let moveToPrev = false; + // @ts-expect-error this is a part + this.root.updateSegment(this.part, (prev: string | null) => { + if (prev === null) { + moveToPrev = true; + this.announcer.announce(null); + return null; + } + + if (prev.length === 2 && prev.startsWith("0")) { + this.announcer.announce(null); + return null; } + + const str = prev.toString(); + if (str.length === 1) { + this.announcer.announce(null); + return null; + } + + const next = Number.parseInt(str.slice(0, -1)); + this.announcer.announce(this.#getAnnouncement(next)); + return `${next}`; + }); + + if (moveToPrev) { + moveToPrevSegment(e, this.root.getFieldNode()); } + } - if (isBackspace(e.key)) { - let moveToPrev = false; - this.root.updateSegment("day", (prev) => { - this.root.states.day.hasLeftFocus = false; - if (prev === null) { - moveToPrev = true; - return null; - } - if (prev.length === 2 && prev.startsWith("0")) { - return null; + onfocusout(_: BitsFocusEvent) { + const stateKey = this.part as keyof typeof this.root.states; + if (stateKey in this.root.states) { + this.root.states[stateKey].hasLeftFocus = true; + } + + // Pad with zero if needed + if (this.config.padZero) { + // @ts-expect-error this is a part + this.root.updateSegment(this.part, (prev: string | null) => { + if (prev && prev.length === 1) { + return `0${prev}`; } - const str = prev.toString(); - if (str.length === 1) return null; - return str.slice(0, -1); + return prev; }); + } + } - if (moveToPrev) { - moveToPrevSegment(e, fieldNode); - } + getSegmentProps() { + const segmentValues = this.root.segmentValues; + const placeholder = this.root.placeholder.current; + const isEmpty = segmentValues[this.part as keyof SegmentValueObj] === null; + + let date = placeholder; + if (segmentValues[this.part as keyof SegmentValueObj]) { + date = placeholder.set({ + [this.part]: Number.parseInt( + segmentValues[this.part as keyof SegmentValueObj] as string + ), + }); } - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, fieldNode); + const valueNow = date[this.part as keyof DateValue] as number; + const valueMin = this.#getMin(); + const valueMax = this.#getMax(); + let valueText = isEmpty ? "Empty" : `${valueNow}`; + + // Special handling for hour segment with dayPeriod + if (this.part === "hour" && "dayPeriod" in segmentValues && segmentValues.dayPeriod) { + valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod}`; } - } - onfocusout(_: BitsFocusEvent) { - this.root.states.day.hasLeftFocus = true; - this.root.updateSegment("month", (prev) => { - if (prev && prev.length === 1) { - return `0${prev}`; - } - return prev; - }); + return { + "aria-label": `${this.part}, `, + "aria-valuemin": valueMin, + "aria-valuemax": valueMax, + "aria-valuenow": valueNow, + "aria-valuetext": valueText, + }; } props = $derived.by(() => { - const date = this.root.segmentValues.day - ? this.root.placeholder.current.set({ - day: Number.parseInt(this.root.segmentValues.day), - }) - : this.root.placeholder.current; - return { ...this.root.sharedSegmentAttrs, id: this.opts.id.current, - "aria-label": "day,", - "aria-valuemin": 1, - "aria-valuemax": getDaysInMonth(toDate(date)), - "aria-valuenow": date.day, - "aria-valuetext": this.root.segmentValues.day === null ? "Empty" : `${date.day}`, + ...this.getSegmentProps(), onkeydown: this.onkeydown, onfocusout: this.onfocusout, onclick: this.root.handleSegmentClick, - ...this.root.getBaseSegmentAttrs("day", this.opts.id.current), + ...this.root.getBaseSegmentAttrs(this.part as SegmentPart, this.opts.id.current), }; }); } -type DateFieldMonthSegmentStateProps = WithRefProps; - -class DateFieldMonthSegmentState { - #announcer: Announcer; - - constructor( - readonly opts: DateFieldMonthSegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; - - this.onkeydown = this.onkeydown.bind(this); - this.onfocusout = this.onfocusout.bind(this); - - useRefById(opts); - } +// Year segment needs special handling +class DateFieldYearSegmentState extends BaseNumericSegmentState { + #pressedKeys: string[] = []; + #backspaceCount = 0; - #getAnnouncement(month: number) { - return `${month} - ${this.root.formatter.fullMonth(toDate(this.root.placeholder.current.set({ month })))}`; + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "year", SEGMENT_CONFIGS.year); } onkeydown(e: BitsKeyboardEvent) { @@ -976,1240 +1123,210 @@ class DateFieldMonthSegmentState { if (e.key !== kbd.TAB) e.preventDefault(); if (!isAcceptableSegmentKey(e.key)) return; - const max = 12; - if (isArrowUp(e.key)) { - this.root.updateSegment("month", (prev) => { - if (prev === null) { - const next = this.root.placeholder.current.month; - this.#announcer.announce(this.#getAnnouncement(next)); - - if (String(next).length === 1) { - return `0${next}`; - } - - return `${next}`; - } - const next = this.root.placeholder.current - .set({ month: Number.parseInt(prev) }) - .cycle("month", 1).month; - this.#announcer.announce(this.#getAnnouncement(next)); - if (String(next).length === 1) { - return `0${next}`; - } - return `${next}`; - }); + this.#resetBackspaceCount(); + super.onkeydown(e); return; } if (isArrowDown(e.key)) { - this.root.updateSegment("month", (prev) => { - if (prev === null) { - const next = this.root.placeholder.current.month; - this.#announcer.announce(this.#getAnnouncement(next)); - if (String(next).length === 1) { - return `0${next}`; - } - return `${next}`; - } - const next = this.root.placeholder.current - .set({ month: Number.parseInt(prev) }) - .cycle("month", -1).month; - this.#announcer.announce(this.#getAnnouncement(next)); - if (String(next).length === 1) { - return `0${next}`; - } - return `${next}`; - }); + this.#resetBackspaceCount(); + super.onkeydown(e); return; } if (isNumberString(e.key)) { - const num = Number.parseInt(e.key); - let moveToNext = false; - - this.root.updateSegment("month", (prev) => { - const maxStart = Math.floor(max / 10); - const numIsZero = num === 0; - - /** - * If the user has left the segment, we want to reset the - * `prev` value so that we can start the segment over again - * when the user types a number. - */ - if (this.root.states.month.hasLeftFocus) { - prev = null; - this.root.states.month.hasLeftFocus = false; - } + this.#handleYearNumberKey(e); + return; + } - /** - * We are starting over in the segment if prev is null, which could - * happen in one of two scenarios: - * - the user has left the segment and then comes back to it - * - the segment was empty and the user begins typing a number - */ - if (prev === null) { - /** - * If the user types a 0 as the first number, we want - * to keep track of that so that when they type the next - * number, we can move to the next segment. - */ - if (numIsZero) { - this.root.states.month.lastKeyZero = true; - this.#announcer.announce("0"); - return "0"; - } + if (isBackspace(e.key)) { + this.#handleYearBackspace(e); + return; + } + + if (isSegmentNavigationKey(e.key)) { + handleSegmentNavigation(e, this.root.getFieldNode()); + } + } - /////////////////////////// + #resetBackspaceCount() { + this.#backspaceCount = 0; + } - /** - * If the last key was a 0, or if the first number is - * greater than the max start digit (0-3 in most cases), then - * we want to move to the next segment, since it's not possible - * to continue typing a valid number in this segment. - */ - if (this.root.states.month.lastKeyZero || num > maxStart) { - moveToNext = true; - } + #incrementBackspaceCount() { + this.#backspaceCount++; + } - this.root.states.month.lastKeyZero = false; + #handleYearNumberKey(e: BitsKeyboardEvent) { + this.#pressedKeys.push(e.key); + let moveToNext = false; + const num = Number.parseInt(e.key); - /** - * If we're moving to the next segment and the number is less than - * two digits, we want to announce the number and return it with a - * leading zero to follow the placeholder format of `MM/DD/YYYY`. - */ - if (moveToNext && String(num).length === 1) { - this.#announcer.announce(num); - return `0${num}`; - } + this.root.updateSegment("year", (prev) => { + if (this.root.states.year.hasLeftFocus) { + prev = null; + this.root.states.year.hasLeftFocus = false; + } - /** - * If none of the above conditions are met, then we can just - * return the number as the segment value and continue typing - * in this segment. - */ - return `${num}`; - } + if (prev === null) { + this.announcer.announce(num); + return `000${num}`; + } - /** - * If the number of digits is 2, or if the total with the existing digit - * and the pressed digit is greater than the maximum value for this - * month, then we will reset the segment as if the user had pressed the - * backspace key and then typed the number. - */ - const total = Number.parseInt(prev + num.toString()); - - if (this.root.states.month.lastKeyZero) { - /** - * If the new number is not 0, then we reset the lastKeyZero state and - * move to the next segment, returning the new number with a leading 0. - */ - if (num !== 0) { - moveToNext = true; - this.root.states.month.lastKeyZero = false; - return `0${num}`; - } + const str = prev.toString() + num.toString(); + const mergedInt = Number.parseInt(str); + const mergedIntDigits = String(mergedInt).length; - /** - * If the new number is 0, then we simply return the previous value, since - * they didn't actually type a new number. - */ - return prev; + if (mergedIntDigits < 4) { + if ( + this.#backspaceCount > 0 && + this.#pressedKeys.length <= this.#backspaceCount && + str.length <= 4 + ) { + this.announcer.announce(mergedInt); + return str; } - /** - * If the total is greater than the max day value possible for this month, then - * we want to move to the next segment, trimming the first digit from the total, - * replacing it with a 0. - */ - if (total > max) { - moveToNext = true; - return `0${num}`; - } + this.announcer.announce(mergedInt); + return prependYearZeros(mergedInt); + } - /** - * If the total has two digits and is less than or equal to the max day value, - * we will move to the next segment and return the total as the segment value. - */ - moveToNext = true; - return `${total}`; - }); + this.announcer.announce(mergedInt); + moveToNext = true; - if (moveToNext) { - moveToNextSegment(e, this.root.getFieldNode()); + const mergedIntStr = `${mergedInt}`; + + if (mergedIntStr.length > 4) { + return mergedIntStr.slice(0, 4); } - } - if (isBackspace(e.key)) { - this.root.states.month.hasLeftFocus = false; - let moveToPrev = false; - this.root.updateSegment("month", (prev) => { - if (prev === null) { - this.#announcer.announce(null); - moveToPrev = true; - return null; - } + return mergedIntStr; + }); - if (prev.length === 2 && prev.startsWith("0")) { - this.#announcer.announce(null); - return null; - } + if (this.#pressedKeys.length === 4 || this.#pressedKeys.length === this.#backspaceCount) { + moveToNext = true; + } - const str = prev.toString(); - if (str.length === 1) { - this.#announcer.announce(null); - return null; - } - const next = Number.parseInt(str.slice(0, -1)); - this.#announcer.announce(this.#getAnnouncement(next)); - return `${next}`; - }); + if (moveToNext) { + moveToNextSegment(e, this.root.getFieldNode()); + } + } + + #handleYearBackspace(e: BitsKeyboardEvent) { + this.#pressedKeys = []; + this.#incrementBackspaceCount(); + let moveToPrev = false; - if (moveToPrev) { - moveToPrevSegment(e, this.root.getFieldNode()); + this.root.updateSegment("year", (prev) => { + this.root.states.year.hasLeftFocus = false; + if (prev === null) { + moveToPrev = true; + this.announcer.announce(null); + return null; } - } + const str = prev.toString(); + if (str.length === 1) { + this.announcer.announce(null); + return null; + } + const next = str.slice(0, -1); + this.announcer.announce(next); - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, this.root.getFieldNode()); + return `${next}`; + }); + + if (moveToPrev) { + moveToPrevSegment(e, this.root.getFieldNode()); } } onfocusout(_: BitsFocusEvent) { - this.root.states.month.hasLeftFocus = true; - this.root.updateSegment("month", (prev) => { - if (prev && prev.length === 1) { - return `0${prev}`; + this.root.states.year.hasLeftFocus = true; + this.#pressedKeys = []; + this.#resetBackspaceCount(); + this.root.updateSegment("year", (prev) => { + if (prev && prev.length !== 4) { + return prependYearZeros(Number.parseInt(prev)); } return prev; }); } +} - props = $derived.by(() => { - const date = this.root.segmentValues.month - ? this.root.placeholder.current.set({ - month: Number.parseInt(this.root.segmentValues.month), - }) - : this.root.placeholder.current; - - return { - ...this.root.sharedSegmentAttrs, - id: this.opts.id.current, - "aria-label": "month, ", - contenteditable: "true", - "aria-valuemin": 1, - "aria-valuemax": 12, - "aria-valuenow": date.month, - "aria-valuetext": - this.root.segmentValues.month === null - ? "Empty" - : `${date.month} - ${this.root.formatter.fullMonth(toDate(date))}`, - onkeydown: this.onkeydown, - onfocusout: this.onfocusout, - onclick: this.root.handleSegmentClick, - ...this.root.getBaseSegmentAttrs("month", this.opts.id.current), - } as const; - }); +// Create segment states using the base class +class DateFieldDaySegmentState extends BaseNumericSegmentState { + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "day", SEGMENT_CONFIGS.day); + } } -type DateFieldYearSegmentStateProps = WithRefProps; +class DateFieldMonthSegmentState extends BaseNumericSegmentState { + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "month", SEGMENT_CONFIGS.month); + } +} -class DateFieldYearSegmentState { - #announcer: Announcer; +class DateFieldHourSegmentState extends BaseNumericSegmentState { + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "hour", SEGMENT_CONFIGS.hour); + } - /** - * When typing a year, a user may want to type `0090` to represent `90`. - * So we track the keys they've pressed in this specific interaction to - * determine once they've pressed four to move to the next segment. - * - * On `focusout` this is reset to an empty array. - */ - #pressedKeys: string[] = []; + // Override to handle special hour logic + onkeydown(e: BitsKeyboardEvent) { + // Add special handling for hour display with dayPeriod + if (isNumberString(e.key)) { + const oldUpdateSegment = this.root.updateSegment.bind(this.root); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.root.updateSegment = (part: any, cb: any) => { + const result = oldUpdateSegment(part, cb); + + // After updating hour, check if we need to display "12" instead of "0" + if (part === "hour" && "hour" in this.root.segmentValues) { + const hourValue = this.root.segmentValues.hour; + if ( + hourValue === "0" && + this.root.dayPeriodNode && + this.root.hourCycle.current !== 24 + ) { + this.root.segmentValues.hour = "12"; + } + } - /** - * When a user re-enters a completed segment and backspaces, if they have - * leading zeroes on the year, they won't automatically be sent to the next - * segment even if they complete all 4 digits. This is because the leading zeroes - * get stripped out for the digit count. - * - * This lets us keep track of how many times the user has backspaced in a row - * to determine how many additional key presses should move them to the next segment. - * - * For example, if the user has `0098` in the year segment and backspaces once, - * the segment will contain `009` and if the user types `7`, the segment should - * contain `0097` and move to the next segment. - * - * If the segment contains `0100` and the user backspaces twice, the segment will - * contain `01` and if the user types `2`, the segment should contain `012` and - * it should _not_ move to the next segment until the user types another digit. - */ - #backspaceCount = 0; + return result; + }; + } - constructor( - readonly opts: DateFieldYearSegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; - this.onkeydown = this.onkeydown.bind(this); - this.onfocusout = this.onfocusout.bind(this); + super.onkeydown(e); - useRefById(opts); + // Restore original updateSegment + this.root.updateSegment = this.root.updateSegment.bind(this.root); } +} - #resetBackspaceCount() { - this.#backspaceCount = 0; +class DateFieldMinuteSegmentState extends BaseNumericSegmentState { + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "minute", SEGMENT_CONFIGS.minute); } +} - #incrementBackspaceCount() { - this.#backspaceCount++; - } - - onkeydown(e: BitsKeyboardEvent) { - const placeholder = this.root.placeholder.current; - if (e.ctrlKey || e.metaKey || this.root.disabled.current) return; - if (e.key !== kbd.TAB) e.preventDefault(); - if (!isAcceptableSegmentKey(e.key)) return; - - if (isArrowUp(e.key)) { - this.#resetBackspaceCount(); - this.root.updateSegment("year", (prev) => { - if (prev === null) { - const next = placeholder.year; - this.#announcer.announce(next); - return `${next}`; - } - const next = placeholder.set({ year: Number.parseInt(prev) }).cycle("year", 1).year; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - if (isArrowDown(e.key)) { - this.#resetBackspaceCount(); - this.root.updateSegment("year", (prev) => { - if (prev === null) { - const next = placeholder.year; - this.#announcer.announce(next); - return `${next}`; - } - const next = placeholder - .set({ year: Number.parseInt(prev) }) - .cycle("year", -1).year; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isNumberString(e.key)) { - this.#pressedKeys.push(e.key); - let moveToNext = false; - const num = Number.parseInt(e.key); - this.root.updateSegment("year", (prev) => { - if (this.root.states.year.hasLeftFocus) { - prev = null; - this.root.states.year.hasLeftFocus = false; - } - - if (prev === null) { - this.#announcer.announce(num); - return `000${num}`; - } - - const str = prev.toString() + num.toString(); - const mergedInt = Number.parseInt(str); - const mergedIntDigits = String(mergedInt).length; - - if (mergedIntDigits < 4) { - /** - * If the user has backspaced and hasn't typed enough digits to make up - * for the amount of backspaces, then we want to keep them in the segment - * and not prepend any zeroes to the number. - */ - if ( - this.#backspaceCount > 0 && - this.#pressedKeys.length <= this.#backspaceCount && - str.length <= 4 - ) { - this.#announcer.announce(mergedInt); - return str; - } - - /** - * If the mergedInt is less than 4 digits and we haven't backspaced, - * then we want to prepend zeroes to the number to keep the format - * of `YYYY` - */ - this.#announcer.announce(mergedInt); - return prependYearZeros(mergedInt); - } - - this.#announcer.announce(mergedInt); - moveToNext = true; - - const mergedIntStr = `${mergedInt}`; - - if (mergedIntStr.length > 4) { - return mergedIntStr.slice(0, 4); - } - - return mergedIntStr; - }); - - if ( - this.#pressedKeys.length === 4 || - this.#pressedKeys.length === this.#backspaceCount - ) { - moveToNext = true; - } - - if (moveToNext) { - moveToNextSegment(e, this.root.getFieldNode()); - } - } - - if (isBackspace(e.key)) { - this.#pressedKeys = []; - this.#incrementBackspaceCount(); - let moveToPrev = false; - this.root.updateSegment("year", (prev) => { - this.root.states.year.hasLeftFocus = false; - if (prev === null) { - moveToPrev = true; - this.#announcer.announce(null); - return null; - } - const str = prev.toString(); - if (str.length === 1) { - this.#announcer.announce(null); - return null; - } - const next = str.slice(0, -1); - this.#announcer.announce(next); - - return `${next}`; - }); - - if (moveToPrev) { - moveToPrevSegment(e, this.root.getFieldNode()); - } - } - - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, this.root.getFieldNode()); - } - } - - onfocusout(_: BitsFocusEvent) { - this.root.states.year.hasLeftFocus = true; - this.#pressedKeys = []; - this.#resetBackspaceCount(); - this.root.updateSegment("year", (prev) => { - if (prev && prev.length !== 4) { - return prependYearZeros(Number.parseInt(prev)); - } - return prev; - }); +class DateFieldSecondSegmentState extends BaseNumericSegmentState { + constructor(opts: WithRefProps, root: DateFieldRootState) { + super(opts, root, "second", SEGMENT_CONFIGS.second); } - - props = $derived.by(() => { - const segmentValues = this.root.segmentValues; - const placeholder = this.root.placeholder.current; - const isEmpty = segmentValues.year === null; - const date = segmentValues.year - ? placeholder.set({ year: Number.parseInt(segmentValues.year) }) - : placeholder; - const valueMin = 1; - const valueMax = 9999; - const valueNow = date.year; - const valueText = isEmpty ? "Empty" : `${valueNow}`; - - return { - ...this.root.sharedSegmentAttrs, - id: this.opts.id.current, - "aria-label": "year, ", - "aria-valuemin": valueMin, - "aria-valuemax": valueMax, - "aria-valuenow": valueNow, - "aria-valuetext": valueText, - onkeydown: this.onkeydown, - onclick: this.root.handleSegmentClick, - onfocusout: this.onfocusout, - ...this.root.getBaseSegmentAttrs("year", this.opts.id.current), - }; - }); -} - -type DateFieldHourSegmentStateProps = WithRefProps; - -class DateFieldHourSegmentState { - #announcer: Announcer; - - constructor( - readonly opts: DateFieldHourSegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; - this.onkeydown = this.onkeydown.bind(this); - this.onfocusout = this.onfocusout.bind(this); - - useRefById(opts); - } - - onkeydown(e: BitsKeyboardEvent) { - const placeholder = this.root.placeholder.current; - if (e.ctrlKey || e.metaKey || this.root.disabled.current || !("hour" in placeholder)) - return; - if (e.key !== kbd.TAB) e.preventDefault(); - if (!isAcceptableSegmentKey(e.key)) return; - - const hourCycle = this.root.hourCycle.current; - - if (isArrowUp(e.key)) { - this.root.updateSegment("hour", (prev) => { - if (prev === null) { - const next = placeholder.cycle("hour", 1, { hourCycle }).hour; - this.#announcer.announce(next); - return `${next}`; - } - const next = placeholder - .set({ hour: Number.parseInt(prev) }) - .cycle("hour", 1, { hourCycle }).hour; - - if ( - next === 0 && - "dayPeriod" in this.root.segmentValues && - this.root.segmentValues.dayPeriod !== null && - this.root.hourCycle.current !== 24 - ) { - this.#announcer.announce("12"); - return "12"; - } - - if (next === 0 && this.root.hourCycle.current === 24) { - this.#announcer.announce("00"); - return "00"; - } - - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isArrowDown(e.key)) { - this.root.updateSegment("hour", (prev) => { - if (prev === null) { - const next = placeholder.cycle("hour", -1, { hourCycle }).hour; - this.#announcer.announce(next); - return `${next}`; - } - const next = placeholder - .set({ hour: Number.parseInt(prev) }) - .cycle("hour", -1, { hourCycle }).hour; - - if ( - next === 0 && - "dayPeriod" in this.root.segmentValues && - this.root.segmentValues.dayPeriod !== null && - this.root.hourCycle.current !== 24 - ) { - this.#announcer.announce("12"); - return "12"; - } - - if (next === 0 && this.root.hourCycle.current === 24) { - this.#announcer.announce("00"); - return "00"; - } - - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isNumberString(e.key)) { - const num = Number.parseInt(e.key); - const max = - this.root.hourCycle.current === 24 - ? 23 - : "dayPeriod" in this.root.segmentValues && - this.root.segmentValues.dayPeriod !== null - ? 12 - : 23; - const maxStart = Math.floor(max / 10); - let moveToNext = false; - const numIsZero = num === 0; - this.root.updateSegment("hour", (prev) => { - /** - * If the user has left the segment, we want to reset the - * `prev` value so that we can start the segment over again - * when the user types a number. - */ - if (this.root.states.hour.hasLeftFocus) { - prev = null; - this.root.states.hour.hasLeftFocus = false; - } - - /** - * We are starting over in the segment if prev is null, which could - * happen in one of two scenarios: - * - the user has left the segment and then comes back to it - * - the segment was empty and the user begins typing a number - */ - if (prev === null) { - /** - * If the user types a 0 as the first number, we want - * to keep track of that so that when they type the next - * number, we can move to the next segment. - */ - if (numIsZero) { - this.root.states.hour.lastKeyZero = true; - this.#announcer.announce("0"); - return "0"; - } - - /////////////////////////// - - /** - * If the last key was a 0, or if the first number is - * greater than the max start digit (0-3 in most cases), then - * we want to move to the next segment, since it's not possible - * to continue typing a valid number in this segment. - */ - if (this.root.states.hour.lastKeyZero || num > maxStart) { - moveToNext = true; - } - - this.root.states.hour.lastKeyZero = false; - - /** - * If we're moving to the next segment and the number is less than - * two digits, we want to announce the number and return it with a - * leading zero to follow the placeholder format of `MM/DD/YYYY`. - */ - if (moveToNext && String(num).length === 1) { - this.#announcer.announce(num); - return `0${num}`; - } - - /** - * If none of the above conditions are met, then we can just - * return the number as the segment value and continue typing - * in this segment. - */ - return `${num}`; - } - - /** - * If the number of digits is 2, or if the total with the existing digit - * and the pressed digit is greater than the maximum value for this - * hour, then we will reset the segment as if the user had pressed the - * backspace key and then typed the number. - */ - const total = Number.parseInt(prev + num.toString()); - - if (this.root.states.hour.lastKeyZero) { - /** - * If the new number is not 0, then we reset the lastKeyZero state and - * move to the next segment, returning the new number with a leading 0. - */ - if (num !== 0) { - moveToNext = true; - this.root.states.hour.lastKeyZero = false; - return `0${num}`; - } - - /** - * If the new number is 0 and the hour cycle is set to 24, then we move - * to the next segment, returning the new number with a leading 0. - */ - if (num === 0 && this.root.hourCycle.current === 24) { - moveToNext = true; - this.root.states.hour.lastKeyZero = false; - return `0${num}`; - } - - /** - * If the new number is 0, then we simply return the previous value, since - * they didn't actually type a new number. - */ - return prev; - } - - /** - * If the total is greater than the max day value possible for this month, then - * we want to move to the next segment, trimming the first digit from the total, - * replacing it with a 0. - */ - if (total > max) { - moveToNext = true; - return `0${num}`; - } - - /** - * If the total has two digits and is less than or equal to the max day value, - * we will move to the next segment and return the total as the segment value. - */ - moveToNext = true; - return `${total}`; - }); - - if (moveToNext) { - moveToNextSegment(e, this.root.getFieldNode()); - } - } - - if (isBackspace(e.key)) { - this.root.states.hour.hasLeftFocus = false; - let moveToPrev = false; - this.root.updateSegment("hour", (prev) => { - if (prev === null) { - this.#announcer.announce(null); - moveToPrev = true; - return null; - } - const str = prev.toString(); - if (str.length === 1) { - this.#announcer.announce(null); - return null; - } - const next = Number.parseInt(str.slice(0, -1)); - this.#announcer.announce(next); - return `${next}`; - }); - - if (moveToPrev) { - moveToPrevSegment(e, this.root.getFieldNode()); - } - } - - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, this.root.getFieldNode()); - } - } - - onfocusout(_: BitsFocusEvent) { - this.root.states.hour.hasLeftFocus = true; - } - - props = $derived.by(() => { - const segmentValues = this.root.segmentValues; - const hourCycle = this.root.hourCycle.current; - const placeholder = this.root.placeholder.current; - if (!("hour" in segmentValues) || !("hour" in placeholder)) return {}; - const isEmpty = segmentValues.hour === null; - const date = segmentValues.hour - ? placeholder.set({ hour: Number.parseInt(segmentValues.hour) }) - : placeholder; - const valueMin = hourCycle === 12 ? 1 : 0; - const valueMax = hourCycle === 12 ? 12 : 23; - const valueNow = date.hour; - const valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod ?? ""}`; - - return { - ...this.root.sharedSegmentAttrs, - id: this.opts.id.current, - "aria-label": "hour, ", - "aria-valuemin": valueMin, - "aria-valuemax": valueMax, - "aria-valuenow": valueNow, - "aria-valuetext": valueText, - onkeydown: this.onkeydown, - onfocusout: this.onfocusout, - onclick: this.root.handleSegmentClick, - ...this.root.getBaseSegmentAttrs("hour", this.opts.id.current), - }; - }); -} - -type DateFieldMinuteSegmentStateProps = WithRefProps; - -class DateFieldMinuteSegmentState { - #announcer: Announcer; - - constructor( - readonly opts: DateFieldMinuteSegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; - this.onkeydown = this.onkeydown.bind(this); - this.onfocusout = this.onfocusout.bind(this); - - useRefById(opts); - } - - onkeydown(e: BitsKeyboardEvent) { - const placeholder = this.root.placeholder.current; - if (e.ctrlKey || e.metaKey || this.root.disabled.current || !("minute" in placeholder)) - return; - if (e.key !== kbd.TAB) e.preventDefault(); - if (!isAcceptableSegmentKey(e.key)) return; - - const min = 0; - const max = 59; - - if (isArrowUp(e.key)) { - this.root.updateSegment("minute", (prev) => { - if (prev === null) { - this.#announcer.announce(min); - return `${min}`; - } - const next = placeholder - .set({ minute: Number.parseInt(prev) }) - .cycle("minute", 1).minute; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isArrowDown(e.key)) { - this.root.updateSegment("minute", (prev) => { - if (prev === null) { - this.#announcer.announce(max); - return `${max}`; - } - const next = placeholder - .set({ minute: Number.parseInt(prev) }) - .cycle("minute", -1).minute; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isNumberString(e.key)) { - const num = Number.parseInt(e.key); - let moveToNext = false; - const numIsZero = num === 0; - this.root.updateSegment("minute", (prev) => { - const maxStart = Math.floor(max / 10); - - /** - * If the user has left the segment, we want to reset the - * `prev` value so that we can start the segment over again - * when the user types a number. - */ - if (this.root.states.minute.hasLeftFocus) { - prev = null; - this.root.states.minute.hasLeftFocus = false; - } - - /** - * We are starting over in the segment if prev is null, which could - * happen in one of two scenarios: - * - the user has left the segment and then comes back to it - * - the segment was empty and the user begins typing a number - */ - if (prev === null) { - /** - * If the user types a 0 as the first number, we want - * to keep track of that so that when they type the next - * number, we can move to the next segment. - */ - if (numIsZero) { - this.root.states.minute.lastKeyZero = true; - this.#announcer.announce("0"); - return "0"; - } - - /////////////////////////// - - /** - * If the last key was a 0, or if the first number is - * greater than the max start digit (0-3 in most cases), then - * we want to move to the next segment, since it's not possible - * to continue typing a valid number in this segment. - */ - if (this.root.states.minute.lastKeyZero || num > maxStart) { - moveToNext = true; - } - - this.root.states.minute.lastKeyZero = false; - - /** - * If we're moving to the next segment and the number is less than - * two digits, we want to announce the number and return it with a - * leading zero to follow the placeholder format of `MM/DD/YYYY`. - */ - if (moveToNext && String(num).length === 1) { - this.#announcer.announce(num); - return `0${num}`; - } - - /** - * If none of the above conditions are met, then we can just - * return the number as the segment value and continue typing - * in this segment. - */ - return `${num}`; - } - - /** - * If the number of digits is 2, or if the total with the existing digit - * and the pressed digit is greater than the maximum value for this - * minute, then we will reset the segment as if the user had pressed the - * backspace key and then typed the number. - */ - const total = Number.parseInt(prev + num.toString()); - - if (this.root.states.minute.lastKeyZero) { - /** - * If the new number is not 0, then we reset the lastKeyZero state and - * move to the next segment, returning the new number with a leading 0. - */ - if (num !== 0) { - moveToNext = true; - this.root.states.minute.lastKeyZero = false; - return `0${num}`; - } - - /** - * If the new number is 0, then we simply return `00` since that is - * an acceptable minute value. - */ - moveToNext = true; - this.root.states.minute.lastKeyZero = false; - return "00"; - } - - /** - * If the total is greater than the max day value possible for this month, then - * we want to move to the next segment, trimming the first digit from the total, - * replacing it with a 0. - */ - if (total > max) { - moveToNext = true; - return `0${num}`; - } - - /** - * If the total has two digits and is less than or equal to the max day value, - * we will move to the next segment and return the total as the segment value. - */ - moveToNext = true; - return `${total}`; - }); - - if (moveToNext) { - moveToNextSegment(e, this.root.getFieldNode()); - } - return; - } - - if (isBackspace(e.key)) { - this.root.states.minute.hasLeftFocus = false; - let moveToPrev = false; - this.root.updateSegment("minute", (prev) => { - if (prev === null) { - moveToPrev = true; - this.#announcer.announce("Empty"); - return null; - } - const str = prev.toString(); - if (str.length === 1) { - this.#announcer.announce("Empty"); - return null; - } - const next = Number.parseInt(str.slice(0, -1)); - this.#announcer.announce(next); - return `${next}`; - }); - - if (moveToPrev) { - moveToPrevSegment(e, this.root.getFieldNode()); - } - return; - } - - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, this.root.getFieldNode()); - } - } - - onfocusout(_: BitsFocusEvent) { - this.root.states.minute.hasLeftFocus = true; - } - - props = $derived.by(() => { - const segmentValues = this.root.segmentValues; - const placeholder = this.root.placeholder.current; - - if (!("minute" in segmentValues) || !("minute" in placeholder)) return {}; - const isEmpty = segmentValues.minute === null; - const date = segmentValues.minute - ? placeholder.set({ minute: Number.parseInt(segmentValues.minute) }) - : placeholder; - const valueNow = date.minute; - const valueMin = 0; - const valueMax = 59; - const valueText = isEmpty ? "Empty" : `${valueNow}`; - - return { - ...this.root.sharedSegmentAttrs, - id: this.opts.id.current, - "aria-label": "minute, ", - "aria-valuemin": valueMin, - "aria-valuemax": valueMax, - "aria-valuenow": valueNow, - "aria-valuetext": valueText, - onkeydown: this.onkeydown, - onfocusout: this.onfocusout, - onclick: this.root.handleSegmentClick, - ...this.root.getBaseSegmentAttrs("minute", this.opts.id.current), - }; - }); -} - -type DateFieldSecondSegmentStateProps = WithRefProps; - -class DateFieldSecondSegmentState { - #announcer: Announcer; - - constructor( - readonly opts: DateFieldSecondSegmentStateProps, - readonly root: DateFieldRootState - ) { - this.#announcer = this.root.announcer; - - this.onkeydown = this.onkeydown.bind(this); - this.onfocusout = this.onfocusout.bind(this); - - useRefById(opts); - } - - onkeydown(e: BitsKeyboardEvent) { - const placeholder = this.root.placeholder.current; - if (e.ctrlKey || e.metaKey || this.root.disabled.current || !("second" in placeholder)) - return; - if (e.key !== kbd.TAB) e.preventDefault(); - if (!isAcceptableSegmentKey(e.key)) return; - - const min = 0; - const max = 59; - - if (isArrowUp(e.key)) { - this.root.updateSegment("second", (prev) => { - if (prev === null) { - this.#announcer.announce(min); - return `${min}`; - } - const next = placeholder - .set({ second: Number.parseInt(prev) }) - .cycle("second", 1).second; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isArrowDown(e.key)) { - this.root.updateSegment("second", (prev) => { - if (prev === null) { - this.#announcer.announce(max); - return `${max}`; - } - const next = placeholder - .set({ second: Number.parseInt(prev) }) - .cycle("second", -1).second; - this.#announcer.announce(next); - return `${next}`; - }); - return; - } - - if (isNumberString(e.key)) { - const num = Number.parseInt(e.key); - const numIsZero = num === 0; - let moveToNext = false; - this.root.updateSegment("second", (prev) => { - const maxStart = Math.floor(max / 10); - - /** - * If the user has left the segment, we want to reset the - * `prev` value so that we can start the segment over again - * when the user types a number. - */ - if (this.root.states.second.hasLeftFocus) { - prev = null; - this.root.states.second.hasLeftFocus = false; - } - - /** - * We are starting over in the segment if prev is null, which could - * happen in one of two scenarios: - * - the user has left the segment and then comes back to it - * - the segment was empty and the user begins typing a number - */ - if (prev === null) { - /** - * If the user types a 0 as the first number, we want - * to keep track of that so that when they type the next - * number, we can move to the next segment. - */ - if (numIsZero) { - this.root.states.second.lastKeyZero = true; - this.#announcer.announce("0"); - return "0"; - } - - /////////////////////////// - - /** - * If the last key was a 0, or if the first number is - * greater than the max start digit (0-3 in most cases), then - * we want to move to the next segment, since it's not possible - * to continue typing a valid number in this segment. - */ - if (this.root.states.second.lastKeyZero || num > maxStart) { - moveToNext = true; - } - - this.root.states.second.lastKeyZero = false; - - /** - * If we're moving to the next segment and the number is less than - * two digits, we want to announce the number and return it with a - * leading zero to follow the placeholder format of `MM/DD/YYYY`. - */ - if (moveToNext && String(num).length === 1) { - this.#announcer.announce(num); - return `0${num}`; - } - - /** - * If none of the above conditions are met, then we can just - * return the number as the segment value and continue typing - * in this segment. - */ - return `${num}`; - } - - /** - * If the number of digits is 2, or if the total with the existing digit - * and the pressed digit is greater than the maximum value for this - * second, then we will reset the segment as if the user had pressed the - * backspace key and then typed the number. - */ - const total = Number.parseInt(prev + num.toString()); - - if (this.root.states.second.lastKeyZero) { - /** - * If the new number is not 0, then we reset the lastKeyZero state and - * move to the next segment, returning the new number with a leading 0. - */ - if (num !== 0) { - moveToNext = true; - this.root.states.second.lastKeyZero = false; - return `0${num}`; - } - - /** - * If the new number is 0, then we simply return `00` since that is - * an acceptable second value. - */ - moveToNext = true; - this.root.states.second.lastKeyZero = false; - return "00"; - } - - /** - * If the total is greater than the max day value possible for this month, then - * we want to move to the next segment, trimming the first digit from the total, - * replacing it with a 0. - */ - if (total > max) { - moveToNext = true; - return `0${num}`; - } - - /** - * If the total has two digits and is less than or equal to the max day value, - * we will move to the next segment and return the total as the segment value. - */ - moveToNext = true; - return `${total}`; - }); - - if (moveToNext) { - moveToNextSegment(e, this.root.getFieldNode()); - } - } - - if (isBackspace(e.key)) { - this.root.states.second.hasLeftFocus = false; - let moveToPrev = false; - this.root.updateSegment("second", (prev) => { - if (prev === null) { - moveToPrev = true; - this.#announcer.announce(null); - return null; - } - const str = prev.toString(); - if (str.length === 1) { - this.#announcer.announce(null); - return null; - } - const next = Number.parseInt(str.slice(0, -1)); - this.#announcer.announce(next); - return `${next}`; - }); - - if (moveToPrev) { - moveToPrevSegment(e, this.root.getFieldNode()); - } - } - - if (isSegmentNavigationKey(e.key)) { - handleSegmentNavigation(e, this.root.getFieldNode()); - } - } - - onfocusout(_: BitsFocusEvent) { - this.root.states.second.hasLeftFocus = true; - } - - props = $derived.by(() => { - const segmentValues = this.root.segmentValues; - const placeholder = this.root.placeholder.current; - if (!("second" in segmentValues) || !("second" in placeholder)) return {}; - const isEmpty = segmentValues.second === null; - const date = segmentValues.second - ? placeholder.set({ second: Number.parseInt(segmentValues.second) }) - : placeholder; - const valueNow = date.second; - const valueMin = 0; - const valueMax = 59; - const valueText = isEmpty ? "Empty" : `${valueNow}`; - - return { - ...this.root.sharedSegmentAttrs, - id: this.opts.id.current, - "aria-label": "second, ", - "aria-valuemin": valueMin, - "aria-valuemax": valueMax, - "aria-valuenow": valueNow, - "aria-valuetext": valueText, - onkeydown: this.onkeydown, - onfocusout: this.onfocusout, - onclick: this.root.handleSegmentClick, - ...this.root.getBaseSegmentAttrs("second", this.opts.id.current), - }; - }); } +// Special segments that don't extend the base class type DateFieldDayPeriodSegmentStateProps = WithRefProps; class DateFieldDayPeriodSegmentState { + readonly opts: DateFieldDayPeriodSegmentStateProps; + readonly root: DateFieldRootState; #announcer: Announcer; - constructor( - readonly opts: DateFieldDayPeriodSegmentStateProps, - readonly root: DateFieldRootState - ) { + constructor(opts: DateFieldDayPeriodSegmentStateProps, root: DateFieldRootState) { + this.opts = opts; + this.root = root; this.#announcer = this.root.announcer; this.onkeydown = this.onkeydown.bind(this); @@ -2291,10 +1408,13 @@ class DateFieldDayPeriodSegmentState { type DateFieldLiteralSegmentStateProps = WithRefProps; class DateFieldDayLiteralSegmentState { - constructor( - readonly opts: DateFieldLiteralSegmentStateProps, - readonly root: DateFieldRootState - ) { + readonly opts: DateFieldLiteralSegmentStateProps; + readonly root: DateFieldRootState; + + constructor(opts: DateFieldLiteralSegmentStateProps, root: DateFieldRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -2309,10 +1429,12 @@ class DateFieldDayLiteralSegmentState { } class DateFieldTimeZoneSegmentState { - constructor( - readonly opts: DateFieldMinuteSegmentStateProps, - readonly root: DateFieldRootState - ) { + readonly opts: DateFieldLiteralSegmentStateProps; + readonly root: DateFieldRootState; + + constructor(opts: DateFieldLiteralSegmentStateProps, root: DateFieldRootState) { + this.opts = opts; + this.root = root; this.onkeydown = this.onkeydown.bind(this); useRefById(opts); diff --git a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte index 29099bd20..dfe79268c 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte +++ b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte @@ -9,6 +9,7 @@ import { useDateFieldRoot } from "$lib/bits/date-field/date-field.svelte.js"; import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { open = $bindable(false), @@ -34,7 +35,7 @@ disableDaysOutsideMonth = true, preventDeselect = false, pagedNavigation = false, - weekStartsOn = 0, + weekStartsOn, weekdayFormat = "narrow", isDateDisabled = () => false, fixedWeeks = false, @@ -50,10 +51,26 @@ defaultValue: value, }); - if (placeholder === undefined) { + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; placeholder = defaultPlaceholder; } + // SSR + handleDefaultPlaceholder(); + + /** + * Covers an edge case where when a spread props object is reassigned, + * the props are reset to their default values, which would make placeholder + * undefined which causes errors to be thrown. + */ + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + function onDateSelect() { if (closeOnDateSelect) { open = false; diff --git a/packages/bits-ui/src/lib/bits/date-picker/date-picker.svelte.ts b/packages/bits-ui/src/lib/bits/date-picker/date-picker.svelte.ts index 34766fdf0..5fa5a9b3b 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/date-picker.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-picker/date-picker.svelte.ts @@ -24,7 +24,7 @@ type DatePickerRootStateProps = WritableBoxedValues<{ required: boolean; preventDeselect: boolean; pagedNavigation: boolean; - weekStartsOn: WeekStartsOn; + weekStartsOn: WeekStartsOn | undefined; weekdayFormat: Intl.DateTimeFormatOptions["weekday"]; fixedWeeks: boolean; numberOfMonths: number; @@ -35,7 +35,11 @@ type DatePickerRootStateProps = WritableBoxedValues<{ }> & { defaultPlaceholder: DateValue }; class DatePickerRootState { - constructor(readonly opts: DatePickerRootStateProps) {} + readonly opts: DatePickerRootStateProps; + + constructor(opts: DatePickerRootStateProps) { + this.opts = opts; + } } export const DatePickerRootContext = new Context("DatePicker.Root"); diff --git a/packages/bits-ui/src/lib/bits/date-picker/exports.ts b/packages/bits-ui/src/lib/bits/date-picker/exports.ts index 6a982a626..f8dc5ceab 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/exports.ts +++ b/packages/bits-ui/src/lib/bits/date-picker/exports.ts @@ -19,6 +19,7 @@ export { default as NextButton } from "$lib/bits/calendar/components/calendar-ne export { default as PrevButton } from "$lib/bits/calendar/components/calendar-prev-button.svelte"; export { default as Cell } from "$lib/bits/calendar/components/calendar-cell.svelte"; export { default as Day } from "$lib/bits/calendar/components/calendar-day.svelte"; +export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte"; export type { DatePickerRootProps as RootProps, @@ -42,4 +43,5 @@ export type { DatePickerHeadingProps as HeadingProps, DatePickerNextButtonProps as NextButtonProps, DatePickerPrevButtonProps as PrevButtonProps, + DatePickerPortalProps as PortalProps, } from "./types.js"; diff --git a/packages/bits-ui/src/lib/bits/date-picker/types.ts b/packages/bits-ui/src/lib/bits/date-picker/types.ts index a6a835cf2..d43f0d735 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/types.ts +++ b/packages/bits-ui/src/lib/bits/date-picker/types.ts @@ -9,6 +9,7 @@ import type { EditableSegmentPart, } from "$lib/shared/index.js"; import type { Granularity, WeekStartsOn } from "$lib/shared/date/types.js"; +import type { PortalProps } from "$lib/bits/utilities/portal/index.js"; export type DatePickerRootPropsWithoutHTML = WithChildren<{ /** @@ -292,6 +293,9 @@ export type DatePickerCalendarPropsWithoutHTML = WithChild<{}, CalendarRootSnipp export type DatePickerCalendarProps = DatePickerCalendarPropsWithoutHTML & Without; +export type DatePickerPortalPropsWithoutHTML = PortalProps; +export type DatePickerPortalProps = DatePickerPortalPropsWithoutHTML; + export type { CalendarCellPropsWithoutHTML as DatePickerCellPropsWithoutHTML, CalendarCellProps as DatePickerCellProps, diff --git a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte index 199c781c6..01beb6e20 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte +++ b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field.svelte @@ -7,6 +7,7 @@ import { noop } from "$lib/internal/noop.js"; import type { DateRange } from "$lib/shared/index.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { id = useId(), @@ -38,21 +39,43 @@ let startValue = $state(value?.start); let endValue = $state(value?.end); - if (placeholder === undefined) { - const defaultPlaceholder = getDefaultDate({ - granularity, - defaultValue: value?.start, - }); - + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; + const defaultPlaceholder = getDefaultDate({ granularity, defaultValue: value?.start }); placeholder = defaultPlaceholder; } - if (value === undefined) { - const defaultValue = { start: undefined, end: undefined }; + // SSR + handleDefaultPlaceholder(); + + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + function handleDefaultValue() { + if (value !== undefined) return; + const defaultValue = { start: undefined, end: undefined }; value = defaultValue; } + // SSR + handleDefaultValue(); + + /** + * Covers an edge case where when a spread props object is reassigned, + * the props are reset to their default values, which would make value + * undefined which causes errors to be thrown. + */ + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useDateRangeFieldRoot({ id: box.with(() => id), ref: box.with( diff --git a/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts b/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts index 1c63e2d5e..b30784e71 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts @@ -47,6 +47,7 @@ type DateRangeFieldRootStateProps = WithRefProps< >; export class DateRangeFieldRootState { + readonly opts: DateRangeFieldRootStateProps; startFieldState: DateFieldRootState | undefined = undefined; endFieldState: DateFieldRootState | undefined = undefined; descriptionId = useId(); @@ -58,7 +59,8 @@ export class DateRangeFieldRootState { endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined); rangeComplete = $derived(this.startValueComplete && this.endValueComplete); - constructor(readonly opts: DateRangeFieldRootStateProps) { + constructor(opts: DateRangeFieldRootStateProps) { + this.opts = opts; this.formatter = createFormatter(this.opts.locale.current); useRefById({ @@ -126,18 +128,10 @@ export class DateRangeFieldRootState { if (prev.start === startValue && prev.end === endValue) { return prev; } - if (isBefore(endValue, startValue)) { - const start = startValue; - const end = endValue; - this.#setStartValue(end); - this.#setEndValue(start); - return { start: endValue, end: startValue }; - } else { - return { - start: startValue, - end: endValue, - }; - } + return { + start: startValue, + end: endValue, + }; }); } else if ( this.opts.value.current && @@ -200,14 +194,6 @@ export class DateRangeFieldRootState { this.opts.value.current = newValue; } - #setStartValue(value: DateValue | undefined) { - this.opts.startValue.current = value; - } - - #setEndValue(value: DateValue | undefined) { - this.opts.endValue.current = value; - } - props = $derived.by( () => ({ @@ -222,10 +208,13 @@ export class DateRangeFieldRootState { type DateRangeFieldLabelStateProps = WithRefProps; class DateRangeFieldLabelState { - constructor( - readonly opts: DateRangeFieldLabelStateProps, - readonly root: DateRangeFieldRootState - ) { + readonly opts: DateRangeFieldLabelStateProps; + readonly root: DateRangeFieldRootState; + + constructor(opts: DateRangeFieldLabelStateProps, root: DateRangeFieldRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte b/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte index fe97a2742..4c336e567 100644 --- a/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte +++ b/packages/bits-ui/src/lib/bits/date-range-picker/components/date-range-picker.svelte @@ -10,6 +10,7 @@ import { useId } from "$lib/internal/use-id.js"; import type { DateRange } from "$lib/shared/index.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { open = $bindable(false), @@ -36,7 +37,7 @@ disableDaysOutsideMonth = true, preventDeselect = false, pagedNavigation = false, - weekStartsOn = 0, + weekStartsOn, weekdayFormat = "narrow", isDateDisabled = () => false, fixedWeeks = false, @@ -54,18 +55,51 @@ let startValue = $state(value?.start); let endValue = $state(value?.end); - if (value === undefined) { + function handleDefaultValue() { + if (value !== undefined) return; value = { start: undefined, end: undefined }; } + + // SSR + handleDefaultValue(); + + /** + * Covers an edge case where when a spread props object is reassigned, + * the props are reset to their default values, which would make value + * undefined which causes errors to be thrown. + */ + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const defaultPlaceholder = getDefaultDate({ granularity, defaultValue: value?.start, }); - if (placeholder === undefined) { + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; placeholder = defaultPlaceholder; } + // SSR + handleDefaultPlaceholder(); + + /** + * Covers an edge case where when a spread props object is reassigned, + * the props are reset to their default values, which would make placeholder + * undefined which causes errors to be thrown. + */ + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + function onRangeSelect() { if (closeOnRangeSelect) { open = false; diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/date-range-picker.svelte.ts b/packages/bits-ui/src/lib/bits/date-range-picker/date-range-picker.svelte.ts index 059c46df2..486773097 100644 --- a/packages/bits-ui/src/lib/bits/date-range-picker/date-range-picker.svelte.ts +++ b/packages/bits-ui/src/lib/bits/date-range-picker/date-range-picker.svelte.ts @@ -26,7 +26,7 @@ type DateRangePickerRootStateProps = WritableBoxedValues<{ required: boolean; preventDeselect: boolean; pagedNavigation: boolean; - weekStartsOn: WeekStartsOn; + weekStartsOn: WeekStartsOn | undefined; weekdayFormat: Intl.DateTimeFormatOptions["weekday"]; fixedWeeks: boolean; numberOfMonths: number; @@ -38,7 +38,11 @@ type DateRangePickerRootStateProps = WritableBoxedValues<{ }; class DateRangePickerRootState { - constructor(readonly opts: DateRangePickerRootStateProps) {} + readonly opts: DateRangePickerRootStateProps; + + constructor(opts: DateRangePickerRootStateProps) { + this.opts = opts; + } } export const DateRangePickerRootContext = new Context( 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 2ddc5f1be..5c14a71db 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 @@ -19,6 +19,7 @@ ref = $bindable(null), forceMount = false, onCloseAutoFocus = noop, + onOpenAutoFocus = noop, onEscapeKeydown = noop, onInteractOutside = noop, trapFocus = true, @@ -52,7 +53,8 @@ trapFocus, open: contentState.root.opts.open.current, })} - {...mergedProps} + {onOpenAutoFocus} + {id} onCloseAutoFocus={(e) => { onCloseAutoFocus(e); if (e.defaultPrevented) return; 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 061b987b9..cb2c20881 100644 --- a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts +++ b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts @@ -28,8 +28,8 @@ type DialogRootStateProps = WritableBoxedValues<{ }>; class DialogRootState { + readonly opts: DialogRootStateProps; triggerNode = $state(null); - titleNode = $state(null); contentNode = $state(null); descriptionNode = $state(null); contentId = $state(undefined); @@ -39,7 +39,8 @@ class DialogRootState { cancelNode = $state(null); attrs = $derived.by(() => createAttrs(this.opts.variant.current)); - constructor(readonly opts: DialogRootStateProps) { + constructor(opts: DialogRootStateProps) { + this.opts = opts; this.handleOpen = this.handleOpen.bind(this); this.handleClose = this.handleClose.bind(this); } @@ -65,10 +66,13 @@ class DialogRootState { type DialogTriggerStateProps = WithRefProps & ReadableBoxedValues<{ disabled: boolean }>; class DialogTriggerState { - constructor( - readonly opts: DialogTriggerStateProps, - readonly root: DialogRootState - ) { + readonly opts: DialogTriggerStateProps; + readonly root: DialogRootState; + + constructor(opts: DialogTriggerStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -117,12 +121,13 @@ type DialogCloseStateProps = WithRefProps & disabled: boolean; }>; class DialogCloseState { + readonly opts: DialogCloseStateProps; + readonly root: DialogRootState; #attr = $derived.by(() => this.root.attrs[this.opts.variant.current]); - constructor( - readonly opts: DialogCloseStateProps, - readonly root: DialogRootState - ) { + constructor(opts: DialogCloseStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); @@ -163,12 +168,14 @@ class DialogCloseState { type DialogActionStateProps = WithRefProps; class DialogActionState { + readonly opts: DialogActionStateProps; + readonly root: DialogRootState; #attr = $derived.by(() => this.root.attrs.action); - constructor( - readonly opts: DialogActionStateProps, - readonly root: DialogRootState - ) { + constructor(opts: DialogActionStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -188,14 +195,16 @@ type DialogTitleStateProps = WithRefProps< }> >; class DialogTitleState { - constructor( - readonly opts: DialogTitleStateProps, - readonly root: DialogRootState - ) { + readonly opts: DialogTitleStateProps; + readonly root: DialogRootState; + + constructor(opts: DialogTitleStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { - this.root.titleNode = node; this.root.titleId = node?.id; }, deps: () => this.root.opts.open.current, @@ -217,10 +226,13 @@ class DialogTitleState { type DialogDescriptionStateProps = WithRefProps; class DialogDescriptionState { - constructor( - readonly opts: DialogDescriptionStateProps, - readonly root: DialogRootState - ) { + readonly opts: DialogDescriptionStateProps; + readonly root: DialogRootState; + + constructor(opts: DialogDescriptionStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.root.opts.open.current, @@ -244,10 +256,13 @@ class DialogDescriptionState { type DialogContentStateProps = WithRefProps; class DialogContentState { - constructor( - readonly opts: DialogContentStateProps, - readonly root: DialogRootState - ) { + readonly opts: DialogContentStateProps; + readonly root: DialogRootState; + + constructor(opts: DialogContentStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.root.opts.open.current, @@ -282,10 +297,13 @@ class DialogContentState { type DialogOverlayStateProps = WithRefProps; class DialogOverlayState { - constructor( - readonly opts: DialogOverlayStateProps, - readonly root: DialogRootState - ) { + readonly opts: DialogOverlayStateProps; + readonly root: DialogRootState; + + constructor(opts: DialogOverlayStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.root.opts.open.current, @@ -313,10 +331,12 @@ type AlertDialogCancelStateProps = WithRefProps & }>; class AlertDialogCancelState { - constructor( - readonly opts: AlertDialogCancelStateProps, - readonly root: DialogRootState - ) { + readonly opts: AlertDialogCancelStateProps; + readonly root: DialogRootState; + + constructor(opts: AlertDialogCancelStateProps, root: DialogRootState) { + this.opts = opts; + this.root = root; this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/exports.ts b/packages/bits-ui/src/lib/bits/dropdown-menu/exports.ts index ca5ef13d0..0dbcfaa77 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/exports.ts +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/exports.ts @@ -15,6 +15,7 @@ export { default as SubContentStatic } from "$lib/bits/menu/components/menu-sub- 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"; +export { default as CheckboxGroup } from "$lib/bits/menu/components/menu-checkbox-group.svelte"; export type { DropdownMenuArrowProps as ArrowProps, @@ -34,4 +35,5 @@ export type { DropdownMenuSubTriggerProps as SubTriggerProps, DropdownMenuTriggerProps as TriggerProps, DropdownMenuPortalProps as PortalProps, + DropdownMenuCheckboxGroupProps as CheckboxGroupProps, } from "./types.js"; 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 6675f8ed5..3f2639729 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/types.ts @@ -16,6 +16,7 @@ export type { MenuSubTriggerProps as DropdownMenuSubTriggerProps, MenuTriggerProps as DropdownMenuTriggerProps, MenuPortalProps as DropdownMenuPortalProps, + MenuCheckboxGroupProps as DropdownMenuCheckboxGroupProps, } from "$lib/bits/menu/types.js"; export type { @@ -36,4 +37,5 @@ export type { MenuSubContentStaticPropsWithoutHTML as DropdownMenuSubContentStaticPropsWithoutHTML, MenuTriggerPropsWithoutHTML as DropdownMenuTriggerPropsWithoutHTML, MenuPortalPropsWithoutHTML as DropdownMenuPortalPropsWithoutHTML, + MenuCheckboxGroupPropsWithoutHTML as DropdownMenuCheckboxGroupPropsWithoutHTML, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/label/label.svelte.ts b/packages/bits-ui/src/lib/bits/label/label.svelte.ts index e4ee9c5da..5246736c3 100644 --- a/packages/bits-ui/src/lib/bits/label/label.svelte.ts +++ b/packages/bits-ui/src/lib/bits/label/label.svelte.ts @@ -5,7 +5,10 @@ const ROOT_ATTR = "data-label-root"; type LabelRootStateProps = WithRefProps; class LabelRootState { - constructor(readonly opts: LabelRootStateProps) { + readonly opts: LabelRootStateProps; + + constructor(opts: LabelRootStateProps) { + this.opts = opts; this.onmousedown = this.onmousedown.bind(this); useRefById(opts); diff --git a/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts b/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts index dc83f468e..46a4f1c34 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts +++ b/packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts @@ -20,6 +20,7 @@ type LinkPreviewRootStateProps = WritableBoxedValues<{ }>; class LinkPreviewRootState { + readonly opts: LinkPreviewRootStateProps; hasSelection = $state(false); isPointerDownOnContent = $state(false); containsSelection = $state(false); @@ -29,7 +30,9 @@ class LinkPreviewRootState { triggerNode = $state(null); isOpening = false; - constructor(readonly opts: LinkPreviewRootStateProps) { + constructor(opts: LinkPreviewRootStateProps) { + this.opts = opts; + watch( () => this.opts.open.current, (isOpen) => { @@ -111,10 +114,12 @@ class LinkPreviewRootState { type LinkPreviewTriggerStateProps = WithRefProps; class LinkPreviewTriggerState { - constructor( - readonly opts: LinkPreviewTriggerStateProps, - readonly root: LinkPreviewRootState - ) { + readonly opts: LinkPreviewTriggerStateProps; + readonly root: LinkPreviewRootState; + + constructor(opts: LinkPreviewTriggerStateProps, root: LinkPreviewRootState) { + this.opts = opts; + this.root = root; this.onpointerenter = this.onpointerenter.bind(this); this.onpointerleave = this.onpointerleave.bind(this); this.onfocus = this.onfocus.bind(this); @@ -174,10 +179,12 @@ type LinkPreviewContentStateProps = WithRefProps & }>; class LinkPreviewContentState { - constructor( - readonly opts: LinkPreviewContentStateProps, - readonly root: LinkPreviewRootState - ) { + readonly opts: LinkPreviewContentStateProps; + readonly root: LinkPreviewRootState; + + constructor(opts: LinkPreviewContentStateProps, root: LinkPreviewRootState) { + this.opts = opts; + this.root = root; this.onpointerdown = this.onpointerdown.bind(this); this.onpointerenter = this.onpointerenter.bind(this); this.onfocusout = this.onfocusout.bind(this); diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-group.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-group.svelte new file mode 100644 index 000000000..8ba3c12a7 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-group.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render children?.()} +
    +{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte index e363942d2..bf2d74d75 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte @@ -1,9 +1,10 @@ -{#if contentState.context.viewportRef.current} - - - {#snippet presence()} - - - {/snippet} - - -{/if} + + + {#snippet presence()} + + + {/snippet} + + diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte index 93eecb3e5..71326f16b 100644 --- a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte +++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte @@ -10,6 +10,7 @@ ref = $bindable(null), child, children, + openOnHover = true, ...restProps }: NavigationMenuItemProps = $props(); @@ -20,6 +21,7 @@ (v) => (ref = v) ), value: box.with(() => value), + openOnHover: box.with(() => openOnHover), }); const mergedProps = $derived(mergeProps(restProps, itemState.props)); diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte index 6fd503df7..5f681628a 100644 --- a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte +++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte @@ -4,6 +4,7 @@ import { useId } from "$lib/internal/use-id.js"; import PresenceLayer from "$lib/bits/utilities/presence-layer/presence-layer.svelte"; import { box, mergeProps } from "svelte-toolbelt"; + import { Mounted } from "$lib/bits/utilities/index.js"; let { id = useId(), @@ -34,5 +35,6 @@ {@render children?.()} {/if} + {/snippet} diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts b/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts index 2a7e20f94..a574e407b 100644 --- a/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts @@ -39,6 +39,12 @@ import { useArrowNavigation } from "$lib/internal/use-arrow-navigation.js"; import { boxAutoReset } from "$lib/internal/box-auto-reset.svelte.js"; import { useResizeObserver } from "$lib/internal/use-resize-observer.svelte.js"; import { isElement } from "$lib/internal/is.js"; +import type { + FocusEventHandler, + KeyboardEventHandler, + MouseEventHandler, + PointerEventHandler, +} from "svelte/elements"; const NAVIGATION_MENU_ROOT_ATTR = "data-navigation-menu-root"; const NAVIGATION_MENU_ATTR = "data-navigation-menu"; @@ -61,15 +67,16 @@ type NavigationMenuProviderStateProps = ReadableBoxedValues<{ previousValue: string; }> & { isRootMenu: boolean; - onTriggerEnter: (itemValue: string) => void; + onTriggerEnter: (itemValue: string, itemState: NavigationMenuItemState | null) => void; onTriggerLeave?: () => void; onContentEnter?: () => void; onContentLeave?: () => void; - onItemSelect: (itemValue: string) => void; + onItemSelect: (itemValue: string, itemState: NavigationMenuItemState | null) => void; onItemDismiss: () => void; }; class NavigationMenuProviderState { + readonly opts: NavigationMenuProviderStateProps; indicatorTrackRef = box(null); viewportRef = box(null); viewportContent = new SvelteMap(); @@ -79,8 +86,11 @@ class NavigationMenuProviderState { onContentLeave: () => void = noop; onItemSelect: NavigationMenuProviderStateProps["onItemSelect"]; onItemDismiss: NavigationMenuProviderStateProps["onItemDismiss"]; + activeItem: NavigationMenuItemState | null = null; + prevActiveItem: NavigationMenuItemState | null = null; - constructor(readonly opts: NavigationMenuProviderStateProps) { + constructor(opts: NavigationMenuProviderStateProps) { + this.opts = opts; this.onTriggerEnter = opts.onTriggerEnter; this.onTriggerLeave = opts.onTriggerLeave ?? noop; this.onContentEnter = opts.onContentEnter ?? noop; @@ -88,6 +98,11 @@ class NavigationMenuProviderState { this.onItemDismiss = opts.onItemDismiss; this.onItemSelect = opts.onItemSelect; } + + setActiveItem = (item: NavigationMenuItemState | null) => { + this.prevActiveItem = this.activeItem; + this.activeItem = item; + }; } type NavigationMenuRootStateProps = WithRefProps< @@ -103,6 +118,7 @@ type NavigationMenuRootStateProps = WithRefProps< >; class NavigationMenuRootState { + readonly opts: NavigationMenuRootStateProps; provider: NavigationMenuProviderState; previousValue = box(""); isDelaySkipped: WritableBox; @@ -116,7 +132,8 @@ class NavigationMenuRootState { } }); - constructor(readonly opts: NavigationMenuRootStateProps) { + constructor(opts: NavigationMenuRootStateProps) { + this.opts = opts; this.isDelaySkipped = boxAutoReset(false, this.opts.skipDelayDuration.current); useRefById(opts); @@ -127,8 +144,8 @@ class NavigationMenuRootState { orientation: this.opts.orientation, rootNavigationMenuRef: this.opts.ref, isRootMenu: true, - onTriggerEnter: (itemValue) => { - this.#onTriggerEnter(itemValue); + onTriggerEnter: (itemValue, itemState) => { + this.#onTriggerEnter(itemValue, itemState); }, onTriggerLeave: this.#onTriggerLeave, onContentEnter: this.#onContentEnter, @@ -139,43 +156,56 @@ class NavigationMenuRootState { } #debouncedFn = useDebounce( - (val?: string) => { + (val: string | undefined, itemState: NavigationMenuItemState | null) => { // passing `undefined` meant to reset the debounce timer if (typeof val === "string") { - this.setValue(val); + this.setValue(val, itemState); } }, () => this.#derivedDelay ); - #onTriggerEnter = (itemValue: string) => { - this.#debouncedFn(itemValue); + #onTriggerEnter = (itemValue: string, itemState: NavigationMenuItemState | null) => { + this.#debouncedFn(itemValue, itemState); }; #onTriggerLeave = () => { this.isDelaySkipped.current = false; - this.#debouncedFn(""); + this.#debouncedFn("", null); }; #onContentEnter = () => { - this.#debouncedFn(); + this.#debouncedFn(undefined, null); }; #onContentLeave = () => { - this.#debouncedFn(""); + if ( + this.provider.activeItem && + this.provider.activeItem.opts.openOnHover.current === false + ) { + return; + } + this.#debouncedFn("", null); }; - #onItemSelect = (itemValue: string) => { - this.setValue(itemValue); + #onItemSelect = (itemValue: string, itemState: NavigationMenuItemState | null) => { + this.setValue(itemValue, itemState); }; #onItemDismiss = () => { - this.setValue(""); + this.setValue("", null); }; - setValue = (newValue: string) => { + setValue = (newValue: string, itemState: NavigationMenuItemState | null) => { this.previousValue.current = this.opts.value.current; this.opts.value.current = newValue; + this.provider.setActiveItem(itemState); + + // When all menus are closed, we want to reset previousValue to prevent + // weird transitions from old positions when opening fresh + if (newValue === "") { + this.previousValue.current = ""; + } }; props = $derived.by( @@ -200,15 +230,18 @@ type NavigationMenuSubStateProps = WithRefProps< >; class NavigationMenuSubState { + readonly opts: NavigationMenuSubStateProps; + readonly context: NavigationMenuProviderState; previousValue = box(""); + subProvider: NavigationMenuProviderState; + + constructor(opts: NavigationMenuSubStateProps, context: NavigationMenuProviderState) { + this.opts = opts; + this.context = context; - constructor( - readonly opts: NavigationMenuSubStateProps, - readonly context: NavigationMenuProviderState - ) { useRefById(opts); - useNavigationMenuProvider({ + this.subProvider = useNavigationMenuProvider({ isRootMenu: false, value: this.opts.value, dir: this.context.opts.dir, @@ -216,13 +249,21 @@ class NavigationMenuSubState { rootNavigationMenuRef: this.opts.ref, onTriggerEnter: this.setValue, onItemSelect: this.setValue, - onItemDismiss: () => this.setValue(""), + onItemDismiss: () => this.setValue("", null), previousValue: this.previousValue, }); } - setValue = (newValue: string) => { + setValue = (newValue: string, itemState: NavigationMenuItemState | null) => { + this.previousValue.current = this.opts.value.current; this.opts.value.current = newValue; + this.subProvider.setActiveItem(itemState); + + // When all menus are closed, we want to reset previousValue to prevent + // weird transitions from old positions when opening fresh + if (newValue === "") { + this.previousValue.current = ""; + } }; props = $derived.by( @@ -239,16 +280,18 @@ class NavigationMenuSubState { type NavigationMenuListStateProps = WithRefProps; class NavigationMenuListState { + readonly opts: NavigationMenuListStateProps; + readonly context: NavigationMenuProviderState; wrapperId = box(useId()); wrapperRef = box(null); listTriggers = $state.raw([]); rovingFocusGroup: ReturnType; wrapperMounted = $state(false); - constructor( - readonly opts: NavigationMenuListStateProps, - readonly context: NavigationMenuProviderState - ) { + constructor(opts: NavigationMenuListStateProps, context: NavigationMenuProviderState) { + this.opts = opts; + this.context = context; + useRefById(opts); useRefById({ @@ -262,8 +305,7 @@ class NavigationMenuListState { this.rovingFocusGroup = useRovingFocus({ rootNodeId: opts.id, - candidateAttr: NAVIGATION_MENU_ITEM_ATTR, - candidateSelector: `:is([${NAVIGATION_MENU_TRIGGER_ATTR}], [data-list-link]):not([data-disabled])`, + candidateSelector: `[${NAVIGATION_MENU_TRIGGER_ATTR}]:not([data-disabled]), [${NAVIGATION_MENU_LINK_ATTR}]:not([data-disabled])`, loop: box.with(() => false), orientation: this.context.opts.orientation, }); @@ -296,10 +338,13 @@ class NavigationMenuListState { type NavigationMenuItemStateProps = WithRefProps< ReadableBoxedValues<{ value: string; + openOnHover: boolean; }> >; export class NavigationMenuItemState { + readonly opts: NavigationMenuItemStateProps; + readonly listContext: NavigationMenuListState; contentNode = $state(null); triggerNode = $state(null); focusProxyNode = $state(null); @@ -312,10 +357,10 @@ export class NavigationMenuItemState { box(undefined); contentProps: ReadableBox> = box({}); - constructor( - readonly opts: NavigationMenuItemStateProps, - readonly listContext: NavigationMenuListState - ) {} + constructor(opts: NavigationMenuItemStateProps, listContext: NavigationMenuListState) { + this.opts = opts; + this.listContext = listContext; + } #handleContentEntry = (side: "start" | "end" = "start") => { if (!this.contentNode) return; @@ -350,6 +395,7 @@ type NavigationMenuTriggerStateProps = WithRefProps & }>; class NavigationMenuTriggerState { + readonly opts: NavigationMenuTriggerStateProps; focusProxyId = box(useId()); focusProxyRef = box(null); context: NavigationMenuProviderState; @@ -363,13 +409,15 @@ class NavigationMenuTriggerState { focusProxyMounted = $state(false); constructor( - readonly opts: NavigationMenuTriggerStateProps, + opts: NavigationMenuTriggerStateProps, context: { provider: NavigationMenuProviderState; item: NavigationMenuItemState; list: NavigationMenuListState; + sub: NavigationMenuSubState | null; } ) { + this.opts = opts; this.hasPointerMoveOpened = boxAutoReset(false, 300); this.context = context.provider; this.itemContext = context.item; @@ -411,32 +459,36 @@ class NavigationMenuTriggerState { this.opts.disabled.current || this.wasClickClose || this.itemContext.wasEscapeClose || - this.hasPointerMoveOpened.current + this.hasPointerMoveOpened.current || + !this.itemContext.opts.openOnHover.current ) { return; } - this.context.onTriggerEnter(this.itemContext.opts.value.current); + this.context.onTriggerEnter(this.itemContext.opts.value.current, this.itemContext); this.hasPointerMoveOpened.current = true; }); onpointerleave = whenMouse(() => { - if (this.opts.disabled.current) return; + if (this.opts.disabled.current || !this.itemContext.opts.openOnHover.current) return; this.context.onTriggerLeave(); this.hasPointerMoveOpened.current = false; }); - onclick = (_: BitsMouseEvent) => { + onclick: MouseEventHandler = () => { // if opened via pointer move, we prevent the click event if (this.hasPointerMoveOpened.current) return; - if (this.open) { - this.context.onItemSelect(""); - } else { - this.context.onItemSelect(this.itemContext.opts.value.current); + const shouldClose = + this.open && + (!this.itemContext.opts.openOnHover.current || this.context.opts.isRootMenu); + if (shouldClose) { + this.context.onItemSelect("", null); + } else if (!this.open) { + this.context.onItemSelect(this.itemContext.opts.value.current, this.itemContext); } - this.wasClickClose = this.open; + this.wasClickClose = shouldClose; }; - onkeydown = (e: BitsKeyboardEvent) => { + onkeydown: KeyboardEventHandler = (e) => { const verticalEntryKey = this.context.opts.dir.current === "rtl" ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT; const entryKey = { horizontal: kbd.ARROW_DOWN, vertical: verticalEntryKey }[ @@ -451,7 +503,7 @@ class NavigationMenuTriggerState { this.itemContext.listContext.rovingFocusGroup.handleKeydown(this.opts.ref.current, e); }; - focusProxyOnFocus = (e: BitsFocusEvent) => { + focusProxyOnFocus: FocusEventHandler = (e) => { const content = this.itemContext.contentNode; const prevFocusedElement = e.relatedTarget as HTMLElement | null; const wasTriggerFocused = @@ -516,14 +568,17 @@ const ROOT_CONTENT_DISMISS_EVENT = new CustomEventDispatcher("bitsRootContentDis }); class NavigationMenuLinkState { + readonly opts: NavigationMenuLinkStateProps; + readonly context: { provider: NavigationMenuProviderState; item: NavigationMenuItemState }; isFocused = $state(false); + constructor( - readonly opts: NavigationMenuLinkStateProps, - readonly context: { - provider: NavigationMenuProviderState; - item: NavigationMenuItemState; - } + opts: NavigationMenuLinkStateProps, + context: { provider: NavigationMenuProviderState; item: NavigationMenuItemState } ) { + this.opts = opts; + this.context = context; + useRefById(opts); } @@ -551,6 +606,25 @@ class NavigationMenuLinkState { this.isFocused = false; }; + #handlePointerDismiss = () => { + // only close submenu if this link is not inside the currently open submenu content + const currentlyOpenValue = this.context.provider.opts.value.current; + const isInsideOpenSubmenu = this.context.item.opts.value.current === currentlyOpenValue; + const activeItem = this.context.item.listContext.context.activeItem; + if (activeItem && !activeItem.opts.openOnHover.current) return; + if (currentlyOpenValue && !isInsideOpenSubmenu) { + this.context.provider.onItemDismiss(); + } + }; + + onpointerenter: PointerEventHandler = () => { + this.#handlePointerDismiss(); + }; + + onpointermove = whenMouse(() => { + this.#handlePointerDismiss(); + }); + props = $derived.by( () => ({ @@ -562,6 +636,8 @@ class NavigationMenuLinkState { onkeydown: this.onkeydown, onfocus: this.onfocus, onblur: this.onblur, + onpointerenter: this.onpointerenter, + onpointermove: this.onpointermove, [NAVIGATION_MENU_LINK_ATTR]: "", }) as const ); @@ -579,6 +655,7 @@ class NavigationMenuIndicatorState { } class NavigationMenuIndicatorImplState { + readonly opts: NavigationMenuIndicatorStateProps; context: NavigationMenuProviderState; listContext: NavigationMenuListState; position = $state.raw<{ size: number; offset: number } | null>(null); @@ -594,12 +671,13 @@ class NavigationMenuIndicatorImplState { shouldRender = $derived.by(() => this.position !== null); constructor( - readonly opts: NavigationMenuIndicatorStateProps, + opts: NavigationMenuIndicatorStateProps, context: { provider: NavigationMenuProviderState; list: NavigationMenuListState; } ) { + this.opts = opts; this.context = context.provider; this.listContext = context.list; @@ -652,6 +730,7 @@ class NavigationMenuIndicatorImplState { type NavigationMenuContentStateProps = WithRefProps; class NavigationMenuContentState { + readonly opts: NavigationMenuContentStateProps; context: NavigationMenuProviderState; itemContext: NavigationMenuItemState; listContext: NavigationMenuListState; @@ -674,13 +753,14 @@ class NavigationMenuContentState { }); constructor( - readonly opts: NavigationMenuContentStateProps, + opts: NavigationMenuContentStateProps, context: { provider: NavigationMenuProviderState; item: NavigationMenuItemState; list: NavigationMenuListState; } ) { + this.opts = opts; this.context = context.provider; this.itemContext = context.item; this.listContext = context.list; @@ -699,6 +779,7 @@ class NavigationMenuContentState { }; onpointerleave = whenMouse(() => { + if (!this.itemContext.opts.openOnHover.current) return; this.context.onContentLeave(); }); @@ -716,6 +797,8 @@ type MotionAttribute = "to-start" | "to-end" | "from-start" | "from-end"; type NavigationMenuContentImplStateProps = WithRefProps; class NavigationMenuContentImplState { + readonly opts: NavigationMenuContentImplStateProps; + readonly itemContext: NavigationMenuItemState; context: NavigationMenuProviderState; listContext: NavigationMenuListState; prevMotionAttribute: MotionAttribute | null = $state(null); @@ -728,6 +811,12 @@ class NavigationMenuContentImplState { const isSelected = this.itemContext.opts.value.current === this.context.opts.value.current; const wasSelected = prevIndex === values.indexOf(this.itemContext.opts.value.current); + // When all menus are closed, we want to reset motion state to prevent residual animations + if (!this.context.opts.value.current && !this.context.opts.previousValue.current) { + untrack(() => (this.prevMotionAttribute = null)); + return null; + } + // We only want to update selected and the last selected content // this avoids animations being interrupted outside of that range if (!isSelected && !wasSelected) return untrack(() => this.prevMotionAttribute); @@ -750,10 +839,9 @@ class NavigationMenuContentImplState { return attribute; }); - constructor( - readonly opts: NavigationMenuContentImplStateProps, - readonly itemContext: NavigationMenuItemState - ) { + constructor(opts: NavigationMenuContentImplStateProps, itemContext: NavigationMenuItemState) { + this.opts = opts; + this.itemContext = itemContext; this.listContext = itemContext.listContext; this.context = itemContext.listContext.context; @@ -804,7 +892,17 @@ class NavigationMenuContentImplState { const isTrigger = this.listContext.listTriggers.some((trigger) => trigger.contains(target)); const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef.current?.contains(target); - if (isTrigger || isRootViewport || !this.context.opts.isRootMenu) e.preventDefault(); + if (!this.context.opts.isRootMenu && !isTrigger) { + this.context.onItemDismiss(); + return; + } + if (isTrigger || isRootViewport) { + e.preventDefault(); + return; + } + if (!this.itemContext.opts.openOnHover.current) { + this.context.onItemSelect("", null); + } }; onkeydown = (e: BitsKeyboardEvent) => { @@ -887,17 +985,20 @@ class NavigationMenuContentImplState { } class NavigationMenuViewportState { + readonly opts: NavigationMenuViewportImplStateProps; + readonly context: NavigationMenuProviderState; open = $derived.by(() => !!this.context.opts.value.current); size = $state<{ width: number; height: number } | null>(null); contentNode = $state(null); viewportWidth = $derived.by(() => (this.size ? `${this.size.width}px` : undefined)); viewportHeight = $derived.by(() => (this.size ? `${this.size.height}px` : undefined)); activeContentValue = $derived.by(() => this.context.opts.value.current); + mounted = $state(false); + + constructor(opts: NavigationMenuViewportImplStateProps, context: NavigationMenuProviderState) { + this.opts = opts; + this.context = context; - constructor( - readonly opts: NavigationMenuViewportImplStateProps, - readonly context: NavigationMenuProviderState - ) { useRefById({ ...opts, onRefChange: (node) => { @@ -935,6 +1036,16 @@ class NavigationMenuViewportState { } } ); + + // reset size when viewport closes to prevent residual size animations + watch( + () => this.mounted, + () => { + if (!this.mounted && this.size) { + this.size = null; + } + } + ); } props = $derived.by( @@ -971,6 +1082,8 @@ const NavigationMenuContentContext = new Context( "NavigationMenu.Content" ); +const NavigationMenuSubContext = new Context("NavigationMenu.Sub"); + export function useNavigationMenuRoot(props: NavigationMenuRootStateProps) { return new NavigationMenuRootState(props); } @@ -1007,6 +1120,7 @@ export function useNavigationMenuTrigger(props: NavigationMenuTriggerStateProps) provider: NavigationMenuProviderContext.get(), item: NavigationMenuItemContext.get(), list: NavigationMenuListContext.get(), + sub: NavigationMenuSubContext.getOr(null), }); } @@ -1067,13 +1181,9 @@ function removeFromTabOrder(candidates: HTMLElement[]) { }; } -type BitsPointerEventHandler = ( - e: BitsPointerEvent -) => void; - function whenMouse( - handler: BitsPointerEventHandler -): BitsPointerEventHandler { + handler: PointerEventHandler +): PointerEventHandler { return (e) => (e.pointerType === "mouse" ? handler(e) : undefined); } diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/types.ts b/packages/bits-ui/src/lib/bits/navigation-menu/types.ts index 4696acd25..a89c095bb 100644 --- a/packages/bits-ui/src/lib/bits/navigation-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/navigation-menu/types.ts @@ -30,23 +30,24 @@ export type NavigationMenuRootPropsWithoutHTML = WithChild<{ onValueChange?: OnChangeFn; /** - * The duration from when the mouse enters a trigger until the content opens. + * The amount of time in ms from when the mouse enters a trigger until the content opens. * - * @defaultValue 200 + * @default 200 */ delayDuration?: number; /** - * How much time a user has to enter another trigger without incurring a delay again. + * The amount of time in ms that a user has to enter another trigger without + * incurring a delay again. * - * @defaultValue 300 + * @default 300 */ skipDelayDuration?: number; /** * The reading direction of the content. * - * @defaultValue "ltr" + * @default "ltr" */ dir?: Direction; @@ -100,6 +101,14 @@ export type NavigationMenuItemPropsWithoutHTML = WithChild<{ * The value of the menu item. */ value?: string; + + /** + * Whether to open the menu associated with the item when the item's trigger + * is hovered. + * + * @default true + */ + openOnHover?: boolean; }>; export type NavigationMenuItemProps = NavigationMenuItemPropsWithoutHTML & @@ -120,7 +129,6 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{ /** * Callback fired when an interaction occurs outside the content. * Default behavior can be prevented with `event.preventDefault()` - * */ onInteractOutside?: (event: PointerEvent) => void; @@ -151,7 +159,7 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{ * This is useful when wanting to use more custom transition and animation * libraries. * - * @defaultValue false + * @default false */ forceMount?: boolean; }>; diff --git a/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts b/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts index a62088864..17ddd4c6a 100644 --- a/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts @@ -28,6 +28,7 @@ type PaginationRootStateProps = WithRefProps< >; class PaginationRootState { + readonly opts: PaginationRootStateProps; totalPages = $derived.by(() => { if (this.opts.count.current === 0) return 1; return Math.ceil(this.opts.count.current / this.opts.perPage.current); @@ -45,7 +46,9 @@ class PaginationRootState { }) ); - constructor(readonly opts: PaginationRootStateProps) { + constructor(opts: PaginationRootStateProps) { + this.opts = opts; + useRefById(opts); } @@ -104,12 +107,14 @@ type PaginationPageStateProps = WithRefProps< >; class PaginationPageState { + readonly opts: PaginationPageStateProps; + readonly root: PaginationRootState; #isSelected = $derived.by(() => this.opts.page.current.value === this.root.opts.page.current); - constructor( - readonly opts: PaginationPageStateProps, - readonly root: PaginationRootState - ) { + constructor(opts: PaginationPageStateProps, root: PaginationRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); this.onclick = this.onclick.bind(this); @@ -158,10 +163,13 @@ type PaginationButtonStateProps = WithRefProps<{ }>; class PaginationButtonState { - constructor( - readonly opts: PaginationButtonStateProps, - readonly root: PaginationRootState - ) { + readonly opts: PaginationButtonStateProps; + readonly root: PaginationRootState; + + constructor(opts: PaginationButtonStateProps, root: PaginationRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); this.onclick = this.onclick.bind(this); diff --git a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts index f54831a9a..c5de837e2 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts @@ -68,6 +68,7 @@ const KEYS_TO_IGNORE = [ ]; class PinInputRootState { + readonly opts: PinInputRootStateProps; #inputRef = box(null); #isHoveringInput = $state(false); #isFocused = box(false); @@ -90,7 +91,9 @@ class PinInputRootState { #pwmb: ReturnType; #initialLoad: InitialLoad; - constructor(readonly opts: PinInputRootStateProps) { + constructor(opts: PinInputRootStateProps) { + this.opts = opts; + this.#initialLoad = { value: this.opts.value, isIOS: @@ -510,7 +513,11 @@ type PinInputCellStateProps = WithRefProps & }>; class PinInputCellState { - constructor(readonly opts: PinInputCellStateProps) { + readonly opts: PinInputCellStateProps; + + constructor(opts: PinInputCellStateProps) { + this.opts = opts; + useRefById({ id: this.opts.id, ref: this.opts.ref, diff --git a/packages/bits-ui/src/lib/bits/popover/exports.ts b/packages/bits-ui/src/lib/bits/popover/exports.ts index 7b43cd78a..7801adff2 100644 --- a/packages/bits-ui/src/lib/bits/popover/exports.ts +++ b/packages/bits-ui/src/lib/bits/popover/exports.ts @@ -13,6 +13,5 @@ export type { PopoverContentStaticProps as ContentStaticProps, PopoverTriggerProps as TriggerProps, PopoverCloseProps as CloseProps, + PopoverPortalProps as PortalProps, } from "./types.js"; - -export type { PortalProps } from "$lib/bits/utilities/portal/types.js"; diff --git a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts index 1dd982189..e2ce08144 100644 --- a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts +++ b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts @@ -16,10 +16,13 @@ type PopoverRootStateProps = WritableBoxedValues<{ }>; class PopoverRootState { + readonly opts: PopoverRootStateProps; contentNode = $state(null); triggerNode = $state(null); - constructor(readonly opts: PopoverRootStateProps) {} + constructor(opts: PopoverRootStateProps) { + this.opts = opts; + } toggleOpen() { this.opts.open.current = !this.opts.open.current; @@ -34,10 +37,13 @@ class PopoverRootState { type PopoverTriggerStateProps = WithRefProps & ReadableBoxedValues<{ disabled: boolean }>; class PopoverTriggerState { - constructor( - readonly opts: PopoverTriggerStateProps, - readonly root: PopoverRootState - ) { + readonly opts: PopoverTriggerStateProps; + readonly root: PopoverRootState; + + constructor(opts: PopoverTriggerStateProps, root: PopoverRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -93,10 +99,13 @@ type PopoverContentStateProps = WithRefProps & onCloseAutoFocus: (e: Event) => void; }>; class PopoverContentState { - constructor( - readonly opts: PopoverContentStateProps, - readonly root: PopoverRootState - ) { + readonly opts: PopoverContentStateProps; + readonly root: PopoverRootState; + + constructor(opts: PopoverContentStateProps, root: PopoverRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.root.opts.open.current, @@ -154,10 +163,13 @@ class PopoverContentState { type PopoverCloseStateProps = WithRefProps; class PopoverCloseState { - constructor( - readonly opts: PopoverCloseStateProps, - readonly root: PopoverRootState - ) { + readonly opts: PopoverCloseStateProps; + readonly root: PopoverRootState; + + constructor(opts: PopoverCloseStateProps, root: PopoverRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.root.opts.open.current, diff --git a/packages/bits-ui/src/lib/bits/popover/types.ts b/packages/bits-ui/src/lib/bits/popover/types.ts index ef98ea732..1072fecd4 100644 --- a/packages/bits-ui/src/lib/bits/popover/types.ts +++ b/packages/bits-ui/src/lib/bits/popover/types.ts @@ -12,6 +12,7 @@ import type { BitsPrimitiveDivAttributes, } from "$lib/shared/attributes.js"; import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "$lib/shared/types.js"; +import type { PortalProps } from "$lib/types.js"; export type PopoverRootPropsWithoutHTML = WithChildren<{ /** @@ -56,3 +57,6 @@ export type PopoverCloseProps = PopoverClosePropsWithoutHTML & export type PopoverArrowPropsWithoutHTML = ArrowPropsWithoutHTML; export type PopoverArrowProps = ArrowProps; + +export type PopoverPortalPropsWithoutHTML = PortalProps; +export type PopoverPortalProps = PortalProps; diff --git a/packages/bits-ui/src/lib/bits/progress/progress.svelte.ts b/packages/bits-ui/src/lib/bits/progress/progress.svelte.ts index 917ba80cb..de6a43586 100644 --- a/packages/bits-ui/src/lib/bits/progress/progress.svelte.ts +++ b/packages/bits-ui/src/lib/bits/progress/progress.svelte.ts @@ -13,7 +13,11 @@ type ProgressRootStateProps = WithRefProps< >; class ProgressRootState { - constructor(readonly opts: ProgressRootStateProps) { + readonly opts: ProgressRootStateProps; + + constructor(opts: ProgressRootStateProps) { + this.opts = opts; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts b/packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts index d93c35375..ee480b6c3 100644 --- a/packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts +++ b/packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts @@ -29,10 +29,12 @@ type RadioGroupRootStateProps = WithRefProps< WritableBoxedValues<{ value: string }> >; class RadioGroupRootState { + readonly opts: RadioGroupRootStateProps; rovingFocusGroup: UseRovingFocusReturn; hasValue = $derived.by(() => this.opts.value.current !== ""); - constructor(readonly opts: RadioGroupRootStateProps) { + constructor(opts: RadioGroupRootStateProps) { + this.opts = opts; this.rovingFocusGroup = useRovingFocus({ rootNodeId: this.opts.id, candidateAttr: RADIO_GROUP_ITEM_ATTR, @@ -79,15 +81,17 @@ type RadioGroupItemStateProps = WithRefProps< >; class RadioGroupItemState { + readonly opts: RadioGroupItemStateProps; + readonly root: RadioGroupRootState; checked = $derived.by(() => this.root.opts.value.current === this.opts.value.current); #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); #isChecked = $derived.by(() => this.root.isChecked(this.opts.value.current)); #tabIndex = $state(-1); - constructor( - readonly opts: RadioGroupItemStateProps, - readonly root: RadioGroupRootState - ) { + constructor(opts: RadioGroupItemStateProps, root: RadioGroupRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); if (this.opts.value.current === this.root.opts.value.current) { @@ -162,6 +166,7 @@ class RadioGroupItemState { // class RadioGroupInputState { + readonly root: RadioGroupRootState; shouldRender = $derived.by(() => this.root.opts.name.current !== undefined); props = $derived.by( () => @@ -173,7 +178,9 @@ class RadioGroupInputState { }) as const ); - constructor(readonly root: RadioGroupRootState) {} + constructor(root: RadioGroupRootState) { + this.root = root; + } } const RadioGroupRootContext = new Context("RadioGroup.Root"); diff --git a/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte b/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte index 44b3ef20f..f49731b1a 100644 --- a/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte +++ b/packages/bits-ui/src/lib/bits/range-calendar/components/range-calendar.svelte @@ -6,6 +6,7 @@ import { noop } from "$lib/internal/noop.js"; import { useId } from "$lib/internal/use-id.js"; import { getDefaultDate } from "$lib/internal/date-time/utils.js"; + import { watch } from "runed"; let { children, @@ -17,7 +18,7 @@ placeholder = $bindable(), onPlaceholderChange = noop, weekdayFormat = "narrow", - weekStartsOn = 0, + weekStartsOn, pagedNavigation = false, isDateDisabled = () => false, isDateUnavailable = () => false, @@ -43,15 +44,36 @@ defaultValue: value?.start, }); - if (placeholder === undefined) { + function handleDefaultPlaceholder() { + if (placeholder !== undefined) return; placeholder = defaultPlaceholder; } - if (value === undefined) { - const defaultValue = { start: undefined, end: undefined }; - value = defaultValue; + // SSR + handleDefaultPlaceholder(); + + watch.pre( + () => placeholder, + () => { + handleDefaultPlaceholder(); + } + ); + + function handleDefaultValue() { + if (value !== undefined) return; + value = { start: undefined, end: undefined }; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useRangeCalendarRoot({ id: box.with(() => id), ref: box.with( diff --git a/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts b/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts index 0047ea5c4..302a5f9b6 100644 --- a/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts @@ -50,6 +50,7 @@ import { isBetweenInclusive, toDate, } from "$lib/internal/date-time/utils.js"; +import type { WeekStartsOn } from "$lib/shared/date/types.js"; type RangeCalendarRootStateProps = WithRefProps< WritableBoxedValues<{ @@ -64,7 +65,7 @@ type RangeCalendarRootStateProps = WithRefProps< maxValue: DateValue | undefined; disabled: boolean; pagedNavigation: boolean; - weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6; + weekStartsOn: WeekStartsOn | undefined; weekdayFormat: Intl.DateTimeFormatOptions["weekday"]; isDateDisabled: (date: DateValue) => boolean; isDateUnavailable: (date: DateValue) => boolean; @@ -85,6 +86,7 @@ type RangeCalendarRootStateProps = WithRefProps< >; export class RangeCalendarRootState { + readonly opts: RangeCalendarRootStateProps; months: Month[] = $state([]); visibleMonths = $derived.by(() => this.months.map((month) => month.value)); announcer: Announcer; @@ -93,7 +95,8 @@ export class RangeCalendarRootState { focusedValue = $state(undefined); lastPressedDateValue: DateValue | undefined = undefined; - constructor(readonly opts: RangeCalendarRootStateProps) { + constructor(opts: RangeCalendarRootStateProps) { + this.opts = opts; this.announcer = getAnnouncer(); this.formatter = createFormatter(this.opts.locale.current); @@ -577,6 +580,8 @@ type RangeCalendarCellStateProps = WithRefProps< >; export class RangeCalendarCellState { + readonly opts: RangeCalendarCellStateProps; + readonly root: RangeCalendarRootState; cellDate = $derived.by(() => toDate(this.opts.date.current)); isDisabled = $derived.by(() => this.root.isDateDisabled(this.opts.date.current)); isUnavailable = $derived.by(() => @@ -614,10 +619,10 @@ export class RangeCalendarCellState { }) ); - constructor( - readonly opts: RangeCalendarCellStateProps, - readonly root: RangeCalendarRootState - ) { + constructor(opts: RangeCalendarCellStateProps, root: RangeCalendarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -672,10 +677,13 @@ export class RangeCalendarCellState { type RangeCalendarDayStateProps = WithRefProps; class RangeCalendarDayState { - constructor( - readonly opts: RangeCalendarDayStateProps, - readonly cell: RangeCalendarCellState - ) { + readonly opts: RangeCalendarDayStateProps; + readonly cell: RangeCalendarCellState; + + constructor(opts: RangeCalendarDayStateProps, cell: RangeCalendarCellState) { + this.opts = opts; + this.cell = cell; + useRefById(opts); this.onclick = this.onclick.bind(this); diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index dee7dd156..496c20996 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -43,6 +43,7 @@ type ScrollAreaRootStateProps = WithRefProps< >; class ScrollAreaRootState { + readonly opts: ScrollAreaRootStateProps; scrollAreaNode = $state(null); viewportNode = $state(null); contentNode = $state(null); @@ -53,7 +54,9 @@ class ScrollAreaRootState { scrollbarXEnabled = $state(false); scrollbarYEnabled = $state(false); - constructor(readonly opts: ScrollAreaRootStateProps) { + constructor(opts: ScrollAreaRootStateProps) { + this.opts = opts; + useRefById({ ...opts, onRefChange: (node) => { @@ -80,13 +83,15 @@ class ScrollAreaRootState { type ScrollAreaViewportStateProps = WithRefProps; class ScrollAreaViewportState { + readonly opts: ScrollAreaViewportStateProps; + readonly root: ScrollAreaRootState; #contentId = box(useId()); #contentRef = box(null); - constructor( - readonly opts: ScrollAreaViewportStateProps, - readonly root: ScrollAreaRootState - ) { + constructor(opts: ScrollAreaViewportStateProps, root: ScrollAreaRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -139,13 +144,15 @@ type ScrollAreaScrollbarStateProps = WithRefProps< >; class ScrollAreaScrollbarState { + readonly opts: ScrollAreaScrollbarStateProps; + readonly root: ScrollAreaRootState; isHorizontal = $derived.by(() => this.opts.orientation.current === "horizontal"); hasThumb = $state(false); - constructor( - readonly opts: ScrollAreaScrollbarStateProps, - readonly root: ScrollAreaRootState - ) { + constructor(opts: ScrollAreaScrollbarStateProps, root: ScrollAreaRootState) { + this.opts = opts; + this.root = root; + $effect(() => { this.isHorizontal ? (this.root.scrollbarXEnabled = true) @@ -161,10 +168,12 @@ class ScrollAreaScrollbarState { } class ScrollAreaScrollbarHoverState { + readonly scrollbar: ScrollAreaScrollbarState; root: ScrollAreaRootState; isVisible = $state(false); - constructor(readonly scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; this.root = scrollbar.root; $effect(() => { @@ -208,6 +217,7 @@ class ScrollAreaScrollbarHoverState { } class ScrollAreaScrollbarScrollState { + readonly scrollbar: ScrollAreaScrollbarState; root: ScrollAreaRootState; machine = useStateMachine("hidden", { hidden: { @@ -229,7 +239,8 @@ class ScrollAreaScrollbarScrollState { }); isHidden = $derived.by(() => this.machine.state.current === "hidden"); - constructor(readonly scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; this.root = scrollbar.root; const debounceScrollend = useDebounce(() => this.machine.dispatch("SCROLL_END"), 100); @@ -289,10 +300,12 @@ class ScrollAreaScrollbarScrollState { } class ScrollAreaScrollbarAutoState { + readonly scrollbar: ScrollAreaScrollbarState; root: ScrollAreaRootState; isVisible = $state(false); - constructor(readonly scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; this.root = scrollbar.root; const handleResize = useDebounce(() => { @@ -316,6 +329,7 @@ class ScrollAreaScrollbarAutoState { } class ScrollAreaScrollbarVisibleState { + readonly scrollbar: ScrollAreaScrollbarState; root: ScrollAreaRootState; thumbNode = $state(null); pointerOffset = $state(0); @@ -326,9 +340,13 @@ class ScrollAreaScrollbarVisibleState { }); thumbRatio = $derived.by(() => getThumbRatio(this.sizes.viewport, this.sizes.content)); hasThumb = $derived.by(() => Boolean(this.thumbRatio > 0 && this.thumbRatio < 1)); - prevTransformStyle = ""; + // this needs to be a $state to properly restore the transform style when the scrollbar + // goes from a hidden to visible state, otherwise it will start at the beginning of the + // scrollbar and flicker to the correct position after + prevTransformStyle = $state(""); - constructor(readonly scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; this.root = scrollbar.root; $effect(() => { @@ -373,6 +391,7 @@ class ScrollAreaScrollbarVisibleState { }); const transformStyle = `translate3d(${offset}px, 0, 0)`; this.thumbNode.style.transform = transformStyle; + this.prevTransformStyle = transformStyle; } xOnWheelScroll(scrollPos: number) { @@ -394,6 +413,7 @@ class ScrollAreaScrollbarVisibleState { const offset = getThumbOffsetFromScroll({ scrollPos, sizes: this.sizes }); const transformStyle = `translate3d(0, ${offset}px, 0)`; this.thumbNode.style.transform = transformStyle; + this.prevTransformStyle = transformStyle; } yOnWheelScroll(scrollPos: number) { @@ -427,14 +447,14 @@ type ScrollbarAxisState = { }; class ScrollAreaScrollbarXState implements ScrollbarAxisState { + readonly opts: ScrollbarAxisStateProps; + readonly scrollbarVis: ScrollAreaScrollbarVisibleState; root: ScrollAreaRootState; computedStyle = $state(); scrollbar: ScrollAreaScrollbarState; - constructor( - readonly opts: ScrollbarAxisStateProps, - readonly scrollbarVis: ScrollAreaScrollbarVisibleState - ) { + constructor(opts: ScrollbarAxisStateProps, scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.opts = opts; this.scrollbarVis = scrollbarVis; this.root = scrollbarVis.root; this.scrollbar = scrollbarVis.scrollbar; @@ -527,14 +547,15 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { } class ScrollAreaScrollbarYState implements ScrollbarAxisState { + readonly opts: ScrollbarAxisStateProps; + readonly scrollbarVis: ScrollAreaScrollbarVisibleState; root: ScrollAreaRootState; scrollbar: ScrollAreaScrollbarState; computedStyle = $state(); - constructor( - readonly opts: ScrollbarAxisStateProps, - readonly scrollbarVis: ScrollAreaScrollbarVisibleState - ) { + constructor(opts: ScrollbarAxisStateProps, scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.opts = opts; + this.scrollbarVis = scrollbarVis; this.root = scrollbarVis.root; this.scrollbar = scrollbarVis.scrollbar; @@ -630,6 +651,7 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { type ScrollbarAxis = ScrollAreaScrollbarXState | ScrollAreaScrollbarYState; class ScrollAreaScrollbarSharedState { + readonly scrollbarState: ScrollbarAxis; root: ScrollAreaRootState; scrollbarVis: ScrollAreaScrollbarVisibleState; scrollbar: ScrollAreaScrollbarState; @@ -644,7 +666,8 @@ class ScrollAreaScrollbarSharedState { () => this.scrollbarVis.sizes.content - this.scrollbarVis.sizes.viewport ); - constructor(readonly scrollbarState: ScrollbarAxis) { + constructor(scrollbarState: ScrollbarAxis) { + this.scrollbarState = scrollbarState; this.root = scrollbarState.root; this.scrollbarVis = scrollbarState.scrollbarVis; this.scrollbar = scrollbarState.scrollbarVis.scrollbar; @@ -672,13 +695,13 @@ class ScrollAreaScrollbarSharedState { return unsubListener; }); - $effect(() => { + $effect.pre(() => { // react to changes to this: this.scrollbarVis.sizes; untrack(() => this.handleThumbPositionChange()); }); - $effect(() => { + $effect.pre(() => { this.handleThumbPositionChange(); }); @@ -744,6 +767,8 @@ type ScrollAreaThumbImplStateProps = WithRefProps & mounted: boolean; }>; class ScrollAreaThumbImplState { + readonly opts: ScrollAreaThumbImplStateProps; + readonly scrollbarState: ScrollAreaScrollbarSharedState; #root: ScrollAreaRootState; #removeUnlinkedScrollListener = $state<() => void>(); #debounceScrollEnd = useDebounce(() => { @@ -754,9 +779,11 @@ class ScrollAreaThumbImplState { }, 100); constructor( - readonly opts: ScrollAreaThumbImplStateProps, - readonly scrollbarState: ScrollAreaScrollbarSharedState + opts: ScrollAreaThumbImplStateProps, + scrollbarState: ScrollAreaScrollbarSharedState ) { + this.opts = opts; + this.scrollbarState = scrollbarState; this.#root = scrollbarState.root; useRefById({ @@ -823,14 +850,16 @@ class ScrollAreaThumbImplState { type ScrollAreaCornerImplStateProps = WithRefProps; class ScrollAreaCornerImplState { + readonly opts: ScrollAreaCornerImplStateProps; + readonly root: ScrollAreaRootState; #width = $state(0); #height = $state(0); hasSize = $derived(Boolean(this.#width && this.#height)); - constructor( - readonly opts: ScrollAreaCornerImplStateProps, - readonly root: ScrollAreaRootState - ) { + constructor(opts: ScrollAreaCornerImplStateProps, root: ScrollAreaRootState) { + this.opts = opts; + this.root = root; + useResizeObserver( () => this.root.scrollbarXNode, () => { diff --git a/packages/bits-ui/src/lib/bits/select/components/select-scroll-down-button.svelte b/packages/bits-ui/src/lib/bits/select/components/select-scroll-down-button.svelte index 5274d1c0b..4c26718d8 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select-scroll-down-button.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select-scroll-down-button.svelte @@ -8,6 +8,7 @@ let { id = useId(), ref = $bindable(null), + delay = () => 50, child, children, ...restProps @@ -19,13 +20,14 @@ () => ref, (v) => (ref = v) ), + delay: box.with(() => delay), }); const mergedProps = $derived(mergeProps(restProps, scrollButtonState.props)); {#if scrollButtonState.canScrollDown} - + {#if child} {@render child({ props: restProps })} {:else} diff --git a/packages/bits-ui/src/lib/bits/select/components/select-scroll-up-button.svelte b/packages/bits-ui/src/lib/bits/select/components/select-scroll-up-button.svelte index d05d552c8..ce47c4253 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select-scroll-up-button.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select-scroll-up-button.svelte @@ -8,6 +8,7 @@ let { id = useId(), ref = $bindable(null), + delay = () => 50, child, children, ...restProps @@ -19,13 +20,14 @@ () => ref, (v) => (ref = v) ), + delay: box.with(() => delay), }); const mergedProps = $derived(mergeProps(restProps, scrollButtonState.props)); {#if scrollButtonState.canScrollUp} - + {#if child} {@render child({ props: restProps })} {:else} diff --git a/packages/bits-ui/src/lib/bits/select/components/select.svelte b/packages/bits-ui/src/lib/bits/select/components/select.svelte index 97ca4c286..fd99ee155 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select.svelte @@ -5,6 +5,7 @@ import { useSelectRoot } from "../select.svelte.js"; import type { SelectRootProps } from "../types.js"; import SelectHiddenInput from "./select-hidden-input.svelte"; + import { watch } from "runed"; let { value = $bindable(), @@ -22,12 +23,21 @@ children, }: SelectRootProps = $props(); - if (value === undefined) { - const defaultValue = type === "single" ? "" : []; - - value = defaultValue; + function handleDefaultValue() { + if (value !== undefined) return; + value = type === "single" ? "" : []; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useSelectRoot({ type, value: box.with( diff --git a/packages/bits-ui/src/lib/bits/select/select.svelte.ts b/packages/bits-ui/src/lib/bits/select/select.svelte.ts index 39df29ba4..1c7a53bd1 100644 --- a/packages/bits-ui/src/lib/bits/select/select.svelte.ts +++ b/packages/bits-ui/src/lib/bits/select/select.svelte.ts @@ -51,6 +51,7 @@ type SelectBaseRootStateProps = ReadableBoxedValues<{ }; class SelectBaseRootState { + readonly opts: SelectBaseRootStateProps; touchedInput = $state(false); inputValue = $state(""); inputNode = $state(null); @@ -74,7 +75,8 @@ class SelectBaseRootState { isCombobox = false; bitsAttrs: SelectBitsAttrs; - constructor(readonly opts: SelectBaseRootStateProps) { + constructor(opts: SelectBaseRootStateProps) { + this.opts = opts; this.isCombobox = opts.isCombobox; this.bitsAttrs = getSelectBitsAttrs(this); $effect.pre(() => { @@ -87,17 +89,16 @@ class SelectBaseRootState { setHighlightedNode(node: HTMLElement | null, initial = false) { this.highlightedNode = node; if (node && (this.isUsingKeyboard || initial)) { - node.scrollIntoView({ block: "nearest" }); + node.scrollIntoView({ block: this.opts.scrollAlignment.current }); } } getCandidateNodes(): HTMLElement[] { const node = this.contentNode; if (!node) return []; - const nodes = Array.from( + return Array.from( node.querySelectorAll(`[${this.bitsAttrs.item}]:not([data-disabled])`) ); - return nodes; } setHighlightedToFirstCandidate() { @@ -140,6 +141,7 @@ type SelectSingleRootStateProps = SelectBaseRootStateProps & }>; class SelectSingleRootState extends SelectBaseRootState { + readonly opts: SelectSingleRootStateProps; isMulti = false as const; hasValue = $derived.by(() => this.opts.value.current !== ""); currentLabel = $derived.by(() => { @@ -160,9 +162,11 @@ class SelectSingleRootState extends SelectBaseRootState { return true; }); - constructor(readonly opts: SelectSingleRootStateProps) { + constructor(opts: SelectSingleRootStateProps) { super(opts); + this.opts = opts; + $effect(() => { if (!this.opts.open.current && this.highlightedNode) { this.setHighlightedNode(null); @@ -211,12 +215,15 @@ type SelectMultipleRootStateProps = SelectBaseRootStateProps & }>; class SelectMultipleRootState extends SelectBaseRootState { + readonly opts: SelectMultipleRootStateProps; isMulti = true as const; hasValue = $derived.by(() => this.opts.value.current.length > 0); - constructor(readonly opts: SelectMultipleRootStateProps) { + constructor(opts: SelectMultipleRootStateProps) { super(opts); + this.opts = opts; + $effect(() => { if (!this.opts.open.current && this.highlightedNode) { this.setHighlightedNode(null); @@ -271,10 +278,13 @@ type SelectInputStateProps = WithRefProps & }>; class SelectInputState { - constructor( - readonly opts: SelectInputStateProps, - readonly root: SelectRootState - ) { + readonly opts: SelectInputStateProps; + readonly root: SelectRootState; + + constructor(opts: SelectInputStateProps, root: SelectRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -423,10 +433,13 @@ class SelectInputState { type SelectComboTriggerStateProps = WithRefProps; class SelectComboTriggerState { - constructor( - readonly opts: SelectComboTriggerStateProps, - readonly root: SelectBaseRootState - ) { + readonly opts: SelectComboTriggerStateProps; + readonly root: SelectBaseRootState; + + constructor(opts: SelectComboTriggerStateProps, root: SelectBaseRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); this.onkeydown = this.onkeydown.bind(this); @@ -474,13 +487,15 @@ class SelectComboTriggerState { type SelectTriggerStateProps = WithRefProps; class SelectTriggerState { + readonly opts: SelectTriggerStateProps; + readonly root: SelectRootState; #domTypeahead: DOMTypeahead; #dataTypeahead: DataTypeahead; - constructor( - readonly opts: SelectTriggerStateProps, - readonly root: SelectRootState - ) { + constructor(opts: SelectTriggerStateProps, root: SelectRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -736,13 +751,15 @@ type SelectContentStateProps = WithRefProps & }>; class SelectContentState { + readonly opts: SelectContentStateProps; + readonly root: SelectRootState; viewportNode = $state(null); isPositioned = $state(false); - constructor( - readonly opts: SelectContentStateProps, - readonly root: SelectRootState - ) { + constructor(opts: SelectContentStateProps, root: SelectRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -836,7 +853,11 @@ class SelectContentState { trapFocus: false, loop: false, onPlaced: () => { - this.isPositioned = true; + // onPlaced is also called when the menu is closed, so we need to check if the menu + // is actually open to avoid setting positioning to true when the menu is closed + if (this.root.opts.open.current) { + this.isPositioned = true; + } }, }; } @@ -852,15 +873,17 @@ type SelectItemStateProps = WithRefProps< >; class SelectItemState { + readonly opts: SelectItemStateProps; + readonly root: SelectRootState; isSelected = $derived.by(() => this.root.includesItem(this.opts.value.current)); isHighlighted = $derived.by(() => this.root.highlightedValue === this.opts.value.current); prevHighlighted = new Previous(() => this.isHighlighted); mounted = $state(false); - constructor( - readonly opts: SelectItemStateProps, - readonly root: SelectRootState - ) { + constructor(opts: SelectItemStateProps, root: SelectRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, deps: () => this.mounted, @@ -978,7 +1001,10 @@ class SelectItemState { "data-value": this.opts.value.current, "data-disabled": getDataDisabled(this.opts.disabled.current), "data-highlighted": - this.root.highlightedValue === this.opts.value.current ? "" : undefined, + this.root.highlightedValue === this.opts.value.current && + !this.opts.disabled.current + ? "" + : undefined, "data-selected": this.root.includesItem(this.opts.value.current) ? "" : undefined, "data-label": this.opts.label.current, [this.root.bitsAttrs.item]: "", @@ -992,12 +1018,14 @@ class SelectItemState { type SelectGroupStateProps = WithRefProps; class SelectGroupState { + readonly opts: SelectGroupStateProps; + readonly root: SelectBaseRootState; labelNode = $state(null); - constructor( - readonly opts: SelectGroupStateProps, - readonly root: SelectBaseRootState - ) { + constructor(opts: SelectGroupStateProps, root: SelectBaseRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -1015,10 +1043,13 @@ class SelectGroupState { type SelectGroupHeadingStateProps = WithRefProps; class SelectGroupHeadingState { - constructor( - readonly opts: SelectGroupHeadingStateProps, - readonly group: SelectGroupState - ) { + readonly opts: SelectGroupHeadingStateProps; + readonly group: SelectGroupState; + + constructor(opts: SelectGroupHeadingStateProps, group: SelectGroupState) { + this.opts = opts; + this.group = group; + useRefById({ ...opts, onRefChange: (node) => { @@ -1041,12 +1072,13 @@ type SelectHiddenInputStateProps = ReadableBoxedValues<{ }>; class SelectHiddenInputState { + readonly opts: SelectHiddenInputStateProps; + readonly root: SelectBaseRootState; shouldRender = $derived.by(() => this.root.opts.name.current !== ""); - constructor( - readonly opts: SelectHiddenInputStateProps, - readonly root: SelectBaseRootState - ) { + constructor(opts: SelectHiddenInputStateProps, root: SelectBaseRootState) { + this.opts = opts; + this.root = root; this.onfocus = this.onfocus.bind(this); } @@ -1075,13 +1107,14 @@ class SelectHiddenInputState { type SelectViewportStateProps = WithRefProps; class SelectViewportState { + readonly opts: SelectViewportStateProps; + readonly content: SelectContentState; root: SelectBaseRootState; prevScrollTop = $state(0); - constructor( - readonly opts: SelectViewportStateProps, - readonly content: SelectContentState - ) { + constructor(opts: SelectViewportStateProps, content: SelectContentState) { + this.opts = opts; + this.content = content; this.root = content.root; useRefById({ @@ -1111,20 +1144,24 @@ class SelectViewportState { ); } -type SelectScrollButtonImplStateProps = WithRefProps; +type SelectScrollButtonImplStateProps = WithRefProps & + ReadableBoxedValues<{ + delay: (tick: number) => number; + }>; class SelectScrollButtonImplState { + readonly opts: SelectScrollButtonImplStateProps; + readonly content: SelectContentState; root: SelectBaseRootState; - autoScrollInterval: number | null = null; + autoScrollTimer: number | null = null; userScrollTimer = -1; isUserScrolling = false; onAutoScroll: () => void = noop; mounted = $state(false); - constructor( - readonly opts: SelectScrollButtonImplStateProps, - readonly content: SelectContentState - ) { + constructor(opts: SelectScrollButtonImplStateProps, content: SelectContentState) { + this.opts = opts; + this.content = content; this.root = content.root; useRefById({ @@ -1159,23 +1196,25 @@ class SelectScrollButtonImplState { } clearAutoScrollInterval() { - if (this.autoScrollInterval === null) return; - window.clearInterval(this.autoScrollInterval); - this.autoScrollInterval = null; + if (this.autoScrollTimer === null) return; + window.clearTimeout(this.autoScrollTimer); + this.autoScrollTimer = null; } onpointerdown(_: BitsPointerEvent) { - if (this.autoScrollInterval !== null) return; - this.autoScrollInterval = window.setInterval(() => { + if (this.autoScrollTimer !== null) return; + const autoScroll = (tick: number) => { this.onAutoScroll(); - }, 50); + this.autoScrollTimer = window.setTimeout( + () => autoScroll(tick + 1), + this.opts.delay.current(tick) + ); + }; + this.autoScrollTimer = window.setTimeout(() => autoScroll(1), this.opts.delay.current(0)); } - onpointermove(_: BitsPointerEvent) { - if (this.autoScrollInterval !== null) return; - this.autoScrollInterval = window.setInterval(() => { - this.onAutoScroll(); - }, 50); + onpointermove(e: BitsPointerEvent) { + this.onpointerdown(e); } onpointerleave(_: BitsPointerEvent) { @@ -1198,36 +1237,35 @@ class SelectScrollButtonImplState { } class SelectScrollDownButtonState { + readonly scrollButtonState: SelectScrollButtonImplState; content: SelectContentState; root: SelectBaseRootState; canScrollDown = $state(false); scrollIntoViewTimer: ReturnType | null = null; - constructor(readonly state: SelectScrollButtonImplState) { - this.content = state.content; - this.root = state.root; - this.state.onAutoScroll = this.handleAutoScroll; + constructor(scrollButtonState: SelectScrollButtonImplState) { + this.scrollButtonState = scrollButtonState; + this.content = scrollButtonState.content; + this.root = scrollButtonState.root; + this.scrollButtonState.onAutoScroll = this.handleAutoScroll; watch([() => this.content.viewportNode, () => this.content.isPositioned], () => { - if (!this.content.viewportNode || !this.content.isPositioned) { - return; - } - + if (!this.content.viewportNode || !this.content.isPositioned) return; this.handleScroll(true); return on(this.content.viewportNode, "scroll", () => this.handleScroll()); }); watch( - () => this.state.mounted, + () => this.scrollButtonState.mounted, () => { - if (!this.state.mounted) return; + if (!this.scrollButtonState.mounted) return; if (this.scrollIntoViewTimer) { clearTimeout(this.scrollIntoViewTimer); } this.scrollIntoViewTimer = afterSleep(5, () => { const activeItem = this.root.highlightedNode; - activeItem?.scrollIntoView({ block: "nearest" }); + activeItem?.scrollIntoView({ block: this.root.opts.scrollAlignment.current }); }); } ); @@ -1238,7 +1276,7 @@ class SelectScrollDownButtonState { */ handleScroll = (manual = false) => { if (!manual) { - this.state.handleUserScroll(); + this.scrollButtonState.handleUserScroll(); } if (!this.content.viewportNode) return; const maxScroll = @@ -1260,19 +1298,25 @@ class SelectScrollDownButtonState { }; props = $derived.by( - () => ({ ...this.state.props, [this.root.bitsAttrs["scroll-down-button"]]: "" }) as const + () => + ({ + ...this.scrollButtonState.props, + [this.root.bitsAttrs["scroll-down-button"]]: "", + }) as const ); } class SelectScrollUpButtonState { + readonly scrollButtonState: SelectScrollButtonImplState; content: SelectContentState; root: SelectBaseRootState; canScrollUp = $state(false); - constructor(readonly state: SelectScrollButtonImplState) { - this.content = state.content; - this.root = state.root; - this.state.onAutoScroll = this.handleAutoScroll; + constructor(scrollButtonState: SelectScrollButtonImplState) { + this.scrollButtonState = scrollButtonState; + this.content = scrollButtonState.content; + this.root = scrollButtonState.root; + this.scrollButtonState.onAutoScroll = this.handleAutoScroll; watch([() => this.content.viewportNode, () => this.content.isPositioned], () => { if (!this.content.viewportNode || !this.content.isPositioned) return; @@ -1288,7 +1332,7 @@ class SelectScrollUpButtonState { */ handleScroll = (manual = false) => { if (!manual) { - this.state.handleUserScroll(); + this.scrollButtonState.handleUserScroll(); } if (!this.content.viewportNode) return; const paddingTop = Number.parseInt( @@ -1305,7 +1349,11 @@ class SelectScrollUpButtonState { }; props = $derived.by( - () => ({ ...this.state.props, [this.root.bitsAttrs["scroll-up-button"]]: "" }) as const + () => + ({ + ...this.scrollButtonState.props, + [this.root.bitsAttrs["scroll-up-button"]]: "", + }) as const ); } diff --git a/packages/bits-ui/src/lib/bits/select/types.ts b/packages/bits-ui/src/lib/bits/select/types.ts index 50f5419c8..da1de8962 100644 --- a/packages/bits-ui/src/lib/bits/select/types.ts +++ b/packages/bits-ui/src/lib/bits/select/types.ts @@ -262,12 +262,22 @@ export type SelectViewportPropsWithoutHTML = WithChild; export type SelectViewportProps = SelectViewportPropsWithoutHTML & Without; -export type SelectScrollUpButtonPropsWithoutHTML = WithChild; +export type SelectScrollButtonPropsWithoutHTML = WithChild<{ + /** + * Controls the initial delay (tick 0) and delay between auto-scrolls in milliseconds. + * The default function always returns 50ms. + * + * @param tick current tick number + */ + delay?: (tick: number) => number; +}>; + +export type SelectScrollUpButtonPropsWithoutHTML = SelectScrollButtonPropsWithoutHTML; export type SelectScrollUpButtonProps = SelectScrollUpButtonPropsWithoutHTML & Without; -export type SelectScrollDownButtonPropsWithoutHTML = WithChild; +export type SelectScrollDownButtonPropsWithoutHTML = SelectScrollButtonPropsWithoutHTML; export type SelectScrollDownButtonProps = SelectScrollDownButtonPropsWithoutHTML & Without; diff --git a/packages/bits-ui/src/lib/bits/separator/separator.svelte.ts b/packages/bits-ui/src/lib/bits/separator/separator.svelte.ts index eb79a24aa..272307ad8 100644 --- a/packages/bits-ui/src/lib/bits/separator/separator.svelte.ts +++ b/packages/bits-ui/src/lib/bits/separator/separator.svelte.ts @@ -14,7 +14,11 @@ type SeparatorRootStateProps = WithRefProps< >; class SeparatorRootState { - constructor(readonly opts: SeparatorRootStateProps) { + readonly opts: SeparatorRootStateProps; + + constructor(opts: SeparatorRootStateProps) { + this.opts = opts; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/slider/components/slider-thumb.svelte b/packages/bits-ui/src/lib/bits/slider/components/slider-thumb.svelte index 8f7df238a..f844bc3f0 100644 --- a/packages/bits-ui/src/lib/bits/slider/components/slider-thumb.svelte +++ b/packages/bits-ui/src/lib/bits/slider/components/slider-thumb.svelte @@ -28,9 +28,14 @@ {#if child} - {@render child({ props: mergedProps })} + {@render child({ + active: thumbState.root.isThumbActive(thumbState.opts.index.current), + props: mergedProps, + })} {:else} - {@render children?.()} + {@render children?.({ + active: thumbState.root.isThumbActive(thumbState.opts.index.current), + })} {/if} diff --git a/packages/bits-ui/src/lib/bits/slider/components/slider.svelte b/packages/bits-ui/src/lib/bits/slider/components/slider.svelte index aadce8b69..058deb3c5 100644 --- a/packages/bits-ui/src/lib/bits/slider/components/slider.svelte +++ b/packages/bits-ui/src/lib/bits/slider/components/slider.svelte @@ -4,6 +4,7 @@ import { useSliderRoot } from "../slider.svelte.js"; import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; + import { watch } from "runed"; let { children, @@ -21,13 +22,25 @@ dir = "ltr", autoSort = true, orientation = "horizontal", + thumbPositioning = "contain", ...restProps }: SliderRootProps = $props(); - if (value === undefined) { + function handleDefaultValue() { + if (value !== undefined) return; value = type === "single" ? 0 : []; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useSliderRoot({ id: box.with(() => id), ref: box.with( @@ -51,6 +64,7 @@ dir: box.with(() => dir), autoSort: box.with(() => autoSort), orientation: box.with(() => orientation), + thumbPositioning: box.with(() => thumbPositioning), type, }); diff --git a/packages/bits-ui/src/lib/bits/slider/slider.svelte.ts b/packages/bits-ui/src/lib/bits/slider/slider.svelte.ts index 0623f5458..670f2d050 100644 --- a/packages/bits-ui/src/lib/bits/slider/slider.svelte.ts +++ b/packages/bits-ui/src/lib/bits/slider/slider.svelte.ts @@ -24,7 +24,7 @@ import { isElementOrSVGElement } from "$lib/internal/is.js"; import { isValidIndex } from "$lib/internal/arrays.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import type { BitsKeyboardEvent, OnChangeFn, WithRefProps } from "$lib/internal/types.js"; -import type { Direction, Orientation } from "$lib/shared/index.js"; +import type { Direction, Orientation, SliderThumbPositioning } from "$lib/shared/index.js"; import { linearScale, snapValueToStep } from "$lib/internal/math.js"; const SLIDER_ROOT_ATTR = "data-slider-root"; @@ -41,10 +41,12 @@ type SliderBaseRootStateProps = WithRefProps< step: number; dir: Direction; autoSort: boolean; + thumbPositioning: SliderThumbPositioning; }> >; class SliderBaseRootState { + readonly opts: SliderBaseRootStateProps; isActive = $state(false); direction: "rl" | "lr" | "tb" | "bt" = $derived.by(() => { if (this.opts.orientation.current === "horizontal") { @@ -54,10 +56,16 @@ class SliderBaseRootState { } }); - constructor(readonly opts: SliderBaseRootStateProps) { + constructor(opts: SliderBaseRootStateProps) { + this.opts = opts; + useRefById(opts); } + isThumbActive(_index: number) { + return this.isActive; + } + #touchAction = $derived.by(() => { if (this.opts.disabled.current) return undefined; return this.opts.orientation.current === "horizontal" ? "pan-y" : "pan-x"; @@ -70,6 +78,11 @@ class SliderBaseRootState { }; getThumbScale = (): [number, number] => { + if (this.opts.thumbPositioning.current === "exact") { + // User opted out of containment + return [0, 100]; + } + const isVertical = this.opts.orientation.current === "vertical"; // this assumes all thumbs are the same width @@ -127,11 +140,14 @@ type SliderSingleRootStateProps = SliderBaseRootStateProps & }>; class SliderSingleRootState extends SliderBaseRootState { + readonly opts: SliderSingleRootStateProps; isMulti = false as const; - constructor(readonly opts: SliderSingleRootStateProps) { + constructor(opts: SliderSingleRootStateProps) { super(opts); + this.opts = opts; + onMountEffect(() => { return executeCallbacks( on(document, "pointerdown", this.handlePointerDown), @@ -305,12 +321,9 @@ class SliderSingleRootState extends SliderBaseRootState { const currValue = this.opts.value.current; return Array.from({ length: count }, (_, i) => { - const tickPosition = i * (step / difference) * 100; + const tickPosition = i * step; - const scale = linearScale( - [this.opts.min.current, this.opts.max.current], - this.getThumbScale() - ); + const scale = linearScale([0, (count - 1) * step], this.getThumbScale()); const isFirst = i === 0; const isLast = i === count - 1; @@ -353,13 +366,16 @@ type SliderMultiRootStateProps = SliderBaseRootStateProps & }>; class SliderMultiRootState extends SliderBaseRootState { + readonly opts: SliderMultiRootStateProps; isMulti = true as const; activeThumb = $state<{ node: HTMLElement; idx: number } | null>(null); currentThumbIdx = $state(0); - constructor(readonly opts: SliderMultiRootStateProps) { + constructor(opts: SliderMultiRootStateProps) { super(opts); + this.opts = opts; + onMountEffect(() => { return executeCallbacks( on(document, "pointerdown", this.handlePointerDown), @@ -393,6 +409,10 @@ class SliderMultiRootState extends SliderBaseRootState { ); } + isThumbActive(index: number): boolean { + return this.isActive && this.activeThumb?.idx === index; + } + applyPosition({ clientXY, activeThumbIdx, @@ -623,12 +643,15 @@ class SliderMultiRootState extends SliderBaseRootState { const currValue = this.opts.value.current; return Array.from({ length: count }, (_, i) => { - const tickPosition = i * (step / difference) * 100; + const tickPosition = i * step; + + const scale = linearScale([0, (count - 1) * step], this.getThumbScale()); const isFirst = i === 0; const isLast = i === count - 1; const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50; - const style = getTickStyles(this.direction, tickPosition, offsetPercentage); + + const style = getTickStyles(this.direction, scale(tickPosition), offsetPercentage); const tickValue = min + i * step; const bounded = currValue.length === 1 @@ -671,10 +694,13 @@ const VALID_SLIDER_KEYS = [ type SliderRangeStateProps = WithRefProps; class SliderRangeState { - constructor( - readonly opts: SliderRangeStateProps, - readonly root: SliderRootState - ) { + readonly opts: SliderRangeStateProps; + readonly root: SliderRootState; + + constructor(opts: SliderRangeStateProps, root: SliderRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -712,12 +738,14 @@ type SliderThumbStateProps = WithRefProps & }>; class SliderThumbState { + readonly opts: SliderThumbStateProps; + readonly root: SliderRootState; #isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current); - constructor( - readonly opts: SliderThumbStateProps, - readonly root: SliderRootState - ) { + constructor(opts: SliderThumbStateProps, root: SliderRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); this.onkeydown = this.onkeydown.bind(this); @@ -815,6 +843,7 @@ class SliderThumbState { ...this.root.thumbsPropsArr[this.opts.index.current]!, id: this.opts.id.current, onkeydown: this.onkeydown, + "data-active": this.root.isThumbActive(this.opts.index.current) ? "" : undefined, }) as const ); } @@ -825,10 +854,13 @@ type SliderTickStateProps = WithRefProps & }>; class SliderTickState { - constructor( - readonly opts: SliderTickStateProps, - readonly root: SliderRootState - ) { + readonly opts: SliderTickStateProps; + readonly root: SliderRootState; + + constructor(opts: SliderTickStateProps, root: SliderRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/slider/types.ts b/packages/bits-ui/src/lib/bits/slider/types.ts index c98594ad0..b09e3081c 100644 --- a/packages/bits-ui/src/lib/bits/slider/types.ts +++ b/packages/bits-ui/src/lib/bits/slider/types.ts @@ -1,6 +1,6 @@ import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { BitsPrimitiveSpanAttributes } from "$lib/shared/attributes.js"; -import type { Direction, Orientation } from "$lib/shared/index.js"; +import type { Direction, Orientation, SliderThumbPositioning } from "$lib/shared/index.js"; export type SliderRootSnippetProps = { ticks: number[]; @@ -60,6 +60,13 @@ export type BaseSliderRootPropsWithoutHTML = { * @defaultValue false */ disabled?: boolean; + + /** + * The positioning of the slider thumb. + * + * @defaultValue "contain" + */ + thumbPositioning?: SliderThumbPositioning; }; export type SliderSingleRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & { @@ -140,20 +147,25 @@ export type SliderRangePropsWithoutHTML = WithChild; export type SliderRangeProps = SliderRangePropsWithoutHTML & Without; -export type SliderThumbPropsWithoutHTML = WithChild<{ - /** - * Whether the thumb is disabled or not. - * - * @defaultValue false - */ - disabled?: boolean; - - /** - * The index of the thumb in the array of thumbs provided by the `children` snippet prop of the - * `Slider.Root` component. - */ - index: number; -}>; +export type SliderThumbSnippetProps = { active: boolean }; + +export type SliderThumbPropsWithoutHTML = WithChild< + { + /** + * Whether the thumb is disabled or not. + * + * @defaultValue false + */ + disabled?: boolean; + + /** + * The index of the thumb in the array of thumbs provided by the `children` snippet prop of the + * `Slider.Root` component. + */ + index: number; + }, + SliderThumbSnippetProps +>; export type SliderThumbProps = SliderThumbPropsWithoutHTML & Without; diff --git a/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts index 8c171f3ba..ebd2c5c49 100644 --- a/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts +++ b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts @@ -27,7 +27,11 @@ type SwitchRootStateProps = WithRefProps< }> >; class SwitchRootState { - constructor(readonly opts: SwitchRootStateProps) { + readonly opts: SwitchRootStateProps; + + constructor(opts: SwitchRootStateProps) { + this.opts = opts; + useRefById(opts); this.onkeydown = this.onkeydown.bind(this); @@ -77,9 +81,12 @@ class SwitchRootState { } class SwitchInputState { + readonly root: SwitchRootState; shouldRender = $derived.by(() => this.root.opts.name.current !== undefined); - constructor(readonly root: SwitchRootState) {} + constructor(root: SwitchRootState) { + this.root = root; + } props = $derived.by( () => @@ -97,10 +104,13 @@ class SwitchInputState { type SwitchThumbStateProps = WithRefProps; class SwitchThumbState { - constructor( - readonly opts: SwitchThumbStateProps, - readonly root: SwitchRootState - ) { + readonly opts: SwitchThumbStateProps; + readonly root: SwitchRootState; + + constructor(opts: SwitchThumbStateProps, root: SwitchRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } diff --git a/packages/bits-ui/src/lib/bits/tabs/tabs.svelte.ts b/packages/bits-ui/src/lib/bits/tabs/tabs.svelte.ts index fce5f813b..924c40ab5 100644 --- a/packages/bits-ui/src/lib/bits/tabs/tabs.svelte.ts +++ b/packages/bits-ui/src/lib/bits/tabs/tabs.svelte.ts @@ -42,6 +42,7 @@ type TabsRootStateProps = WithRefProps< >; class TabsRootState { + readonly opts: TabsRootStateProps; rovingFocusGroup: UseRovingFocusReturn; triggerIds = $state([]); // holds the trigger ID for each value to associate it with the content @@ -49,7 +50,9 @@ class TabsRootState { // holds the content ID for each value to associate it with the trigger valueToContentId = new SvelteMap(); - constructor(readonly opts: TabsRootStateProps) { + constructor(opts: TabsRootStateProps) { + this.opts = opts; + useRefById(opts); this.rovingFocusGroup = useRovingFocus({ @@ -101,12 +104,14 @@ class TabsRootState { type TabsListStateProps = WithRefProps; class TabsListState { + readonly opts: TabsListStateProps; + readonly root: TabsRootState; #isDisabled = $derived.by(() => this.root.opts.disabled.current); - constructor( - readonly opts: TabsListStateProps, - readonly root: TabsRootState - ) { + constructor(opts: TabsListStateProps, root: TabsRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -135,15 +140,17 @@ type TabsTriggerStateProps = WithRefProps< >; class TabsTriggerState { + readonly opts: TabsTriggerStateProps; + readonly root: TabsRootState; #isActive = $derived.by(() => this.root.opts.value.current === this.opts.value.current); #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); #tabIndex = $state(0); #ariaControls = $derived.by(() => this.root.valueToContentId.get(this.opts.value.current)); - constructor( - readonly opts: TabsTriggerStateProps, - readonly root: TabsRootState - ) { + constructor(opts: TabsTriggerStateProps, root: TabsRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); watch([() => this.opts.id.current, () => this.opts.value.current], ([id, value]) => { @@ -220,13 +227,15 @@ type TabsContentStateProps = WithRefProps< >; class TabsContentState { + readonly opts: TabsContentStateProps; + readonly root: TabsRootState; #isActive = $derived.by(() => this.root.opts.value.current === this.opts.value.current); #ariaLabelledBy = $derived.by(() => this.root.valueToTriggerId.get(this.opts.value.current)); - constructor( - readonly opts: TabsContentStateProps, - readonly root: TabsRootState - ) { + constructor(opts: TabsContentStateProps, root: TabsRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); watch([() => this.opts.id.current, () => this.opts.value.current], ([id, value]) => { diff --git a/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte b/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte index a0a3eeb77..dda0d6d93 100644 --- a/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte +++ b/packages/bits-ui/src/lib/bits/toggle-group/components/toggle-group.svelte @@ -5,6 +5,7 @@ import { useToggleGroupRoot } from "../toggle-group.svelte.js"; import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; + import { watch } from "runed"; let { id = useId(), @@ -21,12 +22,21 @@ ...restProps }: ToggleGroupRootProps = $props(); - if (value === undefined) { - const defaultValue = type === "single" ? "" : []; - - value = defaultValue; + function handleDefaultValue() { + if (value !== undefined) return; + value = type === "single" ? "" : []; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const rootState = useToggleGroupRoot({ id: box.with(() => id), value: box.with( diff --git a/packages/bits-ui/src/lib/bits/toggle-group/toggle-group.svelte.ts b/packages/bits-ui/src/lib/bits/toggle-group/toggle-group.svelte.ts index b6141f9b3..48e6ba1f1 100644 --- a/packages/bits-ui/src/lib/bits/toggle-group/toggle-group.svelte.ts +++ b/packages/bits-ui/src/lib/bits/toggle-group/toggle-group.svelte.ts @@ -29,9 +29,11 @@ type ToggleGroupBaseStateProps = WithRefProps< >; class ToggleGroupBaseState { + readonly opts: ToggleGroupBaseStateProps; rovingFocusGroup: UseRovingFocusReturn; - constructor(readonly opts: ToggleGroupBaseStateProps) { + constructor(opts: ToggleGroupBaseStateProps) { + this.opts = opts; this.rovingFocusGroup = useRovingFocus({ candidateAttr: TOGGLE_GROUP_ITEM_ATTR, rootNodeId: opts.id, @@ -64,11 +66,13 @@ type ToggleGroupSingleStateProps = ToggleGroupBaseStateProps & }>; class ToggleGroupSingleState extends ToggleGroupBaseState { + readonly opts: ToggleGroupSingleStateProps; isMulti = false; anyPressed = $derived.by(() => this.opts.value.current !== ""); - constructor(readonly opts: ToggleGroupSingleStateProps) { + constructor(opts: ToggleGroupSingleStateProps) { super(opts); + this.opts = opts; } includesItem(item: string) { @@ -95,11 +99,14 @@ type ToggleGroupMultipleStateProps = ToggleGroupBaseStateProps & }>; class ToggleGroupMultipleState extends ToggleGroupBaseState { + readonly opts: ToggleGroupMultipleStateProps; isMulti = true; anyPressed = $derived.by(() => this.opts.value.current.length > 0); - constructor(readonly opts: ToggleGroupMultipleStateProps) { + constructor(opts: ToggleGroupMultipleStateProps) { super(opts); + + this.opts = opts; } includesItem(item: string) { @@ -130,12 +137,14 @@ type ToggleGroupItemStateProps = WithRefProps< >; class ToggleGroupItemState { + readonly opts: ToggleGroupItemStateProps; + readonly root: ToggleGroupState; #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); - constructor( - readonly opts: ToggleGroupItemStateProps, - readonly root: ToggleGroupState - ) { + constructor(opts: ToggleGroupItemStateProps, root: ToggleGroupState) { + this.opts = opts; + this.root = root; + useRefById(opts); $effect(() => { diff --git a/packages/bits-ui/src/lib/bits/toggle/toggle.svelte.ts b/packages/bits-ui/src/lib/bits/toggle/toggle.svelte.ts index 9e48ca3a4..90da4ab1e 100644 --- a/packages/bits-ui/src/lib/bits/toggle/toggle.svelte.ts +++ b/packages/bits-ui/src/lib/bits/toggle/toggle.svelte.ts @@ -15,7 +15,11 @@ type ToggleRootStateProps = WithRefProps< >; class ToggleRootState { - constructor(readonly opts: ToggleRootStateProps) { + readonly opts: ToggleRootStateProps; + + constructor(opts: ToggleRootStateProps) { + this.opts = opts; + useRefById(opts); this.onclick = this.onclick.bind(this); diff --git a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte index 88c297130..4cc72a3f7 100644 --- a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte +++ b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-group.svelte @@ -5,6 +5,7 @@ import { useToolbarGroup } from "../toolbar.svelte.js"; import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; + import { watch } from "runed"; let { id = useId(), @@ -18,11 +19,21 @@ ...restProps }: ToolbarGroupProps = $props(); - if (value === undefined) { - const defaultValue = type === "single" ? "" : []; - value = defaultValue; + function handleDefaultValue() { + if (value !== undefined) return; + value = type === "single" ? "" : []; } + // SSR + handleDefaultValue(); + + watch.pre( + () => value, + () => { + handleDefaultValue(); + } + ); + const groupState = useToolbarGroup({ id: box.with(() => id), disabled: box.with(() => disabled), diff --git a/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts b/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts index f97a2393c..43da49c3f 100644 --- a/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts @@ -32,9 +32,12 @@ type ToolbarRootStateProps = WithRefProps< >; class ToolbarRootState { + readonly opts: ToolbarRootStateProps; rovingFocusGroup: UseRovingFocusReturn; - constructor(readonly opts: ToolbarRootStateProps) { + constructor(opts: ToolbarRootStateProps) { + this.opts = opts; + useRefById(opts); this.rovingFocusGroup = useRovingFocus({ @@ -63,10 +66,13 @@ type ToolbarGroupBaseStateProps = WithRefProps< >; class ToolbarGroupBaseState { - constructor( - readonly opts: ToolbarGroupBaseStateProps, - readonly root: ToolbarRootState - ) { + readonly opts: ToolbarGroupBaseStateProps; + readonly root: ToolbarRootState; + + constructor(opts: ToolbarGroupBaseStateProps, root: ToolbarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); } @@ -92,14 +98,16 @@ type ToolbarGroupSingleStateProps = ToolbarGroupBaseStateProps & }>; class ToolbarGroupSingleState extends ToolbarGroupBaseState { + readonly opts: ToolbarGroupSingleStateProps; + readonly root: ToolbarRootState; isMulti = false; anyPressed = $derived.by(() => this.opts.value.current !== ""); - constructor( - readonly opts: ToolbarGroupSingleStateProps, - readonly root: ToolbarRootState - ) { + constructor(opts: ToolbarGroupSingleStateProps, root: ToolbarRootState) { super(opts, root); + + this.opts = opts; + this.root = root; } includesItem(item: string) { @@ -125,14 +133,16 @@ type ToolbarGroupMultipleStateProps = ToolbarGroupBaseStateProps & }>; class ToolbarGroupMultipleState extends ToolbarGroupBaseState { + readonly opts: ToolbarGroupMultipleStateProps; + readonly root: ToolbarRootState; isMulti = true; anyPressed = $derived.by(() => this.opts.value.current.length > 0); - constructor( - readonly opts: ToolbarGroupMultipleStateProps, - readonly root: ToolbarRootState - ) { + constructor(opts: ToolbarGroupMultipleStateProps, root: ToolbarRootState) { super(opts, root); + + this.opts = opts; + this.root = root; } includesItem(item: string) { @@ -162,13 +172,20 @@ type ToolbarGroupItemStateProps = WithRefProps< >; class ToolbarGroupItemState { + readonly opts: ToolbarGroupItemStateProps; + readonly group: ToolbarGroupState; + readonly root: ToolbarRootState; #isDisabled = $derived.by(() => this.opts.disabled.current || this.group.opts.disabled.current); constructor( - readonly opts: ToolbarGroupItemStateProps, - readonly group: ToolbarGroupState, - readonly root: ToolbarRootState + opts: ToolbarGroupItemStateProps, + group: ToolbarGroupState, + root: ToolbarRootState ) { + this.opts = opts; + this.group = group; + this.root = root; + useRefById(opts); $effect(() => { @@ -237,10 +254,13 @@ class ToolbarGroupItemState { type ToolbarLinkStateProps = WithRefProps; class ToolbarLinkState { - constructor( - readonly opts: ToolbarLinkStateProps, - readonly root: ToolbarRootState - ) { + readonly opts: ToolbarLinkStateProps; + readonly root: ToolbarRootState; + + constructor(opts: ToolbarLinkStateProps, root: ToolbarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); $effect(() => { @@ -285,10 +305,13 @@ type ToolbarButtonStateProps = WithRefProps< >; class ToolbarButtonState { - constructor( - readonly opts: ToolbarButtonStateProps, - readonly root: ToolbarRootState - ) { + readonly opts: ToolbarButtonStateProps; + readonly root: ToolbarRootState; + + constructor(opts: ToolbarButtonStateProps, root: ToolbarRootState) { + this.opts = opts; + this.root = root; + useRefById(opts); $effect(() => { diff --git a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts index 113c19397..a6713b21f 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts +++ b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts @@ -1,4 +1,4 @@ -import { box, executeCallbacks, onMountEffect, useRefById } from "svelte-toolbelt"; +import { box, onMountEffect, useRefById } from "svelte-toolbelt"; import { on } from "svelte/events"; import { Context, watch } from "runed"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; @@ -7,16 +7,11 @@ import { isElement, isFocusVisible } from "$lib/internal/is.js"; import { useGraceArea } from "$lib/internal/use-grace-area.svelte.js"; import { getDataDisabled } from "$lib/internal/attrs.js"; import type { WithRefProps } from "$lib/internal/types.js"; -import { CustomEventDispatcher } from "$lib/internal/events.js"; +import type { PointerEventHandler } from "svelte/elements"; const TOOLTIP_CONTENT_ATTR = "data-tooltip-content"; const TOOLTIP_TRIGGER_ATTR = "data-tooltip-trigger"; -export const TooltipOpenEvent = new CustomEventDispatcher("bits.tooltip.open", { - bubbles: false, - cancelable: false, -}); - type TooltipProviderStateProps = ReadableBoxedValues<{ delayDuration: number; disableHoverableContent: boolean; @@ -25,13 +20,15 @@ type TooltipProviderStateProps = ReadableBoxedValues<{ ignoreNonKeyboardFocus: boolean; skipDelayDuration: number; }>; - class TooltipProviderState { + readonly opts: TooltipProviderStateProps; isOpenDelayed = $state(true); isPointerInTransit = box(false); #timerFn: ReturnType; + #openTooltip = $state(null); - constructor(readonly opts: TooltipProviderStateProps) { + constructor(opts: TooltipProviderStateProps) { + this.opts = opts; this.#timerFn = useTimeoutFn( () => { this.isOpenDelayed = true; @@ -42,21 +39,39 @@ class TooltipProviderState { } #startTimer = () => { - this.#timerFn.start(); + const skipDuration = this.opts.skipDelayDuration.current; + + if (skipDuration === 0) { + return; + } else { + this.#timerFn.start(); + } }; #clearTimer = () => { this.#timerFn.stop(); }; - onOpen = () => { + onOpen = (tooltip: TooltipRootState) => { + if (this.#openTooltip && this.#openTooltip !== tooltip) { + this.#openTooltip.handleClose(); + } + this.#clearTimer(); this.isOpenDelayed = false; + this.#openTooltip = tooltip; }; - onClose = () => { + onClose = (tooltip: TooltipRootState) => { + if (this.#openTooltip === tooltip) { + this.#openTooltip = null; + } this.#startTimer(); }; + + isTooltipOpen = (tooltip: TooltipRootState) => { + return this.#openTooltip === tooltip; + }; } type TooltipRootStateProps = ReadableBoxedValues<{ @@ -71,6 +86,8 @@ type TooltipRootStateProps = ReadableBoxedValues<{ }>; class TooltipRootState { + readonly opts: TooltipRootStateProps; + readonly provider: TooltipProviderState; delayDuration = $derived.by( () => this.opts.delayDuration.current ?? this.provider.opts.delayDuration.current ); @@ -99,10 +116,9 @@ class TooltipRootState { return this.#wasOpenDelayed ? "delayed-open" : "instant-open"; }); - constructor( - readonly opts: TooltipRootStateProps, - readonly provider: TooltipProviderState - ) { + constructor(opts: TooltipRootStateProps, provider: TooltipProviderState) { + this.opts = opts; + this.provider = provider; this.#timerFn = useTimeoutFn( () => { this.#wasOpenDelayed = true; @@ -130,12 +146,10 @@ class TooltipRootState { watch( () => this.opts.open.current, (isOpen) => { - if (!this.provider.onClose) return; if (isOpen) { - this.provider.onOpen(); - TooltipOpenEvent.dispatch(document); + this.provider.onOpen(this); } else { - this.provider.onClose(); + this.provider.onClose(this); } } ); @@ -153,7 +167,20 @@ class TooltipRootState { }; #handleDelayedOpen = () => { - this.#timerFn.start(); + this.#timerFn.stop(); + + const shouldSkipDelay = !this.provider.isOpenDelayed; + const delayDuration = this.delayDuration ?? 0; + + // if no delay needed (either skip delay active or delay is 0), open immediately + if (shouldSkipDelay || delayDuration === 0) { + // set wasOpenDelayed based on whether we actually had a delay + this.#wasOpenDelayed = delayDuration > 0 && shouldSkipDelay; + this.opts.open.current = true; + } else { + // use timer for actual delays + this.#timerFn.start(); + } }; onTriggerEnter = () => { @@ -176,14 +203,16 @@ type TooltipTriggerStateProps = WithRefProps< >; class TooltipTriggerState { + readonly opts: TooltipTriggerStateProps; + readonly root: TooltipRootState; #isPointerDown = box(false); #hasPointerMoveOpened = $state(false); #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled); - constructor( - readonly opts: TooltipTriggerStateProps, - readonly root: TooltipRootState - ) { + constructor(opts: TooltipTriggerStateProps, root: TooltipRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -213,10 +242,13 @@ class TooltipTriggerState { ); }; - #onpointermove = (e: PointerEvent) => { + #onpointermove: PointerEventHandler = (e) => { if (this.#isDisabled) return; if (e.pointerType === "touch") return; - if (this.#hasPointerMoveOpened || this.root.provider.isPointerInTransit.current) return; + if (this.#hasPointerMoveOpened) return; + + if (this.root.provider.isPointerInTransit.current) return; + this.root.onTriggerEnter(); this.#hasPointerMoveOpened = true; }; @@ -270,10 +302,12 @@ type TooltipContentStateProps = WithRefProps & }>; class TooltipContentState { - constructor( - readonly opts: TooltipContentStateProps, - readonly root: TooltipRootState - ) { + readonly opts: TooltipContentStateProps; + readonly root: TooltipRootState; + constructor(opts: TooltipContentStateProps, root: TooltipRootState) { + this.opts = opts; + this.root = root; + useRefById({ ...opts, onRefChange: (node) => { @@ -287,24 +321,24 @@ class TooltipContentState { contentNode: () => this.root.contentNode, enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent, onPointerExit: () => { - this.root.handleClose(); + if (this.root.provider.isTooltipOpen(this.root)) { + this.root.handleClose(); + } }, setIsPointerInTransit: (value) => { this.root.provider.isPointerInTransit.current = value; }, + transitTimeout: this.root.provider.opts.skipDelayDuration.current, }); onMountEffect(() => - executeCallbacks( - on(window, "scroll", (e) => { - const target = e.target as HTMLElement | null; - if (!target) return; - if (target.contains(this.root.triggerNode)) { - this.root.handleClose(); - } - }), - TooltipOpenEvent.listen(window, this.root.handleClose) - ) + on(window, "scroll", (e) => { + const target = e.target as HTMLElement | null; + if (!target) return; + if (target.contains(this.root.triggerNode)) { + this.root.handleClose(); + } + }) ); } diff --git a/packages/bits-ui/src/lib/bits/utilities/dismissible-layer/use-dismissable-layer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/dismissible-layer/use-dismissable-layer.svelte.ts index 2461aa59e..999ecc77c 100644 --- a/packages/bits-ui/src/lib/bits/utilities/dismissible-layer/use-dismissable-layer.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/dismissible-layer/use-dismissable-layer.svelte.ts @@ -29,6 +29,7 @@ type DismissibleLayerStateProps = ReadableBoxedValues< >; export class DismissibleLayerState { + readonly opts: DismissibleLayerStateProps; #interactOutsideProp: ReadableBox>; #behaviorType: ReadableBox; #interceptedEvents: Record = { @@ -42,7 +43,9 @@ export class DismissibleLayerState { currNode = $state(null); #unsubClickListener = noop; - constructor(readonly opts: DismissibleLayerStateProps) { + constructor(opts: DismissibleLayerStateProps) { + this.opts = opts; + useRefById({ id: opts.id, ref: this.node, diff --git a/packages/bits-ui/src/lib/bits/utilities/escape-layer/use-escape-layer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/escape-layer/use-escape-layer.svelte.ts index 1bd87bdfc..0cbd7dc58 100644 --- a/packages/bits-ui/src/lib/bits/utilities/escape-layer/use-escape-layer.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/escape-layer/use-escape-layer.svelte.ts @@ -11,7 +11,11 @@ globalThis.bitsEscapeLayers ??= new Map>>; export class EscapeLayerState { - constructor(readonly opts: EscapeLayerStateProps) { + readonly opts: EscapeLayerStateProps; + + constructor(opts: EscapeLayerStateProps) { + this.opts = opts; + let unsubEvents = noop; watch( () => opts.enabled.current, diff --git a/packages/bits-ui/src/lib/bits/utilities/floating-layer/use-floating-layer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/floating-layer/use-floating-layer.svelte.ts index 46c003b6b..ee83b531a 100644 --- a/packages/bits-ui/src/lib/bits/utilities/floating-layer/use-floating-layer.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/floating-layer/use-floating-layer.svelte.ts @@ -78,6 +78,9 @@ export type FloatingContentStateProps = ReadableBoxedValues<{ }>; class FloatingContentState { + readonly opts: FloatingContentStateProps; + readonly root: FloatingRootState; + // nodes contentRef = box(null); wrapperRef = box(null); @@ -224,10 +227,10 @@ class FloatingContentState { visibility: this.cannotCenterArrow ? "hidden" : undefined, }); - constructor( - readonly opts: FloatingContentStateProps, - readonly root: FloatingRootState - ) { + constructor(opts: FloatingContentStateProps, root: FloatingRootState) { + this.opts = opts; + this.root = root; + if (opts.customAnchor) { this.root.customAnchorNode.current = opts.customAnchor.current; } @@ -287,10 +290,13 @@ class FloatingContentState { type FloatingArrowStateProps = WithRefProps; class FloatingArrowState { - constructor( - readonly opts: FloatingArrowStateProps, - readonly content: FloatingContentState - ) { + readonly opts: FloatingArrowStateProps; + readonly content: FloatingContentState; + + constructor(opts: FloatingArrowStateProps, content: FloatingContentState) { + this.opts = opts; + this.content = content; + useRefById({ ...opts, onRefChange: (node) => { @@ -316,12 +322,14 @@ type FloatingAnchorStateProps = ReadableBoxedValues<{ }>; class FloatingAnchorState { + readonly opts: FloatingAnchorStateProps; + readonly root: FloatingRootState; ref = box(null); - constructor( - readonly opts: FloatingAnchorStateProps, - readonly root: FloatingRootState - ) { + constructor(opts: FloatingAnchorStateProps, root: FloatingRootState) { + this.opts = opts; + this.root = root; + if (opts.virtualEl && opts.virtualEl.current) { root.triggerNode = box.from(opts.virtualEl.current); } else { diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/use-focus-scope.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/focus-scope/use-focus-scope.svelte.ts index 5b9e60182..48dca602d 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/use-focus-scope.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/use-focus-scope.svelte.ts @@ -118,15 +118,50 @@ export function useFocusScope({ } } - // When the focused element gets removed from the DOM, browsers move focus - // back to the document.body. In this case, we move focus to the container - // to keep focus trapped correctly. - // instead of leaning on document.activeElement, we use lastFocusedElement to check - // if the element still exists inside the container, - // if not then we focus to the container - function handleMutations(_: MutationRecord[]) { - const lastFocusedElementExists = ref.current?.contains(lastFocusedElement); - if (!lastFocusedElementExists && ref.current) { + /** + * Handles DOM mutations within the container. Specifically checks if the + * last known focused element inside the container has been removed. If so, + * and focus has escaped the container (likely moved to document.body), + * it refocuses the container itself to maintain the trap. + */ + function handleMutations(mutations: MutationRecord[]) { + // if there's no record of a last focused el, or container isn't mounted, bail + if (!lastFocusedElement || !ref.current) return; + + // track if the last focused element was removed + let elementWasRemoved = false; + + for (const mutation of mutations) { + // we only care about mutations where nodes were removed + if (mutation.type === "childList" && mutation.removedNodes.length > 0) { + // check if any removed nodes are the last focused element or contain it + for (const removedNode of mutation.removedNodes) { + if (removedNode === lastFocusedElement) { + elementWasRemoved = true; + // found it directly + break; + } + // contains() only works on elements, so we need to check nodeType + if ( + removedNode.nodeType === Node.ELEMENT_NODE && + (removedNode as Element).contains(lastFocusedElement) + ) { + elementWasRemoved = true; + // descendant found, + break; + } + } + } + + // if we've confirmed removal in any mutation, bail + if (elementWasRemoved) break; + } + + /** + * If the element was removed and focus is now outside the container, + * (e.g., browser moved it to body), refocus the container. + */ + if (elementWasRemoved && ref.current && !ref.current.contains(document.activeElement)) { focus(ref.current); } } @@ -139,7 +174,11 @@ export function useFocusScope({ on(document, "focusout", manageFocus) ); const mutationObserver = new MutationObserver(handleMutations); - mutationObserver.observe(container, { childList: true, subtree: true }); + mutationObserver.observe(container, { + childList: true, + subtree: true, + attributes: false, + }); return () => { removeEvents(); mutationObserver.disconnect(); @@ -184,11 +223,11 @@ export function useFocusScope({ if (!mountEvent.defaultPrevented) { afterTick(() => { if (!container) return; - focusFirst(removeLinks(getTabbableCandidates(container)), { select: true }); + const result = focusFirst(removeLinks(getTabbableCandidates(container)), { + select: true, + }); - if (document.activeElement === prevFocusedElement) { - focus(container); - } + if (!result) focus(container); }); } } @@ -196,7 +235,7 @@ export function useFocusScope({ function handleClose(prevFocusedElement: HTMLElement | null) { const destroyEvent = AutoFocusOnDestroyEvent.createEvent(); - onCloseAutoFocus.current(destroyEvent); + onCloseAutoFocus.current?.(destroyEvent); const shouldIgnore = ctx.ignoreCloseAutoFocus; afterSleep(0, () => { diff --git a/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/text-selection-layer.svelte b/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/text-selection-layer.svelte index a40a6fbaa..c39fe78e3 100644 --- a/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/text-selection-layer.svelte +++ b/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/text-selection-layer.svelte @@ -15,10 +15,9 @@ useTextSelectionLayer({ id: box.with(() => id), - preventOverflowTextSelection: box.with(() => preventOverflowTextSelection), onPointerDown: box.with(() => onPointerDown), onPointerUp: box.with(() => onPointerUp), - enabled: box.with(() => enabled), + enabled: box.with(() => enabled && preventOverflowTextSelection), }); diff --git a/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.ts index d0ae44675..6327df3ba 100644 --- a/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.ts @@ -13,15 +13,20 @@ import { noop } from "$lib/internal/noop.js"; import { isHTMLElement } from "$lib/internal/is.js"; import { isOrContainsTarget } from "$lib/internal/elements.js"; -type StateProps = ReadableBoxedValues>>; +type TextSelectionLayerStateProps = ReadableBoxedValues< + Required> +>; globalThis.bitsTextSelectionLayers ??= new Map>(); export class TextSelectionLayerState { + readonly opts: TextSelectionLayerStateProps; #unsubSelectionLock = noop; #ref = box(null); - constructor(readonly opts: StateProps) { + constructor(opts: TextSelectionLayerStateProps) { + this.opts = opts; + useRefById({ id: opts.id, ref: this.#ref, @@ -79,7 +84,7 @@ export class TextSelectionLayerState { }; } -export function useTextSelectionLayer(props: StateProps) { +export function useTextSelectionLayer(props: TextSelectionLayerStateProps) { return new TextSelectionLayerState(props); } diff --git a/packages/bits-ui/src/lib/internal/create-shared-hook.svelte.ts b/packages/bits-ui/src/lib/internal/create-shared-hook.svelte.ts index f122dcfb8..7fd0038e3 100644 --- a/packages/bits-ui/src/lib/internal/create-shared-hook.svelte.ts +++ b/packages/bits-ui/src/lib/internal/create-shared-hook.svelte.ts @@ -14,7 +14,7 @@ export function createSharedHook(factory: T): T { } } - return ((...args) => { + return ((...args) => { subscribers += 1; if (state === undefined) { scope = $effect.root(() => { @@ -29,5 +29,5 @@ export function createSharedHook(factory: T): T { }); return state; - }); + }) as T; } diff --git a/packages/bits-ui/src/lib/internal/date-time/calendar-helpers.svelte.ts b/packages/bits-ui/src/lib/internal/date-time/calendar-helpers.svelte.ts index e1474f67f..59259f49d 100644 --- a/packages/bits-ui/src/lib/internal/date-time/calendar-helpers.svelte.ts +++ b/packages/bits-ui/src/lib/internal/date-time/calendar-helpers.svelte.ts @@ -61,7 +61,7 @@ export type CreateMonthProps = { /** * The day of the week to start the calendar on (0 for Sunday, 1 for Monday, etc.). */ - weekStartsOn: number; + weekStartsOn: number | undefined; /** * Whether to always render 6 weeks in the calendar, even if the month doesn't @@ -97,8 +97,14 @@ function createMonth(props: CreateMonthProps): Month { const firstDayOfMonth = startOfMonth(dateObj); const lastDayOfMonth = endOfMonth(dateObj); - const lastSunday = getLastFirstDayOfWeek(firstDayOfMonth, weekStartsOn, locale); - const nextSaturday = getNextLastDayOfWeek(lastDayOfMonth, weekStartsOn, locale); + const lastSunday = + weekStartsOn !== undefined + ? getLastFirstDayOfWeek(firstDayOfMonth, weekStartsOn, "en-US") + : getLastFirstDayOfWeek(firstDayOfMonth, 0, locale); + const nextSaturday = + weekStartsOn !== undefined + ? getNextLastDayOfWeek(lastDayOfMonth, weekStartsOn, "en-US") + : getNextLastDayOfWeek(lastDayOfMonth, 0, locale); const lastMonthDays = getDaysBetween(lastSunday.subtract({ days: 1 }), firstDayOfMonth); const nextMonthDays = getDaysBetween(lastDayOfMonth, nextSaturday.add({ days: 1 })); @@ -402,7 +408,7 @@ type HandleCalendarPageProps = { setMonths: (months: Month[]) => void; numberOfMonths: number; pagedNavigation: boolean; - weekStartsOn: number; + weekStartsOn: number | undefined; locale: string; fixedWeeks: boolean; setPlaceholder: (date: DateValue) => void; @@ -483,7 +489,7 @@ export function getWeekdays({ months, formatter, weekdayFormat }: GetWeekdaysPro } type UseMonthViewSyncProps = { - weekStartsOn: ReadableBox; + weekStartsOn: ReadableBox; locale: ReadableBox; fixedWeeks: ReadableBox; numberOfMonths: ReadableBox; @@ -562,7 +568,7 @@ export function createAccessibleHeading({ type UseMonthViewPlaceholderSyncProps = { placeholder: WritableBox; getVisibleMonths: () => DateValue[]; - weekStartsOn: ReadableBox; + weekStartsOn: ReadableBox; locale: ReadableBox; fixedWeeks: ReadableBox; numberOfMonths: ReadableBox; diff --git a/packages/bits-ui/src/lib/internal/events.ts b/packages/bits-ui/src/lib/internal/events.ts index e6dd3e2b5..a2cf081c9 100644 --- a/packages/bits-ui/src/lib/internal/events.ts +++ b/packages/bits-ui/src/lib/internal/events.ts @@ -57,10 +57,16 @@ export function addEventListener( * @param options - CustomEvent options (bubbles, cancelable, etc.) */ export class CustomEventDispatcher { + readonly eventName: string; + readonly options: Omit, "detail">; + constructor( - readonly eventName: string, - readonly options: Omit, "detail"> = { bubbles: true, cancelable: true } - ) {} + eventName: string, + options: Omit, "detail"> = { bubbles: true, cancelable: true } + ) { + this.eventName = eventName; + this.options = options; + } createEvent(detail?: T): CustomEvent { return new CustomEvent(this.eventName, { diff --git a/packages/bits-ui/src/lib/internal/focus.ts b/packages/bits-ui/src/lib/internal/focus.ts index 7e47694dd..e87b0feff 100644 --- a/packages/bits-ui/src/lib/internal/focus.ts +++ b/packages/bits-ui/src/lib/internal/focus.ts @@ -53,9 +53,7 @@ export function focusFirst(candidates: HTMLElement[], { select = false } = {}) { const previouslyFocusedElement = document.activeElement; for (const candidate of candidates) { focus(candidate, { select }); - if (document.activeElement !== previouslyFocusedElement) { - return true; - } + if (document.activeElement !== previouslyFocusedElement) return true; } } diff --git a/packages/bits-ui/src/lib/internal/use-dom-typeahead.svelte.ts b/packages/bits-ui/src/lib/internal/use-dom-typeahead.svelte.ts index c57bd41aa..e5c81e665 100644 --- a/packages/bits-ui/src/lib/internal/use-dom-typeahead.svelte.ts +++ b/packages/bits-ui/src/lib/internal/use-dom-typeahead.svelte.ts @@ -22,13 +22,13 @@ export function useDOMTypeahead(opts?: UseDOMTypeaheadOpts) { search.current = search.current + key; const currentItem = getCurrentItem(); - const currentMatch = candidates.find((item) => item === currentItem)?.textContent ?? ""; - const values = candidates.map((item) => item.textContent ?? ""); + const currentMatch = + candidates.find((item) => item === currentItem)?.textContent?.trim() ?? ""; + + const values = candidates.map((item) => item.textContent?.trim() ?? ""); const nextMatch = getNextMatch(values, search.current, currentMatch); - const newItem = candidates.find((item) => item.textContent === nextMatch); - if (newItem) { - onMatch(newItem); - } + const newItem = candidates.find((item) => item.textContent?.trim() === nextMatch); + if (newItem) onMatch(newItem); return newItem; } diff --git a/packages/bits-ui/src/lib/internal/use-grace-area.svelte.ts b/packages/bits-ui/src/lib/internal/use-grace-area.svelte.ts index ae2e495ed..c80f55592 100644 --- a/packages/bits-ui/src/lib/internal/use-grace-area.svelte.ts +++ b/packages/bits-ui/src/lib/internal/use-grace-area.svelte.ts @@ -10,15 +10,20 @@ interface UseGraceAreaOpts { contentNode: Getter; onPointerExit: () => void; setIsPointerInTransit?: (value: boolean) => void; + transitTimeout?: number; } export function useGraceArea(opts: UseGraceAreaOpts) { const enabled = $derived(opts.enabled()); - const isPointerInTransit = boxAutoReset(false as boolean, 300, (value) => { - if (enabled) { - opts.setIsPointerInTransit?.(value); + const isPointerInTransit = boxAutoReset( + false as boolean, + opts.transitTimeout ?? 300, + (value) => { + if (enabled) { + opts.setIsPointerInTransit?.(value); + } } - }); + ); let pointerGraceArea = $state(null); diff --git a/packages/bits-ui/src/lib/internal/use-roving-focus.svelte.ts b/packages/bits-ui/src/lib/internal/use-roving-focus.svelte.ts index 3b6614859..3fe7375d5 100644 --- a/packages/bits-ui/src/lib/internal/use-roving-focus.svelte.ts +++ b/packages/bits-ui/src/lib/internal/use-roving-focus.svelte.ts @@ -5,17 +5,22 @@ import { kbd } from "./kbd.js"; import { isBrowser } from "./is.js"; import type { Orientation } from "$lib/shared/index.js"; -type UseRovingFocusProps = { - /** - * The selector used to find the focusable candidates. - */ - candidateAttr: string; - - /** - * Custom candidate selector - */ - candidateSelector?: string; - +type UseRovingFocusProps = ( + | { + /** + * The selector used to find the focusable candidates. + */ + candidateAttr: string; + candidateSelector?: undefined; + } + | { + /** + * Custom candidate selector + */ + candidateSelector: string; + candidateAttr?: undefined; + } +) & { /** * The id of the root node */ @@ -53,12 +58,14 @@ export function useRovingFocus(props: UseRovingFocusProps) { node.querySelectorAll(props.candidateSelector) ); return candidates; - } else { + } else if (props.candidateAttr) { const candidates = Array.from( node.querySelectorAll(`[${props.candidateAttr}]:not([data-disabled])`) ); return candidates; } + + return []; } function focusFirstCandidate() { diff --git a/packages/bits-ui/src/lib/shared/index.ts b/packages/bits-ui/src/lib/shared/index.ts index 98333ad49..b506e6ae7 100644 --- a/packages/bits-ui/src/lib/shared/index.ts +++ b/packages/bits-ui/src/lib/shared/index.ts @@ -34,6 +34,14 @@ export type StyleProperties = CSS.Properties & { export type Orientation = "horizontal" | "vertical"; export type Direction = "ltr" | "rtl"; +/** + * Controls positioning of the slider thumb. + * + * - `exact`: The thumb is centered exactly at the value of the slider. + * - `contain`: The thumb is centered exactly at the value of the slider, but will be contained within the slider track at the ends. + */ +export type SliderThumbPositioning = "exact" | "contain"; + export type WithoutChildrenOrChild = WithoutChildren>; export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChild = T extends { child?: any } ? Omit : T; diff --git a/packages/bits-ui/tsconfig.json b/packages/bits-ui/tsconfig.json index b98351fbd..18c370010 100644 --- a/packages/bits-ui/tsconfig.json +++ b/packages/bits-ui/tsconfig.json @@ -12,6 +12,7 @@ "moduleResolution": "NodeNext", "module": "NodeNext", "verbatimModuleSyntax": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "erasableSyntaxOnly": true } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66ab7c134..44e954ead 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,9 +123,6 @@ importers: clsx: specifier: ^2.1.0 version: 2.1.1 - concurrently: - specifier: ^8.2.2 - version: 8.2.2 consola: specifier: ^3.4.0 version: 3.4.0 @@ -234,6 +231,9 @@ importers: '@internationalized/date': specifier: ^3.5.6 version: 3.5.6 + css.escape: + specifier: ^1.5.1 + version: 1.5.1 esm-env: specifier: ^1.1.2 version: 1.1.4 @@ -252,10 +252,13 @@ importers: version: 2.16.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.22.6)(vite@5.4.11(@types/node@20.17.6)(lightningcss@1.29.1)(terser@5.36.0)))(svelte@5.22.6)(vite@5.4.11(@types/node@20.17.6)(lightningcss@1.29.1)(terser@5.36.0)) '@sveltejs/package': specifier: ^2.3.9 - version: 2.3.9(svelte@5.22.6)(typescript@5.6.3) + version: 2.3.9(svelte@5.22.6)(typescript@5.8.3) '@sveltejs/vite-plugin-svelte': specifier: 4.0.0 version: 4.0.0(svelte@5.22.6)(vite@5.4.11(@types/node@20.17.6)(lightningcss@1.29.1)(terser@5.36.0)) + '@types/css.escape': + specifier: ^1.5.2 + version: 1.5.2 '@types/node': specifier: ^20.17.6 version: 20.17.6 @@ -279,13 +282,13 @@ importers: version: 5.22.6 svelte-check: specifier: ^4.1.4 - version: 4.1.4(picomatch@4.0.2)(svelte@5.22.6)(typescript@5.6.3) + version: 4.1.4(picomatch@4.0.2)(svelte@5.22.6)(typescript@5.8.3) tslib: specifier: ^2.8.1 version: 2.8.1 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.8.3 + version: 5.8.3 vite: specifier: ^5.4.11 version: 5.4.11(@types/node@20.17.6)(lightningcss@1.29.1)(terser@5.36.0) @@ -1710,6 +1713,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/css.escape@1.5.2': + resolution: {integrity: sha512-sq0h0y8i83T20MB434AZWgK3byezbBG48jllitFpVXlPOjHc5RNs3bg6rUen/EtMUjM4ZJjD4x+0aik9AXqLdQ==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2087,10 +2093,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2131,11 +2133,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concurrently@8.2.2: - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} - engines: {node: ^14.13.0 || >=16.0.0} - hasBin: true - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -2183,10 +2180,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -2279,9 +2272,6 @@ packages: electron-to-chromium@1.5.58: resolution: {integrity: sha512-al2l4r+24ZFL7WzyPTlyD0fC33LLzvxqLCwurtBibVPghRGO9hSTl+tis8t1kD7biPiH/en4U0I7o/nQbYeoVA==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -2560,10 +2550,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} @@ -2752,10 +2738,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3634,10 +3616,6 @@ packages: remove-markdown@0.5.5: resolution: {integrity: sha512-lMR8tOtDqazFT6W2bZidoXwkptMdF3pCxpri0AEokHg0sZlC2GdoLqnoaxsEj1o7/BtXV1MKtT3YviA1t7rW7g==} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -3703,9 +3681,6 @@ packages: peerDependencies: svelte: ^5.7.0 - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -3754,9 +3729,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - shiki@1.22.2: resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} @@ -3799,9 +3771,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - spawn-command@0.0.2: - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} - spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} @@ -3825,10 +3794,6 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -3862,10 +3827,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -3997,10 +3958,6 @@ packages: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4038,6 +3995,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -4323,10 +4285,6 @@ packages: '@cloudflare/workers-types': optional: true - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4352,10 +4310,6 @@ packages: xxhash-wasm@1.0.2: resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} @@ -4368,14 +4322,6 @@ packages: engines: {node: '>= 14'} hasBin: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5346,14 +5292,14 @@ snapshots: svelte: 5.22.6 vite: 6.1.1(@types/node@20.17.6)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.36.0)(yaml@2.4.5) - '@sveltejs/package@2.3.9(svelte@5.22.6)(typescript@5.6.3)': + '@sveltejs/package@2.3.9(svelte@5.22.6)(typescript@5.8.3)': dependencies: chokidar: 4.0.1 kleur: 4.1.5 sade: 1.8.1 semver: 7.6.3 svelte: 5.22.6 - svelte2tsx: 0.7.34(svelte@5.22.6)(typescript@5.6.3) + svelte2tsx: 0.7.34(svelte@5.22.6)(typescript@5.8.3) transitivePeerDependencies: - typescript @@ -5522,6 +5468,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/css.escape@1.5.2': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -5948,12 +5896,6 @@ snapshots: ci-info@3.9.0: {} - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clsx@2.1.1: {} collapse-white-space@2.1.0: {} @@ -5990,18 +5932,6 @@ snapshots: concat-map@0.0.1: {} - concurrently@8.2.2: - dependencies: - chalk: 4.1.2 - date-fns: 2.30.0 - lodash: 4.17.21 - rxjs: 7.8.1 - shell-quote: 1.8.1 - spawn-command: 0.0.2 - supports-color: 8.1.1 - tree-kill: 1.2.2 - yargs: 17.7.2 - confbox@0.1.8: {} consola@3.4.0: {} @@ -6041,10 +5971,6 @@ snapshots: dataloader@1.4.0: {} - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.24.8 - date-fns@4.1.0: {} debug@4.3.7: @@ -6101,8 +6027,6 @@ snapshots: electron-to-chromium@1.5.58: {} - emoji-regex@8.0.0: {} - enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -6504,8 +6428,6 @@ snapshots: function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-source@2.0.12: dependencies: data-uri-to-buffer: 2.0.2 @@ -6787,8 +6709,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -7931,8 +7851,6 @@ snapshots: remove-markdown@0.5.5: {} - require-directory@2.1.1: {} - requires-port@1.0.0: {} resize-observer-polyfill@1.5.1: {} @@ -8032,10 +7950,6 @@ snapshots: esm-env: 1.2.2 svelte: 5.22.6 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -8095,8 +8009,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.1: {} - shiki@1.22.2: dependencies: '@shikijs/core': 1.22.2 @@ -8137,8 +8049,6 @@ snapshots: space-separated-tokens@2.0.2: {} - spawn-command@0.0.2: {} - spawndamnit@2.0.0: dependencies: cross-spawn: 5.1.0 @@ -8161,12 +8071,6 @@ snapshots: stoppable@1.1.0: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -8200,10 +8104,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} svelte-check@4.1.4(picomatch@4.0.2)(svelte@5.22.6)(typescript@5.6.3): @@ -8218,6 +8118,18 @@ snapshots: transitivePeerDependencies: - picomatch + svelte-check@4.1.4(picomatch@4.0.2)(svelte@5.22.6)(typescript@5.8.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + chokidar: 4.0.1 + fdir: 6.4.2(picomatch@4.0.2) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.22.6 + typescript: 5.8.3 + transitivePeerDependencies: + - picomatch + svelte-eslint-parser@0.43.0(svelte@5.22.6): dependencies: eslint-scope: 7.2.2 @@ -8239,12 +8151,12 @@ snapshots: style-to-object: 1.0.8 svelte: 5.22.6 - svelte2tsx@0.7.34(svelte@5.22.6)(typescript@5.6.3): + svelte2tsx@0.7.34(svelte@5.22.6)(typescript@5.8.3): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 svelte: 5.22.6 - typescript: 5.6.3 + typescript: 5.8.3 svelte@5.22.6: dependencies: @@ -8333,8 +8245,6 @@ snapshots: dependencies: punycode: 2.3.1 - tree-kill@1.2.2: {} - trim-lines@3.0.1: {} trim-trailing-lines@2.1.0: {} @@ -8367,6 +8277,8 @@ snapshots: typescript@5.6.3: {} + typescript@5.8.3: {} + ufo@1.5.4: {} undici-types@6.19.8: {} @@ -8699,12 +8611,6 @@ snapshots: - supports-color - utf-8-validate - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrappy@1.0.2: {} ws@8.18.0: {} @@ -8715,26 +8621,12 @@ snapshots: xxhash-wasm@1.0.2: {} - y18n@5.0.8: {} - yallist@2.1.2: {} yaml@1.10.2: {} yaml@2.4.5: {} - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yocto-queue@0.1.0: {} youch@3.3.4: diff --git a/tests/src/tests/calendar/calendar.test.ts b/tests/src/tests/calendar/calendar.test.ts index bdb2fad67..db15c670c 100644 --- a/tests/src/tests/calendar/calendar.test.ts +++ b/tests/src/tests/calendar/calendar.test.ts @@ -457,6 +457,30 @@ describe("type='single'", () => { expect(weekdayEl).toHaveTextContent(weekday); } }); + + it("should respect the `weekStartsOn` prop", async () => { + const t = setup({ placeholder: calendarDate, weekStartsOn: 2, weekdayFormat: "short" }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("Tue"); + }); + + it("should respect the `weekStartsOn` prop regardless of locale", async () => { + const t = setup({ + placeholder: calendarDate, + weekStartsOn: 2, + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("mar."); + }); + + it("should default the first day of the week to the locale's first day of the week if `weekStartsOn` is not provided", async () => { + const t = setup({ + placeholder: calendarDate, + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("lun."); + }); }); describe("Availability and Interaction", () => { diff --git a/tests/src/tests/checkbox/checkbox-group-test.svelte b/tests/src/tests/checkbox/checkbox-group-test.svelte index 5ef1aaa56..09f1b0647 100644 --- a/tests/src/tests/checkbox/checkbox-group-test.svelte +++ b/tests/src/tests/checkbox/checkbox-group-test.svelte @@ -2,10 +2,12 @@ import { Checkbox } from "bits-ui"; let { - value = $bindable([]), + value: valueProp = $bindable([]), items = [], disabledItems = [], onFormSubmit, + getValue: getValueProp, + setValue: setValueProp, ...restProps }: Checkbox.GroupProps & { /** @@ -14,7 +16,11 @@ items?: string[]; disabledItems?: string[]; onFormSubmit?: (fd: FormData) => void; + setValue?: (value: string[]) => void; + getValue?: () => string[]; } = $props(); + + let myValue = $state(valueProp); {#snippet MyCheckbox({ itemValue }: { itemValue: string })} @@ -44,8 +50,21 @@ onFormSubmit?.(formData); }} > -

    {value}

    - +

    {myValue}

    + { + getValueProp?.(); + return myValue; + }, + (v) => { + setValueProp?.(v); + myValue = v; + } + } + {...restProps} + > My Group {#each items as itemValue} {@render MyCheckbox({ itemValue })} @@ -53,5 +72,7 @@ - + diff --git a/tests/src/tests/checkbox/checkbox.test.ts b/tests/src/tests/checkbox/checkbox.test.ts index c4838fbb6..ced75e7d9 100644 --- a/tests/src/tests/checkbox/checkbox.test.ts +++ b/tests/src/tests/checkbox/checkbox.test.ts @@ -256,9 +256,28 @@ describe("Checkbox Group", () => { expect(t.binding).toHaveTextContent("b,d"); }); + it("should handle function binding", async () => { + const setMock = vi.fn(); + const getMock = vi.fn(); + const t = setupGroup({ getValue: getMock, setValue: setMock, value: [] }); + const [a, b, _, d] = t.checkboxes; + await t.user.click(a); + expect(setMock).toHaveBeenCalledWith(["a"]); + setMock.mockClear(); + await t.user.click(b); + expect(setMock).toHaveBeenCalledWith(["a", "b"]); + setMock.mockClear(); + await t.user.click(d); + expect(setMock).toHaveBeenCalledWith(["a", "b", "d"]); + setMock.mockClear(); + await t.user.click(a); + expect(setMock).toHaveBeenCalledWith(["b", "d"]); + }); + it("should handle programmatic value changes", async () => { const t = setupGroup({ value: ["a", "b"] }); const [a, b, c, d] = t.checkboxes; + await tick(); expectChecked(a, b); await t.user.click(t.updateBtn); expectUnchecked(a, b); diff --git a/tests/src/tests/context-menu/context-menu-test.svelte b/tests/src/tests/context-menu/context-menu-test.svelte index aa14b7b04..fed3bb2f0 100644 --- a/tests/src/tests/context-menu/context-menu-test.svelte +++ b/tests/src/tests/context-menu/context-menu-test.svelte @@ -6,8 +6,11 @@ radio?: string; subRadio?: string; open?: boolean; - contentProps?: Omit; - portalProps?: Omit; + group?: string[]; + contentProps?: Omit; + portalProps?: Omit; + subTriggerProps?: Omit; + checkboxGroupProps?: Omit; }; @@ -18,8 +21,11 @@ radio = "", subRadio = "", open = false, + group = [], contentProps = {}, portalProps = {}, + subTriggerProps = {}, + checkboxGroupProps = {}, ...restProps }: ContextMenuTestProps = $props(); @@ -50,7 +56,7 @@ - + subtrigger @@ -101,6 +107,24 @@ {/snippet} + + + {#snippet children({ checked })} + {checked} + Checkbox Item 1 + {/snippet} + + + {#snippet children({ checked })} + {checked} + Checkbox Item 2 + {/snippet} + + @@ -120,6 +144,10 @@ >{subRadio} - +
    diff --git a/tests/src/tests/context-menu/context-menu.test.ts b/tests/src/tests/context-menu/context-menu.test.ts index 03d62f4e2..1a165637e 100644 --- a/tests/src/tests/context-menu/context-menu.test.ts +++ b/tests/src/tests/context-menu/context-menu.test.ts @@ -1,6 +1,6 @@ import { render, screen, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; -import { it } from "vitest"; +import { it, vi } from "vitest"; import { getTestKbd, setupUserEvents } from "../utils.js"; import ContextMenuTest from "./context-menu-test.svelte"; import type { ContextMenuTestProps } from "./context-menu-test.svelte"; @@ -21,12 +21,17 @@ function setup(props: ContextMenuSetupProps = {}) { const user = setupUserEvents(); const returned = render(component, { ...rest }); const trigger = returned.getByTestId("trigger"); + + const open = async () => + await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]); + return { ...returned, getContent: () => returned.queryByTestId("content"), getSubContent: () => returned.queryByTestId("sub-content"), user, trigger, + open, }; } @@ -79,6 +84,7 @@ it("should have bits data attrs", async () => { "checkbox-item", "radio-group", "radio-item", + "checkbox-group", ]; for (const part of parts) { @@ -218,6 +224,10 @@ it("should not loop through the menu items when the `loop` prop is set to false" await user.keyboard(kbd.ARROW_DOWN); await waitFor(() => expect(queryByTestId("radio-item-2")).toHaveFocus()); await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(queryByTestId("checkbox-group-item-1")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(queryByTestId("checkbox-group-item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); await waitFor(() => expect(queryByTestId("item")).not.toHaveFocus()); }); @@ -240,6 +250,10 @@ it("should loop through the menu items when the `loop` prop is set to true", asy await user.keyboard(kbd.ARROW_DOWN); await waitFor(() => expect(getByTestId("radio-item-2")).toHaveFocus()); await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-group-item-1")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-group-item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); await waitFor(() => expect(getByTestId("item")).toHaveFocus()); }); @@ -376,3 +390,66 @@ it("should respect the `onCloseAutoFocus` prop", async () => { await user.keyboard(kbd.ESCAPE); expect(getByTestId("on-focus-override")).toHaveFocus(); }); + +it("should respect the `onSelect` prop on SubTrigger", async () => { + const onSelect = vi.fn(); + const { getByTestId, user } = await open({ + subTriggerProps: { + onSelect, + }, + }); + + await user.click(getByTestId("sub-trigger")); + expect(onSelect).toHaveBeenCalled(); + + await user.keyboard(kbd.ENTER); + expect(onSelect).toHaveBeenCalledTimes(2); + + await user.keyboard(kbd.ARROW_RIGHT); + expect(onSelect).toHaveBeenCalledTimes(3); +}); + +it("should respect the `value` prop on CheckboxGroup", async () => { + const t = await open({ + group: ["1"], + }); + + const checkboxGroupItem1 = t.getByTestId("checkbox-group-item-1"); + expect(checkboxGroupItem1).toHaveAttribute("aria-checked", "true"); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("true"); + expect(t.queryByTestId("checkbox-indicator-2")).toHaveTextContent("false"); + + await t.user.click(checkboxGroupItem1); + await t.open(); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("false"); + + await t.user.click(t.getByTestId("checkbox-group-item-2")); + await t.open(); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("true"); + + await t.user.click(t.getByTestId("checkbox-group-binding")); + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("false"); +}); + +it("calls `onValueChange` when the value of the checkbox group changes", async () => { + const onValueChange = vi.fn(); + const t = await open({ + checkboxGroupProps: { + onValueChange, + }, + }); + await t.user.click(t.getByTestId("checkbox-group-item-1")); + expect(onValueChange).toHaveBeenCalledWith(["1"]); + await t.open(); + await t.user.click(t.getByTestId("checkbox-group-item-2")); + expect(onValueChange).toHaveBeenCalledWith(["1", "2"]); + await t.open(); + await t.user.click(t.getByTestId("checkbox-group-item-1")); + expect(onValueChange).toHaveBeenCalledWith(["2"]); +}); diff --git a/tests/src/tests/date-range-field/date-range-field.test.ts b/tests/src/tests/date-range-field/date-range-field.test.ts index 1a9a94d03..ebf35036c 100644 --- a/tests/src/tests/date-range-field/date-range-field.test.ts +++ b/tests/src/tests/date-range-field/date-range-field.test.ts @@ -1,7 +1,7 @@ import { render } from "@testing-library/svelte/svelte5"; import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; -import { describe, it } from "vitest"; +import { it } from "vitest"; import { CalendarDate, CalendarDateTime, toZoned } from "@internationalized/date"; import { getTestKbd } from "../utils.js"; import DateRangeFieldTest, { type DateRangeFieldTestProps } from "./date-range-field-test.svelte"; @@ -49,167 +49,203 @@ function setup(props: DateRangeFieldTestProps = {}) { return { ...returned, user, start, end, root, startInput, endInput, label }; } -describe("date range field", () => { - it("should have no axe violations", async () => { - const { container } = setup(); - expect(await axe(container)).toHaveNoViolations(); +it("should have no axe violations", async () => { + const { container } = setup(); + expect(await axe(container)).toHaveNoViolations(); +}); + +it("should populate segment with value - `CalendarDate`", async () => { + const { start, end } = setup({ + value: calendarDate, }); - it("should populate segment with value - `CalendarDate`", async () => { - const { start, end } = setup({ - value: calendarDate, - }); + expect(start.month).toHaveTextContent(String(calendarDate.start.month)); + expect(start.day).toHaveTextContent(String(calendarDate.start.day)); + expect(start.year).toHaveTextContent(String(calendarDate.start.year)); + expect(start.value).toHaveTextContent(calendarDate.start.toString()); - expect(start.month).toHaveTextContent(String(calendarDate.start.month)); - expect(start.day).toHaveTextContent(String(calendarDate.start.day)); - expect(start.year).toHaveTextContent(String(calendarDate.start.year)); - expect(start.value).toHaveTextContent(calendarDate.start.toString()); + expect(end.month).toHaveTextContent(String(calendarDate.end.month)); + expect(end.day).toHaveTextContent(String(calendarDate.end.day)); + expect(end.year).toHaveTextContent(String(calendarDate.end.year)); + expect(end.value).toHaveTextContent(calendarDate.end.toString()); +}); - expect(end.month).toHaveTextContent(String(calendarDate.end.month)); - expect(end.day).toHaveTextContent(String(calendarDate.end.day)); - expect(end.year).toHaveTextContent(String(calendarDate.end.year)); - expect(end.value).toHaveTextContent(calendarDate.end.toString()); +it("should populate segment with value - `CalendarDateTime`", async () => { + const { start, end, getByTestId } = setup({ + value: calendarDateTime, + granularity: "second", }); - it("should populate segment with value - `CalendarDateTime`", async () => { - const { start, end, getByTestId } = setup({ - value: calendarDateTime, - granularity: "second", - }); - - expect(start.month).toHaveTextContent(String(calendarDateTime.start.month)); - expect(start.day).toHaveTextContent(String(calendarDateTime.start.day)); - expect(start.year).toHaveTextContent(String(calendarDateTime.start.year)); - expect(getByTestId("start-hour")).toHaveTextContent(String(calendarDateTime.start.hour)); - expect(getByTestId("start-minute")).toHaveTextContent( - String(calendarDateTime.start.minute) - ); - expect(getByTestId("start-second")).toHaveTextContent( - String(calendarDateTime.start.second) - ); - expect(start.value).toHaveTextContent(calendarDateTime.start.toString()); - - expect(end.month).toHaveTextContent(String(calendarDateTime.end.month)); - expect(end.day).toHaveTextContent(String(calendarDateTime.end.day)); - expect(end.year).toHaveTextContent(String(calendarDateTime.end.year)); - expect(getByTestId("end-hour")).toHaveTextContent(String(calendarDateTime.end.hour)); - expect(getByTestId("end-minute")).toHaveTextContent(String(calendarDateTime.end.minute)); - expect(getByTestId("end-second")).toHaveTextContent(String(calendarDateTime.end.second)); - expect(end.value).toHaveTextContent(calendarDateTime.end.toString()); - }); + expect(start.month).toHaveTextContent(String(calendarDateTime.start.month)); + expect(start.day).toHaveTextContent(String(calendarDateTime.start.day)); + expect(start.year).toHaveTextContent(String(calendarDateTime.start.year)); + expect(getByTestId("start-hour")).toHaveTextContent(String(calendarDateTime.start.hour)); + expect(getByTestId("start-minute")).toHaveTextContent(String(calendarDateTime.start.minute)); + expect(getByTestId("start-second")).toHaveTextContent(String(calendarDateTime.start.second)); + expect(start.value).toHaveTextContent(calendarDateTime.start.toString()); + + expect(end.month).toHaveTextContent(String(calendarDateTime.end.month)); + expect(end.day).toHaveTextContent(String(calendarDateTime.end.day)); + expect(end.year).toHaveTextContent(String(calendarDateTime.end.year)); + expect(getByTestId("end-hour")).toHaveTextContent(String(calendarDateTime.end.hour)); + expect(getByTestId("end-minute")).toHaveTextContent(String(calendarDateTime.end.minute)); + expect(getByTestId("end-second")).toHaveTextContent(String(calendarDateTime.end.second)); + expect(end.value).toHaveTextContent(calendarDateTime.end.toString()); +}); - it("should populate segment with value - `ZonedDateTime`", async () => { - const { start, end, getByTestId } = setup({ - value: zonedDateTime, - granularity: "second", - }); - - expect(start.month).toHaveTextContent(String(calendarDateTime.start.month)); - expect(start.day).toHaveTextContent(String(calendarDateTime.start.day)); - expect(start.year).toHaveTextContent(String(calendarDateTime.start.year)); - expect(getByTestId("start-hour")).toHaveTextContent(String(calendarDateTime.start.hour)); - expect(getByTestId("start-minute")).toHaveTextContent( - String(calendarDateTime.start.minute) - ); - expect(getByTestId("start-second")).toHaveTextContent( - String(calendarDateTime.start.second) - ); - expect(start.value).toHaveTextContent(calendarDateTime.start.toString()); - - expect(end.month).toHaveTextContent(String(calendarDateTime.end.month)); - expect(end.day).toHaveTextContent(String(calendarDateTime.end.day)); - expect(end.year).toHaveTextContent(String(calendarDateTime.end.year)); - expect(getByTestId("end-hour")).toHaveTextContent(String(calendarDateTime.end.hour)); - expect(getByTestId("end-minute")).toHaveTextContent(String(calendarDateTime.end.minute)); - expect(getByTestId("end-second")).toHaveTextContent(String(calendarDateTime.end.second)); - expect(end.value).toHaveTextContent(calendarDateTime.end.toString()); +it("should populate segment with value - `ZonedDateTime`", async () => { + const { start, end, getByTestId } = setup({ + value: zonedDateTime, + granularity: "second", }); - it("should navigate between the fields", async () => { - const { getByTestId, user } = setup({ - value: calendarDate, - }); + expect(start.month).toHaveTextContent(String(calendarDateTime.start.month)); + expect(start.day).toHaveTextContent(String(calendarDateTime.start.day)); + expect(start.year).toHaveTextContent(String(calendarDateTime.start.year)); + expect(getByTestId("start-hour")).toHaveTextContent(String(calendarDateTime.start.hour)); + expect(getByTestId("start-minute")).toHaveTextContent(String(calendarDateTime.start.minute)); + expect(getByTestId("start-second")).toHaveTextContent(String(calendarDateTime.start.second)); + expect(start.value).toHaveTextContent(calendarDateTime.start.toString()); + + expect(end.month).toHaveTextContent(String(calendarDateTime.end.month)); + expect(end.day).toHaveTextContent(String(calendarDateTime.end.day)); + expect(end.year).toHaveTextContent(String(calendarDateTime.end.year)); + expect(getByTestId("end-hour")).toHaveTextContent(String(calendarDateTime.end.hour)); + expect(getByTestId("end-minute")).toHaveTextContent(String(calendarDateTime.end.minute)); + expect(getByTestId("end-second")).toHaveTextContent(String(calendarDateTime.end.second)); + expect(end.value).toHaveTextContent(calendarDateTime.end.toString()); +}); + +it("should navigate between the fields", async () => { + const { getByTestId, user } = setup({ + value: calendarDate, + }); - const fields = ["start", "end"] as const; - const segments = ["month", "day", "year"] as const; + const fields = ["start", "end"] as const; + const segments = ["month", "day", "year"] as const; - await user.click(getByTestId("start-month")); + await user.click(getByTestId("start-month")); - for (const field of fields) { - for (const segment of segments) { - if (field === "start" && segment === "month") continue; - const seg = getByTestId(`${field}-${segment}`); - await user.keyboard(kbd.ARROW_RIGHT); - expect(seg).toHaveFocus(); - } + for (const field of fields) { + for (const segment of segments) { + if (field === "start" && segment === "month") continue; + const seg = getByTestId(`${field}-${segment}`); + await user.keyboard(kbd.ARROW_RIGHT); + expect(seg).toHaveFocus(); } + } - await user.click(getByTestId("start-month")); + await user.click(getByTestId("start-month")); - for (const field of fields) { - for (const segment of segments) { - if (field === "start" && segment === "month") continue; - const seg = getByTestId(`${field}-${segment}`); - await user.keyboard(kbd.TAB); - expect(seg).toHaveFocus(); - } + for (const field of fields) { + for (const segment of segments) { + if (field === "start" && segment === "month") continue; + const seg = getByTestId(`${field}-${segment}`); + await user.keyboard(kbd.TAB); + expect(seg).toHaveFocus(); } - }); + } +}); - it("should navigate between the fields - right to left", async () => { - const { getByTestId, user } = setup({ - value: calendarDate, - }); +it("should navigate between the fields - right to left", async () => { + const { getByTestId, user } = setup({ + value: calendarDate, + }); - const fields = ["end", "start"] as const; - const segments = ["year", "day", "month"] as const; + const fields = ["end", "start"] as const; + const segments = ["year", "day", "month"] as const; - await user.click(getByTestId("end-year")); + await user.click(getByTestId("end-year")); - for (const field of fields) { - for (const segment of segments) { - if (field === "end" && segment === "year") continue; - const seg = getByTestId(`${field}-${segment}`); - await user.keyboard(kbd.ARROW_LEFT); - expect(seg).toHaveFocus(); - } + for (const field of fields) { + for (const segment of segments) { + if (field === "end" && segment === "year") continue; + const seg = getByTestId(`${field}-${segment}`); + await user.keyboard(kbd.ARROW_LEFT); + expect(seg).toHaveFocus(); } + } - await user.click(getByTestId("end-year")); + await user.click(getByTestId("end-year")); - for (const field of fields) { - for (const segment of segments) { - if (field === "end" && segment === "year") continue; - const seg = getByTestId(`${field}-${segment}`); - await user.keyboard(kbd.SHIFT_TAB); - expect(seg).toHaveFocus(); - } + for (const field of fields) { + for (const segment of segments) { + if (field === "end" && segment === "year") continue; + const seg = getByTestId(`${field}-${segment}`); + await user.keyboard(kbd.SHIFT_TAB); + expect(seg).toHaveFocus(); } + } +}); + +it("should respect `bind:value` to the value", async () => { + const { start, end, user } = setup({ + value: calendarDate, }); + expect(start.value).toHaveTextContent(calendarDate.start.toString()); + expect(end.value).toHaveTextContent(calendarDate.end.toString()); + + await user.click(start.month); + await user.keyboard("2"); + expect(start.value).toHaveTextContent("2022-02-01"); + expect(end.value).toHaveTextContent(calendarDate.end.toString()); +}); - it("should respect `bind:value` to the value", async () => { - const { start, end, user } = setup({ - value: calendarDate, - }); - expect(start.value).toHaveTextContent(calendarDate.start.toString()); - expect(end.value).toHaveTextContent(calendarDate.end.toString()); - - await user.click(start.month); - await user.keyboard("2"); - expect(start.value).toHaveTextContent("2022-02-01"); - expect(end.value).toHaveTextContent(calendarDate.end.toString()); +it("should render an input for the start and end", async () => { + const { container } = setup({ + startProps: { + name: "start-hidden-input", + }, + endProps: { + name: "end-hidden-input", + }, }); + expect(container.querySelector('input[name="start-hidden-input"]')).toBeInTheDocument(); + expect(container.querySelector('input[name="end-hidden-input"]')).toBeInTheDocument(); +}); + +it("should populate calendar date with keyboard", async () => { + const { start, end, user } = setup({ value: calendarDate }); + + await user.click(start.month); + + await user.keyboard("2142020"); + await user.keyboard("2152020"); + + expect(start.value).toHaveTextContent("2020-02-14"); + expect(end.value).toHaveTextContent("2020-02-15"); +}); - it("should render an input for the start and end", async () => { - const { container } = setup({ - startProps: { - name: "start-hidden-input", - }, - endProps: { - name: "end-hidden-input", - }, - }); - expect(container.querySelector('input[name="start-hidden-input"]')).toBeInTheDocument(); - expect(container.querySelector('input[name="end-hidden-input"]')).toBeInTheDocument(); +it("should allow valid days in end month regardless of start month", async () => { + const t = setup(); + + await t.user.click(t.start.month); + await t.user.keyboard("2"); + await t.user.keyboard("02"); + await t.user.keyboard("2025"); + await t.user.keyboard("12"); + await t.user.keyboard("31"); + await t.user.keyboard("2025"); + + const seg = t.getByTestId(`end-day`); + expect(seg).toHaveTextContent("31"); + + expect(t.start.value).toHaveTextContent("2025-02-02"); + expect(t.end.value).toHaveTextContent("2025-12-31"); +}); + +it("should allow valid days in end month when a value is prepopulated", async () => { + const t = setup({ + value: { + start: new CalendarDate(2025, 2, 1), + end: new CalendarDate(2025, 5, 31), + }, }); + + const seg = t.getByTestId("end-day"); + expect(seg).toHaveTextContent("31"); + + seg.focus(); + await t.user.keyboard(kbd.ARROW_DOWN); + expect(seg).toHaveTextContent("30"); }); diff --git a/tests/src/tests/date-range-picker/date-range-picker.test.ts b/tests/src/tests/date-range-picker/date-range-picker.test.ts index 28d87cfe5..1abf2c2a4 100644 --- a/tests/src/tests/date-range-picker/date-range-picker.test.ts +++ b/tests/src/tests/date-range-picker/date-range-picker.test.ts @@ -272,6 +272,18 @@ it("should respect `bind:value` to the value", async () => { expect(end.value).toHaveTextContent(calendarDate.end.toString()); }); +it("should populate calendar date with keyboard", async () => { + const { start, end, user } = setup({ value: calendarDate }); + + await user.click(start.month); + + await user.keyboard("2142020"); + await user.keyboard("2152020"); + + expect(start.value).toHaveTextContent("2020-02-14"); + expect(end.value).toHaveTextContent("2020-02-15"); +}); + it("should render an input for the start and end", async () => { const { container } = setup({ startProps: { @@ -599,3 +611,22 @@ describe("correct weekday label formatting", () => { } }); }); + +it("should respect the `weekStartsOn` prop regardless of locale", async () => { + const t = await open({ + placeholder: new CalendarDate(1980, 1, 1), + weekStartsOn: 2, + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("mar."); +}); + +it("should default the first day of the week to the locale's first day of the week if `weekStartsOn` is not provided", async () => { + const t = await open({ + placeholder: new CalendarDate(1980, 1, 1), + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("lun."); +}); diff --git a/tests/src/tests/dialog/dialog.test.ts b/tests/src/tests/dialog/dialog.test.ts index 53712425a..21f0ffe90 100644 --- a/tests/src/tests/dialog/dialog.test.ts +++ b/tests/src/tests/dialog/dialog.test.ts @@ -203,6 +203,13 @@ it("should apply the correct `aria-describedby` attribute to the `Dialog.Content expect(content).toHaveAttribute("aria-describedby", description.id); }); +it("should have role='heading'", async () => { + const { getByTestId } = await open(); + + const title = getByTestId("title"); + expect(title).toHaveAttribute("role", "heading"); +}); + it("should apply a default `aria-level` attribute to the `Dialog.Title` element", async () => { const { getByTestId } = await open(); diff --git a/tests/src/tests/dropdown-menu/dropdown-menu-test.svelte b/tests/src/tests/dropdown-menu/dropdown-menu-test.svelte index 5552b2559..e77e7b326 100644 --- a/tests/src/tests/dropdown-menu/dropdown-menu-test.svelte +++ b/tests/src/tests/dropdown-menu/dropdown-menu-test.svelte @@ -6,9 +6,12 @@ radio?: string; subRadio?: string; open?: boolean; - contentProps?: Omit; - subContentProps?: Omit; + contentProps?: Omit; + subContentProps?: Omit; portalProps?: DropdownMenu.PortalProps; + subTriggerProps?: Omit; + checkboxGroupProps?: Omit; + group?: string[]; }; @@ -17,11 +20,14 @@ checked = false, subChecked = false, radio = "", + group = [], subRadio = "", open = false, contentProps = {}, subContentProps = {}, portalProps = {}, + subTriggerProps = {}, + checkboxGroupProps = {}, ...restProps }: DropdownMenuTestProps = $props(); @@ -45,7 +51,7 @@ - + subtrigger @@ -94,6 +100,24 @@ {/snippet} + + + {#snippet children({ checked })} + {checked} + Checkbox Item 1 + {/snippet} + + + {#snippet children({ checked })} + {checked} + Checkbox Item 2 + {/snippet} + + @@ -112,5 +136,10 @@ +
    diff --git a/tests/src/tests/dropdown-menu/dropdown-menu.test.ts b/tests/src/tests/dropdown-menu/dropdown-menu.test.ts index ebb28742b..edbc2df73 100644 --- a/tests/src/tests/dropdown-menu/dropdown-menu.test.ts +++ b/tests/src/tests/dropdown-menu/dropdown-menu.test.ts @@ -1,6 +1,6 @@ import { render, screen, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; -import { describe, it } from "vitest"; +import { it, vi } from "vitest"; import { tick } from "svelte"; import { getTestKbd, setupUserEvents, sleep } from "../utils.js"; import DropdownMenuTest from "./dropdown-menu-test.svelte"; @@ -71,325 +71,391 @@ async function openSubmenu(props: Awaited>) { }; } -describe("dropdown menu", () => { - it("should have no accessibility violations", async () => { - const { container } = render(DropdownMenuTest); - expect(await axe(container)).toHaveNoViolations(); - }); +it("should have no accessibility violations", async () => { + const { container } = render(DropdownMenuTest); + expect(await axe(container)).toHaveNoViolations(); +}); - it("should have bits data attrs", async () => { - const { user, trigger, getByTestId } = setup(); - await user.click(trigger); +it("should have bits data attrs", async () => { + const { user, trigger, getByTestId } = setup(); + await user.click(trigger); - const parts = [ - "content", - "trigger", - "group", - "group-heading", - "separator", - "sub-trigger", - "item", - "checkbox-item", - "radio-group", - "radio-item", - ]; - - for (const part of parts) { - const el = screen.getByTestId(part); - expect(el).toHaveAttribute(`data-dropdown-menu-${part}`); - } - - await user.click(getByTestId("sub-trigger")); - - const subContent = getByTestId("sub-content"); - expect(subContent).toHaveAttribute(`data-dropdown-menu-sub-content`); - }); + const parts = [ + "content", + "trigger", + "group", + "group-heading", + "separator", + "sub-trigger", + "item", + "checkbox-item", + "radio-group", + "radio-item", + "checkbox-group", + ]; + + for (const part of parts) { + const el = screen.getByTestId(part); + expect(el).toHaveAttribute(`data-dropdown-menu-${part}`); + } + + await user.click(getByTestId("sub-trigger")); + + const subContent = getByTestId("sub-content"); + expect(subContent).toHaveAttribute(`data-dropdown-menu-sub-content`); +}); - it.each(OPEN_KEYS)("should open when %s is pressed & respects binding", async (key) => { - await openWithKbd({}, key); - }); +it.each(OPEN_KEYS)("should open when %s is pressed & respects binding", async (key) => { + await openWithKbd({}, key); +}); - it("should open when clicked & respects binding", async () => { - const { getByTestId, queryByTestId, user, trigger } = setup(); - const binding = getByTestId("binding"); - expect(binding).toHaveTextContent("false"); - await user.click(trigger); - expect(queryByTestId("content")).not.toBeNull(); - expect(binding).toHaveTextContent("true"); - }); +it("should open when clicked & respects binding", async () => { + const { getByTestId, queryByTestId, user, trigger } = setup(); + const binding = getByTestId("binding"); + expect(binding).toHaveTextContent("false"); + await user.click(trigger); + expect(queryByTestId("content")).not.toBeNull(); + expect(binding).toHaveTextContent("true"); +}); - it("should manage focus correctly when opened with pointer", async () => { - const { getByTestId, user } = await openWithPointer(); +it("should manage focus correctly when opened with pointer", async () => { + const { getByTestId, user } = await openWithPointer(); - const item = getByTestId("item"); - expect(item).not.toHaveFocus(); + const item = getByTestId("item"); + expect(item).not.toHaveFocus(); - await user.keyboard(kbd.ARROW_DOWN); - expect(item).toHaveFocus(); - }); + await user.keyboard(kbd.ARROW_DOWN); + expect(item).toHaveFocus(); +}); - it("should manage focus correctly when opened with keyboard", async () => { - const { user, getByTestId, queryByTestId, trigger } = setup(); +it("should manage focus correctly when opened with keyboard", async () => { + const { user, getByTestId, queryByTestId, trigger } = setup(); - expect(queryByTestId("content")).toBeNull(); + expect(queryByTestId("content")).toBeNull(); - trigger.focus(); - await user.keyboard(kbd.ENTER); + trigger.focus(); + await user.keyboard(kbd.ENTER); - expect(queryByTestId("content")).not.toBeNull(); - const item = getByTestId("item"); - await waitFor(() => expect(item).toHaveFocus()); - }); + expect(queryByTestId("content")).not.toBeNull(); + const item = getByTestId("item"); + await waitFor(() => expect(item).toHaveFocus()); +}); - it("should open submenu with keyboard on subtrigger", async () => { - const { getByTestId, queryByTestId, user } = await openWithKbd(); +it("should open submenu with keyboard on subtrigger", async () => { + const { getByTestId, queryByTestId, user } = await openWithKbd(); - await user.keyboard(kbd.ARROW_DOWN); - const subtrigger = getByTestId("sub-trigger"); - await waitFor(() => expect(subtrigger).toHaveFocus()); - expect(queryByTestId("sub-content")).toBeNull(); - await user.keyboard(kbd.ARROW_RIGHT); - expect(queryByTestId("sub-content")).not.toBeNull(); - await waitFor(() => expect(getByTestId("sub-item")).toHaveFocus()); - }); + await user.keyboard(kbd.ARROW_DOWN); + const subtrigger = getByTestId("sub-trigger"); + await waitFor(() => expect(subtrigger).toHaveFocus()); + expect(queryByTestId("sub-content")).toBeNull(); + await user.keyboard(kbd.ARROW_RIGHT); + expect(queryByTestId("sub-content")).not.toBeNull(); + await waitFor(() => expect(getByTestId("sub-item")).toHaveFocus()); +}); - it("should toggle the checkbox item when clicked & respects binding", async () => { - const { getByTestId, user, trigger } = await openWithPointer(); - const checkedBinding = getByTestId("checked-binding"); - const indicator = getByTestId("checkbox-indicator"); - expect(indicator).not.toHaveTextContent("checked"); - expect(checkedBinding).toHaveTextContent("false"); - const checkbox = getByTestId("checkbox-item"); - await user.click(checkbox); - expect(checkedBinding).toHaveTextContent("true"); - await user.click(trigger); - expect(indicator).toHaveTextContent("true"); - await user.click(getByTestId("checkbox-item")); - expect(checkedBinding).toHaveTextContent("false"); +it("should toggle the checkbox item when clicked & respects binding", async () => { + const { getByTestId, user, trigger } = await openWithPointer(); + const checkedBinding = getByTestId("checked-binding"); + const indicator = getByTestId("checkbox-indicator"); + expect(indicator).not.toHaveTextContent("checked"); + expect(checkedBinding).toHaveTextContent("false"); + const checkbox = getByTestId("checkbox-item"); + await user.click(checkbox); + expect(checkedBinding).toHaveTextContent("true"); + await user.click(trigger); + expect(indicator).toHaveTextContent("true"); + await user.click(getByTestId("checkbox-item")); + expect(checkedBinding).toHaveTextContent("false"); - await user.click(checkedBinding); - expect(checkedBinding).toHaveTextContent("true"); - await user.click(trigger); - expect(getByTestId("checkbox-indicator")).toHaveTextContent("true"); - }); + await user.click(checkedBinding); + expect(checkedBinding).toHaveTextContent("true"); + await user.click(trigger); + expect(getByTestId("checkbox-indicator")).toHaveTextContent("true"); +}); - it("should toggle checkbox items within submenus when clicked & respects binding", async () => { - const props = await openWithKbd(); - const { getByTestId, user, trigger } = props; - await openSubmenu(props); - const subCheckedBinding = getByTestId("sub-checked-binding"); - expect(subCheckedBinding).toHaveTextContent("false"); - const indicator = getByTestId("sub-checkbox-indicator"); - expect(indicator).not.toHaveTextContent("true"); - const subCheckbox = getByTestId("sub-checkbox-item"); - await user.click(subCheckbox); - expect(subCheckedBinding).toHaveTextContent("true"); - trigger.focus(); - await user.keyboard(kbd.ARROW_DOWN); - await openSubmenu(props); - expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true"); - await user.click(getByTestId("sub-checkbox-item")); - expect(subCheckedBinding).toHaveTextContent("false"); - - await user.click(subCheckedBinding); - expect(subCheckedBinding).toHaveTextContent("true"); - trigger.focus(); - await user.keyboard(kbd.ARROW_DOWN); - await openSubmenu(props); - expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true"); - }); +it("should toggle checkbox items within submenus when clicked & respects binding", async () => { + const props = await openWithKbd(); + const { getByTestId, user, trigger } = props; + await openSubmenu(props); + const subCheckedBinding = getByTestId("sub-checked-binding"); + expect(subCheckedBinding).toHaveTextContent("false"); + const indicator = getByTestId("sub-checkbox-indicator"); + expect(indicator).not.toHaveTextContent("true"); + const subCheckbox = getByTestId("sub-checkbox-item"); + await user.click(subCheckbox); + expect(subCheckedBinding).toHaveTextContent("true"); + trigger.focus(); + await user.keyboard(kbd.ARROW_DOWN); + await openSubmenu(props); + expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true"); + await user.click(getByTestId("sub-checkbox-item")); + expect(subCheckedBinding).toHaveTextContent("false"); - it("should check the radio item when clicked & respects binding", async () => { - const { getByTestId, queryByTestId, user, trigger } = await openWithPointer(); - const radioBinding = getByTestId("radio-binding"); - expect(radioBinding).toHaveTextContent(""); - const radioItem1 = getByTestId("radio-item"); - await user.click(radioItem1); - expect(radioBinding).toHaveTextContent("1"); - await user.click(trigger); - const radioIndicator1 = getByTestId("radio-indicator-1"); - expect(radioIndicator1).not.toBeNull(); - expect(radioIndicator1).toHaveTextContent("true"); - const radioItem2 = getByTestId("radio-item-2"); - await user.click(radioItem2); - expect(radioBinding).toHaveTextContent("2"); - await user.click(trigger); - expect(queryByTestId("radio-indicator-1")).toHaveTextContent("false"); - expect(queryByTestId("radio-indicator-2")).toHaveTextContent("true"); + await user.click(subCheckedBinding); + expect(subCheckedBinding).toHaveTextContent("true"); + trigger.focus(); + await user.keyboard(kbd.ARROW_DOWN); + await openSubmenu(props); + expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true"); +}); - await user.keyboard(kbd.ESCAPE); - expect(queryByTestId("content")).toBeNull(); - await user.click(radioBinding); - expect(radioBinding).toHaveTextContent(""); - await user.click(trigger); - }); +it("should check the radio item when clicked & respects binding", async () => { + const { getByTestId, queryByTestId, user, trigger } = await openWithPointer(); + const radioBinding = getByTestId("radio-binding"); + expect(radioBinding).toHaveTextContent(""); + const radioItem1 = getByTestId("radio-item"); + await user.click(radioItem1); + expect(radioBinding).toHaveTextContent("1"); + await user.click(trigger); + const radioIndicator1 = getByTestId("radio-indicator-1"); + expect(radioIndicator1).not.toBeNull(); + expect(radioIndicator1).toHaveTextContent("true"); + const radioItem2 = getByTestId("radio-item-2"); + await user.click(radioItem2); + expect(radioBinding).toHaveTextContent("2"); + await user.click(trigger); + expect(queryByTestId("radio-indicator-1")).toHaveTextContent("false"); + expect(queryByTestId("radio-indicator-2")).toHaveTextContent("true"); - it("should skip over disabled items when navigating with the keyboard", async () => { - const { user, getByTestId } = await openWithKbd(); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); - expect(getByTestId("disabled-item")).not.toHaveFocus(); - expect(getByTestId("disabled-item-2")).not.toHaveFocus(); - }); + await user.keyboard(kbd.ESCAPE); + expect(queryByTestId("content")).toBeNull(); + await user.click(radioBinding); + expect(radioBinding).toHaveTextContent(""); + await user.click(trigger); +}); - it("should not loop through the menu items when the `loop` prop is set to false/undefined", async () => { - const { user, getByTestId } = await openWithKbd({ - contentProps: { - loop: false, - }, - }); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("item-2")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("radio-item")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("radio-item-2")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("item")).not.toHaveFocus()); - }); +it("should skip over disabled items when navigating with the keyboard", async () => { + const { user, getByTestId } = await openWithKbd(); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); + expect(getByTestId("disabled-item")).not.toHaveFocus(); + expect(getByTestId("disabled-item-2")).not.toHaveFocus(); +}); - it("should loop through the menu items when the `loop` prop is set to true", async () => { - const { user, getByTestId } = await openWithKbd({ - contentProps: { - loop: true, - }, - }); - await sleep(25); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("item-2")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("radio-item")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("radio-item-2")).toHaveFocus()); - await user.keyboard(kbd.ARROW_DOWN); - await waitFor(() => expect(getByTestId("item")).toHaveFocus()); +it("should not loop through the menu items when the `loop` prop is set to false/undefined", async () => { + const { user, getByTestId } = await openWithKbd({ + contentProps: { + loop: false, + }, }); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("radio-item")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("radio-item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("item")).not.toHaveFocus()); +}); - it("should close the menu on escape", async () => { - const { queryByTestId, user } = await openWithKbd(); - await user.keyboard(kbd.ESCAPE); - expect(queryByTestId("content")).toBeNull(); +it("should loop through the menu items when the `loop` prop is set to true", async () => { + const { user, getByTestId } = await openWithKbd({ + contentProps: { + loop: true, + }, }); + await sleep(25); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-item")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("radio-item")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("radio-item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-group-item-1")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("checkbox-group-item-2")).toHaveFocus()); + await user.keyboard(kbd.ARROW_DOWN); + await waitFor(() => expect(getByTestId("item")).toHaveFocus()); +}); - it("should respect the `escapeKeydownBehavior` prop", async () => { - const { queryByTestId, user } = await openWithKbd({ - contentProps: { - escapeKeydownBehavior: "ignore", - }, - }); - await user.keyboard(kbd.ESCAPE); - expect(queryByTestId("content")).not.toBeNull(); - }); +it("should close the menu on escape", async () => { + const { queryByTestId, user } = await openWithKbd(); + await user.keyboard(kbd.ESCAPE); + expect(queryByTestId("content")).toBeNull(); +}); - it("should respect the `interactOutsideBehavior` prop", async () => { - const { queryByTestId, user, getByTestId } = await openWithPointer({ - contentProps: { - interactOutsideBehavior: "ignore", - }, - }); - const outside = getByTestId("outside"); - await user.click(outside); - expect(queryByTestId("content")).not.toBeNull(); +it("should respect the `escapeKeydownBehavior` prop", async () => { + const { queryByTestId, user } = await openWithKbd({ + contentProps: { + escapeKeydownBehavior: "ignore", + }, }); + await user.keyboard(kbd.ESCAPE); + expect(queryByTestId("content")).not.toBeNull(); +}); - it("should portal to the body if a `portal` prop is not passed", async () => { - const { getByTestId } = await openWithPointer(); - const content = getByTestId("content"); - expect(content.parentElement?.parentElement).toEqual(document.body); +it("should respect the `interactOutsideBehavior` prop", async () => { + const { queryByTestId, user, getByTestId } = await openWithPointer({ + contentProps: { + interactOutsideBehavior: "ignore", + }, }); + const outside = getByTestId("outside"); + await user.click(outside); + expect(queryByTestId("content")).not.toBeNull(); +}); - it("should portal to the portal target if a valid `portal` prop is passed", async () => { - const { getByTestId } = await openWithPointer({ - portalProps: { - to: "#portal-target", - }, - }); - const content = getByTestId("content"); - const portalTarget = getByTestId("portal-target"); - expect(content.parentElement?.parentElement).toEqual(portalTarget); +it("should portal to the body if a `portal` prop is not passed", async () => { + const { getByTestId } = await openWithPointer(); + const content = getByTestId("content"); + expect(content.parentElement?.parentElement).toEqual(document.body); +}); + +it("should portal to the portal target if a valid `portal` prop is passed", async () => { + const { getByTestId } = await openWithPointer({ + portalProps: { + to: "#portal-target", + }, }); + const content = getByTestId("content"); + const portalTarget = getByTestId("portal-target"); + expect(content.parentElement?.parentElement).toEqual(portalTarget); +}); - it("should not portal if `disabled` is passed to the portal", async () => { - const { getByTestId } = await openWithPointer({ - portalProps: { - disabled: true, - }, - }); - const content = getByTestId("content"); - const ogContainer = getByTestId("non-portal-container"); - const contentWrapper = content.parentElement; - expect(contentWrapper?.parentElement).not.toEqual(document.body); - expect(contentWrapper?.parentElement).toEqual(ogContainer); +it("should not portal if `disabled` is passed to the portal", async () => { + const { getByTestId } = await openWithPointer({ + portalProps: { + disabled: true, + }, }); + const content = getByTestId("content"); + const ogContainer = getByTestId("non-portal-container"); + const contentWrapper = content.parentElement; + expect(contentWrapper?.parentElement).not.toEqual(document.body); + expect(contentWrapper?.parentElement).toEqual(ogContainer); +}); - it("should allow preventing autofocusing first item with `onOpenAutoFocus` prop", async () => { - const { getByTestId } = await openWithKbd({ - contentProps: { - onOpenAutoFocus: (e) => { - e.preventDefault(); - }, +it("should allow preventing autofocusing first item with `onOpenAutoFocus` prop", async () => { + const { getByTestId } = await openWithKbd({ + contentProps: { + onOpenAutoFocus: (e) => { + e.preventDefault(); }, - }); - await waitFor(() => expect(getByTestId("item")).not.toHaveFocus()); + }, }); + await waitFor(() => expect(getByTestId("item")).not.toHaveFocus()); +}); - it("should forceMount the content when `forceMount` is true", async () => { - const { getByTestId } = setup({ component: DropdownMenuForceMountTest }); +it("should forceMount the content when `forceMount` is true", async () => { + const { getByTestId } = setup({ component: DropdownMenuForceMountTest }); - expect(getByTestId("content")).toBeVisible(); + expect(getByTestId("content")).toBeVisible(); +}); + +it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { + const { queryByTestId, getByTestId, user, trigger } = setup({ + withOpenCheck: true, + component: DropdownMenuForceMountTest, }); + expect(queryByTestId("content")).toBeNull(); + + await user.click(trigger); - it("should forceMount the content when `forceMount` is true and the `open` snippet prop is used to conditionally render the content", async () => { - const { queryByTestId, getByTestId, user, trigger } = setup({ + const content = getByTestId("content"); + expect(content).toBeVisible(); +}); + +it.each([DropdownMenuTest, DropdownMenuForceMountTest])( + "should close the menu and focus the next tabbable element when `TAB` is pressed while the menu is open", + async (component) => { + const { trigger, user, getByTestId, queryByTestId } = await openWithKbd({ + component, withOpenCheck: true, - component: DropdownMenuForceMountTest, }); - expect(queryByTestId("content")).toBeNull(); + const nextButton = getByTestId("next-button"); + await user.keyboard(kbd.TAB); + await waitFor(() => expect(nextButton).toHaveFocus()); + expect(queryByTestId("content")).toBeNull(); await user.click(trigger); + await waitFor(() => expect(queryByTestId("content")).not.toBeNull()); + } +); + +it.each([DropdownMenuTest, DropdownMenuForceMountTest])( + "should close the menu and focus the previous tabbable element when `SHIFT+TAB` is pressed while the menu is open", + async (component) => { + const { user, getByTestId, queryByTestId } = await openWithKbd({ + component, + withOpenCheck: true, + }); + const previousButton = getByTestId("previous-button"); + await user.keyboard(kbd.SHIFT_TAB); + await waitFor(() => expect(previousButton).toHaveFocus()); + expect(queryByTestId("content")).toBeNull(); + } +); + +it("should respect the `onSelect` prop on SubTrigger", async () => { + const onSelect = vi.fn(); + const { getByTestId, user } = await openWithPointer({ + subTriggerProps: { + onSelect, + }, + }); + + await user.click(getByTestId("sub-trigger")); + expect(onSelect).toHaveBeenCalled(); + + await user.keyboard(kbd.ENTER); + expect(onSelect).toHaveBeenCalledTimes(2); + + await user.keyboard(kbd.ARROW_RIGHT); + expect(onSelect).toHaveBeenCalledTimes(3); +}); - const content = getByTestId("content"); - expect(content).toBeVisible(); +it("should respect the `value` prop on CheckboxGroup", async () => { + const t = await openWithPointer({ + group: ["1"], }); - it.each([DropdownMenuTest, DropdownMenuForceMountTest])( - "should close the menu and focus the next tabbable element when `TAB` is pressed while the menu is open", - async (component) => { - const { trigger, user, getByTestId, queryByTestId } = await openWithKbd({ - component, - withOpenCheck: true, - }); - const nextButton = getByTestId("next-button"); - await user.keyboard(kbd.TAB); - await waitFor(() => expect(nextButton).toHaveFocus()); - - expect(queryByTestId("content")).toBeNull(); - await user.click(trigger); - await waitFor(() => expect(queryByTestId("content")).not.toBeNull()); - } - ); - - it.each([DropdownMenuTest, DropdownMenuForceMountTest])( - "should close the menu and focus the previous tabbable element when `SHIFT+TAB` is pressed while the menu is open", - async (component) => { - const { user, getByTestId, queryByTestId } = await openWithKbd({ - component, - withOpenCheck: true, - }); - const previousButton = getByTestId("previous-button"); - await user.keyboard(kbd.SHIFT_TAB); - await waitFor(() => expect(previousButton).toHaveFocus()); - expect(queryByTestId("content")).toBeNull(); - } - ); + const checkboxGroupItem1 = t.getByTestId("checkbox-group-item-1"); + expect(checkboxGroupItem1).toHaveAttribute("aria-checked", "true"); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("true"); + expect(t.queryByTestId("checkbox-indicator-2")).toHaveTextContent("false"); + + await t.user.click(checkboxGroupItem1); + await t.user.click(t.trigger); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("false"); + + await t.user.click(t.getByTestId("checkbox-group-item-2")); + await t.user.click(t.trigger); + + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("true"); + + await t.user.click(t.getByTestId("checkbox-group-binding")); + expect(t.getByTestId("checkbox-indicator-1")).toHaveTextContent("false"); + expect(t.getByTestId("checkbox-indicator-2")).toHaveTextContent("false"); +}); + +it("calls `onValueChange` when the value of the checkbox group changes", async () => { + const onValueChange = vi.fn(); + const t = await openWithPointer({ + checkboxGroupProps: { + onValueChange, + }, + }); + await t.user.click(t.getByTestId("checkbox-group-item-1")); + expect(onValueChange).toHaveBeenCalledWith(["1"]); + await t.user.click(t.trigger); + await t.user.click(t.getByTestId("checkbox-group-item-2")); + expect(onValueChange).toHaveBeenCalledWith(["1", "2"]); + await t.user.click(t.trigger); + await t.user.click(t.getByTestId("checkbox-group-item-1")); + expect(onValueChange).toHaveBeenCalledWith(["2"]); }); diff --git a/tests/src/tests/menubar/menubar-menu-test.svelte b/tests/src/tests/menubar/menubar-menu-test.svelte index a4770bef1..79f43d7a7 100644 --- a/tests/src/tests/menubar/menubar-menu-test.svelte +++ b/tests/src/tests/menubar/menubar-menu-test.svelte @@ -1,16 +1,18 @@ @@ -25,7 +27,7 @@ - + subtrigger diff --git a/tests/src/tests/menubar/menubar.test.ts b/tests/src/tests/menubar/menubar.test.ts index 5fffdd88d..7155086cb 100644 --- a/tests/src/tests/menubar/menubar.test.ts +++ b/tests/src/tests/menubar/menubar.test.ts @@ -137,3 +137,28 @@ it("should call the menus `onOpenChange` callback when the menu is opened or clo expect(callback).toHaveBeenCalledTimes(2); } }); + +it("should respect the `onSelect` prop on SubTrigger", async () => { + const onSelect = vi.fn(); + const { user, getTrigger, getSubTrigger } = setup({ + one: { + subTriggerProps: { + onSelect, + }, + }, + }); + + const trigger = getTrigger("1"); + await user.click(trigger); + const subTrigger = getSubTrigger("1"); + + expect(subTrigger).not.toBeNull(); + await user.click(subTrigger!); + expect(onSelect).toHaveBeenCalled(); + + await user.keyboard(kbd.ENTER); + expect(onSelect).toHaveBeenCalledTimes(2); + + await user.keyboard(kbd.ARROW_RIGHT); + expect(onSelect).toHaveBeenCalledTimes(3); +}); diff --git a/tests/src/tests/navigation-menu/navigation-menu-test.svelte b/tests/src/tests/navigation-menu/navigation-menu-test.svelte new file mode 100644 index 000000000..1b9789f0d --- /dev/null +++ b/tests/src/tests/navigation-menu/navigation-menu-test.svelte @@ -0,0 +1,110 @@ + + + + +
    + + + + + + trigger + + + + + + + + + + sub + + + + + + + sub1 + + + + + + + + sub2 + + + + + + + {#if !noSubViewport} + + {/if} + + + + + + + link + + + + + + + {#if !noViewport} + + {/if} + + +
    diff --git a/tests/src/tests/navigation-menu/navigation-menu.test.ts b/tests/src/tests/navigation-menu/navigation-menu.test.ts new file mode 100644 index 000000000..2463b72df --- /dev/null +++ b/tests/src/tests/navigation-menu/navigation-menu.test.ts @@ -0,0 +1,233 @@ +import { render, waitFor } from "@testing-library/svelte/svelte5"; +import { userEvent } from "@testing-library/user-event"; +import { axe } from "jest-axe"; +import { it } from "vitest"; +import NavigationMenuTest, { type NavigationMenuTestProps } from "./navigation-menu-test.svelte"; +import { getTestKbd } from "../utils"; + +const kbd = getTestKbd(); + +/** + * Helper function to reduce boilerplate in tests + */ +function setup(props: NavigationMenuTestProps = {}) { + const user = userEvent.setup(); + const returned = render(NavigationMenuTest, { ...props }); + + return { user, ...returned }; +} + +it("should have no accessibility violations", async () => { + const { container } = render(NavigationMenuTest); + expect(await axe(container)).toHaveNoViolations(); +}); + +it("should open viewport when hovering trigger", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + expect(t.queryByTestId("viewport")).toBeNull(); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + expect(t.queryByTestId("viewport")).toContainElement(t.queryByTestId("group-item-content")); +}); + +it("should toggle viewport when pressing enter on focused trigger", async () => { + const t = setup(); + t.getByTestId("sub-group-item-trigger").focus(); + expect(t.queryByTestId("viewport")).toBeNull(); + await t.user.keyboard(kbd.ENTER); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + expect(t.queryByTestId("viewport")).toContainElement(t.queryByTestId("sub-group-item-content")); + await t.user.keyboard(kbd.ENTER); + await waitFor(() => expect(t.queryByTestId("viewport")).toBeNull()); +}); + +it("should show initial submenu items on trigger hover", async () => { + const t = setup(); + const trigger = t.getByTestId("sub-group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + const visibleSubContent = t.queryByTestId("sub-group-item-sub-item1-content"); + expect(visibleSubContent).not.toBeNull(); + expect(t.queryByTestId("sub-group-item-sub-item2-content")).toBeNull(); + expect(t.queryByTestId("sub-group-item-sub-viewport")).toContainElement(visibleSubContent); +}); + +it("should show submenu items on subtrigger hover", async () => { + const t = setup(); + const trigger = t.getByTestId("sub-group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + const subTrigger = t.getByTestId("sub-group-item-sub-item2-trigger"); + await t.user.hover(subTrigger); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull()); + expect(t.queryByTestId("sub-group-item-sub-item1-content")).toBeNull(); + expect(t.queryByTestId("sub-group-item-sub-viewport")).toContainElement( + t.queryByTestId("sub-group-item-sub-item2-content") + ); + // does not hide when clicking open subtrigger + await t.user.click(t.getByTestId("sub-group-item-sub-item2-trigger")); + expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull(); +}); + +it("should open submenu viewport when pressing enter on focused subtrigger", async () => { + const t = setup(); + t.getByTestId("sub-group-item-trigger").focus(); + await t.user.keyboard(kbd.ENTER); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item2-trigger")).not.toBeNull()); + t.getByTestId("sub-group-item-sub-item2-trigger").focus(); + await t.user.keyboard(kbd.ENTER); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull()); + expect(t.queryByTestId("sub-group-item-sub-viewport")).toContainElement( + t.queryByTestId("sub-group-item-sub-item2-content") + ); + // does not hide when re-pressing enter + await t.user.keyboard(kbd.ENTER); + expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull(); +}); + +it("should show indicator when hovering trigger", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("indicator")).not.toBeNull()); +}); + +it("should receive focus on the first item", async () => { + const t = setup(); + t.getByTestId("previous-button").focus(); + await t.user.keyboard(kbd.TAB); + expect(t.getByTestId("group-item-trigger")).toHaveFocus(); +}); + +it("should focus next item with right arrow or down arrow, and previous with left or up", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + trigger.focus(); + await t.user.keyboard(kbd.ARROW_RIGHT); + expect(t.getByTestId("sub-group-item-trigger")).toHaveFocus(); + await t.user.keyboard(kbd.ARROW_RIGHT); + expect(t.getByTestId("link-item-link")).toHaveFocus(); +}); + +it("should focus next item with tab", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + trigger.focus(); + await t.user.keyboard(kbd.TAB); + expect(t.getByTestId("sub-group-item-trigger")).toHaveFocus(); + await t.user.keyboard(kbd.TAB); + expect(t.getByTestId("link-item-link")).toHaveFocus(); +}); + +it("should focus next sub-item with right arrow, and previous with left", async () => { + const t = setup(); + const trigger = t.getByTestId("sub-group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-viewport")).not.toBeNull()); + const subTrigger = t.getByTestId("sub-group-item-sub-item1-trigger"); + subTrigger.focus(); + await t.user.keyboard(kbd.ARROW_RIGHT); + expect(t.getByTestId("sub-group-item-sub-item2-trigger")).toHaveFocus(); + await t.user.keyboard(kbd.ARROW_LEFT); + expect(t.getByTestId("sub-group-item-sub-item1-trigger")).toHaveFocus(); +}); + +it("should focus next content item with right arrow or down arrow, and previous with left or up", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + trigger.focus(); + await t.user.keyboard(kbd.ENTER); + t.getByTestId("group-item-content-button1").focus(); + await t.user.keyboard(kbd.ARROW_RIGHT); + expect(t.getByTestId("group-item-content-button2")).toHaveFocus(); + await t.user.keyboard(kbd.ARROW_LEFT); + expect(t.getByTestId("group-item-content-button1")).toHaveFocus(); + await t.user.keyboard(kbd.ARROW_DOWN); + expect(t.getByTestId("group-item-content-button2")).toHaveFocus(); + await t.user.keyboard(kbd.ARROW_UP); + expect(t.getByTestId("group-item-content-button1")).toHaveFocus(); +}); + +it("should focus next on content with tab when opened", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + trigger.focus(); + await t.user.keyboard(kbd.ENTER); + await t.user.keyboard(kbd.TAB); + expect(t.getByTestId("group-item-content-button1")).toHaveFocus(); +}); + +it("should focus next on content with down arrow when opened", async () => { + const t = setup(); + const trigger = t.getByTestId("group-item-trigger"); + trigger.focus(); + await t.user.keyboard(kbd.ENTER); + await t.user.keyboard(kbd.ARROW_DOWN); + expect(t.getByTestId("group-item-content-button1")).toHaveFocus(); +}); + +it("should render content without viewport", async () => { + const t = setup({ noViewport: true }); + const trigger = t.getByTestId("group-item-trigger"); + expect(t.queryByTestId("viewport")).toBeNull(); + expect(t.queryByTestId("group-item-content")).toBeNull(); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("group-item-content")).not.toBeNull()); + expect(t.queryByTestId("viewport")).toBeNull(); + expect(t.queryByTestId("group-item")).toContainElement(t.queryByTestId("group-item-content")); +}); + +it("should render subcontent without subviewport", async () => { + const t = setup({ noSubViewport: true }); + const trigger = t.getByTestId("sub-group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + const subTrigger = t.getByTestId("sub-group-item-sub-item2-trigger"); + await t.user.hover(subTrigger); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull()); + expect(t.queryByTestId("sub-group-item-sub-item2")).toContainElement( + t.queryByTestId("sub-group-item-sub-item2-content") + ); + expect(t.queryByTestId("sub-group-item-sub-viewport")).toBeNull(); +}); + +it("should switch between submenu items on pointer hover", async () => { + const t = setup(); + + // First open the main submenu + const trigger = t.getByTestId("sub-group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + + // Hover over first sub-trigger to open its content + const subTrigger1 = t.getByTestId("sub-group-item-sub-item1-trigger"); + await t.user.hover(subTrigger1); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item1-content")).not.toBeNull()); + expect(t.queryByTestId("sub-group-item-sub-item2-content")).toBeNull(); + + // Hover over second sub-trigger - should close first and open second + const subTrigger2 = t.getByTestId("sub-group-item-sub-item2-trigger"); + await t.user.hover(subTrigger2); + await waitFor(() => expect(t.queryByTestId("sub-group-item-sub-item2-content")).not.toBeNull()); + expect(t.queryByTestId("sub-group-item-sub-item1-content")).toBeNull(); +}); + +it("should not open menu on hover when `openOnHover` is false", async () => { + const t = setup({ groupItemProps: { openOnHover: false } }); + const trigger = t.getByTestId("group-item-trigger"); + await t.user.hover(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).toBeNull()); + await waitFor(() => expect(t.queryByTestId("group-item-content")).toBeNull()); +}); + +it("should toggle on trigger click when `openOnHover` is false", async () => { + const t = setup({ groupItemProps: { openOnHover: false } }); + const trigger = t.getByTestId("group-item-trigger"); + await t.user.click(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).not.toBeNull()); + await waitFor(() => expect(t.queryByTestId("group-item-content")).not.toBeNull()); + await t.user.click(trigger); + await waitFor(() => expect(t.queryByTestId("viewport")).toBeNull()); + await waitFor(() => expect(t.queryByTestId("group-item-content")).toBeNull()); +}); diff --git a/tests/src/tests/range-calendar/range-calendar.test.ts b/tests/src/tests/range-calendar/range-calendar.test.ts index 0275f5fa4..7073bb091 100644 --- a/tests/src/tests/range-calendar/range-calendar.test.ts +++ b/tests/src/tests/range-calendar/range-calendar.test.ts @@ -363,3 +363,22 @@ it("should not allow focusing on disabled dates, even if they are the only selec await user.keyboard(kbd.TAB); expect(getByTestId("date-1-1")).toHaveFocus(); }); + +it("should respect the `weekStartsOn` prop regardless of locale", async () => { + const t = setup({ + placeholder: new CalendarDate(1980, 1, 1), + weekStartsOn: 2, + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("mar."); +}); + +it("should default the first day of the week to the locale's first day of the week if `weekStartsOn` is not provided", async () => { + const t = setup({ + placeholder: new CalendarDate(1980, 1, 1), + weekdayFormat: "short", + locale: "fr", + }); + expect(t.getByTestId("weekday-1-0").textContent).toBe("lun."); +});