@primitiv-ui/react 0.1.29 → 0.1.31
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/RadioCard/README.md +2 -1
- package/src/RadioCard/RadioCard.tsx +5 -2
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +23 -0
- 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,143 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { Children, type ComponentProps, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { MillerColumns } from "../MillerColumns";
|
|
6
|
+
import { MILLER_COLUMNS_PART } from "../utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A stand-in for a styling layer's wrapper — exactly what the registry
|
|
10
|
+
* `miller-columns` surface renders. `Item` inspects its children *before*
|
|
11
|
+
* rendering them, so it sees this component, not the `MillerColumns.Column`
|
|
12
|
+
* it returns.
|
|
13
|
+
*/
|
|
14
|
+
function StyledColumn(props: ComponentProps<typeof MillerColumns.Column>) {
|
|
15
|
+
return <MillerColumns.Column {...props} />;
|
|
16
|
+
}
|
|
17
|
+
StyledColumn[MILLER_COLUMNS_PART] = "column" as const;
|
|
18
|
+
|
|
19
|
+
function Tree() {
|
|
20
|
+
return (
|
|
21
|
+
<MillerColumns.Root>
|
|
22
|
+
<MillerColumns.Column>
|
|
23
|
+
<MillerColumns.Item value="fruit">
|
|
24
|
+
Fruit
|
|
25
|
+
<StyledColumn>
|
|
26
|
+
<MillerColumns.Item value="apple">Apple</MillerColumns.Item>
|
|
27
|
+
</StyledColumn>
|
|
28
|
+
</MillerColumns.Item>
|
|
29
|
+
<MillerColumns.Item value="veg">
|
|
30
|
+
Veg
|
|
31
|
+
<StyledColumn>
|
|
32
|
+
<MillerColumns.Item value="carrot">Carrot</MillerColumns.Item>
|
|
33
|
+
</StyledColumn>
|
|
34
|
+
</MillerColumns.Item>
|
|
35
|
+
</MillerColumns.Column>
|
|
36
|
+
</MillerColumns.Root>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("MillerColumns — styled wrappers standing in for parts", () => {
|
|
41
|
+
it("keeps a wrapped child column closed until its branch is selected", async () => {
|
|
42
|
+
const user = userEvent.setup();
|
|
43
|
+
|
|
44
|
+
render(<Tree />);
|
|
45
|
+
|
|
46
|
+
// Nothing is selected, so the child column must not be mounted at all.
|
|
47
|
+
expect(
|
|
48
|
+
screen.queryByRole("treeitem", { name: "Apple" }),
|
|
49
|
+
).not.toBeInTheDocument();
|
|
50
|
+
|
|
51
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("opens only the selected branch's column, not every sibling's at once", async () => {
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
|
|
59
|
+
render(<Tree />);
|
|
60
|
+
|
|
61
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
62
|
+
|
|
63
|
+
// Selecting Fruit opens a slot at depth 1. Every unrecognised wrapped
|
|
64
|
+
// column projects into that slot on sight, so Veg's children ride along.
|
|
65
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toBeInTheDocument();
|
|
66
|
+
expect(
|
|
67
|
+
screen.queryByRole("treeitem", { name: "Carrot" }),
|
|
68
|
+
).not.toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("treats a wrapped child column as the branch's column, not cell content", () => {
|
|
72
|
+
render(<Tree />);
|
|
73
|
+
|
|
74
|
+
// A branch is a branch because it nests a column — even a wrapped one.
|
|
75
|
+
expect(screen.getByRole("treeitem", { name: "Fruit" })).toHaveAttribute(
|
|
76
|
+
"aria-expanded",
|
|
77
|
+
"false",
|
|
78
|
+
);
|
|
79
|
+
expect(screen.getByRole("treeitem", { name: "Fruit" })).toHaveAttribute(
|
|
80
|
+
"data-has-children",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The registry surface wraps BOTH parts, and its Item maps over `children` to
|
|
87
|
+
* wrap bare text in a label span before handing them to the headless. That map
|
|
88
|
+
* sits between the consumer's JSX and `partitionItemChildren`, so it is the
|
|
89
|
+
* thing most likely to lose a nested column.
|
|
90
|
+
*/
|
|
91
|
+
function wrapRowTextNodes(children: ReactNode): ReactNode {
|
|
92
|
+
const mapped = Children.map(children, (child) =>
|
|
93
|
+
typeof child === "string" || typeof child === "number" ? (
|
|
94
|
+
<span className="label">{child}</span>
|
|
95
|
+
) : (
|
|
96
|
+
child
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
return Array.isArray(mapped) && mapped.length === 1 ? mapped[0] : mapped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function StyledItem({
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: ComponentProps<typeof MillerColumns.Item>) {
|
|
106
|
+
return (
|
|
107
|
+
<MillerColumns.Item {...props}>
|
|
108
|
+
{wrapRowTextNodes(children)}
|
|
109
|
+
</MillerColumns.Item>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe("MillerColumns — the registry's own composition", () => {
|
|
114
|
+
it("opens only the selected branch when Item and Column are both wrapped", async () => {
|
|
115
|
+
const user = userEvent.setup();
|
|
116
|
+
|
|
117
|
+
render(
|
|
118
|
+
<MillerColumns.Root>
|
|
119
|
+
<MillerColumns.Column>
|
|
120
|
+
<StyledItem value="fruit">
|
|
121
|
+
Fruit
|
|
122
|
+
<StyledColumn>
|
|
123
|
+
<StyledItem value="apple">Apple</StyledItem>
|
|
124
|
+
</StyledColumn>
|
|
125
|
+
</StyledItem>
|
|
126
|
+
<StyledItem value="veg">
|
|
127
|
+
Veg
|
|
128
|
+
<StyledColumn>
|
|
129
|
+
<StyledItem value="carrot">Carrot</StyledItem>
|
|
130
|
+
</StyledColumn>
|
|
131
|
+
</StyledItem>
|
|
132
|
+
</MillerColumns.Column>
|
|
133
|
+
</MillerColumns.Root>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
137
|
+
|
|
138
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toBeInTheDocument();
|
|
139
|
+
expect(
|
|
140
|
+
screen.queryByRole("treeitem", { name: "Carrot" }),
|
|
141
|
+
).not.toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { TYPEAHEAD_RESET_MS } from "../constants";
|
|
5
|
+
import { MillerColumns } from "../MillerColumns";
|
|
6
|
+
|
|
7
|
+
function Tree() {
|
|
8
|
+
return (
|
|
9
|
+
<MillerColumns.Root>
|
|
10
|
+
<MillerColumns.Column>
|
|
11
|
+
<MillerColumns.Item value="apricot">Apricot</MillerColumns.Item>
|
|
12
|
+
<MillerColumns.Item value="banana">Banana</MillerColumns.Item>
|
|
13
|
+
<MillerColumns.Item value="blueberry">Blueberry</MillerColumns.Item>
|
|
14
|
+
<MillerColumns.Item value="cherry">
|
|
15
|
+
Cherry
|
|
16
|
+
<MillerColumns.Column>
|
|
17
|
+
<MillerColumns.Item value="morello">Morello</MillerColumns.Item>
|
|
18
|
+
<MillerColumns.Item value="maraschino">
|
|
19
|
+
Maraschino
|
|
20
|
+
</MillerColumns.Item>
|
|
21
|
+
</MillerColumns.Column>
|
|
22
|
+
</MillerColumns.Item>
|
|
23
|
+
</MillerColumns.Column>
|
|
24
|
+
</MillerColumns.Root>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("MillerColumns — typeahead", () => {
|
|
29
|
+
it("focuses the first item whose label starts with the typed character", async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
|
|
32
|
+
render(<Tree />);
|
|
33
|
+
screen.getByRole("treeitem", { name: "Apricot" }).focus();
|
|
34
|
+
|
|
35
|
+
await user.keyboard("c");
|
|
36
|
+
|
|
37
|
+
expect(screen.getByRole("treeitem", { name: "Cherry" })).toHaveFocus();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accumulates characters into a prefix query", async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
|
|
43
|
+
render(<Tree />);
|
|
44
|
+
screen.getByRole("treeitem", { name: "Apricot" }).focus();
|
|
45
|
+
|
|
46
|
+
await user.keyboard("bl");
|
|
47
|
+
|
|
48
|
+
expect(screen.getByRole("treeitem", { name: "Blueberry" })).toHaveFocus();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("cycles through matches when the same character repeats", async () => {
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
|
|
54
|
+
render(<Tree />);
|
|
55
|
+
screen.getByRole("treeitem", { name: "Apricot" }).focus();
|
|
56
|
+
|
|
57
|
+
await user.keyboard("b");
|
|
58
|
+
expect(screen.getByRole("treeitem", { name: "Banana" })).toHaveFocus();
|
|
59
|
+
|
|
60
|
+
await user.keyboard("b");
|
|
61
|
+
expect(screen.getByRole("treeitem", { name: "Blueberry" })).toHaveFocus();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("searches only within the focused column", async () => {
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
|
|
67
|
+
render(<Tree />);
|
|
68
|
+
await user.click(screen.getByRole("treeitem", { name: "Cherry" }));
|
|
69
|
+
screen.getByRole("treeitem", { name: "Morello" }).focus();
|
|
70
|
+
|
|
71
|
+
// "B" matches Banana and Blueberry, but both are in the parent column.
|
|
72
|
+
await user.keyboard("b");
|
|
73
|
+
expect(screen.getByRole("treeitem", { name: "Morello" })).toHaveFocus();
|
|
74
|
+
|
|
75
|
+
// Let the query lapse — otherwise the next keys extend it to "bma".
|
|
76
|
+
await new Promise((resolve) =>
|
|
77
|
+
setTimeout(resolve, TYPEAHEAD_RESET_MS + 100),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await user.keyboard("ma");
|
|
81
|
+
expect(screen.getByRole("treeitem", { name: "Maraschino" })).toHaveFocus();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("skips disabled items", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<MillerColumns.Root>
|
|
89
|
+
<MillerColumns.Column>
|
|
90
|
+
<MillerColumns.Item value="a">Apricot</MillerColumns.Item>
|
|
91
|
+
<MillerColumns.Item value="b1" disabled>
|
|
92
|
+
Banana
|
|
93
|
+
</MillerColumns.Item>
|
|
94
|
+
<MillerColumns.Item value="b2">Blueberry</MillerColumns.Item>
|
|
95
|
+
</MillerColumns.Column>
|
|
96
|
+
</MillerColumns.Root>,
|
|
97
|
+
);
|
|
98
|
+
screen.getByRole("treeitem", { name: "Apricot" }).focus();
|
|
99
|
+
|
|
100
|
+
await user.keyboard("b");
|
|
101
|
+
|
|
102
|
+
expect(screen.getByRole("treeitem", { name: "Blueberry" })).toHaveFocus();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -7,17 +7,20 @@ import type { MillerColumnsColumnContextValue } from "../types";
|
|
|
7
7
|
import { useMillerColumnsContext } from "./useMillerColumnsContext";
|
|
8
8
|
|
|
9
9
|
export function useMillerColumnsColumn() {
|
|
10
|
-
const { slots, columnWidths } = useMillerColumnsContext();
|
|
10
|
+
const { slots, columnWidths, getColumnItems } = useMillerColumnsContext();
|
|
11
11
|
const parentColumn = useContext(MillerColumnsColumnContext);
|
|
12
12
|
const depth = parentColumn ? parentColumn.depth + 1 : 0;
|
|
13
13
|
|
|
14
14
|
const slot = slots.get(depth) ?? null;
|
|
15
15
|
const width = columnWidths.get(depth);
|
|
16
|
+
// Selecting a childless branch opens a column with nothing in it; the
|
|
17
|
+
// styling layer needs to know so it can say so rather than show a void.
|
|
18
|
+
const empty = getColumnItems(depth).length === 0;
|
|
16
19
|
|
|
17
20
|
const columnContextValue = useMemo<MillerColumnsColumnContextValue>(
|
|
18
21
|
() => ({ depth }),
|
|
19
22
|
[depth],
|
|
20
23
|
);
|
|
21
24
|
|
|
22
|
-
return { slot, depth, width, columnContextValue };
|
|
25
|
+
return { slot, depth, width, empty, columnContextValue };
|
|
23
26
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
FocusEvent,
|
|
3
3
|
KeyboardEvent,
|
|
4
4
|
MouseEvent,
|
|
5
|
-
|
|
5
|
+
useLayoutEffect,
|
|
6
6
|
useMemo,
|
|
7
7
|
useRef,
|
|
8
8
|
} from "react";
|
|
@@ -31,6 +31,7 @@ export function useMillerColumnsItem(
|
|
|
31
31
|
hasChildren: boolean,
|
|
32
32
|
) {
|
|
33
33
|
const {
|
|
34
|
+
dir,
|
|
34
35
|
activePath,
|
|
35
36
|
select,
|
|
36
37
|
registerItem,
|
|
@@ -40,18 +41,26 @@ export function useMillerColumnsItem(
|
|
|
40
41
|
requestColumnFocus,
|
|
41
42
|
activeItem,
|
|
42
43
|
setActiveItem,
|
|
44
|
+
handleTypeahead,
|
|
43
45
|
} = useMillerColumnsContext();
|
|
44
46
|
const { depth } = useMillerColumnsColumnContext();
|
|
45
47
|
|
|
46
48
|
const selected = activePath[depth] === value;
|
|
49
|
+
// The deepest selected item — the one the user actually landed on, as
|
|
50
|
+
// opposed to the ancestors its path passes through. Nothing else in the
|
|
51
|
+
// DOM distinguishes the two, and styling them alike loses the depth cue.
|
|
52
|
+
const terminal = selected && depth === activePath.length - 1;
|
|
47
53
|
|
|
48
54
|
const itemRef = useRef<HTMLDivElement>(null);
|
|
49
55
|
const composedRef = ref ? composeRefs(itemRef, ref) : itemRef;
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
// Registration is a *layout* effect so the resulting state update lands
|
|
58
|
+
// before paint. As a passive effect, a populated column would paint one
|
|
59
|
+
// frame with `data-empty` before its items registered.
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
registerItem(depth, value, itemRef.current, disabled, hasChildren);
|
|
62
|
+
return () => registerItem(depth, value, null, disabled, hasChildren);
|
|
63
|
+
}, [depth, value, disabled, hasChildren, registerItem]);
|
|
55
64
|
|
|
56
65
|
const enabledValues = getColumnItems(depth)
|
|
57
66
|
.filter((item) => !item.disabled)
|
|
@@ -100,10 +109,15 @@ export function useMillerColumnsItem(
|
|
|
100
109
|
: depth === 0 &&
|
|
101
110
|
getColumnItems(0).find((item) => !item.disabled)?.value === value;
|
|
102
111
|
|
|
112
|
+
// Columns run left-to-right under ltr and right-to-left under rtl, so
|
|
113
|
+
// the key that steps *into* a child column mirrors with the direction.
|
|
114
|
+
const stepInKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
115
|
+
const stepOutKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
116
|
+
|
|
103
117
|
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
104
118
|
onKeyDown?.(event);
|
|
105
119
|
|
|
106
|
-
if (event.key ===
|
|
120
|
+
if (event.key === stepInKey) {
|
|
107
121
|
if (!hasChildren || disabled) {
|
|
108
122
|
return;
|
|
109
123
|
}
|
|
@@ -117,7 +131,7 @@ export function useMillerColumnsItem(
|
|
|
117
131
|
return;
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
if (event.key ===
|
|
134
|
+
if (event.key === stepOutKey) {
|
|
121
135
|
if (depth === 0) {
|
|
122
136
|
return;
|
|
123
137
|
}
|
|
@@ -129,6 +143,13 @@ export function useMillerColumnsItem(
|
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
rovingKeyDown(event);
|
|
146
|
+
|
|
147
|
+
// The keymap gets first refusal — it calls preventDefault on every key
|
|
148
|
+
// it consumes, so only a still-unhandled printable character reaches
|
|
149
|
+
// typeahead (Space activates, and never falls through).
|
|
150
|
+
if (!event.defaultPrevented) {
|
|
151
|
+
handleTypeahead(event);
|
|
152
|
+
}
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
const itemProps = {
|
|
@@ -139,6 +160,7 @@ export function useMillerColumnsItem(
|
|
|
139
160
|
"aria-level": depth + 1,
|
|
140
161
|
...(hasChildren ? { "aria-expanded": selected } : {}),
|
|
141
162
|
"data-state": selected ? "selected" : "unselected",
|
|
163
|
+
...(terminal ? { "data-terminal": "" } : {}),
|
|
142
164
|
"data-depth": depth,
|
|
143
165
|
...(hasChildren ? { "data-has-children": "" } : {}),
|
|
144
166
|
...(disabled ? { "aria-disabled": true, "data-disabled": "" } : {}),
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
KeyboardEvent,
|
|
3
|
+
PointerEvent,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
2
9
|
|
|
3
10
|
import type { MillerColumnsResizeHandleProps } from "../types";
|
|
4
11
|
|
|
@@ -9,12 +16,43 @@ type DragState = { startX: number; startWidth: number };
|
|
|
9
16
|
|
|
10
17
|
export function useMillerColumnsResizeHandle({
|
|
11
18
|
onPointerDown,
|
|
19
|
+
onKeyDown,
|
|
20
|
+
minWidth = 0,
|
|
21
|
+
maxWidth = Number.POSITIVE_INFINITY,
|
|
22
|
+
step = 10,
|
|
12
23
|
...rest
|
|
13
24
|
}: MillerColumnsResizeHandleProps) {
|
|
14
|
-
const { columnWidths, setColumnWidth } = useMillerColumnsContext();
|
|
25
|
+
const { columnWidths, setColumnWidth, dir } = useMillerColumnsContext();
|
|
15
26
|
const { depth } = useMillerColumnsColumnContext();
|
|
16
27
|
|
|
28
|
+
const handleRef = useRef<HTMLDivElement>(null);
|
|
17
29
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
30
|
+
// The column's rendered width before anything resizes it. Tracked so
|
|
31
|
+
// `aria-valuenow` is truthful from the first render — a splitter whose
|
|
32
|
+
// value only appears after the first drag announces nothing until then.
|
|
33
|
+
const [measured, setMeasured] = useState(0);
|
|
34
|
+
|
|
35
|
+
const clamp = (width: number) =>
|
|
36
|
+
Math.min(maxWidth, Math.max(minWidth, width));
|
|
37
|
+
|
|
38
|
+
// The resized width wins; otherwise the column is still sized by CSS.
|
|
39
|
+
const width = columnWidths.get(depth) ?? measured;
|
|
40
|
+
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
const column = handleRef.current?.closest<HTMLElement>(
|
|
43
|
+
"[data-miller-columns-column]",
|
|
44
|
+
);
|
|
45
|
+
/* c8 ignore next 3 -- the handle only renders inside a Column, which
|
|
46
|
+
context already enforces, so the ancestor is always present. */
|
|
47
|
+
if (!column) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const measure = () => setMeasured(column.getBoundingClientRect().width);
|
|
51
|
+
measure();
|
|
52
|
+
const observer = new ResizeObserver(measure);
|
|
53
|
+
observer.observe(column);
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
}, []);
|
|
18
56
|
|
|
19
57
|
// A drag is tracked on `window`, not the handle, so the pointer can
|
|
20
58
|
// travel anywhere once it is down; the listeners live only for the
|
|
@@ -27,13 +65,8 @@ export function useMillerColumnsResizeHandle({
|
|
|
27
65
|
const activeDrag = drag;
|
|
28
66
|
|
|
29
67
|
function handleMove(event: globalThis.PointerEvent) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Math.max(
|
|
33
|
-
0,
|
|
34
|
-
activeDrag.startWidth + event.clientX - activeDrag.startX,
|
|
35
|
-
),
|
|
36
|
-
);
|
|
68
|
+
const next = activeDrag.startWidth + event.clientX - activeDrag.startX;
|
|
69
|
+
setColumnWidth(depth, Math.min(maxWidth, Math.max(minWidth, next)));
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
function endDrag() {
|
|
@@ -48,7 +81,7 @@ export function useMillerColumnsResizeHandle({
|
|
|
48
81
|
window.removeEventListener("pointerup", endDrag);
|
|
49
82
|
window.removeEventListener("pointercancel", endDrag);
|
|
50
83
|
};
|
|
51
|
-
}, [drag, depth, setColumnWidth]);
|
|
84
|
+
}, [drag, depth, setColumnWidth, minWidth, maxWidth]);
|
|
52
85
|
|
|
53
86
|
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
|
54
87
|
onPointerDown?.(event);
|
|
@@ -63,13 +96,50 @@ export function useMillerColumnsResizeHandle({
|
|
|
63
96
|
setDrag({ startX: event.clientX, startWidth });
|
|
64
97
|
}
|
|
65
98
|
|
|
99
|
+
// The column extends towards the inline-end edge, which is leftwards
|
|
100
|
+
// under rtl — so the key that widens it mirrors with the direction.
|
|
101
|
+
const growKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
102
|
+
const shrinkKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
103
|
+
|
|
104
|
+
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
105
|
+
onKeyDown?.(event);
|
|
106
|
+
|
|
107
|
+
const next = (() => {
|
|
108
|
+
switch (event.key) {
|
|
109
|
+
case growKey:
|
|
110
|
+
return width + step;
|
|
111
|
+
case shrinkKey:
|
|
112
|
+
return width - step;
|
|
113
|
+
case "Home":
|
|
114
|
+
return minWidth;
|
|
115
|
+
case "End":
|
|
116
|
+
return maxWidth;
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
if (next === null || !Number.isFinite(next)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
setColumnWidth(depth, clamp(next));
|
|
128
|
+
}
|
|
129
|
+
|
|
66
130
|
return {
|
|
67
131
|
handleProps: {
|
|
132
|
+
ref: handleRef,
|
|
68
133
|
role: "separator" as const,
|
|
69
134
|
"aria-orientation": "vertical" as const,
|
|
135
|
+
tabIndex: 0,
|
|
136
|
+
"aria-valuenow": Math.round(width),
|
|
137
|
+
"aria-valuemin": minWidth,
|
|
138
|
+
...(Number.isFinite(maxWidth) ? { "aria-valuemax": maxWidth } : {}),
|
|
70
139
|
"data-miller-columns-resize-handle": "",
|
|
71
140
|
...(drag ? { "data-dragging": "" } : {}),
|
|
72
141
|
onPointerDown: handlePointerDown,
|
|
142
|
+
onKeyDown: handleKeyDown,
|
|
73
143
|
...rest,
|
|
74
144
|
},
|
|
75
145
|
};
|
|
Binary file
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import { ComponentProps, ReactNode, Ref } from "react";
|
|
1
|
+
import { ComponentProps, KeyboardEvent, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import type { Direction } from "../DirectionProvider/types";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Shared base for both {@link MillerColumnsRootProps} variants — the native
|
|
5
|
-
* `<div>` attributes (minus `ref`, which the strip owns internally
|
|
7
|
+
* `<div>` attributes (minus `ref`, which the strip owns internally, and
|
|
8
|
+
* `dir`, narrowed below to the {@link Direction} union).
|
|
6
9
|
*/
|
|
7
10
|
export type MillerColumnsRootBaseProps = Omit<
|
|
8
11
|
ComponentProps<"div">,
|
|
9
|
-
"ref" | "defaultValue"
|
|
12
|
+
"ref" | "defaultValue" | "dir"
|
|
10
13
|
> & {
|
|
14
|
+
/**
|
|
15
|
+
* Reading direction. Under `"rtl"` the horizontal arrow pair is
|
|
16
|
+
* mirrored — ArrowLeft steps *into* a branch's child column and
|
|
17
|
+
* ArrowRight walks back out — so navigation follows the visual order.
|
|
18
|
+
* Inherited from a `DirectionProvider` ancestor when omitted.
|
|
19
|
+
* @default "ltr"
|
|
20
|
+
*/
|
|
21
|
+
dir?: Direction;
|
|
11
22
|
/**
|
|
12
23
|
* The strip's tree, authored recursively: root
|
|
13
24
|
* {@link MillerColumnsColumnProps | Column(s)} whose branch
|
|
@@ -65,8 +76,10 @@ export type MillerColumnsRootProps =
|
|
|
65
76
|
*/
|
|
66
77
|
export type MillerColumnsColumnProps = ComponentProps<"div"> & {
|
|
67
78
|
/** The column's {@link MillerColumnsItemProps | Items} and, optionally, a
|
|
68
|
-
* {@link MillerColumnsResizeHandleProps | ResizeHandle}.
|
|
69
|
-
|
|
79
|
+
* {@link MillerColumnsResizeHandleProps | ResizeHandle}. Optional: a
|
|
80
|
+
* childless branch legitimately opens an empty column, which carries
|
|
81
|
+
* `data-empty` for the styling layer. */
|
|
82
|
+
children?: ReactNode;
|
|
70
83
|
};
|
|
71
84
|
|
|
72
85
|
/**
|
|
@@ -118,7 +131,26 @@ export type MillerColumnsItemIndicatorProps = ComponentProps<"span"> & {
|
|
|
118
131
|
* Props for {@link MillerColumnsResizeHandle | `MillerColumns.ResizeHandle`},
|
|
119
132
|
* the drag-to-resize separator for its containing column.
|
|
120
133
|
*/
|
|
121
|
-
export type MillerColumnsResizeHandleProps = ComponentProps<"div"
|
|
134
|
+
export type MillerColumnsResizeHandleProps = ComponentProps<"div"> & {
|
|
135
|
+
/**
|
|
136
|
+
* Smallest width the column can be dragged or stepped to, in pixels.
|
|
137
|
+
* Also published as `aria-valuemin`.
|
|
138
|
+
* @default 0
|
|
139
|
+
*/
|
|
140
|
+
minWidth?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Largest width the column can be dragged or stepped to, in pixels.
|
|
143
|
+
* Published as `aria-valuemax` when finite; left unbounded by default,
|
|
144
|
+
* in which case the attribute is omitted.
|
|
145
|
+
* @default Infinity
|
|
146
|
+
*/
|
|
147
|
+
maxWidth?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Pixels moved per arrow-key press.
|
|
150
|
+
* @default 10
|
|
151
|
+
*/
|
|
152
|
+
step?: number;
|
|
153
|
+
};
|
|
122
154
|
|
|
123
155
|
/**
|
|
124
156
|
* Props for {@link MillerColumnsPreviewPanel | `MillerColumns.PreviewPanel`},
|
|
@@ -128,6 +160,14 @@ export type MillerColumnsPreviewPanelProps = ComponentProps<"div"> & {
|
|
|
128
160
|
/** The preview content for the current selection (typically driven by
|
|
129
161
|
* {@link useMillerColumnsSelection}). */
|
|
130
162
|
children?: ReactNode;
|
|
163
|
+
/**
|
|
164
|
+
* Keep the panel mounted regardless of the selection, instead of following
|
|
165
|
+
* Finder's rule (a preview only where a selected *leaf* would open a child
|
|
166
|
+
* column). Use it for a persistent inspector, or to animate the panel in
|
|
167
|
+
* and out.
|
|
168
|
+
* @default false
|
|
169
|
+
*/
|
|
170
|
+
forceMount?: boolean;
|
|
131
171
|
};
|
|
132
172
|
|
|
133
173
|
/** The current selection, as returned by `useMillerColumnsSelection`. */
|
|
@@ -156,6 +196,8 @@ export type MillerColumnsItemAddress = {
|
|
|
156
196
|
|
|
157
197
|
/** Value shared through the root MillerColumns context. */
|
|
158
198
|
export type MillerColumnsContextValue = {
|
|
199
|
+
/** The resolved reading direction — the `dir` prop, else the inherited one. */
|
|
200
|
+
dir: Direction;
|
|
159
201
|
/** Portal slots keyed by column depth. */
|
|
160
202
|
slots: Map<number, HTMLElement>;
|
|
161
203
|
/** Per-depth column widths set via the resize handle. */
|
|
@@ -172,9 +214,15 @@ export type MillerColumnsContextValue = {
|
|
|
172
214
|
value: string,
|
|
173
215
|
element: HTMLElement | null,
|
|
174
216
|
disabled: boolean,
|
|
217
|
+
hasChildren: boolean,
|
|
175
218
|
) => void;
|
|
219
|
+
/** Whether the deepest selected item is a leaf — Finder's rule for whether a
|
|
220
|
+
* preview occupies the slot after it. `false` when nothing is selected. */
|
|
221
|
+
terminalIsLeaf: boolean;
|
|
176
222
|
/** Get the registered items for a column. */
|
|
177
223
|
getColumnItems: (depth: number) => MillerColumnsItemMeta[];
|
|
224
|
+
/** Read a registered item's rendered cell text, for typeahead matching. */
|
|
225
|
+
getItemLabel: (depth: number, value: string) => string;
|
|
178
226
|
/** Move focus to the given item. */
|
|
179
227
|
focusItem: (depth: number, value: string) => void;
|
|
180
228
|
/** Focus the first item in a column; returns whether one was focused. */
|
|
@@ -185,6 +233,8 @@ export type MillerColumnsContextValue = {
|
|
|
185
233
|
activeItem: MillerColumnsItemAddress | null;
|
|
186
234
|
/** Set the active (focused) item. */
|
|
187
235
|
setActiveItem: (item: MillerColumnsItemAddress) => void;
|
|
236
|
+
/** Printable-character typeahead, scoped to the focused item's column. */
|
|
237
|
+
handleTypeahead: (event: KeyboardEvent<HTMLElement>) => void;
|
|
188
238
|
};
|
|
189
239
|
|
|
190
240
|
/** Value shared through a column's context. */
|