@acronis-platform/ui-react 4.1.0 → 5.1.0
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/dist/components/ui/area-chart/area-chart.js +191 -85
- package/dist/components/ui/area-chart/area-chart.js.map +1 -1
- package/dist/components/ui/bar-chart/bar-chart.js +288 -225
- package/dist/components/ui/bar-chart/bar-chart.js.map +1 -1
- package/dist/components/ui/card-widget-carousel/card-widget-carousel.js +85 -0
- package/dist/components/ui/card-widget-carousel/card-widget-carousel.js.map +1 -0
- package/dist/components/ui/card-widget-carousel/card-widget.js +82 -0
- package/dist/components/ui/card-widget-carousel/card-widget.js.map +1 -0
- package/dist/components/ui/category-bar/category-bar.js.map +1 -1
- package/dist/components/ui/chart/chart-format.js +25 -22
- package/dist/components/ui/chart/chart-format.js.map +1 -1
- package/dist/components/ui/chart/chart-palette.js +9 -8
- package/dist/components/ui/chart/chart-palette.js.map +1 -1
- package/dist/components/ui/chart/chart.js +127 -89
- package/dist/components/ui/chart/chart.js.map +1 -1
- package/dist/components/ui/funnel-chart/funnel-chart.js +189 -113
- package/dist/components/ui/funnel-chart/funnel-chart.js.map +1 -1
- package/dist/components/ui/input-select/input-select.js +18 -9
- package/dist/components/ui/input-select/input-select.js.map +1 -1
- package/dist/components/ui/line-chart/line-chart.js +194 -86
- package/dist/components/ui/line-chart/line-chart.js.map +1 -1
- package/dist/components/ui/radar-chart/radar-chart.js +76 -66
- package/dist/components/ui/radar-chart/radar-chart.js.map +1 -1
- package/dist/components/ui/scatter-chart/scatter-chart.js +5 -2
- package/dist/components/ui/scatter-chart/scatter-chart.js.map +1 -1
- package/dist/components/ui/separator/separator.js +52 -10
- package/dist/components/ui/separator/separator.js.map +1 -1
- package/dist/components/ui/sidebar-secondary/sidebar-secondary.js +144 -121
- package/dist/components/ui/sidebar-secondary/sidebar-secondary.js.map +1 -1
- package/dist/components/ui/tree-item/tree-item.js +67 -0
- package/dist/components/ui/tree-item/tree-item.js.map +1 -0
- package/dist/components/ui/treemap/treemap.js +73 -66
- package/dist/components/ui/treemap/treemap.js.map +1 -1
- package/dist/index.js +109 -107
- package/dist/llms/area-chart.md +2 -1
- package/dist/llms/bar-chart.md +132 -33
- package/dist/llms/card-widget-carousel.md +252 -0
- package/dist/llms/category-bar.md +2 -2
- package/dist/llms/funnel-chart.md +269 -101
- package/dist/llms/input-select.md +175 -1
- package/dist/llms/line-chart.md +2 -1
- package/dist/llms/metric.md +10 -5
- package/dist/llms/radar-chart.md +15 -5
- package/dist/llms/scatter-chart.md +2 -1
- package/dist/llms/separator.md +27 -8
- package/dist/llms/sidebar-secondary.md +235 -4
- package/dist/llms/tree-item.md +359 -0
- package/dist/llms/treemap.md +47 -36
- package/dist/llms.txt +5 -4
- package/dist/react.js +109 -107
- package/dist/src/components/ui/area-chart/area-chart.d.ts +10 -0
- package/dist/src/components/ui/bar-chart/bar-chart.d.ts +75 -10
- package/dist/src/components/ui/bar-chart/bar-chart.docs.d.ts +146 -0
- package/dist/src/components/ui/bar-chart/index.d.ts +1 -1
- package/dist/src/components/ui/card-widget-carousel/card-widget-carousel.d.ts +9 -0
- package/dist/src/components/ui/card-widget-carousel/card-widget.d.ts +43 -0
- package/dist/src/components/ui/card-widget-carousel/index.d.ts +2 -0
- package/dist/src/components/ui/chart/chart-format.d.ts +9 -1
- package/dist/src/components/ui/chart/chart-palette.d.ts +38 -10
- package/dist/src/components/ui/chart/chart.d.ts +8 -1
- package/dist/src/components/ui/chart/index.d.ts +2 -2
- package/dist/src/components/ui/funnel-chart/funnel-chart.d.ts +92 -26
- package/dist/src/components/ui/funnel-chart/index.d.ts +1 -1
- package/dist/src/components/ui/input-select/input-select.d.ts +34 -2
- package/dist/src/components/ui/line-chart/line-chart.d.ts +11 -1
- package/dist/src/components/ui/radar-chart/radar-chart.d.ts +42 -9
- package/dist/src/components/ui/separator/separator.d.ts +9 -1
- package/dist/src/components/ui/sidebar-secondary/sidebar-secondary.d.ts +23 -3
- package/dist/src/components/ui/tree-item/index.d.ts +1 -0
- package/dist/src/components/ui/tree-item/tree-item.d.ts +64 -0
- package/dist/src/components/ui/treemap/treemap.d.ts +16 -5
- package/dist/src/index.d.ts +2 -1
- package/dist/ui-react.css +1 -1
- package/package.json +1 -1
- package/dist/components/ui/meter/meter.js +0 -79
- package/dist/components/ui/meter/meter.js.map +0 -1
- package/dist/llms/meter.md +0 -177
- package/dist/src/components/ui/meter/index.d.ts +0 -1
- package/dist/src/components/ui/meter/meter.d.ts +0 -25
|
@@ -67,7 +67,7 @@ breadcrumb rail.
|
|
|
67
67
|
| `SidebarSecondaryMenuSubContent` | `Collapsible.Panel` `<div><ul>` | Level-2 child list |
|
|
68
68
|
| `SidebarSecondaryMenuSubItem` | `<li><a>` | Level-2 indented leaf |
|
|
69
69
|
| `SidebarSecondaryMenuItemExtras` | `<span>` | Trailing shortcut / external-link / tag |
|
|
70
|
-
| `SidebarSecondaryCollapseTrigger` | `<li><button>` | Footer "Collapse menu" button; toggles `expanded`
|
|
70
|
+
| `SidebarSecondaryCollapseTrigger` | `<li><button>` | Footer "Collapse menu" button; toggles `expanded` (`disabled` when not collapsible) |
|
|
71
71
|
|
|
72
72
|
## Quick Examples
|
|
73
73
|
|
|
@@ -149,6 +149,44 @@ Author both the `SidebarSecondaryContent` (section list) and the
|
|
|
149
149
|
`expanded` state via CSS — both stay in the DOM, so the breadcrumb is
|
|
150
150
|
SSR-present and no JS branch is needed.
|
|
151
151
|
|
|
152
|
+
## Locking the collapse state
|
|
153
|
+
|
|
154
|
+
`collapsible` defaults to `true`. Pass `collapsible={false}` when the surface
|
|
155
|
+
must not let the user change the panel's expanded/collapsed state. It locks that
|
|
156
|
+
toggle only — **not the width**, which the user can still change by dragging or
|
|
157
|
+
Arrow-shrinking while the panel is expanded. `resizable={false}` is what locks
|
|
158
|
+
the width value itself; the two props are independent and compose:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
<SidebarSecondary collapsible={false}>{/* … */}</SidebarSecondary>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Every user-initiated collapse/expand path is blocked — resize-edge click,
|
|
165
|
+
double-click-to-expand, drag past the collapse threshold, the resize edge's
|
|
166
|
+
Arrow/Enter/Space/Home keys, and the footer
|
|
167
|
+
`SidebarSecondaryCollapseTrigger` (which renders natively `disabled` and
|
|
168
|
+
shows no `expandTooltip`).
|
|
169
|
+
- **Resizing still works while the panel is expanded.** A drag or Arrow-shrink
|
|
170
|
+
that would have collapsed the panel clamps to the minimum width instead. Pass
|
|
171
|
+
`resizable={false}` as well to lock the width outright.
|
|
172
|
+
- It does **not** force `expanded`. `collapsible={false}` with
|
|
173
|
+
`defaultExpanded={false}` is valid and renders a permanently collapsed rail; a
|
|
174
|
+
controlled `expanded` prop still has full authority. On that permanently
|
|
175
|
+
collapsed rail the resize edge stays focusable and keeps its accessible name,
|
|
176
|
+
but it is inert — drag, click and the Arrow keys do nothing observable, and
|
|
177
|
+
only `Home` (or a double-click) still writes the default width, invisibly.
|
|
178
|
+
- Every tooltip is adjusted for you, so the copy never advertises an inert
|
|
179
|
+
gesture. The footer collapse trigger's `expandTooltip` is suppressed outright
|
|
180
|
+
(a disabled control cannot expand anything; an explicit value does not bring
|
|
181
|
+
it back). The two resize-edge defaults narrow: **Expanded**, the default drops
|
|
182
|
+
the
|
|
183
|
+
"Collapse: Click" line and reads "Resize: Drag" / "Reset size: Double click".
|
|
184
|
+
**Collapsed**, the default narrows to "Reset size: Double click" alone —
|
|
185
|
+
dragging and clicking a permanently collapsed rail do nothing, but a
|
|
186
|
+
double-click still resets the width. An explicit `resizeTooltipExpanded` /
|
|
187
|
+
`resizeTooltipCollapsed` value always wins, including `null` to suppress the
|
|
188
|
+
tooltip entirely.
|
|
189
|
+
|
|
152
190
|
## Spec Files
|
|
153
191
|
|
|
154
192
|
| File | Contents |
|
|
@@ -177,10 +215,11 @@ SSR-present and no JS branch is needed.
|
|
|
177
215
|
| `actions` | `ReactNode` | `undefined` | no | Trailing header actions on the section-label (e.g. a ghost ButtonIcon), rendered outside the toggle so they stay independently operable. |
|
|
178
216
|
| `unreadRollup` | `ReactNode` | `undefined` | no | A rollup badge (e.g. a Tag with an unread count) shown in an expandable section's header only while it is collapsed. |
|
|
179
217
|
| `resizable` | `boolean` | `true` | no | Enable the draggable resize edge on the right border. When `true`, the sidebar can be resized between the expanded-width token and twice that value. |
|
|
218
|
+
| `collapsible` | `boolean` | `true` | no | Allow the user to collapse/expand the panel (resize-edge click, drag past the collapse threshold, keyboard, and the footer collapse trigger). When `false` those interactions never change `expanded` and the footer collapse trigger renders natively disabled with no `aria-expanded`; while the panel is expanded, dragging still resizes it, clamped to the minimum width. Independent of `expanded` — pair it with `defaultExpanded: false` for a permanently collapsed rail, on which the resize edge stays focusable but is inert apart from the (invisible) width reset, which `Home` and a double-click on the edge both still perform. |
|
|
180
219
|
| `resizeAriaLabel` | `string` | `'Resize sidebar'` | no | Accessible label for the resize edge (`role="separator"`). Consumers should localize this string for i18n. |
|
|
181
|
-
| `resizeTooltipExpanded` | `ReactNode` | `(English tooltip: Resize/Collapse/Reset)` | no | Tooltip content shown when the sidebar is expanded. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
|
|
182
|
-
| `resizeTooltipCollapsed` | `ReactNode` | `(English tooltip: Resize/Expand)` | no | Tooltip content shown when the sidebar is collapsed. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
|
|
183
|
-
| `expandTooltip` | `ReactNode` | `'Expand'` | no | Tooltip text shown on the collapse trigger when the sidebar is collapsed. Consumers should localize this string for i18n. |
|
|
220
|
+
| `resizeTooltipExpanded` | `ReactNode` | `(English tooltip: Resize/Collapse/Reset; Resize/Reset when not collapsible)` | no | Tooltip content shown when the sidebar is expanded. The default depends on `collapsible`: with `collapsible: false` it drops the "Collapse: Click" line, since clicking the edge can no longer collapse the panel. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
|
|
221
|
+
| `resizeTooltipCollapsed` | `ReactNode` | `(English tooltip: Resize/Expand; 'Reset size: Double click' only when not collapsible)` | no | Tooltip content shown when the sidebar is collapsed. The default depends on `collapsible`: with `collapsible: false` it narrows to the single "Reset size: Double click" line, because dragging and clicking a permanently collapsed rail have no effect while a double-click still resets the width. Pass `null` to hide the tooltip entirely, or an explicit node to replace it. Consumers should localize for i18n. |
|
|
222
|
+
| `expandTooltip` | `ReactNode` | `'Expand'` | no | Tooltip text shown on the collapse trigger when the sidebar is collapsed. Suppressed entirely with `collapsible: false` — the trigger is disabled then, so the copy would advertise an action that cannot run. An explicit value does not override that suppression. Consumers should localize this string for i18n. |
|
|
184
223
|
| `extras-variant` | `'tag' | 'externalLink' | 'shortcut' | 'tag-externalLink'` | — | yes | Which trailing affordance the extras part renders. React: the `variant` prop on `SidebarSecondaryMenuItemExtras`. (Named here without the bare `variant` key reserved for a root cva axis.) |
|
|
185
224
|
| `shortcut` | `string` | `undefined` | no | Keyboard-shortcut text for the extras `shortcut` variant. |
|
|
186
225
|
| `tag` | `ReactNode` | `undefined` | no | Tag content for the extras `tag` / `tag-externalLink` variants. |
|
|
@@ -256,6 +295,111 @@ SSR-present and no JS branch is needed.
|
|
|
256
295
|
> DOM; visibility is toggled purely by `data-[state]` selectors (SSR-safe, no JS
|
|
257
296
|
> branch).
|
|
258
297
|
|
|
298
|
+
#### `collapsible={false}` blocks every user collapse/expand path
|
|
299
|
+
|
|
300
|
+
**Given** a SidebarSecondary with `collapsible={false}`
|
|
301
|
+
**When** the user clicks the resize edge, double-clicks it while collapsed,
|
|
302
|
+
drags it past the collapse threshold, presses the shrink Arrow at the minimum
|
|
303
|
+
width, presses the grow Arrow while collapsed, presses Enter/Space on the resize
|
|
304
|
+
edge, presses Home while collapsed, or activates the footer
|
|
305
|
+
`SidebarSecondaryCollapseTrigger`
|
|
306
|
+
**Then** `expanded` never changes and `data-state` stays as it was
|
|
307
|
+
**And** the footer collapse trigger renders natively `disabled`
|
|
308
|
+
|
|
309
|
+
> **The width reset is not inert while collapsed.** Blocking the expand does not
|
|
310
|
+
> block the width reset: on the resize edge of a `collapsible={false}`, collapsed
|
|
311
|
+
> panel, **`Home` and a double-click** both still reset the stored width to
|
|
312
|
+
> `defaultWidth` — neither write is gated on `collapsible` (in
|
|
313
|
+
> `handleDoubleClick`, only its `toggleExpanded()` call is). `onWidthChange`
|
|
314
|
+
> fires **only when `defaultWidth` differs from the current stored width**:
|
|
315
|
+
> `setWidth` returns early on `next === currentWidth`, so a width write that
|
|
316
|
+
> resolves to the current value is a no-op, and on a panel whose width has never
|
|
317
|
+
> moved these two gestures emit nothing. When the width _has_ moved, nothing
|
|
318
|
+
> changes visually at that moment (the rail stays collapsed), but a
|
|
319
|
+
> controlled consumer observes the new width, and it takes effect the moment the
|
|
320
|
+
> panel is expanded by other means (a controlled `expanded` prop). Every _other_
|
|
321
|
+
> expand-triggering gesture — a single click, a drag, the grow Arrow, Enter/Space
|
|
322
|
+
> — is genuinely inert in that state.
|
|
323
|
+
|
|
324
|
+
#### A disabled collapse trigger looks inert
|
|
325
|
+
|
|
326
|
+
**Given** a SidebarSecondary with `collapsible={false}` (or an explicitly
|
|
327
|
+
`disabled` `SidebarSecondaryCollapseTrigger`)
|
|
328
|
+
**When** the footer collapse trigger renders
|
|
329
|
+
**Then** it shows a `not-allowed` cursor instead of the pointer cursor
|
|
330
|
+
**And** its label and icon use the disabled on-surface foreground token
|
|
331
|
+
**And** hovering or pressing it produces no highlight — the hover and active
|
|
332
|
+
fills stay pinned to the row's idle container color
|
|
333
|
+
**And** this is in addition to the native `disabled` attribute above, so the
|
|
334
|
+
row reads as visually inert exactly where it is functionally inert
|
|
335
|
+
|
|
336
|
+
#### Resizing stays live and clamps at the minimum
|
|
337
|
+
|
|
338
|
+
**Given** an expanded, `resizable` SidebarSecondary with `collapsible={false}`
|
|
339
|
+
**When** the user drags the resize edge below the collapse threshold, or presses
|
|
340
|
+
the shrink Arrow so the next step would fall under `minWidth`
|
|
341
|
+
**Then** the width clamps to `minWidth` (the expanded-width token's shipped
|
|
342
|
+
default, 256px) instead of collapsing
|
|
343
|
+
**And** drag/Arrow resizing between `minWidth` and `maxWidth` behaves exactly as
|
|
344
|
+
it does when `collapsible` is `true`
|
|
345
|
+
|
|
346
|
+
#### A permanently collapsed rail is a valid combination
|
|
347
|
+
|
|
348
|
+
**Given** `collapsible={false}` together with `defaultExpanded={false}`
|
|
349
|
+
**When** the panel renders
|
|
350
|
+
**Then** it renders as the collapsed breadcrumb rail (`data-state="collapsed"`)
|
|
351
|
+
**And** no user interaction can expand it — only a controlled `expanded` prop can
|
|
352
|
+
**And** the resize edge stays focusable and keeps its accessible name, but is
|
|
353
|
+
inert: dragging it, clicking it, and the grow/shrink Arrow keys produce no
|
|
354
|
+
observable change
|
|
355
|
+
**And** the one exception is the width reset — `Home` **and** a double-click on
|
|
356
|
+
the edge both still write `defaultWidth` while the rail stays visually
|
|
357
|
+
unchanged, firing `onWidthChange` only when `defaultWidth` differs from the
|
|
358
|
+
current stored width (see the width-reset note above)
|
|
359
|
+
|
|
360
|
+
> `collapsible` never forces `expanded` to a value; it only gates the
|
|
361
|
+
> user-initiated transitions. A controlled `expanded` prop keeps full authority
|
|
362
|
+
> either way.
|
|
363
|
+
|
|
364
|
+
#### The expanded default tooltip drops the click gesture
|
|
365
|
+
|
|
366
|
+
**Given** `collapsible={false}`, an **expanded** panel, and no
|
|
367
|
+
`resizeTooltipExpanded` override
|
|
368
|
+
**When** the user hovers the resize edge
|
|
369
|
+
**Then** the tooltip reads "**Resize:** Drag" and "**Reset size:** Double click"
|
|
370
|
+
— the "**Collapse:** Click" line the `collapsible={true}` default carries is
|
|
371
|
+
omitted, because clicking the edge can no longer collapse the panel
|
|
372
|
+
**And** an explicit `resizeTooltipExpanded` value still wins, including `null`
|
|
373
|
+
to suppress the tooltip
|
|
374
|
+
|
|
375
|
+
#### A permanently collapsed rail's tooltip advertises only the double-click
|
|
376
|
+
|
|
377
|
+
**Given** `collapsible={false}`, a **collapsed** panel, and no
|
|
378
|
+
`resizeTooltipCollapsed` override
|
|
379
|
+
**When** the user hovers the resize edge
|
|
380
|
+
**Then** the tooltip reads "**Reset size:** Double click" — and nothing else
|
|
381
|
+
**And** the other two lines the `collapsible={true}` collapsed default carries
|
|
382
|
+
are dropped, because neither gesture is live: clicking the edge bails in
|
|
383
|
+
`handleClick`, and the collapsed drag branch only ever calls `toggleExpanded()`
|
|
384
|
+
(itself gated off) and never writes a width, so both "**Resize:** Drag" and
|
|
385
|
+
"**Expand:** Click" would be false claims
|
|
386
|
+
**And** the double-click line _is_ honest: `handleDoubleClick` writes
|
|
387
|
+
`defaultWidth` unconditionally, so it stays a live pointer gesture — the pointer
|
|
388
|
+
counterpart of `Home`
|
|
389
|
+
**And** an explicit `resizeTooltipCollapsed` value still wins, including `null`
|
|
390
|
+
to suppress the tooltip entirely
|
|
391
|
+
|
|
392
|
+
#### The collapse trigger shows no tooltip at all
|
|
393
|
+
|
|
394
|
+
**Given** `collapsible={false}` and a footer `SidebarSecondaryCollapseTrigger`
|
|
395
|
+
**When** the user hovers or focuses that trigger while the panel is collapsed
|
|
396
|
+
**Then** no tooltip appears — the trigger's own `Tooltip` is disabled whenever
|
|
397
|
+
the panel is expanded **or** not collapsible
|
|
398
|
+
**And** this holds even when an explicit `expandTooltip` value is passed, unlike
|
|
399
|
+
the two resize-edge tooltips where an override still wins: the trigger is
|
|
400
|
+
natively `disabled` here, so any "Expand" copy would advertise an action the
|
|
401
|
+
user cannot perform
|
|
402
|
+
|
|
259
403
|
---
|
|
260
404
|
|
|
261
405
|
### Disclosure (expandable sections)
|
|
@@ -360,6 +504,33 @@ When collapsed, the section list is hidden and a vertical breadcrumb is shown
|
|
|
360
504
|
(`aria-hidden`). Any menu-item labels that remain are kept in the DOM as
|
|
361
505
|
`sr-only`, never `display:none`.
|
|
362
506
|
|
|
507
|
+
### Collapse trigger (footer)
|
|
508
|
+
|
|
509
|
+
`SidebarSecondaryCollapseTrigger` is a `<button>` that toggles the panel width.
|
|
510
|
+
|
|
511
|
+
| `collapsible` | Exposed as | `expandTooltip` while collapsed |
|
|
512
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
513
|
+
| `true` | `aria-expanded` mirrors the panel's `expanded` state; button enabled unless the consumer passes `disabled` | shown on hover/focus |
|
|
514
|
+
| `false` | **no** `aria-expanded`; button carries the native `disabled` | **suppressed** (no tooltip) |
|
|
515
|
+
|
|
516
|
+
When `collapsible` is `false` the button has no state to expose or flip, so
|
|
517
|
+
`aria-expanded` is omitted rather than pinned to a value a user cannot change,
|
|
518
|
+
and `disabled` communicates the unavailable action to assistive tech. Its
|
|
519
|
+
`expandTooltip` is suppressed for the same reason — under `collapsible={false}`
|
|
520
|
+
the panel can never be expanded by the user, so an "Expand" hint would be a
|
|
521
|
+
false affordance. (The tooltip is likewise suppressed whenever the panel is
|
|
522
|
+
expanded, in either mode: there is nothing to expand.)
|
|
523
|
+
|
|
524
|
+
The suppression keys off `collapsible`, not off the button's disabled state:
|
|
525
|
+
passing `disabled` directly to the trigger while `collapsible` is `true` still
|
|
526
|
+
disables the button, but the `expandTooltip` keeps showing on hover.
|
|
527
|
+
|
|
528
|
+
### Resize edge
|
|
529
|
+
|
|
530
|
+
When `resizable`, the panel renders a focusable handle on its inline-end border
|
|
531
|
+
with `role="separator"`, `aria-orientation="vertical"`, `tabIndex={0}` and an
|
|
532
|
+
overridable `aria-label` (default `'Resize sidebar'`).
|
|
533
|
+
|
|
363
534
|
---
|
|
364
535
|
|
|
365
536
|
## Keyboard Navigation
|
|
@@ -374,6 +545,55 @@ When collapsed, the section list is hidden and a vertical breadcrumb is shown
|
|
|
374
545
|
No roving tabindex — this is a list of links plus disclosure buttons, not a
|
|
375
546
|
`menu`/`menubar`.
|
|
376
547
|
|
|
548
|
+
### Resize edge (`role="separator"`)
|
|
549
|
+
|
|
550
|
+
Arrow keys follow the writing direction: the **grow** key is `ArrowRight` in LTR
|
|
551
|
+
and `ArrowLeft` in RTL; the **shrink** key is the mirror.
|
|
552
|
+
|
|
553
|
+
| Key | `collapsible: true` | `collapsible: false` |
|
|
554
|
+
| ----------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
555
|
+
| Grow key | Expanded: +16px. Collapsed: expands the panel | Expanded: +16px. Collapsed: **inert** |
|
|
556
|
+
| Shrink key | Expanded: −16px; below the minimum width it collapses the panel. Collapsed: **inert** | Expanded: −16px, **clamps at the minimum width**, never collapses. Collapsed: **inert** |
|
|
557
|
+
| Enter/Space | Toggles expanded/collapsed | **Inert** |
|
|
558
|
+
| Home | Expands (if collapsed) and resets to the default width | Resets to the default width; never expands |
|
|
559
|
+
|
|
560
|
+
With `collapsible={false}` the edge stays focusable and keeps its accessible
|
|
561
|
+
name. **While the panel is expanded**, resizing is still a real operation — only
|
|
562
|
+
the collapse transition is suppressed. **While the panel is permanently
|
|
563
|
+
collapsed** (`collapsible={false}` + collapsed) the edge is a focusable but
|
|
564
|
+
visually inert tab stop: it keeps its `role="separator"` and accessible name,
|
|
565
|
+
yet dragging, clicking, and both Arrow keys produce no observable change. Every
|
|
566
|
+
**expand-triggering** gesture is inert here: dragging, a single click, the grow
|
|
567
|
+
key and Enter/Space all fire no `toggleExpanded`, so `data-state` stays
|
|
568
|
+
`collapsed`. **Two gestures remain live** — `Home` on the keyboard and a
|
|
569
|
+
**double-click** on the edge. Both reset the stored width to `defaultWidth`, and
|
|
570
|
+
neither is gated on `collapsible` (in `handleDoubleClick`, only the
|
|
571
|
+
`toggleExpanded()` call is). They fire `onWidthChange` **only when
|
|
572
|
+
`defaultWidth` differs from the current stored width** — `setWidth` returns
|
|
573
|
+
early on `next === currentWidth`, so a width write that resolves to the current
|
|
574
|
+
value is a no-op, and on a panel whose width has never moved these two gestures
|
|
575
|
+
write nothing at all. When the width _has_ moved, the callback fires even though
|
|
576
|
+
the rail is collapsed and looks unchanged at that moment; the reset only becomes
|
|
577
|
+
visible if the panel is later expanded by other means (a controlled `expanded`
|
|
578
|
+
prop flip), so a controlled consumer that mirrors `onWidthChange` must expect a
|
|
579
|
+
width update while the panel is collapsed.
|
|
580
|
+
|
|
581
|
+
> **Tooltip copy.** With `collapsible={false}` every tooltip on the panel is
|
|
582
|
+
> adjusted for you, so the copy never advertises a gesture that is inert. The
|
|
583
|
+
> footer collapse trigger's own `expandTooltip` is **suppressed outright** —
|
|
584
|
+
> nothing can expand the panel, and an explicit value does not bring the
|
|
585
|
+
> tooltip back — and both resize-edge tooltip defaults narrow.
|
|
586
|
+
> **Expanded**, the resize-edge default drops the "**Collapse:** Click" line
|
|
587
|
+
> and reads "**Resize:** Drag" / "**Reset size:** Double click". **Collapsed**,
|
|
588
|
+
> the default
|
|
589
|
+
> narrows to the single line "**Reset size:** Double click" — dragging and
|
|
590
|
+
> clicking a permanently collapsed rail do nothing, but a double-click still
|
|
591
|
+
> writes the default width, so that is the one pointer gesture the tooltip can
|
|
592
|
+
> honestly advertise. An explicit `resizeTooltipExpanded` /
|
|
593
|
+
> `resizeTooltipCollapsed` value always wins over the default (including `null`
|
|
594
|
+
> to suppress the tooltip entirely), `collapsible` notwithstanding. All of these
|
|
595
|
+
> strings are English by default and must be localized regardless.
|
|
596
|
+
|
|
377
597
|
---
|
|
378
598
|
|
|
379
599
|
## Focus
|
|
@@ -415,3 +635,14 @@ A keyboard-focused row shows a visible focus ring using the shared
|
|
|
415
635
|
- [ ] Disclosure trigger exposes `aria-expanded` / `aria-controls`; toggles on click + Enter/Space
|
|
416
636
|
- [ ] Level-2 sub-items are indented and reachable via Tab when open
|
|
417
637
|
- [ ] Collapsed rows keep an accessible name (sr-only label, not removed)
|
|
638
|
+
- [ ] Resize edge is `role="separator"`, focusable, and has an overridable `aria-label`
|
|
639
|
+
- [ ] With `collapsible={false}`: the footer collapse trigger is `disabled`,
|
|
640
|
+
exposes no `aria-expanded`, and shows **no Expand tooltip** on hover or
|
|
641
|
+
focus; Enter/Space on the resize edge does not change
|
|
642
|
+
`data-state`; the shrink key clamps at the minimum width
|
|
643
|
+
- [ ] With `collapsible={false}` while collapsed: Home **and a double-click on
|
|
644
|
+
the edge** leave `data-state` `collapsed` but still reset the stored width
|
|
645
|
+
(`onWidthChange` fires with `defaultWidth` only if that differs from the
|
|
646
|
+
current stored width)
|
|
647
|
+
- [ ] With `collapsible={false}` while collapsed: the default resize tooltip
|
|
648
|
+
reads "Reset size: Double click" only
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# TreeItem
|
|
2
|
+
|
|
3
|
+
> One flat row of a tree or nested-list UI — an optional expand chevron, an optional checkbox, an optional leading icon, the row title, and an optional trailing extras slot. It is a single row and nothing more: it renders no nested list and implements no expand/collapse, so `isExpandable` and `expanded` are purely visual affordances and `selected` is a prop the consumer drives. The tree itself — its ARIA roles, indentation, expand state, selection, and keyboard roving — belongs to the composition, the same way `Breadcrumb` ships the trail's primitives without owning the trail.
|
|
4
|
+
|
|
5
|
+
**Status:** ready · **Category:** navigation · **Since:** v5.1.0
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { TreeItem } from '@acronis-platform/ui-react'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick example
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<ul role="tree" aria-label="Workloads">
|
|
17
|
+
<li role="treeitem" aria-expanded={open}>
|
|
18
|
+
<TreeItem
|
|
19
|
+
expanded={open}
|
|
20
|
+
hasIcon
|
|
21
|
+
icon={<FolderIcon size={16} />}
|
|
22
|
+
title="All workloads"
|
|
23
|
+
onClick={() => setOpen((v) => !v)}
|
|
24
|
+
onKeyDown={(event) => {
|
|
25
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
setOpen((v) => !v);
|
|
28
|
+
}
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<Tag variant="info">24</Tag>
|
|
32
|
+
</TreeItem>
|
|
33
|
+
<ul role="group" className="ps-4">
|
|
34
|
+
<li role="treeitem" aria-selected>
|
|
35
|
+
<TreeItem
|
|
36
|
+
isExpandable={false}
|
|
37
|
+
hasIcon
|
|
38
|
+
selected
|
|
39
|
+
title="Cloud applications"
|
|
40
|
+
/>
|
|
41
|
+
</li>
|
|
42
|
+
</ul>
|
|
43
|
+
</li>
|
|
44
|
+
</ul>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
One flat row of a tree or nested-list UI: an optional expand chevron, an optional
|
|
48
|
+
checkbox, an optional leading icon, the row title, and an optional trailing
|
|
49
|
+
extras slot. It is deliberately **one row** — it renders no nested list, owns no
|
|
50
|
+
expand state, and selects nothing. You compose the rows, indent them, hold the
|
|
51
|
+
open/selected state, and supply the ARIA tree semantics.
|
|
52
|
+
|
|
53
|
+
## When to use
|
|
54
|
+
|
|
55
|
+
- A file tree, a workload/resource hierarchy, a nested navigation panel — one
|
|
56
|
+
`TreeItem` per row.
|
|
57
|
+
- A multi-select hierarchy: turn on `hasCheckbox` and drive the box through
|
|
58
|
+
`checkboxProps`.
|
|
59
|
+
|
|
60
|
+
## When not to use
|
|
61
|
+
|
|
62
|
+
- For a flat, non-hierarchical secondary nav — use
|
|
63
|
+
[`SidebarSecondary`](../sidebar-secondary/README.md).
|
|
64
|
+
- For a path back to the current location — use
|
|
65
|
+
[`Breadcrumb`](../breadcrumb/README.md).
|
|
66
|
+
- For collapsible **content** panels rather than a hierarchy of rows — use
|
|
67
|
+
`Accordion` / `AccordionContainer`.
|
|
68
|
+
- As the whole tree. There is no `Tree` container component today; the row is the
|
|
69
|
+
primitive and the tree is assembled in the application.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { TreeItem } from '@acronis-platform/ui-react';
|
|
75
|
+
|
|
76
|
+
const [open, setOpen] = React.useState(true);
|
|
77
|
+
|
|
78
|
+
<ul role="tree" aria-label="Workloads">
|
|
79
|
+
<TreeItem
|
|
80
|
+
render={<li role="treeitem" aria-expanded={open} aria-level={1} />}
|
|
81
|
+
expanded={open}
|
|
82
|
+
hasIcon
|
|
83
|
+
icon={<FolderIcon size={16} />}
|
|
84
|
+
title="All workloads"
|
|
85
|
+
onClick={() => setOpen((v) => !v)}
|
|
86
|
+
>
|
|
87
|
+
<Tag variant="info">24</Tag>
|
|
88
|
+
</TreeItem>
|
|
89
|
+
|
|
90
|
+
{open && (
|
|
91
|
+
<ul role="group" className="ps-4">
|
|
92
|
+
<TreeItem
|
|
93
|
+
render={<li role="treeitem" aria-level={2} aria-selected />}
|
|
94
|
+
isExpandable={false}
|
|
95
|
+
hasIcon
|
|
96
|
+
selected
|
|
97
|
+
title="Cloud applications"
|
|
98
|
+
/>
|
|
99
|
+
<TreeItem
|
|
100
|
+
render={<li role="treeitem" aria-level={2} />}
|
|
101
|
+
isExpandable={false}
|
|
102
|
+
hasIcon
|
|
103
|
+
title="Unmanaged workloads"
|
|
104
|
+
/>
|
|
105
|
+
</ul>
|
|
106
|
+
)}
|
|
107
|
+
</ul>;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Three things the row hands back to you on purpose:
|
|
111
|
+
|
|
112
|
+
- **`isExpandable` and `expanded` are artwork, not behavior.** The first draws
|
|
113
|
+
the chevron, the second rotates it a quarter turn to point down. Neither
|
|
114
|
+
expands anything, and there is no change event: the Figma node's expandable
|
|
115
|
+
slot has no interaction either. Pass `expanded` from the same boolean that
|
|
116
|
+
feeds your `aria-expanded`, so the affordance and the announced state agree.
|
|
117
|
+
- **`selected` is a look you drive.** The row never selects itself on click; it
|
|
118
|
+
only reflects the flag. Figma's `state=active` swatch paints the same fill,
|
|
119
|
+
which is why `active` is not wired to CSS `:active`.
|
|
120
|
+
- **The ARIA tree is yours.** No `role="treeitem"`, no tab stop by default — see
|
|
121
|
+
[`accessibility.md`](./accessibility.md) for why a role on an orphan row would
|
|
122
|
+
make the tree invalid rather than accessible.
|
|
123
|
+
|
|
124
|
+
## Parts
|
|
125
|
+
|
|
126
|
+
| Part | Element | Notes |
|
|
127
|
+
| ---------- | ------- | ------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| row (root) | `div` | Polymorphic via `render`; carries the hover / `selected` fill and focus ring |
|
|
129
|
+
| `expander` | `span` | Optional (`isExpandable`, default on); `aria-hidden` chevron, flips in RTL, rotates on `expanded` |
|
|
130
|
+
| `checkbox` | `span` | Optional (`hasCheckbox`); the shared `Checkbox`, `aria-label` defaults to title |
|
|
131
|
+
| `icon` | `span` | Optional (`hasIcon`); falls back to the design's square-dashed placeholder |
|
|
132
|
+
| `title` | `span` | The row label; takes the remaining width and truncates |
|
|
133
|
+
| `extras` | `span` | Optional (`hasExtras`, default on); the trailing `children` slot |
|
|
134
|
+
|
|
135
|
+
## Design status
|
|
136
|
+
|
|
137
|
+
**No dedicated token tier.** `tokens-pd` ships no `Tree` tier: the Figma node's
|
|
138
|
+
variables still sit in the pre-next-gen `componentLegacy/tree/*` and
|
|
139
|
+
`componentLegacy/sidebar/*` namespaces, which the `brand.components` migration
|
|
140
|
+
has not reached. Every colour the row paints therefore comes from the semantic
|
|
141
|
+
tier, and the layout numbers (8px gap/padding, 128px minimum row width, 4px slot
|
|
142
|
+
inset, 16px minimum extras well) are plain utilities matching the design's pixel
|
|
143
|
+
values — see [`tokens.yaml`](./tokens.yaml) for the full reasoning. Re-point both
|
|
144
|
+
when a `--ui-tree-item-*` tier lands.
|
|
145
|
+
|
|
146
|
+
**Deviation from the Figma frame — the extras well.** Figma measures the trailing
|
|
147
|
+
slot as `min-w-[16px] w-[24px]`; the 24px is the resolved width of its
|
|
148
|
+
placeholder content in a hug-content frame, not a constraint. The implementation
|
|
149
|
+
keeps the 16px minimum and lets the slot grow, because a hard 24px would clip the
|
|
150
|
+
action buttons and count badges the slot exists to hold.
|
|
151
|
+
|
|
152
|
+
## Props
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Default | Required | Description |
|
|
155
|
+
|------|------|---------|----------|-------------|
|
|
156
|
+
| `title` | `string` | `Title` | no | The row label. Truncates rather than wrapping. Note this shadows the native `title` tooltip attribute on the underlying element: the Figma component names the property `title`, and it is the row's visible label. |
|
|
157
|
+
| `isExpandable` | `boolean` | `true` | no | Show the leading expand chevron. Purely a visual affordance — the row implements no expand/collapse and renders no nested list, and emits no change event. Set it false for a leaf row. |
|
|
158
|
+
| `expanded` | `boolean` | `false` | no | Whether the row's nested list is currently expanded. Purely visual — like `isExpandable`, the row owns no expand/collapse state and renders no nested list. Rotates the leading chevron to reflect the state the consumer is already tracking (and should also be reflecting via `aria-expanded` on its own element, e.g. `<li role="treeitem" aria-expanded={open}>`). Has no effect when `isExpandable` is false. |
|
|
159
|
+
| `hasCheckbox` | `boolean` | `false` | no | Show a leading checkbox, for a multi-select tree. |
|
|
160
|
+
| `checkboxProps` | `CheckboxProps` | — | no | Props forwarded verbatim to the leading checkbox (`checked`, `onCheckedChange`, `indeterminate`, `aria-label`, …). The row keeps no checked state of its own, so the consumer controls the box exactly as it would a standalone `Checkbox`. `aria-label` defaults to `title`. |
|
|
161
|
+
| `hasIcon` | `boolean` | `false` | no | Show the leading icon slot. |
|
|
162
|
+
| `icon` | `ReactNode` | — | no | The leading icon. Only rendered when `hasIcon` is set; falls back to the design's square-dashed placeholder icon, which is what the Figma node draws. |
|
|
163
|
+
| `hasExtras` | `boolean` | `true` | no | Render the trailing extras slot at all. When false, `children` are dropped entirely rather than merely hidden. |
|
|
164
|
+
| `selected` | `boolean` | `false` | no | Whether the row is selected — the Figma `variant` axis. Paints the persistent highlighted background, the same fill the design's `state=active` swatch previews. Selection is the consumer's state to own; the row only reflects it. |
|
|
165
|
+
| `render` | `RenderProp` | — | no | Base UI composition prop — replaces the rendered `<div>` while merging props and classes. This is how a real ARIA tree supplies the semantics the standalone row deliberately does not force (`<li role="treeitem" aria-expanded aria-level>`). React-only; other frameworks use their own polymorphism. |
|
|
166
|
+
|
|
167
|
+
## Events
|
|
168
|
+
|
|
169
|
+
| Event | Payload | Description |
|
|
170
|
+
|-------|---------|-------------|
|
|
171
|
+
| `onClick` | `(event: MouseEvent) => void` | Fired when the row is clicked. Expanding and selecting are the consumer's to implement — the row emits nothing of its own for either. |
|
|
172
|
+
|
|
173
|
+
## Content slots
|
|
174
|
+
|
|
175
|
+
| Slot | Description |
|
|
176
|
+
|------|-------------|
|
|
177
|
+
| `default` | The trailing extras slot: action buttons, a count badge. Rendered only when `hasExtras` is true. |
|
|
178
|
+
|
|
179
|
+
## Behavior
|
|
180
|
+
|
|
181
|
+
### Renders one row
|
|
182
|
+
|
|
183
|
+
- **Given** no props
|
|
184
|
+
**Then** the row renders the expand chevron (`isExpandable` defaults to true),
|
|
185
|
+
the default `title`, and an empty extras slot — exactly the Figma default.
|
|
186
|
+
- **Given** a `title`
|
|
187
|
+
**Then** it renders in the row's remaining width and truncates rather than
|
|
188
|
+
wrapping or widening the row.
|
|
189
|
+
- **Given** `hasIcon`
|
|
190
|
+
**Then** the leading icon slot renders, using `icon` if supplied and the
|
|
191
|
+
design's square-dashed placeholder otherwise.
|
|
192
|
+
- **Given** an `icon` **but not** `hasIcon`
|
|
193
|
+
**Then** nothing renders — `hasIcon` is the gate, matching the Figma boolean.
|
|
194
|
+
- **Given** `hasCheckbox`
|
|
195
|
+
**Then** the shared `Checkbox` renders before the icon, unlabelled, with its
|
|
196
|
+
`aria-label` defaulting to `title`.
|
|
197
|
+
- **Given** `checkboxProps`
|
|
198
|
+
**Then** they are forwarded verbatim to that checkbox, including an
|
|
199
|
+
`aria-label` that overrides the `title` default. The row itself keeps no
|
|
200
|
+
checked state.
|
|
201
|
+
- **Given** `isExpandable={false}`
|
|
202
|
+
**Then** the chevron is not rendered and the row reads as a leaf.
|
|
203
|
+
- **Then** the slot order is always chevron, checkbox, icon, title, extras.
|
|
204
|
+
|
|
205
|
+
### The extras slot is gated, not hidden
|
|
206
|
+
|
|
207
|
+
- **Given** `children` **and** `hasExtras` (the default)
|
|
208
|
+
**Then** the trailing slot renders them, reserving a 16px minimum so an empty
|
|
209
|
+
slot still keeps a column of rows aligned.
|
|
210
|
+
- **Given** `children` **and** `hasExtras={false}`
|
|
211
|
+
**Then** the slot and the children are removed from the DOM entirely — a
|
|
212
|
+
button in there is not merely invisible, it does not exist, so it cannot be
|
|
213
|
+
tabbed to.
|
|
214
|
+
|
|
215
|
+
### Selection is a prop, not an interaction
|
|
216
|
+
|
|
217
|
+
- **Given** `selected={false}` (the default)
|
|
218
|
+
**Then** the row background is transparent.
|
|
219
|
+
- **Given** the pointer over the row
|
|
220
|
+
**Then** the background takes the hover token — regardless of `selected`,
|
|
221
|
+
since the design draws hover as its own swatch.
|
|
222
|
+
- **Given** `selected`
|
|
223
|
+
**Then** the background takes the highlighted `--ui-background-surface-active`
|
|
224
|
+
fill persistently, and the row carries `data-selected` so a consumer can key
|
|
225
|
+
off it. This is the same fill the Figma `state=active` swatch previews.
|
|
226
|
+
- **Given** a click
|
|
227
|
+
**Then** `onClick` fires and nothing else changes: the row does not select
|
|
228
|
+
itself. Selection belongs to the tree the consumer composes.
|
|
229
|
+
|
|
230
|
+
### Expansion is reflected, not implemented
|
|
231
|
+
|
|
232
|
+
- **Given** a click on the chevron
|
|
233
|
+
**Then** nothing expands. The chevron is `aria-hidden` artwork; the row renders
|
|
234
|
+
no nested list and emits no expand event, matching the Figma node, whose
|
|
235
|
+
expandable slot has no interaction either.
|
|
236
|
+
- The consumer owns the expand state, renders the child level itself, and puts
|
|
237
|
+
`aria-expanded` on the element it supplies through `render`.
|
|
238
|
+
- **Given** `expanded={false}` (the default)
|
|
239
|
+
**Then** the chevron points along the inline-end direction — the collapsed
|
|
240
|
+
affordance.
|
|
241
|
+
- **Given** `expanded`
|
|
242
|
+
**Then** the chevron rotates a quarter turn to point down, so the affordance
|
|
243
|
+
agrees with the `aria-expanded` the consumer publishes. It is a reflection of
|
|
244
|
+
the consumer's state, not a state the row holds: nothing else about the row
|
|
245
|
+
changes, no nested list appears, and no event fires.
|
|
246
|
+
- **Given** `expanded` **and** `isExpandable={false}`
|
|
247
|
+
**Then** it has no effect — there is no chevron to rotate.
|
|
248
|
+
|
|
249
|
+
### Focus
|
|
250
|
+
|
|
251
|
+
- **Given** the default rendering
|
|
252
|
+
**Then** the row is not in the tab order — it sets no `tabIndex` and no role.
|
|
253
|
+
- **Given** a consumer-supplied `tabIndex` (or a composed focusable element)
|
|
254
|
+
**Then** keyboard focus paints the library's standard 3px `--ui-focus-primary`
|
|
255
|
+
ring, flush to the row's 2px radius. Mouse focus does not, because it is a
|
|
256
|
+
`:focus-visible` ring.
|
|
257
|
+
|
|
258
|
+
### RTL
|
|
259
|
+
|
|
260
|
+
- **Given** `dir="rtl"`
|
|
261
|
+
**Then** the whole row mirrors: the slot order, the gap, and the padding are
|
|
262
|
+
all logical, and the collapsed chevron artwork itself flips via an explicit
|
|
263
|
+
`rtl:rotate-180` — logical layout alone would leave it pointing the wrong way.
|
|
264
|
+
- **Given** `dir="rtl"` **and** `expanded`
|
|
265
|
+
**Then** the chevron points down, the same as in LTR: "open" is a
|
|
266
|
+
direction-agnostic direction, so the expanded rotation replaces the RTL mirror
|
|
267
|
+
rather than compounding with it.
|
|
268
|
+
|
|
269
|
+
### Composition
|
|
270
|
+
|
|
271
|
+
- **Given** a `render` prop
|
|
272
|
+
**Then** the rendered element is replaced (e.g. by `<li role="treeitem">`) and
|
|
273
|
+
the row's props, classes, and data attributes merge onto it.
|
|
274
|
+
- **Given** native `<div>` attributes (`onClick`, `tabIndex`, `aria-*`,
|
|
275
|
+
`className`)
|
|
276
|
+
**Then** they pass through, and `className` merges with the row's own classes.
|
|
277
|
+
|
|
278
|
+
### Not owned here
|
|
279
|
+
|
|
280
|
+
- The tree: its `role="tree"` owner, indentation, `aria-level`, keyboard roving,
|
|
281
|
+
and type-ahead. The consumer composes rows and supplies all of it.
|
|
282
|
+
- Expand/collapse state and the nested list. The row only mirrors that state
|
|
283
|
+
visually, through `expanded`.
|
|
284
|
+
- Selection state, single vs. multi-select, and the checkbox's checked value.
|
|
285
|
+
|
|
286
|
+
## Accessibility
|
|
287
|
+
|
|
288
|
+
- **No `role="treeitem"` is forced, on purpose.** A tree item is only valid
|
|
289
|
+
inside a `role="tree"` (or `role="group"`) owner, and to be announced usefully
|
|
290
|
+
it needs `aria-expanded`, `aria-level`, `aria-selected`, and a roving
|
|
291
|
+
`tabindex` — none of which a standalone row can know. Stamping the role on an
|
|
292
|
+
orphan row would produce an **invalid** tree, not an accessible one. The row is
|
|
293
|
+
a plain `<div>`, and `render` is how the consumer supplies the real semantics:
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
<ul role="tree" aria-label="Workloads">
|
|
297
|
+
<TreeItem render={<li role="treeitem" aria-expanded={open} aria-level={1} />} … />
|
|
298
|
+
</ul>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
This is the same boundary `Breadcrumb` draws — it ships `nav`/`ol`/`li` and the
|
|
302
|
+
current-page part, not the trail — and `StepperItem`, which documents that
|
|
303
|
+
sequence semantics belong to the container.
|
|
304
|
+
|
|
305
|
+
- **Not in the tab order by default.** The row sets no `tabIndex`, so it never
|
|
306
|
+
becomes a silent tab stop inside a tree that manages focus itself. Pass
|
|
307
|
+
`tabIndex` (or compose a focusable element) when your tree wants the row
|
|
308
|
+
focusable; a roving `tabindex` — exactly one focusable row — is the pattern the
|
|
309
|
+
ARIA tree view calls for.
|
|
310
|
+
|
|
311
|
+
- **Keyboard focus is visible.** The row carries the library's standard 3px
|
|
312
|
+
`--ui-focus-primary` `focus-visible` ring, the same one `Button` and
|
|
313
|
+
`BreadcrumbLink` use, so a focusable row is never focused invisibly.
|
|
314
|
+
|
|
315
|
+
- **The chevron is `aria-hidden` decoration.** It is a visual affordance only:
|
|
316
|
+
the row implements no expand/collapse, so there is no state for it to announce.
|
|
317
|
+
Announce expansion with `aria-expanded` on the element you compose — that is
|
|
318
|
+
where the state actually lives. Do **not** give the chevron its own button
|
|
319
|
+
role; a row that only _looks_ expandable would then promise an action nothing
|
|
320
|
+
handles.
|
|
321
|
+
|
|
322
|
+
- **Keep `expanded` and `aria-expanded` in step.** `expanded` rotates the chevron
|
|
323
|
+
but publishes nothing to assistive technology; `aria-expanded` on your `render`
|
|
324
|
+
element publishes the state but paints nothing. They are two renderings of the
|
|
325
|
+
one boolean you own, so drive both from it (`aria-expanded={open}` +
|
|
326
|
+
`expanded={open}`). Setting only one leaves the sighted affordance and the
|
|
327
|
+
announced state disagreeing — which is the bug `expanded` exists to close.
|
|
328
|
+
|
|
329
|
+
- **The checkbox is always named.** Composed without a visible label, it would
|
|
330
|
+
otherwise be an unnamed control, so its `aria-label` defaults to `title`.
|
|
331
|
+
Override it through `checkboxProps` when the row title is not the right name
|
|
332
|
+
for the box (e.g. `aria-label="Select all workloads"`).
|
|
333
|
+
|
|
334
|
+
- **Selection is announced by the consumer.** `selected` is a _visual_ fill.
|
|
335
|
+
Colour alone is not a status, so put `aria-selected` on the element you compose
|
|
336
|
+
via `render` (and `aria-current` if the row marks a location rather than a
|
|
337
|
+
selection). The row also exposes `data-selected` for styling and testing.
|
|
338
|
+
|
|
339
|
+
- **One localizable string.** `title` is the only text the component renders on
|
|
340
|
+
its own, and the literal `'Title'` appears only as that prop's default —
|
|
341
|
+
callers pass a localized value. Everything else (`children`, `icon`,
|
|
342
|
+
`checkboxProps['aria-label']`) is consumer-supplied.
|
|
343
|
+
|
|
344
|
+
- **RTL**: the slot order, gap, and padding are logical, so the row mirrors under
|
|
345
|
+
`dir="rtl"` on its own. The collapsed chevron is direction-sensitive
|
|
346
|
+
**artwork**, which logical properties cannot mirror, so it carries an explicit
|
|
347
|
+
`rtl:rotate-180`. The `expanded` chevron points down in both directions —
|
|
348
|
+
"open" has no inline direction to mirror.
|
|
349
|
+
|
|
350
|
+
## Contrast
|
|
351
|
+
|
|
352
|
+
The title uses `--ui-text-on-surface-primary` and the glyphs
|
|
353
|
+
`--ui-glyph-on-surface-primary`, both over the row's transparent, hover, or
|
|
354
|
+
`selected` background (`--ui-background-surface-hover` /
|
|
355
|
+
`--ui-background-surface-active`). All are semantic-tier tokens defined per brand
|
|
356
|
+
and light/dark mode via `light-dark()`. Re-check the title and glyph contrast
|
|
357
|
+
against the **selected** fill specifically when a brand overrides either — the
|
|
358
|
+
idle case sits on the page surface, the selected case does not. Should a
|
|
359
|
+
dedicated `--ui-tree-item-*` tier land (see `tokens.yaml`), re-check all pairs.
|