@ui-sdk/react 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +10 -2
- package/dist/accordion.d.ts +331 -0
- package/dist/accordion.js +25 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +8 -0
- package/dist/alert-dialog.d.ts +56 -0
- package/dist/alert-dialog.js +45 -0
- package/dist/async-options.d.ts +22 -0
- package/dist/async-options.js +56 -0
- package/dist/autocomplete.d.ts +102 -0
- package/dist/autocomplete.js +195 -0
- package/dist/avatar.d.ts +301 -0
- package/dist/avatar.js +17 -0
- package/dist/button.d.ts +21 -0
- package/dist/button.js +45 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +9 -0
- package/dist/calendar.d.ts +80 -0
- package/dist/calendar.js +257 -0
- package/dist/carousel.d.ts +52 -0
- package/dist/carousel.js +151 -0
- package/dist/checkbox.d.ts +24 -0
- package/dist/checkbox.js +98 -0
- package/dist/children.d.ts +38 -0
- package/dist/children.js +63 -0
- package/dist/collapsible.d.ts +302 -0
- package/dist/collapsible.js +19 -0
- package/dist/color-picker.d.ts +72 -0
- package/dist/color-picker.js +133 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +22 -0
- package/dist/combobox.d.ts +152 -0
- package/dist/combobox.js +338 -0
- package/dist/command.d.ts +88 -0
- package/dist/command.js +233 -0
- package/dist/controller-target.d.ts +3 -0
- package/dist/controller-target.js +59 -0
- package/dist/date-input.d.ts +19 -0
- package/dist/date-input.js +58 -0
- package/dist/date-picker.d.ts +47 -0
- package/dist/date-picker.js +310 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +60 -0
- package/dist/dialog-parts.d.ts +23 -0
- package/dist/dialog-parts.js +142 -0
- package/dist/dialog.d.ts +57 -0
- package/dist/dialog.js +32 -0
- package/dist/drawer.d.ts +70 -0
- package/dist/drawer.js +158 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +9 -0
- package/dist/field-context.d.ts +47 -0
- package/dist/field-context.js +87 -0
- package/dist/field.d.ts +49 -0
- package/dist/field.js +169 -0
- package/dist/file-input.d.ts +46 -0
- package/dist/file-input.js +170 -0
- package/dist/form-inputs.d.ts +5 -0
- package/dist/form-inputs.js +22 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +16 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +12 -0
- package/dist/hover-card.d.ts +33 -0
- package/dist/hover-card.js +84 -0
- package/dist/image-cropper.d.ts +333 -0
- package/dist/image-cropper.js +37 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +6 -0
- package/dist/input.d.ts +22 -0
- package/dist/input.js +51 -0
- package/dist/label.d.ts +294 -0
- package/dist/label.js +9 -0
- package/dist/list-items.d.ts +15 -0
- package/dist/list-items.js +39 -0
- package/dist/list-text.d.ts +4 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +56 -0
- package/dist/listbox.js +122 -0
- package/dist/menu.d.ts +116 -0
- package/dist/menu.js +426 -0
- package/dist/menubar-context.d.ts +4 -0
- package/dist/menubar-context.js +7 -0
- package/dist/menubar.d.ts +11 -0
- package/dist/menubar.js +32 -0
- package/dist/meter.d.ts +312 -0
- package/dist/meter.js +30 -0
- package/dist/navigation-menu.d.ts +61 -0
- package/dist/navigation-menu.js +169 -0
- package/dist/number-input.d.ts +44 -0
- package/dist/number-input.js +185 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +40 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +8 -0
- package/dist/otp-input.d.ts +29 -0
- package/dist/otp-input.js +94 -0
- package/dist/overlays.d.ts +26 -0
- package/dist/overlays.js +82 -0
- package/dist/pagination.d.ts +336 -0
- package/dist/pagination.js +71 -0
- package/dist/password-input.d.ts +23 -0
- package/dist/password-input.js +65 -0
- package/dist/popover.d.ts +54 -0
- package/dist/popover.js +151 -0
- package/dist/popup.d.ts +41 -0
- package/dist/popup.js +180 -0
- package/dist/preferences.d.ts +35 -0
- package/dist/preferences.js +93 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +14 -0
- package/dist/progress.d.ts +312 -0
- package/dist/progress.js +30 -0
- package/dist/radio.d.ts +22 -0
- package/dist/radio.js +86 -0
- package/dist/rating.d.ts +20 -0
- package/dist/rating.js +94 -0
- package/dist/render-element.d.ts +35 -0
- package/dist/render-element.js +103 -0
- package/dist/resizable.d.ts +302 -0
- package/dist/resizable.js +23 -0
- package/dist/scroll-area.d.ts +324 -0
- package/dist/scroll-area.js +49 -0
- package/dist/segmented-control.d.ts +24 -0
- package/dist/segmented-control.js +78 -0
- package/dist/segmented-input.d.ts +15 -0
- package/dist/segmented-input.js +48 -0
- package/dist/select.d.ts +125 -0
- package/dist/select.js +330 -0
- package/dist/separator.d.ts +290 -0
- package/dist/separator.js +9 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +61 -0
- package/dist/slider.d.ts +38 -0
- package/dist/slider.js +163 -0
- package/dist/switch.d.ts +15 -0
- package/dist/switch.js +39 -0
- package/dist/tabs.d.ts +331 -0
- package/dist/tabs.js +28 -0
- package/dist/text-control.d.ts +22 -0
- package/dist/text-control.js +24 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +8 -0
- package/dist/textarea.d.ts +10 -0
- package/dist/textarea.js +48 -0
- package/dist/time-input.d.ts +19 -0
- package/dist/time-input.js +63 -0
- package/dist/timeline.d.ts +327 -0
- package/dist/timeline.js +31 -0
- package/dist/toast-parts.d.ts +63 -0
- package/dist/toast-parts.js +144 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +22 -0
- package/dist/toggle.d.ts +23 -0
- package/dist/toggle.js +76 -0
- package/dist/token-input.d.ts +35 -0
- package/dist/token-input.js +100 -0
- package/dist/toolbar-context.d.ts +6 -0
- package/dist/toolbar-context.js +16 -0
- package/dist/toolbar.d.ts +38 -0
- package/dist/toolbar.js +63 -0
- package/dist/tooltip.d.ts +39 -0
- package/dist/tooltip.js +108 -0
- package/dist/tree.d.ts +51 -0
- package/dist/tree.js +150 -0
- package/dist/trigger-menu.d.ts +83 -0
- package/dist/trigger-menu.js +167 -0
- package/dist/use-primitive.d.ts +178 -0
- package/dist/use-primitive.js +674 -0
- package/dist/waterfall.d.ts +80 -0
- package/dist/waterfall.js +108 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1391 -0
- package/docs/components/alert-dialog.md +850 -0
- package/docs/components/autocomplete.md +1963 -0
- package/docs/components/avatar.md +252 -0
- package/docs/components/button.md +444 -0
- package/docs/components/calendar.md +1687 -0
- package/docs/components/carousel.md +1244 -0
- package/docs/components/checkbox.md +1069 -0
- package/docs/components/collapsible.md +567 -0
- package/docs/components/color-picker.md +736 -0
- package/docs/components/combobox.md +3647 -0
- package/docs/components/command.md +1573 -0
- package/docs/components/date-input.md +608 -0
- package/docs/components/date-picker.md +1266 -0
- package/docs/components/dialog.md +1469 -0
- package/docs/components/drawer.md +1197 -0
- package/docs/components/field.md +549 -0
- package/docs/components/file-input.md +609 -0
- package/docs/components/hover-card.md +514 -0
- package/docs/components/image-cropper.md +579 -0
- package/docs/components/input.md +301 -0
- package/docs/components/label.md +217 -0
- package/docs/components/listbox.md +1130 -0
- package/docs/components/menu.md +1286 -0
- package/docs/components/menubar.md +455 -0
- package/docs/components/meter.md +304 -0
- package/docs/components/navigation-menu.md +1581 -0
- package/docs/components/number-input.md +1086 -0
- package/docs/components/otp-input.md +565 -0
- package/docs/components/pagination.md +648 -0
- package/docs/components/password-input.md +737 -0
- package/docs/components/popover.md +1552 -0
- package/docs/components/preferences.md +220 -0
- package/docs/components/progress.md +443 -0
- package/docs/components/radio.md +846 -0
- package/docs/components/rating.md +458 -0
- package/docs/components/resizable.md +982 -0
- package/docs/components/scroll-area.md +980 -0
- package/docs/components/segmented-control.md +465 -0
- package/docs/components/select.md +2133 -0
- package/docs/components/separator.md +243 -0
- package/docs/components/slider.md +1169 -0
- package/docs/components/switch.md +613 -0
- package/docs/components/tabs.md +1014 -0
- package/docs/components/textarea.md +318 -0
- package/docs/components/time-input.md +231 -0
- package/docs/components/timeline.md +677 -0
- package/docs/components/toast.md +1395 -0
- package/docs/components/toggle.md +541 -0
- package/docs/components/token-input.md +1007 -0
- package/docs/components/toolbar.md +787 -0
- package/docs/components/tooltip.md +884 -0
- package/docs/components/tree.md +1346 -0
- package/docs/components/trigger-menu.md +1257 -0
- package/docs/components/waterfall.md +754 -0
- package/docs/controllers.md +89 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +120 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +148 -0
- package/docs/hooks/async-options.md +366 -0
- package/docs/hooks/shortcuts.md +221 -0
- package/docs/installation.md +45 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +170 -0
- package/docs/llms.txt +1844 -0
- package/docs/parts-and-composition.md +93 -0
- package/docs/popups.md +117 -0
- package/docs/rendering.md +121 -0
- package/docs/server-rendering.md +253 -0
- package/docs/state-and-events.md +105 -0
- package/docs/styling-and-animation.md +176 -0
- package/package.json +333 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Parts and Composition
|
|
2
|
+
|
|
3
|
+
Put a component's parts together and choose what each one renders.
|
|
4
|
+
|
|
5
|
+
A component can have several parts. A Popover has a root to hold its state, a trigger to open it, and content to show.
|
|
6
|
+
|
|
7
|
+
## Put Parts Together
|
|
8
|
+
|
|
9
|
+
Import the parts you need and nest them as shown in the component's Composition:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverClose,
|
|
15
|
+
PopoverContent,
|
|
16
|
+
PopoverTitle,
|
|
17
|
+
PopoverTrigger,
|
|
18
|
+
} from "@ui-sdk/react/popover"
|
|
19
|
+
|
|
20
|
+
function Example() {
|
|
21
|
+
return (
|
|
22
|
+
<Popover>
|
|
23
|
+
<PopoverTrigger>Details</PopoverTrigger>
|
|
24
|
+
<PopoverContent sideOffset={8}>
|
|
25
|
+
<PopoverTitle>Project Details</PopoverTitle>
|
|
26
|
+
<p>Last updated today.</p>
|
|
27
|
+
<PopoverClose>Done</PopoverClose>
|
|
28
|
+
</PopoverContent>
|
|
29
|
+
</Popover>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { Example }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Popover` shares state with its parts without adding an element. Other roots, such as `Tabs`, do render an element. Each component's page shows its structure.
|
|
37
|
+
|
|
38
|
+
Parts find their root through React context. You can put your own components or layout elements between them.
|
|
39
|
+
|
|
40
|
+
## Choose the Element
|
|
41
|
+
|
|
42
|
+
Use `render` to replace a part's default element. For example, render a menu item as a link:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/react/menu"
|
|
46
|
+
|
|
47
|
+
function Example() {
|
|
48
|
+
return (
|
|
49
|
+
<Menu>
|
|
50
|
+
<MenuTrigger>Account</MenuTrigger>
|
|
51
|
+
<MenuContent>
|
|
52
|
+
<MenuItem render={<a href="/settings" />}>Settings</MenuItem>
|
|
53
|
+
</MenuContent>
|
|
54
|
+
</Menu>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Example }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The part passes its attributes, handlers, and ref to the element. A custom component used here needs to forward those props and the ref to its DOM element.
|
|
62
|
+
|
|
63
|
+
Your event handler runs first. Calling `event.preventDefault()` skips the part's built-in handler. Class names, styles, and refs combine.
|
|
64
|
+
|
|
65
|
+
## Read the State
|
|
66
|
+
|
|
67
|
+
Pass a function to `children` when the content depends on state:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
"use client"
|
|
71
|
+
|
|
72
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
73
|
+
|
|
74
|
+
function Example() {
|
|
75
|
+
return (
|
|
76
|
+
<Toggle aria-label="Mute">
|
|
77
|
+
{(state) => (state.pressed ? "Mute on" : "Mute off")}
|
|
78
|
+
</Toggle>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { Example }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Keep the accessible name stable when the content changes. Here, `aria-label="Mute"` names the toggle and `aria-pressed` reports its state.
|
|
86
|
+
|
|
87
|
+
`render` can also be a function of the props and state. [Custom Parts](./custom-parts.md) covers writing a part with logic of its own.
|
|
88
|
+
|
|
89
|
+
## Name the Control
|
|
90
|
+
|
|
91
|
+
Use the component's label or title part, such as `FieldLabel` or `DialogTitle`. For a control without visible text, give it an `aria-label`.
|
|
92
|
+
|
|
93
|
+
Parts manage their own roles, IDs, and ARIA state. Those attributes take precedence over conflicting props passed directly to the part. Props on the element passed to `render` can override them, so leave the managed attributes intact. If you use a render function, forward the supplied props to its element. A name or typing hint you pass replaces the part's own: `aria-label`, `aria-labelledby`, `aria-description`, `title`, `autoComplete`, `autoCapitalize`, `autoCorrect`, `inputMode` and `spellCheck`. Roles, `tabindex`, references like `aria-controls` and `aria-describedby`, state attributes and `data-slot` stay the part's, since its behavior reads them.
|
package/docs/popups.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Popups
|
|
2
|
+
|
|
3
|
+
Place popup content, control dismissal, and choose when it stays mounted.
|
|
4
|
+
|
|
5
|
+
Popovers, menus, selects, and dialogs use the browser's top layer. They render in place, above the page, so you don't need a Portal or Positioner part. Their events bubble through the elements around them like any other child's, so a click handler or a link wrapped around a popup also gets the clicks inside it. Keep popups outside clickable cards and links.
|
|
6
|
+
|
|
7
|
+
## Place the Content
|
|
8
|
+
|
|
9
|
+
Set placement on the content part:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@ui-sdk/react/popover"
|
|
13
|
+
|
|
14
|
+
function Example() {
|
|
15
|
+
return (
|
|
16
|
+
<Popover>
|
|
17
|
+
<PopoverTrigger>Details</PopoverTrigger>
|
|
18
|
+
<PopoverContent side="bottom" align="start" sideOffset={8}>
|
|
19
|
+
Last updated today.
|
|
20
|
+
</PopoverContent>
|
|
21
|
+
</Popover>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { Example }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The popup flips or shifts when there isn't enough room.
|
|
29
|
+
|
|
30
|
+
| Prop | What It Does |
|
|
31
|
+
| ------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `side` | Chooses `top`, `bottom`, `left`, `right`, `start`, or `end`. Start and end follow reading direction. |
|
|
33
|
+
| `align` | Aligns the popup to the `start`, `center`, or `end` of that side. |
|
|
34
|
+
| `sideOffset` | Sets the gap from the anchor, in pixels. |
|
|
35
|
+
| `alignOffset` | Shifts the popup along its side, in pixels. |
|
|
36
|
+
| `collisionPadding` | Leaves space from the window's edges. |
|
|
37
|
+
| `arrowPadding` | Keeps an arrow away from the popup's corners. |
|
|
38
|
+
|
|
39
|
+
An `anchor` can position the popup against another element. The component's page lists the anchors it accepts.
|
|
40
|
+
|
|
41
|
+
## Control Dismissal
|
|
42
|
+
|
|
43
|
+
Set `dismissible={false}` to stop Escape, outside presses, focus leaving, Back, and sheet swipes from closing a popup. Its trigger, Close part, and controller can still close it.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<Popover
|
|
47
|
+
dismissible={false}
|
|
48
|
+
onDismissAttempt={(details) => console.log(details.reason)}
|
|
49
|
+
>
|
|
50
|
+
{/* Trigger, content, and a Close part. */}
|
|
51
|
+
</Popover>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use [State and Events](./state-and-events.md) when you want to cancel only a particular reason, such as an outside press while a form has unsaved changes.
|
|
55
|
+
|
|
56
|
+
## Modals and Sheets
|
|
57
|
+
|
|
58
|
+
`modal` makes the rest of the page inert and locks scrolling. Each component documents its default.
|
|
59
|
+
|
|
60
|
+
Menu, Select, Combobox, Autocomplete, Popover, and Dialog support `presentation="auto"` to become a sheet on small touch screens. Use `presentation="sheet"` to always show a sheet.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Popover presentation="auto">{/* Trigger and content. */}</Popover>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Focus
|
|
67
|
+
|
|
68
|
+
Dialogs, alert dialogs, drawers, and popovers move focus into their content when they open. Where it lands depends on how they opened:
|
|
69
|
+
|
|
70
|
+
| Opened With | Focus Goes To |
|
|
71
|
+
| -------------------------------------------- | ------------------------------------------------------------ |
|
|
72
|
+
| Keyboard, screen reader, mouse, pen, or code | The element `initialFocus` picks. |
|
|
73
|
+
| Touch | The content itself, so no on-screen keyboard appears. |
|
|
74
|
+
| Hover | Nowhere. Focus stays where it is. |
|
|
75
|
+
| Rendered open | Nowhere, except in a dialog, which takes focus as it mounts. |
|
|
76
|
+
|
|
77
|
+
`initialFocus` defaults to `"auto"`: the first focusable element, or the content when it has none. A dialog tries an element with `autofocus` first, and an alert dialog tries its first Close part. Pass `"content"` to focus the content itself, `"none"` to leave focus alone, or a CSS selector, an element, or a function that returns one.
|
|
78
|
+
|
|
79
|
+
`modal` decides whether Tab can leave. With `true` or `"trap-focus"`, Tab and Shift+Tab wrap inside the content. With `false`, Tab moves from the trigger into a popover and on to the page after it, and focus leaving closes the popover. A non-modal dialog stays open.
|
|
80
|
+
|
|
81
|
+
When a popup closes while focus is inside it, focus returns to the trigger. If the trigger is gone or can't take focus, focus goes to the element focused before the popup opened, then where the popup it was opened from would return focus, then to the nearest focusable element after where the trigger was. A popup a controller opened returns to the element focused before it first. Set `finalFocus` to `"none"` to leave focus alone, or to a selector, an element, or a function to send it somewhere else. Closing because focus moved away never moves it back, and neither does pressing something focusable outside a non-modal popup.
|
|
82
|
+
|
|
83
|
+
Menus, selects, and comboboxes handle focus within their own lists, and tooltips never move it. Each component's page covers its own.
|
|
84
|
+
|
|
85
|
+
## Mounting and Containers
|
|
86
|
+
|
|
87
|
+
Closed content unmounts after its exit animation. `keepMounted` leaves it in the page, hidden. `onOpenChangeComplete` runs when opening or closing finishes, including animations.
|
|
88
|
+
|
|
89
|
+
Use `container` to render into another element, a shadow root, or an element in an iframe. Focus, Escape, and scroll locking then work in the document and shadow root the content renders in. Keep a dialog containing a form outside any surrounding form, or move its content with `container`, to avoid nested forms.
|
|
90
|
+
|
|
91
|
+
In a tree a hidden `<Activity>` or a Suspense fallback hides, an open popup hides with it, from its `container` too, and comes back open when the tree shows, with focus in it if it's modal.
|
|
92
|
+
|
|
93
|
+
[Styling and Animation](./styling-and-animation.md) shows how to animate the content.
|
|
94
|
+
|
|
95
|
+
## Toasts and Confirms
|
|
96
|
+
|
|
97
|
+
Mount `OverlayHost` once near your app's root, then call `toast()` from event handlers:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
"use client"
|
|
101
|
+
|
|
102
|
+
import { OverlayHost } from "@ui-sdk/react/overlays"
|
|
103
|
+
import { toast } from "@ui-sdk/react/toast"
|
|
104
|
+
|
|
105
|
+
function Example() {
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<OverlayHost />
|
|
109
|
+
<button onClick={() => toast.success("Saved")}>Save</button>
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { Example }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See [Toast](./components/toast.md) and [Alert Dialog](./components/alert-dialog.md) for the complete APIs.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Rendering Primitives
|
|
2
|
+
|
|
3
|
+
Turn a Core primitive into React components with createParts.
|
|
4
|
+
|
|
5
|
+
This page renders the rating from [Compose Behaviors](./core.md#compose-behaviors). Start with that guide's [complete `rating.ts`](./core.md#complete-primitive): it already handles selection, arrow keys, and focus. Here we'll connect its `root` and `star` slots to React.
|
|
6
|
+
|
|
7
|
+
## Create the Parts
|
|
8
|
+
|
|
9
|
+
`createParts` maps slots to elements. Save this beside `rating.ts`:
|
|
10
|
+
|
|
11
|
+
```ts title="rating-parts.ts"
|
|
12
|
+
"use client"
|
|
13
|
+
|
|
14
|
+
import { createParts } from "@ui-sdk/react/primitive"
|
|
15
|
+
import { rating } from "./rating"
|
|
16
|
+
|
|
17
|
+
const { Root: RatingRoot, Star: RatingStar } = createParts(rating, {
|
|
18
|
+
root: "div",
|
|
19
|
+
star: "button",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export { RatingRoot, RatingStar }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`Root` creates the instance and shares it with the parts inside. Each mapped slot becomes a component. Slot names become Pascal case: `star` becomes `Star`, and `tab-icon` would become `TabIcon`.
|
|
26
|
+
|
|
27
|
+
If you leave out the `root` tag, `Root` still shares the instance but renders only its children.
|
|
28
|
+
|
|
29
|
+
## Render the Rating
|
|
30
|
+
|
|
31
|
+
The root accepts the primitive's options and the element's props. Each star receives its slot value and a name a screen reader can announce:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
"use client"
|
|
35
|
+
|
|
36
|
+
import { RatingRoot, RatingStar } from "./rating-parts"
|
|
37
|
+
|
|
38
|
+
function Example() {
|
|
39
|
+
return (
|
|
40
|
+
<RatingRoot defaultValue={4} aria-label="Rating" className="flex gap-1">
|
|
41
|
+
{Array.from({ length: 5 }, (_, index) => index + 1).map((star) => (
|
|
42
|
+
<RatingStar
|
|
43
|
+
key={star}
|
|
44
|
+
value={String(star)}
|
|
45
|
+
aria-label={`${star} ${star === 1 ? "star" : "stars"}`}
|
|
46
|
+
className="opacity-40 data-filled:opacity-100"
|
|
47
|
+
>
|
|
48
|
+
★
|
|
49
|
+
</RatingStar>
|
|
50
|
+
))}
|
|
51
|
+
</RatingRoot>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Example }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The primitive supplies `data-filled`; these classes make the filled stars easier to see. [Styling and Animation](./styling-and-animation.md) covers other ways to style parts.
|
|
59
|
+
|
|
60
|
+
Generated parts also support the same `render` prop and state callbacks as the components in the catalog. Use [Parts and Composition](./parts-and-composition.md) when you want to change their markup.
|
|
61
|
+
|
|
62
|
+
## Wrap the Parts
|
|
63
|
+
|
|
64
|
+
If callers always want a row of stars, wrap the parts in one `Rating` component. This version adds `max` and `starLabel` while forwarding the primitive options and root element props. You can use it instead of the separate parts above.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// rating-react.tsx
|
|
68
|
+
"use client"
|
|
69
|
+
|
|
70
|
+
import { createParts, type PartProps } from "@ui-sdk/react/primitive"
|
|
71
|
+
import { rating, type RatingOptions } from "./rating"
|
|
72
|
+
|
|
73
|
+
const parts = createParts(rating, { root: "div", star: "button" })
|
|
74
|
+
|
|
75
|
+
function Rating({
|
|
76
|
+
max = 5,
|
|
77
|
+
starLabel = (star) => `${star} ${star === 1 ? "star" : "stars"}`,
|
|
78
|
+
...props
|
|
79
|
+
}: RatingOptions & {
|
|
80
|
+
max?: number
|
|
81
|
+
starLabel?: (star: number) => string
|
|
82
|
+
} & Omit<PartProps<"div">, "defaultValue">) {
|
|
83
|
+
return (
|
|
84
|
+
<parts.Root {...props}>
|
|
85
|
+
{Array.from({ length: max }, (_, index) => index + 1).map((star) => (
|
|
86
|
+
<parts.Star
|
|
87
|
+
key={star}
|
|
88
|
+
value={String(star)}
|
|
89
|
+
aria-label={starLabel(star)}
|
|
90
|
+
>
|
|
91
|
+
★
|
|
92
|
+
</parts.Star>
|
|
93
|
+
))}
|
|
94
|
+
</parts.Root>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { Rating }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Rating } from "./rating-react"
|
|
103
|
+
|
|
104
|
+
function Example() {
|
|
105
|
+
return (
|
|
106
|
+
<Rating
|
|
107
|
+
defaultValue={4}
|
|
108
|
+
aria-label="Rating"
|
|
109
|
+
className="flex gap-1 [&>button]:opacity-40 [&>[data-filled]]:opacity-100"
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { Example }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pass classes to the rendered parts. [Styling and Animation](./styling-and-animation.md) also shows how to use CSS Modules.
|
|
118
|
+
|
|
119
|
+
## Add Logic to a Part
|
|
120
|
+
|
|
121
|
+
Keep `createParts` when mapping slots to elements is enough. [Custom Parts](./custom-parts.md) uses the same rating to show how `usePrimitive` and `usePart` let you write your own root or child part.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Server Rendering
|
|
2
|
+
|
|
3
|
+
Render the initial HTML on the server and add interactions when React hydrates.
|
|
4
|
+
|
|
5
|
+
ui-sdk parts render their initial HTML on the server, including ARIA state and keyboard tab stops. Hydration connects the event handlers in the browser.
|
|
6
|
+
|
|
7
|
+
## Render from a Server Component
|
|
8
|
+
|
|
9
|
+
In an app with React Server Components, such as Next.js, you can render ui-sdk components directly from a server page. Their modules already include `"use client"`:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
13
|
+
|
|
14
|
+
function Page() {
|
|
15
|
+
return <Toggle defaultPressed>Bold</Toggle>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Page }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The server sends a button with `aria-pressed="true"`. It can toggle after hydration. Native HTML behavior, such as submitting a form, can work before hydration.
|
|
22
|
+
|
|
23
|
+
## Wrappers in a Server Component
|
|
24
|
+
|
|
25
|
+
A wrapper that only renders markup can stay on the server. `renderElement` lets it support the same `render` prop as other parts:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { renderElement, type PartProps } from "@ui-sdk/react/primitive"
|
|
29
|
+
|
|
30
|
+
function Card({ render, ...props }: PartProps<"div">) {
|
|
31
|
+
return renderElement("div", { ...props, "data-card": "" }, render)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Page() {
|
|
35
|
+
return (
|
|
36
|
+
<Card render={<section />} className="card">
|
|
37
|
+
Your plan renews on May 1.
|
|
38
|
+
</Card>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { Card, Page }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The server renders a `section` with the card’s props.
|
|
46
|
+
|
|
47
|
+
Put custom parts built with `usePrimitive`, `usePart`, or `createParts` in a client module. A Server Component can then render those exported parts, just like `Toggle` above.
|
|
48
|
+
|
|
49
|
+
## Add Your Own Client Boundary
|
|
50
|
+
|
|
51
|
+
If your component owns React state, callbacks, or render functions, put `"use client"` at the top of its entry file:
|
|
52
|
+
|
|
53
|
+
```tsx title="mute-button.tsx"
|
|
54
|
+
"use client"
|
|
55
|
+
|
|
56
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
57
|
+
import { useState } from "react"
|
|
58
|
+
|
|
59
|
+
function MuteButton() {
|
|
60
|
+
const [muted, setMuted] = useState(false)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<Toggle pressed={muted} onPressedChange={setMuted}>
|
|
65
|
+
Mute
|
|
66
|
+
</Toggle>
|
|
67
|
+
<output>{muted ? "Sound is off" : "Sound is on"}</output>
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { MuteButton }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Render `MuteButton` from your server page. Clicking Mute changes its pressed state and the output; the button keeps the same accessible name.
|
|
76
|
+
|
|
77
|
+
Keep ordinary callbacks and render functions inside the client boundary. They cannot cross from a Server Component as props.
|
|
78
|
+
|
|
79
|
+
## Saved Preferences and Storage
|
|
80
|
+
|
|
81
|
+
Preferences like a color scheme or an emoji skin tone are saved in the browser, so the server can't render them. [Preferences](./core.md#preferences) covers the preferences themselves; this is how they fit server rendering.
|
|
82
|
+
|
|
83
|
+
### Show Them Before Hydration
|
|
84
|
+
|
|
85
|
+
Render `Preferences` at the top of `<body>`. On the server it renders one inline script as its first child, which puts every saved value on its `data-*` attribute as the page loads, before React hydrates, so CSS that selects those attributes is right from the first paint. Preferences hold functions, which can't cross from a Server Component as props, so give them to `Preferences` in a client module:
|
|
86
|
+
|
|
87
|
+
```tsx title="app-preferences.tsx"
|
|
88
|
+
"use client"
|
|
89
|
+
|
|
90
|
+
import { Preferences } from "@ui-sdk/react/preferences"
|
|
91
|
+
import { colorScheme, skinTone } from "./preferences"
|
|
92
|
+
|
|
93
|
+
function AppPreferences({ children }: { children: React.ReactNode }) {
|
|
94
|
+
return (
|
|
95
|
+
<Preferences preferences={[colorScheme, skinTone]}>{children}</Preferences>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { AppPreferences }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```tsx title="root.tsx"
|
|
103
|
+
import { AppPreferences } from "./app-preferences"
|
|
104
|
+
|
|
105
|
+
function Root({ children }: { children: React.ReactNode }) {
|
|
106
|
+
return (
|
|
107
|
+
<html lang="en" suppressHydrationWarning>
|
|
108
|
+
<body>
|
|
109
|
+
<AppPreferences>{children}</AppPreferences>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { Root }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A preference made with `global: true` goes on `<html>`, which the script sets before hydration, so `<html>` needs `suppressHydrationWarning`. Pass `nonce` to `Preferences` if your Content Security Policy only allows inline scripts with one.
|
|
119
|
+
|
|
120
|
+
Content that reads a preference through `usePreference` renders the default on the server and the saved value right after hydration. Style by the attribute instead wherever the first paint has to be right, as [Style by Preferences](./core.md#style-by-preferences) shows.
|
|
121
|
+
|
|
122
|
+
### Read a Cookie on the Server
|
|
123
|
+
|
|
124
|
+
A preference kept in a cookie can render on the server too, content included. Read the request's `Cookie` header on the server, however your server or framework gives you the request, and pass it to your root as a prop, like `<Root cookie={request.headers.get("cookie")}>`. A preference made from a request belongs to that request, so create it in the client boundary, once per render of the app, and share it through context:
|
|
125
|
+
|
|
126
|
+
```tsx title="root.tsx"
|
|
127
|
+
import { AppPreferences } from "./app-preferences"
|
|
128
|
+
|
|
129
|
+
function Root({
|
|
130
|
+
cookie,
|
|
131
|
+
children,
|
|
132
|
+
}: {
|
|
133
|
+
cookie: string | null
|
|
134
|
+
children: React.ReactNode
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<html lang="en" suppressHydrationWarning>
|
|
138
|
+
<body>
|
|
139
|
+
<AppPreferences cookie={cookie}>{children}</AppPreferences>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { Root }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```tsx title="app-preferences.tsx"
|
|
149
|
+
"use client"
|
|
150
|
+
|
|
151
|
+
import { createCookieStorage, createPreference } from "@ui-sdk/core"
|
|
152
|
+
import { Preferences } from "@ui-sdk/react/preferences"
|
|
153
|
+
import { createContext, useState } from "react"
|
|
154
|
+
|
|
155
|
+
const createSkinTone = (cookie: string | null) =>
|
|
156
|
+
createPreference({
|
|
157
|
+
values: ["default", "light", "medium", "dark"],
|
|
158
|
+
defaultValue: "default",
|
|
159
|
+
attribute: "data-skin-tone",
|
|
160
|
+
storage: createCookieStorage("skin-tone", { cookie }),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const SkinToneContext = createContext<ReturnType<typeof createSkinTone> | null>(
|
|
164
|
+
null
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
function AppPreferences({
|
|
168
|
+
cookie,
|
|
169
|
+
children,
|
|
170
|
+
}: {
|
|
171
|
+
cookie: string | null
|
|
172
|
+
children: React.ReactNode
|
|
173
|
+
}) {
|
|
174
|
+
const [skinTone] = useState(() => createSkinTone(cookie))
|
|
175
|
+
return (
|
|
176
|
+
<SkinToneContext value={skinTone}>
|
|
177
|
+
<Preferences preferences={[skinTone]}>{children}</Preferences>
|
|
178
|
+
</SkinToneContext>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { AppPreferences, SkinToneContext }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The server reads the value from `cookie`, and the browser from `document.cookie`, so both render it and hydration matches. `usePreference(skinTone)` returns the saved value from the first render.
|
|
186
|
+
|
|
187
|
+
### Render the Storage Script Yourself
|
|
188
|
+
|
|
189
|
+
`Preferences` is the usual way. For one component that keeps its own value, like a toolbar's density, a storage's `script` returns the same kind of inline script for a single element. Render it right after the element, since it writes to the element right before it:
|
|
190
|
+
|
|
191
|
+
```tsx title="toolbar.tsx"
|
|
192
|
+
"use client"
|
|
193
|
+
|
|
194
|
+
import { createLocalStorage } from "@ui-sdk/core"
|
|
195
|
+
import { useEffect, useState, useSyncExternalStore } from "react"
|
|
196
|
+
|
|
197
|
+
const density = createLocalStorage("density")
|
|
198
|
+
const script = density.script({
|
|
199
|
+
attribute: "data-density",
|
|
200
|
+
values: ["comfortable", "compact"],
|
|
201
|
+
})
|
|
202
|
+
const noSubscribe = () => () => {}
|
|
203
|
+
|
|
204
|
+
function Toolbar({ children }: { children: React.ReactNode }) {
|
|
205
|
+
const [value, setValue] = useState("comfortable")
|
|
206
|
+
// True on the server and while hydrating, when the script is in the HTML.
|
|
207
|
+
const fromServer = useSyncExternalStore(
|
|
208
|
+
noSubscribe,
|
|
209
|
+
() => false,
|
|
210
|
+
() => true
|
|
211
|
+
)
|
|
212
|
+
useEffect(
|
|
213
|
+
() => setValue(density.get() === "compact" ? "compact" : "comfortable"),
|
|
214
|
+
[]
|
|
215
|
+
)
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<div data-density={value} suppressHydrationWarning>
|
|
219
|
+
{children}
|
|
220
|
+
</div>
|
|
221
|
+
{fromServer && script ? (
|
|
222
|
+
<script dangerouslySetInnerHTML={{ __html: script }} />
|
|
223
|
+
) : null}
|
|
224
|
+
</>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { Toolbar }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- **Only from the server.** The browser never runs a script React inserts, and React warns about one, so leave it out of a render in the browser only. `fromServer` is true on the server and while hydrating, and false after.
|
|
232
|
+
- **The element differs until hydration.** The script changes its attribute before React hydrates, so `suppressHydrationWarning` goes on that element. After hydration, render the saved value from state, as above.
|
|
233
|
+
- **Shared by ID.** Every script with the same storage kind and ID reads the storage once, however many elements render it, and the scripts are identical, so compression shrinks the repeats.
|
|
234
|
+
- **Only safe values.** Any script on your site can write the saved value, so `script` only sets attributes that start with `data-`, and only to one of `values`.
|
|
235
|
+
- **Not for cookies.** A cookie storage's `script` returns undefined, since the server reads the cookie and renders the value itself.
|
|
236
|
+
|
|
237
|
+
To run before the page's content at all, render `preferencesScript([...])` from `@ui-sdk/core` in `<head>` yourself. Only global preferences make sense there, since the others go on the script's parent element.
|
|
238
|
+
|
|
239
|
+
## Keep the First Render Consistent
|
|
240
|
+
|
|
241
|
+
Use the same initial values on the server and client. Read browser-only values in an effect or event handler, after hydration.
|
|
242
|
+
|
|
243
|
+
### IDs
|
|
244
|
+
|
|
245
|
+
Parts generate IDs for their relationships, such as a Dialog trigger’s `aria-controls` and a Field label’s `for`. You usually do not need to supply them.
|
|
246
|
+
|
|
247
|
+
Pass an explicit `id` when something outside the component needs to refer to a part. On the server, render order matters: if a Field label renders before a control with your ID, the label initially uses the generated ID. Hydration updates the reference to yours. The same applies to Dialog triggers, titles, and descriptions.
|
|
248
|
+
|
|
249
|
+
Keep a part with your ID in the same Suspense boundary as the parts that reference it so they hydrate together. Pass the ID to the part or its `render` element; an ID assigned inside a render function cannot be read ahead and is picked up during hydration.
|
|
250
|
+
|
|
251
|
+
Use classes or data attributes for styling, rather than generated IDs.
|
|
252
|
+
|
|
253
|
+
Popups render in place, so they don't need a portal root in your server layout. [Popups](./popups.md) covers their placement and containers; [Forms](./forms.md) covers submission and validation.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# State and Events
|
|
2
|
+
|
|
3
|
+
Set an initial value, control it from React, and respond to changes.
|
|
4
|
+
|
|
5
|
+
Components manage their own state unless you pass a controlled value. A change callback lets you respond in either case.
|
|
6
|
+
|
|
7
|
+
## Set the Starting Value
|
|
8
|
+
|
|
9
|
+
Use a default prop to choose the starting state. Here, Bold starts pressed and changes when you click it:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
13
|
+
|
|
14
|
+
function Example() {
|
|
15
|
+
return <Toggle defaultPressed>Bold</Toggle>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Example }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The prop depends on the component: `defaultPressed`, `defaultChecked`, `defaultOpen`, or `defaultValue`.
|
|
22
|
+
|
|
23
|
+
## Control the Value
|
|
24
|
+
|
|
25
|
+
Pass the value and its change callback to keep it in React state:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
"use client"
|
|
29
|
+
|
|
30
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
31
|
+
import { useState } from "react"
|
|
32
|
+
|
|
33
|
+
function Example() {
|
|
34
|
+
const [pressed, setPressed] = useState(false)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Toggle pressed={pressed} onPressedChange={setPressed}>
|
|
39
|
+
Bold
|
|
40
|
+
</Toggle>
|
|
41
|
+
<output>{pressed ? "On" : "Off"}</output>
|
|
42
|
+
</>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Example }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Other components use pairs such as `open` and `onOpenChange`, or `value` and `onValueChange`. The component requests the next value; your callback updates the state you pass back. If you keep passing `false`, the toggle stays off.
|
|
50
|
+
|
|
51
|
+
To keep a value in the URL, still update your state in the callback, then navigate. A value that changes only when the navigation finishes leaves the component showing the old one until then, and logs a warning in development.
|
|
52
|
+
|
|
53
|
+
## Understand a Change
|
|
54
|
+
|
|
55
|
+
The first callback argument is the requested value. The second, `details`, tells you where the request came from and lets you cancel it:
|
|
56
|
+
|
|
57
|
+
| Detail | What It Tells You |
|
|
58
|
+
| ---------- | ----------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `reason` | What requested the change, such as `trigger-press` or `escape-key`. Each component lists its reasons. |
|
|
60
|
+
| `event` | The DOM event, when the change came from one. |
|
|
61
|
+
| `source` | `keyboard`, `mouse`, `touch`, `pen`, `virtual`, `programmatic`, or `agent`. |
|
|
62
|
+
| `cancel()` | Stops the requested change. |
|
|
63
|
+
|
|
64
|
+
For example, keep a popover open when someone clicks outside it:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
"use client"
|
|
68
|
+
|
|
69
|
+
import {
|
|
70
|
+
Popover,
|
|
71
|
+
PopoverClose,
|
|
72
|
+
PopoverContent,
|
|
73
|
+
PopoverTitle,
|
|
74
|
+
PopoverTrigger,
|
|
75
|
+
} from "@ui-sdk/react/popover"
|
|
76
|
+
|
|
77
|
+
function Example() {
|
|
78
|
+
return (
|
|
79
|
+
<Popover
|
|
80
|
+
onOpenChange={(open, details) => {
|
|
81
|
+
if (!open && details.reason === "outside-press") {
|
|
82
|
+
details.cancel()
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<PopoverTrigger>Details</PopoverTrigger>
|
|
87
|
+
<PopoverContent>
|
|
88
|
+
<PopoverTitle>Project Details</PopoverTitle>
|
|
89
|
+
<p>Last updated today.</p>
|
|
90
|
+
<PopoverClose>Done</PopoverClose>
|
|
91
|
+
</PopoverContent>
|
|
92
|
+
</Popover>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { Example }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The Done button and Escape still close the popover. Cancellation works for controlled and uncontrolled state. Add `debug` to a root to log changes and their reasons while you work.
|
|
100
|
+
|
|
101
|
+
## Empty and Multiple Values
|
|
102
|
+
|
|
103
|
+
Select, Tabs, and Radio Group report `null` when nothing is selected. A multiple Select or Combobox uses an array. Accordion and Toggle Group always use arrays.
|
|
104
|
+
|
|
105
|
+
To change a component from outside its React tree, use a [Controller](./controllers.md).
|