Skip to content

Commit

Permalink
next: docs work (#701)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Sep 29, 2024
1 parent 11cef9a commit abaace6
Show file tree
Hide file tree
Showing 20 changed files with 234 additions and 151 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-ligers-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

Forward `dir` prop to underling elements
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,7 +52,7 @@ class LinkPreviewRootState {
this.containsSelection = false;
this.isPointerDownOnContent = false;

sleep(1).then(() => {
afterSleep(1, () => {
const isSelection = document.getSelection()?.toString() !== "";

if (isSelection) {
Expand Down
7 changes: 4 additions & 3 deletions packages/bits-ui/src/lib/bits/menu/menu.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ class MenuContentState {
onblur: this.#onblur,
onpointermove: this.#onpointermove,
onfocus: this.#onfocus,
dir: this.parentMenu.root.dir.current,
style: {
pointerEvents: "auto",
},
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<number | null>(null);

Expand Down
16 changes: 7 additions & 9 deletions packages/bits-ui/src/lib/bits/select/select.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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<SelectNativeOption>) => {
Expand All @@ -144,19 +144,17 @@ export class SelectRootState {
getTriggerTypeaheadCandidateNodes = () => {
const node = this.contentFragment;
if (!node) return [];
const candidates = Array.from(
return Array.from(
node.querySelectorAll<HTMLElement>(`[${ITEM_ATTR}]:not([data-disabled])`)
);
return candidates;
};

getCandidateNodes = () => {
const node = this.contentNode;
if (!node) return [];
const candidates = Array.from(
return Array.from(
node.querySelectorAll<HTMLElement>(`[${ITEM_ATTR}]:not([data-disabled])`)
);
return candidates;
};

createTrigger(props: SelectTriggerStateProps) {
Expand Down
76 changes: 76 additions & 0 deletions sites/docs/content/child-snippet.md
Original file line number Diff line number Diff line change
@@ -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
<button>
{@render children()}
</button>
```

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
<Accordion.Trigger>
{#snippet child({ props })}
<div {...props}>Open accordion item</div>
{/snippet}
</Accordion.Trigger>
```

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
<Accordion.Trigger id="my-custom-id" onclick={() => console.log("clicked")}>
<!-- your custom ID and event handler is now inside the `props` object -->
{#snippet child({ props })}
<div {...props}>Open accordion item</div>
{/snippet}
</Accordion.Trigger>
```

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
<script lang="ts">
// other imports/props/logic omitted for brevity
let { child, children, ...restProps } = $props();
const trigger = makeTrigger();
const mergedProps = $derived(mergeProps(restProps, trigger.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}
```
2 changes: 1 addition & 1 deletion sites/docs/content/components/accordion.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ For each individual item, you need an `Accordion.Item`, `Accordion.Header`, `Acc
</Accordion.Item>
```

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.

Expand Down
4 changes: 4 additions & 0 deletions sites/docs/content/components/listbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,8 @@ To trigger side effects when an item is highlighted or unhighlighted, you can us
</Listbox.Item>
```

## Select vs. Listbox

Use `Select` as a drop-in replacement for `<select>`, supporting form auto-fill. Use `Listbox` for multi-select or custom single-select needs outside forms. For single-select within forms, prefer `Select`.

<APISection {schemas} />
4 changes: 4 additions & 0 deletions sites/docs/content/components/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,4 +302,8 @@ The `Select.ScrollUpButton` and `Select.ScrollDownButton` components are used to

The `Select` component does not support multiple selections. If you're looking for a multi-select component, check out the [Listbox](/docs/components/listbox) component.

## Select vs. Listbox

Use `Select` as a drop-in replacement for `<select>`, supporting form auto-fill. Use `Listbox` for multi-select or custom single-select needs outside forms. For single-select within forms, prefer `Select`.

<APISection {schemas} />
65 changes: 53 additions & 12 deletions sites/docs/content/controlled-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,77 @@ 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
<script lang="ts">
import { Accordion } from "bits-ui";
let myValue = $state("");
</script>
<Accordion.Root bind:value={myValue} type="single">
<!-- Accordion content -->
</Accordion.Root>
```

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<state>` 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 `on<state>Change` 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<State>` prop to true (e.g., `controlledValue`).
- Pass a local state variable to the component.
- Use the `on<State>`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:

```svelte
<script lang="ts">
import { Accordion } from "bits-ui";
let myValue = $state<string>("");
let myValue = $state("");
</script>
<Accordion.Root controlledValue value={myValue} onValueChange={(v) => (myValue = v)}>
<Accordion.Root type="single" controlledValue value={myValue} onValueChange={(v) => (myValue = v)}>
<!-- ... -->
</Accordion.Root>
```

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
63 changes: 0 additions & 63 deletions sites/docs/content/delegation.md

This file was deleted.

2 changes: 1 addition & 1 deletion sites/docs/content/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 8 additions & 4 deletions sites/docs/content/ref.md
Original file line number Diff line number Diff line change
@@ -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
<script lang="ts">
Expand All @@ -27,7 +27,7 @@ For example, the `Accordion.Trigger` component exposes a `ref` prop, which gives

## 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 [delegation](/docs/delegation), the `ref` prop will still work.
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 [delegation](/docs/child-snippet), the `ref` prop will still work.

```svelte
<script lang="ts">
Expand Down Expand Up @@ -92,6 +92,10 @@ The following example would not work, as the `Accordion.Trigger` component has n
</Accordion.Trigger>
```

## 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 [`WithElementRef`](/docs/type-helpers/with-element-ref) type which enables you to create your own components following the same `ref` prop pattern.
Loading

0 comments on commit abaace6

Please sign in to comment.