@stealthscale/component-disclosure 0.1.0 → 0.2.3
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/README.md +759 -284
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/index.d.ts +1701 -513
- package/dist/index.js +4781 -1429
- package/dist/recipe-95yCB2V3.js +2130 -0
- package/dist/theme.js +39 -21
- package/package.json +42 -33
- package/dist/recipe-BdO9ZFWU.js +0 -708
package/README.md
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
# @stealthscale/component-disclosure
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
React components that show and hide content, styled by the theme's recipes.
|
|
4
|
+
|
|
5
|
+
| Component | Renders |
|
|
6
|
+
| ------------- | --------------------------------------------------------------- |
|
|
7
|
+
| `Collapsible` | A trigger that shows and hides the content beneath it |
|
|
8
|
+
| `Details` | A native `details` element the browser opens and closes |
|
|
9
|
+
| `Accordion` | A column of headings whose triggers show and hide their content |
|
|
10
|
+
| `Tabs` | A list of tabs and the panel of the selected tab |
|
|
11
|
+
| `Steps` | A flow in steps, the current step's content and its buttons |
|
|
12
|
+
| `Popover` | A panel anchored to its trigger |
|
|
13
|
+
| `Tooltip` | A short label shown on hover and focus |
|
|
14
|
+
| `Truncate` | Text clipped to its lines, with the whole text in a tooltip |
|
|
15
|
+
| `ToggleTip` | A short note beside its trigger, opened by a press |
|
|
16
|
+
| `HoverCard` | A card beside a link that previews what the link leads to |
|
|
17
|
+
| `Menu` | A list of actions and options opened from a trigger |
|
|
18
|
+
| `Menubar` | A row of menus along the top of an application |
|
|
19
|
+
|
|
20
|
+
Every value a theme can change is an axis of a component's recipe. Set it as a prop, and write no
|
|
21
|
+
style. Change the element a component renders with `as`. A component with parts is exported as a
|
|
22
|
+
namespace, such as `Menu.Root`.
|
|
9
23
|
|
|
10
24
|
## Install
|
|
11
25
|
|
|
@@ -13,159 +27,387 @@ prop and writes no style. A caller changes the element a component draws with `a
|
|
|
13
27
|
pnpm add @stealthscale/component-disclosure
|
|
14
28
|
```
|
|
15
29
|
|
|
16
|
-
The package peers on `react`, `@stealthscale/hooks
|
|
17
|
-
|
|
18
|
-
compiler installs.
|
|
30
|
+
The package peers on `react`, `@stealthscale/hooks`, `@stealthscale/theme`,
|
|
31
|
+
`@stealthscale/component-a11y` and `@stealthscale/component-primitives`, and depends on the Zag
|
|
32
|
+
state machines. List the preset under `./theme` among the presets your compiler installs.
|
|
19
33
|
|
|
20
34
|
## Collapsible
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
the block.
|
|
36
|
+
`Collapsible` renders a trigger that shows and hides the content beneath it.
|
|
24
37
|
|
|
25
38
|
```tsx
|
|
39
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
40
|
+
|
|
26
41
|
import { Collapsible } from "@stealthscale/component-disclosure";
|
|
27
42
|
|
|
28
43
|
<Collapsible.Root variant="outline">
|
|
29
44
|
<Collapsible.Trigger>
|
|
30
45
|
Delivery details
|
|
31
46
|
<Collapsible.Indicator>
|
|
32
|
-
<
|
|
47
|
+
<ChevronDownIcon size="100%" />
|
|
33
48
|
</Collapsible.Indicator>
|
|
34
49
|
</Collapsible.Trigger>
|
|
35
50
|
<Collapsible.Content>Arrives Thursday.</Collapsible.Content>
|
|
36
51
|
</Collapsible.Root>;
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
| Axis | Values
|
|
40
|
-
| --------- |
|
|
41
|
-
| `variant` | `
|
|
42
|
-
| `size` | `xs
|
|
43
|
-
| `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
| Axis | Values | Default |
|
|
55
|
+
| --------- | ----------------------------------------------------------------- | --------- |
|
|
56
|
+
| `variant` | `subtle`, `surface`, `outline`, `plain` | `plain` |
|
|
57
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
58
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
59
|
+
| `motion` | `slide`, `fade`, `none` | `slide` |
|
|
60
|
+
|
|
61
|
+
| Part | Element | What it renders |
|
|
62
|
+
| ----------- | -------- | --------------------------------------------- |
|
|
63
|
+
| `Root` | `div` | The root, and the machine its parts share |
|
|
64
|
+
| `Trigger` | `button` | The button that shows and hides the content |
|
|
65
|
+
| `Content` | `div` | The content |
|
|
66
|
+
| `Indicator` | `span` | The mark that turns while the content is open |
|
|
67
|
+
|
|
68
|
+
- The root takes the machine's settings: `open`, `defaultOpen`, `onOpenChange`, `disabled`,
|
|
69
|
+
`collapsedHeight`, `onExitComplete`, `id` and `dir`.
|
|
70
|
+
- `collapsedHeight` keeps part of the closed content visible, such as `"2lh"` for two lines.
|
|
71
|
+
- Pass `id` to the root for a stable id in tests or server rendering. The machine derives the
|
|
72
|
+
trigger's `aria-controls` from it, so the root takes no element `id`.
|
|
73
|
+
- The machine sets `aria-expanded` and `aria-controls` on the trigger, and `hidden` on closed
|
|
74
|
+
content, so a control inside closed content leaves the tab order.
|
|
75
|
+
- The indicator sets `aria-hidden` and turns 180 degrees while the content is open. Reduced motion
|
|
76
|
+
removes the transition.
|
|
77
|
+
- Content that starts open does not animate in, because the machine sets its open state from the
|
|
78
|
+
first animation frame.
|
|
79
|
+
- The size sets the trigger on the control scale, so its height matches a button of the same size.
|
|
80
|
+
The palette tints the subtle, surface and outline looks.
|
|
81
|
+
|
|
82
|
+
## Details
|
|
83
|
+
|
|
84
|
+
`Details` renders a native `details` element, which the browser opens and closes without a script.
|
|
64
85
|
|
|
65
86
|
```tsx
|
|
66
|
-
|
|
87
|
+
import { ChevronRightIcon } from "lucide-react";
|
|
88
|
+
|
|
89
|
+
import { Details } from "@stealthscale/component-disclosure";
|
|
90
|
+
|
|
91
|
+
<Details.Root>
|
|
92
|
+
<Details.Summary>
|
|
93
|
+
<Details.Indicator>
|
|
94
|
+
<ChevronRightIcon />
|
|
95
|
+
</Details.Indicator>
|
|
96
|
+
How long does a refund take?
|
|
97
|
+
</Details.Summary>
|
|
98
|
+
<Details.Content>A refund reaches the card within 5 to 10 business days.</Details.Content>
|
|
99
|
+
</Details.Root>;
|
|
67
100
|
```
|
|
68
101
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
| Axis | Values | Default |
|
|
103
|
+
| --------- | ----------------------------------------------------------------- | --------- |
|
|
104
|
+
| `variant` | `subtle`, `surface`, `outline`, `plain` | `outline` |
|
|
105
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
106
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
107
|
+
|
|
108
|
+
| Part | Element | What it renders |
|
|
109
|
+
| ----------- | --------- | --------------------------------------------- |
|
|
110
|
+
| `Root` | `details` | The root, which the browser opens |
|
|
111
|
+
| `Summary` | `summary` | The line that names it and opens it |
|
|
112
|
+
| `Indicator` | `span` | The mark that turns while the details is open |
|
|
113
|
+
| `Content` | `div` | The content |
|
|
114
|
+
|
|
115
|
+
- The root takes the element's own props. `open` sets the state on the first render and whenever it
|
|
116
|
+
changes, `onToggle` reports every change, and details with the same `name` form an exclusive
|
|
117
|
+
group, in which opening one closes the others.
|
|
118
|
+
- The browser gives the summary its role, its name from its text and its expanded state, and opens
|
|
119
|
+
it on a press, Enter and Space. It also opens a closed details to show a fragment the address
|
|
120
|
+
points into.
|
|
121
|
+
- The indicator sets `aria-hidden`, goes before the words and turns a quarter clockwise while the
|
|
122
|
+
details is open, a quarter the other way under `dir="rtl"`. Pass a chevron that points to the end.
|
|
123
|
+
An `svg` in the indicator fills it, so the glyph scales with the size.
|
|
124
|
+
- The looks and the palette are the collapsible's. A one-line summary is a collapsible trigger's
|
|
125
|
+
height at the same size, and a longer one grows by its lines. The plain look draws no box and pads
|
|
126
|
+
no side, so its summary and content line up with the text around them. Under forced colors the
|
|
127
|
+
subtle box takes a `CanvasText` outline.
|
|
128
|
+
- Use `Collapsible` for open state the application controls or animates. `Details` has no
|
|
129
|
+
`onOpenChange` and no motion.
|
|
130
|
+
|
|
131
|
+
## Accordion
|
|
132
|
+
|
|
133
|
+
`Accordion` renders a column of headings. Each heading contains a trigger that shows and hides the
|
|
134
|
+
content under it.
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
136
|
+
```tsx
|
|
137
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
138
|
+
|
|
139
|
+
import { Accordion } from "@stealthscale/component-disclosure";
|
|
140
|
+
|
|
141
|
+
<Accordion.Root collapsible defaultValue={["delivery"]}>
|
|
142
|
+
<Accordion.Item value="delivery">
|
|
143
|
+
<Accordion.ItemHeading>
|
|
144
|
+
<Accordion.ItemTrigger>
|
|
145
|
+
How long does delivery take?
|
|
146
|
+
<Accordion.ItemIndicator>
|
|
147
|
+
<ChevronDownIcon size="100%" />
|
|
148
|
+
</Accordion.ItemIndicator>
|
|
149
|
+
</Accordion.ItemTrigger>
|
|
150
|
+
</Accordion.ItemHeading>
|
|
151
|
+
<Accordion.ItemContent>
|
|
152
|
+
<Accordion.ItemBody>Orders arrive in one to three working days.</Accordion.ItemBody>
|
|
153
|
+
</Accordion.ItemContent>
|
|
154
|
+
</Accordion.Item>
|
|
155
|
+
</Accordion.Root>;
|
|
156
|
+
```
|
|
87
157
|
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
| Axis | Values | Default |
|
|
159
|
+
| --------- | ----------------------------------------------------------------- | --------- |
|
|
160
|
+
| `variant` | `subtle`, `surface`, `outline`, `flushed`, `plain` | `flushed` |
|
|
161
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
162
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
163
|
+
| `motion` | `slide`, `fade`, `none` | `slide` |
|
|
164
|
+
|
|
165
|
+
| Part | Element | What it renders |
|
|
166
|
+
| --------------- | -------- | --------------------------------------------------- |
|
|
167
|
+
| `Root` | `div` | The root, and the machine its items share |
|
|
168
|
+
| `Item` | `div` | One item, and the machine that animates its content |
|
|
169
|
+
| `ItemHeading` | `h3` | The heading that contains the trigger |
|
|
170
|
+
| `ItemTrigger` | `button` | The button that opens and closes the item |
|
|
171
|
+
| `ItemIndicator` | `span` | The mark that turns while the item is open |
|
|
172
|
+
| `ItemContent` | `div` | The region the trigger shows and hides |
|
|
173
|
+
| `ItemBody` | `div` | The padded block inside the content |
|
|
174
|
+
|
|
175
|
+
- The root takes the machine's settings: `value`, `defaultValue`, `onValueChange`, `multiple`,
|
|
176
|
+
`collapsible`, `disabled`, `onFocusChange`, `ids`, `id` and `dir`. `value` is an array of the
|
|
177
|
+
values of the open items.
|
|
178
|
+
- One item is open at a time. `multiple` keeps the other items open when one opens. `collapsible`
|
|
179
|
+
lets a press close the open item, so that no item is open.
|
|
180
|
+
- The trigger sets `aria-expanded` and `aria-controls`. The content is a `region` named by its
|
|
181
|
+
trigger and is `hidden` while closed, so a control inside a closed item leaves the tab order.
|
|
182
|
+
- ArrowDown and ArrowUp move focus to the next and the previous trigger, and Home and End move it to
|
|
183
|
+
the first and the last. The keys skip a disabled trigger.
|
|
184
|
+
- `disabled` on the root disables every item, whatever an item sets. `disabled` on an item disables
|
|
185
|
+
that item alone.
|
|
186
|
+
- An item's value may contain a space. The IDs built from it are percent-encoded.
|
|
187
|
+
- The heading is an `h3`. Pass another level through `as` to fit the page's outline.
|
|
188
|
+
- A button after the trigger inside the heading, such as a download action, renders at the end of
|
|
189
|
+
the row and outside the trigger. The heading's accessible name then includes the button's name.
|
|
190
|
+
- The content animates to the height the machine measures. `ItemBody` has the padding, so put the
|
|
191
|
+
words inside it. An item that starts open does not animate in, and reduced motion removes the
|
|
192
|
+
animation.
|
|
193
|
+
- The trigger is at least a control's height and grows with a title that wraps. It sizes a leading
|
|
194
|
+
`svg` one icon size smaller than the indicator.
|
|
195
|
+
- The subtle, surface and outline looks inset the trigger and the body from the box's edges. The
|
|
196
|
+
flushed and plain looks align both with the text around the accordion.
|
|
197
|
+
- The machine's `orientation` is not offered, because the recipe lays the items out in a column.
|
|
198
|
+
Closed content remains in the document under `hidden`.
|
|
90
199
|
|
|
91
200
|
## Tabs
|
|
92
201
|
|
|
93
|
-
|
|
94
|
-
and a panel for each control.
|
|
202
|
+
`Tabs` renders a list of tabs and the panel of the selected tab. A person can close a closable tab.
|
|
95
203
|
|
|
96
204
|
```tsx
|
|
97
205
|
import { Tabs } from "@stealthscale/component-disclosure";
|
|
98
206
|
|
|
99
207
|
<Tabs.Root defaultValue="overview">
|
|
100
|
-
<Tabs.List>
|
|
208
|
+
<Tabs.List aria-label="Account">
|
|
101
209
|
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
102
210
|
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
|
103
211
|
<Tabs.Indicator />
|
|
104
212
|
</Tabs.List>
|
|
105
|
-
<Tabs.Content value="overview">
|
|
106
|
-
<Tabs.Content value="activity">
|
|
213
|
+
<Tabs.Content value="overview">The balance and the owners of the account.</Tabs.Content>
|
|
214
|
+
<Tabs.Content value="activity">The payments of the last thirty days.</Tabs.Content>
|
|
215
|
+
</Tabs.Root>;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Axis | Values | Default |
|
|
219
|
+
| --------- | ----------------------------------------------------------------- | ------- |
|
|
220
|
+
| `variant` | `line`, `enclosed`, `subtle`, `plain` | `line` |
|
|
221
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
222
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | none |
|
|
223
|
+
| `justify` | `start`, `center`, `end`, `between`, `around`, `evenly` | none |
|
|
224
|
+
| `fitted` | `true` | off |
|
|
225
|
+
|
|
226
|
+
| Part | Element | What it renders |
|
|
227
|
+
| -------------- | -------- | ------------------------------------------------ |
|
|
228
|
+
| `Root` | `div` | The root, and the machine its parts share |
|
|
229
|
+
| `List` | `div` | The list of tabs, with `role="tablist"` |
|
|
230
|
+
| `Trigger` | `button` | One tab, with `role="tab"` |
|
|
231
|
+
| `CloseTrigger` | `span` | The pointer's control that closes a closable tab |
|
|
232
|
+
| `Indicator` | `div` | The indicator of the selected tab |
|
|
233
|
+
| `Content` | `div` | One panel, with `role="tabpanel"` |
|
|
234
|
+
|
|
235
|
+
- A tab and its panel take the same `value`. The machine sets every role, id and ARIA reference.
|
|
236
|
+
Name the list with `aria-label` on `Tabs.List`.
|
|
237
|
+
- The root takes the machine's settings: `value`, `defaultValue`, `onValueChange`, `orientation`,
|
|
238
|
+
`activationMode`, `loopFocus`, `deselectable` and `id`, and `onClose`.
|
|
239
|
+
- `orientation` is the machine's option, not an axis. When it is `vertical`, the list turns into a
|
|
240
|
+
column, the indicator moves to the inline start and the up and down arrows move between tabs. A
|
|
241
|
+
vertical tab starts its words at its inline start.
|
|
242
|
+
- `activationMode="automatic"`, the default, selects a tab on focus. `manual` moves focus with the
|
|
243
|
+
arrows and selects on a press, for panels that are slow to render.
|
|
244
|
+
- Only the selected tab is in the tab order. A panel takes `tabIndex={0}`, so Tab from the list
|
|
245
|
+
lands on the selected panel, and a panel that is not selected is `hidden`.
|
|
246
|
+
- The machine measures the selected tab and positions the indicator. The indicator is `hidden` until
|
|
247
|
+
the first measurement, and measures again whenever a tab enters or leaves the list.
|
|
248
|
+
- `fitted` shares the list's width equally between the tabs. `justify` distributes the tabs in a
|
|
249
|
+
list wider than they are.
|
|
250
|
+
- The palette colours the line indicator, the subtle indicator and the selected tab's text. Under
|
|
251
|
+
forced colors the line and plain looks mark the selected tab with a `Highlight` bar, and the
|
|
252
|
+
subtle look outlines it in `Highlight`.
|
|
253
|
+
- An `svg` placed directly in a tab is one text size.
|
|
254
|
+
|
|
255
|
+
Close tabs from a list you keep, and scroll the list where it can be wider than its room:
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { XIcon } from "lucide-react";
|
|
259
|
+
|
|
260
|
+
import { Tabs } from "@stealthscale/component-disclosure";
|
|
261
|
+
import { ScrollArea } from "@stealthscale/component-primitives";
|
|
262
|
+
|
|
263
|
+
<Tabs.Root defaultValue="build.yml" onClose={({ value }) => close(value)}>
|
|
264
|
+
<ScrollArea.Root scrolls="horizontal">
|
|
265
|
+
<ScrollArea.Viewport focusable={false}>
|
|
266
|
+
<ScrollArea.Content>
|
|
267
|
+
<Tabs.List aria-label="Open files">
|
|
268
|
+
{files.map((file) => (
|
|
269
|
+
<Tabs.Trigger closable key={file} value={file}>
|
|
270
|
+
{file}
|
|
271
|
+
<Tabs.CloseTrigger>
|
|
272
|
+
<XIcon />
|
|
273
|
+
</Tabs.CloseTrigger>
|
|
274
|
+
</Tabs.Trigger>
|
|
275
|
+
))}
|
|
276
|
+
<Tabs.Indicator />
|
|
277
|
+
</Tabs.List>
|
|
278
|
+
</ScrollArea.Content>
|
|
279
|
+
</ScrollArea.Viewport>
|
|
280
|
+
<ScrollArea.Scrollbar orientation="horizontal" />
|
|
281
|
+
</ScrollArea.Root>
|
|
282
|
+
{files.map((file) => (
|
|
283
|
+
<Tabs.Content key={file} value={file}>
|
|
284
|
+
{file}
|
|
285
|
+
</Tabs.Content>
|
|
286
|
+
))}
|
|
107
287
|
</Tabs.Root>;
|
|
108
288
|
```
|
|
109
289
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
290
|
+
- `closable` on a `Tabs.Trigger` lets a person close the tab: Delete while it has focus, a press
|
|
291
|
+
with the middle button, or a press on its `Tabs.CloseTrigger`. The tab states
|
|
292
|
+
`aria-keyshortcuts="Delete"`. A disabled tab does not close.
|
|
293
|
+
- `onClose` on the root receives `{ value }`, typed `CloseDetails`. Remove the tab and its panel
|
|
294
|
+
there, because the root does not remove them.
|
|
295
|
+
- When the selected tab closes, the next enabled tab is selected, else the previous one, else none.
|
|
296
|
+
`onValueChange` runs before `onClose`. A close of another tab keeps the selection.
|
|
297
|
+
- When the closing tab has focus, focus moves to the next enabled tab, else the previous one. When
|
|
298
|
+
the last tab closes, focus falls to the document's body, so move it to what replaces the list.
|
|
299
|
+
- `Tabs.CloseTrigger` renders your glyph in a `span` hidden from assistive technology, titled
|
|
300
|
+
"Close" unless you pass `label`. It is at least 24px square at the default scale. A press on it
|
|
301
|
+
neither focuses nor selects its tab.
|
|
302
|
+
- `Tabs.selectionAfterClose(values, selected, closed)` returns the value to select after a close.
|
|
303
|
+
Use it to close a tab from outside the list, such as from a button or a menu.
|
|
304
|
+
- Compose the list in the primitives package's horizontal `ScrollArea` when the tabs can be wider
|
|
305
|
+
than their room. Pass `focusable={false}` to the viewport, because the tabs take focus. The
|
|
306
|
+
selected tab scrolls into view inside the scroll area on every selection, and the page does not
|
|
307
|
+
scroll.
|
|
308
|
+
|
|
309
|
+
## Steps
|
|
310
|
+
|
|
311
|
+
`Steps` renders a flow in steps: an ordered list of the steps, the content of the current step and
|
|
312
|
+
the buttons that move between steps.
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
import { CheckIcon } from "lucide-react";
|
|
316
|
+
|
|
317
|
+
import { Button } from "@stealthscale/component-actions";
|
|
318
|
+
import { Steps } from "@stealthscale/component-disclosure";
|
|
319
|
+
|
|
320
|
+
<Steps.Root count={2}>
|
|
321
|
+
<Steps.List>
|
|
322
|
+
<Steps.Item index={0}>
|
|
323
|
+
<Steps.Trigger>
|
|
324
|
+
<Steps.Indicator>
|
|
325
|
+
<Steps.Status complete={<CheckIcon />} />
|
|
326
|
+
</Steps.Indicator>
|
|
327
|
+
<Steps.Title>Account</Steps.Title>
|
|
328
|
+
</Steps.Trigger>
|
|
329
|
+
<Steps.Separator />
|
|
330
|
+
</Steps.Item>
|
|
331
|
+
<Steps.Item index={1}>
|
|
332
|
+
<Steps.Trigger>
|
|
333
|
+
<Steps.Indicator />
|
|
334
|
+
<Steps.Title>Amount</Steps.Title>
|
|
335
|
+
</Steps.Trigger>
|
|
336
|
+
</Steps.Item>
|
|
337
|
+
</Steps.List>
|
|
338
|
+
<Steps.Content index={0}>The account the payout leaves from.</Steps.Content>
|
|
339
|
+
<Steps.Content index={1}>The amount and the invoices it pays.</Steps.Content>
|
|
340
|
+
<Steps.CompletedContent>Payout queued.</Steps.CompletedContent>
|
|
341
|
+
<Steps.PrevTrigger as={Button}>Back</Steps.PrevTrigger>
|
|
342
|
+
<Steps.NextTrigger as={Button}>Next</Steps.NextTrigger>
|
|
343
|
+
</Steps.Root>;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Axis | Values | Default |
|
|
347
|
+
| ---------------- | ----------------------------------------------------------------- | --------- |
|
|
348
|
+
| `variant` | `solid`, `subtle` | `solid` |
|
|
349
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
350
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
351
|
+
| `labelPlacement` | `beside`, `below` | `beside` |
|
|
352
|
+
|
|
353
|
+
| Part | Element | What it renders |
|
|
354
|
+
| ------------------ | -------- | ------------------------------------------------------------- |
|
|
355
|
+
| `Root` | `div` | The root, and the machine its parts share |
|
|
356
|
+
| `List` | `ol` | The ordered list of the steps |
|
|
357
|
+
| `Item` | `li` | One step |
|
|
358
|
+
| `Trigger` | `button` | The button that moves the flow to the step |
|
|
359
|
+
| `Indicator` | `span` | The disc, with the step's number unless it has children |
|
|
360
|
+
| `Status` | none | The node for the step's state, or the step's number |
|
|
361
|
+
| `Title` | `span` | The step's name, after hidden words for its state |
|
|
362
|
+
| `Description` | `span` | The line under the title |
|
|
363
|
+
| `Separator` | `span` | The rule to the next step |
|
|
364
|
+
| `Content` | `div` | The content of one step, shown while the step is current |
|
|
365
|
+
| `CompletedContent` | `div` | The content shown once every step is completed |
|
|
366
|
+
| `NextTrigger` | `button` | The button that moves to the next step, and past the last one |
|
|
367
|
+
| `PrevTrigger` | `button` | The button that moves to the previous step |
|
|
368
|
+
|
|
369
|
+
- The root takes the machine's settings: `count`, `step`, `defaultStep`, `onStepChange`,
|
|
370
|
+
`onStepComplete`, `linear`, `isStepValid`, `isStepSkippable`, `onStepInvalid`, `orientation`,
|
|
371
|
+
`ids`, `id` and `dir`. `step` counts from zero, and `count` marks the flow as completed.
|
|
372
|
+
- The list is an `ol`, and `Title` renders "Completed: " or "Current: " visually hidden before the
|
|
373
|
+
name, as the W3C's tutorial on multi-page forms marks a progress list. Pass other words through
|
|
374
|
+
`completedLabel` and `currentLabel`. Render a `Title` in every item, because it contains the words
|
|
375
|
+
for the state.
|
|
376
|
+
- The machine's tab semantics are dropped: the list is not a `tablist`, a trigger is not a `tab` and
|
|
377
|
+
a content is not a `tabpanel`. Every trigger is in the tab order, and Tab moves between them.
|
|
378
|
+
- A press on a trigger moves to its step. Moving past the current step asks `isStepValid` first, and
|
|
379
|
+
a refusal calls `onStepInvalid`.
|
|
380
|
+
- With `linear`, a trigger moves back to a completed step, and the trigger of a later step is
|
|
381
|
+
disabled.
|
|
382
|
+
- `NextTrigger` and `PrevTrigger` set `aria-disabled` at the ends of the flow in place of
|
|
383
|
+
`disabled`, so the button keeps focus after the press that ended the flow. Pass the library's
|
|
384
|
+
`Button` through `as` for a button's look.
|
|
385
|
+
- `Steps.Status` swaps the disc's number by state. Pass `complete={<CheckIcon />}` for a check on
|
|
386
|
+
completed steps. An item without a trigger shows progress without navigation.
|
|
387
|
+
- `orientation="vertical"` stacks the steps in a column beside the content, with a rule under each
|
|
388
|
+
disc.
|
|
389
|
+
- `labelPlacement="below"` centres each title under its disc and shares the row equally between the
|
|
390
|
+
steps. With `beside`, the list measures its steps at their natural width whenever its size
|
|
391
|
+
changes, and places the titles below the discs while they do not fit.
|
|
392
|
+
- The discs measure 36, 40 and 44px at `sm`, `md` and `lg`. Under forced colors a completed disc
|
|
393
|
+
fills with `Highlight` and the current disc takes a `Highlight` ring.
|
|
394
|
+
- The machine's `getProgressProps` is not offered. Render the feedback package's `Progress` beside
|
|
395
|
+
the steps for a bar.
|
|
156
396
|
|
|
157
397
|
## Popover
|
|
158
398
|
|
|
159
|
-
|
|
399
|
+
`Popover` renders a panel beside its trigger, with a title, a description and a close button.
|
|
160
400
|
|
|
161
401
|
```tsx
|
|
402
|
+
import { ChevronDownIcon, XIcon } from "lucide-react";
|
|
403
|
+
|
|
162
404
|
import { Popover } from "@stealthscale/component-disclosure";
|
|
163
405
|
|
|
164
406
|
<Popover.Root>
|
|
165
407
|
<Popover.Trigger>
|
|
166
408
|
Filters
|
|
167
409
|
<Popover.Indicator>
|
|
168
|
-
<
|
|
410
|
+
<ChevronDownIcon size="1em" />
|
|
169
411
|
</Popover.Indicator>
|
|
170
412
|
</Popover.Trigger>
|
|
171
413
|
<Popover.Positioner>
|
|
@@ -175,58 +417,63 @@ import { Popover } from "@stealthscale/component-disclosure";
|
|
|
175
417
|
</Popover.Arrow>
|
|
176
418
|
<Popover.Title>Filter the list</Popover.Title>
|
|
177
419
|
<Popover.Description>Only rows matching all of these are shown.</Popover.Description>
|
|
178
|
-
<Popover.CloseTrigger>
|
|
179
|
-
<
|
|
420
|
+
<Popover.CloseTrigger aria-label="Close the filters">
|
|
421
|
+
<XIcon />
|
|
180
422
|
</Popover.CloseTrigger>
|
|
181
423
|
</Popover.Content>
|
|
182
424
|
</Popover.Positioner>
|
|
183
425
|
</Popover.Root>;
|
|
184
426
|
```
|
|
185
427
|
|
|
186
|
-
| Axis | Values
|
|
187
|
-
| --------- |
|
|
188
|
-
| `variant` | `surface`, `elevated`, `glass`
|
|
189
|
-
| `size` | `xs
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
|
|
217
|
-
-
|
|
218
|
-
|
|
219
|
-
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
428
|
+
| Axis | Values | Default |
|
|
429
|
+
| --------- | ------------------------------ | --------- |
|
|
430
|
+
| `variant` | `surface`, `elevated`, `glass` | `surface` |
|
|
431
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
432
|
+
|
|
433
|
+
| Part | Element | What it renders |
|
|
434
|
+
| -------------- | -------- | --------------------------------------------------------------- |
|
|
435
|
+
| `Root` | `div` | A `display: contents` element that provides the variants |
|
|
436
|
+
| `Anchor` | `div` | The element the panel is positioned against, if not the trigger |
|
|
437
|
+
| `Trigger` | `button` | The button that opens and closes the panel |
|
|
438
|
+
| `Indicator` | `span` | The mark that turns while the panel is open |
|
|
439
|
+
| `Positioner` | `div` | The element the machine positions beside the trigger |
|
|
440
|
+
| `Content` | `div` | The panel, with `role="dialog"` |
|
|
441
|
+
| `Title` | `h2` | The heading that names the panel |
|
|
442
|
+
| `Description` | `p` | The text that describes the panel |
|
|
443
|
+
| `CloseTrigger` | `button` | The button that closes the panel |
|
|
444
|
+
| `Arrow` | `div` | The element the machine places against the panel's edge |
|
|
445
|
+
| `ArrowTip` | `div` | The arrow's rotated square |
|
|
446
|
+
|
|
447
|
+
- The root takes the machine's settings: `open`, `defaultOpen`, `onOpenChange`, `modal`,
|
|
448
|
+
`autoFocus`, `initialFocusEl`, `finalFocusEl`, `closeOnEscape`, `closeOnInteractOutside`,
|
|
449
|
+
`persistentElements`, `positioning` and `id`.
|
|
450
|
+
- The panel is not in the document until it first opens, and leaves once its exit animation ends.
|
|
451
|
+
`lazyMount` and `unmountOnExit` are true by default. Pass `false` to keep the closed panel
|
|
452
|
+
rendered under `hidden`. The panel is `inert` while it leaves. `skipAnimationOnMount` and
|
|
453
|
+
`onExitComplete` go to the root as well.
|
|
454
|
+
- The trigger sets `aria-expanded` and `aria-controls`. While a `Title` is mounted the panel sets
|
|
455
|
+
`aria-labelledby` to it, and while a `Description` is mounted `aria-describedby`, in a panel that
|
|
456
|
+
mounts on opening too. A panel without a title takes `aria-label`.
|
|
457
|
+
- The panel takes focus as it opens and returns it to the trigger as it closes. Escape and a press
|
|
458
|
+
outside close it. `modal` traps focus and hides the rest of the page from a screen reader.
|
|
459
|
+
- The close trigger's accessible name is "close" unless you pass `aria-label`.
|
|
460
|
+
- The title is an `h2`. Pass another heading level through `as` to fit the page's outline.
|
|
461
|
+
- The size sets the panel's padding, the title and the description, and the panel is at least as
|
|
462
|
+
wide as the trigger. The title and the description read the body text at the size, the title in
|
|
463
|
+
the semibold weight.
|
|
464
|
+
- A press on the trigger of another popover or menu closes this panel and opens the other. A popover
|
|
465
|
+
opened from inside another popover's panel closes with it.
|
|
466
|
+
- The package does not portal. Wrap `Popover.Positioner` in a portal when an ancestor clips the
|
|
467
|
+
panel.
|
|
223
468
|
|
|
224
469
|
## Tooltip
|
|
225
470
|
|
|
226
|
-
|
|
227
|
-
and the box it opens.
|
|
471
|
+
`Tooltip` renders a short label beside a control, shown on hover and on keyboard focus.
|
|
228
472
|
|
|
229
473
|
```tsx
|
|
474
|
+
import { SaveIcon } from "lucide-react";
|
|
475
|
+
|
|
476
|
+
import { IconButton } from "@stealthscale/component-actions";
|
|
230
477
|
import { Tooltip } from "@stealthscale/component-disclosure";
|
|
231
478
|
|
|
232
479
|
<Tooltip.Root>
|
|
@@ -244,75 +491,231 @@ import { Tooltip } from "@stealthscale/component-disclosure";
|
|
|
244
491
|
</Tooltip.Root>;
|
|
245
492
|
```
|
|
246
493
|
|
|
247
|
-
| Axis | Values
|
|
248
|
-
| --------- |
|
|
249
|
-
| `variant` | `inverted`, `surface`
|
|
250
|
-
| `size` | `xs
|
|
494
|
+
| Axis | Values | Default |
|
|
495
|
+
| --------- | --------------------- | ---------- |
|
|
496
|
+
| `variant` | `inverted`, `surface` | `inverted` |
|
|
497
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
498
|
+
|
|
499
|
+
| Part | Element | What it renders |
|
|
500
|
+
| ------------ | -------- | --------------------------------------------------------- |
|
|
501
|
+
| `Root` | `div` | A `display: contents` element that provides the variants |
|
|
502
|
+
| `Trigger` | `button` | The control the tooltip describes |
|
|
503
|
+
| `Positioner` | `div` | The element the machine positions beside the trigger |
|
|
504
|
+
| `Content` | `div` | The label, with `role="tooltip"` |
|
|
505
|
+
| `Arrow` | `div` | The element the machine places against the content's edge |
|
|
506
|
+
| `ArrowTip` | `div` | The arrow's rotated square |
|
|
507
|
+
|
|
508
|
+
- The root takes the machine's settings: `open`, `defaultOpen`, `onOpenChange`, `openDelay`,
|
|
509
|
+
`closeDelay`, `disabled`, `interactive`, `closeOnClick`, `closeOnEscape`, `closeOnScroll`,
|
|
510
|
+
`closeOnPointerDown`, `positioning` and `id`.
|
|
511
|
+
- The content is not in the document until it first opens, and leaves once its exit animation ends.
|
|
512
|
+
`lazyMount` and `unmountOnExit` are true by default, and the content is `inert` while it leaves.
|
|
513
|
+
- The trigger sets `aria-describedby` to the content's id, so a screen reader reads the label as the
|
|
514
|
+
control's description.
|
|
515
|
+
- Keyboard focus opens the tooltip, and pointer focus does not. Escape, a scroll and a press on the
|
|
516
|
+
control close it.
|
|
517
|
+
- The trigger is a `button` by default, so it takes keyboard focus. Pass another control with `as`.
|
|
518
|
+
- Set `interactive` when the content contains a link, so a pointer can move into it.
|
|
519
|
+
- The content sets `--tooltip-surface`, and the arrow tip reads it.
|
|
520
|
+
- The package does not portal. Wrap the positioner in a portal when an ancestor clips it:
|
|
251
521
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`display: contents`, so it takes part in no layout and a tooltip attached to a control inside a row
|
|
255
|
-
leaves that row as it was.
|
|
522
|
+
```tsx
|
|
523
|
+
import { Portal } from "@stealthscale/component-primitives";
|
|
256
524
|
|
|
257
|
-
|
|
258
|
-
|
|
525
|
+
<Portal>
|
|
526
|
+
<Tooltip.Positioner>…</Tooltip.Positioner>
|
|
527
|
+
</Portal>;
|
|
528
|
+
```
|
|
259
529
|
|
|
260
|
-
|
|
530
|
+
## Truncate
|
|
261
531
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
| `open`, `defaultOpen` | Sets the state from outside, or opens it to begin with |
|
|
265
|
-
| `onOpenChange` | Called each time the box opens or shuts |
|
|
266
|
-
| `openDelay`, `closeDelay` | How long a pointer rests before it opens, and after |
|
|
267
|
-
| `disabled` | Stops it opening at all |
|
|
268
|
-
| `interactive` | Keeps it open while a pointer is inside the box |
|
|
269
|
-
| `closeOnClick`, `closeOnEscape`, `closeOnScroll`, `closeOnPointerDown` | What shuts it |
|
|
270
|
-
| `positioning` | Which side it opens on, and how far from the control |
|
|
271
|
-
| `id` | Names the machine, which builds its ARIA references |
|
|
532
|
+
`Truncate` clips text to a number of lines with an ellipsis, and shows the whole text in a tooltip
|
|
533
|
+
while something is cut off.
|
|
272
534
|
|
|
273
|
-
|
|
274
|
-
|
|
535
|
+
```tsx
|
|
536
|
+
import { Truncate } from "@stealthscale/component-disclosure";
|
|
275
537
|
|
|
276
|
-
|
|
538
|
+
<Truncate>{shipment.note}</Truncate>;
|
|
539
|
+
<Truncate focusable lines={2}>
|
|
540
|
+
{ticket.description}
|
|
541
|
+
</Truncate>;
|
|
542
|
+
```
|
|
277
543
|
|
|
278
|
-
|
|
279
|
-
|
|
544
|
+
| Prop | Values | Default |
|
|
545
|
+
| ----------- | ------------------------ | -------- |
|
|
546
|
+
| `children` | the whole text, a string | required |
|
|
547
|
+
| `lines` | a positive whole number | `1` |
|
|
548
|
+
| `focusable` | `true`, `false` | `false` |
|
|
549
|
+
|
|
550
|
+
- The text is measured whenever it or its parent resizes. While something is cut off the text takes
|
|
551
|
+
`data-truncated` and the tooltip opens on hover. Text that fits renders no tooltip and no tab
|
|
552
|
+
stop.
|
|
553
|
+
- `focusable` gives clipped text a tab stop, and keyboard focus opens the tooltip.
|
|
554
|
+
- A clamp clips the paint and not the document, so a screen reader reads the whole text once. The
|
|
555
|
+
text drops the tooltip's `aria-describedby`, and the tooltip's content is `aria-hidden`.
|
|
556
|
+
- The tooltip opens under the start of the text, in a portal. A scroll does not close it, because a
|
|
557
|
+
Tab onto text below the fold scrolls the page as it opens the tooltip.
|
|
558
|
+
- In a flex row the text shrinks to the room its siblings leave and clips there. A word longer than
|
|
559
|
+
the line breaks and clips as well.
|
|
560
|
+
- The root is a `span`, which is valid inside a paragraph or a table cell. The text is a block box,
|
|
561
|
+
because a clamp does not apply to an inline one.
|
|
562
|
+
- The recipe has no axis. The text takes its size from the text around it, and the focus ring of a
|
|
563
|
+
tab stop is outside the text.
|
|
564
|
+
- A touch screen does not open the tooltip. Where a touch reader needs the whole text, show it in
|
|
565
|
+
full or behind a control.
|
|
566
|
+
|
|
567
|
+
Not offered: an expand control and a clamp at the start of the text.
|
|
568
|
+
|
|
569
|
+
## ToggleTip
|
|
570
|
+
|
|
571
|
+
`ToggleTip` renders a short note beside its trigger, opened and closed by a press. It looks like a
|
|
572
|
+
tooltip and opens like a popover, so it works on a touch screen and can contain a link.
|
|
280
573
|
|
|
281
574
|
```tsx
|
|
575
|
+
import { InfoIcon } from "lucide-react";
|
|
576
|
+
|
|
577
|
+
import { IconButton } from "@stealthscale/component-actions";
|
|
578
|
+
import { ToggleTip } from "@stealthscale/component-disclosure";
|
|
282
579
|
import { Portal } from "@stealthscale/component-primitives";
|
|
283
580
|
|
|
284
|
-
<
|
|
285
|
-
<
|
|
286
|
-
|
|
581
|
+
<ToggleTip.Root>
|
|
582
|
+
<ToggleTip.Trigger as={IconButton} aria-label="About chargebacks">
|
|
583
|
+
<InfoIcon />
|
|
584
|
+
</ToggleTip.Trigger>
|
|
585
|
+
<Portal>
|
|
586
|
+
<ToggleTip.Positioner>
|
|
587
|
+
<ToggleTip.Content>
|
|
588
|
+
<ToggleTip.Arrow>
|
|
589
|
+
<ToggleTip.ArrowTip />
|
|
590
|
+
</ToggleTip.Arrow>
|
|
591
|
+
A payment the cardholder's bank takes back after a dispute.
|
|
592
|
+
</ToggleTip.Content>
|
|
593
|
+
</ToggleTip.Positioner>
|
|
594
|
+
</Portal>
|
|
595
|
+
</ToggleTip.Root>;
|
|
287
596
|
```
|
|
288
597
|
|
|
289
|
-
|
|
290
|
-
|
|
598
|
+
| Axis | Values | Default |
|
|
599
|
+
| --------- | --------------------- | ---------- |
|
|
600
|
+
| `variant` | `inverted`, `surface` | `inverted` |
|
|
601
|
+
| `size` | `xs` to `4xl` | `md` |
|
|
602
|
+
|
|
603
|
+
| Part | Element | What it renders |
|
|
604
|
+
| ------------ | -------- | -------------------------------------------------------- |
|
|
605
|
+
| `Root` | `span` | A `display: contents` element that provides the variants |
|
|
606
|
+
| `Trigger` | `button` | The button that opens and closes the note |
|
|
607
|
+
| `Positioner` | `div` | The element the machine positions beside the trigger |
|
|
608
|
+
| `Content` | `div` | The note |
|
|
609
|
+
| `Arrow` | `div` | The element the machine places against the note's edge |
|
|
610
|
+
| `ArrowTip` | `div` | The arrow's rotated square |
|
|
611
|
+
|
|
612
|
+
- The root takes the popover machine's settings: `open`, `defaultOpen`, `onOpenChange`, `autoFocus`,
|
|
613
|
+
`initialFocusEl`, `finalFocusEl`, `closeOnEscape`, `closeOnInteractOutside`, `positioning` and
|
|
614
|
+
`id`.
|
|
615
|
+
- A press on the trigger opens the note, and a second press closes it. Escape and a press outside
|
|
616
|
+
close it too. Opening another toggle tip, popover or menu closes this note.
|
|
617
|
+
- The trigger keeps focus as the note opens, because `autoFocus` is false by default. The note
|
|
618
|
+
announces its text through the document's polite live region each time it opens, so a screen
|
|
619
|
+
reader user hears it without leaving the trigger.
|
|
620
|
+
- The trigger sets `aria-expanded` and `aria-controls`. The note has no role, because a note is not
|
|
621
|
+
a dialog: the trigger drops the machine's `aria-haspopup="dialog"` and the note its `dialog` role.
|
|
622
|
+
- Tab from the trigger moves into the note while it contains a link, and Tab out of the note closes
|
|
623
|
+
it. A link inside the note takes the note's ink and an underline.
|
|
624
|
+
- The note is not in the document until it first opens, and leaves once its exit animation ends.
|
|
625
|
+
`lazyMount` and `unmountOnExit` are true by default. `skipAnimationOnMount` and `onExitComplete`
|
|
626
|
+
go to the root as well.
|
|
627
|
+
- The note is at most 320px wide at the default scale, and narrower where the room beside the
|
|
628
|
+
trigger is less. The size sets its padding on the inset scale and its text on the label role.
|
|
629
|
+
- Use `Tooltip` for a control's label, `ToggleTip` for a note a reader asks for, and `Popover` for a
|
|
630
|
+
panel with a title and controls.
|
|
631
|
+
- The root is a `span`, which is valid inside a table cell or a heading. Wrap the positioner in a
|
|
632
|
+
portal there, because phrasing content cannot contain its `div`.
|
|
633
|
+
|
|
634
|
+
## HoverCard
|
|
635
|
+
|
|
636
|
+
`HoverCard` renders a card beside a link that previews what the link leads to. The card opens while
|
|
637
|
+
a pointer rests on the link or the link has keyboard focus.
|
|
291
638
|
|
|
292
|
-
|
|
639
|
+
```tsx
|
|
640
|
+
import { HoverCard } from "@stealthscale/component-disclosure";
|
|
641
|
+
import { Portal } from "@stealthscale/component-primitives";
|
|
293
642
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
643
|
+
<HoverCard.Root>
|
|
644
|
+
<HoverCard.Trigger href="/people/ada">@ada</HoverCard.Trigger>
|
|
645
|
+
<Portal>
|
|
646
|
+
<HoverCard.Positioner>
|
|
647
|
+
<HoverCard.Content>
|
|
648
|
+
<HoverCard.Arrow>
|
|
649
|
+
<HoverCard.ArrowTip />
|
|
650
|
+
</HoverCard.Arrow>
|
|
651
|
+
Ada Lovelace, staff engineer on the compiler team.
|
|
652
|
+
</HoverCard.Content>
|
|
653
|
+
</HoverCard.Positioner>
|
|
654
|
+
</Portal>
|
|
655
|
+
</HoverCard.Root>;
|
|
656
|
+
```
|
|
299
657
|
|
|
300
|
-
|
|
301
|
-
|
|
658
|
+
| Axis | Values | Default |
|
|
659
|
+
| --------- | ------------------------------ | --------- |
|
|
660
|
+
| `variant` | `surface`, `elevated`, `glass` | `surface` |
|
|
661
|
+
| `size` | `xs`, `sm`, `md`, `lg` | `md` |
|
|
662
|
+
|
|
663
|
+
| Part | Element | What it renders |
|
|
664
|
+
| ------------ | ------- | -------------------------------------------------------- |
|
|
665
|
+
| `Root` | `span` | A `display: contents` element that provides the variants |
|
|
666
|
+
| `Trigger` | `a` | The link the card previews |
|
|
667
|
+
| `Positioner` | `div` | The element the machine positions beside the trigger |
|
|
668
|
+
| `Content` | `div` | The card |
|
|
669
|
+
| `Arrow` | `div` | The element the machine places against the card's edge |
|
|
670
|
+
| `ArrowTip` | `div` | The arrow's rotated square |
|
|
671
|
+
|
|
672
|
+
- The root takes the machine's settings: `open`, `defaultOpen`, `onOpenChange`, `openDelay`,
|
|
673
|
+
`closeDelay`, `disabled`, `positioning`, `triggerValue`, `defaultTriggerValue`,
|
|
674
|
+
`onTriggerValueChange`, `ids`, `id` and `dir`, and the dismiss handlers `onInteractOutside`,
|
|
675
|
+
`onPointerDownOutside` and `onFocusOutside`.
|
|
676
|
+
- The card opens 600 ms after a pointer comes to rest on the link or the link takes focus. It closes
|
|
677
|
+
300 ms after the pointer leaves both the link and the card, and at once when the link loses focus
|
|
678
|
+
while no pointer rests on it. `openDelay` and `closeDelay` set the two waits.
|
|
679
|
+
- A pointer can move from the link onto the card without closing it, so a reader can select the
|
|
680
|
+
card's text. Escape and a press outside close the card. The machine ignores touch input, so a tap
|
|
681
|
+
follows the link.
|
|
682
|
+
- The trigger is an `a`. Give it an `href`, because an `a` without one takes no keyboard focus and
|
|
683
|
+
its card never opens from the keyboard. The machine does not set an ARIA attribute on the link.
|
|
684
|
+
- The card has no role and is outside the tab order. The machine moves no focus into it and
|
|
685
|
+
announces nothing, so a screen reader user does not learn that it opened. Put nothing in the card
|
|
686
|
+
that the linked page does not show, and no control a reader needs.
|
|
687
|
+
- Two or more links can share one card. Give each `Trigger` a `value`. The root reports the value of
|
|
688
|
+
the link that opened the card through `onTriggerValueChange`, and the machine places the card
|
|
689
|
+
against that link. A pointer that moves to another link moves the open card at once. Place the
|
|
690
|
+
card beside a column of links, because a card below one link covers the next.
|
|
691
|
+
- The card is not in the document until it first opens, and leaves once its exit animation ends.
|
|
692
|
+
`lazyMount` and `unmountOnExit` are true by default, and the card is `inert` while it leaves.
|
|
693
|
+
`skipAnimationOnMount` and `onExitComplete` go to the root as well.
|
|
694
|
+
- The root is a `span`, so a card can open from a link inside a paragraph. Pass `as="div"` when the
|
|
695
|
+
root contains block content, such as a list of links.
|
|
696
|
+
- The link is in the link ink and underlined at rest. The card slides in from the side of the link
|
|
697
|
+
it opens on. It is at most `sizes.xs` wide, 320px at the default scale, and narrower where the
|
|
698
|
+
room beside the link is less. The size sets the card's padding on the inset scale: 8, 12, 16 and
|
|
699
|
+
20px from `xs` to `lg`. The text reads the body role at the same size.
|
|
700
|
+
- The package does not portal. Wrap the positioner in a portal when the link is inside a paragraph
|
|
701
|
+
or an ancestor clips the card. A paragraph cannot contain the positioner's `div`.
|
|
302
702
|
|
|
303
703
|
## Menu
|
|
304
704
|
|
|
305
|
-
|
|
306
|
-
|
|
705
|
+
`Menu` renders a panel of actions and options, opened from a button, from a right click on a region,
|
|
706
|
+
or from a row of another menu.
|
|
307
707
|
|
|
308
708
|
```tsx
|
|
709
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
710
|
+
|
|
711
|
+
import { Button } from "@stealthscale/component-actions";
|
|
309
712
|
import { Menu } from "@stealthscale/component-disclosure";
|
|
310
713
|
|
|
311
714
|
<Menu.Root>
|
|
312
|
-
<Menu.Trigger>
|
|
715
|
+
<Menu.Trigger as={Button}>
|
|
313
716
|
Actions
|
|
314
717
|
<Menu.Indicator>
|
|
315
|
-
<
|
|
718
|
+
<ChevronDownIcon size="1em" />
|
|
316
719
|
</Menu.Indicator>
|
|
317
720
|
</Menu.Trigger>
|
|
318
721
|
<Menu.Positioner>
|
|
@@ -328,101 +731,173 @@ import { Menu } from "@stealthscale/component-disclosure";
|
|
|
328
731
|
</Menu.Root>;
|
|
329
732
|
```
|
|
330
733
|
|
|
331
|
-
| Axis | Values
|
|
332
|
-
| ----------- |
|
|
333
|
-
| `variant` | `surface`, `elevated`, `glass`
|
|
334
|
-
| `size` | `sm`, `md`, `lg`
|
|
335
|
-
| `highlight` | `tint`, `fill`, `bar`
|
|
336
|
-
| `
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
`
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
`
|
|
352
|
-
|
|
353
|
-
|
|
734
|
+
| Axis | Values | Default |
|
|
735
|
+
| ----------- | ----------------------------------------------------------------- | --------- |
|
|
736
|
+
| `variant` | `surface`, `elevated`, `glass` | `surface` |
|
|
737
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
738
|
+
| `highlight` | `tint`, `fill`, `bar` | `tint` |
|
|
739
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
740
|
+
| `inset` | `true` | off |
|
|
741
|
+
|
|
742
|
+
| Part | Element | What it renders |
|
|
743
|
+
| ----------------- | -------- | ------------------------------------------------------------ |
|
|
744
|
+
| `Root` | `div` | A `display: contents` element that provides the variants |
|
|
745
|
+
| `Trigger` | `button` | The button that opens and closes the menu |
|
|
746
|
+
| `ContextTrigger` | `div` | The region that opens the menu on a right click |
|
|
747
|
+
| `Indicator` | `span` | The mark that turns while the menu is open |
|
|
748
|
+
| `Positioner` | `div` | The element the machine positions beside the trigger |
|
|
749
|
+
| `Content` | `div` | The panel, around a scroll area whose viewport is the `menu` |
|
|
750
|
+
| `Item` | `div` | A row, with `role="menuitem"` |
|
|
751
|
+
| `OptionItem` | `div` | A checkbox or radio row |
|
|
752
|
+
| `TriggerItem` | `button` | A row that opens a submenu |
|
|
753
|
+
| `ItemGroup` | `div` | A group of rows, with `role="group"` |
|
|
754
|
+
| `ItemGroupLabel` | `div` | The label that names a group |
|
|
755
|
+
| `ItemMark` | `span` | The square at a row's start that shows an initial or an icon |
|
|
756
|
+
| `ItemLines` | `span` | The column that stacks a row's text over its description |
|
|
757
|
+
| `ItemText` | `span` | The text of a row |
|
|
758
|
+
| `ItemDescription` | `span` | The line under a row's text |
|
|
759
|
+
| `ItemIndicator` | `span` | The check at the end of a checked row |
|
|
760
|
+
| `ItemCommand` | `kbd` | The shortcut at the end of a row |
|
|
761
|
+
| `Separator` | `div` | The rule between two rows, with `role="separator"` |
|
|
762
|
+
| `Arrow` | `div` | The element the machine places against the panel's edge |
|
|
763
|
+
| `ArrowTip` | `div` | The arrow's rotated square |
|
|
764
|
+
|
|
765
|
+
- The root takes the machine's settings: `open`, `defaultOpen`, `onOpenChange`, `onSelect`,
|
|
766
|
+
`highlightedValue`, `defaultHighlightedValue`, `onHighlightChange`, `closeOnSelect`, `loopFocus`,
|
|
767
|
+
`typeahead`, `navigate`, `positioning`, `triggerValue`, `onTriggerValueChange`, `aria-label`, `id`
|
|
768
|
+
and `dir`, and the dismiss handlers such as `onEscapeKeyDown`.
|
|
769
|
+
- The panel is not in the document until it first opens, and leaves once its exit animation ends.
|
|
770
|
+
`lazyMount` and `unmountOnExit` are true by default. The panel is `inert` while it leaves, so a
|
|
771
|
+
highlight and Escape pressed in one frame leave focus on the trigger.
|
|
772
|
+
- The trigger sets `aria-haspopup="menu"`, `aria-expanded` and `aria-controls`. The panel takes
|
|
773
|
+
focus as it opens and keeps it. `aria-activedescendant` points a screen reader at the highlighted
|
|
774
|
+
row.
|
|
775
|
+
- The up and down arrows, Home and End move the highlight, and typing the start of a row's text
|
|
776
|
+
moves it to that row. Enter and Space select the highlighted row. Escape closes the menu and its
|
|
777
|
+
submenus and returns focus to the trigger.
|
|
778
|
+
- A menu that closes returns focus to its trigger while focus is in a menu's panel or on the body. A
|
|
779
|
+
press outside the panel that moves focus to another element leaves focus on that element, with
|
|
780
|
+
`open` controlled or not.
|
|
781
|
+
- A checkbox row keeps the menu open when selected, and every other row closes it. `closeOnSelect`
|
|
782
|
+
on a row overrides either.
|
|
783
|
+
- `Menu.OptionItem` takes `type`, `checked` and `onCheckedChange`. The caller keeps the checked
|
|
784
|
+
state and updates it in `onCheckedChange`.
|
|
785
|
+
- `Menu.ItemIndicator` renders at the row's end in any source order, and keeps its room while the
|
|
786
|
+
row is unchecked, so checking a row does not change the panel's width.
|
|
787
|
+
- `Menu.ItemGroup` and `Menu.ItemGroupLabel` take the same `value`, and the group sets
|
|
788
|
+
`aria-labelledby` to the label. `Menu.ItemMark` sets `aria-hidden`.
|
|
789
|
+
- `tone="critical"` sets a row in the error palette. The panel sets the `palette`, and every row and
|
|
790
|
+
the highlight inherit it.
|
|
791
|
+
- A row sizes the `svg` it starts with to the icon size one size smaller than the row. `inset` pads
|
|
792
|
+
a row that starts with neither an icon nor a `Menu.ItemMark` by the room an icon and its gap take,
|
|
793
|
+
so its text starts where the text of an icon row starts.
|
|
794
|
+
- A `Menu.Root` inside another menu's content is a submenu, and its `Menu.TriggerItem` is a row of
|
|
795
|
+
the parent. The submenu takes the parent's variants and `dir` unless it sets its own.
|
|
796
|
+
`Menu.TriggerItem` throws outside a submenu.
|
|
797
|
+
- A submenu opens beside the parent's panel, level with its row, at `right-start` in a left-to-right
|
|
798
|
+
document and at `left-start` in a right-to-left one. The machine sets that placement over the
|
|
799
|
+
submenu's own `positioning.placement`. `Menu.Indicator` mirrors in a right-to-left menu, so a
|
|
800
|
+
submenu row's chevron points to the side the submenu opens on.
|
|
801
|
+
- A press on the trigger of another menu or popover closes this menu and opens the other. A submenu
|
|
802
|
+
closes with its menu.
|
|
803
|
+
- `Menu.ContextTrigger` opens the menu at the pointer on a right click, and on a 700 ms press from
|
|
804
|
+
touch or a pen. It stops the browser's own menu. The machine positions a context menu once, as it
|
|
805
|
+
opens. Give the region `tabIndex={0}` or render it as a focusable element, so Shift+F10 opens the
|
|
806
|
+
menu from the keyboard.
|
|
807
|
+
- The panel is at least `sizes.44` wide and grows to its widest row. Pass
|
|
808
|
+
`positioning={{ sameWidth: true }}` to size the positioner to the trigger's width. The panel's
|
|
809
|
+
height stops at the room left in the window, and its rows scroll inside it.
|
|
810
|
+
- The rows scroll in the primitives package's scroll area inside the panel, with the theme's thin
|
|
811
|
+
bar at the panel's end edge, which follows the menu's `dir`. The scroll area's viewport is the
|
|
812
|
+
`menu`, so the element that has focus is the element that scrolls. A row the keys highlight
|
|
813
|
+
scrolls into view by the least distance, and a row the pointer highlights does not move. A
|
|
814
|
+
`Menu.Arrow` that is a direct child of `Menu.Content` renders outside the scroll area.
|
|
815
|
+
- `as` and `style` on `Menu.Content` go to the panel. Every other prop goes to the `menu`.
|
|
816
|
+
- The panel takes the `dropdown` z-index plus its depth in the nest, so a submenu renders above its
|
|
817
|
+
parent.
|
|
818
|
+
- The package does not portal. Wrap `Menu.Positioner` in a portal when an ancestor clips the panel.
|
|
819
|
+
Wrap a submenu's positioner in a portal too, because the parent's panel scrolls and clips a
|
|
820
|
+
submenu rendered inside it.
|
|
821
|
+
|
|
822
|
+
## Menubar
|
|
823
|
+
|
|
824
|
+
`Menubar` renders a row of menus along the top of an application, such as an editor's File, Edit and
|
|
825
|
+
View. The bar is one tab stop. The arrow keys move along it. At most one menu is open.
|
|
354
826
|
|
|
355
827
|
```tsx
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
<CheckIcon />
|
|
359
|
-
</Menu.ItemIndicator>
|
|
360
|
-
<Menu.ItemText>Compact rows</Menu.ItemText>
|
|
361
|
-
</Menu.OptionItem>
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
The row passes its value down. `Menu.ItemText` and `Menu.ItemIndicator` take no props of their own.
|
|
365
|
-
Group rows with `Menu.ItemGroup` and `Menu.ItemGroupLabel`. The two share one `value`.
|
|
366
|
-
|
|
367
|
-
### Submenus
|
|
368
|
-
|
|
369
|
-
Write a `Menu.Root` inside the panel of another. The inner root finds the menu above it and joins
|
|
370
|
-
the two machines. It draws itself in the variants that menu was given, unless it picks its own.
|
|
828
|
+
import { Menu, Menubar } from "@stealthscale/component-disclosure";
|
|
829
|
+
import { Portal } from "@stealthscale/component-primitives";
|
|
371
830
|
|
|
372
|
-
|
|
373
|
-
<Menu
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
</
|
|
831
|
+
<Menubar.Root aria-label="Editor">
|
|
832
|
+
<Menubar.Menu value="file">
|
|
833
|
+
<Menubar.Trigger>File</Menubar.Trigger>
|
|
834
|
+
<Portal>
|
|
835
|
+
<Menubar.Content>
|
|
836
|
+
<Menu.Item value="new">New file</Menu.Item>
|
|
837
|
+
<Menu.Item value="open">Open…</Menu.Item>
|
|
838
|
+
</Menubar.Content>
|
|
839
|
+
</Portal>
|
|
840
|
+
</Menubar.Menu>
|
|
841
|
+
<Menubar.Menu value="edit">
|
|
842
|
+
<Menubar.Trigger>Edit</Menubar.Trigger>
|
|
843
|
+
<Portal>
|
|
844
|
+
<Menubar.Content>
|
|
845
|
+
<Menu.Item value="undo">Undo</Menu.Item>
|
|
846
|
+
<Menu.Item value="redo">Redo</Menu.Item>
|
|
847
|
+
</Menubar.Content>
|
|
848
|
+
</Portal>
|
|
849
|
+
</Menubar.Menu>
|
|
850
|
+
</Menubar.Root>;
|
|
384
851
|
```
|
|
385
852
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
|
399
|
-
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
853
|
+
| Axis | Values | Default |
|
|
854
|
+
| --------- | ----------------------------------------------------------------- | --------- |
|
|
855
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
856
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | `neutral` |
|
|
857
|
+
|
|
858
|
+
The root passes `size`, `palette`, `variant` and `highlight` to every menu. The last two are axes of
|
|
859
|
+
the menu, with the menu's values and defaults.
|
|
860
|
+
|
|
861
|
+
| Part | Element | What it renders |
|
|
862
|
+
| --------- | -------- | -------------------------------------------------------------- |
|
|
863
|
+
| `Root` | `div` | The row that contains the bar of names and the fold trigger |
|
|
864
|
+
| `Menu` | `div` | A `display: contents` element around one menu of the bar |
|
|
865
|
+
| `Trigger` | `button` | A name of the bar, with `role="menuitem"`, that opens its menu |
|
|
866
|
+
| `Content` | `div` | The positioner and the panel of a menu, with `role="menu"` |
|
|
867
|
+
|
|
868
|
+
A panel's rows are `Menu` parts, such as `Menu.Item`, `Menu.OptionItem` and `Menu.Separator`. A
|
|
869
|
+
submenu is a `Menu.Root` around a `Menu.TriggerItem`.
|
|
870
|
+
|
|
871
|
+
- The bar is a `div` with `role="menubar"`. The root requires `aria-label`, the bar's accessible
|
|
872
|
+
name. `value`, `defaultValue` and `onValueChange` on the root read and set the `value` of the open
|
|
873
|
+
menu. The value is an empty string while every menu is closed.
|
|
874
|
+
- The bar is one tab stop. ArrowLeft and ArrowRight move focus along the names. Home and End move it
|
|
875
|
+
to the ends. A letter moves it to the next name that starts with that letter. `loop`, true by
|
|
876
|
+
default, continues a step past one end at the other.
|
|
877
|
+
- ArrowDown, Enter and Space on a name open its menu with the first row highlighted, and ArrowUp
|
|
878
|
+
opens it with the last. A press on a name opens its menu, and a press on the name of the open menu
|
|
879
|
+
closes it. While a menu is open, a mouse that moves onto another name opens that name's menu.
|
|
880
|
+
- ArrowRight in a panel opens the next menu with its first row highlighted, and ArrowLeft opens the
|
|
881
|
+
one before. While the highlighted row opens a submenu, the arrow towards the submenu opens it.
|
|
882
|
+
- Tab and Shift+Tab in a panel close the menu and move focus to the element after or before the bar.
|
|
883
|
+
Escape closes the menu and returns focus to its name.
|
|
884
|
+
- `dir="rtl"` lays the bar out from the right, swaps the arrows along the bar and in its panels, and
|
|
885
|
+
opens submenus to the left.
|
|
886
|
+
- The root measures its row. While the names do not fit at their natural width, the root sets
|
|
887
|
+
`data-crowded`, hides the bar and shows one trigger in its place: `foldIcon`, then `fold`, which
|
|
888
|
+
reads `Menu` by default. Its menu has one row per menu of the bar, which opens that menu as a
|
|
889
|
+
submenu and ends with `foldIndicator`.
|
|
890
|
+
- A name has the padding and text of a row of its menu, 27, 33 and 40px tall at `sm`, `md` and `lg`
|
|
891
|
+
at the default scale. It fills in the palette's subtle role under the pointer and in its muted
|
|
892
|
+
role while its menu is open, the fill of a highlighted row. Under forced colors an open name fills
|
|
893
|
+
with `Highlight`.
|
|
894
|
+
- `Menubar.Menu` takes the props of `Menu.Root` except `open`, `defaultOpen` and `onOpenChange`,
|
|
895
|
+
which the bar keeps. A variant set on a menu overrides the bar's.
|
|
896
|
+
- The package does not portal. Wrap each `Menubar.Content` in a portal, and each submenu's
|
|
897
|
+
positioner too.
|
|
898
|
+
- The menubar is not built on Zag's `@zag-js/menubar`, which is published only as 2.0 prereleases.
|
|
899
|
+
It does not open a menu on hover while every menu is closed, and a crowded bar folds rather than
|
|
900
|
+
scrolls.
|
|
426
901
|
|
|
427
902
|
## Licence
|
|
428
903
|
|