@astryxdesign/core 0.1.7 → 0.1.8-canary.c5fa19b
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/CHANGELOG.md +65 -0
- package/README.md +30 -19
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -8
- package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
- package/dist/Avatar/AvatarSizeContext.js +1 -1
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
- package/dist/Avatar/AvatarStatusDot.js +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -2
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
- package/dist/Carousel/Carousel.d.ts +3 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +32 -2
- package/dist/Chat/ChatMessage.d.ts +1 -1
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- package/dist/Collapsible/Collapsible.d.ts +11 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +36 -4
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/ContextMenu/index.d.ts +3 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +7 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +89 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +170 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +70 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
- package/dist/DropdownMenu/index.d.ts +5 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +10 -1
- package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemRoles.js +15 -0
- package/dist/Icon/Icon.d.ts +43 -13
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +62 -27
- package/dist/Outline/Outline.d.ts +69 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +129 -26
- package/dist/Outline/useScrollSpy.d.ts +27 -7
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +202 -70
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +2 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/Table.d.ts +1 -1
- package/dist/Table/Table.js +1 -1
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
- package/dist/Table/plugins/tree/index.d.ts +5 -0
- package/dist/Table/plugins/tree/index.d.ts.map +1 -0
- package/dist/Table/plugins/tree/index.js +4 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +6 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +8 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +5 -14
- package/dist/astryx.css +24 -4
- package/dist/astryx.umd.js +54 -54
- package/dist/astryx.umd.js.map +4 -4
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +0 -3
- package/docs.mjs +32 -461
- package/locales/en.json +200 -172
- package/locales/pseudo.json +21 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/Avatar/Avatar.doc.mjs +6 -6
- package/src/Avatar/Avatar.tsx +16 -11
- package/src/Avatar/AvatarSizeContext.ts +1 -1
- package/src/Avatar/AvatarStatusDot.tsx +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
- package/src/AvatarGroup/AvatarGroup.tsx +3 -3
- package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +96 -1
- package/src/Carousel/Carousel.tsx +29 -2
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Citation/Citation.test.tsx +51 -0
- package/src/Citation/Citation.tsx +8 -2
- package/src/Collapsible/Collapsible.doc.mjs +7 -0
- package/src/Collapsible/Collapsible.test.tsx +80 -0
- package/src/Collapsible/Collapsible.tsx +58 -5
- package/src/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
- package/src/DropdownMenu/DropdownMenu.tsx +11 -6
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +237 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +119 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
- package/src/DropdownMenu/index.ts +23 -3
- package/src/DropdownMenu/menuItemRoles.ts +22 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/Icon/Icon.doc.mjs +12 -0
- package/src/Icon/Icon.test.tsx +142 -0
- package/src/Icon/Icon.tsx +98 -30
- package/src/Item/Item.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +184 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/TabList.tsx +8 -0
- package/src/TabList/TabMenu.tsx +4 -1
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/Table.tsx +1 -1
- package/src/Table/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
- package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
- package/src/Table/plugins/tree/index.ts +12 -0
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
- package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
- package/src/Table/useBaseTablePlugins.ts +6 -2
- package/src/Table/useTableTreeData.doc.mjs +75 -0
- package/src/Table/useTableTreeState.doc.mjs +98 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.tsx +10 -3
- package/src/TreeList/TreeList.test.tsx +32 -1
- package/src/TreeList/TreeListItem.tsx +19 -10
- package/src/docs-script.test.mjs +18 -13
- package/src/postinstall.test.mjs +71 -0
- package/src/theme/Theme.doc.mjs +4 -4
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/hct.ts +0 -4
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
- package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useTableTreeData.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, Table components
|
|
6
|
+
* @output Unit tests for the useTableTreeData plugin
|
|
7
|
+
* @position Testing; validates tree column decoration, expander behavior,
|
|
8
|
+
* row ARIA, and the flat-data migration no-op
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
12
|
+
import {render, screen, within} from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import {useState} from 'react';
|
|
15
|
+
import {Table} from '../../Table';
|
|
16
|
+
import type {TableColumn} from '../../types';
|
|
17
|
+
import {useTableSelection, useTableSelectionState} from '../selection';
|
|
18
|
+
import {useTableSortableState} from '../sortable';
|
|
19
|
+
import {useTableTreeData} from './useTableTreeData';
|
|
20
|
+
import {useTableTreeState} from './useTableTreeState';
|
|
21
|
+
import type {UseTableTreeStateConfig} from './useTableTreeState';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Test Data
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
interface FileRow extends Record<string, unknown> {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
size: number;
|
|
31
|
+
children?: FileRow[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fileTree: FileRow[] = [
|
|
35
|
+
{
|
|
36
|
+
id: 'src',
|
|
37
|
+
name: 'src',
|
|
38
|
+
size: 0,
|
|
39
|
+
children: [
|
|
40
|
+
{
|
|
41
|
+
id: 'components',
|
|
42
|
+
name: 'components',
|
|
43
|
+
size: 0,
|
|
44
|
+
children: [{id: 'button', name: 'Button.tsx', size: 120}],
|
|
45
|
+
},
|
|
46
|
+
{id: 'utils', name: 'utils.ts', size: 40},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{id: 'readme', name: 'README.md', size: 10},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const columns: TableColumn<FileRow>[] = [
|
|
53
|
+
{key: 'name', header: 'Name'},
|
|
54
|
+
{key: 'size', header: 'Size'},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function TreeTable(
|
|
58
|
+
props: Partial<UseTableTreeStateConfig<FileRow>> & {data?: FileRow[]},
|
|
59
|
+
) {
|
|
60
|
+
const {visibleData, treeConfig} = useTableTreeState<FileRow>({
|
|
61
|
+
data: props.data ?? fileTree,
|
|
62
|
+
idKey: 'id',
|
|
63
|
+
...props,
|
|
64
|
+
});
|
|
65
|
+
const tree = useTableTreeData(treeConfig);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Table data={visibleData} columns={columns} idKey="id" plugins={{tree}} />
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** All body rows (skips the header row). */
|
|
73
|
+
function getBodyRows(): HTMLElement[] {
|
|
74
|
+
return screen.getAllByRole('row').slice(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getRowByText(text: string): HTMLElement {
|
|
78
|
+
const row = getBodyRows().find(r => within(r).queryByText(text));
|
|
79
|
+
if (!row) {
|
|
80
|
+
throw new Error(`no row containing "${text}"`);
|
|
81
|
+
}
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Expander button
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
describe('useTableTreeData — expander', () => {
|
|
90
|
+
it('renders an "Expand row" button on collapsed expandable rows only', () => {
|
|
91
|
+
render(<TreeTable />);
|
|
92
|
+
|
|
93
|
+
const srcRow = getRowByText('src');
|
|
94
|
+
expect(
|
|
95
|
+
within(srcRow).getByRole('button', {name: 'Expand row'}),
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
|
|
98
|
+
const readmeRow = getRowByText('README.md');
|
|
99
|
+
expect(within(readmeRow).queryByRole('button')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('expands children on click and relabels the button "Collapse row"', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(<TreeTable />);
|
|
105
|
+
|
|
106
|
+
expect(screen.queryByText('components')).toBeNull();
|
|
107
|
+
|
|
108
|
+
await user.click(
|
|
109
|
+
within(getRowByText('src')).getByRole('button', {name: 'Expand row'}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('components')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('utils.ts')).toBeInTheDocument();
|
|
114
|
+
expect(
|
|
115
|
+
within(getRowByText('src')).getByRole('button', {name: 'Collapse row'}),
|
|
116
|
+
).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('collapses an expanded row on click, unmounting the subtree', async () => {
|
|
120
|
+
const user = userEvent.setup();
|
|
121
|
+
render(<TreeTable defaultExpandedIds={['src', 'components']} />);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('Button.tsx')).toBeInTheDocument();
|
|
124
|
+
|
|
125
|
+
await user.click(
|
|
126
|
+
within(getRowByText('src')).getByRole('button', {name: 'Collapse row'}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(screen.queryByText('components')).toBeNull();
|
|
130
|
+
expect(screen.queryByText('Button.tsx')).toBeNull();
|
|
131
|
+
// Unmounted, not hidden: only roots + header remain.
|
|
132
|
+
expect(screen.getAllByRole('row')).toHaveLength(3);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('sets aria-expanded on the expander button', async () => {
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
render(<TreeTable />);
|
|
138
|
+
|
|
139
|
+
const button = within(getRowByText('src')).getByRole('button', {
|
|
140
|
+
name: 'Expand row',
|
|
141
|
+
});
|
|
142
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
143
|
+
|
|
144
|
+
await user.click(button);
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
within(getRowByText('src')).getByRole('button', {name: 'Collapse row'}),
|
|
148
|
+
).toHaveAttribute('aria-expanded', 'true');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Row ARIA
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
describe('useTableTreeData — row ARIA', () => {
|
|
157
|
+
it('sets 1-based aria-level on every body row', () => {
|
|
158
|
+
render(<TreeTable defaultExpandedIds={['src', 'components']} />);
|
|
159
|
+
|
|
160
|
+
expect(getRowByText('src')).toHaveAttribute('aria-level', '1');
|
|
161
|
+
expect(getRowByText('components')).toHaveAttribute('aria-level', '2');
|
|
162
|
+
expect(getRowByText('Button.tsx')).toHaveAttribute('aria-level', '3');
|
|
163
|
+
expect(getRowByText('README.md')).toHaveAttribute('aria-level', '1');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('sets aria-expanded on expandable rows and omits it on leaves', () => {
|
|
167
|
+
render(<TreeTable defaultExpandedIds={['src']} />);
|
|
168
|
+
|
|
169
|
+
expect(getRowByText('src')).toHaveAttribute('aria-expanded', 'true');
|
|
170
|
+
expect(getRowByText('components')).toHaveAttribute(
|
|
171
|
+
'aria-expanded',
|
|
172
|
+
'false',
|
|
173
|
+
);
|
|
174
|
+
expect(getRowByText('utils.ts')).not.toHaveAttribute('aria-expanded');
|
|
175
|
+
expect(getRowByText('README.md')).not.toHaveAttribute('aria-expanded');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// =============================================================================
|
|
180
|
+
// Migration no-op (flat data)
|
|
181
|
+
// =============================================================================
|
|
182
|
+
|
|
183
|
+
describe('useTableTreeData — flat data is a no-op', () => {
|
|
184
|
+
const flat: FileRow[] = [
|
|
185
|
+
{id: 'a', name: 'alpha', size: 1},
|
|
186
|
+
{id: 'b', name: 'beta', size: 2},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
it('renders no expanders, spacers, or tree ARIA for flat data', () => {
|
|
190
|
+
render(<TreeTable data={flat} />);
|
|
191
|
+
|
|
192
|
+
expect(screen.queryByRole('button')).toBeNull();
|
|
193
|
+
for (const row of getBodyRows()) {
|
|
194
|
+
expect(row).not.toHaveAttribute('aria-level');
|
|
195
|
+
expect(row).not.toHaveAttribute('aria-expanded');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('renders first-column cell content identically to a plugin-free Table', () => {
|
|
200
|
+
const {container: withPlugin} = render(<TreeTable data={flat} />);
|
|
201
|
+
const pluginCell = withPlugin.querySelector('tbody td');
|
|
202
|
+
|
|
203
|
+
const {container: without} = render(
|
|
204
|
+
<Table data={flat} columns={columns} idKey="id" />,
|
|
205
|
+
);
|
|
206
|
+
const plainCell = without.querySelector('tbody td');
|
|
207
|
+
|
|
208
|
+
expect(pluginCell).toBeTruthy();
|
|
209
|
+
expect(plainCell).toBeTruthy();
|
|
210
|
+
expect(pluginCell?.innerHTML).toBe(plainCell?.innerHTML);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// Stability across data-shape changes
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
describe('useTableTreeData — stability when the data shape changes', () => {
|
|
219
|
+
const flat: FileRow[] = [
|
|
220
|
+
{id: 'a', name: 'alpha', size: 1},
|
|
221
|
+
{id: 'b', name: 'beta', size: 2},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
it('does not remount the table when flat data becomes nested', () => {
|
|
225
|
+
const {container, rerender} = render(<TreeTable data={flat} />);
|
|
226
|
+
const tableBefore = container.querySelector('table');
|
|
227
|
+
|
|
228
|
+
rerender(<TreeTable data={fileTree} />);
|
|
229
|
+
|
|
230
|
+
expect(container.querySelector('table')).toBe(tableBefore);
|
|
231
|
+
// The tree affordance appears without a remount.
|
|
232
|
+
expect(
|
|
233
|
+
screen.getAllByRole('button', {name: 'Expand row'}).length,
|
|
234
|
+
).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not warn about an empty plugin for flat data', () => {
|
|
238
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
239
|
+
try {
|
|
240
|
+
render(<TreeTable data={flat} />);
|
|
241
|
+
const pluginWarnings = warn.mock.calls.filter(args =>
|
|
242
|
+
String(args[0]).includes('no transform methods'),
|
|
243
|
+
);
|
|
244
|
+
expect(pluginWarnings).toHaveLength(0);
|
|
245
|
+
} finally {
|
|
246
|
+
warn.mockRestore();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('removes tree ARIA from rows when nested data becomes flat', () => {
|
|
251
|
+
const {rerender} = render(
|
|
252
|
+
<TreeTable data={fileTree} defaultExpandedIds={['src']} />,
|
|
253
|
+
);
|
|
254
|
+
expect(getRowByText('src')).toHaveAttribute('aria-level', '1');
|
|
255
|
+
|
|
256
|
+
rerender(<TreeTable data={flat} />);
|
|
257
|
+
|
|
258
|
+
for (const row of getBodyRows()) {
|
|
259
|
+
expect(row).not.toHaveAttribute('aria-level');
|
|
260
|
+
expect(row).not.toHaveAttribute('aria-expanded');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Toggle batching
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
describe('useTableTreeState — batched toggles', () => {
|
|
270
|
+
it('applies two different toggles landing in the same React batch', async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
|
|
273
|
+
function TwoTogglesTable() {
|
|
274
|
+
const {visibleData, treeConfig, expandAll} = useTableTreeState<FileRow>({
|
|
275
|
+
data: fileTree,
|
|
276
|
+
idKey: 'id',
|
|
277
|
+
});
|
|
278
|
+
const tree = useTableTreeData(treeConfig);
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
onClick={() => {
|
|
284
|
+
// Two commits in one event handler — one React batch.
|
|
285
|
+
treeConfig.onToggleItem(fileTree[0]); // expand 'src'
|
|
286
|
+
treeConfig.onToggleItem(fileTree[0].children![0]); // expand 'components'
|
|
287
|
+
}}>
|
|
288
|
+
expand two
|
|
289
|
+
</button>
|
|
290
|
+
<Table
|
|
291
|
+
data={visibleData}
|
|
292
|
+
columns={columns}
|
|
293
|
+
idKey="id"
|
|
294
|
+
plugins={{tree}}
|
|
295
|
+
/>
|
|
296
|
+
<button type="button" onClick={expandAll}>
|
|
297
|
+
noop
|
|
298
|
+
</button>
|
|
299
|
+
</>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
render(<TwoTogglesTable />);
|
|
304
|
+
|
|
305
|
+
await user.click(screen.getByRole('button', {name: 'expand two'}));
|
|
306
|
+
|
|
307
|
+
// Both toggles must survive the batch.
|
|
308
|
+
expect(screen.getByText('components')).toBeInTheDocument();
|
|
309
|
+
expect(screen.getByText('Button.tsx')).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// Indentation
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
describe('useTableTreeData — indentation', () => {
|
|
318
|
+
it('indents nested rows by calc(level * step) and leaves roots unindented', () => {
|
|
319
|
+
render(<TreeTable defaultExpandedIds={['src', 'components']} />);
|
|
320
|
+
|
|
321
|
+
const cellWrapper = (text: string) =>
|
|
322
|
+
within(getRowByText(text)).getByText(text).closest('td')
|
|
323
|
+
?.firstElementChild as HTMLElement;
|
|
324
|
+
|
|
325
|
+
expect(cellWrapper('src').getAttribute('style') ?? '').not.toContain(
|
|
326
|
+
'calc',
|
|
327
|
+
);
|
|
328
|
+
expect(cellWrapper('components').getAttribute('style')).toContain(
|
|
329
|
+
'calc(1 *',
|
|
330
|
+
);
|
|
331
|
+
expect(cellWrapper('Button.tsx').getAttribute('style')).toContain(
|
|
332
|
+
'calc(2 *',
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('respects the indent token size', () => {
|
|
337
|
+
render(<TreeTable defaultExpandedIds={['src']} indent="lg" />);
|
|
338
|
+
|
|
339
|
+
const wrapper = within(getRowByText('utils.ts'))
|
|
340
|
+
.getByText('utils.ts')
|
|
341
|
+
.closest('td')?.firstElementChild as HTMLElement;
|
|
342
|
+
// lg maps to the --spacing-6 token
|
|
343
|
+
expect(wrapper.getAttribute('style')).toContain('--spacing-6');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('never indents with a physical inline padding (logical property lives in the StyleX class)', () => {
|
|
347
|
+
// The indent is a StyleX dynamic style: the inline style carries only
|
|
348
|
+
// the CSS variable with the calc value; the paddingInlineStart property
|
|
349
|
+
// itself is compiled into the class. Guard against a regression to
|
|
350
|
+
// physical inline padding, which would not mirror in RTL.
|
|
351
|
+
render(<TreeTable defaultExpandedIds={['src']} />);
|
|
352
|
+
|
|
353
|
+
const wrapper = within(getRowByText('utils.ts'))
|
|
354
|
+
.getByText('utils.ts')
|
|
355
|
+
.closest('td')?.firstElementChild as HTMLElement;
|
|
356
|
+
const style = wrapper.getAttribute('style') ?? '';
|
|
357
|
+
expect(style).toContain('calc(1 *');
|
|
358
|
+
expect(style).not.toContain('padding-left');
|
|
359
|
+
expect(style).not.toContain('margin-left');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('supports deep nesting with no depth cap', () => {
|
|
363
|
+
// a > b > c > d > e (levels 0..4)
|
|
364
|
+
let node: FileRow = {id: 'e', name: 'leaf-e', size: 0};
|
|
365
|
+
for (const id of ['d', 'c', 'b', 'a']) {
|
|
366
|
+
node = {id, name: `node-${id}`, size: 0, children: [node]};
|
|
367
|
+
}
|
|
368
|
+
render(
|
|
369
|
+
<TreeTable data={[node]} defaultExpandedIds={['a', 'b', 'c', 'd']} />,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const leafRow = getRowByText('leaf-e');
|
|
373
|
+
expect(leafRow).toHaveAttribute('aria-level', '5');
|
|
374
|
+
const wrapper = within(leafRow).getByText('leaf-e').closest('td')
|
|
375
|
+
?.firstElementChild as HTMLElement;
|
|
376
|
+
expect(wrapper.getAttribute('style')).toContain('calc(4 *');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// =============================================================================
|
|
381
|
+
// treeColumnKey
|
|
382
|
+
// =============================================================================
|
|
383
|
+
|
|
384
|
+
describe('useTableTreeData — treeColumnKey', () => {
|
|
385
|
+
it('moves the expander into the configured column', () => {
|
|
386
|
+
render(<TreeTable treeColumnKey="size" />);
|
|
387
|
+
|
|
388
|
+
const srcRow = getRowByText('src');
|
|
389
|
+
const cells = within(srcRow).getAllByRole('cell');
|
|
390
|
+
// name column carries no expander; size column does
|
|
391
|
+
expect(within(cells[0]).queryByRole('button')).toBeNull();
|
|
392
|
+
expect(
|
|
393
|
+
within(cells[1]).getByRole('button', {name: 'Expand row'}),
|
|
394
|
+
).toBeInTheDocument();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('falls back to the first column when the configured column is absent', () => {
|
|
398
|
+
// e.g. columnSettings hid the configured tree column — the expander
|
|
399
|
+
// must not vanish while rows still announce aria-expanded.
|
|
400
|
+
render(<TreeTable treeColumnKey="not-a-column" />);
|
|
401
|
+
|
|
402
|
+
const srcRow = getRowByText('src');
|
|
403
|
+
const cells = within(srcRow).getAllByRole('cell');
|
|
404
|
+
expect(
|
|
405
|
+
within(cells[0]).getByRole('button', {name: 'Expand row'}),
|
|
406
|
+
).toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// =============================================================================
|
|
411
|
+
// Lazy loading
|
|
412
|
+
// =============================================================================
|
|
413
|
+
|
|
414
|
+
describe('useTableTreeData — lazy loading', () => {
|
|
415
|
+
it('shows an expander before children exist and reveals them once loaded', async () => {
|
|
416
|
+
const user = userEvent.setup();
|
|
417
|
+
|
|
418
|
+
function LazyTable() {
|
|
419
|
+
const [data, setData] = useState<FileRow[]>([
|
|
420
|
+
{id: 'folder', name: 'folder', size: 0},
|
|
421
|
+
]);
|
|
422
|
+
const {visibleData, treeConfig} = useTableTreeState<FileRow>({
|
|
423
|
+
data,
|
|
424
|
+
idKey: 'id',
|
|
425
|
+
isItemExpandable: item => item.id === 'folder',
|
|
426
|
+
onExpandedIdsChange: ids => {
|
|
427
|
+
if (ids.has('folder')) {
|
|
428
|
+
// Simulated fetch: children arrive after expansion.
|
|
429
|
+
setData([
|
|
430
|
+
{
|
|
431
|
+
id: 'folder',
|
|
432
|
+
name: 'folder',
|
|
433
|
+
size: 0,
|
|
434
|
+
children: [{id: 'lazy-child', name: 'lazy-child', size: 3}],
|
|
435
|
+
},
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
const tree = useTableTreeData(treeConfig);
|
|
441
|
+
return (
|
|
442
|
+
<Table
|
|
443
|
+
data={visibleData}
|
|
444
|
+
columns={columns}
|
|
445
|
+
idKey="id"
|
|
446
|
+
plugins={{tree}}
|
|
447
|
+
/>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
render(<LazyTable />);
|
|
452
|
+
|
|
453
|
+
await user.click(screen.getByRole('button', {name: 'Expand row'}));
|
|
454
|
+
|
|
455
|
+
expect(screen.getByText('lazy-child')).toBeInTheDocument();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// =============================================================================
|
|
460
|
+
// Degenerate configurations
|
|
461
|
+
// =============================================================================
|
|
462
|
+
|
|
463
|
+
describe('useTableTreeData — degenerate configurations', () => {
|
|
464
|
+
it('renders without crashing when the table has zero columns', () => {
|
|
465
|
+
function ZeroColumnsTable() {
|
|
466
|
+
const {visibleData, treeConfig} = useTableTreeState<FileRow>({
|
|
467
|
+
data: fileTree,
|
|
468
|
+
idKey: 'id',
|
|
469
|
+
});
|
|
470
|
+
const tree = useTableTreeData(treeConfig);
|
|
471
|
+
return (
|
|
472
|
+
<Table data={visibleData} columns={[]} idKey="id" plugins={{tree}} />
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
expect(() => render(<ZeroColumnsTable />)).not.toThrow();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('updates aria-level in place when a row is reparented deeper', () => {
|
|
480
|
+
const flat: FileRow[] = [
|
|
481
|
+
{
|
|
482
|
+
id: 'a',
|
|
483
|
+
name: 'alpha',
|
|
484
|
+
size: 0,
|
|
485
|
+
children: [{id: 'k', name: 'kid', size: 1}],
|
|
486
|
+
},
|
|
487
|
+
{id: 'm', name: 'mover', size: 2},
|
|
488
|
+
];
|
|
489
|
+
const nested: FileRow[] = [
|
|
490
|
+
{
|
|
491
|
+
id: 'a',
|
|
492
|
+
name: 'alpha',
|
|
493
|
+
size: 0,
|
|
494
|
+
children: [
|
|
495
|
+
{id: 'k', name: 'kid', size: 1},
|
|
496
|
+
{id: 'm', name: 'mover', size: 2},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const {rerender} = render(
|
|
502
|
+
<TreeTable data={flat} defaultExpandedIds={['a']} />,
|
|
503
|
+
);
|
|
504
|
+
expect(getRowByText('mover')).toHaveAttribute('aria-level', '1');
|
|
505
|
+
|
|
506
|
+
rerender(<TreeTable data={nested} defaultExpandedIds={['a']} />);
|
|
507
|
+
|
|
508
|
+
expect(getRowByText('mover')).toHaveAttribute('aria-level', '2');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// =============================================================================
|
|
513
|
+
// Composition with selection
|
|
514
|
+
// =============================================================================
|
|
515
|
+
|
|
516
|
+
describe('useTableTreeData — composition with selection', () => {
|
|
517
|
+
it('prepends the selection checkbox column before the tree column', () => {
|
|
518
|
+
function ComposedTable() {
|
|
519
|
+
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(
|
|
520
|
+
() => new Set(),
|
|
521
|
+
);
|
|
522
|
+
const {visibleData, treeConfig} = useTableTreeState<FileRow>({
|
|
523
|
+
data: fileTree,
|
|
524
|
+
idKey: 'id',
|
|
525
|
+
defaultExpandedIds: ['src'],
|
|
526
|
+
});
|
|
527
|
+
const {selectionConfig} = useTableSelectionState({
|
|
528
|
+
data: visibleData,
|
|
529
|
+
idKey: 'id',
|
|
530
|
+
selectedKeys,
|
|
531
|
+
setSelectedKeys,
|
|
532
|
+
});
|
|
533
|
+
const tree = useTableTreeData(treeConfig);
|
|
534
|
+
const selection = useTableSelection(selectionConfig);
|
|
535
|
+
return (
|
|
536
|
+
<Table
|
|
537
|
+
data={visibleData}
|
|
538
|
+
columns={columns}
|
|
539
|
+
idKey="id"
|
|
540
|
+
plugins={{tree, selection}}
|
|
541
|
+
/>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
render(<ComposedTable />);
|
|
546
|
+
|
|
547
|
+
const srcRow = getRowByText('src');
|
|
548
|
+
const cells = within(srcRow).getAllByRole('cell');
|
|
549
|
+
// Checkbox column first, then the tree (name) column with the expander.
|
|
550
|
+
expect(within(cells[0]).getByRole('checkbox')).toBeInTheDocument();
|
|
551
|
+
expect(
|
|
552
|
+
within(cells[1]).getByRole('button', {name: 'Collapse row'}),
|
|
553
|
+
).toBeInTheDocument();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// =============================================================================
|
|
558
|
+
// Composition with sorting
|
|
559
|
+
// =============================================================================
|
|
560
|
+
|
|
561
|
+
describe('useTableTreeData — composition with sorting', () => {
|
|
562
|
+
it('sorts sibling groups via applySort without interleaving levels', () => {
|
|
563
|
+
function SortedTree() {
|
|
564
|
+
const {applySort} = useTableSortableState<FileRow>({
|
|
565
|
+
data: fileTree,
|
|
566
|
+
defaultSort: [{sortKey: 'name', direction: 'descending'}],
|
|
567
|
+
});
|
|
568
|
+
const {visibleData, treeConfig} = useTableTreeState<FileRow>({
|
|
569
|
+
data: fileTree,
|
|
570
|
+
idKey: 'id',
|
|
571
|
+
defaultExpandedIds: ['src'],
|
|
572
|
+
sortSiblings: applySort,
|
|
573
|
+
});
|
|
574
|
+
const tree = useTableTreeData(treeConfig);
|
|
575
|
+
return (
|
|
576
|
+
<Table
|
|
577
|
+
data={visibleData}
|
|
578
|
+
columns={columns}
|
|
579
|
+
idKey="id"
|
|
580
|
+
plugins={{tree}}
|
|
581
|
+
/>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
render(<SortedTree />);
|
|
586
|
+
|
|
587
|
+
const names = getBodyRows().map(
|
|
588
|
+
row => within(row).getAllByRole('cell')[0].textContent,
|
|
589
|
+
);
|
|
590
|
+
// Roots desc: src > README.md; src's children desc: utils.ts > components.
|
|
591
|
+
// Children stay directly under their parent.
|
|
592
|
+
expect(names).toEqual(['src', 'utils.ts', 'components', 'README.md']);
|
|
593
|
+
});
|
|
594
|
+
});
|