@ahrowe/ui 0.26.0 → 0.28.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/esm/common/accordion/accordion.mjs +1 -1
- package/dist/esm/common/accordion/accordion.mjs.map +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
- package/dist/esm/common/confirmModal/confirmModal.mjs +1 -1
- package/dist/esm/common/confirmModal/confirmModal.mjs.map +1 -1
- package/dist/esm/common/dropZone/dropZone.mjs +1 -1
- package/dist/esm/common/dropZone/dropZone.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.mesh.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.mesh.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs.map +1 -0
- package/dist/esm/common/hooks/useAnchorTracking.mjs +2 -0
- package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -0
- package/dist/esm/common/idleManager/idleManager.mjs +1 -1
- package/dist/esm/common/idleManager/idleManager.mjs.map +1 -1
- package/dist/esm/common/input/input.mjs +1 -1
- package/dist/esm/common/input/input.mjs.map +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs.map +1 -1
- package/dist/esm/common/multiSelect/multiSelect.mjs +2 -0
- package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -0
- package/dist/esm/common/multiSelect/multiSelect.module.mjs +2 -0
- package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -0
- package/dist/esm/common/overscroll/overscroll.mjs +1 -1
- package/dist/esm/common/overscroll/overscroll.mjs.map +1 -1
- package/dist/esm/common/pagination/pagination.mjs +2 -0
- package/dist/esm/common/pagination/pagination.mjs.map +1 -0
- package/dist/esm/common/pagination/pagination.module.mjs +2 -0
- package/dist/esm/common/pagination/pagination.module.mjs.map +1 -0
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.module.mjs.map +1 -1
- package/dist/esm/common/roomViewer/roomViewer.module.mjs.map +1 -1
- package/dist/esm/common/timer/timer.mjs.map +1 -1
- package/dist/esm/common/tree/flattenTree.mjs +2 -0
- package/dist/esm/common/tree/flattenTree.mjs.map +1 -0
- package/dist/esm/common/tree/tree.mjs +2 -0
- package/dist/esm/common/tree/tree.mjs.map +1 -0
- package/dist/esm/common/tree/tree.module.mjs +2 -0
- package/dist/esm/common/tree/tree.module.mjs.map +1 -0
- package/dist/esm/common/virtualList/virtualList.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualRow.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/services/formValidation/validatableComponent.mjs.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/configProvider/configProvider.types.d.ts +6 -0
- package/dist/types/package/common/floatingMenu/useFloatingPosition.d.ts +4 -6
- package/dist/types/package/common/floorPlan/floorPlan.graph.d.ts +45 -0
- package/dist/types/package/common/floorPlan/floorPlan.mesh.d.ts +36 -0
- package/dist/types/package/common/floorPlan/floorPlan.triangulate.d.ts +11 -0
- package/dist/types/package/common/floorPlan/index.d.ts +4 -2
- package/dist/types/package/common/hooks/useAnchorTracking.d.ts +40 -0
- package/dist/types/package/common/inputDropdown/inputDropdown.types.d.ts +6 -0
- package/dist/types/package/common/multiSelect/index.d.ts +2 -0
- package/dist/types/package/common/multiSelect/multiSelect.d.ts +4 -0
- package/dist/types/package/common/multiSelect/multiSelect.types.d.ts +62 -0
- package/dist/types/package/common/pagination/index.d.ts +2 -0
- package/dist/types/package/common/pagination/pagination.d.ts +4 -0
- package/dist/types/package/common/pagination/pagination.types.d.ts +30 -0
- package/dist/types/package/common/popover/usePopoverPosition.d.ts +4 -2
- package/dist/types/package/common/tree/flattenTree.d.ts +3 -0
- package/dist/types/package/common/tree/index.d.ts +3 -0
- package/dist/types/package/common/tree/tree.d.ts +4 -0
- package/dist/types/package/common/tree/tree.types.d.ts +62 -0
- package/dist/types/package/common/virtualList/virtualList.types.d.ts +8 -0
- package/dist/types/package/common/virtualList/virtualRow.d.ts +1 -1
- package/dist/types/package/index.d.ts +6 -0
- package/docs/CLAUDE.md +3 -0
- package/docs/FloorPlan.md +39 -1
- package/docs/InputDropdown.md +3 -0
- package/docs/MultiSelect.md +100 -0
- package/docs/Pagination.md +72 -0
- package/docs/RoomDrawer.md +14 -0
- package/docs/Tree.md +95 -0
- package/docs/VirtualList.md +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# MultiSelect
|
|
2
|
+
|
|
3
|
+
**When to use:** Pick several options from a list. Selections appear as removable chips in the control, and the list opens in a panel below it. For a single choice use [Dropdown](Dropdown.md); for a handful of options that should stay visible use [OptionPicker](OptionPicker.md) or [Checkbox](Checkbox.md); for type-ahead against one value use [InputDropdown](InputDropdown.md).
|
|
4
|
+
|
|
5
|
+
**Keywords:** tags, token input, multi-value, pick many, select multiple, facet, chips picker
|
|
6
|
+
|
|
7
|
+
**Import:** `import { MultiSelect } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { MultiSelectProps, MultiSelectItem, MultiSelectValue } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
**Requires:** `<div id="bodyEnd"></div>` in your app — the panel renders through a portal.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { MultiSelect } from '@ahrowe/ui';
|
|
15
|
+
import type { MultiSelectItem, MultiSelectValue } from '@ahrowe/ui';
|
|
16
|
+
|
|
17
|
+
const items: MultiSelectItem[] = [
|
|
18
|
+
{ key: 'apple', label: 'Apple' },
|
|
19
|
+
{ key: 'banana', label: 'Banana' },
|
|
20
|
+
{ key: 'fig', label: 'Fig', disabled: true },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Controlled
|
|
24
|
+
const [value, setValue] = useState<MultiSelectValue[]>([]);
|
|
25
|
+
<MultiSelect items={items} value={value} onChange={setValue} label="Fruit" placeholder="Pick some" />
|
|
26
|
+
|
|
27
|
+
// Uncontrolled
|
|
28
|
+
<MultiSelect items={items} defaultValue={['apple']} onChange={(value) => console.log(value)} />
|
|
29
|
+
|
|
30
|
+
// Searchable, for a list too long to scan
|
|
31
|
+
<MultiSelect items={countries} searchable searchPlaceholder="Filter countries…" />
|
|
32
|
+
|
|
33
|
+
// Tag entry: type a value that isn't in `items` and pick the "add" row
|
|
34
|
+
<MultiSelect items={commonTags} allowCustomValues createLabel={(value) => `Neu: ${value}`} />
|
|
35
|
+
|
|
36
|
+
// Cap the selection — unselected options disable once the cap is reached
|
|
37
|
+
<MultiSelect items={items} maxSelected={3} />
|
|
38
|
+
|
|
39
|
+
// Connected to a FormValidator, like Input and Dropdown
|
|
40
|
+
<MultiSelect items={items} formValidator={form.fruit} label="Fruit" />
|
|
41
|
+
|
|
42
|
+
// Manual error state, when there's no validator
|
|
43
|
+
<MultiSelect items={items} isValid={false} errorMessage="Pick at least one" />
|
|
44
|
+
|
|
45
|
+
// Always a bottom sheet, rather than only on a small touch screen
|
|
46
|
+
import { Presentation } from '@ahrowe/ui';
|
|
47
|
+
<MultiSelect items={items} presentation={Presentation.Sheet} />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Value:** `onChange` gets the full new selection as an array of `key`s, in the order the user picked them, and the chips render in that same order. It is never called with a partial change, so `onChange={setValue}` is all a controlled field needs.
|
|
51
|
+
|
|
52
|
+
**The label floats like `Input`'s.** It rests vertically centred over the control and rises into a notch cut out of the border once the field is focused, open, or has a selection, using the same `fieldset`/`legend` construction and the same `--input-notch-height` geometry as `Input`. A `MultiSelect` sitting next to an `Input` in a form animates with it rather than against it. Set `alwaysFloatLabel` to pin it up, which is what you want when a `placeholder` should always be readable: with a label present the placeholder is otherwise faded in on a delay matching the label's animation, so the two never sit on top of each other. Without a label there is nothing to wait for and it fades in at once.
|
|
53
|
+
|
|
54
|
+
**Errors:** with a `formValidator` the field stays quiet until it has been touched (the first time the panel closes), then shows the validator's message while the field is hovered or focused, and drops it as soon as the validator is happy again. Without a validator, `isValid` and `errorMessage` are plain props and nothing clears them for you: derive them from your own state, as in `isValid={picked.length > 0}`.
|
|
55
|
+
|
|
56
|
+
**Height:** a MultiSelect with one row of chips is exactly as tall as an `Input` or `Dropdown` beside it, so a form row lines up. It grows only when the chips wrap onto a second row. The chips render at `0.8em` to fit: a standalone `Chip` is nearly as tall as the whole field, because its remove button carries `ActionIcon`'s padding around a `1em` icon.
|
|
57
|
+
|
|
58
|
+
**Custom values** (`allowCustomValues`) turn this into a tag input. Anything typed that matches no item's label or key, and isn't already selected, gets an extra row at the bottom of the panel offering to add it; picking that row commits the typed string as its own key and clears the box. It goes through the same highlight and `Enter` path as a normal option, so the keyboard works unchanged, and `maxSelected` still caps it. Because a custom value has no entry in `items`, its chip falls back to showing the value itself. Without `allowCustomValues`, a selected key that isn't in `items` renders no chip at all.
|
|
59
|
+
|
|
60
|
+
**Item labels are strings, not nodes.** The same text renders twice, in the option row and inside the `Chip`, and `Chip` takes a string. Reach for `Dropdown` when an option needs rich content.
|
|
61
|
+
|
|
62
|
+
**Key props:**
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Description |
|
|
65
|
+
|------|------|-------------|
|
|
66
|
+
| `items` | `MultiSelectItem[]` | The options: `{ key, label, disabled? }` |
|
|
67
|
+
| `value` | `MultiSelectValue[]` | Controlled selection (omit for uncontrolled) |
|
|
68
|
+
| `defaultValue` | `MultiSelectValue[]` | Initial selection when uncontrolled (default `[]`) |
|
|
69
|
+
| `onChange` | `(value: MultiSelectValue[]) => void` | Fires with the full new selection |
|
|
70
|
+
| `label` | `string` | Field label. Rests inside the control, floats into the border notch |
|
|
71
|
+
| `alwaysFloatLabel` | `boolean` | Keep the label floating even when empty and unfocused (default `false`) |
|
|
72
|
+
| `placeholder` | `string` | Shown while nothing is selected. With a `label`, it fades in only after the label has finished floating clear |
|
|
73
|
+
| `searchable` | `boolean` | Show a search box in the panel (default `false`) |
|
|
74
|
+
| `allowCustomValues` | `boolean` | Let the user add a value not in `items`. Implies `searchable` |
|
|
75
|
+
| `createLabel` | `(value: string) => string` | Label for the add row (default `Add "…"`) |
|
|
76
|
+
| `searchPlaceholder` | `string` | Placeholder for that box (default `'Search…'`) |
|
|
77
|
+
| `emptyLabel` | `string` | Shown when nothing matches (default `'No matches'`) |
|
|
78
|
+
| `maxSelected` | `number` | Cap the selection; unselected options disable at the cap |
|
|
79
|
+
| `clearable` | `boolean` | Show the clear-all button (default `true`) |
|
|
80
|
+
| `disabled` | `boolean` | Dims the field and blocks interaction |
|
|
81
|
+
| `readOnly` | `boolean` | Shows the selection, allows no changes, no dimming |
|
|
82
|
+
| `presentation` | `Presentation` | Anchored panel, bottom sheet, or sheet only on small touch screens |
|
|
83
|
+
| `formValidator` | `FormValidator \| null` | Owns the value when present. See [FormValidator.md](FormValidator.md) |
|
|
84
|
+
| `errorMessage` | `string` | Manual error, shown in a tooltip when there's no validator |
|
|
85
|
+
| `isValid` | `boolean` | Manual valid state (default `true`) |
|
|
86
|
+
|
|
87
|
+
**Keyboard:** `↓` or `Enter` opens the panel. `↑`/`↓` move the highlight and wrap around, `Home`/`End` jump to the ends, `Enter` toggles the highlighted option, `Escape` closes. `Backspace` removes the last selection when the search box is empty. With `searchable`, focus moves into the search box on open and the same keys work from there.
|
|
88
|
+
|
|
89
|
+
**Accessibility:** the control is a `role="combobox"` with `aria-expanded`, `aria-haspopup="listbox"` and `aria-controls`; the panel is a `role="listbox"` with `aria-multiselectable="true"`, and each option carries `aria-selected`. The highlighted option is reported through `aria-activedescendant`, so focus stays on the control (or the search box) rather than moving between options. Each chip's remove button is labelled `Remove <label>`.
|
|
90
|
+
|
|
91
|
+
**Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
|
|
92
|
+
|
|
93
|
+
| Variable | Falls back to |
|
|
94
|
+
|----------|---------------|
|
|
95
|
+
| `--multi-select-min-height` | `calc(1.25em + 14px)`, the same height `Input` and `Dropdown` resolve to |
|
|
96
|
+
| `--multi-select-panel-height` | `260px` |
|
|
97
|
+
|
|
98
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ MultiSelect: { searchable: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
99
|
+
|
|
100
|
+
**Slots:** `root` `label` `fieldset` `control` `values` `chip` `placeholder` `clearButton` `icon` `panel` `search` `options` `option` `optionCheck` `optionLabel` `empty`
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
**When to use:** Move through server-paged results a page at a time — a results list, an admin table, a report. For endless scrolling instead, use `InfiniteBlock`; for a long list that's all in memory, use `VirtualList`.
|
|
4
|
+
|
|
5
|
+
**Keywords:** pager, paging, page numbers, next page, previous page, page navigation, offset, results
|
|
6
|
+
|
|
7
|
+
**Import:** `import { Pagination } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { PaginationProps } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Pagination } from '@ahrowe/ui';
|
|
13
|
+
|
|
14
|
+
// Controlled — the usual shape, since the page drives a fetch
|
|
15
|
+
const [page, setPage] = useState(1);
|
|
16
|
+
<Pagination total={20} page={page} onChange={setPage} />
|
|
17
|
+
|
|
18
|
+
// Uncontrolled
|
|
19
|
+
<Pagination total={20} defaultPage={3} onChange={(page) => console.log(page)} />
|
|
20
|
+
|
|
21
|
+
// Total pages from a row count
|
|
22
|
+
<Pagination total={Math.ceil(rowCount / pageSize)} page={page} onChange={setPage} />
|
|
23
|
+
|
|
24
|
+
// First/last jump controls outside the previous/next ones
|
|
25
|
+
<Pagination total={50} page={page} onChange={setPage} showEdges />
|
|
26
|
+
|
|
27
|
+
// Page numbers only, no previous/next
|
|
28
|
+
<Pagination total={20} page={page} onChange={setPage} showControls={false} />
|
|
29
|
+
|
|
30
|
+
// Narrower — one page either side of the current one is already the default, `0` shows just the current
|
|
31
|
+
<Pagination total={100} page={page} onChange={setPage} siblings={0} />
|
|
32
|
+
|
|
33
|
+
// Wider window and more pages pinned at each end
|
|
34
|
+
<Pagination total={100} page={page} onChange={setPage} siblings={2} boundaries={2} />
|
|
35
|
+
|
|
36
|
+
// Disabled while the page's data is loading
|
|
37
|
+
<Pagination total={20} page={page} onChange={setPage} disabled={isLoading} />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Which pages are shown:** the first and last `boundaries` pages, the current page with `siblings` pages either side, and a `…` wherever a run was collapsed. A gap only appears when it replaces more than one page — collapsing a single page would take the same width and hide a reachable page, so that side lists a fixed-size block instead. With the defaults (`siblings={1}`, `boundaries={1}`) the control is always 7 page slots wide, so it never reflows as the user pages through.
|
|
41
|
+
|
|
42
|
+
**Key props:**
|
|
43
|
+
|
|
44
|
+
| Prop | Type | Description |
|
|
45
|
+
|------|------|-------------|
|
|
46
|
+
| `total` | `number` | Total number of pages (not rows). Renders nothing below `1` |
|
|
47
|
+
| `page` | `number` | Controlled current page, 1-based (omit for uncontrolled). Clamped to `1…total` |
|
|
48
|
+
| `defaultPage` | `number` | Initial page when uncontrolled (default `1`) |
|
|
49
|
+
| `onChange` | `(page: number) => void` | Fires with the new page; not called when the current page is clicked again |
|
|
50
|
+
| `siblings` | `number` | Pages shown either side of the current one (default `1`) |
|
|
51
|
+
| `boundaries` | `number` | Pages always shown at the start and end (default `1`) |
|
|
52
|
+
| `showControls` | `boolean` | Show the previous/next controls (default `true`) |
|
|
53
|
+
| `showEdges` | `boolean` | Show first/last jump controls outside the previous/next ones (default `false`) |
|
|
54
|
+
| `disabled` | `boolean` | Disables every button |
|
|
55
|
+
| `previousIcon` / `nextIcon` | `IconDefinition \| ReactElement` | Override the previous/next icons (default chevrons) |
|
|
56
|
+
| `firstIcon` / `lastIcon` | `IconDefinition \| ReactElement` | Override the first/last icons (default double chevrons) |
|
|
57
|
+
| `aria-label` | `string` | Accessible name for the `<nav>` (default `'Pagination'`) |
|
|
58
|
+
|
|
59
|
+
**Accessibility:** renders a `<nav>` of `<button>`s. The current page carries `aria-current="page"`; each page button is labelled `Go to page N`, the controls `Go to previous page` / `Go to next page` / `Go to first page` / `Go to last page`. Previous/next are disabled at the respective end rather than hidden, so the control doesn't reflow. The `…` is `aria-hidden`.
|
|
60
|
+
|
|
61
|
+
**Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
|
|
62
|
+
|
|
63
|
+
| Variable | Falls back to |
|
|
64
|
+
|----------|---------------|
|
|
65
|
+
| `--pagination-active-background` | `var(--primary-color)` |
|
|
66
|
+
| `--pagination-active-color` | `var(--text-on-primary)` |
|
|
67
|
+
| `--pagination-size` | `36px` |
|
|
68
|
+
| `--pagination-gap` | `4px` |
|
|
69
|
+
|
|
70
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Pagination: { showEdges: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
71
|
+
|
|
72
|
+
**Slots:** `root` `item` `control` `ellipsis`
|
package/docs/RoomDrawer.md
CHANGED
|
@@ -47,6 +47,19 @@ const [plan, setPlan] = useState<FloorPlan>(() => emptyFloorPlan());
|
|
|
47
47
|
- an opening slides along its wall
|
|
48
48
|
- a room moves every corner it owns
|
|
49
49
|
|
|
50
|
+
**Hold Ctrl/Cmd while dragging a wall to detach it instead.** The wall comes off the
|
|
51
|
+
corners it shares, the neighbours keep their ends, and a connecting wall grows between
|
|
52
|
+
each old corner and the new one. Use it when a partition joins the wall being moved: a
|
|
53
|
+
plain drag takes the partition's end along with it, a detach drag leaves it where it is.
|
|
54
|
+
|
|
55
|
+
Pulling a wall off a plain corner is deliberately a no-op on the drawing: the connector
|
|
56
|
+
continues the wall that was already there, so the seam it leaves is straight and is
|
|
57
|
+
dissolved on the spot. Nothing is detached until the wall has moved at least one grid
|
|
58
|
+
step, so a Ctrl-click on a wall is still just a selection toggle. Push the wall back onto
|
|
59
|
+
the line it came off and the corners weld back together, whether that happens in the same
|
|
60
|
+
drag or a later one — the corner that survives is the one that was already there.
|
|
61
|
+
Grabbing a selected wall's midpoint handle always adds a corner, Ctrl or not.
|
|
62
|
+
|
|
50
63
|
## Measuring without drawing
|
|
51
64
|
|
|
52
65
|
The `measure` tool is a ruler. Drag it across the plan and it reports length and angle, snapping to corners, walls and the grid like everything else, with `Shift` for a fixed angle.
|
|
@@ -127,6 +140,7 @@ The `snap`, `angleLock` and `showGrid` props seed the toggles; changing one rese
|
|
|
127
140
|
| `S` / `L` / `G` | Toggle snapping / angle lock / grid |
|
|
128
141
|
| `Ctrl/Cmd+A` | Select everything |
|
|
129
142
|
| `Ctrl/Cmd`-click | Add or remove one thing from the selection |
|
|
143
|
+
| `Ctrl/Cmd`-drag a wall | Detach it from its corners instead of dragging them along |
|
|
130
144
|
| `Shift` (hold) | Momentarily invert angle lock |
|
|
131
145
|
| `Alt` (hold) | Momentarily invert snapping |
|
|
132
146
|
| `Esc` | Cancel the wall chain, or clear the selection |
|
package/docs/Tree.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Tree
|
|
2
|
+
|
|
3
|
+
**When to use:** A hierarchical list the user can expand, collapse and pick from: categories, nested locations, folder structures, org charts. For a flat list of thousands of rows use [VirtualList](VirtualList.md); for picking several unrelated values use [MultiSelect](MultiSelect.md).
|
|
4
|
+
|
|
5
|
+
**Keywords:** hierarchy, directory, expand collapse, parent child, drilldown, navigation sidebar, outline, subfolder
|
|
6
|
+
|
|
7
|
+
**Import:** `import { Tree, flattenTree } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { TreeProps, TreeNode, TreeValue, FlatTreeNode } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Tree } from '@ahrowe/ui';
|
|
13
|
+
import type { TreeNode, TreeValue } from '@ahrowe/ui';
|
|
14
|
+
|
|
15
|
+
const nodes: TreeNode[] = [
|
|
16
|
+
{
|
|
17
|
+
key: 'hall-1',
|
|
18
|
+
label: 'Hall 1',
|
|
19
|
+
children: [
|
|
20
|
+
{ key: 'bay-a', label: 'Bay A' },
|
|
21
|
+
{ key: 'bay-b', label: 'Bay B' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{ key: 'archive', label: 'Archive', disabled: true },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Controlled selection
|
|
28
|
+
const [selected, setSelected] = useState<TreeValue | null>(null);
|
|
29
|
+
<Tree nodes={nodes} selectedKey={selected} onSelect={setSelected} aria-label="Locations" />
|
|
30
|
+
|
|
31
|
+
// Uncontrolled, with some branches open to begin with
|
|
32
|
+
<Tree nodes={nodes} defaultExpandedKeys={['hall-1']} defaultSelectedKey="bay-a" />
|
|
33
|
+
|
|
34
|
+
// Controlled expansion, e.g. to persist it
|
|
35
|
+
<Tree nodes={nodes} expandedKeys={open} onExpandedChange={setOpen} />
|
|
36
|
+
|
|
37
|
+
// Clicking a branch selects it without opening it
|
|
38
|
+
<Tree nodes={nodes} expandOnSelect={false} />
|
|
39
|
+
|
|
40
|
+
// Icons per node, and a wider indent
|
|
41
|
+
import { faWarehouse } from '@fortawesome/free-solid-svg-icons';
|
|
42
|
+
<Tree nodes={nodes.map((n) => ({ ...n, icon: faWarehouse }))} indent={40} />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Branch or leaf:** a node is a branch when it has a `children` array, even an empty one. `children: []` renders a toggle with nothing behind it yet, which is what you want while a branch's contents are still loading; omit `children` entirely for a true leaf.
|
|
46
|
+
|
|
47
|
+
**Selection is single.** `onSelect` fires with the key and the node itself, so you do not have to look it up again. There is no multi-select or checkbox mode.
|
|
48
|
+
|
|
49
|
+
**Key props:**
|
|
50
|
+
|
|
51
|
+
| Prop | Type | Description |
|
|
52
|
+
|------|------|-------------|
|
|
53
|
+
| `nodes` | `TreeNode[]` | The tree: `{ key, label, icon?, children?, disabled? }` |
|
|
54
|
+
| `expandedKeys` | `TreeValue[]` | Controlled open branches (omit for uncontrolled) |
|
|
55
|
+
| `defaultExpandedKeys` | `TreeValue[]` | Branches open initially when uncontrolled (default `[]`) |
|
|
56
|
+
| `onExpandedChange` | `(keys: TreeValue[]) => void` | Fires with the full new set of open branches |
|
|
57
|
+
| `selectedKey` | `TreeValue \| null` | Controlled selection (omit for uncontrolled) |
|
|
58
|
+
| `defaultSelectedKey` | `TreeValue` | Selection when uncontrolled |
|
|
59
|
+
| `onSelect` | `(key: TreeValue, node: TreeNode) => void` | Fires with the picked key and node |
|
|
60
|
+
| `expandOnSelect` | `boolean` | Clicking a branch's row also toggles it (default `true`) |
|
|
61
|
+
| `indent` | `number` | Pixels per level (default `20`) |
|
|
62
|
+
| `toggleIcon` | `IconDefinition \| ReactElement` | Toggle icon, rotated 90° when open (default a right chevron) |
|
|
63
|
+
| `height` | `number \| string` | Windows the rows through `VirtualList` at this viewport height |
|
|
64
|
+
| `estimatedRowHeight` | `number` | Row height for the windowed renderer (default `32`) |
|
|
65
|
+
| `emptyLabel` | `string` | Shown when `nodes` is empty (default `'Nothing here'`) |
|
|
66
|
+
| `disabled` | `boolean` | Dims the tree and blocks interaction |
|
|
67
|
+
| `aria-label` | `string` | Accessible name for the tree (default `'Tree'`) |
|
|
68
|
+
|
|
69
|
+
**Keyboard:** `↑`/`↓` move between visible rows. `→` opens a closed branch, then steps into its first child. `←` closes an open branch, then steps out to its parent. `Home`/`End` jump to the ends. `Enter` or `Space` selects.
|
|
70
|
+
|
|
71
|
+
**Accessibility:** `role="tree"` containing `role="treeitem"` rows. Because the rows render flat rather than nested, each carries `aria-level`, `aria-posinset` and `aria-setsize`; branches also carry `aria-expanded`. Focus uses a roving tabindex, so one row at a time is reachable by Tab, and it is the selected one (or the first, before anything is selected). The toggle is `aria-hidden`, since `aria-expanded` on the row already reports the state.
|
|
72
|
+
|
|
73
|
+
**Why flat:** the rows are a flattened list, not nested `<div>`s. Keyboard movement becomes a step along one array instead of a tree walk, and a node's accessible name stays its own label rather than absorbing its descendants'.
|
|
74
|
+
|
|
75
|
+
**Very large trees:** set `height`. `Tree` then windows its rows through [VirtualList](VirtualList.md), so only the ones in view are in the DOM, and it keeps everything else: the ARIA, the expansion, the selection and the keyboard.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Tree nodes={nodes} height={300} expandedKeys={open} onExpandedChange={setOpen} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Two things change in that mode. Rows must be a uniform height, so size them with `--tree-node-height` rather than per row. And the tab stop moves from the selected row to the tree itself, because a roving tabindex on a row that is scrolled out of the window would leave the tree with no tab stop at all; tabbing in still lands on the selected row, which is scrolled into view first.
|
|
82
|
+
|
|
83
|
+
`flattenTree` is exported separately if you want the rows without the component: it returns `FlatTreeNode`s carrying `node`, `level`, `parentKey`, `isBranch`, `isExpanded`, `setSize` and `posInSet`, ready for `VirtualList`'s `items` and `getItemDepth` (which also feeds its `treeReorder` drag-and-drop). You then write the row markup, ARIA and keyboard handling yourself.
|
|
84
|
+
|
|
85
|
+
**Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
|
|
86
|
+
|
|
87
|
+
| Variable | Falls back to |
|
|
88
|
+
|----------|---------------|
|
|
89
|
+
| `--tree-node-height` | `32px` |
|
|
90
|
+
| `--tree-selected-background` | `var(--primary-color)` |
|
|
91
|
+
| `--tree-selected-color` | `var(--text-on-primary)` |
|
|
92
|
+
|
|
93
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Tree: { indent: 30 } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
94
|
+
|
|
95
|
+
**Slots:** `root` `node` `toggle` `icon` `label` `empty`
|
package/docs/VirtualList.md
CHANGED
|
@@ -139,6 +139,7 @@ const columns: VirtualListColumn<User>[] = [
|
|
|
139
139
|
| `onLoadMore` | `() => Promise<void>` | Triggered near the bottom, and repeatedly while the rows don't fill the viewport — append items in the handler |
|
|
140
140
|
| `loadMoreThreshold` | `number` | Distance from bottom that triggers `onLoadMore` (default `100`) |
|
|
141
141
|
| `isLoading` | `boolean` | Replaces list body with a full-height spinner |
|
|
142
|
+
| `ariaRoles` | `{ container: string; row: string }` | Replaces the roles put on the container and rows (default `list`/`listitem`, or `grid`/`row` with columns). Pass `'none'` for both when the rows carry their own semantics, as [Tree](Tree.md) does when windowing `treeitem` rows |
|
|
142
143
|
|
|
143
144
|
Drag-and-drop reordering adds `reorderable`, `treeReorder`, `dragHandle`, and their callbacks — see **Row reordering** below.
|
|
144
145
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahrowe/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"build:mcp": "vite build --config vite.mcp.config.ts",
|
|
56
56
|
"preview": "vite preview",
|
|
57
57
|
"gen-barrel": "tsx scripts/gen-barrel.ts",
|
|
58
|
-
"gc": "
|
|
58
|
+
"gc": "tsx scripts/generate-component.ts",
|
|
59
59
|
"changeset": "changeset",
|
|
60
60
|
"lint": "eslint .",
|
|
61
61
|
"lint:fix": "eslint . --fix",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"@types/react": "^19.2.15",
|
|
102
102
|
"@types/react-dom": "^19.2.3",
|
|
103
103
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
104
|
+
"@types/three": "^0.186.0",
|
|
104
105
|
"@vitejs/plugin-react": "^6.0.2",
|
|
105
106
|
"@vitest/ui": "^4.1.7",
|
|
106
107
|
"eslint": "^9.39.4",
|
|
@@ -118,6 +119,7 @@
|
|
|
118
119
|
"react-dom": "^19.2.6",
|
|
119
120
|
"react-element-to-jsx-string": "^17.0.1",
|
|
120
121
|
"react-syntax-highlighter": "^16.1.1",
|
|
122
|
+
"three": "^0.186.0",
|
|
121
123
|
"tsx": "^4.22.3",
|
|
122
124
|
"typescript": "^6.0.3",
|
|
123
125
|
"typescript-eslint": "^8.60.0",
|