diff --git a/.changeset/healthy-ligers-sell.md b/.changeset/healthy-ligers-sell.md new file mode 100644 index 000000000..d1ae7b25b --- /dev/null +++ b/.changeset/healthy-ligers-sell.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +Forward `dir` prop to underling elements 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 ea81acf86..7abfa2252 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 @@ -11,6 +11,7 @@ import { getTabbableCandidates } from "$lib/internal/focus.js"; import { createContext } from "$lib/internal/createContext.js"; import { useGraceArea } from "$lib/internal/useGraceArea.svelte.js"; import { onDestroyEffect } from "$lib/internal/onDestroyEffect.svelte.js"; +import { afterSleep } from "$lib/internal/afterSleep.js"; const CONTENT_ATTR = "data-link-preview-content"; const TRIGGER_ATTR = "data-link-preview-trigger"; @@ -51,7 +52,7 @@ class LinkPreviewRootState { this.containsSelection = false; this.isPointerDownOnContent = false; - sleep(1).then(() => { + afterSleep(1, () => { const isSelection = document.getSelection()?.toString() !== ""; if (isSelection) { diff --git a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts index 9a4e5d356..c91e2f763 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -415,6 +415,7 @@ class MenuContentState { onblur: this.#onblur, onpointermove: this.#onpointermove, onfocus: this.#onfocus, + dir: this.parentMenu.root.dir.current, style: { pointerEvents: "auto", }, @@ -551,7 +552,7 @@ class MenuItemState { if (!isHTMLElement(e.currentTarget)) return; e.currentTarget.click(); /** - * We prevent default browser behaviour for selection keys as they should trigger + * We prevent default browser behavior for selection keys as they should trigger * a selection only: * - prevents space from scrolling the page. * - if keydown causes focus to move, prevents keydown from firing on the new target. @@ -601,9 +602,9 @@ class MenuItemState { class MenuSubTriggerState { #item: MenuItemSharedState; - // The menu this subtrigger item belongs within + // The menu this sub-trigger item belongs within #content: MenuContentState; - // the menu this subtrigger item opens + // the menu this sub-trigger item opens #submenu: MenuMenuState; #openTimer = $state(null); 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 9f467ed6b..961e2f535 100644 --- a/packages/bits-ui/src/lib/bits/select/select.svelte.ts +++ b/packages/bits-ui/src/lib/bits/select/select.svelte.ts @@ -30,6 +30,7 @@ import { noop } from "$lib/internal/callbacks.js"; import { addEventListener } from "$lib/internal/events.js"; import { sleep } from "$lib/internal/sleep.js"; import type { WithRefProps } from "$lib/internal/types.js"; +import { afterSleep } from "$lib/internal/afterSleep.js"; export const OPEN_KEYS = [kbd.SPACE, kbd.ENTER, kbd.ARROW_UP, kbd.ARROW_DOWN]; export const SELECTION_KEYS = [" ", kbd.ENTER]; @@ -126,11 +127,10 @@ export class SelectRootState { focusTriggerNode = (preventScroll: boolean = true) => { const node = this.triggerNode; - if (node) { - sleep(1).then(() => { - node.focus({ preventScroll }); - }); - } + if (!node) return; + afterSleep(1, () => { + node.focus({ preventScroll }); + }); }; onNativeOptionAdd = (option: ReadableBox) => { @@ -144,19 +144,17 @@ export class SelectRootState { getTriggerTypeaheadCandidateNodes = () => { const node = this.contentFragment; if (!node) return []; - const candidates = Array.from( + return Array.from( node.querySelectorAll(`[${ITEM_ATTR}]:not([data-disabled])`) ); - return candidates; }; getCandidateNodes = () => { const node = this.contentNode; if (!node) return []; - const candidates = Array.from( + return Array.from( node.querySelectorAll(`[${ITEM_ATTR}]:not([data-disabled])`) ); - return candidates; }; createTrigger(props: SelectTriggerStateProps) { diff --git a/sites/docs/content/child-snippet.md b/sites/docs/content/child-snippet.md new file mode 100644 index 000000000..e3147882f --- /dev/null +++ b/sites/docs/content/child-snippet.md @@ -0,0 +1,76 @@ +--- +title: Child Snippet +description: Learn how to use the `child` snippet to render your own elements. +--- + +## Usage + +Many Bits UI components have a default HTML element that wraps their `children`. For example, `Accordion.Trigger` typically renders as: + +```svelte + +``` + +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: + +```svelte + + {#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 [`mergeProps`](/docs/utilities/merge-props) function to handle cancelling internal handlers, etc. + +Correct usage: + +```svelte + 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: + +```svelte + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} +``` diff --git a/sites/docs/content/components/accordion.md b/sites/docs/content/components/accordion.md index dc8fc0f72..9e9a8e752 100644 --- a/sites/docs/content/components/accordion.md +++ b/sites/docs/content/components/accordion.md @@ -85,7 +85,7 @@ For each individual item, you need an `Accordion.Item`, `Accordion.Header`, `Acc ``` -We used the [`WithoutChildrenOrChild`](/docs/type-helpers/without-children-or-child) type helper to omit the `child` and `children` snippet props from `Accordion.ItemProps`, since we are opting out of using [Delegation](/docs/delegation) and are already taking care of rendering the children as text via the `content` prop. +We used the [`WithoutChildrenOrChild`](/docs/type-helpers/without-children-or-child) type helper to omit the `child` and `children` snippet props from `Accordion.ItemProps`, since we are opting out of using [delegation](/docs/child-snippet) and are already taking care of rendering the children as text via the `content` prop. For our `MyAccordion` component, we'll accept all the props that `Accordion.Root` accepts, as well as an additional `items` prop that will be used to render the `MyAccordionItem` components. diff --git a/sites/docs/content/components/listbox.md b/sites/docs/content/components/listbox.md index 66c7545c7..b218b1f89 100644 --- a/sites/docs/content/components/listbox.md +++ b/sites/docs/content/components/listbox.md @@ -420,4 +420,8 @@ To trigger side effects when an item is highlighted or unhighlighted, you can us ``` +## Select vs. Listbox + +Use `Select` as a drop-in replacement for ``, supporting form auto-fill. Use `Listbox` for multi-select or custom single-select needs outside forms. For single-select within forms, prefer `Select`. + diff --git a/sites/docs/content/controlled-state.md b/sites/docs/content/controlled-state.md index c2978b9d0..21d487b0c 100644 --- a/sites/docs/content/controlled-state.md +++ b/sites/docs/content/controlled-state.md @@ -3,23 +3,47 @@ title: Controlled State description: Learn how to use controlled state in Bits UI components. --- -Sometimes, Bits UI doesn't know what's best for your specific use case. In these cases, you can use controlled state to ensure the component remains in a specific state depending on your needs. +Bits UI components offer flexibility in state management, allowing you to choose between uncontrolled and controlled states. This guide will help you understand when and how to use controlled state effectively. -## Uncontrolled State +## Understanding State Management -By default, Bits UI components are uncontrolled. This means that the component is responsible for managing its own state. You can `bind:` to that state for a reference to it, but the component decides when and how to update that state. +### Uncontrolled State (Default) -For example, the `Accordion.Root` component manages its own `value` state. When you click or press on any of the triggers, the component will update the `value` state to the value of the trigger that was clicked. +By default, Bits UI components operate in an uncontrolled state. In this mode: -You can update the `value` state of the `Accordion.Root` component yourself from the outside, but you can't prevent the component from updating it. Preventing the component from updating the state is where controlled state comes in. +- The component internally manages its own state. +- You can `bind:` to the state for reference. +- The component decides when and how to update its state. +- You can update the state of the component yourself from the outside, but you can't prevent the component from updating it. -## Controlled State +Here's an example of an uncontrolled Accordion: -Controlled state is when you, as the user, are responsible for updating the state of the component. The component will let you know when it thinks it needs to update the state, but you'll be responsible for whether that update happens. +```svelte + + + + + +``` + +In this example, the `Accordion.Root` component manages its value state internally. When a user interacts with the accordion, the component updates the value automatically. The local `myValue` is synced with the component's internal `value` state in both directions. -This is useful when you have specific conditions that should be met before the component can update, or anything else your requirements dictate. +### Controlled State -To effectively use controlled state, you'll need to set the `controlled` prop to `true` on the component, and you'll also need to pass a local state variable to the component that you'll update yourself. You'll use the `onChange` callback to update the local state variable. +Controlled state puts you in charge of the component's state management. Use this approach when: + +- You need to meet specific conditions before state updates. +- You want to synchronize the component's state with other parts of your application. +- You require custom logic for state updates. + +To implement controlled state: + +- Set the `controlled` prop to true (e.g., `controlledValue`). +- Pass a local state variable to the component. +- Use the `on`Change callback to update the local state (e.g., `onValueChange`). Here's an example of how you might use controlled state with the `Accordion` component: @@ -27,12 +51,29 @@ Here's an example of how you might use controlled state with the `Accordion` com - (myValue = v)}> + (myValue = v)}> ``` -In the example above, we're using the `controlledValue` prop to tell the `Accordion.Root` component that it should be in controlled state. If we were to remove the `onValueChange` callback, the component wouldn't respond to user interactions and wouldn't update the `value` state. +In this controlled state example: + +- We set `controlledValue` to true. +- We pass our local `myValue` state to the value prop. +- We use `onValueChange` to handle state updates + +## Best Practices + +- **Choose wisely**: Use controlled state only when necessary. Uncontrolled state is simpler and sufficient for most use cases. +- **Consistent control**: If you opt for controlled state, ensure you handle all related state updates to maintain consistency. +- **Performance consideration**: Be mindful of potential performance impacts when using controlled state, especially with frequently updating components. + +## Common Controlled State Scenarios + +- Form validation before state updates +- Syncing component state with external data sources +- Implementing undo/redo functionality +- Creating interdependent component behaviors diff --git a/sites/docs/content/delegation.md b/sites/docs/content/delegation.md deleted file mode 100644 index 84c9a3ec6..000000000 --- a/sites/docs/content/delegation.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Render Delegation -description: Learn how to use the `child` snippet to render your own elements. ---- - -## Usage - -For certain components, we set a default underlying HTML element to wrap the `children` with. As a high level example, the `Accordion.Trigger`, looks something like this: - -```svelte - -``` - -While we do allow you to set any attribute that you normally could on a button, let's say you want to apply a [Svelte Transition](https://svelte.dev/docs#transition) or [Svelte Action](https://svelte.dev/docs#use_action) to the button, or use a custom component you've made, that's when render delegation comes into play. - -Each of the components that support render delegation accept an optional prop called `child`, which is a [Snippet](https://svelte.dev). When used, the component will pass the attributes as a snippet prop, which you can then apply to the element of your choosing. Note, if you use `child` any other children that aren't within that `child` snippet will be ignored. - -Let's take a look at an example using the `Accordion.Trigger` component: - -```svelte - - {#snippet child({ props })} -
Open accordion item
- {/snippet} -
-``` - -We're passing all the props/attributes we would normally apply to the ` -{/if} -``` diff --git a/sites/docs/content/introduction.md b/sites/docs/content/introduction.md index 4d297eefa..471784f75 100644 --- a/sites/docs/content/introduction.md +++ b/sites/docs/content/introduction.md @@ -21,7 +21,7 @@ Bits UI components have been designed following the [W3C ARIA Authoring Practice ### Composable -Bits UI is built with composability in mind. Each component is designed to be used in isolation, but can be composed together to create more complex UIs. Providing flexibility in the form of [Delegation](/docs/delegation) and event overrides puts the power of bending the components to your will in your hands. +Bits UI is built with composability in mind. Each component is designed to be used in isolation, but can be composed together to create more complex UIs. Providing flexibility in the form of [Composition](/docs/child-snippet) and event overrides puts the power of bending the components to your will in your hands. ## About diff --git a/sites/docs/content/ref.md b/sites/docs/content/ref.md index df9eaa664..2514bc00c 100644 --- a/sites/docs/content/ref.md +++ b/sites/docs/content/ref.md @@ -1,11 +1,11 @@ --- title: Ref -description: Learn about the ref prop. +description: Learn about the $bindable ref prop. --- -Bits UI components that render an underlying HTML element expose a `ref` prop, which gives you a reference to the underlying element. +Bits UI components with underlying HTML elements provide a `ref` prop for direct element access. -For example, the `Accordion.Trigger` component exposes a `ref` prop, which gives you a reference to the `HTMLButtonElement` that is rendered by the component. +For example, `Accordion.Trigger`'s `ref` gives access to its rendered `HTMLButtonElement`: ```svelte