@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,79 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { SplitButton } from "../SplitButton";
|
|
5
|
+
|
|
6
|
+
describe("SplitButton — disabled", () => {
|
|
7
|
+
it("disables both halves when the group is disabled", () => {
|
|
8
|
+
render(
|
|
9
|
+
<SplitButton disabled>
|
|
10
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
11
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
12
|
+
<SplitButton.Menu />
|
|
13
|
+
</SplitButton>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(screen.getByRole("group")).toHaveAttribute("data-disabled", "");
|
|
17
|
+
expect(
|
|
18
|
+
screen.getByRole("button", { name: "Squash and merge" }),
|
|
19
|
+
).toBeDisabled();
|
|
20
|
+
expect(
|
|
21
|
+
screen.getByRole("button", { name: "More merge options" }),
|
|
22
|
+
).toBeDisabled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("disables one half independently, leaving the other interactive", () => {
|
|
26
|
+
render(
|
|
27
|
+
<SplitButton>
|
|
28
|
+
<SplitButton.Action disabled>Squash and merge</SplitButton.Action>
|
|
29
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
30
|
+
<SplitButton.Menu />
|
|
31
|
+
</SplitButton>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByRole("group")).not.toHaveAttribute("data-disabled");
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByRole("button", { name: "Squash and merge" }),
|
|
37
|
+
).toHaveAttribute("data-disabled", "");
|
|
38
|
+
expect(
|
|
39
|
+
screen.getByRole("button", { name: "More merge options" }),
|
|
40
|
+
).toBeEnabled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("disables only the menu half when the trigger is disabled", () => {
|
|
44
|
+
render(
|
|
45
|
+
<SplitButton>
|
|
46
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
47
|
+
<SplitButton.Trigger disabled aria-label="More merge options" />
|
|
48
|
+
<SplitButton.Menu />
|
|
49
|
+
</SplitButton>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
screen.getByRole("button", { name: "Squash and merge" }),
|
|
54
|
+
).toBeEnabled();
|
|
55
|
+
expect(
|
|
56
|
+
screen.getByRole("button", { name: "More merge options" }),
|
|
57
|
+
).toHaveAttribute("data-disabled", "");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("re-propagates when the group's disabled flips after mount", () => {
|
|
61
|
+
const markup = (disabled: boolean) => (
|
|
62
|
+
<SplitButton disabled={disabled}>
|
|
63
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
64
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
65
|
+
<SplitButton.Menu />
|
|
66
|
+
</SplitButton>
|
|
67
|
+
);
|
|
68
|
+
const { rerender } = render(markup(false));
|
|
69
|
+
|
|
70
|
+
rerender(markup(true));
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
screen.getByRole("button", { name: "Squash and merge" }),
|
|
74
|
+
).toBeDisabled();
|
|
75
|
+
expect(
|
|
76
|
+
screen.getByRole("button", { name: "More merge options" }),
|
|
77
|
+
).toBeDisabled();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { SplitButton } from "../SplitButton";
|
|
5
|
+
|
|
6
|
+
const outsideRoot =
|
|
7
|
+
"SplitButton sub-components must be rendered inside a <SplitButton.Root>.";
|
|
8
|
+
|
|
9
|
+
describe("SplitButton — error handling", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("throws when SplitButton.Action is used outside SplitButton.Root", () => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
render(<SplitButton.Action>Orphan</SplitButton.Action>),
|
|
21
|
+
).toThrowError(outsideRoot);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("throws when SplitButton.Trigger is used outside SplitButton.Root", () => {
|
|
25
|
+
expect(() => render(<SplitButton.Trigger aria-label="Orphan" />)).toThrowError(
|
|
26
|
+
outsideRoot,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { SplitButton } from "../SplitButton";
|
|
6
|
+
|
|
7
|
+
describe("SplitButton — keyboard interaction", () => {
|
|
8
|
+
it("opens the menu from the action with ArrowDown and focuses the first item", async () => {
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
render(
|
|
11
|
+
<SplitButton>
|
|
12
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
13
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
14
|
+
<SplitButton.Menu>
|
|
15
|
+
<SplitButton.Item>Create a merge commit</SplitButton.Item>
|
|
16
|
+
<SplitButton.Item>Rebase and merge</SplitButton.Item>
|
|
17
|
+
</SplitButton.Menu>
|
|
18
|
+
</SplitButton>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await user.tab();
|
|
22
|
+
await user.keyboard("{ArrowDown}");
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
screen.getByRole("button", { name: "More merge options" }),
|
|
26
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
27
|
+
expect(
|
|
28
|
+
screen.getByRole("menuitem", {
|
|
29
|
+
name: "Create a merge commit",
|
|
30
|
+
hidden: true,
|
|
31
|
+
}),
|
|
32
|
+
).toHaveFocus();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("leaves the menu closed for other keys, and still runs the action's own handler", async () => {
|
|
36
|
+
const user = userEvent.setup();
|
|
37
|
+
const onKeyDown = vi.fn();
|
|
38
|
+
render(
|
|
39
|
+
<SplitButton>
|
|
40
|
+
<SplitButton.Action onKeyDown={onKeyDown}>
|
|
41
|
+
Squash and merge
|
|
42
|
+
</SplitButton.Action>
|
|
43
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
44
|
+
<SplitButton.Menu>
|
|
45
|
+
<SplitButton.Item>Create a merge commit</SplitButton.Item>
|
|
46
|
+
</SplitButton.Menu>
|
|
47
|
+
</SplitButton>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await user.tab();
|
|
51
|
+
await user.keyboard("{ArrowUp}");
|
|
52
|
+
|
|
53
|
+
expect(onKeyDown).toHaveBeenCalledOnce();
|
|
54
|
+
expect(screen.getByRole("group")).toHaveAttribute("data-state", "closed");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Dropdown } from "../../Dropdown/index.ts";
|
|
6
|
+
import { SplitButton } from "../SplitButton";
|
|
7
|
+
|
|
8
|
+
describe("SplitButton — state modes", () => {
|
|
9
|
+
it("starts open in uncontrolled mode when defaultOpen is set", () => {
|
|
10
|
+
render(
|
|
11
|
+
<SplitButton defaultOpen>
|
|
12
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
13
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
14
|
+
<SplitButton.Menu>
|
|
15
|
+
<SplitButton.Item>Rebase and merge</SplitButton.Item>
|
|
16
|
+
</SplitButton.Menu>
|
|
17
|
+
</SplitButton>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(screen.getByRole("group")).toHaveAttribute("data-state", "open");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("defers to the parent in controlled mode", async () => {
|
|
24
|
+
const user = userEvent.setup();
|
|
25
|
+
const onOpenChange = vi.fn();
|
|
26
|
+
render(
|
|
27
|
+
<SplitButton open={false} onOpenChange={onOpenChange}>
|
|
28
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
29
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
30
|
+
<SplitButton.Menu>
|
|
31
|
+
<SplitButton.Item>Rebase and merge</SplitButton.Item>
|
|
32
|
+
</SplitButton.Menu>
|
|
33
|
+
</SplitButton>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await user.click(screen.getByRole("button", { name: "More merge options" }));
|
|
37
|
+
|
|
38
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
39
|
+
expect(screen.getByRole("group")).toHaveAttribute("data-state", "closed");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("passes the reading direction down to menus composed from Dropdown", async () => {
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
render(
|
|
45
|
+
<SplitButton dir="rtl" defaultOpen>
|
|
46
|
+
<SplitButton.Action>Squash and merge</SplitButton.Action>
|
|
47
|
+
<SplitButton.Trigger aria-label="More merge options" />
|
|
48
|
+
<SplitButton.Menu>
|
|
49
|
+
<Dropdown.Sub>
|
|
50
|
+
<Dropdown.SubTrigger>Merge strategies</Dropdown.SubTrigger>
|
|
51
|
+
<Dropdown.SubContent>
|
|
52
|
+
<SplitButton.Item>Rebase and merge</SplitButton.Item>
|
|
53
|
+
</Dropdown.SubContent>
|
|
54
|
+
</Dropdown.Sub>
|
|
55
|
+
</SplitButton.Menu>
|
|
56
|
+
</SplitButton>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await user.keyboard("{ArrowLeft}");
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
screen.getByRole("menuitem", {
|
|
63
|
+
name: "Merge strategies",
|
|
64
|
+
hidden: true,
|
|
65
|
+
}),
|
|
66
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import type { Direction } from "../DirectionProvider/index.ts";
|
|
4
|
+
import type {
|
|
5
|
+
DropdownContentProps,
|
|
6
|
+
DropdownItemProps,
|
|
7
|
+
DropdownSeparatorProps,
|
|
8
|
+
DropdownTriggerProps,
|
|
9
|
+
} from "../Dropdown/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Shared base for both {@link SplitButtonRootProps} variants — the native
|
|
13
|
+
* `<div>` attributes (minus the component-owned `role` and the narrowed
|
|
14
|
+
* `dir`) plus the `asChild` escape hatch, reading direction, and the
|
|
15
|
+
* group-level `disabled`.
|
|
16
|
+
*/
|
|
17
|
+
export type SplitButtonRootBaseProps = Omit<
|
|
18
|
+
ComponentProps<"div">,
|
|
19
|
+
"role" | "dir"
|
|
20
|
+
> & {
|
|
21
|
+
/**
|
|
22
|
+
* The widget's parts — typically one
|
|
23
|
+
* {@link SplitButtonActionProps | `SplitButton.Action`}, one
|
|
24
|
+
* {@link SplitButtonTriggerProps | `SplitButton.Trigger`}, and one
|
|
25
|
+
* {@link SplitButtonMenuProps | `SplitButton.Menu`}.
|
|
26
|
+
*/
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
/** Forwarded to the underlying `HTMLDivElement` (or to the `asChild`
|
|
29
|
+
* element). */
|
|
30
|
+
ref?: Ref<HTMLDivElement>;
|
|
31
|
+
/**
|
|
32
|
+
* When `true`, Root delegates rendering to a single consumer-supplied
|
|
33
|
+
* element via the {@link Slot} pattern instead of the default `<div>`;
|
|
34
|
+
* `role="group"`, `data-state`, `data-disabled` and the remaining props are
|
|
35
|
+
* merged onto it.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Reading direction for the menu. Only affects which arrow key opens or
|
|
41
|
+
* closes a submenu composed from `Dropdown.Sub` — `ArrowRight` opens in
|
|
42
|
+
* `"ltr"`, `ArrowLeft` in `"rtl"`. When omitted it is inherited from the
|
|
43
|
+
* nearest {@link DirectionProvider}, falling back to `"ltr"`.
|
|
44
|
+
*/
|
|
45
|
+
dir?: Direction;
|
|
46
|
+
/**
|
|
47
|
+
* Disables the whole widget — both the primary action and the menu trigger
|
|
48
|
+
* become non-interactive, and `data-disabled=""` is set on the group. OR-ed
|
|
49
|
+
* with each half's own `disabled`, so a disabled group can never be
|
|
50
|
+
* overridden back to enabled by a part.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Uncontrolled variant of {@link SplitButtonRootProps}: the component owns the
|
|
58
|
+
* menu's open state. Pass `defaultOpen` (or omit it); `onOpenChange` is
|
|
59
|
+
* optional and `open` is forbidden.
|
|
60
|
+
*/
|
|
61
|
+
export type SplitButtonRootUncontrolledProps = SplitButtonRootBaseProps & {
|
|
62
|
+
/**
|
|
63
|
+
* Whether the menu is open on first render. Omit to start closed. The
|
|
64
|
+
* component owns the state thereafter.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
defaultOpen?: boolean;
|
|
68
|
+
/** Forbidden in uncontrolled mode — use `defaultOpen` instead. */
|
|
69
|
+
open?: never;
|
|
70
|
+
/**
|
|
71
|
+
* Called whenever a user-driven transition opens or closes the menu
|
|
72
|
+
* (trigger click, `ArrowDown` on the action, Escape, outside click,
|
|
73
|
+
* selection). Optional in uncontrolled mode.
|
|
74
|
+
*/
|
|
75
|
+
onOpenChange?: (open: boolean) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Controlled variant of {@link SplitButtonRootProps}: the parent owns the
|
|
80
|
+
* menu's open state. Pass `open` and `onOpenChange` together; `defaultOpen` is
|
|
81
|
+
* forbidden.
|
|
82
|
+
*/
|
|
83
|
+
export type SplitButtonRootControlledProps = SplitButtonRootBaseProps & {
|
|
84
|
+
/** Forbidden in controlled mode — use `open` instead. */
|
|
85
|
+
defaultOpen?: never;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the menu is currently open. Must be kept in sync by the parent via
|
|
88
|
+
* `onOpenChange`; the component never mutates it internally.
|
|
89
|
+
*/
|
|
90
|
+
open: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Called whenever a user-driven transition would open or close the menu. The
|
|
93
|
+
* parent is responsible for reflecting the new value back into `open`.
|
|
94
|
+
* Required in controlled mode.
|
|
95
|
+
*/
|
|
96
|
+
onOpenChange: (open: boolean) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Props for {@link SplitButtonRoot | `SplitButton.Root`} — the
|
|
101
|
+
* controlled/uncontrolled discriminated union pairing
|
|
102
|
+
* {@link SplitButtonRootUncontrolledProps} with
|
|
103
|
+
* {@link SplitButtonRootControlledProps}, plus the shared group props.
|
|
104
|
+
*/
|
|
105
|
+
export type SplitButtonRootProps =
|
|
106
|
+
| SplitButtonRootUncontrolledProps
|
|
107
|
+
| SplitButtonRootControlledProps;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Props for the internal frame component that renders the group element. Root
|
|
111
|
+
* splits it out so the frame sits *inside* the `Dropdown.Root` it renders and
|
|
112
|
+
* can therefore read the menu's open state.
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export type SplitButtonFrameProps = Omit<
|
|
117
|
+
SplitButtonRootBaseProps,
|
|
118
|
+
"dir" | "disabled"
|
|
119
|
+
> & {
|
|
120
|
+
/** Already defaulted by Root, so the frame receives a definite boolean. */
|
|
121
|
+
disabled: boolean;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Value published by `SplitButtonContext` — the derived ids that wire the
|
|
126
|
+
* trigger's accessible name to the action, plus the group's disabled flag.
|
|
127
|
+
*
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
export type SplitButtonContextValue = {
|
|
131
|
+
/** DOM id applied to {@link SplitButtonActionProps | `SplitButton.Action`};
|
|
132
|
+
* referenced by the trigger's derived `aria-labelledby`. */
|
|
133
|
+
actionId: string;
|
|
134
|
+
/** DOM id applied to {@link SplitButtonTriggerProps | `SplitButton.Trigger`};
|
|
135
|
+
* the self-reference half of its derived `aria-labelledby`. */
|
|
136
|
+
triggerId: string;
|
|
137
|
+
/** Whether the whole widget is disabled (Root's `disabled` prop). */
|
|
138
|
+
disabled: boolean;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Props for {@link SplitButtonAction | `SplitButton.Action`} — the native
|
|
143
|
+
* `<button>` attributes minus `id`, which the component owns because the menu
|
|
144
|
+
* trigger's accessible name references it.
|
|
145
|
+
*/
|
|
146
|
+
export type SplitButtonActionProps = Omit<ComponentProps<"button">, "id"> & {
|
|
147
|
+
/** The action's visible label. Keep it real text — the menu trigger borrows
|
|
148
|
+
* it for its own accessible name. */
|
|
149
|
+
children?: ReactNode;
|
|
150
|
+
/** Forwarded to the underlying `HTMLButtonElement` (or to the `asChild`
|
|
151
|
+
* element). */
|
|
152
|
+
ref?: Ref<HTMLButtonElement>;
|
|
153
|
+
/**
|
|
154
|
+
* When `true`, renders the consumer's element instead of `<button>` via the
|
|
155
|
+
* {@link Slot} pattern. `type="button"` is **not** forwarded in this mode —
|
|
156
|
+
* the child owns its own type semantics.
|
|
157
|
+
* @default false
|
|
158
|
+
*/
|
|
159
|
+
asChild?: boolean;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Props for {@link SplitButtonTrigger | `SplitButton.Trigger`} —
|
|
164
|
+
* {@link DropdownTriggerProps} minus `id`, which the component owns because
|
|
165
|
+
* its derived `aria-labelledby` self-references it. Supply `aria-label` or
|
|
166
|
+
* `aria-labelledby` to opt out of the derived name.
|
|
167
|
+
*/
|
|
168
|
+
export type SplitButtonTriggerProps = Omit<DropdownTriggerProps, "id">;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Props for {@link SplitButtonMenu | `SplitButton.Menu`} — identical to
|
|
172
|
+
* {@link DropdownContentProps}; the menu *is* a `Dropdown.Content`.
|
|
173
|
+
*/
|
|
174
|
+
export type SplitButtonMenuProps = DropdownContentProps;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Props for {@link SplitButtonItem | `SplitButton.Item`} — identical to
|
|
178
|
+
* {@link DropdownItemProps}.
|
|
179
|
+
*/
|
|
180
|
+
export type SplitButtonItemProps = DropdownItemProps;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Props for {@link SplitButtonSeparator | `SplitButton.Separator`} — identical
|
|
184
|
+
* to {@link DropdownSeparatorProps}.
|
|
185
|
+
*/
|
|
186
|
+
export type SplitButtonSeparatorProps = DropdownSeparatorProps;
|
package/src/Switch/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Pass `checked` and `onCheckedChange` together. The parent owns the value.
|
|
|
45
45
|
```tsx
|
|
46
46
|
const [enabled, setEnabled] = useState(false);
|
|
47
47
|
|
|
48
|
-
<Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
48
|
+
<Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
49
49
|
<Switch.Thumb />
|
|
50
50
|
</Switch.Root>
|
|
51
51
|
```
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -25,7 +25,7 @@ function dataStateOf(checked: boolean) {
|
|
|
25
25
|
*
|
|
26
26
|
* Props routing: `className` / `style` style the **track** (the `<label>` you
|
|
27
27
|
* see — the input is hidden); every other prop (`name`, `value`, `id`,
|
|
28
|
-
* `aria-*`, `required`, `disabled`, `ref`,
|
|
28
|
+
* `aria-*`, `required`, `disabled`, `ref`, ...) spreads onto the input, because
|
|
29
29
|
* semantically the Root *is* the switch.
|
|
30
30
|
*
|
|
31
31
|
* Supports two state modes, statically discriminated at the type level:
|
|
@@ -59,7 +59,7 @@ function dataStateOf(checked: boolean) {
|
|
|
59
59
|
* ```tsx
|
|
60
60
|
* const [enabled, setEnabled] = useState(false);
|
|
61
61
|
*
|
|
62
|
-
* <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
62
|
+
* <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
63
63
|
* <Switch.Thumb />
|
|
64
64
|
* </Switch.Root>
|
|
65
65
|
* ```
|
package/src/Switch/types.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
|
|
|
8
8
|
*
|
|
9
9
|
* Prop routing: `className` / `style` style the **track** (the visible
|
|
10
10
|
* `<label>`, since the input is visually hidden); every other prop — `name`,
|
|
11
|
-
* `value`, `id`, `aria-*`, `required`, `disabled`, `ref`,
|
|
11
|
+
* `value`, `id`, `aria-*`, `required`, `disabled`, `ref`, ... — spreads onto the
|
|
12
12
|
* hidden `<input>`, because semantically the Root *is* the switch.
|
|
13
13
|
*/
|
|
14
14
|
export type SwitchRootBaseProps = Omit<
|
package/src/Table/README.md
CHANGED
|
@@ -51,6 +51,9 @@ import { Table } from "@primitiv-ui/react";
|
|
|
51
51
|
| `Table.Cell` | `<td>` | Data cell. Accepts `colSpan` and `rowSpan`. |
|
|
52
52
|
| `Table.ScrollArea` | `<div>` | Horizontal-scroll wrapper — see [Responsive scrolling](#responsive-scrolling). |
|
|
53
53
|
| `Table.Caption` | `<caption>` | Visible table label — see [Caption](#caption). |
|
|
54
|
+
| `Table.Expandable` | *nothing* | Pairs a row with its detail row — see [Expandable rows](#expandable-rows). |
|
|
55
|
+
| `Table.ExpandTrigger` | `<button>` | The row's disclosure control; owns `aria-expanded` / `aria-controls`. |
|
|
56
|
+
| `Table.DetailRow` | `<tr>` | The revealed panel, as a `<td colSpan>`. `gutter` offsets it to the first data column. |
|
|
54
57
|
|
|
55
58
|
## Accessible headers
|
|
56
59
|
|
|
@@ -96,14 +99,14 @@ the table via the CSS `caption-side` property. Defaults to `"bottom"`.
|
|
|
96
99
|
/* Caption below table (default) */
|
|
97
100
|
}
|
|
98
101
|
<Table.Root>
|
|
99
|
-
<Table.Caption>Q1 sales by region</Table.Caption
|
|
102
|
+
<Table.Caption>Q1 sales by region</Table.Caption>...
|
|
100
103
|
</Table.Root>;
|
|
101
104
|
|
|
102
105
|
{
|
|
103
106
|
/* Caption above table */
|
|
104
107
|
}
|
|
105
108
|
<Table.Root>
|
|
106
|
-
<Table.Caption captionSide="top">Q1 sales by region</Table.Caption
|
|
109
|
+
<Table.Caption captionSide="top">Q1 sales by region</Table.Caption>...
|
|
107
110
|
</Table.Root>;
|
|
108
111
|
```
|
|
109
112
|
|
|
@@ -111,7 +114,7 @@ When a visible caption is not desirable, use `aria-label` directly on
|
|
|
111
114
|
`Table.Root` instead:
|
|
112
115
|
|
|
113
116
|
```tsx
|
|
114
|
-
<Table.Root aria-label="Q1 sales by region"
|
|
117
|
+
<Table.Root aria-label="Q1 sales by region">...</Table.Root>
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
## Responsive scrolling
|
|
@@ -124,7 +127,7 @@ narrow viewports instead of overflowing or compressing columns.
|
|
|
124
127
|
|
|
125
128
|
```tsx
|
|
126
129
|
<Table.ScrollArea>
|
|
127
|
-
<Table.Root
|
|
130
|
+
<Table.Root>...</Table.Root>
|
|
128
131
|
</Table.ScrollArea>
|
|
129
132
|
```
|
|
130
133
|
|
|
@@ -133,7 +136,7 @@ base scroll styles, so you can layer additional styles freely:
|
|
|
133
136
|
|
|
134
137
|
```tsx
|
|
135
138
|
<Table.ScrollArea style={{ borderRadius: "8px" }}>
|
|
136
|
-
<Table.Root
|
|
139
|
+
<Table.Root>...</Table.Root>
|
|
137
140
|
</Table.ScrollArea>
|
|
138
141
|
```
|
|
139
142
|
|
|
@@ -144,14 +147,14 @@ separating logical row groups within a single table:
|
|
|
144
147
|
|
|
145
148
|
```tsx
|
|
146
149
|
<Table.Root>
|
|
147
|
-
<Table.Head
|
|
150
|
+
<Table.Head>...</Table.Head>
|
|
148
151
|
<Table.Body>
|
|
149
152
|
{/* Group 1 */}
|
|
150
|
-
<Table.Row
|
|
153
|
+
<Table.Row>...</Table.Row>
|
|
151
154
|
</Table.Body>
|
|
152
155
|
<Table.Body>
|
|
153
156
|
{/* Group 2 */}
|
|
154
|
-
<Table.Row
|
|
157
|
+
<Table.Row>...</Table.Row>
|
|
155
158
|
</Table.Body>
|
|
156
159
|
</Table.Root>
|
|
157
160
|
```
|
|
@@ -203,3 +206,102 @@ caption {
|
|
|
203
206
|
---
|
|
204
207
|
|
|
205
208
|
[Back to @primitiv-ui/react](../../README.md)
|
|
209
|
+
|
|
210
|
+
## Expandable rows
|
|
211
|
+
|
|
212
|
+
A row can open onto a detail panel. This is a **disclosure**, not a
|
|
213
|
+
[treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/): expanding into
|
|
214
|
+
hierarchical child *rows* is a different pattern with a full cell-navigation
|
|
215
|
+
keyboard model, and is deliberately out of scope here.
|
|
216
|
+
|
|
217
|
+
`Table.Expandable` renders no DOM of its own — it is a context provider, so both
|
|
218
|
+
`<tr>`s stay direct children of the `<tbody>` and the table's structure is
|
|
219
|
+
untouched.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<Table.Body>
|
|
223
|
+
<Table.Expandable expanded={open} onExpandedChange={setOpen}>
|
|
224
|
+
<Table.Row>
|
|
225
|
+
<Table.Cell>
|
|
226
|
+
<Table.ExpandTrigger>
|
|
227
|
+
<ChevronIcon aria-hidden="true" />
|
|
228
|
+
<span className="sr-only">Show details for Ada Lovelace</span>
|
|
229
|
+
</Table.ExpandTrigger>
|
|
230
|
+
</Table.Cell>
|
|
231
|
+
<Table.Cell>Ada Lovelace</Table.Cell>
|
|
232
|
+
</Table.Row>
|
|
233
|
+
|
|
234
|
+
<Table.DetailRow colSpan={2}>
|
|
235
|
+
<ContributorDetail />
|
|
236
|
+
</Table.DetailRow>
|
|
237
|
+
</Table.Expandable>
|
|
238
|
+
</Table.Body>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Controlled via `expanded` + `onExpandedChange` (what an external table engine
|
|
242
|
+
drives), or uncontrolled with `defaultExpanded`.
|
|
243
|
+
|
|
244
|
+
### Why the state is on the button
|
|
245
|
+
|
|
246
|
+
`role="row"` does list `aria-expanded` among its supported properties, but a
|
|
247
|
+
**button's** expanded state is announced universally while a row's is only
|
|
248
|
+
reliably announced inside a `role="treegrid"`. So the button owns
|
|
249
|
+
`aria-expanded` and `aria-controls`, and the detail row publishes the matching
|
|
250
|
+
`id`.
|
|
251
|
+
|
|
252
|
+
Give the trigger a name that identifies **its row**. A column of buttons all
|
|
253
|
+
called "Expand" is the same defect as a column of checkboxes all called
|
|
254
|
+
"Select": obvious on screen, useless in a screen reader's element list.
|
|
255
|
+
|
|
256
|
+
### `colSpan` is required
|
|
257
|
+
|
|
258
|
+
It must cover every column the table renders, control columns included. It is a
|
|
259
|
+
prop rather than a count taken from context on purpose: counting would mean the
|
|
260
|
+
table registering its own columns, and an external engine already knows the
|
|
261
|
+
number (TanStack Table: `table.getVisibleLeafColumns().length`).
|
|
262
|
+
|
|
263
|
+
### `gutter` aligns the panel with the first data column
|
|
264
|
+
|
|
265
|
+
A panel that starts at the table's edge, under the checkbox, reads as a new
|
|
266
|
+
section of the table rather than as an opened row. `gutter` is how many leading
|
|
267
|
+
columns to leave empty — the control columns in front of the data:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
{/* select + expand columns, then eight data columns */}
|
|
271
|
+
<Table.DetailRow colSpan={10} gutter={2}>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The panel spans what remains, so `colSpan` keeps its meaning: the table's full
|
|
275
|
+
column count, gutter included.
|
|
276
|
+
|
|
277
|
+
It renders **one** empty `<td colSpan={gutter}>`, not `gutter` of them — a
|
|
278
|
+
spanning cell takes the full width of the columns it covers, so the alignment is
|
|
279
|
+
identical and the row gains a single blank cell instead of several.
|
|
280
|
+
|
|
281
|
+
Aligning by empty cell rather than by a CSS indent is the point: the browser's
|
|
282
|
+
own table layout resolves the width, so it stays exact across every size,
|
|
283
|
+
density and control-column set. Indenting instead means summing control widths
|
|
284
|
+
and paddings by hand, which is right at one size and wrong at the other four —
|
|
285
|
+
measured drift of 7-21px across xs...xl before this existed.
|
|
286
|
+
|
|
287
|
+
### `forceMount` and the row count
|
|
288
|
+
|
|
289
|
+
By default a collapsed detail row is `hidden`, so it is absent from the
|
|
290
|
+
accessibility tree — which matters more here than on a `<div>`-based disclosure,
|
|
291
|
+
because a mounted `<tr>` is still a **row**: 24 data rows each with a
|
|
292
|
+
force-mounted detail row would announce 48.
|
|
293
|
+
|
|
294
|
+
Pass `forceMount` to keep it mounted so CSS can transition it. While collapsed it
|
|
295
|
+
then carries `aria-hidden="true"`, so the announced row count stays truthful
|
|
296
|
+
either way.
|
|
297
|
+
|
|
298
|
+
### Styling hooks
|
|
299
|
+
|
|
300
|
+
`data-state="open" | "closed"` on both the trigger and the detail row.
|
|
301
|
+
|
|
302
|
+
Note that the `display: grid` 0fr↔1fr open/close technique used by Accordion and
|
|
303
|
+
Collapsible **cannot** go on the `<tr>` or the `<td>` — `display: grid` on either
|
|
304
|
+
takes it out of the table layout algorithm and column alignment collapses with
|
|
305
|
+
it. Put the animated wrapper inside the cell, and zero the cell's block padding
|
|
306
|
+
so a closed row leaves no padded band. The registry `data-table` component does
|
|
307
|
+
this for you.
|