@primitiv-ui/react 0.1.29 → 0.1.30
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/README.md +15 -2
- package/package.json +1 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +1 -1
- package/src/Accordion/Accordion.tsx +7 -7
- package/src/Accordion/README.md +2 -2
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +3 -3
- package/src/Breadcrumb/Breadcrumb.tsx +7 -7
- package/src/Breadcrumb/README.md +2 -2
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +1 -1
- package/src/Breadcrumb/types.ts +1 -1
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Carousel/Carousel.tsx +6 -6
- package/src/Carousel/README.md +22 -22
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +5 -5
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +2 -2
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +1 -1
- package/src/Carousel/loopEngine.ts +1 -1
- package/src/Carousel/types.ts +1 -1
- package/src/Checkbox/Checkbox.tsx +2 -2
- package/src/CheckboxCard/CheckboxCard.tsx +1 -1
- package/src/CheckboxCard/README.md +1 -1
- package/src/Collapsible/README.md +2 -2
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +3 -3
- package/src/ContextMenu/README.md +1 -1
- package/src/DirectionProvider/DirectionProvider.tsx +5 -5
- package/src/DirectionProvider/README.md +4 -4
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Drawer/Drawer.tsx +5 -5
- package/src/Drawer/README.md +2 -2
- package/src/Dropdown/Dropdown.tsx +2 -2
- package/src/Dropdown/README.md +7 -5
- package/src/EmptyState/README.md +2 -2
- package/src/Field/README.md +1 -1
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Fieldset/Fieldset.tsx +3 -3
- package/src/Fieldset/README.md +2 -2
- package/src/Input/Input.tsx +1 -1
- package/src/Input/README.md +2 -2
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +104 -30
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +56 -6
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +4 -4
- package/src/Modal/README.md +5 -5
- package/src/Modal/types.ts +1 -1
- package/src/NavigationMenu/NavigationMenu.tsx +6 -6
- package/src/NavigationMenu/README.md +3 -3
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +1 -1
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +1 -1
- package/src/Popover/Popover.tsx +2 -2
- package/src/Popover/README.md +1 -1
- package/src/Portal/Portal.tsx +2 -2
- package/src/Portal/README.md +2 -2
- package/src/Progress/README.md +1 -1
- package/src/Radio/Radio.tsx +1 -1
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +1 -1
- package/src/SegmentedControl/README.md +2 -2
- package/src/SegmentedControl/SegmentedControl.tsx +1 -1
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +1 -0
- package/src/Select/README.md +2 -2
- package/src/Select/Select.tsx +8 -8
- package/src/Select/__tests__/Select.placeholder.test.tsx +3 -3
- package/src/Select/__tests__/Select.typeahead.test.tsx +3 -3
- package/src/Select/__tests__/Select.value-mirror.test.tsx +1 -1
- package/src/Select/types.ts +2 -2
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +5 -5
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +9 -3
- package/src/Slot/Slot.tsx +2 -2
- package/src/Slot/__tests__/Slot.test.tsx +2 -2
- package/src/Slot/types.ts +1 -1
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +2 -2
- package/src/Switch/types.ts +1 -1
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +206 -10
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +125 -0
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +5 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/ToggleGroup/README.md +4 -4
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +2 -2
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +40 -14
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +1 -1
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +6 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mode-independent props shared by every `Listbox.Root` variant — orientation,
|
|
5
|
+
* direction, selection-follows-focus, `asChild`, children, and the ref.
|
|
6
|
+
* Combined with a mode-specific arm to form {@link ListboxRootProps}.
|
|
7
|
+
*/
|
|
8
|
+
export type ListboxRootBaseProps = Omit<
|
|
9
|
+
HTMLAttributes<HTMLDivElement>,
|
|
10
|
+
"defaultValue" | "dir"
|
|
11
|
+
> & {
|
|
12
|
+
/** Layout axis for cursor navigation. `"vertical"` binds ArrowUp/ArrowDown;
|
|
13
|
+
* `"horizontal"` binds ArrowLeft/ArrowRight and additionally emits
|
|
14
|
+
* `aria-orientation="horizontal"` (vertical is the ARIA default, so it is
|
|
15
|
+
* left implicit). Surfaces as `data-orientation` for styling; it does not
|
|
16
|
+
* itself apply any layout.
|
|
17
|
+
* @default "vertical" */
|
|
18
|
+
orientation?: "horizontal" | "vertical";
|
|
19
|
+
/** Reading direction. In `"rtl"` the horizontal arrow pair is mirrored so
|
|
20
|
+
* the cursor follows visual order — meaningful only when
|
|
21
|
+
* {@link ListboxRootBaseProps.orientation} is `"horizontal"`. Inherited from
|
|
22
|
+
* the nearest {@link DirectionProvider} when omitted, falling back to
|
|
23
|
+
* `"ltr"`. */
|
|
24
|
+
dir?: "ltr" | "rtl";
|
|
25
|
+
/** When `true`, moving the cursor with the arrow / Home / End keys or via
|
|
26
|
+
* typeahead also selects the option it lands on — the behaviour of APG's
|
|
27
|
+
* single-select listbox example. Left `false`, arrowing only moves the
|
|
28
|
+
* highlight and the user commits with Enter or Space, which is what a
|
|
29
|
+
* command palette or search-suggestion list needs.
|
|
30
|
+
* @default false */
|
|
31
|
+
selectionFollowsFocus?: boolean;
|
|
32
|
+
/** Render a single consumer-supplied element in place of the native `<div>`,
|
|
33
|
+
* with the listbox's `role`, `tabIndex`, ARIA state, `data-orientation`, ref
|
|
34
|
+
* and keyboard handlers merged onto it via the {@link Slot} pattern.
|
|
35
|
+
* @default false */
|
|
36
|
+
asChild?: boolean;
|
|
37
|
+
/** The {@link ListboxOptionProps | `Listbox.Option`} and
|
|
38
|
+
* {@link ListboxGroupProps | `Listbox.Group`} elements that make up the
|
|
39
|
+
* list. */
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
42
|
+
ref?: Ref<HTMLDivElement>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Single-selection mode, uncontrolled — at most one option selected, with the
|
|
47
|
+
* initial choice supplied via `defaultValue`.
|
|
48
|
+
*/
|
|
49
|
+
export type ListboxSingleUncontrolledProps = {
|
|
50
|
+
/** Selects single-selection semantics: at most one option selected at a
|
|
51
|
+
* time, and re-selecting the current option is a no-op rather than a
|
|
52
|
+
* deselect. */
|
|
53
|
+
type: "single";
|
|
54
|
+
/** Value of the option selected on first render. Omit to start with nothing
|
|
55
|
+
* selected. */
|
|
56
|
+
defaultValue?: string;
|
|
57
|
+
/** Forbidden in uncontrolled mode — use
|
|
58
|
+
* {@link ListboxSingleUncontrolledProps.defaultValue}. */
|
|
59
|
+
value?: never;
|
|
60
|
+
/** Called with the newly selected option's value whenever the user commits a
|
|
61
|
+
* choice. */
|
|
62
|
+
onValueChange?: (value: string) => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Single-selection mode, controlled — the selected value is owned by the
|
|
67
|
+
* caller via `value` / `onValueChange`.
|
|
68
|
+
*/
|
|
69
|
+
export type ListboxSingleControlledProps = {
|
|
70
|
+
/** Selects single-selection semantics: at most one option selected at a
|
|
71
|
+
* time, and re-selecting the current option is a no-op rather than a
|
|
72
|
+
* deselect. */
|
|
73
|
+
type: "single";
|
|
74
|
+
/** The currently selected option's value. Must be kept in sync by the caller
|
|
75
|
+
* via {@link ListboxSingleControlledProps.onValueChange}. */
|
|
76
|
+
value: string;
|
|
77
|
+
/** Called with the value the user is asking to select. */
|
|
78
|
+
onValueChange?: (value: string) => void;
|
|
79
|
+
/** Forbidden in controlled mode — use
|
|
80
|
+
* {@link ListboxSingleControlledProps.value}. */
|
|
81
|
+
defaultValue?: never;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Multiple-selection mode, uncontrolled — options toggle independently, with
|
|
86
|
+
* the initial set supplied via `defaultValue`.
|
|
87
|
+
*/
|
|
88
|
+
export type ListboxMultipleUncontrolledProps = {
|
|
89
|
+
/** Selects multiple-selection semantics: any number of options can be
|
|
90
|
+
* selected and each toggles independently. Sets `aria-multiselectable` on
|
|
91
|
+
* the root. */
|
|
92
|
+
type: "multiple";
|
|
93
|
+
/** Values of the options selected on first render. Omit to start with
|
|
94
|
+
* nothing selected. */
|
|
95
|
+
defaultValue?: string[];
|
|
96
|
+
/** Forbidden in uncontrolled mode — use
|
|
97
|
+
* {@link ListboxMultipleUncontrolledProps.defaultValue}. */
|
|
98
|
+
value?: never;
|
|
99
|
+
/** Called with the complete next set of selected values whenever the user
|
|
100
|
+
* toggles any option. */
|
|
101
|
+
onValueChange?: (value: string[]) => void;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Multiple-selection mode, controlled — the set of selected values is owned by
|
|
106
|
+
* the caller via `value` / `onValueChange`.
|
|
107
|
+
*/
|
|
108
|
+
export type ListboxMultipleControlledProps = {
|
|
109
|
+
/** Selects multiple-selection semantics: any number of options can be
|
|
110
|
+
* selected and each toggles independently. Sets `aria-multiselectable` on
|
|
111
|
+
* the root. */
|
|
112
|
+
type: "multiple";
|
|
113
|
+
/** The full set of currently selected values. Must be kept in sync by the
|
|
114
|
+
* caller via {@link ListboxMultipleControlledProps.onValueChange}. */
|
|
115
|
+
value: string[];
|
|
116
|
+
/** Called with the complete next set of selected values whenever the user
|
|
117
|
+
* toggles any option. */
|
|
118
|
+
onValueChange?: (value: string[]) => void;
|
|
119
|
+
/** Forbidden in controlled mode — use
|
|
120
|
+
* {@link ListboxMultipleControlledProps.value}. */
|
|
121
|
+
defaultValue?: never;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Props for {@link ListboxRoot | `Listbox.Root`}. Combines the shared
|
|
126
|
+
* {@link ListboxRootBaseProps} with one mode-specific arm — single or
|
|
127
|
+
* multiple, controlled or uncontrolled — so the `value` / `defaultValue` /
|
|
128
|
+
* `onValueChange` shape is enforced per `type`.
|
|
129
|
+
*/
|
|
130
|
+
export type ListboxRootProps = ListboxRootBaseProps &
|
|
131
|
+
(
|
|
132
|
+
| ListboxSingleUncontrolledProps
|
|
133
|
+
| ListboxSingleControlledProps
|
|
134
|
+
| ListboxMultipleUncontrolledProps
|
|
135
|
+
| ListboxMultipleControlledProps
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Props for {@link ListboxOption | `Listbox.Option`} — one selectable choice.
|
|
140
|
+
* `value` identifies it within the listbox; `children` supplies the label that
|
|
141
|
+
* typeahead matches against.
|
|
142
|
+
*/
|
|
143
|
+
export type ListboxOptionProps = HTMLAttributes<HTMLDivElement> & {
|
|
144
|
+
/** Identifies this option within the listbox. It is this string — not the
|
|
145
|
+
* visible label — that is compared against the root's selected `value` /
|
|
146
|
+
* `defaultValue`; use `children` for the label. */
|
|
147
|
+
value: string;
|
|
148
|
+
/** Removes the option from cursor navigation, focus seeding and typeahead,
|
|
149
|
+
* and makes it unselectable by click or key. The option stays in the DOM and
|
|
150
|
+
* in the accessibility tree, marked `aria-disabled`. Also sets
|
|
151
|
+
* `data-disabled=""` for CSS targeting.
|
|
152
|
+
* @default false */
|
|
153
|
+
disabled?: boolean;
|
|
154
|
+
/** Render a single consumer-supplied element in place of the native `<div>`,
|
|
155
|
+
* with the option's `role`, `id`, `aria-selected`, `data-highlighted` and
|
|
156
|
+
* click handler merged onto it via the {@link Slot} pattern. The child must
|
|
157
|
+
* accept a `ref`.
|
|
158
|
+
* @default false */
|
|
159
|
+
asChild?: boolean;
|
|
160
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
161
|
+
ref?: Ref<HTMLDivElement>;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Props for {@link ListboxGroup | `Listbox.Group`} — a named cluster of
|
|
166
|
+
* options. Grouping is presentational; it does not partition navigation.
|
|
167
|
+
*/
|
|
168
|
+
export type ListboxGroupProps = HTMLAttributes<HTMLDivElement> & {
|
|
169
|
+
/** Accessible name for the group, applied as `aria-label`. APG requires
|
|
170
|
+
* every option group to carry a name — supply it either with this prop (an
|
|
171
|
+
* invisible name) or by rendering a
|
|
172
|
+
* {@link ListboxGroupLabelProps | `Listbox.GroupLabel`} heading inside the
|
|
173
|
+
* group, which names it via `aria-labelledby` instead. A rendered
|
|
174
|
+
* `GroupLabel` takes precedence. */
|
|
175
|
+
label?: string;
|
|
176
|
+
/** Render a single consumer-supplied element in place of the native `<div>`,
|
|
177
|
+
* with the group's `role` and `aria-label` merged onto it via the
|
|
178
|
+
* {@link Slot} pattern.
|
|
179
|
+
* @default false */
|
|
180
|
+
asChild?: boolean;
|
|
181
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
182
|
+
ref?: Ref<HTMLDivElement>;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Props for {@link ListboxGroupLabel | `Listbox.GroupLabel`} — the visible
|
|
187
|
+
* heading that names its enclosing {@link ListboxGroup | `Listbox.Group`}.
|
|
188
|
+
*/
|
|
189
|
+
export type ListboxGroupLabelProps = HTMLAttributes<HTMLDivElement> & {
|
|
190
|
+
/** Render a single consumer-supplied element in place of the native
|
|
191
|
+
* `<div>`, with the heading's `id` and `role="presentation"` merged onto it
|
|
192
|
+
* via the {@link Slot} pattern.
|
|
193
|
+
* @default false */
|
|
194
|
+
asChild?: boolean;
|
|
195
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
196
|
+
ref?: Ref<HTMLDivElement>;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Context shared from {@link ListboxGroup | `Listbox.Group`} to its label —
|
|
201
|
+
* the id the heading must adopt so the group can point `aria-labelledby` at
|
|
202
|
+
* it, and the registrar telling the group a heading is present.
|
|
203
|
+
*/
|
|
204
|
+
export type ListboxGroupContextValue = {
|
|
205
|
+
/** The DOM id the {@link ListboxGroupLabel} must render with. */
|
|
206
|
+
labelId: string;
|
|
207
|
+
/** Called by the label on mount/unmount so the group knows whether to emit
|
|
208
|
+
* `aria-labelledby`. */
|
|
209
|
+
registerLabel: (present: boolean) => void;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Context shared from {@link ListboxRoot | `Listbox.Root`} to its options —
|
|
214
|
+
* the selected set, the select action, the virtual-focus cursor, option
|
|
215
|
+
* registration, and the id derivation that wires `aria-activedescendant` to
|
|
216
|
+
* each option's `id`.
|
|
217
|
+
*/
|
|
218
|
+
export type ListboxContextValue = {
|
|
219
|
+
/** Every currently selected option value. Single mode holds at most one. */
|
|
220
|
+
selectedValues: string[];
|
|
221
|
+
/** Selects (single) or toggles (multiple) the given option. */
|
|
222
|
+
select: (optionValue: string) => void;
|
|
223
|
+
/** The option the cursor is on, or `undefined` before the root is focused. */
|
|
224
|
+
activeValue: string | undefined;
|
|
225
|
+
/** Moves the cursor onto the given option and scrolls it into view. Used by
|
|
226
|
+
* `Listbox.Option` so a click leaves the cursor where the user clicked. */
|
|
227
|
+
moveCursor: (optionValue: string) => void;
|
|
228
|
+
/** Registers a mounted option's element and disabled state; pass `null` as
|
|
229
|
+
* the element to unregister. */
|
|
230
|
+
registerOption: (
|
|
231
|
+
optionValue: string,
|
|
232
|
+
element: HTMLElement | null,
|
|
233
|
+
disabled?: boolean,
|
|
234
|
+
) => void;
|
|
235
|
+
/** Derives the stable DOM `id` for an option, shared by the option itself
|
|
236
|
+
* and the root's `aria-activedescendant`. */
|
|
237
|
+
getOptionId: (optionValue: string) => string;
|
|
238
|
+
};
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
useMillerColumnsRoot,
|
|
19
19
|
} from "./hooks/index.ts";
|
|
20
20
|
|
|
21
|
-
import { partitionItemChildren } from "./utils";
|
|
21
|
+
import { partitionAriaProps, partitionItemChildren } from "./utils";
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
24
|
MillerColumnsRootProps,
|
|
@@ -33,9 +33,20 @@ import type {
|
|
|
33
33
|
* The root of a Miller Columns widget — a horizontal strip of vertical
|
|
34
34
|
* lists where selecting a node reveals its children in the next column.
|
|
35
35
|
*
|
|
36
|
-
* Renders
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Renders **two** elements: an outer strip container
|
|
37
|
+
* (`data-miller-columns-strip`, `data-orientation="horizontal"`) that the
|
|
38
|
+
* consumer lays out and scrolls, and an inner `role="tree"` widget
|
|
39
|
+
* (`data-miller-columns-tree`) holding only the columns. The split exists
|
|
40
|
+
* because a tree may own nothing but `treeitem` and `group` — keeping the
|
|
41
|
+
* {@link MillerColumnsPreviewPanel | `PreviewPanel`} outside it means the
|
|
42
|
+
* panel is not an illegal child of the tree.
|
|
43
|
+
*
|
|
44
|
+
* Consequently **`aria-*` props land on the tree widget** (an `aria-label`
|
|
45
|
+
* must name the tree, not its scroll container) while every other prop —
|
|
46
|
+
* `className`, `id`, `style`, `data-*` — lands on the strip container.
|
|
47
|
+
*
|
|
48
|
+
* Every {@link MillerColumnsColumn | `MillerColumns.Column`}
|
|
49
|
+
* projects itself into the tree via a portal. Authoring the tree is **recursive** and
|
|
39
50
|
* data-less: there is no `items` prop — an {@link MillerColumnsItem | `Item`}
|
|
40
51
|
* declares its child column as a *nested* `<MillerColumns.Column>`, and the
|
|
41
52
|
* strip flattens those nested columns into a single left-to-right row by
|
|
@@ -58,7 +69,15 @@ import type {
|
|
|
58
69
|
* before any focus it defaults to the deepest selected item, falling back
|
|
59
70
|
* to the first enabled root item. Within a column, Up/Down/Home/End move
|
|
60
71
|
* focus; ArrowRight opens (and steps into) a branch item's child column,
|
|
61
|
-
* ArrowLeft returns to the parent item.
|
|
72
|
+
* ArrowLeft returns to the parent item. Under
|
|
73
|
+
* {@link MillerColumnsRootBaseProps.dir | `dir="rtl"`} that horizontal pair
|
|
74
|
+
* is mirrored, so the step-in key always points at the child column.
|
|
75
|
+
*
|
|
76
|
+
* **Typeahead.** Printable characters move focus to the first item whose
|
|
77
|
+
* label starts with the accumulated query, wrapping, and cycling through
|
|
78
|
+
* matches when the same character repeats. The query is owned by `Root` —
|
|
79
|
+
* so it survives the focus move a match causes — but resolves against the
|
|
80
|
+
* **focused item's own column**, each column being an independent list.
|
|
62
81
|
*
|
|
63
82
|
* **Context.** Root establishes {@link MillerColumnsContext} (selection
|
|
64
83
|
* path, portal slots, item registry, column widths, and focus commands);
|
|
@@ -83,28 +102,32 @@ export function MillerColumnsRoot({
|
|
|
83
102
|
defaultValue,
|
|
84
103
|
value,
|
|
85
104
|
onValueChange,
|
|
105
|
+
dir,
|
|
86
106
|
...rest
|
|
87
107
|
}: MillerColumnsRootProps): ReactElement {
|
|
88
|
-
const { contextValue, columnCount, registerSlotRef, stripRef } =
|
|
89
|
-
useMillerColumnsRoot(value, defaultValue, onValueChange);
|
|
108
|
+
const { contextValue, columnCount, registerSlotRef, stripRef, resolvedDir } =
|
|
109
|
+
useMillerColumnsRoot(value, defaultValue, onValueChange, dir);
|
|
110
|
+
const { aria, rest: containerProps } = partitionAriaProps(rest);
|
|
90
111
|
|
|
91
112
|
return (
|
|
92
113
|
<MillerColumnsContext.Provider value={contextValue}>
|
|
93
114
|
<div
|
|
94
115
|
ref={stripRef}
|
|
95
|
-
|
|
116
|
+
dir={resolvedDir}
|
|
96
117
|
data-miller-columns-strip=""
|
|
97
118
|
data-orientation="horizontal"
|
|
98
|
-
{...
|
|
119
|
+
{...containerProps}
|
|
99
120
|
>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
<div role="tree" data-miller-columns-tree="" {...aria}>
|
|
122
|
+
{Array.from({ length: columnCount }, (_, depth) => (
|
|
123
|
+
<div
|
|
124
|
+
key={depth}
|
|
125
|
+
data-miller-columns-slot=""
|
|
126
|
+
style={{ display: "contents" }}
|
|
127
|
+
ref={registerSlotRef(depth)}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
108
131
|
{children}
|
|
109
132
|
</div>
|
|
110
133
|
</MillerColumnsContext.Provider>
|
|
@@ -122,6 +145,10 @@ MillerColumnsRoot.displayName = "MillerColumnsRoot";
|
|
|
122
145
|
* ancestors rather than in document flow. Until its slot has mounted the
|
|
123
146
|
* component renders `null`.
|
|
124
147
|
*
|
|
148
|
+
* A column holding no registered items carries **`data-empty`** — selecting
|
|
149
|
+
* a childless branch legitimately opens an empty column, and the styling
|
|
150
|
+
* layer needs to be able to say so rather than show a void.
|
|
151
|
+
*
|
|
125
152
|
* It reads its depth from {@link MillerColumnsContext} and re-provides it to
|
|
126
153
|
* descendant {@link MillerColumnsItem | Items} via
|
|
127
154
|
* {@link MillerColumnsColumnContext}. A width set by a
|
|
@@ -143,7 +170,8 @@ export function MillerColumnsColumn({
|
|
|
143
170
|
style,
|
|
144
171
|
...rest
|
|
145
172
|
}: MillerColumnsColumnProps): ReactPortal | null {
|
|
146
|
-
const { slot, depth, width, columnContextValue } =
|
|
173
|
+
const { slot, depth, width, empty, columnContextValue } =
|
|
174
|
+
useMillerColumnsColumn();
|
|
147
175
|
|
|
148
176
|
if (!slot) {
|
|
149
177
|
return null;
|
|
@@ -155,6 +183,7 @@ export function MillerColumnsColumn({
|
|
|
155
183
|
role="group"
|
|
156
184
|
data-miller-columns-column=""
|
|
157
185
|
data-depth={depth}
|
|
186
|
+
{...(empty ? { "data-empty": "" } : {})}
|
|
158
187
|
style={{ ...style, ...(width !== undefined ? { width } : {}) }}
|
|
159
188
|
{...rest}
|
|
160
189
|
>
|
|
@@ -174,6 +203,13 @@ MillerColumnsColumn.displayName = "MillerColumnsColumn";
|
|
|
174
203
|
* `aria-selected`, `aria-level` (1-based depth), and `data-state` /
|
|
175
204
|
* `data-depth` styling hooks.
|
|
176
205
|
*
|
|
206
|
+
* **Terminal vs ancestor.** Every item on the active path is
|
|
207
|
+
* `data-state="selected"`, including the ancestors the path merely passes
|
|
208
|
+
* through. The deepest one — the item the user actually landed on — also
|
|
209
|
+
* carries **`data-terminal`**. Style the two differently or the strip loses
|
|
210
|
+
* its depth cue: with a uniform selected state there is no way to tell
|
|
211
|
+
* which of four highlighted rows was clicked.
|
|
212
|
+
*
|
|
177
213
|
* The {@link MillerColumnsItemProps.value | `value`} prop is the stable
|
|
178
214
|
* identifier used to match this item against the active selection path.
|
|
179
215
|
*
|
|
@@ -269,7 +305,7 @@ MillerColumnsItem.displayName = "MillerColumnsItem";
|
|
|
269
305
|
* <MillerColumns.Item value="docs">
|
|
270
306
|
* Docs
|
|
271
307
|
* <MillerColumns.ItemIndicator>▸</MillerColumns.ItemIndicator>
|
|
272
|
-
* <MillerColumns.Column
|
|
308
|
+
* <MillerColumns.Column>...</MillerColumns.Column>
|
|
273
309
|
* </MillerColumns.Item>
|
|
274
310
|
* ```
|
|
275
311
|
*/
|
|
@@ -299,9 +335,27 @@ export function MillerColumnsItemIndicator({
|
|
|
299
335
|
MillerColumnsItemIndicator.displayName = "MillerColumnsItemIndicator";
|
|
300
336
|
|
|
301
337
|
/**
|
|
302
|
-
* A
|
|
303
|
-
*
|
|
304
|
-
*
|
|
338
|
+
* A resize affordance for the column it is rendered in — the WAI-ARIA
|
|
339
|
+
* window-splitter pattern. Renders a focusable
|
|
340
|
+
* `<div role="separator" aria-orientation="vertical" tabIndex={0}>` that
|
|
341
|
+
* drives its column's width as state on the `Root`.
|
|
342
|
+
*
|
|
343
|
+
* **Pointer and keyboard.** Drag it, or focus it and use the arrow keys:
|
|
344
|
+
* the inline-end arrow widens by
|
|
345
|
+
* {@link MillerColumnsResizeHandleProps.step | `step`} and the inline-start
|
|
346
|
+
* arrow narrows (mirrored under `dir="rtl"`), with Home and End jumping to
|
|
347
|
+
* {@link MillerColumnsResizeHandleProps.minWidth | `minWidth`} /
|
|
348
|
+
* {@link MillerColumnsResizeHandleProps.maxWidth | `maxWidth`}. Both paths
|
|
349
|
+
* clamp to the same bounds. End is a no-op while `maxWidth` is unbounded.
|
|
350
|
+
*
|
|
351
|
+
* **Value semantics.** `aria-valuenow` tracks the column's real rendered
|
|
352
|
+
* width — measured with a `ResizeObserver`, so it is truthful before the
|
|
353
|
+
* first resize and follows CSS- or container-driven changes too.
|
|
354
|
+
* `aria-valuemin` always renders; `aria-valuemax` renders only when
|
|
355
|
+
* `maxWidth` is finite.
|
|
356
|
+
*
|
|
357
|
+
* **Give it an accessible name.** A splitter announces as an unnamed
|
|
358
|
+
* separator otherwise — pass `aria-label` (e.g. `"Resize column"`).
|
|
305
359
|
*
|
|
306
360
|
* Must be rendered among a {@link MillerColumnsColumn | `MillerColumns.Column`}'s
|
|
307
361
|
* children — it reads the column's depth from
|
|
@@ -313,7 +367,11 @@ MillerColumnsItemIndicator.displayName = "MillerColumnsItemIndicator";
|
|
|
313
367
|
* @example
|
|
314
368
|
* ```tsx
|
|
315
369
|
* <MillerColumns.Column>
|
|
316
|
-
* <MillerColumns.ResizeHandle
|
|
370
|
+
* <MillerColumns.ResizeHandle
|
|
371
|
+
* aria-label="Resize column"
|
|
372
|
+
* minWidth={120}
|
|
373
|
+
* maxWidth={400}
|
|
374
|
+
* />
|
|
317
375
|
* <MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
318
376
|
* </MillerColumns.Column>
|
|
319
377
|
* ```
|
|
@@ -334,9 +392,14 @@ MillerColumnsResizeHandle.displayName = "MillerColumnsResizeHandle";
|
|
|
334
392
|
* macOS-Finder-style preview pane.
|
|
335
393
|
*
|
|
336
394
|
* Renders a plain `<div data-miller-columns-preview>` as the last child
|
|
337
|
-
* of the strip
|
|
338
|
-
*
|
|
339
|
-
*
|
|
395
|
+
* of the strip — *outside* the inner `role="tree"` widget, since a tree may
|
|
396
|
+
* own only `treeitem` and `group`. The component is deliberately
|
|
397
|
+
* content-agnostic: it does not know how to preview an item, so the
|
|
398
|
+
* consumer supplies whatever the panel should show.
|
|
399
|
+
*
|
|
400
|
+
* While nothing is selected the panel carries **`data-empty`**, so the
|
|
401
|
+
* styling layer can render a resting state without the consumer having to
|
|
402
|
+
* re-derive the selection.
|
|
340
403
|
*
|
|
341
404
|
* Pair it with {@link useMillerColumnsSelection} to render content for
|
|
342
405
|
* the current selection. Author it as the **last child** of `Root`, a
|
|
@@ -363,12 +426,23 @@ MillerColumnsResizeHandle.displayName = "MillerColumnsResizeHandle";
|
|
|
363
426
|
*/
|
|
364
427
|
export function MillerColumnsPreviewPanel({
|
|
365
428
|
children,
|
|
429
|
+
forceMount = false,
|
|
366
430
|
...rest
|
|
367
|
-
}: MillerColumnsPreviewPanelProps): ReactElement {
|
|
368
|
-
useMillerColumnsContext();
|
|
431
|
+
}: MillerColumnsPreviewPanelProps): ReactElement | null {
|
|
432
|
+
const { activePath, terminalIsLeaf } = useMillerColumnsContext();
|
|
433
|
+
|
|
434
|
+
// Finder's rule: the preview occupies the slot *after* the selected item, so
|
|
435
|
+
// it exists only where that item has no children to open there instead.
|
|
436
|
+
if (!forceMount && !terminalIsLeaf) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
369
439
|
|
|
370
440
|
return (
|
|
371
|
-
<div
|
|
441
|
+
<div
|
|
442
|
+
data-miller-columns-preview=""
|
|
443
|
+
{...(activePath.length === 0 ? { "data-empty": "" } : {})}
|
|
444
|
+
{...rest}
|
|
445
|
+
>
|
|
372
446
|
{children}
|
|
373
447
|
</div>
|
|
374
448
|
);
|
|
@@ -396,7 +470,7 @@ type MillerColumnsCompound = typeof MillerColumnsRoot & {
|
|
|
396
470
|
/**
|
|
397
471
|
* MillerColumns — a horizontal strip of vertical lists where selecting a
|
|
398
472
|
* node reveals its children in the next column. Use as a namespace
|
|
399
|
-
* (`MillerColumns.Root`, `MillerColumns.Column`, `MillerColumns.Item`,
|
|
473
|
+
* (`MillerColumns.Root`, `MillerColumns.Column`, `MillerColumns.Item`, ...);
|
|
400
474
|
* the default export is also callable as the Root.
|
|
401
475
|
*/
|
|
402
476
|
const MillerColumnsCompound: MillerColumnsCompound = Object.assign(
|
|
@@ -61,7 +61,7 @@ are ever rendered.
|
|
|
61
61
|
| `MillerColumns.Column` | List | A vertical list of items, projected into the strip as `role="group"` |
|
|
62
62
|
| `MillerColumns.Item` | Tree node | A `role="treeitem"`; branch when it nests a `Column`. Supports `disabled`, `asChild`, `ref` |
|
|
63
63
|
| `MillerColumns.ItemIndicator` | Icon wrapper | Decorative `aria-hidden` icon, rendered only for branch items |
|
|
64
|
-
| `MillerColumns.ResizeHandle` | Resize grip | A `role="separator"` drag
|
|
64
|
+
| `MillerColumns.ResizeHandle` | Resize grip | A focusable `role="separator"` window splitter — drag or arrow-key it to set its column's width |
|
|
65
65
|
| `MillerColumns.PreviewPanel` | Preview pane | A content-agnostic trailing panel; pair with `useMillerColumnsSelection` |
|
|
66
66
|
|
|
67
67
|
## Selection model
|
|
@@ -80,7 +80,20 @@ column deeper than _d_ closes.
|
|
|
80
80
|
The two shapes are discriminated at the type level: passing
|
|
81
81
|
`defaultValue` alongside `value` is a type error.
|
|
82
82
|
|
|
83
|
-
There is no multi-select
|
|
83
|
+
**Single path only, by decision.** There is no multi-select and there is
|
|
84
|
+
not going to be one — this is settled, not a v1 shortfall awaiting a
|
|
85
|
+
follow-up. A Miller strip's whole premise is that the selection *is* the
|
|
86
|
+
path: column N+1 exists because exactly one item in column N is chosen.
|
|
87
|
+
Two selections in a column have no defined answer for what the next
|
|
88
|
+
column should show. A consumer needing to mark many nodes should keep
|
|
89
|
+
that set themselves and render it as content inside each `Item` (a
|
|
90
|
+
checkbox, a badge), leaving the path to mean navigation.
|
|
91
|
+
|
|
92
|
+
Every item on the path renders `data-state="selected"`, including the
|
|
93
|
+
ancestors the path merely passes through. The **deepest** one also gets
|
|
94
|
+
`data-terminal`. Style the two apart — with a uniform selected state
|
|
95
|
+
there is no way to tell which of several highlighted rows was actually
|
|
96
|
+
clicked.
|
|
84
97
|
|
|
85
98
|
## Keyboard interaction
|
|
86
99
|
|
|
@@ -92,6 +105,18 @@ There is no multi-select — only one path is active at a time.
|
|
|
92
105
|
| `ArrowRight` | Branch: select it and move focus to its child column's first item; leaf: no-op |
|
|
93
106
|
| `ArrowLeft` | Move focus to the selected item of the parent column |
|
|
94
107
|
| `Tab` | Move into / out of the whole tree (single tabstop) |
|
|
108
|
+
| Printable character | Typeahead — focus the first item in the column whose label matches |
|
|
109
|
+
|
|
110
|
+
Under `dir="rtl"` the horizontal pair is mirrored: `ArrowLeft` steps
|
|
111
|
+
*into* a branch's child column and `ArrowRight` walks back out, so the
|
|
112
|
+
step-in key always points at the column it opens. `dir` is inherited
|
|
113
|
+
from a `DirectionProvider` ancestor when omitted.
|
|
114
|
+
|
|
115
|
+
Typeahead accumulates keystrokes into a prefix query that resets after
|
|
116
|
+
500 ms of inactivity; repeating one character cycles through the items
|
|
117
|
+
starting with it. The query is scoped to the **focused item's own
|
|
118
|
+
column** — each column is an independent list, so typing never jumps the
|
|
119
|
+
cursor sideways.
|
|
95
120
|
|
|
96
121
|
The strip is a single roving-tabindex widget: exactly one item is
|
|
97
122
|
tabbable at a time. The tabstop follows the last-focused item and
|
|
@@ -109,7 +134,20 @@ effect when the strip is itself an overflow container; give
|
|
|
109
134
|
|
|
110
135
|
## ARIA
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
`Root` renders **two** elements: an outer strip container you lay out and
|
|
138
|
+
scroll, and an inner `role="tree"` widget holding only the columns. A
|
|
139
|
+
tree may own nothing but `treeitem` and `group`, so the `PreviewPanel`
|
|
140
|
+
sits outside it.
|
|
141
|
+
|
|
142
|
+
Because of that split, **`aria-*` props go to the tree widget** and every
|
|
143
|
+
other prop (`className`, `id`, `style`, `data-*`) goes to the strip
|
|
144
|
+
container:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<MillerColumns.Root aria-label="Files" className="strip">
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- The tree widget is `role="tree"`.
|
|
113
151
|
- Each `Column` is `role="group"`.
|
|
114
152
|
- Each `Item` is `role="treeitem"` with `aria-level` (1-based column
|
|
115
153
|
depth), `aria-selected`, and — on branch items — `aria-expanded`.
|
|
@@ -156,10 +194,42 @@ it to the column's trailing edge yourself:
|
|
|
156
194
|
```
|
|
157
195
|
|
|
158
196
|
The first drag measures the column's current rendered width and
|
|
159
|
-
resizes from there; later drags resume from the last resized width.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
resizes from there; later drags resume from the last resized width.
|
|
198
|
+
|
|
199
|
+
The handle is a full WAI-ARIA **window splitter**: it is focusable, and
|
|
200
|
+
arrow keys resize it. Pass `minWidth` / `maxWidth` / `step` to bound and
|
|
201
|
+
pace that (they clamp pointer drags too, so CSS `min-width` is no longer
|
|
202
|
+
the only floor).
|
|
203
|
+
|
|
204
|
+
| Prop | Default | Effect |
|
|
205
|
+
| ---------- | ---------- | -------------------------------------------------- |
|
|
206
|
+
| `minWidth` | `0` | Floor for drag and keys; published as `aria-valuemin` |
|
|
207
|
+
| `maxWidth` | `Infinity` | Ceiling; published as `aria-valuemax` only when finite |
|
|
208
|
+
| `step` | `10` | Pixels moved per arrow-key press |
|
|
209
|
+
|
|
210
|
+
| Key | Behaviour |
|
|
211
|
+
| ------------------------- | ---------------------------------- |
|
|
212
|
+
| Inline-end arrow | Widen the column by `step` |
|
|
213
|
+
| Inline-start arrow | Narrow the column by `step` |
|
|
214
|
+
| `Home` / `End` | Jump to `minWidth` / `maxWidth` |
|
|
215
|
+
|
|
216
|
+
The arrow pair mirrors under `dir="rtl"`. `End` does nothing while
|
|
217
|
+
`maxWidth` is unbounded — there is no largest width to jump to.
|
|
218
|
+
|
|
219
|
+
`aria-valuenow` tracks the column's real rendered width via a
|
|
220
|
+
`ResizeObserver`, so it is correct before the first resize and follows
|
|
221
|
+
CSS- or container-driven changes as well.
|
|
222
|
+
|
|
223
|
+
**Give the handle an accessible name** — without one it announces as an
|
|
224
|
+
unnamed separator:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<MillerColumns.ResizeHandle
|
|
228
|
+
aria-label="Resize column"
|
|
229
|
+
minWidth={120}
|
|
230
|
+
maxWidth={400}
|
|
231
|
+
/>
|
|
232
|
+
```
|
|
163
233
|
|
|
164
234
|
## Preview panel
|
|
165
235
|
|
|
@@ -199,10 +269,11 @@ function FilePreview() {
|
|
|
199
269
|
both controlled and uncontrolled roots, and throws if called outside
|
|
200
270
|
`MillerColumns.Root`.
|
|
201
271
|
|
|
202
|
-
The panel ships with no ARIA role
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
272
|
+
The panel ships with no ARIA role, and renders *outside* the inner
|
|
273
|
+
`role="tree"` widget — a tree may own only `treeitem`s and `group`s, so
|
|
274
|
+
the panel could not legally sit inside it. Give the panel content its
|
|
275
|
+
own labelled landmark (`role`, `aria-label`, ...) through props if the
|
|
276
|
+
preview warrants being announced.
|
|
206
277
|
|
|
207
278
|
## `asChild` composition
|
|
208
279
|
|
|
@@ -216,7 +287,7 @@ element:
|
|
|
216
287
|
```tsx
|
|
217
288
|
<MillerColumns.Item<HTMLAnchorElement> asChild value="docs">
|
|
218
289
|
<a href="#docs">Docs</a>
|
|
219
|
-
<MillerColumns.Column
|
|
290
|
+
<MillerColumns.Column>...</MillerColumns.Column>
|
|
220
291
|
</MillerColumns.Item>
|
|
221
292
|
```
|
|
222
293
|
|
|
@@ -229,10 +300,11 @@ Zero styles ship with the component. Style it through `data-*` hooks;
|
|
|
229
300
|
typically the strip is `display: flex` and each column scrolls
|
|
230
301
|
vertically.
|
|
231
302
|
|
|
232
|
-
Inside the
|
|
303
|
+
Inside the tree element, each column is wrapped in a transparent
|
|
233
304
|
(`display: contents`) slot element that fixes left-to-right order — so
|
|
234
305
|
target columns with `[data-miller-columns-column]`, not a direct-child
|
|
235
|
-
selector like `[data-miller-columns-
|
|
306
|
+
selector like `[data-miller-columns-tree] > *`. Note the strip's direct
|
|
307
|
+
children are the tree element and the `PreviewPanel`, not the columns.
|
|
236
308
|
|
|
237
309
|
```css
|
|
238
310
|
[data-miller-columns-strip] {
|
|
@@ -258,11 +330,12 @@ selector like `[data-miller-columns-strip] > *`.
|
|
|
258
330
|
| Element | Attributes |
|
|
259
331
|
| --------------- | --------------------------------------------------------------------------- |
|
|
260
332
|
| Strip (`Root`) | `data-miller-columns-strip`, `data-orientation="horizontal"` |
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
333
|
+
| Tree (`Root`) | `data-miller-columns-tree`, `role="tree"` — the inner widget element |
|
|
334
|
+
| `Column` | `data-miller-columns-column`, `data-depth`, `data-empty` (no items) |
|
|
335
|
+
| `Item` | `data-state="selected" \| "unselected"`, `data-terminal` (deepest selected), `data-depth`, `data-has-children`, `data-disabled` |
|
|
263
336
|
| `ItemIndicator` | `data-state`, `data-has-children` |
|
|
264
337
|
| `ResizeHandle` | `data-miller-columns-resize-handle`, `data-dragging` (present mid-drag) |
|
|
265
|
-
| `PreviewPanel` | `data-miller-columns-preview`
|
|
338
|
+
| `PreviewPanel` | `data-miller-columns-preview`, `data-empty` (nothing selected) |
|
|
266
339
|
|
|
267
340
|
## Deferred / follow-up work
|
|
268
341
|
|
|
@@ -273,6 +346,3 @@ good candidates for later, independent cycles:
|
|
|
273
346
|
exists, a leaf image `Item` could open a context menu on
|
|
274
347
|
right-click whose first entry ("Preview", with an eye icon) opens
|
|
275
348
|
the `Modal` to show the image larger.
|
|
276
|
-
2. **Keyboard-operable resize.** `MillerColumns.ResizeHandle` is
|
|
277
|
-
pointer-only; an arrow-key resize on the focused handle would round
|
|
278
|
-
out the WAI-ARIA window-splitter pattern.
|