@astryxdesign/core 0.1.3 → 0.1.4-canary.02643db
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 +51 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +29 -8
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +49 -20
- package/dist/Calendar/dayCellUtils.d.ts +67 -5
- package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
- package/dist/Calendar/dayCellUtils.js +139 -18
- package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +7 -1
- package/dist/Code/Code.d.ts +15 -1
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +32 -2
- package/dist/Code/index.d.ts +1 -1
- package/dist/Code/index.d.ts.map +1 -1
- package/dist/CodeBlock/index.d.ts +1 -1
- package/dist/CodeBlock/index.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
- package/dist/ContextMenu/ContextMenu.d.ts +9 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +6 -3
- package/dist/DateRangeInput/DateRangeInput.js +2 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/Divider/Divider.d.ts +0 -22
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +7 -0
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +300 -6
- package/dist/MultiSelector/MultiSelector.d.ts +7 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/RadioList/RadioList.d.ts +7 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +6 -1
- package/dist/Selector/Selector.d.ts +6 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +9 -0
- package/dist/Slider/Slider.d.ts +6 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +10 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +1 -1
- package/dist/Switch/Switch.d.ts +6 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +7 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -1
- package/dist/Table/TableCell.d.ts.map +1 -1
- package/dist/Table/TableCell.js +73 -4
- 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/index.d.ts +3 -0
- package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/index.js +3 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
- package/dist/Table/tableContextMenu.d.ts +5 -1
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +10 -3
- package/dist/Text/Text.d.ts +1 -1
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -3
- package/dist/Text/text.stylex.d.ts +35 -0
- package/dist/Text/text.stylex.d.ts.map +1 -1
- package/dist/Text/text.stylex.js +52 -1
- package/dist/Tokenizer/Tokenizer.d.ts +6 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +10 -1
- package/dist/astryx.css +16 -0
- package/dist/astryx.umd.js +51 -49
- package/dist/astryx.umd.js.map +4 -4
- package/dist/docs-types.d.ts +12 -0
- package/dist/docs-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
- package/src/Avatar/Avatar.doc.mjs +2 -2
- package/src/Button/Button.tsx +39 -6
- package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +106 -0
- package/src/Calendar/Calendar.tsx +59 -22
- package/src/Calendar/dayCellUtils.test.ts +306 -1
- package/src/Calendar/dayCellUtils.ts +206 -13
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/useChatNewMessages.test.tsx +4 -4
- package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
- package/src/CheckboxInput/CheckboxInput.tsx +11 -0
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/Code/Code.test.tsx +54 -0
- package/src/Code/Code.tsx +48 -4
- package/src/Code/index.ts +1 -1
- package/src/CodeBlock/index.ts +1 -1
- package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
- package/src/ContextMenu/ContextMenu.tsx +17 -1
- package/src/DateInput/DateInput.doc.mjs +6 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.tsx +1 -6
- package/src/Divider/Divider.tsx +1 -23
- package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
- package/src/EmptyState/EmptyState.doc.mjs +1 -1
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.doc.mjs +3 -3
- package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
- package/src/Markdown/Markdown.test.tsx +23 -0
- package/src/Markdown/incremental.test.ts +36 -0
- package/src/Markdown/parser.test.ts +210 -0
- package/src/Markdown/parser.ts +301 -6
- package/src/MobileNav/MobileNav.doc.mjs +20 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
- package/src/MultiSelector/MultiSelector.test.tsx +33 -0
- package/src/MultiSelector/MultiSelector.tsx +20 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/RadioList/RadioList.doc.mjs +7 -1
- package/src/RadioList/RadioList.test.tsx +46 -0
- package/src/RadioList/RadioList.tsx +10 -1
- package/src/RadioList/RadioListItem.tsx +4 -0
- package/src/Section/Section.test.tsx +4 -10
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +12 -1
- package/src/Selector/Selector.test.tsx +30 -0
- package/src/Selector/Selector.tsx +18 -0
- package/src/Slider/Slider.doc.mjs +14 -2
- package/src/Slider/Slider.test.tsx +30 -0
- package/src/Slider/Slider.tsx +20 -0
- package/src/Spinner/Spinner.tsx +1 -0
- package/src/Switch/Switch.doc.mjs +14 -2
- package/src/Switch/Switch.test.tsx +38 -0
- package/src/Switch/Switch.tsx +12 -0
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/Table/BaseTable.tsx +9 -1
- package/src/Table/Table.test.tsx +23 -0
- package/src/Table/TableCell.tsx +87 -8
- package/src/Table/index.ts +5 -0
- package/src/Table/plugins/rowExpansion/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
- package/src/Table/tableContextMenu.test.tsx +27 -3
- package/src/Table/tableContextMenu.tsx +20 -2
- package/src/Table/useTablePagination.doc.mjs +4 -1
- package/src/Table/useTableRowExpansion.doc.mjs +80 -0
- package/src/Table/useTableSortable.doc.mjs +4 -1
- package/src/Text/Text.doc.mjs +2 -2
- package/src/Text/Text.test.tsx +11 -0
- package/src/Text/Text.tsx +4 -2
- package/src/Text/text.stylex.ts +41 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +11 -6
- package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
- package/src/Tokenizer/Tokenizer.test.tsx +33 -0
- package/src/Tokenizer/Tokenizer.tsx +19 -0
- package/src/Toolbar/Toolbar.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
- package/src/Typeahead/Typeahead.doc.mjs +6 -1
- package/src/docs-types.ts +12 -0
- package/src/hooks/useKeyboardHint.doc.mjs +6 -6
package/src/Switch/Switch.tsx
CHANGED
|
@@ -228,6 +228,13 @@ export interface SwitchProps extends Omit<BaseProps, 'onChange'> {
|
|
|
228
228
|
* @default false
|
|
229
229
|
*/
|
|
230
230
|
isDisabled?: boolean;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The HTML name attribute for the underlying checkbox input.
|
|
234
|
+
* Useful for form submissions.
|
|
235
|
+
*/
|
|
236
|
+
htmlName?: string;
|
|
237
|
+
|
|
231
238
|
/**
|
|
232
239
|
* Explains why the switch is disabled. When set together with `isDisabled`,
|
|
233
240
|
* the switch shows a tooltip with this text on hover and keyboard focus, and
|
|
@@ -334,6 +341,7 @@ export function Switch({
|
|
|
334
341
|
isLoading = false,
|
|
335
342
|
value,
|
|
336
343
|
isDisabled = false,
|
|
344
|
+
htmlName,
|
|
337
345
|
disabledMessage,
|
|
338
346
|
isOptional = false,
|
|
339
347
|
isRequired = false,
|
|
@@ -395,6 +403,10 @@ export function Switch({
|
|
|
395
403
|
id={id}
|
|
396
404
|
type="checkbox"
|
|
397
405
|
role="switch"
|
|
406
|
+
// Withhold the name while disabled: with a disabledMessage the
|
|
407
|
+
// input stays focusable (not natively disabled), and a disabled
|
|
408
|
+
// control must not submit.
|
|
409
|
+
name={isDisabled ? undefined : htmlName}
|
|
398
410
|
checked={isOn}
|
|
399
411
|
// With a disabledMessage the switch keeps focusability via aria-disabled
|
|
400
412
|
// so the reason is focus-discoverable; toggling is still blocked by the
|
|
@@ -92,7 +92,7 @@ export const docs = {
|
|
|
92
92
|
bestPractices: [
|
|
93
93
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
94
94
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
95
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\
|
|
95
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\'t overpower the tab row.' },
|
|
96
96
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
97
97
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
98
98
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
|
@@ -113,7 +113,7 @@ export const docsZh = {
|
|
|
113
113
|
bestPractices: [
|
|
114
114
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
115
115
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
116
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\
|
|
116
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\'t overpower the tab row.' },
|
|
117
117
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
118
118
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
119
119
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
|
@@ -135,7 +135,7 @@ export const docsDense = {
|
|
|
135
135
|
bestPractices: [
|
|
136
136
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
137
137
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
138
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\
|
|
138
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, use a smaller button size (sm) so the actions don\'t overpower the tab row.' },
|
|
139
139
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
140
140
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
141
141
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
package/src/Table/BaseTable.tsx
CHANGED
|
@@ -225,7 +225,7 @@ function TableRowInner<T extends Record<string, unknown>>({
|
|
|
225
225
|
rowIndex,
|
|
226
226
|
);
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
const row = (
|
|
229
229
|
<RowComponent
|
|
230
230
|
key={rowKey}
|
|
231
231
|
ref={rowRenderProps.ref}
|
|
@@ -234,6 +234,8 @@ function TableRowInner<T extends Record<string, unknown>>({
|
|
|
234
234
|
{rowRenderProps.children}
|
|
235
235
|
</RowComponent>
|
|
236
236
|
);
|
|
237
|
+
|
|
238
|
+
return row;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
/**
|
|
@@ -282,6 +284,12 @@ function areRowPropsEqual<T extends Record<string, unknown>>(
|
|
|
282
284
|
const nextItem = nextProps.item;
|
|
283
285
|
const keys = Object.keys(nextItem);
|
|
284
286
|
|
|
287
|
+
// A key deleted from nextItem would never be visited by the loop below,
|
|
288
|
+
// and the row would keep rendering the removed field's stale value.
|
|
289
|
+
if (Object.keys(prevItem).length !== keys.length) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
285
293
|
for (const key of keys) {
|
|
286
294
|
if (prevItem[key] !== nextItem[key]) {
|
|
287
295
|
return false;
|
package/src/Table/Table.test.tsx
CHANGED
|
@@ -617,6 +617,29 @@ describe('BaseTable', () => {
|
|
|
617
617
|
// =============================================================================
|
|
618
618
|
|
|
619
619
|
describe('Table', () => {
|
|
620
|
+
it('clears a cell when the field is removed from the row object (#3595)', () => {
|
|
621
|
+
const cols: TableColumn<Record<string, unknown>>[] = [
|
|
622
|
+
{key: 'name', header: 'Name'},
|
|
623
|
+
{key: 'status', header: 'Status'},
|
|
624
|
+
];
|
|
625
|
+
const {rerender} = render(
|
|
626
|
+
<Table
|
|
627
|
+
data={[{id: '1', name: 'Alice', status: 'active'}]}
|
|
628
|
+
columns={cols}
|
|
629
|
+
idKey="id"
|
|
630
|
+
/>,
|
|
631
|
+
);
|
|
632
|
+
expect(screen.getByText('active')).toBeInTheDocument();
|
|
633
|
+
|
|
634
|
+
// Clearing a field by omitting it (optimistic update / server response)
|
|
635
|
+
// must re-render the row — the memo previously only compared the keys of
|
|
636
|
+
// the NEW item, so the deleted field's stale value kept rendering.
|
|
637
|
+
rerender(
|
|
638
|
+
<Table data={[{id: '1', name: 'Alice'}]} columns={cols} idKey="id" />,
|
|
639
|
+
);
|
|
640
|
+
expect(screen.queryByText('active')).not.toBeInTheDocument();
|
|
641
|
+
});
|
|
642
|
+
|
|
620
643
|
it('renders a table with correct structure', () => {
|
|
621
644
|
render(<Table data={users} columns={columns} />);
|
|
622
645
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
package/src/Table/TableCell.tsx
CHANGED
|
@@ -87,6 +87,62 @@ const densityStyles = stylex.create({
|
|
|
87
87
|
},
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
// When a cell owns a context menu, its padding moves from the `<td>` onto the
|
|
91
|
+
// right-click trigger wrapper so the *entire* cell (padding included) opens the
|
|
92
|
+
// menu. Without this, right-clicking the padding ring near a cell edge falls
|
|
93
|
+
// through to the browser's native menu. Paired with `contextMenuCellStyles` on
|
|
94
|
+
// the `<td>` (font/box-sizing only — no padding).
|
|
95
|
+
const densityTextStyles = stylex.create({
|
|
96
|
+
compact: {
|
|
97
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
98
|
+
boxSizing: 'border-box',
|
|
99
|
+
},
|
|
100
|
+
balanced: {
|
|
101
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
102
|
+
boxSizing: 'border-box',
|
|
103
|
+
},
|
|
104
|
+
spacious: {
|
|
105
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
106
|
+
boxSizing: 'border-box',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const densityPaddingStyles = stylex.create({
|
|
111
|
+
compact: {
|
|
112
|
+
paddingBlock: spacingVars['--spacing-1'],
|
|
113
|
+
paddingInline: spacingVars['--spacing-2'],
|
|
114
|
+
},
|
|
115
|
+
balanced: {
|
|
116
|
+
paddingBlock: spacingVars['--spacing-2'],
|
|
117
|
+
paddingInline: spacingVars['--spacing-3'],
|
|
118
|
+
},
|
|
119
|
+
spacious: {
|
|
120
|
+
paddingBlock: spacingVars['--spacing-3'],
|
|
121
|
+
paddingInline: spacingVars['--spacing-4'],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// The trigger wrapper fills the whole cell (a `<td>` child with height:100%
|
|
126
|
+
// stretches to the row height) so a right-click anywhere in the cell opens the
|
|
127
|
+
// menu. It carries the density padding relocated off the `<td>`.
|
|
128
|
+
const triggerFillStyles = stylex.create({
|
|
129
|
+
fill: {
|
|
130
|
+
display: 'block',
|
|
131
|
+
boxSizing: 'border-box',
|
|
132
|
+
blockSize: '100%',
|
|
133
|
+
inlineSize: '100%',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// `height:100%` on the `<td>` is the CSS trick that lets a block child resolve
|
|
138
|
+
// its own `height:100%` against the row height — so the trigger fills the cell
|
|
139
|
+
// vertically (top/bottom padding included) and is fully right-clickable.
|
|
140
|
+
const contextMenuCellStyles = stylex.create({
|
|
141
|
+
cell: {
|
|
142
|
+
blockSize: '100%',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
90
146
|
const dividerRowStyles = stylex.create({
|
|
91
147
|
cell: {
|
|
92
148
|
borderBottomWidth: {
|
|
@@ -151,22 +207,45 @@ export function TableCell({
|
|
|
151
207
|
}: TableCellProps) {
|
|
152
208
|
const ctx = useTableContext();
|
|
153
209
|
|
|
154
|
-
|
|
210
|
+
const hasContextMenu =
|
|
211
|
+
typeof contextMenuActions === 'function' ||
|
|
212
|
+
(Array.isArray(contextMenuActions) && contextMenuActions.length > 0);
|
|
213
|
+
|
|
214
|
+
// When the cell owns a context menu, its density padding is relocated onto
|
|
215
|
+
// the right-click trigger wrapper (see `content` below) so the entire cell —
|
|
216
|
+
// padding included — opens the menu. Otherwise padding lives on the `<td>`.
|
|
155
217
|
const cellStyles: StyleXStyles[] = ctx
|
|
156
218
|
? [
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
219
|
+
hasContextMenu
|
|
220
|
+
? densityTextStyles[ctx.density]
|
|
221
|
+
: densityStyles[ctx.density],
|
|
222
|
+
ctx.textOverflow === 'truncate' ? overflowStyles.cell : wrapStyles.cell,
|
|
161
223
|
containerEdgeStyles[ctx.density],
|
|
162
224
|
verticalAlignStyles[ctx.verticalAlign],
|
|
163
|
-
...buildDividerStyles(
|
|
225
|
+
...buildDividerStyles(
|
|
226
|
+
ctx,
|
|
227
|
+
dividerRowStyles.cell,
|
|
228
|
+
dividerColumnStyles.cell,
|
|
229
|
+
),
|
|
230
|
+
...(hasContextMenu ? [contextMenuCellStyles.cell] : []),
|
|
164
231
|
]
|
|
165
232
|
: [];
|
|
166
233
|
|
|
167
234
|
// The cell owns the context-menu wrapper so it controls how the menu
|
|
168
|
-
// interacts with its padding / content. No-op when no actions.
|
|
169
|
-
|
|
235
|
+
// interacts with its padding / content. No-op when no actions. When actions
|
|
236
|
+
// exist we make the trigger fill the cell and carry the density padding so a
|
|
237
|
+
// right-click anywhere in the cell (including the padding ring) opens it.
|
|
238
|
+
const triggerXstyle =
|
|
239
|
+
hasContextMenu && ctx
|
|
240
|
+
? [triggerFillStyles.fill, densityPaddingStyles[ctx.density]]
|
|
241
|
+
: hasContextMenu
|
|
242
|
+
? triggerFillStyles.fill
|
|
243
|
+
: undefined;
|
|
244
|
+
const content = wrapInTableContextMenu(
|
|
245
|
+
children,
|
|
246
|
+
contextMenuActions,
|
|
247
|
+
triggerXstyle,
|
|
248
|
+
);
|
|
170
249
|
|
|
171
250
|
return (
|
|
172
251
|
<td
|
package/src/Table/index.ts
CHANGED
|
@@ -26,6 +26,10 @@ export {useTableColumnSettings} from './plugins/columnSettings';
|
|
|
26
26
|
export {useTableColumnSettingsState} from './plugins/columnSettings';
|
|
27
27
|
export {useTableColumnResize} from './plugins/columnResize';
|
|
28
28
|
export {useTableStickyColumns} from './plugins/stickyColumns';
|
|
29
|
+
export {
|
|
30
|
+
useTableRowExpansion,
|
|
31
|
+
useTableRowExpansionState,
|
|
32
|
+
} from './plugins/rowExpansion';
|
|
29
33
|
export {resolveContextActions} from './tableContextMenu';
|
|
30
34
|
export {
|
|
31
35
|
useTableFiltering,
|
|
@@ -97,6 +101,7 @@ export type {
|
|
|
97
101
|
} from './plugins/columnSettings';
|
|
98
102
|
export type {UseTableColumnResizeConfig} from './plugins/columnResize';
|
|
99
103
|
export type {UseTableStickyColumnsConfig} from './plugins/stickyColumns';
|
|
104
|
+
export type {UseTableRowExpansionConfig} from './plugins/rowExpansion';
|
|
100
105
|
export type {
|
|
101
106
|
UseTableFilteringConfig,
|
|
102
107
|
TableFilterState,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
4
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Table} from '../../Table';
|
|
7
|
+
import type {TableColumn} from '../../types';
|
|
8
|
+
import {
|
|
9
|
+
useTableRowExpansion,
|
|
10
|
+
useTableRowExpansionState,
|
|
11
|
+
} from './useTableRowExpansion';
|
|
12
|
+
|
|
13
|
+
// popover mock for context-menu tests
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
16
|
+
this.setAttribute('popover-open', '');
|
|
17
|
+
const event = new Event('toggle', {bubbles: false});
|
|
18
|
+
Object.defineProperty(event, 'newState', {value: 'open'});
|
|
19
|
+
this.dispatchEvent(event);
|
|
20
|
+
});
|
|
21
|
+
HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
|
|
22
|
+
this.removeAttribute('popover-open');
|
|
23
|
+
const event = new Event('toggle', {bubbles: false});
|
|
24
|
+
Object.defineProperty(event, 'newState', {value: 'closed'});
|
|
25
|
+
this.dispatchEvent(event);
|
|
26
|
+
});
|
|
27
|
+
const originalMatches = HTMLElement.prototype.matches;
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
(HTMLElement.prototype as any).matches = function (
|
|
30
|
+
selector: string,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (selector === ':popover-open') {
|
|
33
|
+
return this.hasAttribute('popover-open');
|
|
34
|
+
}
|
|
35
|
+
return originalMatches.call(this, selector);
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
interface TreeItem extends Record<string, unknown> {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
children: TreeItem[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const treeData: TreeItem[] = [
|
|
46
|
+
{
|
|
47
|
+
id: 'a',
|
|
48
|
+
name: 'Folder A',
|
|
49
|
+
children: [
|
|
50
|
+
{id: 'a1', name: 'File A1', children: []},
|
|
51
|
+
{id: 'a2', name: 'File A2', children: []},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'b',
|
|
56
|
+
name: 'Folder B',
|
|
57
|
+
children: [{id: 'b1', name: 'File B1', children: []}],
|
|
58
|
+
},
|
|
59
|
+
{id: 'c', name: 'Leaf C', children: []},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const columns: TableColumn<TreeItem>[] = [{key: 'name', header: 'Name'}];
|
|
63
|
+
|
|
64
|
+
const EMPTY_KEYS = new Set<string>();
|
|
65
|
+
|
|
66
|
+
function Harness({
|
|
67
|
+
initialExpanded = EMPTY_KEYS,
|
|
68
|
+
}: {
|
|
69
|
+
initialExpanded?: Set<string>;
|
|
70
|
+
}) {
|
|
71
|
+
const [expandedKeys, setExpandedKeys] = useState(initialExpanded);
|
|
72
|
+
const {data, expansionConfig} = useTableRowExpansionState<TreeItem>({
|
|
73
|
+
baseData: treeData,
|
|
74
|
+
getChildren: item => item.children,
|
|
75
|
+
getRowKey: item => item.id,
|
|
76
|
+
expandedKeys,
|
|
77
|
+
setExpandedKeys,
|
|
78
|
+
});
|
|
79
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
80
|
+
return (
|
|
81
|
+
<Table data={data} columns={columns} idKey="id" plugins={{expansion}} />
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('useTableRowExpansion', () => {
|
|
86
|
+
it('renders a chevron button for expandable rows', () => {
|
|
87
|
+
render(<Harness />);
|
|
88
|
+
const buttons = screen.getAllByRole('button', {name: /expand row/i});
|
|
89
|
+
// Folder A and Folder B are expandable (top-level with children)
|
|
90
|
+
expect(buttons.length).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('shows child rows when expanded', () => {
|
|
94
|
+
render(<Harness initialExpanded={new Set(['a'])} />);
|
|
95
|
+
expect(screen.getByText('File A1')).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText('File A2')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('hides child rows when collapsed', () => {
|
|
100
|
+
render(<Harness />);
|
|
101
|
+
expect(screen.queryByText('File A1')).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('toggles expansion on chevron click', () => {
|
|
105
|
+
render(<Harness />);
|
|
106
|
+
expect(screen.queryByText('File A1')).not.toBeInTheDocument();
|
|
107
|
+
fireEvent.click(screen.getAllByRole('button', {name: /expand row/i})[0]);
|
|
108
|
+
expect(screen.getByText('File A1')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('contributes a context-menu action on expandable rows', () => {
|
|
112
|
+
render(<Harness />);
|
|
113
|
+
fireEvent.contextMenu(screen.getByText('Folder A'));
|
|
114
|
+
const items = screen.getAllByRole('menuitem', {
|
|
115
|
+
name: /expand row/i,
|
|
116
|
+
hidden: true,
|
|
117
|
+
});
|
|
118
|
+
expect(items.length).toBeGreaterThan(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not show chevron for leaf nodes', () => {
|
|
122
|
+
render(<Harness />);
|
|
123
|
+
// Leaf C has no children
|
|
124
|
+
expect(screen.getByText('Leaf C')).toBeInTheDocument();
|
|
125
|
+
// only 2 expand buttons (Folder A, Folder B), not 3
|
|
126
|
+
expect(
|
|
127
|
+
screen.getAllByRole('button', {name: /expand row|collapse row/i}).length,
|
|
128
|
+
).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders an expand-all toggle in the header', () => {
|
|
132
|
+
render(<Harness />);
|
|
133
|
+
expect(
|
|
134
|
+
screen.getByRole('button', {name: /expand all rows/i}),
|
|
135
|
+
).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('expand-all toggle expands every expandable row', () => {
|
|
139
|
+
render(<Harness />);
|
|
140
|
+
expect(screen.queryByText('File A1')).not.toBeInTheDocument();
|
|
141
|
+
fireEvent.click(screen.getByRole('button', {name: /expand all rows/i}));
|
|
142
|
+
// Both folders' children become visible.
|
|
143
|
+
expect(screen.getByText('File A1')).toBeInTheDocument();
|
|
144
|
+
expect(screen.getByText('File A2')).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByText('File B1')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('collapse-all toggle collapses every row', () => {
|
|
149
|
+
render(<Harness initialExpanded={new Set(['a', 'b'])} />);
|
|
150
|
+
expect(screen.getByText('File A1')).toBeInTheDocument();
|
|
151
|
+
fireEvent.click(screen.getByRole('button', {name: /collapse all rows/i}));
|
|
152
|
+
expect(screen.queryByText('File A1')).not.toBeInTheDocument();
|
|
153
|
+
expect(screen.queryByText('File B1')).not.toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
});
|