@box-kite/react 0.0.1 → 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 +65 -0
- package/.claude/skills/box-kite/SKILL.md +138 -0
- 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 +2804 -0
- package/BOX_KITE_AI_CONTEXT.md +2804 -0
- package/LICENSE +21 -0
- package/README.md +744 -7
- package/a11y.cjs +1 -0
- package/a11y.d.ts +15 -0
- package/a11y.mjs +4 -0
- package/anchor.cjs +1 -0
- package/anchor.d.ts +14 -0
- package/anchor.mjs +3 -0
- package/behavior.cjs +1 -0
- package/behavior.mjs +277 -0
- package/box.cjs +1 -0
- package/box.d.ts +46 -0
- package/box.mjs +23 -0
- package/client.cjs +1 -0
- package/client.mjs +164 -0
- package/components/accordion.cjs +1 -0
- package/components/accordion.d.ts +114 -0
- package/components/accordion.mjs +251 -0
- package/components/baseSvg.cjs +1 -0
- package/components/baseSvg.d.ts +5 -0
- package/components/baseSvg.mjs +21 -0
- package/components/button.cjs +1 -0
- package/components/button.d.ts +19 -0
- package/components/button.mjs +19 -0
- package/components/chart.cjs +1 -0
- package/components/chart.d.ts +83 -0
- package/components/chart.mjs +236 -0
- package/components/checkbox.cjs +1 -0
- package/components/checkbox.d.ts +36 -0
- package/components/checkbox.mjs +38 -0
- 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/dataGridBody.d.ts +7 -0
- package/components/dataGrid/components/dataGridBottomBar.d.ts +6 -0
- package/components/dataGrid/components/dataGridCell.d.ts +16 -0
- package/components/dataGrid/components/dataGridCellEditor.d.ts +15 -0
- package/components/dataGrid/components/dataGridCellPlaceholder.d.ts +14 -0
- package/components/dataGrid/components/dataGridCellRowDetail.d.ts +6 -0
- package/components/dataGrid/components/dataGridCellRowSelection.d.ts +6 -0
- package/components/dataGrid/components/dataGridCellText.d.ts +6 -0
- package/components/dataGrid/components/dataGridCellTree.d.ts +16 -0
- package/components/dataGrid/components/dataGridColumnFilter.d.ts +9 -0
- package/components/dataGrid/components/dataGridColumnGroups.d.ts +7 -0
- package/components/dataGrid/components/dataGridContent.d.ts +6 -0
- package/components/dataGrid/components/dataGridDataSourceError.d.ts +12 -0
- package/components/dataGrid/components/dataGridDetailRow.d.ts +8 -0
- package/components/dataGrid/components/dataGridEmptyColumns.d.ts +6 -0
- package/components/dataGrid/components/dataGridExport.d.ts +10 -0
- package/components/dataGrid/components/dataGridFilterCell.d.ts +11 -0
- package/components/dataGrid/components/dataGridFilterRow.d.ts +6 -0
- package/components/dataGrid/components/dataGridFooter.d.ts +11 -0
- package/components/dataGrid/components/dataGridGlobalFilter.d.ts +6 -0
- package/components/dataGrid/components/dataGridGroupRow.d.ts +8 -0
- package/components/dataGrid/components/dataGridHeader.d.ts +6 -0
- package/components/dataGrid/components/dataGridHeaderCell.d.ts +9 -0
- package/components/dataGrid/components/dataGridHeaderCellContextMenu.d.ts +11 -0
- package/components/dataGrid/components/dataGridHeaderCellResizer.d.ts +14 -0
- package/components/dataGrid/components/dataGridLoader.d.ts +11 -0
- package/components/dataGrid/components/dataGridPagination.d.ts +6 -0
- package/components/dataGrid/components/dataGridRow.d.ts +8 -0
- package/components/dataGrid/components/dataGridTopBar.d.ts +6 -0
- package/components/dataGrid/components/dataGridTopBarContextMenu.d.ts +6 -0
- package/components/dataGrid/contracts/dataGridContract.d.ts +671 -0
- package/components/dataGrid/export/gridExport.d.ts +10 -0
- package/components/dataGrid/gridNavigationContext.d.ts +32 -0
- 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 +44 -0
- package/components/dataGrid/models/columnModel.d.ts +160 -0
- package/components/dataGrid/models/columnVisibilityModel.d.ts +24 -0
- package/components/dataGrid/models/dataSourceModel.d.ts +177 -0
- package/components/dataGrid/models/detailRowModel.d.ts +22 -0
- package/components/dataGrid/models/editModel.d.ts +77 -0
- package/components/dataGrid/models/exportModel.d.ts +51 -0
- package/components/dataGrid/models/filterModel.d.ts +22 -0
- package/components/dataGrid/models/gridModel.d.ts +293 -0
- package/components/dataGrid/models/groupRowCellModel.d.ts +34 -0
- package/components/dataGrid/models/groupRowModel.d.ts +68 -0
- package/components/dataGrid/models/headerCellModel.d.ts +83 -0
- package/components/dataGrid/models/paginationModel.d.ts +32 -0
- package/components/dataGrid/models/rowModel.d.ts +46 -0
- 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/models/viewportModel.d.ts +44 -0
- package/components/dataGrid/useGrid.d.ts +8 -0
- package/components/dataGrid/useGridNavigation.d.ts +21 -0
- package/components/dataGrid.cjs +1 -0
- package/components/dataGrid.d.ts +4 -0
- package/components/dataGrid.mjs +4907 -0
- package/components/dialog.cjs +1 -0
- package/components/dialog.d.ts +151 -0
- package/components/dialog.mjs +223 -0
- package/components/dropdown/dropdownContext.d.ts +44 -0
- package/components/dropdown/dropdownItems.d.ts +22 -0
- package/components/dropdown/dropdownRowRenderer.d.ts +8 -0
- package/components/dropdown/dropdownSearch.d.ts +21 -0
- package/components/dropdown/utils.d.ts +1 -0
- package/components/dropdown.cjs +1 -0
- package/components/dropdown.d.ts +77 -0
- package/components/dropdown.mjs +491 -0
- package/components/flex.cjs +1 -0
- package/components/flex.d.ts +6 -0
- package/components/flex.mjs +16 -0
- package/components/form.cjs +1 -0
- package/components/form.d.ts +10 -0
- package/components/form.mjs +58 -0
- package/components/grid.cjs +1 -0
- package/components/grid.d.ts +6 -0
- package/components/grid.mjs +16 -0
- package/components/icon.cjs +1 -0
- package/components/icon.d.ts +37 -0
- package/components/icon.mjs +41 -0
- package/components/menu.cjs +1 -0
- package/components/menu.d.ts +234 -0
- package/components/menu.mjs +422 -0
- package/components/overlay.cjs +1 -0
- package/components/overlay.d.ts +47 -0
- package/components/overlay.mjs +52 -0
- package/components/popover.cjs +1 -0
- package/components/popover.d.ts +100 -0
- package/components/popover.mjs +69 -0
- package/components/presence.cjs +1 -0
- package/components/presence.d.ts +36 -0
- package/components/presence.mjs +11 -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 -0
- package/components/radioButton.d.ts +32 -0
- package/components/radioButton.mjs +32 -0
- package/components/radioGroup.cjs +1 -0
- package/components/radioGroup.d.ts +51 -0
- package/components/radioGroup.mjs +83 -0
- package/components/select.cjs +1 -0
- package/components/select.d.ts +51 -0
- package/components/select.mjs +36 -0
- package/components/semantics.cjs +1 -0
- package/components/semantics.d.ts +39 -0
- package/components/semantics.mjs +17 -0
- package/components/slider.cjs +1 -0
- package/components/slider.d.ts +98 -0
- package/components/slider.mjs +250 -0
- package/components/svg.cjs +1 -0
- package/components/svg.d.ts +65 -0
- package/components/svg.mjs +141 -0
- package/components/switch.cjs +1 -0
- package/components/switch.d.ts +29 -0
- package/components/switch.mjs +25 -0
- package/components/tabs.cjs +1 -0
- package/components/tabs.d.ts +87 -0
- package/components/tabs.mjs +324 -0
- package/components/textarea.cjs +1 -0
- package/components/textarea.d.ts +41 -0
- package/components/textarea.mjs +33 -0
- package/components/textbox.cjs +1 -0
- package/components/textbox.d.ts +38 -0
- package/components/textbox.mjs +33 -0
- package/components/toaster.cjs +1 -0
- package/components/toaster.d.ts +106 -0
- package/components/toaster.mjs +295 -0
- package/components/tooltip.cjs +1 -0
- package/components/tooltip.d.ts +98 -0
- package/components/tooltip.mjs +110 -0
- package/components/visuallyHidden.cjs +1 -0
- package/components/visuallyHidden.d.ts +6 -0
- package/components/visuallyHidden.mjs +23 -0
- 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 -0
- package/effects.mjs +6 -0
- package/forms.cjs +1 -0
- package/forms.mjs +21 -0
- package/gridExport.cjs +2 -0
- package/gridExport.mjs +244 -0
- package/icons/dotsIcon.d.ts +2 -0
- package/icons/expandIcon.d.ts +2 -0
- package/icons/groupingIcon.d.ts +2 -0
- package/icons/pinIcon.d.ts +2 -0
- package/icons/searchIcon.d.ts +2 -0
- package/icons/sortIcon.d.ts +2 -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 -0
- package/motion.mjs +66 -0
- package/package.json +327 -7
- package/placement.cjs +1 -0
- package/placement.mjs +180 -0
- package/platform.cjs +1 -0
- package/platform.mjs +45 -0
- package/react/a11y/callbacks.d.ts +12 -0
- package/react/a11y/useControllableState.d.ts +25 -0
- package/react/a11y/useDismiss.d.ts +25 -0
- package/react/a11y/useFocusReturn.d.ts +18 -0
- package/react/a11y/useRovingFocus.d.ts +84 -0
- package/react/anchor/useAnchorPosition.d.ts +79 -0
- package/react/animation/usePresence.d.ts +32 -0
- package/react/boxClassNames.d.ts +25 -0
- package/react/boxProps.d.ts +49 -0
- package/react/boxTagProps.d.ts +7 -0
- package/react/clientRuntime.d.ts +11 -0
- package/react/dialog/useScrollLock.d.ts +12 -0
- package/react/effects.d.ts +10 -0
- package/react/forms/labelledControl.d.ts +17 -0
- package/react/forms/placeholderProp.d.ts +13 -0
- package/react/hooks/usePortalContainer.d.ts +6 -0
- package/react/hooks/useVirtualization.d.ts +32 -0
- package/react/hooks/useVisibility.d.ts +9 -0
- package/react/identity/useIdentifier.d.ts +17 -0
- package/react/popover/usePopoverLayer.d.ts +87 -0
- package/react/reactTypes.d.ts +10 -0
- package/react/resolveStyles.d.ts +18 -0
- package/react/styleElements.d.ts +3 -0
- package/react/svg/attributesInProps.d.ts +4 -0
- package/react/svg/svgNaming.d.ts +15 -0
- package/react/theme/theme.d.ts +26 -0
- package/react/theme/themeContext.d.ts +7 -0
- package/react/useStyles.d.ts +22 -0
- package/react-shared.cjs +1 -0
- package/react-shared.mjs +122 -0
- package/rolldown-runtime.cjs +1 -0
- package/rsc.cjs +1 -0
- package/rsc.d.ts +27 -0
- package/rsc.mjs +18 -0
- package/ssg.cjs +1 -0
- package/ssg.d.ts +24 -0
- package/ssg.mjs +25 -0
- package/utils/accordion/accordionUtils.d.ts +48 -0
- package/utils/anchor/anchorUtils.d.ts +79 -0
- package/utils/animation/animationUtils.d.ts +29 -0
- package/utils/array/arrayUtils.d.ts +15 -0
- package/utils/array/lazyList.d.ts +10 -0
- package/utils/box/boxUtils.d.ts +5 -0
- package/utils/chart/chartPalette.d.ts +15 -0
- package/utils/chart/chartUtils.d.ts +92 -0
- package/utils/combobox/comboboxUtils.d.ts +89 -0
- package/utils/dom/domUtils.d.ts +32 -0
- package/utils/environment/environmentUtils.d.ts +28 -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/fn/fnUtils.d.ts +4 -0
- package/utils/form/formUtils.d.ts +8 -0
- package/utils/keyboard/typeaheadUtils.d.ts +31 -0
- package/utils/memo.d.ts +10 -0
- package/utils/menu/menuUtils.d.ts +25 -0
- package/utils/object/objectUtils.d.ts +13 -0
- package/utils/slider/sliderUtils.d.ts +67 -0
- package/utils/string/fuzzySearch.d.ts +27 -0
- package/utils/string/stringUtils.d.ts +4 -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/docs/a11y.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# The behaviour primitives (`@box-kite/react/a11y`)
|
|
2
|
+
|
|
3
|
+
Five hooks and one component: the mechanics every accessible widget needs and none of them can be
|
|
4
|
+
seen in the markup. They are what the library's own components are built from, and they are
|
|
5
|
+
published so a consumer building a pattern this library does not ship — a tree view, a split
|
|
6
|
+
button, a command palette — does not have to write them again.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { useControllableState, useDismiss, useFocusReturn, useIdentifier, useRovingFocus } from '@box-kite/react/a11y';
|
|
10
|
+
import VisuallyHidden from '@box-kite/react/components/visuallyHidden';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2.2 KB gzipped for the whole entry, and it pulls in no styling engine: the hooks reach React, two
|
|
14
|
+
leaf modules of DOM helpers, and each other. They are client hooks — the entry carries a `'use client'` banner, so importing it from a
|
|
15
|
+
Server Component opens a client boundary. `VisuallyHidden` is a component and ships separately,
|
|
16
|
+
where it can still render on a server.
|
|
17
|
+
|
|
18
|
+
What they deliberately do **not** do is supply roles or ARIA. A listbox and a menu navigate
|
|
19
|
+
identically and are named completely differently; a hook that decided both would be wrong for one
|
|
20
|
+
of them. Movement, state and focus are here — the semantics belong to whoever is building the
|
|
21
|
+
pattern.
|
|
22
|
+
|
|
23
|
+
`Tooltip` is the first component assembled from them, and a small enough one to read as a worked
|
|
24
|
+
example: `src/components/tooltip.tsx` is `useControllableState` for the open state (so a consumer
|
|
25
|
+
can control it and be told _why_ it changed), `useIdentifier` for the id `aria-describedby` points
|
|
26
|
+
at, and `useDismiss` with `outsidePointer: false` for Escape. Everything left over — the delays,
|
|
27
|
+
the hover grace period, `role="tooltip"` — is the pattern, and that is the split this file is
|
|
28
|
+
about. It uses no `useFocusReturn`: focus never enters a tooltip, so there is nothing to return.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## `useControllableState`
|
|
33
|
+
|
|
34
|
+
One value that behaves the same whether the consumer controls it or not, and every change carrying
|
|
35
|
+
a **reason**.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
const [open, setOpen] = useControllableState<boolean, 'trigger' | 'select' | 'escape'>({
|
|
39
|
+
value: props.open, // present = the consumer owns it
|
|
40
|
+
defaultValue: false,
|
|
41
|
+
onChange: props.onOpenChange,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
setOpen(false, { reason: 'escape', event });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`onChange(value, { reason, event })` is the point of the hook. A component that only reports
|
|
48
|
+
`onOpenChange(false)` makes its consumer guess whether the popup closed because something was
|
|
49
|
+
picked, because Escape was pressed, because the user clicked away, or because the component decided
|
|
50
|
+
on its own — and each of those wants different behaviour from the surrounding app. This is the
|
|
51
|
+
Base UI shape, and it is the answer to the long-standing complaints about controlled state in
|
|
52
|
+
Radix.
|
|
53
|
+
|
|
54
|
+
Details worth knowing:
|
|
55
|
+
|
|
56
|
+
- The setter's identity is **stable**, so it can sit in a dependency array.
|
|
57
|
+
- It takes an updater (`setOpen((value) => !value, details)`), resolved against the current value.
|
|
58
|
+
- A change to the value it already holds is dropped: two dismissal layers closing the same popup is
|
|
59
|
+
normal, and the consumer should hear about it once.
|
|
60
|
+
- "Controlled" means `value !== undefined`, decided per render.
|
|
61
|
+
|
|
62
|
+
## `useDismiss`
|
|
63
|
+
|
|
64
|
+
Light dismiss: Escape, and a pointer press outside — composable across nested layers.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
useDismiss({
|
|
68
|
+
enabled: open,
|
|
69
|
+
inside: [triggerRef, popupRef],
|
|
70
|
+
onDismiss: (reason, event) => setOpen(false, { reason, event }),
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `inside` is what counts as inside. Pass the **trigger** as well as the popup, or a press on the
|
|
75
|
+
trigger of an open popup reads as a press outside it — dismissing and reopening in one gesture.
|
|
76
|
+
- Escape goes to the innermost layer only, worked out from DOM containment rather than registration
|
|
77
|
+
order (React runs a child's effects before its parent's, so registration order gets nesting
|
|
78
|
+
backwards). An outside press is judged by each layer for itself, so a press in the page closes a
|
|
79
|
+
menu and its submenu together, while a press in the menu closes only the submenu.
|
|
80
|
+
- An Escape a consumer already handled (`event.preventDefault()`) is left alone — that is how a
|
|
81
|
+
search box inside the popup clears itself without also closing the popup.
|
|
82
|
+
- It listens on `pointerdown`, not `click`: dismissal belongs at the start of the gesture, and a
|
|
83
|
+
press that starts inside the popup and ends outside it (a drag, a text selection) is not a
|
|
84
|
+
dismissal at all.
|
|
85
|
+
- `escapeKey: false` / `outsidePointer: false` turn off either half.
|
|
86
|
+
|
|
87
|
+
## `useFocusReturn`
|
|
88
|
+
|
|
89
|
+
Put focus back where it came from when a layer closes.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
useFocusReturn({ enabled: open, returnTo: triggerRef });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Losing focus to `<body>` is the most common keyboard bug in a popup: the trigger is gone as far as
|
|
96
|
+
the tab order is concerned, so the next Tab starts again at the top of the page.
|
|
97
|
+
|
|
98
|
+
- The invoker is captured **during the render that opens the layer**, before the layer's own
|
|
99
|
+
`autoFocus` can take focus away — an effect would remember the popup's first control instead.
|
|
100
|
+
- The restore is skipped when something else already holds focus (a menu item that opened a dialog,
|
|
101
|
+
a user who tabbed away first), because taking it back would be the same bug pointing the other
|
|
102
|
+
way.
|
|
103
|
+
- `returnTo` overrides the invoker; `preventScroll` is passed to `focus()`.
|
|
104
|
+
- It also returns `{ returnFocus }` for the cases where the component wants to say when.
|
|
105
|
+
|
|
106
|
+
## `useRovingFocus`
|
|
107
|
+
|
|
108
|
+
Arrow-key navigation over a list — the movement half of every APG list pattern (listbox, menu,
|
|
109
|
+
tabs, radio group) — with both of the focus strategies those patterns use.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const roving = useRovingFocus({
|
|
113
|
+
count: items.length,
|
|
114
|
+
orientation: 'vertical', // 'horizontal' | 'both'
|
|
115
|
+
loop: true,
|
|
116
|
+
isDisabled: (index) => items[index].disabled,
|
|
117
|
+
textOf: (index) => items[index].label, // supplying this turns on typeahead
|
|
118
|
+
onSelect: (index, event) => choose(index),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
<ul onKeyDown={roving.onKeyDown}>
|
|
122
|
+
{items.map((item, index) => (
|
|
123
|
+
<li key={item.id} role="option" aria-selected={index === roving.activeIndex} {...roving.itemProps(index)}>
|
|
124
|
+
{item.label}
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**A sideways arrow follows the reading order.** APG asks for it and a right-to-left list reverses it, so
|
|
131
|
+
`ArrowLeft` is the _next_ item there and `ArrowRight` the previous one. Nothing configures it: the hook
|
|
132
|
+
asks the element for its _resolved_ direction (`getComputedStyle`, so a `dir="auto"` or a `<bdi>` above it
|
|
133
|
+
counts) the moment one of those two keys arrives, which is why a vertical list pays nothing for it. The
|
|
134
|
+
vertical axis, Tab, Home and End never flip — Home is the first item in the reading order either way.
|
|
135
|
+
|
|
136
|
+
Keys: arrows per `orientation`, Home/End (skipping disabled items), Enter and Space to select,
|
|
137
|
+
printable characters for typeahead. A single character — or the same character repeated — moves to
|
|
138
|
+
the next item starting with it; a longer buffer narrows instead of hopping. Space belongs to an
|
|
139
|
+
open typeahead buffer rather than to selection, because "New York" contains one.
|
|
140
|
+
|
|
141
|
+
**Two focus modes.** The default (`focusItems: true`) is roving tabindex: real DOM focus follows
|
|
142
|
+
the active item and `itemProps` puts exactly one item in the tab order, so Tab enters and leaves the
|
|
143
|
+
list in one press. `focusItems: false` is the other half — focus stays where it is (a combobox
|
|
144
|
+
trigger) and the caller names the active item with `aria-activedescendant`, for which
|
|
145
|
+
`roving.activeItem()` hands back the element.
|
|
146
|
+
|
|
147
|
+
`activeIndex` is clamped to `count`, so a list filtered down by a search box cannot leave it
|
|
148
|
+
pointing past the end. It is controllable through `activeIndex`/`onActiveIndexChange`, and
|
|
149
|
+
`setActiveIndex(index, details)` sets it outright — opening a list on the selected item.
|
|
150
|
+
|
|
151
|
+
## `useIdentifier`
|
|
152
|
+
|
|
153
|
+
A stable, unique id for wiring one element to another.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
const id = useIdentifier('select');
|
|
157
|
+
// `${id}-trigger`, `${id}-listbox`, `${id}-option-3`
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
React's `useId` with the punctuation removed: `:r1:` (React 18) and `«r1»` (React 19) are both
|
|
161
|
+
rejected by `querySelector('#…')`, and the part that varies is the word characters. Derive related
|
|
162
|
+
ids from one call rather than calling the hook once per element. On React 16/17, which have no
|
|
163
|
+
`useId`, the fallback is a module counter — pass ids in explicitly there if the tree is hydrated.
|
|
164
|
+
|
|
165
|
+
## `VisuallyHidden`
|
|
166
|
+
|
|
167
|
+
Content for screen readers that is not drawn on screen — the name of an icon-only button, the
|
|
168
|
+
heading a region is labelled by, a live region's announcement.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import VisuallyHidden from '@box-kite/react/components/visuallyHidden';
|
|
172
|
+
|
|
173
|
+
<Button>
|
|
174
|
+
<VisuallyHidden tag="span">Delete the invoice</VisuallyHidden>
|
|
175
|
+
<TrashIcon aria-hidden />
|
|
176
|
+
</Button>;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`display: none` and `hidden` take content out of the accessibility tree along with the layout,
|
|
180
|
+
which is the opposite of what is wanted. This clips the element instead: absolutely positioned, one
|
|
181
|
+
pixel (not zero — some screen readers skip a zero-sized element), `overflow: hidden`,
|
|
182
|
+
`clip-path: inset(50%)` and `white-space: nowrap`, so a long string is not wrapped into a
|
|
183
|
+
one-pixel-wide column and read out one word per line. It is a Box, so props override the defaults
|
|
184
|
+
and `tag` picks the element.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Testing them
|
|
189
|
+
|
|
190
|
+
Each hook has its own unit tests next to it in `src/react/a11y/`. What those cannot prove is that
|
|
191
|
+
they compose, so `src/react/a11y/primitives.a11y.test.tsx` assembles all five into a select-only
|
|
192
|
+
combobox, drives it entirely from the keyboard, and runs axe over it — the pattern A5 has to reach,
|
|
193
|
+
built the way a consumer would build it. The bugs it is there to catch are the ones that only
|
|
194
|
+
appear in combination: a dismissal firing while the list is still taking focus, focus returning to
|
|
195
|
+
a trigger a roving index has moved away from, an id regenerated per render so
|
|
196
|
+
`aria-activedescendant` points at nothing.
|
|
197
|
+
|
|
198
|
+
See [a11y-testing.md](a11y-testing.md) for the test infrastructure itself and what automated checks
|
|
199
|
+
can and cannot see.
|
package/docs/anchor.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Anchor positioning (`@box-kite/react/anchor`)
|
|
2
|
+
|
|
3
|
+
Where a floating layer goes. One hook and two value families, over the six props the engine already
|
|
4
|
+
ships (`anchorName`, `positionAnchor`, `positionArea`, `positionTryFallbacks`, `positionTryOrder`,
|
|
5
|
+
`positionVisibility`).
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useAnchorPosition } from '@box-kite/react/anchor';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
About 2 KB gzipped for the entry, and it pulls in no styling engine. It is a client hook — the entry
|
|
12
|
+
carries a `'use client'` banner — but the thing it produces is ordinary props, so a layer that needs
|
|
13
|
+
no JavaScript at all can be written by hand with the props below and rendered on a server.
|
|
14
|
+
|
|
15
|
+
What it deliberately does **not** do is supply a role, a dismissal or focus handling. A layer is not
|
|
16
|
+
a pattern: a tooltip, a menu and a select popup are placed identically and named completely
|
|
17
|
+
differently. Placement is here; the semantics belong to whoever is building the pattern, and the
|
|
18
|
+
mechanics are in `@box-kite/react/a11y`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## `useAnchorPosition(options)`
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true });
|
|
26
|
+
|
|
27
|
+
<Button {...anchorProps} onClick={toggle}>
|
|
28
|
+
Options
|
|
29
|
+
</Button>;
|
|
30
|
+
{
|
|
31
|
+
isOpen && <Box {...layerProps} component="menu">…</Box>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Spread `anchorProps` on the trigger and `layerProps` on the layer. On a browser with CSS anchor
|
|
36
|
+
positioning that is the whole of it: nothing runs. No measuring, no scroll listener, no state,
|
|
37
|
+
nothing to keep in sync when the page moves — `position-area` and `position-try-fallbacks` are the
|
|
38
|
+
placement and the flip a positioning library ships JavaScript for.
|
|
39
|
+
|
|
40
|
+
| Option | Default | What it means |
|
|
41
|
+
| ------------ | --------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
42
|
+
| `side` | `'bottom'` | `'top'`/`'bottom'` are the block axis, `'start'`/`'end'` the inline one — so a side mirrors in a right-to-left page |
|
|
43
|
+
| `align` | `'center'` | which of the anchor's edges to line the layer up with on the other axis |
|
|
44
|
+
| `offset` | `0` | the gap, on the ÷4 spacing scale, emitted as the margin on the side facing the anchor |
|
|
45
|
+
| `flip` | `true` | `positionTryFallbacks`: the side's own axis, then the alignment's, then both |
|
|
46
|
+
| `matchWidth` | `false` | `minWidth="anchor-size(width)"` — the anchor's width, with nothing measured |
|
|
47
|
+
| `name` | generated | the anchor's name; one per instance unless you pass one |
|
|
48
|
+
| `anchor` | — | an anchor handed to the hook rather than one it spreads props onto (a ref or an element) |
|
|
49
|
+
| `trackSide` | `false` | whether `side` in the result says where the layer ended up — one read after layout, so it is opt-in |
|
|
50
|
+
|
|
51
|
+
`css` says which path ran: `true` is the browser placing the layer, `false` is the measured
|
|
52
|
+
fallback. `side` is where the layer actually is: the requested side, unless `trackSide` is on and a
|
|
53
|
+
flip moved it. There is no `strategy` option — the layer is always `position: fixed`, because that is
|
|
54
|
+
what makes both paths agree about what the coordinates mean.
|
|
55
|
+
|
|
56
|
+
A caller that was handed its trigger rather than rendering it passes `anchor` instead of spreading
|
|
57
|
+
`anchorProps`: the name is written onto the element in a layout effect, which is the one thing an
|
|
58
|
+
element somebody else rendered can be reached by. That is what `Overlay` does, and a ref is read when
|
|
59
|
+
the layer mounts — so an element in state is the robust form when the layer is open from the start.
|
|
60
|
+
|
|
61
|
+
### The fallback
|
|
62
|
+
|
|
63
|
+
CSS anchor positioning is Chrome 125+, Firefox 147+ and Safari 26+. Everywhere else the hook
|
|
64
|
+
measures: it flips to the opposite side when the requested one has no room and the opposite has,
|
|
65
|
+
then shifts along the other axis to keep the layer in the viewport, and hands the layer its
|
|
66
|
+
coordinates as an inline style. Both paths come out of one model, so the fallback is the CSS
|
|
67
|
+
placement worked out by hand rather than a second set of rules.
|
|
68
|
+
|
|
69
|
+
The first render always assumes the CSS path, so a server-rendered anchor and the first client
|
|
70
|
+
render agree; a browser without anchor positioning says so in a layout effect, before it paints.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## `anchor-size()` and `anchor()`
|
|
75
|
+
|
|
76
|
+
Every sizing prop (`width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`) takes an
|
|
77
|
+
`anchor-size()` value, and every single-side inset prop (`top`, `right`, `bottom`, `left`,
|
|
78
|
+
`insetStart`, `insetEnd`) takes an `anchor()` one:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<Box position="fixed" positionAnchor="menu-trigger" minWidth="anchor-size(width)" maxHeight="anchor-size(height, 20rem)" />
|
|
82
|
+
<Box position="fixed" top="anchor(bottom)" insetStart="anchor(left)" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- `anchor-size()` takes an axis: `width`, `height`, `block`, `inline`, `self-block`, `self-inline`.
|
|
86
|
+
- `anchor()` takes an edge: `top`, `right`, `bottom`, `left`, `start`, `end`, `self-start`,
|
|
87
|
+
`self-end`, `center`, `inside`, `outside` — or a percentage along one (`anchor(50%)`).
|
|
88
|
+
- Both take an optional anchor name first, with the same optional `--` the props take
|
|
89
|
+
(`anchor-size(trigger width)`), and an optional fallback after a comma for when there is no anchor
|
|
90
|
+
to measure (`anchor-size(width, 10rem)`).
|
|
91
|
+
- A bare `anchor()` is rejected — CSS needs the edge. A bare `anchor-size()` is not: it means the
|
|
92
|
+
layer's own axis.
|
|
93
|
+
- A value the grammar rejects emits no rule and no class name, the way every validated value does.
|
|
94
|
+
|
|
95
|
+
Together they are what places and sizes a layer against its anchor with no hook at all, which is
|
|
96
|
+
what makes a floating layer possible in a Server Component.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Five things worth knowing
|
|
101
|
+
|
|
102
|
+
All five were measured in Chrome 152 rather than read off the spec.
|
|
103
|
+
|
|
104
|
+
**The layer is `position: fixed`, so it escapes `overflow: hidden` without a portal.** Every clipped
|
|
105
|
+
ancestor, no portal, no `z-index` juggling to get out of one — but *not* a transformed ancestor,
|
|
106
|
+
which is a fixed element's containing block in either path, and not the page's stacking order, which
|
|
107
|
+
is still the page's. `Overlay` (`@box-kite/react/components/overlay`) is this hook plus a portal, and
|
|
108
|
+
is still the answer when the layer has to come out on top of everything.
|
|
109
|
+
|
|
110
|
+
**A flip needs three candidates, not one.** A candidate position has to fit on *both* axes to be
|
|
111
|
+
taken, so a lone `flip-block` does nothing at all for a layer that overflows the *cross* axis — the
|
|
112
|
+
browser leaves it pressed against the edge of the viewport. `flip` therefore offers the side's own
|
|
113
|
+
axis, the alignment's, and both, in that order, and the both-axes rule is what makes the order work:
|
|
114
|
+
a menu with no room on the side its alignment reaches mirrors to the other end instead of running off
|
|
115
|
+
the page.
|
|
116
|
+
|
|
117
|
+
**Knowing which side the browser chose costs the entrance.** `trackSide` reads the *used*
|
|
118
|
+
`position-area`, and that read is the style resolution `@starting-style` computes its before-change
|
|
119
|
+
style from — so a class that depends on the answer arrives after the entrance has been decided, every
|
|
120
|
+
time. An exit runs long afterwards and can use it, which is why `dropdown.items` has a `closedUp`
|
|
121
|
+
variant and no `up` one. A used value is also not the value that went in: a `span-all` half is
|
|
122
|
+
dropped, and a value naming both axes comes back in the `start`/`end` shorthand, where position names
|
|
123
|
+
the axis (`block-end span-inline-end` reads back `end span-end`, and `start span-end` once flipped).
|
|
124
|
+
|
|
125
|
+
**A flip is sticky.** Once the browser takes a `position-try` fallback it keeps it until the layer is
|
|
126
|
+
laid out afresh, which is what stops the layer oscillating as the page scrolls. Hiding the layer and
|
|
127
|
+
showing it again re-evaluates, so a popup that mounts when it opens always picks the side that fits,
|
|
128
|
+
while one that stays mounted keeps the side it first chose.
|
|
129
|
+
|
|
130
|
+
**The anchor's name is an inline style rather than a prop.** An identity is per instance, so a class
|
|
131
|
+
for it would be a rule per instance that is never freed — the same rule that makes a sparkline's `d`
|
|
132
|
+
an attribute and its stroke a class. Everything shared — the area, the flip, the margin, the width —
|
|
133
|
+
is an ordinary prop, and an ordinary class.
|
|
134
|
+
|
|
135
|
+
One more, from the props themselves: **a candidate position has to fit on both axes to be taken**, so
|
|
136
|
+
an overflow the flips cannot fix disqualifies every one of them and the layer silently stays put.
|
|
137
|
+
The hook cannot hit this — `align: 'center'` emits `span-all` rather than the centre cell for exactly
|
|
138
|
+
that reason — but hand-written props can: `positionArea="block-end center"` with a layer wider than
|
|
139
|
+
its anchor never flips.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
`Box` is the default export of `@box-kite/react` and takes every prop in `props.md`. Everything below is `Box` with the right element and its own defaults, one entry point each — **use one of these rather than `<Box tag="…">`**. Read from the built chunks, so the names cannot drift from what the package exports.
|
|
4
|
+
|
|
5
|
+
## Rendered on a server
|
|
6
|
+
|
|
7
|
+
No hook, no effect, no DOM: a React Server Component can render these directly.
|
|
8
|
+
|
|
9
|
+
| Import | Components | Notes |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `@box-kite/react/components/baseSvg` | `BaseSvg` (default) | **deprecated** |
|
|
12
|
+
| `@box-kite/react/components/button` | `Button` (default) | |
|
|
13
|
+
| `@box-kite/react/components/chart` | `ChartContainer`, `Gauge`, `MiniDonut`, `ProgressRing`, `Sparkline` | |
|
|
14
|
+
| `@box-kite/react/components/flex` | `Flex` (default) | |
|
|
15
|
+
| `@box-kite/react/components/grid` | `Grid` (default) | |
|
|
16
|
+
| `@box-kite/react/components/icon` | `Icon` (default) | |
|
|
17
|
+
| `@box-kite/react/components/progress` | `Progress` (default) | |
|
|
18
|
+
| `@box-kite/react/components/radioButton` | `RadioButton` (default) | |
|
|
19
|
+
| `@box-kite/react/components/semantics` | `Article`, `Aside`, `Details`, `Figcaption`, `Figure`, `Footer`, `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `Header`, `Img`, `Label`, `Li`, `Link`, `Main`, `Mark`, `Menu`, `MenuList`, `Nav`, `Ol`, `P`, `Section`, `Span`, `Summary`, `Time`, `Ul` | **deprecated** |
|
|
20
|
+
| `@box-kite/react/components/svg` | `Circle`, `ClipPath`, `Defs`, `Ellipse`, `G`, `Line`, `LinearGradient`, `Marker`, `Mask`, `Path`, `Polygon`, `Polyline`, `RadialGradient`, `Rect`, `Stop`, `Svg`, `SvgSymbol`, `SvgText`, `TSpan`, `Use` | |
|
|
21
|
+
| `@box-kite/react/components/textarea` | `Textarea` (default) | |
|
|
22
|
+
| `@box-kite/react/components/textbox` | `Textbox` (default) | |
|
|
23
|
+
| `@box-kite/react/components/visuallyHidden` | `VisuallyHidden` (default) | |
|
|
24
|
+
|
|
25
|
+
## Client-only (`'use client'`)
|
|
26
|
+
|
|
27
|
+
These hold state, measure the DOM or portal into it. A Server Component may import one — the chunk carries the banner, so the boundary opens — but the component itself renders on the client.
|
|
28
|
+
|
|
29
|
+
| Import | Components | Notes |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `@box-kite/react/components/accordion` | `Accordion` (default), `Collapsible` | |
|
|
32
|
+
| `@box-kite/react/components/checkbox` | `Checkbox` (default) | |
|
|
33
|
+
| `@box-kite/react/components/combobox` | `Combobox` (default), `ComboboxUtils` | |
|
|
34
|
+
| `@box-kite/react/components/dataGrid` | `dataGrid` (default) | |
|
|
35
|
+
| `@box-kite/react/components/dialog` | `Dialog` (default), `AlertDialog` | |
|
|
36
|
+
| `@box-kite/react/components/dropdown` | `Dropdown` (default) | **deprecated** |
|
|
37
|
+
| `@box-kite/react/components/form` | `Form` (default) | |
|
|
38
|
+
| `@box-kite/react/components/menu` | `Menu` (default) | |
|
|
39
|
+
| `@box-kite/react/components/overlay` | `Overlay` (default) | |
|
|
40
|
+
| `@box-kite/react/components/popover` | `Popover` (default) | |
|
|
41
|
+
| `@box-kite/react/components/presence` | `Presence` (default) | |
|
|
42
|
+
| `@box-kite/react/components/radioGroup` | `RadioGroup` (default) | **deprecated** |
|
|
43
|
+
| `@box-kite/react/components/select` | `Select` (default) | **deprecated** |
|
|
44
|
+
| `@box-kite/react/components/slider` | `Slider` (default) | |
|
|
45
|
+
| `@box-kite/react/components/switch` | `Switch` (default) | |
|
|
46
|
+
| `@box-kite/react/components/tabs` | `Tabs` (default) | |
|
|
47
|
+
| `@box-kite/react/components/toaster` | `Toaster` (default), `createToastStore`, `defaultToastStore`, `toast` | |
|
|
48
|
+
| `@box-kite/react/components/tooltip` | `Tooltip` (default) | |
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Documentation for agents
|
|
2
|
+
|
|
3
|
+
The markdown `@box-kite/react` ships for whatever is reading its source tree. No formal discovery standard exists for a package that carries its own docs — these files are here to be found.
|
|
4
|
+
|
|
5
|
+
- [`../AGENTS.md`](../AGENTS.md) — the rules, and the fight-the-prior block. Start here
|
|
6
|
+
- [`props.md`](props.md) — every prop, the CSS it writes and one measured example
|
|
7
|
+
- [`components.md`](components.md) — every component, its import specifier and whether it renders on a server
|
|
8
|
+
- [`a11y.md`](a11y.md) — the behaviour hooks (`@box-kite/react/a11y`) for a pattern this library does not ship
|
|
9
|
+
- [`anchor.md`](anchor.md) — where a floating layer goes (`@box-kite/react/anchor`), in CSS and in the measured fallback
|
|
10
|
+
|
|
11
|
+
The rest sits at the package root: `BOX_KITE_AI_CONTEXT.md`, the long-form reference (prop tables, the DataGrid API, debugging); `.claude/skills/box-kite/`, the same rules as an installable skill with four references beside it; and `.cursor/rules/box-kite.mdc`, which is that skill as a Cursor rule — copy either into the project rather than reading it from here, so it is loaded without anyone asking.
|