@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.
Files changed (213) hide show
  1. package/README.md +15 -2
  2. package/package.json +1 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +1 -1
  4. package/src/Accordion/Accordion.tsx +7 -7
  5. package/src/Accordion/README.md +2 -2
  6. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +3 -3
  7. package/src/Breadcrumb/Breadcrumb.tsx +7 -7
  8. package/src/Breadcrumb/README.md +2 -2
  9. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +1 -1
  10. package/src/Breadcrumb/types.ts +1 -1
  11. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  12. package/src/Carousel/Carousel.tsx +6 -6
  13. package/src/Carousel/README.md +22 -22
  14. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +5 -5
  15. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +2 -2
  16. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +1 -1
  17. package/src/Carousel/loopEngine.ts +1 -1
  18. package/src/Carousel/types.ts +1 -1
  19. package/src/Checkbox/Checkbox.tsx +2 -2
  20. package/src/CheckboxCard/CheckboxCard.tsx +1 -1
  21. package/src/CheckboxCard/README.md +1 -1
  22. package/src/Collapsible/README.md +2 -2
  23. package/src/Combobox/Combobox.tsx +508 -0
  24. package/src/Combobox/ComboboxContext.ts +23 -0
  25. package/src/Combobox/README.md +170 -0
  26. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  27. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  28. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  29. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  30. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  31. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  32. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  33. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  34. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  35. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  36. package/src/Combobox/hooks/index.ts +1 -0
  37. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  38. package/src/Combobox/index.ts +3 -0
  39. package/src/Combobox/types.ts +133 -0
  40. package/src/ContextMenu/ContextMenu.tsx +3 -3
  41. package/src/ContextMenu/README.md +1 -1
  42. package/src/DirectionProvider/DirectionProvider.tsx +5 -5
  43. package/src/DirectionProvider/README.md +4 -4
  44. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  45. package/src/Drawer/Drawer.tsx +5 -5
  46. package/src/Drawer/README.md +2 -2
  47. package/src/Dropdown/Dropdown.tsx +2 -2
  48. package/src/Dropdown/README.md +7 -5
  49. package/src/EmptyState/README.md +2 -2
  50. package/src/Field/README.md +1 -1
  51. package/src/Field/hooks/useFieldProps.ts +1 -1
  52. package/src/Fieldset/Fieldset.tsx +3 -3
  53. package/src/Fieldset/README.md +2 -2
  54. package/src/Input/Input.tsx +1 -1
  55. package/src/Input/README.md +2 -2
  56. package/src/Listbox/Listbox.tsx +475 -0
  57. package/src/Listbox/ListboxContext.ts +48 -0
  58. package/src/Listbox/README.md +248 -0
  59. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  60. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  61. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  62. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  63. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  64. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  65. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  66. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  67. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  68. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  69. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  70. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  71. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  72. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  73. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  74. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  75. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  76. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  77. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  78. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  79. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  80. package/src/Listbox/constants.ts +2 -0
  81. package/src/Listbox/hooks/index.ts +1 -0
  82. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  83. package/src/Listbox/index.ts +3 -0
  84. package/src/Listbox/types.ts +238 -0
  85. package/src/MillerColumns/MillerColumns.tsx +104 -30
  86. package/src/MillerColumns/README.md +90 -20
  87. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  88. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  89. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  90. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  91. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  92. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  93. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  94. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  95. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  96. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  97. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  98. package/src/MillerColumns/constants.ts +2 -0
  99. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  100. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  101. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  102. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  103. package/src/MillerColumns/index.ts +4 -0
  104. package/src/MillerColumns/types.ts +56 -6
  105. package/src/MillerColumns/utils.ts +69 -3
  106. package/src/Modal/Modal.tsx +4 -4
  107. package/src/Modal/README.md +5 -5
  108. package/src/Modal/types.ts +1 -1
  109. package/src/NavigationMenu/NavigationMenu.tsx +6 -6
  110. package/src/NavigationMenu/README.md +3 -3
  111. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +1 -1
  112. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +1 -1
  113. package/src/Popover/Popover.tsx +2 -2
  114. package/src/Popover/README.md +1 -1
  115. package/src/Portal/Portal.tsx +2 -2
  116. package/src/Portal/README.md +2 -2
  117. package/src/Progress/README.md +1 -1
  118. package/src/Radio/Radio.tsx +1 -1
  119. package/src/RadioGroup/README.md +2 -2
  120. package/src/RadioGroup/RadioGroup.tsx +1 -1
  121. package/src/SegmentedControl/README.md +2 -2
  122. package/src/SegmentedControl/SegmentedControl.tsx +1 -1
  123. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +1 -0
  124. package/src/Select/README.md +2 -2
  125. package/src/Select/Select.tsx +8 -8
  126. package/src/Select/__tests__/Select.placeholder.test.tsx +3 -3
  127. package/src/Select/__tests__/Select.typeahead.test.tsx +3 -3
  128. package/src/Select/__tests__/Select.value-mirror.test.tsx +1 -1
  129. package/src/Select/types.ts +2 -2
  130. package/src/SkipNav/README.md +3 -3
  131. package/src/SkipNav/SkipNav.tsx +5 -5
  132. package/src/Slider/README.md +37 -7
  133. package/src/Slider/Slider.tsx +9 -3
  134. package/src/Slot/Slot.tsx +2 -2
  135. package/src/Slot/__tests__/Slot.test.tsx +2 -2
  136. package/src/Slot/types.ts +1 -1
  137. package/src/SplitButton/README.md +225 -0
  138. package/src/SplitButton/SplitButton.tsx +457 -0
  139. package/src/SplitButton/SplitButtonContext.ts +26 -0
  140. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  141. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  142. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  143. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  144. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  145. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  146. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  147. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  148. package/src/SplitButton/index.ts +2 -0
  149. package/src/SplitButton/types.ts +186 -0
  150. package/src/Switch/README.md +1 -1
  151. package/src/Switch/Switch.tsx +2 -2
  152. package/src/Switch/types.ts +1 -1
  153. package/src/Table/README.md +110 -8
  154. package/src/Table/Table.tsx +206 -10
  155. package/src/Table/TableExpandableContext.ts +7 -0
  156. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  157. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  158. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  159. package/src/Table/types.ts +125 -0
  160. package/src/Tabs/README.md +4 -4
  161. package/src/Tabs/Tabs.tsx +5 -4
  162. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  163. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  164. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  165. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  166. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  167. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  168. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  169. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  170. package/src/ToggleGroup/README.md +4 -4
  171. package/src/Tooltip/README.md +7 -7
  172. package/src/Tooltip/Tooltip.tsx +2 -2
  173. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  174. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  175. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  176. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  177. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  178. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  179. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  180. package/src/Tooltip/hooks/useTooltipProvider.ts +40 -14
  181. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  182. package/src/Tree/README.md +69 -5
  183. package/src/Tree/Tree.tsx +1 -1
  184. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  185. package/src/Tree/hooks/useTreePath.ts +33 -1
  186. package/src/Tree/index.ts +6 -1
  187. package/src/Tree/utils.ts +46 -4
  188. package/src/VisuallyHidden/VisuallyHidden.tsx +1 -1
  189. package/src/hooks/index.ts +1 -0
  190. package/src/hooks/useTypeahead.ts +95 -0
  191. package/src/index.ts +6 -0
  192. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  193. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  194. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  195. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  196. package/src/useLocalStorage/index.ts +1 -0
  197. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  198. package/src/useMediaQuery/README.md +66 -0
  199. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  200. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  201. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  202. package/src/useMediaQuery/index.ts +1 -0
  203. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  204. package/src/usePagination/README.md +170 -0
  205. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  206. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  207. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  208. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  209. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  210. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  211. package/src/usePagination/index.ts +1 -0
  212. package/src/usePagination/usePagination.ts +292 -0
  213. 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 the strip container (`role="tree"`, `data-orientation="horizontal"`)
37
- * into which every {@link MillerColumnsColumn | `MillerColumns.Column`}
38
- * projects itself via a portal. Authoring the tree is **recursive** and
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
- role="tree"
116
+ dir={resolvedDir}
96
117
  data-miller-columns-strip=""
97
118
  data-orientation="horizontal"
98
- {...rest}
119
+ {...containerProps}
99
120
  >
100
- {Array.from({ length: columnCount }, (_, depth) => (
101
- <div
102
- key={depth}
103
- data-miller-columns-slot=""
104
- style={{ display: "contents" }}
105
- ref={registerSlotRef(depth)}
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 } = useMillerColumnsColumn();
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>…</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 drag-to-resize affordance for the column it is rendered in. Renders a
303
- * `<div role="separator" aria-orientation="vertical">` that, while
304
- * pointer-dragged, drives that column's width as state on the `Root`.
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, sitting to the right of the columns. The component is
338
- * deliberately content-agnostic: it does not know how to preview an
339
- * item, so the consumer supplies whatever the panel should show.
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 data-miller-columns-preview="" {...rest}>
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 handle that sets its column's width |
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 — only one path is active at a time.
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
- - The strip is `role="tree"`.
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. A
160
- width is clamped so it can never be dragged below zero — use CSS
161
- `min-width` for any larger floor. Resize is pointer-only; the handle
162
- is not keyboard-operable.
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. The strip is a `role="tree"`, whose
203
- conforming children are `treeitem`s and `group`s — so give the panel
204
- content its own labelled landmark (`role`, `aria-label`, …) through
205
- props if the preview warrants being announced.
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>…</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 strip, each column is wrapped in a transparent
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-strip] > *`.
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
- | `Column` | `data-miller-columns-column`, `data-depth` |
262
- | `Item` | `data-state="selected" \| "unselected"`, `data-depth`, `data-has-children`, `data-disabled` |
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.