@box-kite/react 1.0.0 → 2.0.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/.claude/rules/box-kite-rules.md +23 -1
- package/.claude/skills/box-kite/SKILL.md +134 -427
- package/.claude/skills/box-kite/references/components.md +235 -0
- package/.claude/skills/box-kite/references/extending.md +42 -0
- package/.claude/skills/box-kite/references/patterns.md +27 -0
- package/.claude/skills/box-kite/references/styling.md +222 -0
- package/.cursor/rules/box-kite.mdc +73 -0
- package/AGENTS.md +102 -0
- package/BOX_AI_CONTEXT.md +1124 -153
- package/BOX_KITE_AI_CONTEXT.md +1124 -153
- package/README.md +36 -2
- package/a11y.cjs +1 -1
- package/a11y.d.ts +1 -1
- package/a11y.mjs +3 -2
- package/anchor.cjs +1 -0
- package/anchor.d.ts +14 -0
- package/anchor.mjs +3 -0
- package/behavior.cjs +1 -1
- package/behavior.mjs +118 -136
- package/box.cjs +1 -1
- package/box.mjs +1 -1
- package/client.cjs +1 -1
- package/client.mjs +15 -14
- package/components/accordion.cjs +1 -0
- package/components/accordion.d.ts +114 -0
- package/components/accordion.mjs +251 -0
- package/components/button.cjs +1 -1
- package/components/button.d.ts +3 -0
- package/components/button.mjs +1 -1
- package/components/chart.cjs +1 -1
- package/components/chart.d.ts +2 -1
- package/components/chart.mjs +159 -184
- package/components/checkbox.cjs +1 -1
- package/components/checkbox.d.ts +8 -0
- package/components/checkbox.mjs +1 -1
- package/components/combobox/comboboxChips.d.ts +21 -0
- package/components/combobox/comboboxListbox.d.ts +47 -0
- package/components/combobox.cjs +1 -0
- package/components/combobox.d.ts +121 -0
- package/components/combobox.mjs +563 -0
- package/components/dataGrid/components/dataGridAggregateValue.d.ts +10 -0
- package/components/dataGrid/components/dataGridCell.d.ts +2 -1
- package/components/dataGrid/components/dataGridCellEditor.d.ts +15 -0
- package/components/dataGrid/components/dataGridCellPlaceholder.d.ts +14 -0
- package/components/dataGrid/components/dataGridCellTree.d.ts +16 -0
- package/components/dataGrid/components/dataGridDataSourceError.d.ts +12 -0
- package/components/dataGrid/components/dataGridExport.d.ts +10 -0
- package/components/dataGrid/components/dataGridFooter.d.ts +11 -0
- package/components/dataGrid/contracts/dataGridContract.d.ts +474 -16
- package/components/dataGrid/export/gridExport.d.ts +10 -0
- package/components/dataGrid/gridNavigationContext.d.ts +9 -1
- package/components/dataGrid/models/aggregateCellModel.d.ts +34 -0
- package/components/dataGrid/models/aggregationModel.d.ts +32 -0
- package/components/dataGrid/models/cellKeys.d.ts +11 -0
- package/components/dataGrid/models/cellModel.d.ts +27 -1
- package/components/dataGrid/models/columnModel.d.ts +12 -2
- package/components/dataGrid/models/dataSourceModel.d.ts +177 -0
- package/components/dataGrid/models/editModel.d.ts +77 -0
- package/components/dataGrid/models/exportModel.d.ts +51 -0
- package/components/dataGrid/models/gridModel.d.ts +108 -21
- package/components/dataGrid/models/groupRowCellModel.d.ts +11 -3
- package/components/dataGrid/models/groupRowModel.d.ts +22 -2
- package/components/dataGrid/models/rowModel.d.ts +13 -1
- package/components/dataGrid/models/sourceGroupRowModel.d.ts +45 -0
- package/components/dataGrid/models/sourceLevel.d.ts +71 -0
- package/components/dataGrid/models/sourceRowModel.d.ts +29 -0
- package/components/dataGrid/models/sourceTreeRowModel.d.ts +23 -0
- package/components/dataGrid/models/treeModel.d.ts +94 -0
- package/components/dataGrid/models/treeRow.d.ts +22 -0
- package/components/dataGrid/models/treeRowModel.d.ts +50 -0
- package/components/dataGrid.cjs +1 -1
- package/components/dataGrid.d.ts +4 -2
- package/components/dataGrid.mjs +4484 -2755
- package/components/dialog.cjs +1 -0
- package/components/dialog.d.ts +151 -0
- package/components/dialog.mjs +223 -0
- package/components/dropdown.cjs +1 -1
- package/components/dropdown.d.ts +30 -0
- package/components/dropdown.mjs +256 -244
- package/components/icon.cjs +1 -1
- package/components/icon.d.ts +2 -1
- package/components/icon.mjs +3 -3
- package/components/menu.cjs +1 -0
- package/components/menu.d.ts +234 -0
- package/components/menu.mjs +422 -0
- package/components/overlay.cjs +1 -1
- package/components/overlay.d.ts +30 -26
- package/components/overlay.mjs +45 -75
- package/components/popover.cjs +1 -0
- package/components/popover.d.ts +100 -0
- package/components/popover.mjs +69 -0
- package/components/presence.d.ts +6 -0
- package/components/progress.cjs +1 -0
- package/components/progress.d.ts +47 -0
- package/components/progress.mjs +28 -0
- package/components/radioButton.cjs +1 -1
- package/components/radioButton.d.ts +8 -0
- package/components/radioButton.mjs +1 -1
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.d.ts +15 -2
- package/components/radioGroup.mjs +34 -30
- package/components/select.d.ts +9 -0
- package/components/semantics.cjs +1 -1
- package/components/semantics.d.ts +33 -28
- package/components/semantics.mjs +3 -3
- package/components/slider.cjs +1 -0
- package/components/slider.d.ts +98 -0
- package/components/slider.mjs +250 -0
- package/components/svg.cjs +1 -1
- package/components/svg.d.ts +25 -23
- package/components/svg.mjs +3 -3
- package/components/switch.cjs +1 -1
- package/components/switch.d.ts +9 -0
- package/components/switch.mjs +1 -1
- package/components/tabs.cjs +1 -0
- package/components/tabs.d.ts +87 -0
- package/components/tabs.mjs +324 -0
- package/components/textarea.cjs +1 -1
- package/components/textarea.d.ts +14 -0
- package/components/textarea.mjs +1 -1
- package/components/textbox.cjs +1 -1
- package/components/textbox.d.ts +12 -0
- package/components/textbox.mjs +1 -1
- package/components/toaster.cjs +1 -0
- package/components/toaster.d.ts +106 -0
- package/components/toaster.mjs +295 -0
- package/components/tooltip.cjs +1 -1
- package/components/tooltip.d.ts +39 -4
- package/components/tooltip.mjs +56 -54
- package/docs/a11y.md +199 -0
- package/docs/anchor.md +139 -0
- package/docs/components.md +48 -0
- package/docs/index.md +11 -0
- package/docs/props.md +1777 -0
- package/effects.cjs +1 -1
- package/effects.mjs +1 -1
- package/gridExport.cjs +2 -0
- package/gridExport.mjs +244 -0
- package/identity.cjs +1 -0
- package/identity.mjs +12 -0
- package/keyboard.cjs +1 -0
- package/keyboard.mjs +30 -0
- package/layer.cjs +1 -0
- package/layer.mjs +127 -0
- package/motion.cjs +1 -1
- package/motion.mjs +1 -1
- package/package.json +134 -10
- package/placement.cjs +1 -0
- package/placement.mjs +180 -0
- package/platform.cjs +1 -1
- package/platform.mjs +18 -5
- package/react/anchor/useAnchorPosition.d.ts +79 -0
- package/react/dialog/useScrollLock.d.ts +12 -0
- package/react/hooks/usePortalContainer.d.ts +6 -1
- package/react/hooks/useVirtualization.d.ts +24 -35
- package/react/popover/usePopoverLayer.d.ts +87 -0
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +19 -2
- package/rsc.cjs +1 -1
- package/rsc.mjs +1 -1
- package/utils/accordion/accordionUtils.d.ts +48 -0
- package/utils/anchor/anchorUtils.d.ts +79 -0
- package/utils/array/lazyList.d.ts +10 -0
- package/utils/chart/chartPalette.d.ts +15 -0
- package/utils/chart/chartUtils.d.ts +2 -8
- package/utils/combobox/comboboxUtils.d.ts +89 -0
- package/utils/dom/domUtils.d.ts +6 -0
- package/utils/environment/environmentUtils.d.ts +10 -0
- package/utils/export/csvUtils.d.ts +23 -0
- package/utils/export/downloadUtils.d.ts +12 -0
- package/utils/export/xlsxUtils.d.ts +56 -0
- package/utils/export/zipUtils.d.ts +26 -0
- package/utils/keyboard/typeaheadUtils.d.ts +31 -0
- package/utils/menu/menuUtils.d.ts +25 -0
- package/utils/slider/sliderUtils.d.ts +67 -0
- package/utils/tabs/tabsUtils.d.ts +75 -0
- package/utils/toast/toastStore.d.ts +73 -0
- package/utils/virtual/virtualUtils.d.ts +47 -0
- /package/react/{a11y → identity}/useIdentifier.d.ts +0 -0
package/BOX_KITE_AI_CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @box-kite/react - AI Assistant Context
|
|
2
2
|
|
|
3
|
-
Runtime CSS-in-JS library. `Box` component accepts
|
|
3
|
+
Runtime CSS-in-JS library. `Box` component accepts 221 CSS props and generates CSS classes at runtime. Same prop values share a single class.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -51,9 +51,13 @@ NEVER use `<Box tag="...">` when a component exists. NEVER use `<Box display="fl
|
|
|
51
51
|
| `<Box tag="nav/header/footer/main">` | `<Nav>/<Header>/<Footer>/<Main>` | `components/semantics` |
|
|
52
52
|
| `<Box tag="section/article/aside">` | `<Section>/<Article>/<Aside>` | `components/semantics` |
|
|
53
53
|
| `<Box tag="svg/path/circle/rect">` | `<Svg>/<Path>/<Circle>/<Rect>` | `components/svg` |
|
|
54
|
+
| `<Box tag="dialog">` | `<Dialog>/<AlertDialog>` | `components/dialog` |
|
|
55
|
+
| a menu button and its menu | `<Menu>` | `components/menu` |
|
|
56
|
+
| a tablist over one panel at a time | `<Tabs>` | `components/tabs` |
|
|
57
|
+
| a message sent from anywhere | `<Toaster>` + `toast()` | `components/toaster` |
|
|
54
58
|
| a lucide/Tabler icon, styled | `<Icon>` | `components/icon` |
|
|
55
59
|
|
|
56
|
-
All imports from `@box-kite/react/components/...`. Semantics also export: `Mark`, `Figure`, `Figcaption`, `Details`, `Summary`, `Menu
|
|
60
|
+
All imports from `@box-kite/react/components/...`. Semantics also export: `Mark`, `Figure`, `Figcaption`, `Details`, `Summary`, `MenuList` (the semantic `<menu>`; the menu **button** is `Menu` from `components/menu`), `Time`.
|
|
57
61
|
|
|
58
62
|
---
|
|
59
63
|
|
|
@@ -131,8 +135,8 @@ Every axis prop was already logical — `mx` is `margin-inline`, `px` is `paddin
|
|
|
131
135
|
|
|
132
136
|
- **The arrow keys follow the reading order.** `useRovingFocus` reads the element's _resolved_ direction when a sideways arrow arrives, so in a right-to-left list or grid `ArrowLeft` moves to the **next** item — which is what APG asks for. Nothing is configured; a `dir` anywhere above the list is enough, and a vertical list pays nothing. Tab, Home and End never flip.
|
|
133
137
|
- **A DataGrid column pins to `'START'` or `'END'`** of the inline axis rather than to a screen side, so it stays where the reading begins under either direction; `'LEFT'`/`'RIGHT'` are the older spelling of those two. `align` gained `'start'`/`'end'`, the resize handle grows a column towards the reading end, and the column menu's _Pin Left_ reads _Pin Right_ when that is the side it would pin to.
|
|
134
|
-
- **A popup
|
|
135
|
-
- **What stays physical, on purpose**:
|
|
138
|
+
- **A popup keeps the direction it was declared in.** `Overlay` stays in the subtree it was declared in — the top layer inherits normally — so `Tooltip`, the `Dropdown` popup and the grid's column menu simply inherit it. Only on the portal fallback, where the container is a child of the body and nothing of the declaration site reaches it, does it read the direction off its anchor and write it back on as `dir`.
|
|
139
|
+
- **What stays physical, on purpose**: the grid's loading bar sweeps the same way in both. (`Overlay` no longer positions in page coordinates at all — a `side` is logical, so a layer beside its anchor mirrors with the reading order.)
|
|
136
140
|
|
|
137
141
|
### Layout
|
|
138
142
|
|
|
@@ -189,6 +193,90 @@ All sizing, spacing, and positioning props also accept percentage strings: `p="5
|
|
|
189
193
|
| `willChange` | will-change | `'transform'`, `'opacity'`, `'filter'`, `'scroll-position'`, `'contents'`, `'auto'`. A hint with a real cost (a promoted layer holds memory), so it belongs on the few elements that animate, not on a list |
|
|
190
194
|
| `cursor` / `pointerEvents` / `userSelect` | misc | string values. For `transition`, `animation` and the transform props see _Animation and transitions_ below |
|
|
191
195
|
|
|
196
|
+
### Anchor positioning — a floating layer with no JavaScript
|
|
197
|
+
|
|
198
|
+
CSS Anchor Positioning as six props, so a tooltip, popup or badge is placed by the browser instead of measured in an effect. The anchor names itself; the layer says which anchor and which cell of the 3×3 grid around it:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<Button anchorName="menu-trigger">Options</Button>
|
|
202
|
+
|
|
203
|
+
<Box position="absolute" positionAnchor="menu-trigger" positionArea="block-end span-all" positionTryFallbacks="flip-block" mt={2}>
|
|
204
|
+
…
|
|
205
|
+
</Box>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Prop | CSS | Values |
|
|
209
|
+
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
+
| `anchorName` | `anchor-name` | a name with or without `--` (`"trigger"` → `--trigger`), or `'none'`. Global to the document, so one instance needs one name |
|
|
211
|
+
| `positionAnchor` | `position-anchor` | the same name, or `'auto'` / `'none'`. Needs `position="absolute"` (or `"fixed"`) beside it |
|
|
212
|
+
| `positionArea` | `position-area` | one or two keywords, **block axis first**, both from one family: `'block-end center'`, `'top left'`, `'center inline-end'`, `'span-all'` |
|
|
213
|
+
| `positionTryFallbacks` | `position-try-fallbacks` | `'flip-block'`, `'flip-inline'`, `'flip-start'`, space-combined into one candidate, comma-separated into a list tried in order, or `'none'` |
|
|
214
|
+
| `positionTryOrder` | `position-try-order` | `'normal'` (first fit), `'most-width'`, `'most-height'`, `'most-block-size'`, `'most-inline-size'` |
|
|
215
|
+
| `positionVisibility` | `position-visibility` | `'always'`, `'anchors-visible'`, `'no-overflow'`, `'anchors-visible no-overflow'` |
|
|
216
|
+
|
|
217
|
+
`justifySelf` and `alignSelf` also take `'anchor-center'`. Offset a layer from its anchor with an ordinary margin — there is no offset prop, because `mt={2}` already is one.
|
|
218
|
+
|
|
219
|
+
The keyword families do not mix: physical (`top`/`bottom` × `left`/`right`), logical (`block-start`/`block-end` × `inline-start`/`inline-end`), their `self-` twins, the `start`/`end` shorthand, its `self-` twin, and the `y-*` × `x-*` pair — with `center` and `span-all` neutral in all of them. So `'block-end inline-start'` is a value and `'top inline-start'` is not; a mixed pair emits no rule and no class, as does an unknown flip keyword or a name that is not an identifier.
|
|
220
|
+
|
|
221
|
+
**Two traps, both measured in Chrome 152 rather than read off the spec:**
|
|
222
|
+
|
|
223
|
+
- **A candidate position has to fit on _both_ axes to be taken**, so an overflow the flips cannot fix disqualifies every one of them and the layer silently stays where it started. `positionArea="block-end center"` with a layer wider than its anchor overflows the centre column, and `flip-block` then does nothing at all. Span the axis being kept — `"block-end span-all"` — and it flips.
|
|
224
|
+
- **`positionVisibility` hides at paint time**, so the layer keeps its box and its computed `visibility` still reads `visible` (`checkVisibility()` agrees). Only painting and hit-testing stop, so a test asserting it needs a screenshot.
|
|
225
|
+
|
|
226
|
+
Chrome 125+, Firefox 147+, Safari 26+. Where it is missing the layer renders unpositioned, so the portable path is `useAnchorPosition` (which measures instead) or `Overlay`, which is that hook plus the top layer.
|
|
227
|
+
|
|
228
|
+
A length can also come off the anchor. Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one, so a layer sizes and places itself against its anchor with nothing measured:
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
<Box position="fixed" positionAnchor="menu-trigger" minWidth="anchor-size(width)" maxHeight="anchor-size(height, 20rem)" />
|
|
232
|
+
<Box position="fixed" top="anchor(bottom)" insetStart="anchor(left)" />
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`anchor-size()` takes an axis (`width`, `height`, `block`, `inline`, `self-block`, `self-inline`), `anchor()` an edge (`top`, `right`, `bottom`, `left`, `start`, `end`, `self-start`, `self-end`, `center`, `inside`, `outside`) or a percentage along one. Both take an optional anchor name first — with the same optional `--` — and an optional fallback after a comma. A bare `anchor()` is rejected (CSS needs the edge); a bare `anchor-size()` is not (it means the layer's own axis).
|
|
236
|
+
|
|
237
|
+
### `useAnchorPosition` — the placement as one hook
|
|
238
|
+
|
|
239
|
+
`@box-kite/react/anchor` turns a side and an alignment into the props above, and measures instead where the browser has no anchor positioning. It renders nothing and owns no state beyond that.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { useAnchorPosition } from '@box-kite/react/anchor';
|
|
243
|
+
|
|
244
|
+
const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true });
|
|
245
|
+
|
|
246
|
+
<Button {...anchorProps} onClick={toggle}>
|
|
247
|
+
Options
|
|
248
|
+
</Button>;
|
|
249
|
+
{
|
|
250
|
+
isOpen && (
|
|
251
|
+
<Box {...layerProps} component="menu">
|
|
252
|
+
…
|
|
253
|
+
</Box>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Option | Default | What it means |
|
|
259
|
+
| ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
260
|
+
| `side` | `'bottom'` | `'top'`/`'bottom'` are the block axis, `'start'`/`'end'` the inline one — so a side mirrors in a right-to-left page |
|
|
261
|
+
| `align` | `'center'` | which of the anchor's edges to line up with on the other axis |
|
|
262
|
+
| `offset` | `0` | the gap, on the ÷4 spacing scale — emitted as the margin on the side facing the anchor, and a flip flips it too |
|
|
263
|
+
| `flip` | `true` | `positionTryFallbacks`: the side's own axis, then the alignment's, then both |
|
|
264
|
+
| `matchWidth` | `false` | `minWidth="anchor-size(width)"` |
|
|
265
|
+
| `name` | generated | the anchor's name; one per instance unless you pass one |
|
|
266
|
+
| `anchor` | — | an anchor handed to the hook rather than one it spreads props onto — the name is written on in an effect |
|
|
267
|
+
| `trackSide` | `false` | whether `side` in the result reports where the layer ended up, which costs one read after layout |
|
|
268
|
+
|
|
269
|
+
`css` in the result says which path ran: `true` is the browser placing the layer with no JavaScript at all, `false` is the measured fallback (flip to the opposite side, then shift along the other axis to stay in the viewport). `side` is where the layer actually is — the requested side unless `trackSide` is on and a flip moved it.
|
|
270
|
+
|
|
271
|
+
**Three more things worth knowing, all measured:**
|
|
272
|
+
|
|
273
|
+
- **The layer is `position: fixed`**, so it escapes every `overflow: hidden` ancestor without a portal — but not a _transformed_ ancestor, which is a fixed element's containing block, and not the page's stacking order. `Overlay` is this hook plus the browser's top layer, which beats both, and is still the answer when the layer has to come out on top of everything.
|
|
274
|
+
- **Three fallbacks, not one.** A candidate has to fit on _both_ axes, so a lone `flip-block` does nothing at all for a layer that overflows the _cross_ axis — the browser leaves it pressed against the edge of the viewport. `flip` therefore offers the side's axis, the alignment's, and both, and a menu aligned to its trigger's end mirrors to the other end rather than running off the page.
|
|
275
|
+
- **Which side the browser chose costs the entrance.** `trackSide` reads the _used_ `position-area`, and that read is the style resolution `@starting-style` computes its before-change style from — so a class that depends on the answer lands after the entrance has already been decided. The exit runs long afterwards and can use it (`dropdown.items` has `closedUp` and no `up`).
|
|
276
|
+
- **A used `position-area` is not the value that went in.** Chrome 152 drops a `span-all` half (`block-end span-all` reads back `block-end`) and collapses a value naming both axes into the `start`/`end` shorthand, where position names the axis: `block-end span-inline-end` reads back `end span-end`, and `start span-end` once it has flipped.
|
|
277
|
+
- **A flip is sticky, and in the top layer it is never re-evaluated on scroll at all.** Once the browser takes one it keeps it until the layer is laid out afresh, which is what stops it oscillating as the page scrolls: hiding the layer and showing it again re-evaluates. So a popup that mounts when it opens always picks the side that fits, while one that stays mounted keeps the side it first chose. For an element in the **top layer** — which is every pre-built layer: `Overlay`, and so `Tooltip`, the `Dropdown` popup and the DataGrid menu, plus `Popover` — Chrome does not re-evaluate on scroll even once the side has run out of room, so a layer open across a long scroll slides past the viewport edge. Measured in Chrome 152 against an otherwise identical non-top-layer element, which does flip; only leaving and re-entering the top layer re-arms it.
|
|
278
|
+
- **The anchor's name is an inline style, not a prop.** An identity is per instance, so a class for it would be a rule per instance that is never freed — the same reason a sparkline's `d` is an attribute while its stroke is a class. Everything shared — the area, the flip, the margin, the width — is an ordinary prop.
|
|
279
|
+
|
|
192
280
|
### Colours, and the opacity modifier
|
|
193
281
|
|
|
194
282
|
The palette is **Tailwind 4.3's, in OKLCH**: twenty-six families of eleven steps (`50`–`950`) — the five
|
|
@@ -980,10 +1068,20 @@ import Textbox from '@box-kite/react/components/textbox';
|
|
|
980
1068
|
<Box position="fixed" top={0} left={0} right={0} bottom={0} bgColor="black" opacity={0.5} zIndex={50} />
|
|
981
1069
|
```
|
|
982
1070
|
|
|
983
|
-
**
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1071
|
+
**Floating layers**: `Overlay` (`components/overlay`) anchors a layer to a trigger (`anchor`, `side`, `align`,
|
|
1072
|
+
`offset`, `flip`, `matchWidth` — the vocabulary `useAnchorPosition` and `Tooltip` share) and renders it
|
|
1073
|
+
**where it was declared, in the browser's top layer** (`popover="manual"`, shown on mount), so it escapes
|
|
1074
|
+
`overflow: hidden`, clipped and transformed ancestors and the stacking order while still inheriting the
|
|
1075
|
+
theme, the custom properties and the direction around it. `manual` because it owns no dismissal — and no
|
|
1076
|
+
ARIA and no open state either: it is positioning only. **Never declare one inside its trigger**: a
|
|
1077
|
+
listbox or a menu inside a `<button>` is unreachable content, which the portal used to hide by moving it
|
|
1078
|
+
elsewhere. **One cost comes with the top layer**: the layer keeps the side it chose when it opened, because
|
|
1079
|
+
Chrome never re-evaluates `position-try-fallbacks` on scroll for a top-layer element (measured in 152) — so
|
|
1080
|
+
a layer left open while the page scrolls slides past the viewport edge rather than flipping. Every open
|
|
1081
|
+
picks the right side; close a layer if the page can scroll far underneath it. Where the browser has no
|
|
1082
|
+
Popover API it is portalled into `#box-kite-portal` instead, and that path measures, so it flips. For a
|
|
1083
|
+
_panel a user interacts with_, use `Popover` (below); for a _description of a control_, use `Tooltip`.
|
|
1084
|
+
Both are that layer with a pattern on it.
|
|
987
1085
|
|
|
988
1086
|
### Styling an element Box cannot render (`useClassNames`)
|
|
989
1087
|
|
|
@@ -1141,7 +1239,7 @@ import RadioButton from '@box-kite/react/components/radioButton';
|
|
|
1141
1239
|
<Checkbox label="Select all rows" name="rows" indeterminate />
|
|
1142
1240
|
<Switch label="Email notifications" name="notify" defaultChecked />
|
|
1143
1241
|
|
|
1144
|
-
<RadioGroup label="Plan" name="plan" defaultValue="free"
|
|
1242
|
+
<RadioGroup label="Plan" name="plan" defaultValue="free" onValueChange={(plan, { reason }) => setPlan(plan)}>
|
|
1145
1243
|
<RadioGroup.Item value="free" label="Free" />
|
|
1146
1244
|
<RadioGroup.Item value="pro" label="Pro" />
|
|
1147
1245
|
<RadioGroup.Item value="team" label="Team" disabled />
|
|
@@ -1155,16 +1253,16 @@ the association needs no `htmlFor`/`id` pair and the whole row is a click target
|
|
|
1155
1253
|
nothing wraps the input — the markup is what it always was. `labelProps` styles that `<label>`;
|
|
1156
1254
|
every other Box prop styles the control itself.
|
|
1157
1255
|
|
|
1158
|
-
| Component | Prop | What it does
|
|
1159
|
-
| ------------ | ------------------------ |
|
|
1160
|
-
| all three | `label` / `labelProps` | The wrapping `<label>` and its styles.
|
|
1161
|
-
| `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced.
|
|
1162
|
-
| `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit.
|
|
1163
|
-
| `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list.
|
|
1164
|
-
| `RadioGroup` | `name` | The field every item submits under. Generated when left out.
|
|
1165
|
-
| `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio).
|
|
1166
|
-
| `RadioGroup` | `
|
|
1167
|
-
| `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`. Both arrow pairs navigate either way.
|
|
1256
|
+
| Component | Prop | What it does |
|
|
1257
|
+
| ------------ | ------------------------ | -------------------------------------------------------------------------------------------- |
|
|
1258
|
+
| all three | `label` / `labelProps` | The wrapping `<label>` and its styles. |
|
|
1259
|
+
| `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced. |
|
|
1260
|
+
| `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit. |
|
|
1261
|
+
| `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list. |
|
|
1262
|
+
| `RadioGroup` | `name` | The field every item submits under. Generated when left out. |
|
|
1263
|
+
| `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio). |
|
|
1264
|
+
| `RadioGroup` | `onValueChange` | `(value, { reason, event })` — `click` or `keyboard`. `onChange` is the deprecated spelling. |
|
|
1265
|
+
| `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`. Both arrow pairs navigate either way. |
|
|
1168
1266
|
|
|
1169
1267
|
Arrow keys move **and select** inside a group, wrapping at both ends and skipping disabled items;
|
|
1170
1268
|
Tab enters the group once and leaves it once, because a native radio set is already a single tab
|
|
@@ -1213,6 +1311,644 @@ next to the markup. Everything else is a Box prop, and `props` takes the `<form>
|
|
|
1213
1311
|
|
|
1214
1312
|
---
|
|
1215
1313
|
|
|
1314
|
+
## Popover Component
|
|
1315
|
+
|
|
1316
|
+
```tsx
|
|
1317
|
+
import Popover from '@box-kite/react/components/popover';
|
|
1318
|
+
|
|
1319
|
+
<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}>
|
|
1320
|
+
<Checkbox label="Only mine" />
|
|
1321
|
+
</Popover>;
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
A panel anchored to a trigger, on the platform's **Popover API**. The `trigger` is a render prop (the
|
|
1325
|
+
`ref` and the ARIA have to land on the control itself) and the **children are the panel's content** —
|
|
1326
|
+
the other way round from `Tooltip`, whose `content` is a short description and whose `children` are the
|
|
1327
|
+
trigger. The trigger must be a **button**: the toggle is the browser's own `popovertarget`.
|
|
1328
|
+
|
|
1329
|
+
**There is no portal.** The panel is in the top layer, so it paints above every stacking context and
|
|
1330
|
+
outside every clipped ancestor — and because it is never moved, it keeps the theme, the custom
|
|
1331
|
+
properties, the direction and the tab order it was declared in. Use `Popover` rather than `Overlay`
|
|
1332
|
+
whenever the layer is a panel a user interacts with; `Overlay` is in the same top layer, but it is
|
|
1333
|
+
positioning only and light-dismisses nothing.
|
|
1334
|
+
|
|
1335
|
+
| Prop | Default | What it does |
|
|
1336
|
+
| ---------------------- | ------------------- | ----------------------------------------------------------------------------------- |
|
|
1337
|
+
| `trigger` | — | Render prop given `{ ref, props }`. Spread onto a **button**. |
|
|
1338
|
+
| `children` | — | The panel's content. Rendered whether or not it is open. |
|
|
1339
|
+
| `label` / `labelledBy` | — | The panel's accessible name. `role="dialog"` has none of its own — give it one. |
|
|
1340
|
+
| `open` / `defaultOpen` | — | Controlled / uncontrolled open state. |
|
|
1341
|
+
| `onOpenChange` | — | `(open, { reason, event })` — `trigger`, `escape`, `outside-pointer`, `imperative`. |
|
|
1342
|
+
| `side` / `align` | `bottom` / `center` | Which side of the trigger the panel sits on, and how it lines up. |
|
|
1343
|
+
| `offset` | `2` | The gap, on the ÷4 scale — 8px. |
|
|
1344
|
+
| `flip` | `true` | Whether a side with no room is swapped for its opposite. |
|
|
1345
|
+
| `matchWidth` | `false` | Whether the panel is at least as wide as its trigger. |
|
|
1346
|
+
| `autoFocus` | `true` | Whether opening moves focus into the panel. An `autofocus` inside wins. |
|
|
1347
|
+
|
|
1348
|
+
What the component supplies, because the `popover` attribute supplies none of it: `role="dialog"`, the
|
|
1349
|
+
name, and `aria-expanded`/`aria-haspopup="dialog"`/`aria-controls` on the trigger. What the **platform**
|
|
1350
|
+
supplies, so it is not reimplemented: the top layer, light dismiss on Escape and on an outside press,
|
|
1351
|
+
and focus return when the panel is hidden.
|
|
1352
|
+
|
|
1353
|
+
Three traps, all measured in Chrome 152:
|
|
1354
|
+
|
|
1355
|
+
- **The panel is always rendered**; closed is `display: none`, not unmounted. So the exit is a CSS
|
|
1356
|
+
transition (`transitionBehavior="allow-discrete"`, already in the component styles) and **not**
|
|
1357
|
+
`<Presence>` — nothing leaves the DOM. Gate expensive children yourself: `{open ? <Heavy /> : null}`.
|
|
1358
|
+
- **A close cannot be refused.** `beforetoggle` is cancelable opening and not closing, so a controlled
|
|
1359
|
+
`<Popover open>` hears about a light dismiss only after it has happened; keep `open` true and the
|
|
1360
|
+
component re-shows the panel.
|
|
1361
|
+
- **Never put an `onClick` toggle on the trigger.** Light dismiss closes on `pointerdown`, so the
|
|
1362
|
+
`click` after it reads "closed" and reopens the panel — pressing the trigger would never close it.
|
|
1363
|
+
That is exactly why the toggle is handed to `popovertarget`.
|
|
1364
|
+
|
|
1365
|
+
Where the browser has no Popover API the panel falls back to a portalled `Overlay` with `useDismiss`
|
|
1366
|
+
and `useFocusReturn`. Same props, same styling; what is lost is what a portal costs.
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1370
|
+
## Dialog Component
|
|
1371
|
+
|
|
1372
|
+
```tsx
|
|
1373
|
+
import Dialog, { AlertDialog } from '@box-kite/react/components/dialog';
|
|
1374
|
+
|
|
1375
|
+
<Dialog trigger={(t) => <Button {...t}>Rename</Button>}>
|
|
1376
|
+
<Dialog.Title>Rename this view</Dialog.Title>
|
|
1377
|
+
<Dialog.Description>The name is only shown to you.</Dialog.Description>
|
|
1378
|
+
<Textbox name="name" props={{ 'aria-label': 'Name' }} />
|
|
1379
|
+
</Dialog>;
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
A real `<dialog>` shown with **`showModal()`**, which is where the whole pattern comes from: the top
|
|
1383
|
+
layer, the `::backdrop`, an inert page behind it, Escape, focus containment and focus return are all the
|
|
1384
|
+
browser's. There is no focus trap here, no `aria-hidden` sweep over the page and no `z-index` — measured
|
|
1385
|
+
in Chrome 152, a modal dialog paints over a `position: fixed` sibling with `z-index: 9999`.
|
|
1386
|
+
|
|
1387
|
+
| Prop | Default | What it does |
|
|
1388
|
+
| ---------------------- | ------- | ----------------------------------------------------------------------------------- |
|
|
1389
|
+
| `trigger` | — | Render prop given `{ ref, props }`. Optional — open it from anywhere instead. |
|
|
1390
|
+
| `children` | — | The dialog's content. Rendered whether or not it is open. |
|
|
1391
|
+
| `open` / `defaultOpen` | — | Controlled / uncontrolled open state. |
|
|
1392
|
+
| `onOpenChange` | — | `(open, { reason, event })` — `trigger`, `escape`, `outside-pointer`, `imperative`. |
|
|
1393
|
+
| `label` / `labelledBy` | — | The name, when no `Dialog.Title` supplies one. |
|
|
1394
|
+
| `describedBy` | — | The description, when no `Dialog.Description` supplies one. |
|
|
1395
|
+
| `modal` | `true` | `showModal()`. `false` is `show()`: no backdrop, nothing inert. |
|
|
1396
|
+
| `dismissible` | `true` | Whether a press outside closes it. |
|
|
1397
|
+
| `lockScroll` | `modal` | Whether the page behind it stops scrolling. |
|
|
1398
|
+
| `initialFocus` | — | Where focus lands, instead of the first focusable thing inside. |
|
|
1399
|
+
|
|
1400
|
+
`Dialog.Title` and `Dialog.Description` are how it is named: rendering a title writes `aria-labelledby`
|
|
1401
|
+
on the dialog and rendering a description writes `aria-describedby`. **Neither attribute is written when
|
|
1402
|
+
the part is absent**, so a reference never dangles — and a dialog showing no heading wants `label`,
|
|
1403
|
+
`labelledBy` or `describedBy` instead. Both are Boxes (`dialog.title`, `dialog.description` in the style
|
|
1404
|
+
tree), and `AlertDialog` carries the same two.
|
|
1405
|
+
|
|
1406
|
+
```tsx
|
|
1407
|
+
const cancel = useRef(null);
|
|
1408
|
+
|
|
1409
|
+
<AlertDialog initialFocus={cancel} trigger={(t) => <Button {...t}>Delete</Button>}>
|
|
1410
|
+
<AlertDialog.Title>Delete this view?</AlertDialog.Title>
|
|
1411
|
+
<AlertDialog.Description>Nothing here can be undone.</AlertDialog.Description>
|
|
1412
|
+
<Button bgColor="rose-600">Delete</Button>
|
|
1413
|
+
<Button ref={cancel}>Cancel</Button>
|
|
1414
|
+
</AlertDialog>;
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
`AlertDialog` is `role="alertdialog"` — the content is an alert rather than a panel, so the name and the
|
|
1418
|
+
description are announced together on open. It is always modal and **never dismissed by a press
|
|
1419
|
+
outside**; Escape still closes it, because a keyboard user must always have a way out. `initialFocus` is
|
|
1420
|
+
APG's requirement that focus land on the least destructive action, so a deletion cannot be confirmed by
|
|
1421
|
+
reflex. It takes every `Dialog` prop but `modal`, `dismissible` and `lockScroll`, which are not choices
|
|
1422
|
+
it gets to make.
|
|
1423
|
+
|
|
1424
|
+
Four traps, all measured in Chrome 152:
|
|
1425
|
+
|
|
1426
|
+
- **The dialog is always rendered**; closed is `display: none`, not unmounted — the same shape as
|
|
1427
|
+
`Popover`. The exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in the
|
|
1428
|
+
component styles, and it covers the `::backdrop` too) and **not** `<Presence>`. Gate expensive
|
|
1429
|
+
children yourself: `{open ? <Heavy /> : null}`.
|
|
1430
|
+
- **A close cannot be refused.** `cancel` is cancelable, but the browser has already closed the dialog
|
|
1431
|
+
by the time `onOpenChange` runs, so a controlled `<Dialog open>` hears about a dismissal afterwards
|
|
1432
|
+
and keeping `open` true shows it again. `dismissible={false}` is how a decision is made unavoidable.
|
|
1433
|
+
- **For a modal dialog the backdrop _is_ the dialog element.** A press on it targets the `<dialog>`, so
|
|
1434
|
+
every containment test calls it inside. The platform's `closedby="any"` gets this right and the
|
|
1435
|
+
component writes it; where the browser has not got it yet, the press is measured against the dialog's
|
|
1436
|
+
own box instead.
|
|
1437
|
+
- **The platform does not stop the page scrolling** (measured: `overflow` stays `visible` while a modal
|
|
1438
|
+
dialog is open). That is what `lockScroll` is — an `overflow: hidden` class on `<html>`, held by a
|
|
1439
|
+
counter so an inner dialog closing cannot unlock the page under an outer one. The scrollbar's width
|
|
1440
|
+
leaves the page as it is applied, so a document that must not shift wants `scrollbarGutter="stable"`;
|
|
1441
|
+
iOS Safari scrolls anyway.
|
|
1442
|
+
|
|
1443
|
+
The style tree says almost nothing about position or size on purpose: the UA stylesheet centres a modal
|
|
1444
|
+
dialog in the viewport and caps it at `calc(100% - 6px - 2em)`. The **one** exception is `margin: auto`,
|
|
1445
|
+
which `Box.components('dialog')` has to declare because `._b` sets `margin: 0` and outranks the UA rule
|
|
1446
|
+
— without it the dialog sits in the corner. `display: none` while closed is declared for the same
|
|
1447
|
+
reason.
|
|
1448
|
+
|
|
1449
|
+
---
|
|
1450
|
+
|
|
1451
|
+
## Menu Component
|
|
1452
|
+
|
|
1453
|
+
```tsx
|
|
1454
|
+
import Menu from '@box-kite/react/components/menu';
|
|
1455
|
+
|
|
1456
|
+
<Menu trigger={(t) => <Button {...t}>Actions</Button>}>
|
|
1457
|
+
<Menu.Item onSelect={duplicate}>Duplicate</Menu.Item>
|
|
1458
|
+
<Menu.Item disabled>Move</Menu.Item>
|
|
1459
|
+
<Menu.Separator />
|
|
1460
|
+
<Menu.Group label="View">
|
|
1461
|
+
<Menu.CheckboxItem checked={compact} onCheckedChange={setCompact}>
|
|
1462
|
+
Compact rows
|
|
1463
|
+
</Menu.CheckboxItem>
|
|
1464
|
+
</Menu.Group>
|
|
1465
|
+
<Menu.RadioGroup label="Sort by" value={sort} onValueChange={setSort}>
|
|
1466
|
+
<Menu.RadioItem value="name">Name</Menu.RadioItem>
|
|
1467
|
+
<Menu.RadioItem value="date">Date added</Menu.RadioItem>
|
|
1468
|
+
</Menu.RadioGroup>
|
|
1469
|
+
<Menu.Sub label="Share">
|
|
1470
|
+
<Menu.Item onSelect={copyLink}>Copy link</Menu.Item>
|
|
1471
|
+
</Menu.Sub>
|
|
1472
|
+
</Menu>;
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
APG's menu button on the platform's own Popover API, and **a submenu is a popover nested inside its
|
|
1476
|
+
menu**. The top layer, light dismiss and the toggle are the browser's: the menu paints over every
|
|
1477
|
+
stacking context with no portal and no `z-index`, Escape closes the innermost menu first — one layer per
|
|
1478
|
+
press — and a press outside closes the lot. What the component adds is the ARIA, the keyboard, and the
|
|
1479
|
+
two focus moves the platform does not make.
|
|
1480
|
+
|
|
1481
|
+
| Prop | Default | What it does |
|
|
1482
|
+
| ---------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
|
|
1483
|
+
| `trigger` | — | Render prop given `{ ref, props }`. It has to be a **button**. |
|
|
1484
|
+
| `children` | — | The items. Rendered whether or not the menu is open. |
|
|
1485
|
+
| `open` / `defaultOpen` | — | Controlled / uncontrolled open state. |
|
|
1486
|
+
| `onOpenChange` | — | `(open, { reason, event })` — `select`, `tab`, `trigger`, `escape`, `outside-pointer`, `imperative`. |
|
|
1487
|
+
| `label` / `labelledBy` | trigger | The menu's name. Left out, it takes its trigger's, which is what APG asks for. |
|
|
1488
|
+
| `side` | `'bottom'` | `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page). |
|
|
1489
|
+
| `align` | `'start'` | Which of the trigger's edges to line the menu up with. |
|
|
1490
|
+
| `offset` | `1` | The gap, on the ÷4 scale — 4px. |
|
|
1491
|
+
| `flip` | `true` | Whether a side with no room may be swapped for its opposite. |
|
|
1492
|
+
| `matchWidth` | `false` | Whether the menu is at least as wide as its trigger. |
|
|
1493
|
+
|
|
1494
|
+
| Part | Props | Role |
|
|
1495
|
+
| ------------------- | ------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
1496
|
+
| `Menu.Item` | `onSelect`, `disabled`, `closeOnSelect` (default `true`) | `menuitem` |
|
|
1497
|
+
| `Menu.CheckboxItem` | `checked`/`defaultChecked`/`onCheckedChange`, `disabled`, `closeOnSelect` (default `false`) | `menuitemcheckbox` |
|
|
1498
|
+
| `Menu.RadioGroup` | `label`, `value`/`defaultValue`/`onValueChange` | `group` |
|
|
1499
|
+
| `Menu.RadioItem` | `value`, `disabled`, `closeOnSelect` (default `false`) | `menuitemradio` |
|
|
1500
|
+
| `Menu.Group` | `label` — names the group through `aria-labelledby` | `group` |
|
|
1501
|
+
| `Menu.Separator` | every Box prop | `separator` |
|
|
1502
|
+
| `Menu.Sub` | `label` (its item's content), `disabled`, `itemProps`, `side`/`align`/`offset`/`flip` | `menuitem` + a nested `menu` |
|
|
1503
|
+
|
|
1504
|
+
`Menu.Sub` renders both halves: the item that opens it — `aria-haspopup="menu"`, `aria-expanded`, a
|
|
1505
|
+
chevron — and the menu beside it, declared inside the menu it belongs to, which is what nests the two
|
|
1506
|
+
popovers. Its defaults are `side="end"` and `offset={0}`, so a submenu abuts the menu it came out of, and
|
|
1507
|
+
they nest as deeply as the markup does.
|
|
1508
|
+
|
|
1509
|
+
| Key | What it does |
|
|
1510
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1511
|
+
| Enter / Space | On the trigger, opens with the first item focused. On an item, chooses it. On a submenu's item, opens it. |
|
|
1512
|
+
| Down / Up | On the trigger, opens at the first or last item. Inside, moves between items and **wraps**. |
|
|
1513
|
+
| Home / End | The first or the last item. |
|
|
1514
|
+
| Right / Left | Opens the submenu of the focused item, and closes the submenu focus is in — the **reading order**, so the two swap in a right-to-left menu. |
|
|
1515
|
+
| Escape | Closes the innermost menu, and puts focus back on what opened it. |
|
|
1516
|
+
| Tab | Closes the menu and moves focus on, as APG asks. |
|
|
1517
|
+
| A printable character | Typeahead. A longer buffer narrows; the same letter again cycles through the items starting with it. |
|
|
1518
|
+
|
|
1519
|
+
Four things to know, all measured in Chrome 152:
|
|
1520
|
+
|
|
1521
|
+
- **A disabled item is `aria-disabled` and stays focusable.** APG asks for that, so a keyboard user can
|
|
1522
|
+
find out the item is there at all; the `disabled` attribute would take it out of the keyboard's reach
|
|
1523
|
+
and silence it. The arrows land on it and activating it does nothing.
|
|
1524
|
+
- **A command closes the menu and a state does not.** `Menu.Item` closes on select, because choosing a
|
|
1525
|
+
command ends the visit; a checkbox or a radio item does not, so several boxes can be ticked in one go.
|
|
1526
|
+
`closeOnSelect` swaps either default.
|
|
1527
|
+
- **The menu is always rendered**; closed is `display: none`, not unmounted — the same shape as
|
|
1528
|
+
`Popover` and `Dialog`. The exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in
|
|
1529
|
+
the component styles) and **not** `<Presence>`. Gate expensive items yourself:
|
|
1530
|
+
`{open ? <Items /> : null}`.
|
|
1531
|
+
- **The platform returns focus for the outermost layer only.** Closing a nested popover drops focus to
|
|
1532
|
+
`<body>`, so a submenu puts focus back on its own item itself — and opening one moves focus into it,
|
|
1533
|
+
hover included, which is what keeps the highlight and the keyboard in the same place. The top layer's
|
|
1534
|
+
one cost applies as it does to `Popover`: a menu **keeps the side it opened on**.
|
|
1535
|
+
|
|
1536
|
+
The style tree is `menu` with `menu.item` (plus a `sub` variant), `menu.group`, `menu.label`,
|
|
1537
|
+
`menu.separator`, and `menu.indicator`, `menu.check`, `menu.dot` and `menu.arrow` for the four marks a
|
|
1538
|
+
menu draws — all of them borders and a radius rather than an asset, since the library ships no icons.
|
|
1539
|
+
The highlight is drawn on `:focus` because in a menu focus **is** the highlight: the pointer moves it, so
|
|
1540
|
+
hover and the keyboard cannot disagree.
|
|
1541
|
+
|
|
1542
|
+
**The semantic `<menu>` element is `MenuList`** (`components/semantics`) now. `Menu` is still exported
|
|
1543
|
+
there and still works, but it is deprecated: two exports of that name are one import away from the wrong
|
|
1544
|
+
component.
|
|
1545
|
+
|
|
1546
|
+
---
|
|
1547
|
+
|
|
1548
|
+
## Tabs Component
|
|
1549
|
+
|
|
1550
|
+
```tsx
|
|
1551
|
+
import Tabs from '@box-kite/react/components/tabs';
|
|
1552
|
+
|
|
1553
|
+
<Tabs defaultValue="overview" onValueChange={(value, { reason }) => log(value, reason)}>
|
|
1554
|
+
<Tabs.List label="Project">
|
|
1555
|
+
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
|
1556
|
+
<Tabs.Tab value="activity">Activity</Tabs.Tab>
|
|
1557
|
+
<Tabs.Tab value="audit" disabled>
|
|
1558
|
+
Audit log
|
|
1559
|
+
</Tabs.Tab>
|
|
1560
|
+
</Tabs.List>
|
|
1561
|
+
<Tabs.Panel value="overview">Who is on it, and what is left.</Tabs.Panel>
|
|
1562
|
+
<Tabs.Panel value="activity">What changed this week.</Tabs.Panel>
|
|
1563
|
+
</Tabs>;
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
APG's tabs pattern, where **selection follows focus**: one arrow key moves to a tab and shows its panel.
|
|
1567
|
+
`activation="manual"` splits the two — the arrows move, Enter or Space chooses — which is what a panel
|
|
1568
|
+
too expensive to render on the way past needs.
|
|
1569
|
+
|
|
1570
|
+
| Prop | Default | What it does |
|
|
1571
|
+
| --------------- | -------------- | ------------------------------------------------------------------------------------------- |
|
|
1572
|
+
| `value` | — | Controlled selection. |
|
|
1573
|
+
| `defaultValue` | — | Which tab starts selected. Left out, nothing is selected and no panel is shown. |
|
|
1574
|
+
| `onValueChange` | — | `(value, { reason, event })` — `click` or `keyboard`. |
|
|
1575
|
+
| `orientation` | `'horizontal'` | `vertical` turns the axis, moves the arrows to Down/Up and the indicator to the inline end. |
|
|
1576
|
+
| `activation` | `'automatic'` | `manual` moves focus without selecting. |
|
|
1577
|
+
| `loop` | `true` | Whether the arrows wrap at the ends. |
|
|
1578
|
+
| `keepMounted` | `false` | Render every panel rather than only the selected one, hiding the rest. |
|
|
1579
|
+
| `indicator` | `'static'` | `sliding` is one element for the whole list, travelling between tabs. |
|
|
1580
|
+
|
|
1581
|
+
| Part | Props | Role |
|
|
1582
|
+
| ------------- | -------------------------------------- | ------------------------------------------------- |
|
|
1583
|
+
| `Tabs.List` | `label` / `labelledBy`, every Box prop | `tablist` + `aria-orientation` |
|
|
1584
|
+
| `Tabs.Tab` | `value`, `disabled`, every Box prop | `tab` + `aria-selected`, `aria-controls` |
|
|
1585
|
+
| `Tabs.Panels` | `children`, every Box prop | none — the optional box that resizes to its panel |
|
|
1586
|
+
| `Tabs.Panel` | `value`, every Box prop | `tabpanel` + `aria-labelledby`, `tabindex="0"` |
|
|
1587
|
+
|
|
1588
|
+
| Key | What it does |
|
|
1589
|
+
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
1590
|
+
| Tab | Enters the list once, landing on the selected tab; again, leaves it for the panel. |
|
|
1591
|
+
| Right / Left | The next and previous tab in a horizontal list, wrapping and stepping over disabled tabs. |
|
|
1592
|
+
| Down / Up | The same in a vertical list. The off-axis pair is left to the page, so a list does not eat a scroll. |
|
|
1593
|
+
| Home / End | The first and last selectable tab. |
|
|
1594
|
+
| Enter / Space | Chooses the focused tab. Only `activation="manual"` needs them. |
|
|
1595
|
+
|
|
1596
|
+
The arrows follow the **reading order**, so in a right-to-left page ArrowLeft is the _next_ tab, read off
|
|
1597
|
+
the element's own resolved direction rather than an ancestor's `dir`.
|
|
1598
|
+
|
|
1599
|
+
Two things move, and both are opt-in. `indicator="sliding"` swaps each tab's own border for one element
|
|
1600
|
+
for the whole list, which animates between tabs because it _is_ the same element; wrapping the panels in a
|
|
1601
|
+
`Tabs.Panels` gives that container the height of the panel on screen, so panels of different heights
|
|
1602
|
+
transition instead of jumping.
|
|
1603
|
+
|
|
1604
|
+
```tsx
|
|
1605
|
+
<Tabs defaultValue="summary" indicator="sliding">
|
|
1606
|
+
<Tabs.List label="Release">
|
|
1607
|
+
<Tabs.Tab value="summary">Summary</Tabs.Tab>
|
|
1608
|
+
<Tabs.Tab value="changelog">Changelog</Tabs.Tab>
|
|
1609
|
+
</Tabs.List>
|
|
1610
|
+
<Tabs.Panels>
|
|
1611
|
+
<Tabs.Panel value="summary">One line, and the box is one line tall.</Tabs.Panel>
|
|
1612
|
+
<Tabs.Panel value="changelog">Four, and it grows to fit them.</Tabs.Panel>
|
|
1613
|
+
</Tabs.Panels>
|
|
1614
|
+
</Tabs>
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
Both are measured, which is where their three surprises come from. The travelling indicator **appears
|
|
1618
|
+
once the widget has run**, so until then the tabs keep drawing their own border — what a prerendered page
|
|
1619
|
+
paints, in exactly the same place, so the handover is invisible rather than a page with no indicator at
|
|
1620
|
+
all. The container **watches the panel and never itself**, since its own height is what it writes, and
|
|
1621
|
+
watching the panel catches a reflow as well as a switch. And both ride `--transitionTime`, so
|
|
1622
|
+
`prefers-reduced-motion` computes each to `0s` with no opt-out. The container is clipped **only while its
|
|
1623
|
+
height is travelling**: at rest it is exactly as tall as its panel, so a permanent clip would cut the
|
|
1624
|
+
focus ring off anything sitting at that edge — and `overflow` clips both axes whatever is asked for,
|
|
1625
|
+
computing a `visible` companion to a clipped axis up to `auto`.
|
|
1626
|
+
|
|
1627
|
+
Four things to know:
|
|
1628
|
+
|
|
1629
|
+
- **A disabled tab is not selectable and the arrows step over it** — the opposite of `Menu.Item`, which
|
|
1630
|
+
APG asks stay reachable. Selection follows focus here, so a tab focus could reach but selection could
|
|
1631
|
+
not would leave the widget with no state to be in.
|
|
1632
|
+
- **Only the selected panel is rendered.** An unmounted panel costs nothing and gets an entrance from
|
|
1633
|
+
`startingStyle` for free, but it loses whatever state it held — `keepMounted` renders them all and
|
|
1634
|
+
hides the rest, which is what a half-filled form needs. A hidden panel carries the `hidden` attribute
|
|
1635
|
+
**and** a `display: none` of its own, since every Box carries `display: block` and any author rule
|
|
1636
|
+
outranks the UA's `[hidden]` one.
|
|
1637
|
+
- **The tabs are read off the DOM**, not out of a registry, so a tab wrapped in a layout of your own,
|
|
1638
|
+
rendered from a list or put behind a condition navigates like any other — and a nested set of tabs
|
|
1639
|
+
belongs to its own list rather than the one around it.
|
|
1640
|
+
- **The tab sequence follows the selection**, so a controlled `value` changed elsewhere on the page moves
|
|
1641
|
+
the keyboard's entry point with it. Under `activation="manual"` it follows the focus instead, which is
|
|
1642
|
+
what APG's own example does.
|
|
1643
|
+
|
|
1644
|
+
The style tree is `tabs` with `tabs.list`, `tabs.tab` and `tabs.panel` beneath it, plus
|
|
1645
|
+
`tabs.indicator` for the travelling bar and `tabs.panels` for the resizing container. The selected state is
|
|
1646
|
+
`aria-selected`: inside a style tree it is the bare `selected` key, and **on a tab it is
|
|
1647
|
+
`ariaAttr={{ selected: … }}`** — a top-level `selected` _prop_ writes the attribute the component owns
|
|
1648
|
+
rather than nesting styles under it, which is the rule for `selected`, `disabled` and `checked` alike.
|
|
1649
|
+
The indicator is a border rather than a background: a forced-colors mode keeps one and throws the other
|
|
1650
|
+
away, so selection would otherwise read identically on and off.
|
|
1651
|
+
|
|
1652
|
+
---
|
|
1653
|
+
|
|
1654
|
+
## Accordion & Collapsible
|
|
1655
|
+
|
|
1656
|
+
```tsx
|
|
1657
|
+
import Accordion, { Collapsible } from '@box-kite/react/components/accordion';
|
|
1658
|
+
|
|
1659
|
+
<Accordion defaultValue={['shipping']} onValueChange={(open, { reason }) => log(open, reason)}>
|
|
1660
|
+
<Accordion.Item value="shipping">
|
|
1661
|
+
<Accordion.Trigger>Shipping</Accordion.Trigger>
|
|
1662
|
+
<Accordion.Panel>Two to four working days, tracked.</Accordion.Panel>
|
|
1663
|
+
</Accordion.Item>
|
|
1664
|
+
<Accordion.Item value="returns">
|
|
1665
|
+
<Accordion.Trigger>Returns</Accordion.Trigger>
|
|
1666
|
+
<Accordion.Panel>Thirty days, in the packaging it came in.</Accordion.Panel>
|
|
1667
|
+
</Accordion.Item>
|
|
1668
|
+
</Accordion>;
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
APG's accordion. **The height animation is a class, not a measurement**: the panel sits in a grid of one
|
|
1672
|
+
row whose track runs `1fr` to `0fr`, so there is no `ResizeObserver`, no measured pixel and nothing
|
|
1673
|
+
written per instance — and content that grows while a panel is open grows with it.
|
|
1674
|
+
|
|
1675
|
+
| Prop | Default | What it does |
|
|
1676
|
+
| --------------- | ------- | ------------------------------------------------------------------------- |
|
|
1677
|
+
| `value` | — | Controlled: the values of every panel standing open, a `string[]`. |
|
|
1678
|
+
| `defaultValue` | — | Which panels start open. Left out, all of them are closed. |
|
|
1679
|
+
| `onValueChange` | — | `(open, { reason, event })` — `reason` is `'trigger'`. |
|
|
1680
|
+
| `multiple` | `false` | Whether several panels may stand open at once. |
|
|
1681
|
+
| `loop` | `true` | Whether Down and Up wrap at the ends. |
|
|
1682
|
+
| `level` | `3` | Which heading each header sits in. It has to fit the outline of the page. |
|
|
1683
|
+
|
|
1684
|
+
| Part | Props | Role |
|
|
1685
|
+
| ------------------- | ----------------------------------- | --------------------------------------------------- |
|
|
1686
|
+
| `Accordion.Item` | `value`, `disabled`, every Box prop | none — it owns the pair of ids |
|
|
1687
|
+
| `Accordion.Trigger` | `level`, `arrow`, every Box prop | `<button aria-expanded aria-controls>` in a heading |
|
|
1688
|
+
| `Accordion.Panel` | every Box prop | `region` + `aria-labelledby` |
|
|
1689
|
+
|
|
1690
|
+
| Key | What it does |
|
|
1691
|
+
| ------------- | --------------------------------------------------------------------------------------------- |
|
|
1692
|
+
| Tab | Every header is its own tab stop, and so is anything inside an open panel. |
|
|
1693
|
+
| Enter / Space | Opens the focused section, or closes it. |
|
|
1694
|
+
| Down / Up | The next and previous header, wrapping and stepping over disabled ones. Inert inside a panel. |
|
|
1695
|
+
| Home / End | The first and last header. |
|
|
1696
|
+
|
|
1697
|
+
An accordion is **not** a composite widget, so nothing here is a roving tabindex — that is the one thing
|
|
1698
|
+
to unlearn from `Tabs`, where the whole list is a single tab stop.
|
|
1699
|
+
|
|
1700
|
+
**A closed panel is hidden with `visibility`, and is always rendered.** Content behind a zero-height
|
|
1701
|
+
track is still laid out, still tabbable and still read out, so it has to be hidden by something —
|
|
1702
|
+
and `visibility` is the one that is animatable, flipping to hidden only once the track has closed and
|
|
1703
|
+
back the instant it opens. So there is no `@starting-style` and no `transitionBehavior="allow-discrete"`
|
|
1704
|
+
here, a server-rendered open panel does not animate itself open on load, a half-filled form in a panel
|
|
1705
|
+
survives being shut, and children too expensive to render closed are yours to gate:
|
|
1706
|
+
|
|
1707
|
+
```tsx
|
|
1708
|
+
<Accordion.Panel>{open ? <ExpensiveThing /> : null}</Accordion.Panel>
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
One panel stands open at a time unless `multiple`, and closing the open one is always allowed — so an
|
|
1712
|
+
accordion can stand with everything shut and there is no second prop for it. A `disabled` item is the
|
|
1713
|
+
`disabled` attribute, so the browser takes the header out of the tab sequence and the arrows step over it.
|
|
1714
|
+
The panel is a `role="region"` named by its header; past about six panels the landmarks are noise, which
|
|
1715
|
+
is APG's own caveat, and `props={{ role: undefined }}` drops it.
|
|
1716
|
+
|
|
1717
|
+
`Collapsible` is a **named export of the same module**: the same mechanism with no heading, no group and
|
|
1718
|
+
no arrow keys. The trigger is a render prop and the children are the content, the way `<Popover>` reads.
|
|
1719
|
+
|
|
1720
|
+
```tsx
|
|
1721
|
+
<Collapsible defaultOpen trigger={(trigger) => <Button {...trigger}>What is in the box?</Button>}>
|
|
1722
|
+
<P>A kite, and the string for it.</P>
|
|
1723
|
+
</Collapsible>
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
Styling is the `accordion` tree — `accordion.item`, `accordion.heading`, `accordion.trigger`,
|
|
1727
|
+
`accordion.arrow`, `accordion.panel` — plus `collapsible` for the lone one. A header's open state is its
|
|
1728
|
+
own `aria-expanded`, so `ariaAttr={{ expanded: … }}` styles it and no variant is involved:
|
|
1729
|
+
|
|
1730
|
+
```tsx
|
|
1731
|
+
<Accordion.Trigger py={4} ariaAttr={{ expanded: { color: 'emerald-600' } }}>
|
|
1732
|
+
Shipping
|
|
1733
|
+
</Accordion.Trigger>
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
Two parts are not yours: `accordion.track`, the grid whose row animates, and `accordion.clip` inside it,
|
|
1737
|
+
the bare grid item that clips. Bare is the point — **padding cannot be squeezed**, so a grid item carrying
|
|
1738
|
+
any floors the `0fr` track at exactly that much, leaving a permanent stub and a stretch of the transition
|
|
1739
|
+
where the height has stopped but the text is still painted (measured in Chrome, bug #142). The panel you
|
|
1740
|
+
pad is inside the clip, so its padding costs the track nothing; that padding is also the room a focus ring
|
|
1741
|
+
on something at the panel's edge needs, since the clip is permanent.
|
|
1742
|
+
|
|
1743
|
+
---
|
|
1744
|
+
|
|
1745
|
+
## Slider & Progress
|
|
1746
|
+
|
|
1747
|
+
```tsx
|
|
1748
|
+
import Slider from '@box-kite/react/components/slider';
|
|
1749
|
+
import Progress from '@box-kite/react/components/progress';
|
|
1750
|
+
|
|
1751
|
+
<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />;
|
|
1752
|
+
<Slider label="Price" defaultValue={[20, 80]} thumbLabels={['Lowest', 'Highest']} format={(value) => `${value} lei`} />;
|
|
1753
|
+
<Progress label="Upload" value={62} />;
|
|
1754
|
+
<Progress label="Preparing" />;
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
APG's slider and its multi-thumb sibling, plus the `role="progressbar"` beside them. **A number in is a
|
|
1758
|
+
number out**: `defaultValue={40}` is one thumb whose `onValueChange` hands back a `number`, and
|
|
1759
|
+
`defaultValue={[20, 80]}` is a range whose handler takes a `number[]`. The value's own shape is how many
|
|
1760
|
+
thumbs there are, so there is no second prop saying which kind this is and nothing to narrow at the call
|
|
1761
|
+
site.
|
|
1762
|
+
|
|
1763
|
+
| Prop (`Slider`) | Default | What it does |
|
|
1764
|
+
| --------------- | -------------- | ----------------------------------------------------------------------------- |
|
|
1765
|
+
| `value` | — | Controlled. A `number` is one thumb, a `number[]` is one per entry. |
|
|
1766
|
+
| `defaultValue` | `min` | What it starts at, and what decides how many thumbs there are. |
|
|
1767
|
+
| `onValueChange` | — | `(value, { reason, event })` on every step. `reason` is `pointer`/`keyboard`. |
|
|
1768
|
+
| `onValueCommit` | — | The same, once, when the interaction ends. The place for a request. |
|
|
1769
|
+
| `min` / `max` | `0` / `100` | The range. |
|
|
1770
|
+
| `step` | `1` | The grid every value lands on, counted from `min`. `0` is continuous. |
|
|
1771
|
+
| `largeStep` | `step * 10` | What PageUp and PageDown move by. |
|
|
1772
|
+
| `orientation` | `'horizontal'` | `'vertical'` counts up from the bottom edge, in either reading order. |
|
|
1773
|
+
| `disabled` | `false` | `aria-disabled`, so the thumb stays readable and in the tab order. |
|
|
1774
|
+
| `label` | — | Names the thumb on one, and the `role="group"` on a range. |
|
|
1775
|
+
| `labelledBy` | — | The same, naming an element that already says it. |
|
|
1776
|
+
| `thumbLabels` | — | One name per thumb. A range wants them — `label` names the group. |
|
|
1777
|
+
| `format` | — | `(value) => string`, written as `aria-valuetext`. |
|
|
1778
|
+
| `name` | — | Submits: one hidden input per thumb, so a range posts two values. |
|
|
1779
|
+
|
|
1780
|
+
| Prop (`Progress`) | Default | What it does |
|
|
1781
|
+
| ---------------------- | ----------- | ---------------------------------------------------------------- |
|
|
1782
|
+
| `value` | — | Leave it out for indeterminate: it reports no position at all. |
|
|
1783
|
+
| `min` / `max` | `0` / `100` | What "full" means. |
|
|
1784
|
+
| `label` / `labelledBy` | — | What names it. A bar with neither is a percentage about nothing. |
|
|
1785
|
+
| `format` | — | `(value) => string`, written as `aria-valuetext`. |
|
|
1786
|
+
|
|
1787
|
+
| Key | What it does |
|
|
1788
|
+
| ----------------- | ------------------------------------------------------------------------------- |
|
|
1789
|
+
| Tab | Every thumb is its own tab stop, so a range is two of them. |
|
|
1790
|
+
| Right / Up | One step towards the maximum. In a right-to-left page **Left** is the increase. |
|
|
1791
|
+
| Left / Down | One step towards the minimum. |
|
|
1792
|
+
| PageUp / PageDown | `largeStep`. |
|
|
1793
|
+
| Home / End | The minimum and the maximum themselves, on the step grid or off it. |
|
|
1794
|
+
|
|
1795
|
+
**Neither is the native element, and that is the one place the platform loses.** Every other control here
|
|
1796
|
+
is a real form element with a role over it — a `Switch` is a checkbox, a `RadioGroup` is a set of radios.
|
|
1797
|
+
But `<input type="range">` cannot hold two thumbs, and it and `<progress>` both draw themselves with
|
|
1798
|
+
vendor pseudo-elements (`::-webkit-slider-thumb`, `::-moz-range-track`) that no typed prop reaches, so a
|
|
1799
|
+
styled one is a rewrite of every part. `name` is what keeps the half worth keeping: the value submits.
|
|
1800
|
+
|
|
1801
|
+
**The position is an inline style, and it is the only thing here that is not a class.** A thumb's offset
|
|
1802
|
+
is a per-frame value, so a class for it would be a rule per frame of a drag, written into the stylesheet
|
|
1803
|
+
and never freed — the same exception `useAnchorPosition`'s anchor name and the travelling `Tabs`
|
|
1804
|
+
indicator take. Where the value is data rather than an animation, a `ProgressRing` (`components/chart`)
|
|
1805
|
+
rounds its fraction to half a percent and puts it in a shared class instead: nobody drags a ring.
|
|
1806
|
+
|
|
1807
|
+
**No `value` on a `Progress` is a state, not a zero.** `aria-valuenow` is omitted entirely rather than
|
|
1808
|
+
reported as `0` — a position nobody measured is the one thing a reader must not be told — and the bar
|
|
1809
|
+
sweeps instead. The sweep names its duration in milliseconds, so it is outside what `--transitionTime`
|
|
1810
|
+
zeroes and stops itself under `prefers-reduced-motion`.
|
|
1811
|
+
|
|
1812
|
+
**A press travels; a nudge does not.** A press on the track is the one move the eye has to follow, so the
|
|
1813
|
+
thumb animates the whole way. Every other move — a drag, an arrow key, a held arrow — is the `tracking`
|
|
1814
|
+
variant instead: 60ms `ease-out` on both the thumb and the fill, which must both carry it or they come
|
|
1815
|
+
apart. **Short rather than off, because off is exact and steps**: a value on a grid can only be at its grid
|
|
1816
|
+
positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. **Eased out rather than linear or
|
|
1817
|
+
eased**: a repeat restarts the transition every 33ms, so only its first half is ever seen, and on this curve
|
|
1818
|
+
that half is the straight part — it glides while held and still decelerates on the last transition, the one
|
|
1819
|
+
allowed to finish. Linear rode 1.4 steps behind the value and stopped at full speed; this rides 0.9 behind
|
|
1820
|
+
and lands at 0.6 of cruising. An arrow key taking the 250ms travel for one
|
|
1821
|
+
3.2px step was itself the lag: a tap arrives in 60ms now. `step` is the real dial:
|
|
1822
|
+
`step={0}` is continuous and interpolates nothing. And **a controlled slider re-renders whatever component
|
|
1823
|
+
owns its `useState`**, thirty times a second while an arrow is held — the slider costs 0.3ms a keystroke, so
|
|
1824
|
+
keep the value beside it rather than at the top of a page, or use `defaultValue` with `onValueCommit`.
|
|
1825
|
+
|
|
1826
|
+
**It mirrors for free.** The fill and the thumbs are placed with `inset-inline-start` and centred with a
|
|
1827
|
+
logical margin, so a right-to-left page draws the minimum on the right with nothing declared twice and no
|
|
1828
|
+
re-render. The half that is not free is the keyboard: the sideways arrows swap, read off the element's
|
|
1829
|
+
resolved direction when one arrives, and Up/Down never do.
|
|
1830
|
+
|
|
1831
|
+
Styling is `slider`/`slider.track`/`slider.fill`/`slider.thumb`, every part carrying the `vertical`
|
|
1832
|
+
variant, plus `progress`/`progress.fill`, whose `indeterminate` variant is the sweep. `Progress` renders
|
|
1833
|
+
on a **server** — no state, no effect, no measurement — so a page paints a real figure before any
|
|
1834
|
+
JavaScript arrives.
|
|
1835
|
+
|
|
1836
|
+
```tsx
|
|
1837
|
+
Box.components({
|
|
1838
|
+
slider: {
|
|
1839
|
+
children: {
|
|
1840
|
+
fill: { styles: { bgGradient: { linear: 'r', colors: ['sky-400', 'indigo-500'] } } },
|
|
1841
|
+
thumb: { styles: { borderColor: 'sky-500' } },
|
|
1842
|
+
},
|
|
1843
|
+
},
|
|
1844
|
+
});
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
---
|
|
1848
|
+
|
|
1849
|
+
## Toaster (`toast()` + `<Toaster>`)
|
|
1850
|
+
|
|
1851
|
+
```tsx
|
|
1852
|
+
import Toaster, { toast } from '@box-kite/react/components/toaster';
|
|
1853
|
+
|
|
1854
|
+
// once, near the root of the app
|
|
1855
|
+
<Toaster position="bottom-end" limit={3} />;
|
|
1856
|
+
|
|
1857
|
+
// then from anywhere at all — an event handler, a fetch, a module with no React in it
|
|
1858
|
+
toast.success('Saved');
|
|
1859
|
+
toast.error('Could not save', { action: { label: 'Retry', onClick: save } });
|
|
1860
|
+
toast('Row deleted', { description: 'Ada Lovelace, added in March.', action: { label: 'Undo', onClick: restore } });
|
|
1861
|
+
toast.promise(save(), { loading: 'Saving…', success: (saved) => `Saved as ${saved.name}`, error: 'Could not save' });
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
Messages the app sends rather than renders. The store behind `toast()` is framework-free, so a call made
|
|
1865
|
+
**before** the viewport mounts is queued rather than lost, and `<Toaster>` only draws what is there.
|
|
1866
|
+
|
|
1867
|
+
### The imperative API
|
|
1868
|
+
|
|
1869
|
+
| Call | What it does |
|
|
1870
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
1871
|
+
| `toast(message, options?)` | Adds one and returns its id. `message` is any `ReactNode`. |
|
|
1872
|
+
| `toast.success` / `error` / `warning` / `info` | The same with a `kind`. `error` is the assertive one — `role="alert"`. |
|
|
1873
|
+
| `toast.loading(message, options?)` | `duration: Infinity` by default: a spinner that timed out would report nothing. |
|
|
1874
|
+
| `toast.promise(promise, messages, options?)` | One toast for the whole call. The promise comes back untouched. |
|
|
1875
|
+
| `toast.update(id, message, options?)` | Changes one already on screen. An unknown id is ignored rather than added. |
|
|
1876
|
+
| `toast.dismiss(id?)` | Starts the exit for one, or for all of them with no id. |
|
|
1877
|
+
|
|
1878
|
+
An `options.id` that is already on screen is an **update**, not a second toast — which is exactly how
|
|
1879
|
+
`toast.promise` turns into one toast rather than two.
|
|
1880
|
+
|
|
1881
|
+
| Option | Default | What it does |
|
|
1882
|
+
| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
1883
|
+
| `id` | generated | Name it to update it later. |
|
|
1884
|
+
| `kind` | `'default'` | `success` / `error` / `warning` / `info` / `loading`. Decides the accent and the role. |
|
|
1885
|
+
| `description` | — | A second line under the message. Any `ReactNode`. |
|
|
1886
|
+
| `duration` | `<Toaster>`'s | **Milliseconds.** `Infinity` waits for a dismissal. |
|
|
1887
|
+
| `action` | — | `{ label, onClick, closeOnClick }` — one button, which dismisses the toast it answered unless `closeOnClick: false`. |
|
|
1888
|
+
| `dismissible` | `true` | Whether the close button is drawn. |
|
|
1889
|
+
| `onDismiss` | — | `(reason)` — `'timeout'` / `'close'` / `'action'` / `'imperative'`. |
|
|
1890
|
+
|
|
1891
|
+
### The viewport
|
|
1892
|
+
|
|
1893
|
+
| Prop | Default | What it does |
|
|
1894
|
+
| --------------- | ------------------------- | ------------------------------------------------------------------------------- |
|
|
1895
|
+
| `position` | `'bottom-end'` | Six corners. The inline half is logical, so `start`/`end` mirror with the page. |
|
|
1896
|
+
| `limit` | `3` | How many are on screen. The rest **queue**, timers and all. |
|
|
1897
|
+
| `duration` | `5000` | Milliseconds for a toast that names none of its own. |
|
|
1898
|
+
| `label` | `'Notifications'` | Names the region. It is never labelled visually. |
|
|
1899
|
+
| `closeLabel` | `'Close'` | Names the close button. |
|
|
1900
|
+
| `overflowLabel` | `` (n) => `+${n} more` `` | What the counter at the far end of the stack says. |
|
|
1901
|
+
| `hotkey` | `'F6'` | Moves focus to the stack. Takes `'alt+t'` and the like, or `false`. |
|
|
1902
|
+
| `store` | the default one | A second, independent stack — or a test that must not share state. |
|
|
1903
|
+
|
|
1904
|
+
**The limit is a queue, not a cap.** Past it a toast waits _with its timer unstarted_, so nothing expires
|
|
1905
|
+
that was never on screen.
|
|
1906
|
+
|
|
1907
|
+
### Four things to know, all measured in Chrome 153
|
|
1908
|
+
|
|
1909
|
+
1. **The region exists before there is anything in it.** The viewport is a `<section>` carrying
|
|
1910
|
+
`aria-live="polite"` from the moment it mounts — a live region inserted together with its content is
|
|
1911
|
+
not reliably announced. An error toast is `role="alert"`, which is assertive and is the one
|
|
1912
|
+
announcement pattern every screen reader implements; **nothing else carries a region of its own**,
|
|
1913
|
+
because the nearest region to a change is the one that speaks, and a second would only take the
|
|
1914
|
+
first's head start away.
|
|
1915
|
+
2. **The top layer, and no portal.** `popover="manual"`, shown as it mounts, so it paints over every
|
|
1916
|
+
stacking context and outside every clipped or transformed ancestor while staying where it was
|
|
1917
|
+
declared — it inherits the theme, the custom properties and the direction around it. `manual` rather
|
|
1918
|
+
than `auto` because a stack of messages owns no dismissal: a press outside has to reach the page. The
|
|
1919
|
+
viewport takes **no pointer events at all** and the toasts take them back, or a corner-sized fixed
|
|
1920
|
+
strip would swallow every press in its gaps.
|
|
1921
|
+
3. **Four things stop the clock**: the pointer over the stack, focus inside it, a background tab, and a
|
|
1922
|
+
dismissal. A paused timer resumes where it left off rather than restarting. WCAG 2.2.1, and the only
|
|
1923
|
+
reason a toast is allowed to carry a control at all.
|
|
1924
|
+
4. **A toast never takes focus.** `F6` moves focus to the viewport, Tab walks the toasts in the order
|
|
1925
|
+
they are on screen, and Escape dismisses the toast focus is in — handing focus back where it came
|
|
1926
|
+
from once the stack is empty.
|
|
1927
|
+
|
|
1928
|
+
### Styling
|
|
1929
|
+
|
|
1930
|
+
```tsx
|
|
1931
|
+
Box.components({
|
|
1932
|
+
toaster: {
|
|
1933
|
+
children: {
|
|
1934
|
+
toast: { styles: { borderRadius: 4, shadow: 'large' } },
|
|
1935
|
+
action: { styles: { borderColor: 'indigo-500' } },
|
|
1936
|
+
},
|
|
1937
|
+
},
|
|
1938
|
+
});
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
Parts: `toaster` (the viewport), `toaster.toast` — the kind is a **variant** on it, as is `fromTop` —
|
|
1942
|
+
`toaster.message`, `toaster.description`, `toaster.action`, `toaster.close` and `toaster.overflow`. The
|
|
1943
|
+
accent bar the kind paints is never the only signal: the message itself is what says what happened.
|
|
1944
|
+
|
|
1945
|
+
The viewport's own style tree has to answer the UA's `[popover]` rule, which is worth knowing before
|
|
1946
|
+
overriding it: `inset: 0` leaves a corner-pinned stack over-constrained (`top`/`left` win and it lands in
|
|
1947
|
+
the wrong corner) and `overflow: auto` makes it a scroll container, so all four sides are declared `auto`
|
|
1948
|
+
and `overflow: visible` before a `position` variant names the two it wants.
|
|
1949
|
+
|
|
1950
|
+
---
|
|
1951
|
+
|
|
1216
1952
|
## Tooltip Component
|
|
1217
1953
|
|
|
1218
1954
|
```tsx
|
|
@@ -1227,14 +1963,16 @@ component — `<Button {...trigger}>` — or, on a plain element,
|
|
|
1227
1963
|
`<button ref={trigger.ref} {...trigger.props}>`. The `ref` is what the bubble is positioned against,
|
|
1228
1964
|
so the tooltip sits under the trigger and adds nothing to the layout.
|
|
1229
1965
|
|
|
1230
|
-
| Prop
|
|
1231
|
-
|
|
|
1232
|
-
| `content`
|
|
1233
|
-
| `open` / `defaultOpen`
|
|
1234
|
-
| `onOpenChange`
|
|
1235
|
-
| `openDelay`
|
|
1236
|
-
| `closeDelay`
|
|
1237
|
-
| `
|
|
1966
|
+
| Prop | Default | What it does |
|
|
1967
|
+
| ---------------------- | ------------------- | ---------------------------------------------------------------------------------- |
|
|
1968
|
+
| `content` | — | The description. Nothing renders while it is empty. |
|
|
1969
|
+
| `open` / `defaultOpen` | — | Controlled / uncontrolled open state. |
|
|
1970
|
+
| `onOpenChange` | — | `(open, { reason, event })` — `hover`, `focus`, `pointer-leave`, `blur`, `escape`. |
|
|
1971
|
+
| `openDelay` | `300` | Hover dwell before it appears. Focus ignores it and shows immediately. |
|
|
1972
|
+
| `closeDelay` | `150` | Grace period after the pointer leaves, so it can travel onto the tooltip. |
|
|
1973
|
+
| `side` / `align` | `bottom` / `center` | Which side of the trigger the bubble sits on, and how it lines up. |
|
|
1974
|
+
| `offset` | `1` | The gap, on the ÷4 scale — 4px, near enough for the pointer to cross. |
|
|
1975
|
+
| `flip` | `true` | Whether a side with no room is swapped for its opposite. |
|
|
1238
1976
|
|
|
1239
1977
|
Every other Box prop styles the bubble, over the built-in `tooltip` component style. What the
|
|
1240
1978
|
component guarantees, so you do not wire it: `role="tooltip"`, `aria-describedby` on the trigger only
|
|
@@ -1242,6 +1980,13 @@ while it is open, opening on hover **and** focus, Escape to dismiss with focus l
|
|
|
1242
1980
|
no re-show until the pointer leaves and returns), the pointer able to move onto the tooltip, and no
|
|
1243
1981
|
auto-hide timer — the three WCAG 1.4.13 rules.
|
|
1244
1982
|
|
|
1983
|
+
The bubble is an `Overlay`, so it sits in the browser's **top layer** with no portal: it paints over
|
|
1984
|
+
every stacking context and out of every clipped ancestor, and it inherits the theme, the custom
|
|
1985
|
+
properties and the direction around it. Its popover type is `manual` on purpose — light dismiss would
|
|
1986
|
+
close the bubble behind the component's back, and WCAG 1.4.13 asks for a dismissal that _lasts_, which
|
|
1987
|
+
is stricter than what the platform does for free. `<Presence>` still owns the mount, so a closed
|
|
1988
|
+
tooltip renders nothing at all.
|
|
1989
|
+
|
|
1245
1990
|
---
|
|
1246
1991
|
|
|
1247
1992
|
## Chart primitives (`components/chart`)
|
|
@@ -1439,74 +2184,292 @@ import DataGrid from '@box-kite/react/components/dataGrid';
|
|
|
1439
2184
|
contextMenu: { sort: true, pin: true, group: false },
|
|
1440
2185
|
resizerStyle: 'hover',
|
|
1441
2186
|
}}
|
|
1442
|
-
|
|
2187
|
+
onSelectedRowKeysChange={(keys, { reason }) => console.log(keys, reason)}
|
|
1443
2188
|
/>;
|
|
1444
2189
|
```
|
|
1445
2190
|
|
|
1446
2191
|
### DataGridProps
|
|
1447
2192
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
|
1453
|
-
|
|
|
1454
|
-
| `
|
|
1455
|
-
| `
|
|
1456
|
-
| `
|
|
1457
|
-
| `
|
|
1458
|
-
| `
|
|
1459
|
-
| `
|
|
1460
|
-
| `
|
|
1461
|
-
| `
|
|
2193
|
+
Box style props too: `DataGridProps` extends them, so `<DataGrid b={1} borderRadius={2} p={4}>` styles
|
|
2194
|
+
the element wrapping the bars, the rows and the pager. Every callback below is a `ChangeHandler` —
|
|
2195
|
+
`(value, { reason }) => void`, the shape every component in the library reports a change under.
|
|
2196
|
+
|
|
2197
|
+
| Prop | Type | Description |
|
|
2198
|
+
| --------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
2199
|
+
| `data` | `TRow[]` | Row data (required) |
|
|
2200
|
+
| `def` | `GridDefinition` | Grid config (required) |
|
|
2201
|
+
| `component` | `string` | Style tree name (default: `'datagrid'`) |
|
|
2202
|
+
| `loading` | `boolean` | Loading state |
|
|
2203
|
+
| `filters` | `((row) => boolean)[]` | External predicate filters (applied before column/global filters) |
|
|
2204
|
+
| `page` / `pageSize` / `onPaginationChange` | `number` / `number` / `({ page, pageSize }, { reason })` | Controlled pagination (1-indexed). Reason: `page`/`page-size`/`sort`/`filter`/`clear` |
|
|
2205
|
+
| `onSortingChange` | `({ columnKey, direction } \| undefined, { reason })` | Sort. `undefined` once cleared; reason: `sort`/`clear` |
|
|
2206
|
+
| `onServerStateChange` | `(state, { reason })` | Unified: `{ page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }` |
|
|
2207
|
+
| `onSelectedRowKeysChange` | `(keys, { reason })` | Every selected key; reason: `select`/`deselect`/`select-all`/`clear` |
|
|
2208
|
+
| `expandedRowKeys` / `onExpandedRowKeysChange` | `Key[]` / `(keys, { reason })` | Controlled expanded rows; reason: `expand`/`collapse` |
|
|
2209
|
+
| `globalFilterValue` / `onGlobalFilterChange` | `string` / `(value, { reason })` | Controlled global filter; reason: `filter`/`clear` |
|
|
2210
|
+
| `columnFilters` / `onColumnFiltersChange` | `ColumnFilters` / `(filters, { reason })` | Controlled column filters; reason: `filter`/`clear` |
|
|
2211
|
+
| `onCellEditsChange` | `(edits, { reason })` | Every accepted edit, oldest first; reason: `edit`/`clear` |
|
|
2212
|
+
|
|
2213
|
+
`onSelectionChange(event)`, `onSortChange(key, dir)`, `onPageChange(page, size)` and
|
|
2214
|
+
`onPageSizeChange(size)` are the pre-contract spellings of the four above. They still fire; the
|
|
2215
|
+
named ones carry the reason.
|
|
1462
2216
|
|
|
1463
2217
|
### GridDefinition
|
|
1464
2218
|
|
|
1465
|
-
| Prop | Type
|
|
1466
|
-
| ------------------------- |
|
|
1467
|
-
| `columns` | `ColumnType[]`
|
|
1468
|
-
| `rowKey` | `keyof TRow \| (row) => Key`
|
|
1469
|
-
| `rowHeight` | `number`
|
|
1470
|
-
| `visibleRowsCount` | `number \| 'all'`
|
|
1471
|
-
| `showRowNumber` | `boolean \| { pinned?, width? }`
|
|
1472
|
-
| `rowSelection` | `boolean \| { pinned? }`
|
|
1473
|
-
| `rowDetail` | `RowDetailConfig`
|
|
1474
|
-
| `
|
|
1475
|
-
| `
|
|
1476
|
-
| `
|
|
1477
|
-
| `
|
|
1478
|
-
| `
|
|
1479
|
-
| `
|
|
1480
|
-
| `
|
|
1481
|
-
| `
|
|
1482
|
-
| `
|
|
2219
|
+
| Prop | Type | Default | Description |
|
|
2220
|
+
| ------------------------- | ------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
|
|
2221
|
+
| `columns` | `ColumnType[]` | required | Column definitions |
|
|
2222
|
+
| `rowKey` | `keyof TRow \| (row) => Key` | auto | Unique row identifier |
|
|
2223
|
+
| `rowHeight` | `number` | `48` | Row height in px |
|
|
2224
|
+
| `visibleRowsCount` | `number \| 'all'` | `10` | Visible rows. `'all'` disables virtualization |
|
|
2225
|
+
| `showRowNumber` | `boolean \| { pinned?, width? }` | `false` | Row number column |
|
|
2226
|
+
| `rowSelection` | `boolean \| { pinned? }` | `false` | Checkbox selection column |
|
|
2227
|
+
| `rowDetail` | `RowDetailConfig` | — | Expandable detail panel (see below) |
|
|
2228
|
+
| `treeData` | `TreeDataConfig` | — | Rows that hold rows: a `treegrid` with a chevron column (see below) |
|
|
2229
|
+
| `groupBy` | `Key[]` | — | The columns the grid starts grouped by, outermost first. The column menu takes it from there |
|
|
2230
|
+
| `groupDefaultExpanded` | `boolean \| number` | `false` | Which group rows start open: `true` for all, a number for how many levels down |
|
|
2231
|
+
| `editable` | `boolean` | `false` | Whether every column can be edited. A column’s own `editable` takes priority |
|
|
2232
|
+
| `onCellEdit` | `(edit) => void \| boolean \| string` | — | Judges an edit and is told about it: nothing accepts, a string refuses. May be async |
|
|
2233
|
+
| `dataSource` | `DataSource` | — | The grid fetches its own rows a block at a time; `data` is not read at all |
|
|
2234
|
+
| `pagination` | `{ totalCount, pageSize? }` | — | Server-side pagination. Bypasses client-side filtering |
|
|
2235
|
+
| `topBar` / `bottomBar` | `boolean` | `false` | Show top/bottom bars |
|
|
2236
|
+
| `title` / `topBarContent` | `ReactNode` | — | Top bar content |
|
|
2237
|
+
| `globalFilter` | `boolean` | `false` | Enable global fuzzy search |
|
|
2238
|
+
| `globalFilterKeys` | `(keyof TRow)[]` | all | Limit global filter columns |
|
|
2239
|
+
| `sortable` / `resizable` | `boolean` | `true` | Enable sorting/resizing for all columns |
|
|
2240
|
+
| `contextMenu` | `boolean \| ContextMenuConfig` | `true` | Control column header context menu. `false` hides it. Object: `{ sort?, pin?, group? }` |
|
|
2241
|
+
| `resizerStyle` | `'visible' \| 'hover' \| 'hidden'` | `'visible'` | Resizer handle visibility. `'hover'`: shows on header cell hover. `'hidden'`: invisible but resize still works |
|
|
2242
|
+
| `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
|
|
2243
|
+
| `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
|
|
2244
|
+
| `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
|
|
1483
2245
|
|
|
1484
2246
|
### ColumnType
|
|
1485
2247
|
|
|
1486
|
-
| Prop | Type | Default
|
|
1487
|
-
| ------------------------ | --------------------------------------------------- |
|
|
1488
|
-
| `key` | `Key` | required
|
|
1489
|
-
| `header` | `string` | —
|
|
1490
|
-
| `width` | `number` | `200`
|
|
1491
|
-
| `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'`
|
|
1492
|
-
| `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | —
|
|
1493
|
-
| `columns` | `ColumnType[]` | —
|
|
1494
|
-
| `Cell` | `({ cell }) => ReactNode` | —
|
|
1495
|
-
| `sortable` / `resizable` | `boolean` | inherits
|
|
1496
|
-
| `flexible` | `boolean` | `true`
|
|
1497
|
-
| `filterable` | `boolean \| FilterConfig` | —
|
|
1498
|
-
| `contextMenu` | `boolean \| ContextMenuConfig` | inherits
|
|
2248
|
+
| Prop | Type | Default | Description |
|
|
2249
|
+
| ------------------------ | --------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2250
|
+
| `key` | `Key` | required | Column identifier (maps to TRow property) |
|
|
2251
|
+
| `header` | `string` | — | Header text |
|
|
2252
|
+
| `width` | `number` | `200` | Base width in px |
|
|
2253
|
+
| `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
|
|
2254
|
+
| `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | — | Pin to an edge of the **inline axis** (sticky on scroll), so it holds under either `dir`. `LEFT`/`RIGHT` are the older spelling of the same two |
|
|
2255
|
+
| `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
|
|
2256
|
+
| `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }` |
|
|
2257
|
+
| `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
|
|
2258
|
+
| `flexible` | `boolean` | `true` | Participate in flex width distribution |
|
|
2259
|
+
| `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
|
|
2260
|
+
| `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
|
|
2261
|
+
| `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
|
|
2262
|
+
| `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
|
|
2263
|
+
| `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
|
|
2264
|
+
| `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
|
|
2265
|
+
| `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
|
|
2266
|
+
| `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
|
|
2267
|
+
| `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
|
|
2268
|
+
|
|
2269
|
+
### Aggregation
|
|
2270
|
+
|
|
2271
|
+
`aggregate` on a column totals it over the rows under each group row, and over the whole grid when
|
|
2272
|
+
`def.footer` is on. `AggregateName` is `'sum' | 'avg' | 'min' | 'max' | 'count'`; anything else is a
|
|
2273
|
+
function `(values: unknown[], rows: TRow[]) => number | string | null`, handed this column's value from
|
|
2274
|
+
every row in scope beside the rows themselves.
|
|
2275
|
+
|
|
2276
|
+
- **The four numeric ones skip everything that is not a number**, so a blank cell is not a zero — and a
|
|
2277
|
+
column holding no number at all answers **`null` rather than `0`**, because a total of nothing is not
|
|
2278
|
+
zero. `count` counts **rows**, which is why it is the one that never skips anything.
|
|
2279
|
+
- **`avg` rounds to two decimals.** Any other precision is an `AggregateCell`; the value itself is what
|
|
2280
|
+
the aggregation returned, and formatting is the renderer's.
|
|
2281
|
+
- **Every total covers the rows the filters left.** Under server-side pagination that is the page rather
|
|
2282
|
+
than the table, since those are the only rows the grid has.
|
|
2283
|
+
- **A group row's label cell stops spanning at the first aggregated column**, so each figure sits under
|
|
2284
|
+
its own heading. With nothing aggregating, it spans every data column beside it as it always did.
|
|
2285
|
+
- The footer is a row of the grid: `aria-rowcount` counts it, it is numbered last, and Ctrl+End lands in
|
|
2286
|
+
it. Style nodes: `datagrid.footer`, `datagrid.footer.cell`, `datagrid.footer.label`, and
|
|
2287
|
+
`datagrid.body.groupRow.aggregate` for the same value on a group row.
|
|
2288
|
+
|
|
2289
|
+
```tsx
|
|
2290
|
+
<DataGrid
|
|
2291
|
+
data={people}
|
|
2292
|
+
def={{
|
|
2293
|
+
footer: true, // or { label: 'All teams' }
|
|
2294
|
+
columns: [
|
|
2295
|
+
{ key: 'country' },
|
|
2296
|
+
{ key: 'username', header: 'People', aggregate: 'count' },
|
|
2297
|
+
{ key: 'age', header: 'Avg age', aggregate: 'avg' },
|
|
2298
|
+
{ key: 'salary', header: 'Payroll', aggregate: 'sum', AggregateCell: ({ cell }) => <Box px={3}>{format(cell.value)}</Box> },
|
|
2299
|
+
],
|
|
2300
|
+
}}
|
|
2301
|
+
/>
|
|
2302
|
+
```
|
|
2303
|
+
|
|
2304
|
+
### Export to Excel and CSV
|
|
2305
|
+
|
|
2306
|
+
**Never install a spreadsheet library for this.** The grid writes `.xlsx` itself — an `.xlsx` is a ZIP of
|
|
2307
|
+
XML parts, and both writers sit behind a dynamic import, so they cost the grid's bundle nothing until
|
|
2308
|
+
somebody presses the button and there is nothing to add to `package.json`.
|
|
2309
|
+
|
|
2310
|
+
Two ways in. `def.export` puts the buttons in the top bar (which needs `topBar: true`):
|
|
2311
|
+
|
|
2312
|
+
```tsx
|
|
2313
|
+
<DataGrid data={rows} def={{ topBar: true, export: true }} />
|
|
2314
|
+
// or: export: { csv: false, fileName: 'payroll' }
|
|
2315
|
+
```
|
|
2316
|
+
|
|
2317
|
+
A `ref` on the grid is the same two calls, for a toolbar of your own:
|
|
2318
|
+
|
|
2319
|
+
```tsx
|
|
2320
|
+
const grid = useRef<DataGridHandle>(null);
|
|
2321
|
+
|
|
2322
|
+
<DataGrid ref={grid} data={rows} def={{ columns }} />
|
|
2323
|
+
<Button onClick={() => grid.current?.exportXlsx({ sheetName: 'Q1' })}>Download</Button>
|
|
2324
|
+
```
|
|
2325
|
+
|
|
2326
|
+
Both take `fileName`, `columns` (keys, in the order given), `groups` and `footer`; `exportXlsx` also takes
|
|
2327
|
+
`sheetName`, `headerFill` and `headerColor` (as `RRGGBB`), and `exportCsv` takes `delimiter` and
|
|
2328
|
+
`escapeFormulas`. Both return a promise that resolves once the file has been handed to the browser.
|
|
2329
|
+
|
|
2330
|
+
- **The file is what the grid is showing**: the visible columns in their pinned order, the rows the filters
|
|
2331
|
+
and the sort left, the group rows and their totals. The one addition is a column hidden _because the grid
|
|
2332
|
+
is grouped by it_ — its values moved to the group rows, and a file with no Country column in it is not
|
|
2333
|
+
the grid.
|
|
2334
|
+
- **An export runs no React**, so a column drawn by a `Cell` renderer exports the raw field. The column's
|
|
2335
|
+
own `exportValue: (row) => …` is what it writes instead, and `exportFormat: '#,##0.00'` is the number
|
|
2336
|
+
format the workbook's cells wear.
|
|
2337
|
+
- **Values keep their type.** A number is a number, a boolean is a boolean, and a `Date` is a real date cell
|
|
2338
|
+
(given `yyyy-mm-dd` automatically when the column declares no format of its own).
|
|
2339
|
+
- **Grouping is Excel's own outline**, not indentation: a collapsed group in the grid opens collapsed in
|
|
2340
|
+
Excel, and every row is in the file either way. The workbook also gets a bold header on a frozen row,
|
|
2341
|
+
column widths from the grid's own, and an auto-filter when nothing is grouped.
|
|
2342
|
+
- **A CSV starts with a byte-order mark**, without which Excel reads UTF-8 as the local code page, and a
|
|
2343
|
+
value beginning `=`, `+`, `-` or `@` is quoted into text — a downloaded file must not run a formula
|
|
2344
|
+
somebody typed into a cell. `escapeFormulas: false` turns that off.
|
|
2345
|
+
- Style nodes: `datagrid.topBar.export` and `datagrid.topBar.export.button`.
|
|
1499
2346
|
|
|
1500
2347
|
### RowDetailConfig
|
|
1501
2348
|
|
|
1502
|
-
| Prop | Type | Default | Description
|
|
1503
|
-
| -------------------- | ------------------------------------- | -------- |
|
|
1504
|
-
| `content` | `(row: TRow) => ReactNode` | required | Render function for the detail panel
|
|
1505
|
-
| `height` | `'auto' \| number \| (row) => number` | `'auto'` | Detail row height
|
|
1506
|
-
| `expandOnRowClick` | `boolean` | `false` | Click anywhere on the row to toggle expansion
|
|
1507
|
-
| `pinned` | `boolean` | `false` | Pin the expand column to the reading start
|
|
1508
|
-
| `expandColumnWidth` | `number` | `50` | Width of the expand column in px
|
|
1509
|
-
| `expandColumnHeader` | `string` | `''` | Header text for the expand column
|
|
2349
|
+
| Prop | Type | Default | Description |
|
|
2350
|
+
| -------------------- | ------------------------------------- | -------- | ---------------------------------------------- |
|
|
2351
|
+
| `content` | `(row: TRow) => ReactNode` | required | Render function for the detail panel |
|
|
2352
|
+
| `height` | `'auto' \| number \| (row) => number` | `'auto'` | Detail row height |
|
|
2353
|
+
| `expandOnRowClick` | `boolean` | `false` | Click anywhere on the row to toggle expansion |
|
|
2354
|
+
| `pinned` | `boolean` | `false` | Pin the expand column to the reading start |
|
|
2355
|
+
| `expandColumnWidth` | `number` | `50` | Width of the expand column in px |
|
|
2356
|
+
| `expandColumnHeader` | `string` | `''` | Header text for the expand column |
|
|
2357
|
+
| `scrollIntoView` | `boolean` | `true` | Scroll a panel opened below the fold into view |
|
|
2358
|
+
|
|
2359
|
+
### TreeDataConfig
|
|
2360
|
+
|
|
2361
|
+
Rows that hold rows. The shape comes off the data — nested in it, or named by a path on every row — and
|
|
2362
|
+
the grid becomes a `role="treegrid"`: one column grows a chevron and an indent, and every row carries
|
|
2363
|
+
`aria-level`, `aria-posinset`, `aria-setsize` and, where there is something under it, `aria-expanded`.
|
|
2364
|
+
On that column **Right** opens a shut row, **Left** shuts an open one and steps out to the parent of a
|
|
2365
|
+
row that is already shut (they swap in a right-to-left grid).
|
|
2366
|
+
|
|
2367
|
+
| Prop | Type | Default | Description |
|
|
2368
|
+
| ----------------- | ------------------------------------ | --------- | ------------------------------------------------------------------------- |
|
|
2369
|
+
| `childrenKey` | `keyof TRow` | — | Nested data: the field a row's children live in |
|
|
2370
|
+
| `getChildren` | `(row: TRow) => TRow[] \| undefined` | — | Nested data, worked out rather than stored |
|
|
2371
|
+
| `pathKey` | `keyof TRow` | — | Flat data: the field holding the path to this row, its own segment last |
|
|
2372
|
+
| `getPath` | `(row: TRow) => Key[]` | — | Flat data, worked out |
|
|
2373
|
+
| `column` | `Key` | first own | Which column carries the chevrons and the indent |
|
|
2374
|
+
| `indent` | `number` | `4` | How far one level indents, on the ÷4 spacing scale |
|
|
2375
|
+
| `defaultExpanded` | `boolean \| number \| Key[]` | none open | `true` for every row, a number for a depth, or the keys to open |
|
|
2376
|
+
| `selection` | `'self' \| 'cascade'` | `'self'` | `cascade` ticks the whole subtree, and half of one reads as indeterminate |
|
|
2377
|
+
| `hasChildren` | `keyof TRow \| (row) => boolean` | — | **Lazy only**: whether the row holds rows the server can be asked for |
|
|
2378
|
+
|
|
2379
|
+
```tsx
|
|
2380
|
+
<DataGrid
|
|
2381
|
+
data={files}
|
|
2382
|
+
def={{
|
|
2383
|
+
rowKey: 'id',
|
|
2384
|
+
treeData: { childrenKey: 'children', defaultExpanded: 1, selection: 'cascade' },
|
|
2385
|
+
columns: [
|
|
2386
|
+
{ key: 'name', header: 'Name' },
|
|
2387
|
+
{ key: 'size', header: 'Size', align: 'end' },
|
|
2388
|
+
],
|
|
2389
|
+
}}
|
|
2390
|
+
// Controlled instead: `expandedTreeKeys` is the open rows, and this reports them.
|
|
2391
|
+
onExpandedTreeKeysChange={(keys, { reason }) => setOpen(keys)}
|
|
2392
|
+
/>
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
A filter over a tree keeps **a match and every ancestor of one**, so the way to a match stays visible; a
|
|
2396
|
+
sort happens inside each parent. Nothing under a shut row is built at all. _Group By_ is not offered on a
|
|
2397
|
+
tree: the rows are already in a shape.
|
|
2398
|
+
|
|
2399
|
+
**Beside `def.dataSource` the tree is the server's** — one level per open row, and the browser holds none
|
|
2400
|
+
of it. Each request carries `treeKeys` (the row whose children are wanted, named by its `rowKey` and its
|
|
2401
|
+
ancestors', `[]` for the top), and `hasChildren` is how a row says there is a level to ask for — without
|
|
2402
|
+
one no row grows a chevron. `childrenKey`/`getChildren`/`pathKey`/`getPath` are not read, `selection: 'cascade'`
|
|
2403
|
+
is not offered (a tick cannot reach rows nobody has fetched), and `defaultExpanded` costs one request per
|
|
2404
|
+
row it opens, so name the keys rather than passing `true`.
|
|
2405
|
+
|
|
2406
|
+
```tsx
|
|
2407
|
+
<DataGrid
|
|
2408
|
+
def={{
|
|
2409
|
+
rowKey: 'id',
|
|
2410
|
+
dataSource: { getRows: async ({ startRow, endRow, treeKeys, signal }) => fetchLevel(treeKeys, startRow, endRow, signal) },
|
|
2411
|
+
treeData: { hasChildren: 'folder', column: 'name' },
|
|
2412
|
+
columns: [
|
|
2413
|
+
{ key: 'name', header: 'Name' },
|
|
2414
|
+
{ key: 'size', header: 'Size', align: 'end' },
|
|
2415
|
+
],
|
|
2416
|
+
}}
|
|
2417
|
+
/>
|
|
2418
|
+
```
|
|
2419
|
+
|
|
2420
|
+
### Cell editing
|
|
2421
|
+
|
|
2422
|
+
`column.editable` is the opt-in and `def.onCellEdit` is the one function that both **judges** an edit and
|
|
2423
|
+
is **told** about it: answer nothing (or `true`) to accept the value, a string to refuse it with that
|
|
2424
|
+
message, `false` to refuse it with the grid's own. It may return a promise, so a check against a server is
|
|
2425
|
+
the same function — and an answer to an edit the user has since abandoned is dropped.
|
|
2426
|
+
|
|
2427
|
+
```tsx
|
|
2428
|
+
<DataGrid
|
|
2429
|
+
data={people}
|
|
2430
|
+
def={{
|
|
2431
|
+
rowKey: 'id',
|
|
2432
|
+
onCellEdit: ({ columnKey, value, oldValue, row, rowKey }) => {
|
|
2433
|
+
if (columnKey === 'name' && !String(value).trim()) return 'A name is required';
|
|
2434
|
+
},
|
|
2435
|
+
columns: [
|
|
2436
|
+
{ key: 'name', header: 'Name', editable: true },
|
|
2437
|
+
{ key: 'salary', header: 'Salary', editable: true, align: 'end' }, // a number, so a number field
|
|
2438
|
+
{ key: 'active', header: 'Active', editable: (row) => row.salary > 0 }, // a boolean, so a checkbox
|
|
2439
|
+
{ key: 'country', editable: true, editor: { type: 'select', options: [{ value: 'Japan' }, { value: 'Peru' }] } },
|
|
2440
|
+
],
|
|
2441
|
+
}}
|
|
2442
|
+
onCellEditsChange={(edits) => setEdits(edits)}
|
|
2443
|
+
/>
|
|
2444
|
+
```
|
|
2445
|
+
|
|
2446
|
+
- **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean
|
|
2447
|
+
`checkbox`, everything else `text` — unless `column.editor` names one (`text`/`number`/`checkbox`/
|
|
2448
|
+
`select`) or configures it (`{ type, options?, placeholder?, step?, min?, max? }`; `options` may be a
|
|
2449
|
+
function of the row). `column.EditCell` is a control of your own, bound to `cell.draft` with
|
|
2450
|
+
`cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` — the same three calls the built-in four
|
|
2451
|
+
make.
|
|
2452
|
+
- **Pointer:** a press makes a cell the current one — it takes the ring, whether the pointer or the
|
|
2453
|
+
keyboard put it there — and a **double press opens the editor** on an editable cell. A cell that cannot
|
|
2454
|
+
be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a
|
|
2455
|
+
tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor.
|
|
2456
|
+
- **Keys (APG's editable cell):** Enter or F2 opens the editor, any printable character opens it on that
|
|
2457
|
+
character (replacing the value), Escape throws the draft away, Enter commits and hands the keyboard back
|
|
2458
|
+
to the cell, and **Tab commits and opens the next editable cell** — along the row and on into the rows
|
|
2459
|
+
after it. A press somewhere else commits; `checkbox` and `select` commit on the change instead.
|
|
2460
|
+
- **A refused value keeps the editor open**, with the message in a `role="alert"` in the top layer (a
|
|
2461
|
+
bubble inside the scroller would be clipped on the last row) and `aria-invalid` + `aria-describedby` on
|
|
2462
|
+
the field. Typing clears the message.
|
|
2463
|
+
- **The grid does not own `data`, so an accepted value is kept as an edit over the rows it was given**, and
|
|
2464
|
+
is what every cell, every `Cell` renderer and every export reads. That is what lets a `def.dataSource`
|
|
2465
|
+
grid be edited at all. `onCellEditsChange(edits, { reason })` reports the whole list, oldest first —
|
|
2466
|
+
the stream an undo is built from — and `ref.clearEdits()` hands them back once your own data holds them.
|
|
2467
|
+
A datasource `refresh()` drops them too.
|
|
2468
|
+
- **An edit does not re-sort or re-filter the grid.** Sorting and filtering read the row's own values; only
|
|
2469
|
+
what is _displayed_ reads the edit, so a row never jumps out from under the person typing into it.
|
|
2470
|
+
- Style nodes: `datagrid.body.cell` gains `isEditing` and `isInvalid` variants — one ring, red when the
|
|
2471
|
+
value was refused — and `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error` are
|
|
2472
|
+
the editor and its message.
|
|
1510
2473
|
|
|
1511
2474
|
### ContextMenuConfig
|
|
1512
2475
|
|
|
@@ -1521,8 +2484,9 @@ import DataGrid from '@box-kite/react/components/dataGrid';
|
|
|
1521
2484
|
```tsx
|
|
1522
2485
|
<DataGrid
|
|
1523
2486
|
data={pageData} page={page} loading={loading}
|
|
1524
|
-
onServerStateChange={(state) => {
|
|
2487
|
+
onServerStateChange={(state, { reason }) => {
|
|
1525
2488
|
// state = { page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }
|
|
2489
|
+
// reason = what moved: 'page' | 'page-size' | 'sort' | 'filter' | 'clear'
|
|
1526
2490
|
setPage(state.page); refetch(state);
|
|
1527
2491
|
}}
|
|
1528
2492
|
def={{ columns: [...], bottomBar: true, pagination: { totalCount, pageSize: 25 }, globalFilter: true }}
|
|
@@ -1562,7 +2526,8 @@ not add roles or `tabIndex` by hand.
|
|
|
1562
2526
|
- Keyboard: one cell is in the tab order at a time. Arrows move it, Home/End go to the row's ends,
|
|
1563
2527
|
Ctrl+Home/End to the grid's corners (scrolling to a row that has not been rendered), PageUp/Down
|
|
1564
2528
|
move a screenful. Enter/Space sorts on a sortable header and otherwise steps into the cell's own
|
|
1565
|
-
control; F2 steps in even on a header; Escape hands the
|
|
2529
|
+
control — or, on an editable cell, opens its editor; F2 steps in even on a header; Escape hands the
|
|
2530
|
+
keyboard back to the cell, and from an editor throws the draft away as well. Down/Up keep
|
|
1566
2531
|
the **column**, not the cell ordinal, so they land under where they started even through a
|
|
1567
2532
|
grouped header or a group row whose cells cover several columns each.
|
|
1568
2533
|
- The column resizer is APG's window splitter: `role="separator"` with `aria-valuenow`/`-valuemin`/
|
|
@@ -1582,47 +2547,52 @@ stops, as they were before A7.
|
|
|
1582
2547
|
|
|
1583
2548
|
### Component Style Tree
|
|
1584
2549
|
|
|
1585
|
-
| Component Name | Description | Variants
|
|
1586
|
-
| --------------------------------------------------------- | ------------------------------------------------ |
|
|
1587
|
-
| `datagrid` | Root container | —
|
|
1588
|
-
| `datagrid.content` | Scroll container for header + body | —
|
|
1589
|
-
| `datagrid.topBar` | Top bar (title, filters, column groups) | —
|
|
1590
|
-
| `datagrid.topBar.globalFilter` | Global search wrapper | —
|
|
1591
|
-
| `datagrid.topBar.globalFilter.stats` | Filtered rows count badge | —
|
|
1592
|
-
| `datagrid.topBar.columnGroups` | Column group chips container | —
|
|
1593
|
-
| `datagrid.topBar.columnGroups.icon` | Column group icon | —
|
|
1594
|
-
| `datagrid.topBar.columnGroups.separator` | Separator between groups | —
|
|
1595
|
-
| `datagrid.topBar.columnGroups.item` | Column group chip | —
|
|
1596
|
-
| `datagrid.topBar.columnGroups.item.icon` | Remove icon on chip | —
|
|
1597
|
-
| `datagrid.topBar.columnVisibility` | Column visibility dropdown | —
|
|
1598
|
-
| `datagrid.topBar.columnVisibility.badge` | Hidden columns count badge | —
|
|
1599
|
-
| `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`
|
|
1600
|
-
| `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | —
|
|
1601
|
-
| `datagrid.header` | Header grid container (sticky) | —
|
|
1602
|
-
| `datagrid.header.cell` | Header cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isSortable`, `isRowSelection`, `isRowNumber`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell`
|
|
1603
|
-
| `datagrid.header.cell.contextMenu` | Column context menu button | —
|
|
1604
|
-
| `datagrid.header.cell.contextMenu.icon` | Context menu icon | —
|
|
1605
|
-
| `datagrid.header.cell.contextMenu.tooltip` | Context menu popup | —
|
|
1606
|
-
| `datagrid.header.cell.contextMenu.tooltip.item` | Context menu action | —
|
|
1607
|
-
| `datagrid.header.cell.contextMenu.tooltip.item.icon` | Action icon | —
|
|
1608
|
-
| `datagrid.header.cell.contextMenu.tooltip.item.separator` | Menu separator line | —
|
|
1609
|
-
| `datagrid.header.cell.resizer` | Column resize handle | —
|
|
1610
|
-
| `datagrid.body` | Body grid container (virtualized rows) | —
|
|
1611
|
-
| `datagrid.body.cell` | Body cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isRowNumber`, `isRowSelection`, `isRowSelected`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell` |
|
|
1612
|
-
| `datagrid.body.cell.text` | Default cell text renderer | —
|
|
1613
|
-
| `datagrid.body.cell.
|
|
1614
|
-
| `datagrid.body.
|
|
1615
|
-
| `datagrid.body.
|
|
1616
|
-
| `datagrid.body.
|
|
1617
|
-
| `datagrid.body.
|
|
1618
|
-
| `datagrid.body.
|
|
1619
|
-
| `datagrid.
|
|
1620
|
-
| `datagrid.
|
|
1621
|
-
| `datagrid.
|
|
1622
|
-
| `datagrid.
|
|
1623
|
-
| `datagrid.
|
|
1624
|
-
| `datagrid.
|
|
1625
|
-
| `datagrid.bottomBar
|
|
2550
|
+
| Component Name | Description | Variants |
|
|
2551
|
+
| --------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2552
|
+
| `datagrid` | Root container | — |
|
|
2553
|
+
| `datagrid.content` | Scroll container for header + body | — |
|
|
2554
|
+
| `datagrid.topBar` | Top bar (title, filters, column groups) | — |
|
|
2555
|
+
| `datagrid.topBar.globalFilter` | Global search wrapper | — |
|
|
2556
|
+
| `datagrid.topBar.globalFilter.stats` | Filtered rows count badge | — |
|
|
2557
|
+
| `datagrid.topBar.columnGroups` | Column group chips container | — |
|
|
2558
|
+
| `datagrid.topBar.columnGroups.icon` | Column group icon | — |
|
|
2559
|
+
| `datagrid.topBar.columnGroups.separator` | Separator between groups | — |
|
|
2560
|
+
| `datagrid.topBar.columnGroups.item` | Column group chip | — |
|
|
2561
|
+
| `datagrid.topBar.columnGroups.item.icon` | Remove icon on chip | — |
|
|
2562
|
+
| `datagrid.topBar.columnVisibility` | Column visibility dropdown | — |
|
|
2563
|
+
| `datagrid.topBar.columnVisibility.badge` | Hidden columns count badge | — |
|
|
2564
|
+
| `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned` |
|
|
2565
|
+
| `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | — |
|
|
2566
|
+
| `datagrid.header` | Header grid container (sticky) | — |
|
|
2567
|
+
| `datagrid.header.cell` | Header cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isSortable`, `isRowSelection`, `isRowNumber`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell` |
|
|
2568
|
+
| `datagrid.header.cell.contextMenu` | Column context menu button | — |
|
|
2569
|
+
| `datagrid.header.cell.contextMenu.icon` | Context menu icon | — |
|
|
2570
|
+
| `datagrid.header.cell.contextMenu.tooltip` | Context menu popup | — |
|
|
2571
|
+
| `datagrid.header.cell.contextMenu.tooltip.item` | Context menu action | — |
|
|
2572
|
+
| `datagrid.header.cell.contextMenu.tooltip.item.icon` | Action icon | — |
|
|
2573
|
+
| `datagrid.header.cell.contextMenu.tooltip.item.separator` | Menu separator line | — |
|
|
2574
|
+
| `datagrid.header.cell.resizer` | Column resize handle | — |
|
|
2575
|
+
| `datagrid.body` | Body grid container (virtualized rows) | — |
|
|
2576
|
+
| `datagrid.body.cell` | Body cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isRowNumber`, `isRowSelection`, `isRowSelected`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell`, `isEditing`, `isInvalid` |
|
|
2577
|
+
| `datagrid.body.cell.text` | Default cell text renderer | — |
|
|
2578
|
+
| `datagrid.body.cell.editor` | The open cell editor | `isPending` |
|
|
2579
|
+
| `datagrid.body.cell.error` | What a refused value says, in the top layer | — |
|
|
2580
|
+
| `datagrid.body.cell.rowDetail` | Row detail expand/collapse button | — |
|
|
2581
|
+
| `datagrid.body.cell.tree` | Tree cell: the indent, the chevron and the value | — |
|
|
2582
|
+
| `datagrid.body.cell.tree.toggle` | Tree expand/collapse chevron | `isExpanded` |
|
|
2583
|
+
| `datagrid.body.cell.tree.spacer` | What a leaf puts where its chevron would be | — |
|
|
2584
|
+
| `datagrid.body.row` | Data row (display: contents) | — |
|
|
2585
|
+
| `datagrid.body.groupRow` | Group row (display: contents) | — |
|
|
2586
|
+
| `datagrid.body.groupRow.expandButton` | Group expand/collapse button | — |
|
|
2587
|
+
| `datagrid.body.detailRow` | Expanded detail row | — |
|
|
2588
|
+
| `datagrid.body.empty` | Empty state container | — |
|
|
2589
|
+
| `datagrid.emptyColumns` | No columns selected placeholder | — |
|
|
2590
|
+
| `datagrid.bottomBar` | Bottom bar (row count, pagination) | — |
|
|
2591
|
+
| `datagrid.bottomBar.info` | Status text ("Rows: ...", "Selected: ...") | — |
|
|
2592
|
+
| `datagrid.bottomBar.clearFilters` | "Clear filters" link | — |
|
|
2593
|
+
| `datagrid.bottomBar.pagination` | Pagination controls wrapper | — |
|
|
2594
|
+
| `datagrid.bottomBar.pagination.button` | Pagination nav button | — |
|
|
2595
|
+
| `datagrid.bottomBar.pagination.info` | Page info text ("1 of 5") | — |
|
|
1626
2596
|
|
|
1627
2597
|
---
|
|
1628
2598
|
|
|
@@ -1636,14 +2606,14 @@ import Dropdown from '@box-kite/react/components/dropdown';
|
|
|
1636
2606
|
|
|
1637
2607
|
```tsx
|
|
1638
2608
|
// Single selection (uncontrolled). `label` is not decoration — see Accessibility below.
|
|
1639
|
-
<Dropdown<string> label="Fruit" defaultValue="apple"
|
|
2609
|
+
<Dropdown<string> label="Fruit" defaultValue="apple" onValueChange={(fruit, { reason }) => console.log(fruit, reason)}>
|
|
1640
2610
|
<Dropdown.Unselect>Pick a fruit...</Dropdown.Unselect>
|
|
1641
2611
|
<Dropdown.Item value="apple">Apple</Dropdown.Item>
|
|
1642
2612
|
<Dropdown.Item value="banana">Banana</Dropdown.Item>
|
|
1643
2613
|
</Dropdown>
|
|
1644
2614
|
|
|
1645
2615
|
// Controlled
|
|
1646
|
-
<Dropdown<string> label="Fruit" value={fruit}
|
|
2616
|
+
<Dropdown<string> label="Fruit" value={fruit} onValueChange={(value) => setFruit(value as string)}>
|
|
1647
2617
|
<Dropdown.Item value="apple">Apple</Dropdown.Item>
|
|
1648
2618
|
<Dropdown.Item value="banana">Banana</Dropdown.Item>
|
|
1649
2619
|
</Dropdown>
|
|
@@ -1663,21 +2633,22 @@ import Dropdown from '@box-kite/react/components/dropdown';
|
|
|
1663
2633
|
|
|
1664
2634
|
### Props
|
|
1665
2635
|
|
|
1666
|
-
| Prop | Type | Description
|
|
1667
|
-
| ------------------------ | ---------------------------------------------------- |
|
|
1668
|
-
| `value` / `defaultValue` | `TVal \| TVal[]` | Controlled / uncontrolled selected value(s)
|
|
1669
|
-
| `label` | `ReactNode` | The control's name, rendered above it and wired with `aria-labelledby`
|
|
1670
|
-
| `labelProps` | `BoxProps<'div'>` | Styles for the wrapper the label and the trigger share
|
|
1671
|
-
| `multiple` | `boolean` | Multi-select mode
|
|
1672
|
-
| `isSearchable` | `boolean` | Editable combobox: a text field that filters the list (see Accessibility)
|
|
1673
|
-
| `searchPlaceholder` | `string` | Search input placeholder
|
|
1674
|
-
| `hideIcon` | `boolean` | Hide chevron icon
|
|
1675
|
-
| `showCheckbox` | `boolean` | Show checkboxes in multiple mode
|
|
1676
|
-
| `name` | `string` | Form field name (renders hidden `<input>` elements)
|
|
1677
|
-
| `
|
|
1678
|
-
| `
|
|
1679
|
-
| `
|
|
1680
|
-
| `
|
|
2636
|
+
| Prop | Type | Description |
|
|
2637
|
+
| ------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
2638
|
+
| `value` / `defaultValue` | `TVal \| TVal[]` | Controlled / uncontrolled selected value(s) |
|
|
2639
|
+
| `label` | `ReactNode` | The control's name, rendered above it and wired with `aria-labelledby` |
|
|
2640
|
+
| `labelProps` | `BoxProps<'div'>` | Styles for the wrapper the label and the trigger share |
|
|
2641
|
+
| `multiple` | `boolean` | Multi-select mode |
|
|
2642
|
+
| `isSearchable` | `boolean` | Editable combobox: a text field that filters the list (see Accessibility) |
|
|
2643
|
+
| `searchPlaceholder` | `string` | Search input placeholder |
|
|
2644
|
+
| `hideIcon` | `boolean` | Hide chevron icon |
|
|
2645
|
+
| `showCheckbox` | `boolean` | Show checkboxes in multiple mode |
|
|
2646
|
+
| `name` | `string` | Form field name (renders hidden `<input>` elements) |
|
|
2647
|
+
| `onValueChange` | `ChangeHandler<TVal \| TVal[] \| undefined, …>` | The selection — an array in `multiple` mode — and why: `select`, `deselect`, `select-all`, `clear` |
|
|
2648
|
+
| `onChange` | `(value: TVal \| undefined, values: TVal[]) => void` | **Deprecated**, still firing. The option acted on, and the selection beside it |
|
|
2649
|
+
| `itemsProps` | `BoxStyleProps` | Style overrides for the opened items container (`dropdown.items`) |
|
|
2650
|
+
| `iconProps` | `BoxStyleProps` | Style overrides for the chevron icon container (`dropdown.icon`) |
|
|
2651
|
+
| `variant` | `ClassNameType` | Propagates to root **and all child sub-components** |
|
|
1681
2652
|
|
|
1682
2653
|
Also accepts all `BoxProps` (styling props), which apply to the root element — a `<button>`, or the wrapper around the text field when `isSearchable`. Anything in `props` goes to the combobox itself: the button, or that field.
|
|
1683
2654
|
|
|
@@ -1775,15 +2746,15 @@ Box.components({
|
|
|
1775
2746
|
|
|
1776
2747
|
### Component Style Tree
|
|
1777
2748
|
|
|
1778
|
-
| Component Name | Description
|
|
1779
|
-
| -------------------- |
|
|
1780
|
-
| `dropdown` | Root button trigger
|
|
1781
|
-
| `dropdown.items` | Opened items container (
|
|
1782
|
-
| `dropdown.item` | Selectable item
|
|
1783
|
-
| `dropdown.unselect` | Clear selection option
|
|
1784
|
-
| `dropdown.selectAll` | Select all option
|
|
1785
|
-
| `dropdown.emptyItem` | No results placeholder
|
|
1786
|
-
| `dropdown.icon` | Chevron arrow container
|
|
2749
|
+
| Component Name | Description | Built-in Variants |
|
|
2750
|
+
| -------------------- | ---------------------------------- | --------------------- |
|
|
2751
|
+
| `dropdown` | Root button trigger | `compact` |
|
|
2752
|
+
| `dropdown.items` | Opened items container (top layer) | — |
|
|
2753
|
+
| `dropdown.item` | Selectable item | `compact`, `multiple` |
|
|
2754
|
+
| `dropdown.unselect` | Clear selection option | `compact` |
|
|
2755
|
+
| `dropdown.selectAll` | Select all option | `compact` |
|
|
2756
|
+
| `dropdown.emptyItem` | No results placeholder | `compact` |
|
|
2757
|
+
| `dropdown.icon` | Chevron arrow container | — |
|
|
1787
2758
|
|
|
1788
2759
|
---
|
|
1789
2760
|
|
|
@@ -1796,7 +2767,7 @@ import Select from '@box-kite/react/components/select';
|
|
|
1796
2767
|
|
|
1797
2768
|
// Basic — `label` names the combobox, same as on Dropdown
|
|
1798
2769
|
<Select<User, number> label="User" data={users} def={{ valueKey: 'id', displayKey: 'name', placeholder: 'Pick...' }}
|
|
1799
|
-
value={selected}
|
|
2770
|
+
value={selected} onValueChange={(value) => setSelected(value as number)} />
|
|
1800
2771
|
|
|
1801
2772
|
// Multiple + search + custom display
|
|
1802
2773
|
<Select<User, number> label="Users" data={users} multiple showCheckbox isSearchable searchPlaceholder="Search..."
|
|
@@ -1820,7 +2791,7 @@ import Select from '@box-kite/react/components/select';
|
|
|
1820
2791
|
| `selectAllText` | `string` | Select all option text (multiple mode) |
|
|
1821
2792
|
| `emptyText` | `string` | Empty search results text |
|
|
1822
2793
|
|
|
1823
|
-
Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onChange
|
|
2794
|
+
Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onValueChange` (and the deprecated `onChange`), `itemsProps`, `iconProps`, `variant`, and all BoxProps. Same styling, variants and combobox accessibility as Dropdown — including owing it a name, and including `isSearchable` switching it to the editable combobox.
|
|
1824
2795
|
|
|
1825
2796
|
---
|
|
1826
2797
|
|
|
@@ -1830,4 +2801,4 @@ Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `mu
|
|
|
1830
2801
|
2. **Class names**: Elements get classes like `_b`, `_2a`, etc.
|
|
1831
2802
|
3. **CSS variables**: In `:root` rules
|
|
1832
2803
|
4. **Theme issues**: Ensure `<Box.Theme>` wraps your app
|
|
1833
|
-
5. **
|
|
2804
|
+
5. **Layer theming**: a tooltip or dropdown popup stays where it was declared and inherits the theme; only the no-Popover-API fallback uses the `#box-kite-portal` container
|