LayoutReact
A group of items that is one tab stop, where the arrow keys move focus between the items (a roving tab stop).
Rendered by React 19.3.1
Loading React…Installation
npm i @hamur/reactNot on npm yet. The package goes out under the chosen name.
Anatomy
Import the component and assemble its parts:
<Composite.Root>
<Composite.Item />
</Composite.Root>Examples
Listbox
A vertical listbox that loops, with typeahead; an option is selected on a keyboard move or a click.
Grid
A 3 by 3 alignment picker with cols set to 3 and loopFocus off; the tab stop starts in the centre and is the chosen cell.
Controlled
A radio group whose highlightedIndex is the app's state, showing the active index and the reason; a keyboard move checks.
API reference
20 props across 2 parts, in React syntax. className, style, render are listed once, under Shared props.
ItemComposite.Item
An item of the composite: the one that holds the tab stop has tabindex="0", the others -1. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
disabledWhether the item is disabled: it gets aria-disabled and stays in the tab order unless | boolean | false |
focusableWhenDisabledWhether the item remains focusable when disabled. | boolean | true |
indexThe item's position among the items, when it differs from the DOM order. | number | — |
labelThe text typeahead matches. | string | — |
metadataAny value, published with the item through the Root's onMapChange. | unknown | — |
| Data attribute | Description |
|---|---|
data-disabled | Present when the item is disabled. |
data-focusable | Present when the item remains focusable when disabled. |
data-orientation | Indicates the orientation of the composite. |
RootComposite.Root
Groups items into one tab stop and moves focus between them with the arrow keys. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
orientationThe arrow keys that move focus: Left and Right for 'horizontal', Up and Down for | CompositeOrientation | 'both' |
loopFocusWhether focus wraps to the other end of the items once an end is reached; in a grid, within | boolean | true |
onLoopCalled when focus wraps, with the index it leaves and the index it would land on. | ((event: KeyboardEvent, prevIndex: number, nextIndex: number) => number) | — |
enableHomeAndEndKeysWhether the Home and End keys move focus to the first and last item. | boolean | false |
colsThe number of columns: the items are a grid of this many columns in DOM order. | number | — |
itemSizesThe size of each item in grid cells, by item index. | CompositeGridItemSize[] | — |
denseWhether the grid packs itemSizes densely, as CSS grid-auto-flow: dense. | boolean | false |
highlightedIndexThe index of the item that holds the tab stop, in DOM order. | number | — |
defaultHighlightedIndexThe index of the item that initially holds the tab stop, in DOM order. | number | 0 |
onHighlightedIndexChangeCalled before the tab stop moves to another item. | ((index: number, eventDetails: CompositeRootChangeEventDetails) => void) | — |
disabledIndicesThe indices of the items the keys skip. | number[] | — |
typeaheadWhether typing a printable character moves focus to the next item whose label starts with | boolean | false |
stopEventPropagationWhether the keys the composite handles stop propagating. | boolean | true |
modifierKeysThe modifier keys that may be held while an arrow, Home or End key moves focus. | ("Shift" | "Control" | "Alt" | "Meta")[] | [] |
onMapChangeCalled with the registered items, in DOM order, whenever they change. | ((map: Map<Element, CompositeRoot.MapEntry<Metadata>>) => void) | — |
| Data attribute | Description |
|---|---|
data-orientation | Indicates the orientation of the composite. |
Shared props
Every part that renders an element takes these.
| Prop | Type |
|---|---|
classNameCSS class applied to the element, or a function that
returns a class based on the component's state. | string | ((state: State) => string | undefined) |
styleStyle applied to the element, or a function that
returns a style object based on the component's state. | React.CSSProperties | ((state: State) => React.CSSProperties | undefined) |
renderAllows you to replace the component's HTML element
with a different tag, or compose it with another component. | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State> |
Keyboard and accessibility
Keyboard
| Keys | Does | Part |
|---|---|---|
| ArrowRightArrowLeft | Moves to the next or previous item with orientation="horizontal" | Root |
| ArrowDownArrowUp | Moves to the next or previous item with orientation="vertical" | Root |
| ArrowRightArrowDown | Both move forward with orientation="both", the default | Root |
| ArrowLeftArrowRight | Swap in a right-to-left direction; ArrowUp and ArrowDown never do | Root |
| ArrowRight | Wraps to the first item at the end, with loopFocus on by default | Root |
| ArrowRight | Stops at the last item and lets the key through while loopFocus is off | Root |
| HomeEnd | Moves to the first or last item that is not skipped, with enableHomeAndEndKeys | Root |
| HomeEnd | Are left to the page without enableHomeAndEndKeys | Root |
Moves to the next item whose text starts with it, with typeahead | Root | |
| ArrowDownArrowUp | Moves by row and keeps the column, with cols | Root |
| ArrowRightArrowLeft | Moves within the row, with cols and orientation="both" | Root |
| ArrowRight | Reaches a disabled Item, which stays focusable by default | Item |
| ArrowRight | Skips a disabled Item with focusableWhenDisabled off | Item |
| ArrowLeftArrowRight | In a text input Item, move the caret and leave the field only at the end of its text | Item |
| EnterSpaceTab | Are left to the item and the page | Root |
Accessibility
| Part | Sets | So that |
|---|---|---|
| Root | no role, no tabindex and no aria-orientation; the app's role and ARIA attributes pass through | the group is announced as the pattern the app gives it, a toolbar, listbox, grid or radio group |
| Item | tabindex="0" on the item that holds the tab stop and tabindex="-1" on the others, over the app's own tabindex | the group is one Tab stop, and Tab back into it lands where the reader left |
| Item | aria-disabled="true" and data-disabled while disabled, never the native disabled attribute | a disabled item stays in the arrow-key order and is announced as unavailable |
Focus order
- The group is one Tab stop: the Item that holds the tab stop, the first enabled one until something moves it.
- The arrow keys, Home, End and typeahead move focus between the Items; Tab and Shift+Tab leave the group.
- A click or a script's focus on another Item makes it the tab stop (
focus-in), so Shift+Tab back into the group returns to it.