diff --git a/NOTES.md b/NOTES.md index f390a6508..48df4380d 100644 --- a/NOTES.md +++ b/NOTES.md @@ -30,3 +30,11 @@ They also need to be flexible enough to allow for reusing the same component whi --- Should we embrace the [ValidityState](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) API? Something to think about. + +--- + +We need to expose the different `open` states via snippet props for all the `forceMount`-able components to work with Svelte and other transition/animation libs. + +--- + +We need to handle `invalid` state for the `DateRangeField` component. diff --git a/eslint.config.js b/eslint.config.js index 7dd4c041d..545a1e4a5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,23 +6,23 @@ export default config({ svelte: true, ignores: [...DEFAULT_IGNORES, ...ignores] .override("antfu/typescript/rules", { rules: { "ts/consistent-type-definitions": "off", - "ts/ban-types": [ - "error", - { - types: { - "{}": false, - }, - }, - ], + "unused-imports/no-unused-imports": "off", + "unused-imports/no-unused-vars": "off", + "ts/no-unused-expressions": "off", + "no-unused-expressions": "off", + "ts/no-empty-object-type": "off", }, }) .override("antfu/javascript/rules", { rules: { "no-unused-expressions": "off", + "unused-imports/no-unused-imports": "off", }, }) .override("huntabyte/svelte/rules", { rules: { "svelte/no-at-html-tags": "off", + "unused-imports/no-unused-imports": "off", + "unused-imports/no-unused-vars": "off", }, }); diff --git a/package.json b/package.json index e8f61482b..e0437558f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build:packages": "pnpm -F \"./packages/**\" --parallel build", "check": "pnpm build:packages && pnpm -r check", "ci:publish": "pnpm build:packages && changeset publish", - "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel dev", + "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel --reporter append-only --color dev", "format": "prettier --write .", "lint": "prettier --check . && eslint .", "lint:fix": "eslint --fix .", diff --git a/packages/bits-ui/other/setupTest.ts b/packages/bits-ui/other/setupTest.ts index e467f299b..d85e88869 100644 --- a/packages/bits-ui/other/setupTest.ts +++ b/packages/bits-ui/other/setupTest.ts @@ -90,6 +90,7 @@ vi.mock("$app/stores", (): typeof stores => { // eslint-disable-next-line ts/no-require-imports globalThis.ResizeObserver = require("resize-observer-polyfill"); Element.prototype.scrollIntoView = () => {}; +// eslint-disable-next-line ts/no-explicit-any Element.prototype.hasPointerCapture = (() => {}) as any; // @ts-expect-error - shut it 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 b3a6cd51f..50c12627d 100644 --- a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts +++ b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts @@ -1,17 +1,15 @@ +import type { Box, ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; +import type { WithRefProps } from "$lib/internal/types.js"; +import { afterTick } from "$lib/internal/afterTick.js"; import { - type Box, - type ReadableBoxedValues, - type WithRefProps, - type WritableBoxedValues, - afterTick, getAriaDisabled, getAriaExpanded, getDataDisabled, getDataOpenClosed, getDataOrientation, - kbd, - useRefById, -} from "$lib/internal/index.js"; +} from "$lib/internal/attrs.js"; +import { kbd } from "$lib/internal/kbd.js"; +import { useRefById } from "$lib/internal/useRefById.svelte.js"; import { type UseRovingFocusReturn, useRovingFocus } from "$lib/internal/useRovingFocus.svelte.js"; import type { Orientation } from "$lib/shared/index.js"; import { createContext } from "$lib/internal/createContext.js"; @@ -144,7 +142,7 @@ export class AccordionItemState { value: AccordionItemStateProps["value"]; disabled: AccordionItemStateProps["disabled"]; root: AccordionState; - isSelected = $derived.by(() => this.root.includesItem(this.value.current)); + isActive = $derived.by(() => this.root.includesItem(this.value.current)); isDisabled = $derived.by(() => this.disabled.current || this.root.disabled.current); constructor(props: AccordionItemStateProps) { @@ -176,12 +174,15 @@ export class AccordionItemState { return new AccordionHeaderState(props, this); } - props = $derived.by(() => ({ - id: this.#id.current, - [ACCORDION_ITEM_ATTR]: "", - "data-state": getDataOpenClosed(this.isSelected), - "data-disabled": getDataDisabled(this.isDisabled), - })); + props = $derived.by( + () => + ({ + id: this.#id.current, + "data-state": getDataOpenClosed(this.isActive), + "data-disabled": getDataDisabled(this.isDisabled), + [ACCORDION_ITEM_ATTR]: "", + }) as const + ); } // @@ -190,7 +191,7 @@ export class AccordionItemState { type AccordionTriggerStateProps = WithRefProps< ReadableBoxedValues<{ - disabled: boolean; + disabled: boolean | null | undefined; }> >; @@ -240,10 +241,10 @@ class AccordionTriggerState { ({ id: this.#id.current, disabled: this.#isDisabled, - "aria-expanded": getAriaExpanded(this.#itemState.isSelected), + "aria-expanded": getAriaExpanded(this.#itemState.isActive), "aria-disabled": getAriaDisabled(this.#isDisabled), "data-disabled": getDataDisabled(this.#isDisabled), - "data-state": getDataOpenClosed(this.#itemState.isSelected), + "data-state": getDataOpenClosed(this.#itemState.isActive), "data-orientation": getDataOrientation(this.#root.orientation.current), [ACCORDION_TRIGGER_ATTR]: "", tabindex: 0, @@ -273,12 +274,12 @@ class AccordionContentState { #height = $state(0); #forceMount: AccordionContentStateProps["forceMount"]; - present = $derived.by(() => this.#forceMount.current || this.item.isSelected); + present = $derived.by(() => this.#forceMount.current || this.item.isActive); constructor(props: AccordionContentStateProps, item: AccordionItemState) { this.item = item; this.#forceMount = props.forceMount; - this.#isMountAnimationPrevented = this.item.isSelected; + this.#isMountAnimationPrevented = this.item.isActive; this.#id = props.id; this.#ref = props.ref; @@ -328,11 +329,15 @@ class AccordionContentState { }); } + snippetProps = $derived.by(() => ({ + open: this.item.isActive, + })); + props = $derived.by( () => ({ id: this.#id.current, - "data-state": getDataOpenClosed(this.item.isSelected), + "data-state": getDataOpenClosed(this.item.isActive), "data-disabled": getDataDisabled(this.item.isDisabled), "data-orientation": getDataOrientation(this.item.root.orientation.current), [ACCORDION_CONTENT_ATTR]: "", @@ -375,7 +380,7 @@ class AccordionHeaderState { role: "heading", "aria-level": this.#level.current, "data-heading-level": this.#level.current, - "data-state": getDataOpenClosed(this.#item.isSelected), + "data-state": getDataOpenClosed(this.#item.isActive), "data-orientation": getDataOrientation(this.#item.root.orientation.current), [ACCORDION_HEADER_ATTR]: "", }) as const diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte index 0de20cf43..c3eaddf8e 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-content.svelte @@ -3,7 +3,8 @@ import { useAccordionContent } from "../accordion.svelte.js"; import type { AccordionContentProps } from "../types.js"; import { PresenceLayer } from "$lib/bits/utilities/presence-layer/index.js"; - import { mergeProps, useId } from "$lib/internal/index.js"; + import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.js"; let { child, @@ -27,15 +28,16 @@ {#snippet presence({ present })} {@const mergedProps = mergeProps(restProps, contentState.props, { - hidden: !present.current, + hidden: forceMount ? undefined : !present.current, })} {#if child} {@render child({ props: mergedProps, + ...contentState.snippetProps, })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte index 149f0a4ef..1a87321c4 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-item.svelte @@ -8,7 +8,7 @@ let { id = useId(), disabled = false, - value, + value = useId(), children, child, ref = $bindable(null), diff --git a/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte b/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte index df5db9042..d9bb1ab1a 100644 --- a/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/accordion/components/accordion-trigger.svelte @@ -2,7 +2,8 @@ import { box } from "svelte-toolbelt"; import type { AccordionTriggerProps } from "../types.js"; import { useAccordionTrigger } from "../accordion.svelte.js"; - import { mergeProps, useId } from "$lib/internal/index.js"; + import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.js"; let { disabled = false, diff --git a/packages/bits-ui/src/lib/bits/accordion/types.ts b/packages/bits-ui/src/lib/bits/accordion/types.ts index 1457a3025..4fefe917a 100644 --- a/packages/bits-ui/src/lib/bits/accordion/types.ts +++ b/packages/bits-ui/src/lib/bits/accordion/types.ts @@ -1,4 +1,4 @@ -import type { EventCallback, OnChangeFn, WithChild, Without } from "$lib/internal/index.js"; +import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { PrimitiveButtonAttributes, PrimitiveDivAttributes } from "$lib/shared/attributes.js"; import type { Orientation } from "$lib/shared/index.js"; @@ -79,42 +79,58 @@ export type AccordionRootPropsWithoutHTML = export type AccordionRootProps = AccordionRootPropsWithoutHTML & Without; -export type AccordionTriggerPropsWithoutHTML = WithChild<{ - /** - * Whether the trigger for the accordion item is disabled or not. - * - * @defaultValue false - */ - disabled?: boolean | null | undefined; -}>; +export type AccordionTriggerPropsWithoutHTML = WithChild; export type AccordionTriggerProps = AccordionTriggerPropsWithoutHTML & - Omit; + Without; -export type AccordionItemContext = { - value: string; - disabled: boolean; -}; - -export type AccordionContentPropsWithoutHTML = WithChild<{ +export type AccordionContentSnippetProps = { /** - * Whether to forcefully mount the content, regardless of the open state. - * This is useful if you want to use Svelte transitions for the content. + * Whether the accordion content is active/open or not. */ - forceMount?: boolean; -}>; + open: boolean; +}; + +export type AccordionContentPropsWithoutHTML = WithChild< + { + /** + * Whether to forcefully mount the content, regardless of the open state. + * This is useful if you want to use Svelte transitions for the content. + * + * @defaultValue `true` + */ + forceMount?: boolean; + }, + AccordionContentSnippetProps +>; export type AccordionContentProps = AccordionContentPropsWithoutHTML & Without; export type AccordionItemPropsWithoutHTML = WithChild<{ - value: string; + /** + * The value of the accordion item. This is used to identify if the item is open or closed. + * If not provided, a unique ID will be generated for this value. + */ + value?: string; + + /** + * Whether the accordion item is disabled, which prevents users from interacting with it. + * + * @defaultValue `false` + */ disabled?: boolean; }>; export type AccordionItemProps = AccordionItemPropsWithoutHTML & PrimitiveDivAttributes; export type AccordionHeaderPropsWithoutHTML = WithChild<{ + /** + * The level of the accordion header, applied to the element's `aria-level` attribute. + * This is used to indicate the hierarchical relationship between the accordion items. + * + * @defaultValue `3` + */ level?: 1 | 2 | 3 | 4 | 5 | 6; }>; 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 7c36f9d69..d4cd694c3 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 @@ -21,6 +21,7 @@ onDestroyAutoFocus = noop, onEscapeKeydown = noop, onMountAutoFocus = noop, + preventScroll = true, ...restProps }: ContentProps = $props(); @@ -37,7 +38,7 @@ {#snippet presence({ present })} - + [] = $state([]); visibleMonths = $derived.by(() => this.months.map((month) => month.value)); announcer: Announcer; @@ -131,6 +134,7 @@ export class CalendarRootState { this.ref = props.ref; this.disableDaysOutsideMonth = props.disableDaysOutsideMonth; this.onDateSelect = props.onDateSelect; + this.initialFocus = props.initialFocus; this.announcer = getAnnouncer(); this.formatter = createFormatter(this.locale.current); @@ -148,6 +152,18 @@ export class CalendarRootState { numberOfMonths: this.numberOfMonths.current, }); + $effect(() => { + const initialFocus = untrack(() => this.initialFocus.current); + if (initialFocus) { + // focus the first `data-focused` day node + const firstFocusedDay = + this.ref.current?.querySelector(`[data-focused]`); + if (firstFocusedDay) { + firstFocusedDay.focus(); + } + } + }); + $effect(() => { if (!this.ref.current) return; const removeHeading = createAccessibleHeading({ @@ -530,6 +546,7 @@ export class CalendarHeadingState { id: this.id.current, "aria-hidden": getAriaHidden(true), "data-disabled": getDataDisabled(this.root.disabled.current), + "data-readonly": getDataReadonly(this.root.readonly.current), [this.root.getBitsAttr("heading")]: "", }) as const ); @@ -822,12 +839,15 @@ export class CalendarGridBodyState { }); } - props = $derived.by(() => ({ - id: this.id.current, - "data-disabled": getDataDisabled(this.root.disabled.current), - "data-readonly": getDataReadonly(this.root.readonly.current), - [this.root.getBitsAttr("grid-body")]: "", - })); + props = $derived.by( + () => + ({ + id: this.id.current, + "data-disabled": getDataDisabled(this.root.disabled.current), + "data-readonly": getDataReadonly(this.root.readonly.current), + [this.root.getBitsAttr("grid-body")]: "", + }) as const + ); } export type CalendarGridHeadStateProps = WithRefProps; 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 d11797d01..227486743 100644 --- a/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte +++ b/packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte @@ -33,6 +33,7 @@ preventDeselect = false, type, disableDaysOutsideMonth = true, + initialFocus = false, ...restProps }: RootProps = $props(); @@ -65,6 +66,7 @@ minValue: box.with(() => minValue), maxValue: box.with(() => maxValue), disableDaysOutsideMonth: box.with(() => disableDaysOutsideMonth), + initialFocus: box.with(() => initialFocus), placeholder: box.with( () => placeholder as DateValue, (v) => { diff --git a/packages/bits-ui/src/lib/bits/calendar/types.ts b/packages/bits-ui/src/lib/bits/calendar/types.ts index 3a20077c7..c0023da0c 100644 --- a/packages/bits-ui/src/lib/bits/calendar/types.ts +++ b/packages/bits-ui/src/lib/bits/calendar/types.ts @@ -174,6 +174,12 @@ type CalendarBaseRootPropsWithoutHTML = { */ readonly?: boolean; + /** + * If `true`, the calendar will focus the selected day, today, or the first day of the month + * in that order depending on what is visible when the calendar is mounted. + */ + initialFocus?: boolean; + /** * Whether to disable the selection of days outside the current month. By default, * days outside the current month are rendered to fill the calendar grid, but they 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 eb574f633..61e62b701 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts @@ -1,13 +1,8 @@ -import { - type ReadableBoxedValues, - type WithRefProps, - type WritableBoxedValues, - getAriaChecked, - getAriaRequired, - getDataDisabled, - kbd, - useRefById, -} from "$lib/internal/index.js"; +import { useRefById } from "$lib/internal/useRefById.svelte.js"; +import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; +import type { WithRefProps } from "$lib/internal/types.js"; +import { getAriaChecked, getAriaRequired, getDataDisabled } from "$lib/internal/attrs.js"; +import { kbd } from "$lib/internal/kbd.js"; import { createContext } from "$lib/internal/createContext.js"; const CHECKBOX_ROOT_ATTR = "data-checkbox-root"; @@ -69,13 +64,13 @@ class CheckboxRootState { () => ({ id: this.#id.current, - "data-disabled": getDataDisabled(this.disabled.current), - "data-state": getCheckboxDataState(this.checked.current), role: "checkbox", type: "button", + disabled: this.disabled.current, "aria-checked": getAriaChecked(this.checked.current), "aria-required": getAriaRequired(this.required.current), - disabled: this.disabled.current, + "data-disabled": getDataDisabled(this.disabled.current), + "data-state": getCheckboxDataState(this.checked.current), [CHECKBOX_ROOT_ATTR]: "", // onclick: this.#onclick, diff --git a/packages/bits-ui/src/lib/bits/checkbox/types.ts b/packages/bits-ui/src/lib/bits/checkbox/types.ts index e4518b4d2..878209ed7 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/types.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/types.ts @@ -1,4 +1,4 @@ -import type { OnChangeFn, WithChild, Without } from "$lib/internal/index.js"; +import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { PrimitiveButtonAttributes } from "$lib/shared/attributes.js"; export type CheckboxRootSnippetProps = { checked: boolean | "indeterminate" }; 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 70bd545b3..23ddbc068 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts @@ -1,17 +1,12 @@ -import { - type ReadableBoxedValues, - type WritableBoxedValues, - afterTick, - getAriaExpanded, - getDataDisabled, - getDataOpenClosed, - useRefById, -} from "$lib/internal/index.js"; +import { useRefById } from "$lib/internal/useRefById.svelte.js"; +import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; +import { afterTick } from "$lib/internal/afterTick.js"; +import { getAriaExpanded, getDataDisabled, getDataOpenClosed } from "$lib/internal/attrs.js"; import { createContext } from "$lib/internal/createContext.js"; -const ROOT_ATTR = "data-collapsible-root"; -const CONTENT_ATTR = "data-collapsible-content"; -const TRIGGER_ATTR = "data-collapsible-trigger"; +const COLLAPSIBLE_ROOT_ATTR = "data-collapsible-root"; +const COLLAPSIBLE_CONTENT_ATTR = "data-collapsible-content"; +const COLLAPSIBLE_TRIGGER_ATTR = "data-collapsible-trigger"; type CollapsibleRootStateProps = WritableBoxedValues<{ open: boolean; @@ -59,7 +54,7 @@ class CollapsibleRootState { id: this.#id.current, "data-state": getDataOpenClosed(this.open.current), "data-disabled": getDataDisabled(this.disabled.current), - [ROOT_ATTR]: "", + [COLLAPSIBLE_ROOT_ATTR]: "", }) as const ); } @@ -141,12 +136,14 @@ class CollapsibleContentState { }); } + snippetProps = $derived.by(() => ({ + open: this.root.open.current, + })); + props = $derived.by( () => ({ id: this.#id.current, - "data-state": getDataOpenClosed(this.root.open.current), - "data-disabled": getDataDisabled(this.root.disabled.current), style: { "--bits-collapsible-content-height": this.#height ? `${this.#height}px` @@ -155,13 +152,16 @@ class CollapsibleContentState { ? `${this.#width}px` : undefined, }, - [CONTENT_ATTR]: "", + "data-state": getDataOpenClosed(this.root.open.current), + "data-disabled": getDataDisabled(this.root.disabled.current), + [COLLAPSIBLE_CONTENT_ATTR]: "", }) as const ); } type CollapsibleTriggerStateProps = ReadableBoxedValues<{ id: string; + disabled: boolean | null | undefined; }> & WritableBoxedValues<{ ref: HTMLElement | null; @@ -171,11 +171,14 @@ class CollapsibleTriggerState { #root: CollapsibleRootState; #ref: CollapsibleTriggerStateProps["ref"]; #id: CollapsibleTriggerStateProps["id"]; + #disabled: CollapsibleTriggerStateProps["disabled"]; + #isDisabled = $derived.by(() => this.#disabled.current || this.#root.disabled.current); constructor(props: CollapsibleTriggerStateProps, root: CollapsibleRootState) { this.#root = root; this.#id = props.id; this.#ref = props.ref; + this.#disabled = props.disabled; useRefById({ id: this.#id, @@ -192,12 +195,12 @@ class CollapsibleTriggerState { ({ id: this.#id.current, type: "button", + disabled: this.#isDisabled, "aria-controls": this.#root.contentNode?.id, "aria-expanded": getAriaExpanded(this.#root.open.current), "data-state": getDataOpenClosed(this.#root.open.current), - "data-disabled": getDataDisabled(this.#root.disabled.current), - disabled: this.#root.disabled.current, - [TRIGGER_ATTR]: "", + "data-disabled": getDataDisabled(this.#isDisabled), + [COLLAPSIBLE_TRIGGER_ATTR]: "", // onclick: this.#onclick, }) as const diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte index b20e364cf..6d62f1301 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-content.svelte @@ -3,7 +3,8 @@ import { useCollapsibleContent } from "../collapsible.svelte.js"; import type { CollapsibleContentProps } from "../types.js"; import { PresenceLayer } from "$lib/bits/utilities/presence-layer/index.js"; - import { mergeProps, useId } from "$lib/internal/index.js"; + import { mergeProps } from "$lib/internal/mergeProps.js"; + import { useId } from "$lib/internal/useId.js"; let { child, @@ -27,15 +28,16 @@ {#snippet presence({ present })} {@const mergedProps = mergeProps(restProps, contentState.props, { - hidden: !present.current, + hidden: forceMount ? undefined : !present.current, })} {#if child} {@render child({ + ...contentState.snippetProps, props: mergedProps, })} {:else}
- {@render children?.()} + {@render children?.(contentState.snippetProps)}
{/if} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte index a0fd7dc16..872dcade9 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/collapsible/components/collapsible-trigger.svelte @@ -10,6 +10,7 @@ child, ref = $bindable(null), id = useId(), + disabled = false, ...restProps }: TriggerProps = $props(); @@ -19,7 +20,9 @@ () => ref, (v) => (ref = v) ), + disabled: box.with(() => disabled), }); + const mergedProps = $derived(mergeProps(restProps, triggerState.props)); diff --git a/packages/bits-ui/src/lib/bits/collapsible/types.ts b/packages/bits-ui/src/lib/bits/collapsible/types.ts index 4d803e942..7b08788ff 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/types.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/types.ts @@ -1,4 +1,4 @@ -import type { OnChangeFn, WithChild, Without } from "$lib/internal/index.js"; +import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { PrimitiveButtonAttributes, PrimitiveDivAttributes } from "$lib/shared/attributes.js"; export type CollapsibleRootPropsWithoutHTML = WithChild<{ @@ -25,14 +25,21 @@ export type CollapsibleRootPropsWithoutHTML = WithChild<{ export type CollapsibleRootProps = CollapsibleRootPropsWithoutHTML & Without; -export type CollapsibleContentPropsWithoutHTML = WithChild<{ - /** - * Whether to force mount the content to the DOM. - * - * @defaultValue false - */ - forceMount?: boolean; -}>; +export type CollapsibleContentSnippetProps = { + open: boolean; +}; + +export type CollapsibleContentPropsWithoutHTML = WithChild< + { + /** + * Whether to force mount the content to the DOM. + * + * @defaultValue false + */ + forceMount?: boolean; + }, + CollapsibleContentSnippetProps +>; export type CollapsibleContentProps = CollapsibleContentPropsWithoutHTML & Without; diff --git a/packages/bits-ui/src/lib/bits/combobox/combobox.svelte.ts b/packages/bits-ui/src/lib/bits/combobox/combobox.svelte.ts index cded876c2..e3308646e 100644 --- a/packages/bits-ui/src/lib/bits/combobox/combobox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/combobox/combobox.svelte.ts @@ -1,3 +1,4 @@ +import { Previous } from "runed"; import { afterTick } from "$lib/internal/afterTick.js"; import { backward, forward, next, prev } from "$lib/internal/arrays.js"; import { @@ -13,7 +14,6 @@ import { createContext } from "$lib/internal/createContext.js"; import { kbd } from "$lib/internal/kbd.js"; import type { WithRefProps } from "$lib/internal/types.js"; import { useRefById } from "$lib/internal/useRefById.svelte.js"; -import { Previous } from "runed"; // prettier-ignore export const INTERACTION_KEYS = [kbd.ARROW_LEFT, kbd.ESCAPE, kbd.ARROW_RIGHT, kbd.SHIFT, kbd.CAPS_LOCK, kbd.CONTROL, kbd.ALT, kbd.META, kbd.ENTER, kbd.F1, kbd.F2, kbd.F3, kbd.F4, kbd.F5, kbd.F6, kbd.F7, kbd.F8, kbd.F9, kbd.F10, kbd.F11, kbd.F12]; @@ -372,7 +372,7 @@ class ComboboxInputState { if (e.key === kbd.ARROW_DOWN) { nextItem = next(candidateNodes, currIndex, loop); - } else if (e.key == kbd.ARROW_UP) { + } else if (e.key === kbd.ARROW_UP) { nextItem = prev(candidateNodes, currIndex, loop); } else if (e.key === kbd.PAGE_DOWN) { nextItem = forward(candidateNodes, currIndex, 10, loop); @@ -405,12 +405,12 @@ class ComboboxInputState { ({ id: this.#id.current, role: "combobox", + disabled: this.root.disabled.current ? true : undefined, "aria-activedescendant": this.root.highlightedId, "aria-autocomplete": "list", "aria-expanded": getAriaExpanded(this.root.open.current), "data-state": getDataOpenClosed(this.root.open.current), "data-disabled": getDataDisabled(this.root.disabled.current), - disabled: this.root.disabled.current ? true : undefined, onkeydown: this.#onkeydown, oninput: this.#oninput, [COMBOBOX_INPUT_ATTR]: "", @@ -462,16 +462,19 @@ class ComboboxTriggerState { this.root.toggleMenu(); }; - props = $derived.by(() => ({ - id: this.#id.current, - "aria-haspopup": "listbox", - "data-state": getDataOpenClosed(this.root.open.current), - "data-disabled": getDataDisabled(this.root.disabled.current), - disabled: this.root.disabled.current ? true : undefined, - onpointerdown: this.#onpointerdown, - onkeydown: this.#onkeydown, - [COMBOBOX_TRIGGER_ATTR]: "", - })); + props = $derived.by( + () => + ({ + id: this.#id.current, + disabled: this.root.disabled.current ? true : undefined, + "aria-haspopup": "listbox", + "data-state": getDataOpenClosed(this.root.open.current), + "data-disabled": getDataDisabled(this.root.disabled.current), + [COMBOBOX_TRIGGER_ATTR]: "", + onpointerdown: this.#onpointerdown, + onkeydown: this.#onkeydown, + }) as const + ); } type ComboboxContentStateProps = WithRefProps; @@ -549,7 +552,6 @@ class ComboboxItemState { $effect(() => { if (this.isHighlighted) { this.onHighlight.current(); - return; } else if (this.prevHighlighted.current) { this.onUnhighlight.current(); } @@ -602,14 +604,24 @@ class ComboboxItemState { () => ({ id: this.#id.current, + "aria-selected": this.root.includesItem(this.value.current) ? "true" : undefined, "data-value": this.value.current, "data-disabled": getDataDisabled(this.disabled.current), "data-highlighted": this.root.highlightedValue === this.value.current ? "" : undefined, "data-selected": this.root.includesItem(this.value.current) ? "" : undefined, - "aria-selected": this.root.includesItem(this.value.current) ? "true" : undefined, "data-label": this.label.current, [COMBOBOX_ITEM_ATTR]: "", + style: { + "--bits-combobox-content-transform-origin": + "var(--bits-floating-transform-origin)", + "--bits-combobox-content-available-width": + "var(--bits-floating-available-width)", + "--bits-combobox-content-available-height": + "var(--bits-floating-available-height)", + "--bits-combobox-trigger-width": "var(--bits-floating-anchor-width)", + "--bits-combobox-trigger-height": "var(--bits-floating-anchor-height)", + }, onpointermove: this.#onpointermove, onpointerdown: this.#onpointerdown, onpointerleave: this.#onpointerleave, diff --git a/packages/bits-ui/src/lib/bits/combobox/types.ts b/packages/bits-ui/src/lib/bits/combobox/types.ts index 87ce8f824..ef953a30f 100644 --- a/packages/bits-ui/src/lib/bits/combobox/types.ts +++ b/packages/bits-ui/src/lib/bits/combobox/types.ts @@ -1,12 +1,12 @@ -import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js"; +import type { PortalProps } from "../utilities/portal/types.js"; +import type { PopperLayerProps } from "../utilities/popper-layer/types.js"; +import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js"; import type { PrimitiveButtonAttributes, PrimitiveDivAttributes, PrimitiveInputAttributes, } from "$lib/shared/attributes.js"; -import type { PortalProps } from "../utilities/portal/types.js"; -import type { PopperLayerProps } from "../utilities/popper-layer/types.js"; -import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js"; +import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js"; export type ComboboxBaseRootPropsWithoutHTML = WithChildren<{ /** diff --git a/packages/bits-ui/src/lib/bits/context-menu/index.ts b/packages/bits-ui/src/lib/bits/context-menu/index.ts index a3f352acf..c7d34e44f 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/index.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/index.ts @@ -2,7 +2,7 @@ export { default as Root } from "$lib/bits/menu/components/menu.svelte"; export { default as Sub } from "$lib/bits/menu/components/menu-sub.svelte"; export { default as Item } from "$lib/bits/menu/components/menu-item.svelte"; export { default as Group } from "$lib/bits/menu/components/menu-group.svelte"; -export { default as Label } from "$lib/bits/menu/components/menu-group-label.svelte"; +export { default as GroupLabel } from "$lib/bits/menu/components/menu-group-label.svelte"; export { default as Arrow } from "$lib/bits/menu/components/menu-arrow.svelte"; export { default as Content } from "./components/context-menu-content.svelte"; export { default as Trigger } from "./components/context-menu-trigger.svelte"; 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 d60d53107..f12ec89b0 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 @@ -57,6 +57,7 @@ import type { DateMatcher, Granularity, HourCycle } from "$lib/shared/date/types import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js"; export const DATE_FIELD_INPUT_ATTR = "data-date-field-input"; +const DATE_FIELD_LABEL_ATTR = "data-date-field-label"; export type DateFieldRootStateProps = WritableBoxedValues<{ value: DateValue | undefined; @@ -77,7 +78,7 @@ export type DateFieldRootStateProps = WritableBoxedValues<{ required: boolean; }>; -class DateFieldRootState { +export class DateFieldRootState { value: DateFieldRootStateProps["value"]; placeholder: WritableBox; isDateUnavailable: DateFieldRootStateProps["isDateUnavailable"]; @@ -518,6 +519,7 @@ class DateFieldRootState { "aria-readonly": getAriaReadonly(this.readonly.current || inReadonlySegments), "data-invalid": getDataInvalid(this.isInvalid), "data-disabled": getDataDisabled(this.disabled.current), + "data-readonly": getDataReadonly(this.readonly.current || inReadonlySegments), "data-segment": `${part}`, }; @@ -665,6 +667,7 @@ class DateFieldLabelState { id: this.#id.current, "data-invalid": getDataInvalid(this.#root.isInvalid), "data-disabled": getDataDisabled(this.#root.disabled.current), + [DATE_FIELD_LABEL_ATTR]: "", onclick: this.#onclick, }) as const ); @@ -2328,13 +2331,13 @@ class DateFieldTimeZoneSegmentState { role: "textbox", id: this.#id.current, "aria-label": "timezone, ", - "data-readonly": getDataReadonly(true), - tabindex: 0, style: { caretColor: "transparent", }, onkeydown: this.#onkeydown, + tabindex: 0, ...this.#root.getBaseSegmentAttrs("timeZoneName", this.#id.current), + "data-readonly": getDataReadonly(true), }) as const ); } diff --git a/packages/bits-ui/src/lib/bits/date-field/types.ts b/packages/bits-ui/src/lib/bits/date-field/types.ts index 6e164319e..a2ade1fba 100644 --- a/packages/bits-ui/src/lib/bits/date-field/types.ts +++ b/packages/bits-ui/src/lib/bits/date-field/types.ts @@ -1,6 +1,6 @@ import type { DateValue } from "@internationalized/date"; -import type { SegmentPart } from "$lib/shared/index.js"; import type { Snippet } from "svelte"; +import type { SegmentPart } from "$lib/shared/index.js"; import type { OnChangeFn, WithChild, Without } from "$lib/internal/types.js"; import type { PrimitiveDivAttributes, PrimitiveSpanAttributes } from "$lib/shared/attributes.js"; import type { EditableSegmentPart } from "$lib/shared/date/field/types.js"; diff --git a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker-calendar.svelte b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker-calendar.svelte index 354293c43..5a9b7875a 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker-calendar.svelte +++ b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker-calendar.svelte @@ -41,6 +41,7 @@ placeholder: datePickerRootState.props.placeholder, value: datePickerRootState.props.value, onDateSelect: datePickerRootState.props.onDateSelect, + initialFocus: datePickerRootState.props.initialFocus, }); const mergedProps = $derived(mergeProps(restProps, calendarState.props)); 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 00625764b..6ae71797d 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte +++ b/packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte @@ -39,6 +39,7 @@ fixedWeeks = false, numberOfMonths = 1, closeOnDateSelect = true, + initialFocus = false, children, }: RootProps = $props(); @@ -105,6 +106,7 @@ isDateDisabled: box.with(() => isDateDisabled), fixedWeeks: box.with(() => fixedWeeks), numberOfMonths: box.with(() => numberOfMonths), + initialFocus: box.with(() => initialFocus), onDateSelect: box.with(() => onDateSelect), }); 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 a9b6cb3d2..453f706d3 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 @@ -1,8 +1,8 @@ +import type { DateValue } from "@internationalized/date"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { createContext } from "$lib/internal/createContext.js"; import type { DateMatcher, Granularity, HourCycle, WeekStartsOn } from "$lib/shared/date/types.js"; import type { SegmentPart } from "$lib/shared/index.js"; -import type { DateValue } from "@internationalized/date"; type DatePickerRootStateProps = WritableBoxedValues<{ value: DateValue | undefined; @@ -31,6 +31,7 @@ type DatePickerRootStateProps = WritableBoxedValues<{ numberOfMonths: number; calendarLabel: string; disableDaysOutsideMonth: boolean; + initialFocus: boolean; onDateSelect?: () => void; }>; 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 cbd18129f..ee153d5d7 100644 --- a/packages/bits-ui/src/lib/bits/date-picker/types.ts +++ b/packages/bits-ui/src/lib/bits/date-picker/types.ts @@ -241,6 +241,11 @@ export type DatePickerRootPropsWithoutHTML = WithChildren<{ * @defaultValue true */ closeOnDateSelect?: boolean; + + /** + * Whether to focus a date when the picker is first opened. + */ + initialFocus?: boolean; }>; export type DatePickerRootProps = DatePickerRootPropsWithoutHTML; diff --git a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field-input.svelte b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field-input.svelte index dd7b02646..04745c96a 100644 --- a/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field-input.svelte +++ b/packages/bits-ui/src/lib/bits/date-range-field/components/date-range-field-input.svelte @@ -21,10 +21,13 @@ const rootState = getDateRangeFieldRootContext(); - const fieldState = useDateRangeFieldInput({ - name: box.with(() => name), - value: type === "start" ? rootState.startValue : rootState.endValue, - }); + const fieldState = useDateRangeFieldInput( + { + name: box.with(() => name), + value: type === "start" ? rootState.startValue : rootState.endValue, + }, + type + ); const inputState = fieldState.createInput({ id: box.with(() => id), 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 f90173106..9fc29d9e9 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 @@ -1,21 +1,23 @@ +import type { DateValue } from "@internationalized/date"; +import { untrack } from "svelte"; +import type { ReadableBox, WritableBox } from "svelte-toolbelt"; +import type { DateFieldRootState } from "../date-field/date-field.svelte.js"; +import { useDateFieldRoot } from "../date-field/date-field.svelte.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { useId } from "$lib/internal/useId.js"; import { removeDescriptionElement } from "$lib/shared/date/field/helpers.js"; -import { createFormatter, type Formatter } from "$lib/shared/date/formatter.js"; -import type { Granularity, DateMatcher } from "$lib/shared/date/types.js"; +import { type Formatter, createFormatter } from "$lib/shared/date/formatter.js"; +import type { DateMatcher, Granularity } from "$lib/shared/date/types.js"; import type { DateRange, SegmentPart } from "$lib/shared/index.js"; -import type { DateValue } from "@internationalized/date"; -import { untrack } from "svelte"; -import { useDateFieldRoot } from "../date-field/date-field.svelte.js"; import type { WithRefProps } from "$lib/internal/types.js"; import { useRefById } from "$lib/internal/useRefById.svelte.js"; import { createContext } from "$lib/internal/createContext.js"; import { getFirstSegment } from "$lib/shared/date/field.js"; import { getDataDisabled } from "$lib/internal/attrs.js"; -import type { ReadableBox, WritableBox } from "svelte-toolbelt"; import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js"; export const DATE_RANGE_FIELD_ROOT_ATTR = "data-date-range-field-root"; +const DATE_RANGE_FIELD_LABEL_ATTR = "data-date-range-field-label"; type DateRangeFieldRootStateProps = WithRefProps< WritableBoxedValues<{ @@ -57,6 +59,8 @@ export class DateRangeFieldRootState { required: DateRangeFieldRootStateProps["required"]; startValue: DateRangeFieldRootStateProps["startValue"]; endValue: DateRangeFieldRootStateProps["endValue"]; + startFieldState: DateFieldRootState | undefined = undefined; + endFieldState: DateFieldRootState | undefined = undefined; descriptionId = useId(); formatter: Formatter; fieldNode = $state(null); @@ -181,8 +185,8 @@ export class DateRangeFieldRootState { */ childFieldPropOverrides = {}; - createField(props: DateRangeFieldInputStateProps) { - return useDateFieldRoot( + createField(props: DateRangeFieldInputStateProps, type: "start" | "end") { + const fieldState = useDateFieldRoot( { value: props.value, name: props.name, @@ -201,6 +205,13 @@ export class DateRangeFieldRootState { }, this ); + + if (type === "start") { + this.startFieldState = fieldState; + } else { + this.endFieldState = fieldState; + } + return fieldState; } createLabel(props: DateRangeFieldLabelStateProps) { @@ -246,8 +257,10 @@ class DateRangeFieldLabelState { () => ({ id: this.#id.current, + // TODO: invalid state for field // "data-invalid": getDataInvalid(this.#root.isInvalid), "data-disabled": getDataDisabled(this.#root.disabled.current), + [DATE_RANGE_FIELD_LABEL_ATTR]: "", onclick: this.#onclick, }) as const ); @@ -269,8 +282,11 @@ export function useDateRangeFieldLabel(props: DateRangeFieldLabelStateProps) { return getDateRangeFieldRootContext().createLabel(props); } -export function useDateRangeFieldInput(props: DateRangeFieldInputStateProps) { - return getDateRangeFieldRootContext().createField(props); +export function useDateRangeFieldInput( + props: DateRangeFieldInputStateProps, + type: "start" | "end" +) { + return getDateRangeFieldRootContext().createField(props, type); } export { getDateRangeFieldRootContext }; 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 49437cdc2..0818d198d 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 @@ -1,8 +1,8 @@ +import type { DateValue } from "@internationalized/date"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { createContext } from "$lib/internal/createContext.js"; import type { DateMatcher, Granularity, HourCycle, WeekStartsOn } from "$lib/shared/date/types.js"; import type { DateRange, SegmentPart } from "$lib/shared/index.js"; -import type { DateValue } from "@internationalized/date"; type DateRangePickerRootStateProps = WritableBoxedValues<{ value: DateRange; diff --git a/packages/bits-ui/src/lib/bits/dialog/types.ts b/packages/bits-ui/src/lib/bits/dialog/types.ts index 81c844b8f..4d16062de 100644 --- a/packages/bits-ui/src/lib/bits/dialog/types.ts +++ b/packages/bits-ui/src/lib/bits/dialog/types.ts @@ -4,7 +4,7 @@ import type { PresenceLayerProps } from "../utilities/presence-layer/types.js"; import type { FocusScopeProps } from "../utilities/focus-scope/types.js"; import type { TextSelectionLayerProps } from "../utilities/text-selection-layer/types.js"; import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js"; -import type { PrimitiveDivAttributes, PrimitiveButtonAttributes } from "$lib/shared/attributes.js"; +import type { PrimitiveButtonAttributes, PrimitiveDivAttributes } from "$lib/shared/attributes.js"; import type { PortalProps } from "$lib/bits/utilities/portal/index.js"; export type DialogRootPropsWithoutHTML = WithChildren<{ 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 19864bda7..1eec77988 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 @@ -1,3 +1,5 @@ +import { untrack } from "svelte"; +import { box } from "svelte-toolbelt"; import { getAriaExpanded, getDataOpenClosed } from "$lib/internal/attrs.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { addEventListener } from "$lib/internal/events.js"; @@ -5,11 +7,9 @@ import { isElement, isFocusVisible, isTouch } from "$lib/internal/is.js"; import { sleep } from "$lib/internal/sleep.js"; import type { WithRefProps } from "$lib/internal/types.js"; import { useRefById } from "$lib/internal/useRefById.svelte.js"; -import { untrack } from "svelte"; -import { getTabbableCandidates } from "../utilities/focus-scope/utils.js"; +import { getTabbableCandidates } from "$lib/internal/focus.js"; import { createContext } from "$lib/internal/createContext.js"; import { useGraceArea } from "$lib/internal/useGraceArea.svelte.js"; -import { box } from "svelte-toolbelt"; import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js"; const CONTENT_ATTR = "data-link-preview-content"; diff --git a/packages/bits-ui/src/lib/bits/link-preview/types.ts b/packages/bits-ui/src/lib/bits/link-preview/types.ts index 337cb3bbc..ab76ced1d 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/types.ts +++ b/packages/bits-ui/src/lib/bits/link-preview/types.ts @@ -1,10 +1,10 @@ -import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js"; -import type { PrimitiveAnchorAttributes, PrimitiveDivAttributes } from "$lib/shared/attributes.js"; import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js"; import type { DismissableLayerProps } from "../utilities/dismissable-layer/types.js"; import type { EscapeLayerProps } from "../utilities/escape-layer/types.js"; import type { FloatingLayerContentProps } from "../utilities/floating-layer/types.js"; import type { PortalProps } from "../utilities/portal/types.js"; +import type { PrimitiveAnchorAttributes, PrimitiveDivAttributes } from "$lib/shared/attributes.js"; +import type { OnChangeFn, WithChild, WithChildren, Without } from "$lib/internal/types.js"; export type LinkPreviewRootPropsWithoutHTML = WithChildren<{ /** diff --git a/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts b/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts index 1e5acdd66..eee1520ac 100644 --- a/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/listbox/listbox.svelte.ts @@ -1,7 +1,7 @@ import { onMount } from "svelte"; import { SvelteSet } from "svelte/reactivity"; import { IsFocusWithin } from "runed"; -import { focusFirst } from "../utilities/focus-scope/utils.js"; +import { focusFirst } from "$lib/internal/focus.js"; import { getAriaDisabled, getAriaSelected, diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-group-label.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-group-label.svelte index 023464753..538b37a0a 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-group-label.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-group-label.svelte @@ -1,7 +1,7 @@ diff --git a/packages/bits-ui/src/tests/date-range-field/DateRangeFieldTest.svelte b/packages/bits-ui/src/tests/date-range-field/DateRangeFieldTest.svelte index 4e01cdab0..bb5bca3d5 100644 --- a/packages/bits-ui/src/tests/date-range-field/DateRangeFieldTest.svelte +++ b/packages/bits-ui/src/tests/date-range-field/DateRangeFieldTest.svelte @@ -1,4 +1,10 @@ diff --git a/packages/bits-ui/src/tests/dialog/Dialog.spec.ts b/packages/bits-ui/src/tests/dialog/Dialog.spec.ts index 9f11c01c0..a91f31948 100644 --- a/packages/bits-ui/src/tests/dialog/Dialog.spec.ts +++ b/packages/bits-ui/src/tests/dialog/Dialog.spec.ts @@ -10,7 +10,7 @@ import { axe } from "jest-axe"; import { describe, it } from "vitest"; import { getTestKbd } from "../utils.js"; import DialogTest, { type DialogTestProps } from "./DialogTest.svelte"; -import { sleep } from "$lib/internal/index.js"; +import { sleep } from "$lib/internal/sleep.js"; const kbd = getTestKbd(); diff --git a/packages/bits-ui/src/tests/pagination/Pagination.spec.ts b/packages/bits-ui/src/tests/pagination/Pagination.spec.ts index 500a74d7c..42ca2086e 100644 --- a/packages/bits-ui/src/tests/pagination/Pagination.spec.ts +++ b/packages/bits-ui/src/tests/pagination/Pagination.spec.ts @@ -1,8 +1,8 @@ import { render } from "@testing-library/svelte"; import { axe } from "jest-axe"; +import { setupUserEvents } from "../utils.js"; import PaginationTest, { type PaginationTestProps } from "./PaginationTest.svelte"; import { isHTMLElement } from "$lib/internal/is.js"; -import { setupUserEvents } from "../utils.js"; function setup(props: PaginationTestProps = { count: 100 }) { const user = setupUserEvents(); diff --git a/packages/bits-ui/src/tests/pin-input/PinInput.spec.ts b/packages/bits-ui/src/tests/pin-input/PinInput.spec.ts index 78f9bd10c..b356b0cc0 100644 --- a/packages/bits-ui/src/tests/pin-input/PinInput.spec.ts +++ b/packages/bits-ui/src/tests/pin-input/PinInput.spec.ts @@ -9,6 +9,7 @@ const kbd = getTestKbd(); function setup(props: Partial = {}) { const user = setupUserEvents(); + // eslint-disable-next-line ts/no-explicit-any const returned = render(PinInputTest, { ...props } as any); const cell0 = returned.getByTestId("cell-0"); const cell1 = returned.getByTestId("cell-1"); @@ -28,7 +29,7 @@ function setup(props: Partial = {}) { }; } -describe("Pin Input", () => { +describe("pin Input", () => { it("should have no accessibility violations", async () => { const { container } = render(PinInputTest); expect(await axe(container)).toHaveNoViolations(); @@ -51,7 +52,7 @@ describe("Pin Input", () => { }); it("should respect binding to the `value` prop", async () => { - let initialValue = "123456"; + const initialValue = "123456"; const { hiddenInput, binding, user } = setup({ value: initialValue, }); diff --git a/packages/bits-ui/src/tests/select/Select.spec.ts b/packages/bits-ui/src/tests/select/Select.spec.ts index 8d6686e98..d21ff2812 100644 --- a/packages/bits-ui/src/tests/select/Select.spec.ts +++ b/packages/bits-ui/src/tests/select/Select.spec.ts @@ -4,7 +4,7 @@ import { describe, it, vi } from "vitest"; import { getTestKbd, setupUserEvents } from "../utils.js"; import SelectTest from "./SelectTest.svelte"; import type { Item, SelectTestProps } from "./SelectTest.svelte"; -import { sleep } from "$lib/internal/index.js"; +import { sleep } from "$lib/internal/sleep.js"; const kbd = getTestKbd(); diff --git a/packages/bits-ui/src/tests/utils.ts b/packages/bits-ui/src/tests/utils.ts index aa6d6393e..b47aa79c7 100644 --- a/packages/bits-ui/src/tests/utils.ts +++ b/packages/bits-ui/src/tests/utils.ts @@ -1,6 +1,7 @@ import { type Matcher, type MatcherOptions, fireEvent } from "@testing-library/svelte"; import { userEvent } from "@testing-library/user-event"; -import { getKbd, sleep } from "$lib/internal/index.js"; +import { getKbd } from "$lib/internal/kbd.js"; +import { sleep } from "$lib/internal/sleep.js"; /** * A wrapper around the internal kbd object to make it easier to use in tests diff --git a/sites/docs/content/components/accordion.md b/sites/docs/content/components/accordion.md index 09c78c355..9223f2ffc 100644 --- a/sites/docs/content/components/accordion.md +++ b/sites/docs/content/components/accordion.md @@ -4,7 +4,7 @@ description: Organizes content into collapsible sections, allowing users to focu --- @@ -33,6 +33,61 @@ description: Organizes content into collapsible sections, allowing users to focu ``` +## Reusable Components + +If you're planning to use the `Accordion` component throughout your application, it's recommended to create reusable wrapper components to reduce the amount of code you need to write each time. + +```svelte title="CustomAccordion.svelte" + + + +``` + +For each invidual item, you need an `Accordion.Item`, `Accordion.Header`, `Accordion.Trigger` and `Accordion.Content` component. We can combine these into a single `CustomAccordionItem` component that makes it easier to reuse. + +```svelte title="CustomAccordionItem.svelte" + + + + + {item.title} + + + {@render children?.()} + + +``` + +We used the [`WithoutChild`](/docs/type-helpers/without-child) type helper to omit the `child` snippet prop from `Accordion.ItemProps`, since we are opting out of using [Delegation](/docs/delegation) with our custom component. + +```svelte title="+page.svelte" + + + + Content 1 + Content 2 + Content 3 + +``` + ## Usage ### Single @@ -71,7 +126,7 @@ To disable an individual accordion item, set the `disabled` prop to `true`. This You can programmatically control the active of the accordion item(s) using the `value` prop. -```svelte +```svelte {2,5,7} @@ -87,7 +142,7 @@ You can programmatically control the active of the accordion item(s) using the ` You can use the `onValueChange` prop to handle side effects when the value of the accordion changes. -```svelte +```svelte {2-4} { doSomething(value); @@ -99,7 +154,7 @@ You can use the `onValueChange` prop to handle side effects when the value of th Alternatively, you can use `bind:value` with an `$effect` block to handle side effects when the value of the accordion changes. -```svelte +```svelte {4,6-8,11} - - - {#each items as item} - - - {item.title} - - {item.content} - - {/each} - +```svelte + + {#snippet child({ props, open })} + {#if open} +
+ This is the accordion content that will transition in and out. +
+ {/if} + {/snippet} +
``` -Since we're populating the `children` of the `Accordion.Root` within the component, we've excluded the `children` snippet prop from the component props using the `WithoutChildren` type helper. - -### Individual Item + -For each invidual item, you need an `Accordion.Item`, `Accordion.Header`, `Accordion.Trigger` and `Accordion.Content` component. You can make a reusable wrapper to reduce the amount of code you need to write each time. - -```svelte title="CustomItem.svelte" - - - - - - {title} - - - - {content} - - -``` - -```svelte title="+page.svelte" - +{#snippet preview()} + +{/snippet} - - - - - -``` + diff --git a/sites/docs/content/components/collapsible.md b/sites/docs/content/components/collapsible.md index 4bf9bafa1..3812ce8d1 100644 --- a/sites/docs/content/components/collapsible.md +++ b/sites/docs/content/components/collapsible.md @@ -4,7 +4,7 @@ description: Conceals or reveals content sections, enhancing space utilization a --- @@ -46,4 +46,24 @@ Sometimes, you want to either control or be aware of the `open` state of the col ``` +## Svelte Transitions + +You can use the `forceMount` prop on the `Collapsible.Content` component to forcefully mount the content regardless of whether the collapsible is opened or not. This is useful when you want more control over the transitions when the collapsible opens and closes using something like [Svelte Transitions](https://svelte.dev/docs#transition). + +The `open` snippet prop can be used for conditional rendering of the content based on whether the collapsible is open. + +```svelte + + {#snippet child({ props, open })} + {#if open} +
+ This is the collapsible content that will transition in and out. +
+ {/if} + {/snippet} +
+``` + +With the amount of boilerplate needed to handle the transitions, it's recommended to componentize your custom implementation of the collapsible content and use that throughout your application. + diff --git a/sites/docs/content/components/combobox.md b/sites/docs/content/components/combobox.md index 853772932..abfa09868 100644 --- a/sites/docs/content/components/combobox.md +++ b/sites/docs/content/components/combobox.md @@ -25,17 +25,98 @@ description: Enables users to pick from a list of options displayed in a dropdow - - - - - - - - - - + + + + + + + + + + ``` +## Reusable Components + +It's recommended to use the `Combobox` primitives to build your own custom combobox component that can be reused throughout your application. + +```svelte title="CustomCombobox.svelte" + + + + + Open + + + {#each filteredItems as item, i (i + item.value)} + + {#snippet children({ selected })} + {item.label} + {selected ? "✅" : ""} + {/snippet} + + {:else} + + No results found + + {/each} + + + +``` + +```svelte title="+page.svelte" + + + +``` + diff --git a/sites/docs/content/delegation.md b/sites/docs/content/delegation.md index 33dae6fbc..6a5761696 100644 --- a/sites/docs/content/delegation.md +++ b/sites/docs/content/delegation.md @@ -29,18 +29,20 @@ Let's take a look at an example using the `Accordion.Trigger` component: We're passing all the props/attribute we would normally apply to th `