@astryxdesign/core 0.6.2-canary.bfee8a4 → 0.6.2-canary.c8d5849
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/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +1 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +4 -4
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +16 -16
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +2 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +4 -4
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +8 -8
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
- package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +1 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +7 -7
- package/dist/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- package/dist/Markdown/Markdown.d.ts +8 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.bench.js +150 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
- package/dist/Markdown/Markdown.js +262 -109
- package/dist/Markdown/ast.d.ts +162 -0
- package/dist/Markdown/ast.d.ts.map +1 -0
- package/dist/Markdown/ast.js +74 -0
- package/dist/Markdown/index.d.ts +5 -3
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/index.js +3 -2
- package/dist/Markdown/parser/index.d.ts +10 -0
- package/dist/Markdown/parser/index.d.ts.map +1 -0
- package/dist/Markdown/parser/index.js +10 -0
- package/dist/Markdown/parser.d.ts +126 -58
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +716 -167
- package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
- package/dist/Markdown/plugins/frontmatter.js +132 -0
- package/dist/Markdown/plugins/index.d.ts +16 -0
- package/dist/Markdown/plugins/index.d.ts.map +1 -0
- package/dist/Markdown/plugins/index.js +13 -0
- package/dist/Markdown/plugins/protocol.d.ts +191 -0
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
- package/dist/Markdown/plugins/protocol.js +957 -0
- package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
- package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
- package/dist/Markdown/plugins/semanticFence.js +213 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
- package/dist/Markdown/plugins/sourceDecoration.js +393 -0
- package/dist/Markdown/plugins/textTransform.d.ts +30 -0
- package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
- package/dist/Markdown/plugins/textTransform.js +374 -0
- package/dist/Markdown/remark.d.ts +196 -0
- package/dist/Markdown/remark.d.ts.map +1 -0
- package/dist/Markdown/remark.js +1062 -0
- package/dist/Markdown/url.d.ts +4 -0
- package/dist/Markdown/url.d.ts.map +1 -0
- package/dist/Markdown/url.js +25 -0
- package/dist/Markdown/utils.d.ts +4 -2
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/Markdown/utils.js +2 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +4 -4
- package/dist/Outline/index.d.ts +1 -0
- package/dist/Outline/index.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
- package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +8 -3
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +36 -31
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +1 -1
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +14 -8
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -7
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- package/dist/Table/Table.d.ts +1 -0
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +29 -13
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +1 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +2 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +4 -4
- package/dist/ToggleButton/ToggleButton.d.ts +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -7
- package/dist/astryx.css +14 -6
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/package.json +18 -3
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/Chat/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.tsx +10 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
- package/src/ComplexSelector/ComplexSelector.tsx +6 -1
- package/src/DateInput/DateInput.tsx +6 -1
- package/src/DateInput/NativeDateField.tsx +24 -8
- package/src/DateInput/TouchDateField.tsx +6 -2
- package/src/DateRangeInput/DateRangeInput.tsx +6 -1
- package/src/DateTimeInput/DateTimeInput.tsx +6 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
- package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
- package/src/Field/PanelSearchInput.tsx +5 -2
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
- package/src/FileInput/FileInput.tsx +11 -2
- package/src/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Layout/LayoutFooter.doc.mjs +17 -0
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/Markdown/Markdown.doc.mjs +272 -0
- package/src/Markdown/Markdown.fr23.bench.ts +174 -0
- package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
- package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
- package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
- package/src/Markdown/Markdown.public.test.ts +126 -3
- package/src/Markdown/Markdown.spec.md +197 -44
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +414 -135
- package/src/Markdown/ast.test.ts +307 -0
- package/src/Markdown/ast.ts +337 -0
- package/src/Markdown/incremental.test.ts +12 -0
- package/src/Markdown/index.ts +37 -1
- package/src/Markdown/modules/remark.spec.md +269 -0
- package/src/Markdown/parser/index.ts +64 -0
- package/src/Markdown/parser.public.test.ts +103 -0
- package/src/Markdown/parser.ts +1180 -235
- package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
- package/src/Markdown/plugins/frontmatter.ts +198 -0
- package/src/Markdown/plugins/index.ts +53 -0
- package/src/Markdown/plugins/protocol.test.tsx +1022 -0
- package/src/Markdown/plugins/protocol.ts +1618 -0
- package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
- package/src/Markdown/plugins/semanticFence.ts +424 -0
- package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
- package/src/Markdown/plugins/sourceDecoration.ts +539 -0
- package/src/Markdown/plugins/textTransform.test.ts +467 -0
- package/src/Markdown/plugins/textTransform.ts +554 -0
- package/src/Markdown/remark.test.tsx +1877 -0
- package/src/Markdown/remark.ts +1574 -0
- package/src/Markdown/url.ts +30 -0
- package/src/Markdown/utils.ts +13 -1
- package/src/MultiSelector/MultiSelector.test.tsx +1 -35
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
- package/src/NumberInput/NumberInput.tsx +6 -1
- package/src/Outline/Outline.doc.mjs +7 -5
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/index.ts +1 -0
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
- package/src/Outline/useOutlineFromMarkdown.ts +15 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ProgressBar/ProgressBar.test.tsx +29 -11
- package/src/ProgressBar/ProgressBar.tsx +59 -72
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/Selector/Selector.spec.md +12 -9
- package/src/Selector/Selector.tsx +6 -1
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
- package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +11 -0
- package/src/Slider/Slider.test.tsx +128 -0
- package/src/Slider/Slider.tsx +25 -7
- package/src/Spinner/Spinner.test.tsx +4 -0
- package/src/Spinner/Spinner.tsx +31 -18
- package/src/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- package/src/Table/Table.doc.mjs +1 -1
- package/src/Table/Table.spec.md +54 -40
- package/src/Table/Table.test.tsx +83 -10
- package/src/Table/Table.tsx +30 -22
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
- package/src/TextArea/TextArea.tsx +6 -1
- package/src/TextInput/TextInput.test.tsx +20 -1
- package/src/TextInput/TextInput.tsx +6 -1
- package/src/TimeInput/TimeInput.tsx +6 -1
- package/src/ToggleButton/ToggleButton.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/TreeList/TreeList.spec.md +123 -75
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +21 -7
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -0
- package/src/inputFontFloor.test.ts +106 -0
- package/src/utils/interactionOverlay.stylex.ts +20 -0
package/src/TabList/Tab.tsx
CHANGED
|
@@ -137,6 +137,15 @@ const styles = stylex.create({
|
|
|
137
137
|
[stylex.when.ancestor(':hover', tabScope)]: {
|
|
138
138
|
'@media (hover: hover)': colorVars['--color-overlay-hover'],
|
|
139
139
|
},
|
|
140
|
+
// Pressed: the same layer steps up to the pressed overlay, read off the
|
|
141
|
+
// tab the way the hover is, so a press paints on a finger too. Repeated
|
|
142
|
+
// inside the hover-capable branch so it outranks the hover rule there
|
|
143
|
+
// (a media-nested rule carries extra generated priority; see
|
|
144
|
+
// interactionOverlay.stylex.ts).
|
|
145
|
+
[stylex.when.ancestor(':active', tabScope)]: {
|
|
146
|
+
default: colorVars['--color-overlay-pressed'],
|
|
147
|
+
'@media (hover: hover)': colorVars['--color-overlay-pressed'],
|
|
148
|
+
},
|
|
140
149
|
},
|
|
141
150
|
transitionProperty: 'background-color',
|
|
142
151
|
transitionDuration: durationVars['--duration-fast'],
|
|
@@ -249,6 +258,7 @@ export function Tab({
|
|
|
249
258
|
xstyle,
|
|
250
259
|
className,
|
|
251
260
|
style,
|
|
261
|
+
onClick,
|
|
252
262
|
...restProps
|
|
253
263
|
}: TabProps) {
|
|
254
264
|
const tabListCtx = useTabListContext();
|
|
@@ -263,9 +273,23 @@ export function Tab({
|
|
|
263
273
|
const displayIcon = isSelected && selectedIcon ? selectedIcon : icon;
|
|
264
274
|
const hasVisibleLabel = !isLabelHidden && label !== '';
|
|
265
275
|
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
276
|
+
const isDisabled =
|
|
277
|
+
restProps['aria-disabled'] === true ||
|
|
278
|
+
restProps['aria-disabled'] === 'true';
|
|
279
|
+
|
|
280
|
+
const handleSelect = useCallback(
|
|
281
|
+
(event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
|
|
282
|
+
if (isDisabled) {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
onClick?.(event as React.MouseEvent<HTMLButtonElement>);
|
|
287
|
+
if (!event.defaultPrevented) {
|
|
288
|
+
tabListCtx.onChange(value);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
[isDisabled, onClick, tabListCtx, value],
|
|
292
|
+
);
|
|
269
293
|
|
|
270
294
|
useDevWarning(
|
|
271
295
|
'Tab',
|
|
@@ -339,7 +363,7 @@ export function Tab({
|
|
|
339
363
|
sizeStyles[size],
|
|
340
364
|
isSelected && styles.selected,
|
|
341
365
|
isFill && layoutStyles.fill,
|
|
342
|
-
tabScope,
|
|
366
|
+
!isDisabled && tabScope,
|
|
343
367
|
xstyle,
|
|
344
368
|
),
|
|
345
369
|
className,
|
|
@@ -382,10 +406,14 @@ export function Tab({
|
|
|
382
406
|
) : null;
|
|
383
407
|
|
|
384
408
|
if (isLink) {
|
|
409
|
+
// A disabled link tab must not reach a router component: some routers reject
|
|
410
|
+
// a missing destination, while others treat it as the current route. A
|
|
411
|
+
// plain anchor without href preserves the element shape but cannot navigate.
|
|
412
|
+
const LinkRoot = isDisabled ? 'a' : LinkComponent;
|
|
385
413
|
return (
|
|
386
|
-
<
|
|
414
|
+
<LinkRoot
|
|
387
415
|
ref={ref}
|
|
388
|
-
href={href}
|
|
416
|
+
href={isDisabled ? undefined : href}
|
|
389
417
|
onClick={handleSelect}
|
|
390
418
|
{...sharedProps}>
|
|
391
419
|
{hoverBgElement}
|
|
@@ -393,7 +421,7 @@ export function Tab({
|
|
|
393
421
|
{labelElement}
|
|
394
422
|
{endContentElement}
|
|
395
423
|
{indicatorElement}
|
|
396
|
-
</
|
|
424
|
+
</LinkRoot>
|
|
397
425
|
);
|
|
398
426
|
}
|
|
399
427
|
|
|
@@ -103,6 +103,17 @@ export const docs = {
|
|
|
103
103
|
{name: 'TabMenu'},
|
|
104
104
|
],
|
|
105
105
|
usage: {
|
|
106
|
+
accessibility: [
|
|
107
|
+
{
|
|
108
|
+
name: 'Tab label',
|
|
109
|
+
category: 'Color contrast',
|
|
110
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
111
|
+
requirement: '4.5:1',
|
|
112
|
+
states: ['Rest', 'Hover', 'Pointer down', 'Selected'],
|
|
113
|
+
description:
|
|
114
|
+
'Each label must have at least 4.5:1 contrast with the tab surface behind it. For Hover and Pointer down, measure the final background after the overlay layer is applied.',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
106
117
|
description:
|
|
107
118
|
'TabList provides tab-style navigation for organizing content into categorized sections. Use it to let users switch between related views without leaving the page, with overflow items handled by a built-in "more" menu.',
|
|
108
119
|
bestPractices: [
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import {hasPressedArm} from '../__tests__/pressState';
|
|
15
16
|
import {TabList} from './TabList';
|
|
16
17
|
import type {TabListProps} from './TabList';
|
|
17
18
|
import {Tab} from './Tab';
|
|
@@ -1515,3 +1516,68 @@ describe('TabList ARIA pattern — any other role', () => {
|
|
|
1515
1516
|
);
|
|
1516
1517
|
});
|
|
1517
1518
|
});
|
|
1519
|
+
|
|
1520
|
+
describe('pressed state', () => {
|
|
1521
|
+
it('paints the pressed overlay on the tab surface while the tab is pressed', () => {
|
|
1522
|
+
render(
|
|
1523
|
+
<TabList value="home" onChange={() => {}}>
|
|
1524
|
+
<Tab value="home" label="Home" />
|
|
1525
|
+
<Tab value="settings" label="Settings" />
|
|
1526
|
+
</TabList>,
|
|
1527
|
+
);
|
|
1528
|
+
const tab = screen.getByRole('button', {name: 'Settings'});
|
|
1529
|
+
// The hover surface is a child layer read off the tab; the press paints
|
|
1530
|
+
// the same layer, so the assertion is on that layer.
|
|
1531
|
+
const surface = tab.querySelector('span[aria-hidden="true"]');
|
|
1532
|
+
if (surface == null) {
|
|
1533
|
+
throw new Error('the tab has no hover surface to press');
|
|
1534
|
+
}
|
|
1535
|
+
expect(hasPressedArm(surface)).toBe(true);
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
it('does not activate a disabled button tab or call its handler', async () => {
|
|
1539
|
+
const user = userEvent.setup();
|
|
1540
|
+
const onChange = vi.fn();
|
|
1541
|
+
const onClick = vi.fn();
|
|
1542
|
+
render(
|
|
1543
|
+
<TabList value="home" onChange={onChange}>
|
|
1544
|
+
<Tab value="home" label="Home" />
|
|
1545
|
+
<Tab
|
|
1546
|
+
value="settings"
|
|
1547
|
+
label="Settings"
|
|
1548
|
+
aria-disabled="true"
|
|
1549
|
+
onClick={onClick}
|
|
1550
|
+
/>
|
|
1551
|
+
</TabList>,
|
|
1552
|
+
);
|
|
1553
|
+
const tab = screen.getByRole('button', {name: 'Settings'});
|
|
1554
|
+
await user.click(tab);
|
|
1555
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
1556
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('removes navigation from a disabled link tab', () => {
|
|
1560
|
+
const onChange = vi.fn();
|
|
1561
|
+
const {container} = render(
|
|
1562
|
+
<LinkProvider component={CustomLink}>
|
|
1563
|
+
<TabList value="home" onChange={onChange}>
|
|
1564
|
+
<Tab value="home" label="Home" />
|
|
1565
|
+
<Tab
|
|
1566
|
+
value="settings"
|
|
1567
|
+
label="Settings"
|
|
1568
|
+
href="/settings"
|
|
1569
|
+
aria-disabled="true"
|
|
1570
|
+
/>
|
|
1571
|
+
</TabList>
|
|
1572
|
+
</LinkProvider>,
|
|
1573
|
+
);
|
|
1574
|
+
const tab = container.querySelector('a');
|
|
1575
|
+
if (tab == null) {
|
|
1576
|
+
throw new Error('the disabled link tab did not render an anchor');
|
|
1577
|
+
}
|
|
1578
|
+
expect(tab).not.toHaveAttribute('data-custom-link');
|
|
1579
|
+
expect(tab).not.toHaveAttribute('href');
|
|
1580
|
+
fireEvent.click(tab);
|
|
1581
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
@@ -9,37 +9,4 @@
|
|
|
9
9
|
|
|
10
10
|
import type {KnownFailure} from '@astryxdesign/a11y-spec';
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
binding: 'Tab',
|
|
14
|
-
state: 'tab-disabled-unselected',
|
|
15
|
-
evidenceLayer: 'real-browser',
|
|
16
|
-
standardsReference:
|
|
17
|
-
'WCAG 2.2 4.1.2 Name, Role, Value; WAI-ARIA 1.2 aria-disabled',
|
|
18
|
-
reason:
|
|
19
|
-
'This migration records the existing behavior without changing the component interaction contract.',
|
|
20
|
-
} as const;
|
|
21
|
-
|
|
22
|
-
export const TABS_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [
|
|
23
|
-
{
|
|
24
|
-
...COMMON,
|
|
25
|
-
expectation: 'tabs.tab.unavailable-pointer-inert',
|
|
26
|
-
failureEquals:
|
|
27
|
-
'the tab is reported unavailable, but clicking it selected it',
|
|
28
|
-
userImpact:
|
|
29
|
-
'A pointer user can activate a tab that assistive technology reports as unavailable.',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
...COMMON,
|
|
33
|
-
expectation: 'tabs.tab.unavailable-enter-inert',
|
|
34
|
-
failureEquals: 'the tab is reported unavailable, but Enter selected it',
|
|
35
|
-
userImpact:
|
|
36
|
-
'A keyboard user can activate a focusable tab with Enter after assistive technology reports it unavailable.',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
...COMMON,
|
|
40
|
-
expectation: 'tabs.tab.unavailable-space-inert',
|
|
41
|
-
failureEquals: 'the tab is reported unavailable, but Space selected it',
|
|
42
|
-
userImpact:
|
|
43
|
-
'A keyboard user can activate a focusable tab with Space after assistive technology reports it unavailable.',
|
|
44
|
-
},
|
|
45
|
-
];
|
|
12
|
+
export const TABS_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [];
|
package/src/Table/Table.doc.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const anatomy = [
|
|
|
12
12
|
name: 'Scroll region',
|
|
13
13
|
required: true,
|
|
14
14
|
description:
|
|
15
|
-
'
|
|
15
|
+
'Outer region that scrolls horizontally, enters the keyboard order, and contains overscroll only while the columns overflow.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: 'Header section',
|
package/src/Table/Table.spec.md
CHANGED
|
@@ -13,6 +13,7 @@ review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
|
13
13
|
verified_by:
|
|
14
14
|
[
|
|
15
15
|
packages/core/src/Table/Table.test.tsx,
|
|
16
|
+
packages/core/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts,
|
|
16
17
|
packages/core/src/Table/Table.perf.test.tsx,
|
|
17
18
|
packages/core/src/Table/plugins/selection/useTableSelection.test.tsx,
|
|
18
19
|
packages/core/src/Table/plugins/sortable/useTableSortable.test.tsx,
|
|
@@ -33,7 +34,7 @@ architecture:
|
|
|
33
34
|
architecture:public-component-api,
|
|
34
35
|
]
|
|
35
36
|
contributing: []
|
|
36
|
-
system_specs: []
|
|
37
|
+
system_specs: [spec:AST-025]
|
|
37
38
|
---
|
|
38
39
|
|
|
39
40
|
# Table component contract
|
|
@@ -49,11 +50,13 @@ generated anatomy, accessibility, migration, and evidence belong to each listed
|
|
|
49
50
|
|
|
50
51
|
## Compatibility and migration
|
|
51
52
|
|
|
52
|
-
- Released default preserved: `
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
- Released default preserved: `no`; a fitting Table scroll region now passes
|
|
54
|
+
scrolling to its ancestor and stays out of the keyboard order, while an
|
|
55
|
+
overflowing region keeps its existing native scrolling path.
|
|
56
|
+
- Compatibility class: standards-conformance bug fix; no public prop, target,
|
|
57
|
+
alias, or semantic Table element changes.
|
|
55
58
|
- Controlled/uncontrolled behavior: unchanged
|
|
56
|
-
- Migration decision:
|
|
59
|
+
- Migration decision: `spec:AST-025` FR12, FR14, FR19, FR21, and DEC-1
|
|
57
60
|
|
|
58
61
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
59
62
|
|
|
@@ -87,7 +90,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
87
90
|
concerns for `useTableRowStatus`.
|
|
88
91
|
- New targets for current untargeted plugin parts, or correction of current
|
|
89
92
|
target-reachability gaps.
|
|
90
|
-
- New
|
|
93
|
+
- New public API, theme target, or alias.
|
|
91
94
|
|
|
92
95
|
## Public concepts
|
|
93
96
|
|
|
@@ -103,20 +106,20 @@ documented in `Table.doc.mjs` and the member and module docs.
|
|
|
103
106
|
|
|
104
107
|
## Behavioral and layout contract
|
|
105
108
|
|
|
106
|
-
| ID | Invariant
|
|
107
|
-
| ---- |
|
|
108
|
-
| FR1 | Styled Table renders one
|
|
109
|
-
| FR2 | Data-driven mode renders a Body section and a Header section with Column header cells when columns are present. Each data item renders a standard Row and Cells; an empty data array instead renders the configured empty-state row when enabled. It never creates a Footer section.
|
|
110
|
-
| FR3 | Children mode passes caller composition through to the Table. TableHeader, TableBody, and TableFooter provide the respective sections, and TableRow, TableHeaderCell, and TableCell provide standard rows and cells.
|
|
111
|
-
| FR4 | `Table.doc.mjs` is the canonical aggregate consumer owner for the eight current targets: `table`, `table-scroll-wrapper`, `table-header`, `table-body`, `table-footer`, `table-row`, `table-cell`, and `table-header-cell`. Member docs retain direct lookup metadata through `subComponentOf: 'Table'`.
|
|
112
|
-
| FR5 | For a sortable column, useTableSortable renders a Sort control around the column label, an Icon-owned Sort indicator glyph, and a numeric Sort priority only while multi-sort has more than one active entry.
|
|
113
|
-
| FR6 | useTableSelection renders CheckboxInput-owned Selection controls in the generated selection Column header cell and each selectable body Cell; selection remains row state rather than separate anatomy.
|
|
114
|
-
| FR7 | For an expandable row, useTableRowExpansion renders an Expansion control with an Icon-owned Expansion glyph and conditionally appends an Expanded detail panel whose cell spans the column count captured by that plugin's `transformColumns` step.
|
|
115
|
-
| FR8 | Empty data renders the default compact EmptyState unless the caller supplies replacement content or disables it.
|
|
116
|
-
| FR9 | Table converts the caller's named plugin record into one ordered array after its built-in styling plugin. Known names use the current canonical sequence `columnSettings → sort → tree → selection → pagination`; every other name follows that known set while preserving its record insertion order.
|
|
117
|
-
| FR10 | Every applicable non-context transform runs sequentially in the resolved plugin-array order, so a later plugin receives the value returned by every earlier successful plugin. A throwing transform reports a development error and leaves the prior accumulated value in the pipeline.
|
|
118
|
-
| FR11 | `transformColumns` completes before element transforms. Table then applies table, header-cell/header-row, body-cell/body-row, scroll-wrapper, and context phases at their render points. Header-cell contributions use `before`, `content`, `after`, `overlay`, and `below` slots. Context transforms run in reverse so the first plugin becomes the outermost provider.
|
|
119
|
-
| FR12 | When built-in and named plugin references are unchanged, Table reuses the resolved plugin array; unknown/custom plugin value identity and insertion order remain stable inputs to memoization.
|
|
109
|
+
| ID | Invariant | Basis | Evidence state |
|
|
110
|
+
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| FR1 | By default, Styled Table renders one named Scroll region around one observed content box and one semantic Table. The Scroll region enters the keyboard order and contains inline overscroll only while inline content effectively overflows; fitting content passes scroll gestures to its ancestor. Explicit scroll-wrapper plugin HTML attributes remain authoritative. | `spec:AST-025` FR12, FR14, FR19, FR21; current source, docs, and tests | Verified fitting and inline-overflow transitions through the shared `useScrollableArea` behavior core |
|
|
112
|
+
| FR2 | Data-driven mode renders a Body section and a Header section with Column header cells when columns are present. Each data item renders a standard Row and Cells; an empty data array instead renders the configured empty-state row when enabled. It never creates a Footer section. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
|
|
113
|
+
| FR3 | Children mode passes caller composition through to the Table. TableHeader, TableBody, and TableFooter provide the respective sections, and TableRow, TableHeaderCell, and TableCell provide standard rows and cells. | Current source, docs, and tests | Verified current composition; no new API or DOM rule |
|
|
114
|
+
| FR4 | `Table.doc.mjs` is the canonical aggregate consumer owner for the eight current targets: `table`, `table-scroll-wrapper`, `table-header`, `table-body`, `table-footer`, `table-row`, `table-cell`, and `table-header-cell`. Member docs retain direct lookup metadata through `subComponentOf: 'Table'`. | Current docs and CLI target tests | Verified parent ownership; focused placement coverage is partial |
|
|
115
|
+
| FR5 | For a sortable column, useTableSortable renders a Sort control around the column label, an Icon-owned Sort indicator glyph, and a numeric Sort priority only while multi-sort has more than one active entry. | Current source, docs, and tests | Control and priority are tested; glyph presence is source-inspected only |
|
|
116
|
+
| FR6 | useTableSelection renders CheckboxInput-owned Selection controls in the generated selection Column header cell and each selectable body Cell; selection remains row state rather than separate anatomy. | Current source, docs, and tests | Verified stable delegated controls; no target or state change |
|
|
117
|
+
| FR7 | For an expandable row, useTableRowExpansion renders an Expansion control with an Icon-owned Expansion glyph and conditionally appends an Expanded detail panel whose cell spans the column count captured by that plugin's `transformColumns` step. | Current source, docs, and tests | Verified stable plugin parts; current target gaps remain |
|
|
118
|
+
| FR8 | Empty data renders the default compact EmptyState unless the caller supplies replacement content or disables it. | Current source, docs, and tests | Verified conditional delegation; caller content remains outside ownership |
|
|
119
|
+
| FR9 | Table converts the caller's named plugin record into one ordered array after its built-in styling plugin. Known names use the current canonical sequence `columnSettings → sort → tree → selection → pagination`; every other name follows that known set while preserving its record insertion order. | Current source and docs | Current shared ordering; canonical-name coverage is source-inspected |
|
|
120
|
+
| FR10 | Every applicable non-context transform runs sequentially in the resolved plugin-array order, so a later plugin receives the value returned by every earlier successful plugin. A throwing transform reports a development error and leaves the prior accumulated value in the pipeline. | Current source and tests | Sequential composition is tested; failure isolation is source-inspected |
|
|
121
|
+
| FR11 | `transformColumns` completes before element transforms. Table then applies table, header-cell/header-row, body-cell/body-row, scroll-wrapper, and context phases at their render points. Header-cell contributions use `before`, `content`, `after`, `overlay`, and `below` slots. Context transforms run in reverse so the first plugin becomes the outermost provider. | Current source, types, and tests | Transform application is tested; complete cross-phase order is source-inspected |
|
|
122
|
+
| FR12 | When built-in and named plugin references are unchanged, Table reuses the resolved plugin array; unknown/custom plugin value identity and insertion order remain stable inputs to memoization. | Current source and performance tests | Current performance contract; focused named-order coverage is partial |
|
|
120
123
|
|
|
121
124
|
### Current evidence and gaps
|
|
122
125
|
|
|
@@ -193,11 +196,15 @@ documented in `Table.doc.mjs` and the member and module docs.
|
|
|
193
196
|
|
|
194
197
|
## Accessibility contract
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
By default, the Scroll region is named as a `group`, enters sequential keyboard
|
|
200
|
+
navigation only while inline overflow is effective, and keeps native Arrow/Page
|
|
201
|
+
scrolling. A scroll-wrapper plugin may explicitly override its HTML accessibility
|
|
202
|
+
attributes. Losing overflow removes future default tab stops without moving current
|
|
203
|
+
focus. Effective inline scrolling contains overscroll at its edge; fitting content
|
|
204
|
+
leaves page scrolling available. Native table semantics, column-header scope, sort
|
|
205
|
+
`aria-sort`, selection `aria-selected`, CheckboxInput labels, and expansion
|
|
206
|
+
`aria-expanded` remain unchanged. Each public module owns accessibility introduced
|
|
207
|
+
by its transforms; the shared pipeline does not confer correctness on module output.
|
|
201
208
|
|
|
202
209
|
## Design relationships
|
|
203
210
|
|
|
@@ -294,24 +301,31 @@ aggregate ownership.
|
|
|
294
301
|
|
|
295
302
|
## Verification map
|
|
296
303
|
|
|
297
|
-
| Contract | Verification
|
|
298
|
-
| ------------------- |
|
|
299
|
-
| FR1-FR3 | `Table.test.tsx` render, structure, children-mode, section, and target assertions
|
|
300
|
-
| FR4 | CLI discovery and API target tests plus source inspection
|
|
301
|
-
| FR5 | `plugins/sortable/useTableSortable.test.tsx` plus source inspection
|
|
302
|
-
| FR6 | `plugins/selection/useTableSelection.test.tsx`
|
|
303
|
-
| FR7 | `plugins/rowExpansion/useTableRowExpansion.test.tsx` plus source inspection
|
|
304
|
-
| FR8 | `Table.test.tsx` empty-state suite plus EmptyState public target metadata
|
|
305
|
-
| FR9-FR11 | `Table.test.tsx`, `types.ts`, `BaseTable.tsx`, and `useBaseTablePlugins.ts`
|
|
306
|
-
| FR12, PR1 | `Table.perf.test.tsx` plus `useBaseTablePlugins.ts` source inspection
|
|
307
|
-
| Module backlink | `scripts/check-knowledge.mjs`
|
|
308
|
-
| Theming anatomy map | `scripts/check-knowledge.mjs`, `themingTargets.test.ts`, and CLI parent-aware target discovery tests
|
|
304
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
305
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
306
|
+
| FR1-FR3 | `Table.test.tsx` render, structure, fit/overflow transitions, children-mode, section, and target assertions | Fitting and inline-overflowing data-driven tables, empty data, and children composition | Removing Table, Scroll region, observed content box, conditional keyboard access, or effective-axis containment fails existing role, structure, state, class, style, or prop-forwarding assertions; Row and Cell are conditional on rendered data or caller composition. | `audit:Table/anatomy` |
|
|
307
|
+
| FR4 | CLI discovery and API target tests plus source inspection | Direct member docs and unfiltered/scoped target output | Duplicating a parent/member target or changing the canonical owner fails parent-aware discovery assertions; section target placement remains source-inspected. | `audit:Table/theming` |
|
|
308
|
+
| FR5 | `plugins/sortable/useTableSortable.test.tsx` plus source inspection | Unsorted, ascending, descending, and multi-sort | Removing the Sort control, conditional priority, or ARIA state fails existing assertions. Removing the Sort indicator glyph does not currently fail a focused test; glyph presence and delegated Icon target placement remain source-inspected. | `audit:Table/anatomy` |
|
|
309
|
+
| FR6 | `plugins/selection/useTableSelection.test.tsx` | Select all, individual, disabled, and non-selectable | Removing header/body Selection controls or row selection state fails existing structure, label, interaction, and ARIA assertions; delegated CheckboxInput target placement remains source-inspected. | `audit:Table/anatomy` |
|
|
310
|
+
| FR7 | `plugins/rowExpansion/useTableRowExpansion.test.tsx` plus source inspection | Collapsed, expanded, and non-expandable rows | Removing the Expansion control, conditional detail panel, captured-count `colSpan`, or ARIA state fails existing assertions in the covered plugin order; later column transforms, delegated Icon target placement, and untargeted panel wrappers remain source-inspected. | `audit:Table/anatomy` |
|
|
311
|
+
| FR8 | `Table.test.tsx` empty-state suite plus EmptyState public target metadata | Default, custom, disabled, and non-empty | Removing conditional empty behavior fails existing assertions; default EmptyState delegation remains source-inspected. | `audit:Table/anatomy` |
|
|
312
|
+
| FR9-FR11 | `Table.test.tsx`, `types.ts`, `BaseTable.tsx`, and `useBaseTablePlugins.ts` | Built-in plus known and custom named plugins; every transform phase | Sequential composition, base-before-user behavior, transform application, and slot output have focused coverage; complete known-name sorting, phase order, exception continuation, and context nesting remain source-inspected. | `audit:Table/plugins` |
|
|
313
|
+
| FR12, PR1 | `Table.perf.test.tsx` plus `useBaseTablePlugins.ts` source inspection | Same plugin references, recreated record, and changed plugin value | Unchanged plugin values must preserve the resolved array and representative no-op row-update budgets; focused named-record identity coverage remains partial. | `audit:Table/performance` |
|
|
314
|
+
| Module backlink | `scripts/check-knowledge.mjs` | Active parent and colocated module record | A missing, duplicate, mis-parented, wrong-kind, misnamed, or undiscovered module record fails knowledge validation. | `audit:Table/modules` |
|
|
315
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs`, `themingTargets.test.ts`, and CLI parent-aware target discovery tests | Canonical anatomy, eight current targets, legacy alias | Missing, extra, duplicated, prefixed, stale, alias-backed, or independently owned member mappings fail repository validation or discovery coverage. | `audit:Table/theming` |
|
|
309
316
|
|
|
310
317
|
## Decision log
|
|
311
318
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
### DEC-1 — Table adopts the shared scroll behavior on its owned viewport
|
|
320
|
+
|
|
321
|
+
**Reference:** `component:Table/DEC-1`
|
|
322
|
+
**Decider:** `cixzhang`, 2026-09-20
|
|
323
|
+
|
|
324
|
+
Table uses `useScrollableArea` on its existing Scroll region with a private observed
|
|
325
|
+
content box. Inline overflow remains native and keyboard reachable. Containment is
|
|
326
|
+
active only while that axis can scroll, so a fitting Table does not create a dead
|
|
327
|
+
scroll zone for an ancestor. This keeps one Table-owned viewport and adds no public
|
|
328
|
+
prop or parallel overflow detector.
|
|
315
329
|
|
|
316
330
|
## Open questions
|
|
317
331
|
|
package/src/Table/Table.test.tsx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen} from '@testing-library/react';
|
|
13
|
+
import {act, render, screen} from '@testing-library/react';
|
|
14
14
|
import * as stylex from '@stylexjs/stylex';
|
|
15
15
|
import {BaseTable} from './BaseTable';
|
|
16
16
|
import {Table} from './Table';
|
|
@@ -837,22 +837,95 @@ describe('Table', () => {
|
|
|
837
837
|
expect(screen.getAllByRole('row')).toHaveLength(4);
|
|
838
838
|
});
|
|
839
839
|
|
|
840
|
-
it('wraps table in
|
|
840
|
+
it('wraps the table in an observed scroll content box', () => {
|
|
841
841
|
render(<Table data={users} columns={columns} />);
|
|
842
842
|
const table = screen.getByRole('table');
|
|
843
|
-
const
|
|
843
|
+
const content = table.parentElement;
|
|
844
|
+
const wrapper = content?.parentElement;
|
|
845
|
+
expect(content).toHaveAttribute('data-scroll-content');
|
|
844
846
|
expect(wrapper).toBeTruthy();
|
|
845
847
|
expect(wrapper!.className).toContain('astryx-table-scroll-wrapper');
|
|
846
848
|
});
|
|
847
849
|
|
|
848
|
-
it('
|
|
850
|
+
it('keeps a fitting scroll container out of the keyboard order', () => {
|
|
849
851
|
render(<Table data={users} columns={columns} />);
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
expect(wrapper).
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
852
|
+
const wrapper = screen.getByRole('group', {name: 'Table'});
|
|
853
|
+
expect(wrapper).not.toHaveAttribute('tabindex');
|
|
854
|
+
expect(wrapper.style.overscrollBehaviorX).toBe('auto');
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('preserves scroll-wrapper HTML attributes from plugins', () => {
|
|
858
|
+
const plugin: TablePlugin<User> = {
|
|
859
|
+
transformScrollWrapper: props => ({
|
|
860
|
+
...props,
|
|
861
|
+
htmlProps: {
|
|
862
|
+
...props.htmlProps,
|
|
863
|
+
role: 'region',
|
|
864
|
+
'aria-label': 'Orders',
|
|
865
|
+
tabIndex: 2,
|
|
866
|
+
},
|
|
867
|
+
}),
|
|
868
|
+
};
|
|
869
|
+
render(<Table data={users} columns={columns} plugins={{custom: plugin}} />);
|
|
870
|
+
const wrapper = screen.getByRole('region', {name: 'Orders'});
|
|
871
|
+
expect(wrapper).toHaveAttribute('tabindex', '2');
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('contains overscroll only while the table scrolls', () => {
|
|
875
|
+
const frames: FrameRequestCallback[] = [];
|
|
876
|
+
vi.stubGlobal(
|
|
877
|
+
'requestAnimationFrame',
|
|
878
|
+
vi.fn((callback: FrameRequestCallback) => {
|
|
879
|
+
frames.push(callback);
|
|
880
|
+
return frames.length;
|
|
881
|
+
}),
|
|
882
|
+
);
|
|
883
|
+
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
render(<Table data={users} columns={columns} />);
|
|
887
|
+
const wrapper = screen.getByRole('group', {name: 'Table'});
|
|
888
|
+
Object.assign(wrapper.style, {overflowX: 'auto'});
|
|
889
|
+
for (const [key, value] of Object.entries({
|
|
890
|
+
clientWidth: 100,
|
|
891
|
+
clientHeight: 100,
|
|
892
|
+
scrollWidth: 180,
|
|
893
|
+
scrollHeight: 100,
|
|
894
|
+
scrollLeft: 0,
|
|
895
|
+
scrollTop: 0,
|
|
896
|
+
})) {
|
|
897
|
+
Object.defineProperty(wrapper, key, {
|
|
898
|
+
configurable: true,
|
|
899
|
+
value,
|
|
900
|
+
writable: true,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
act(() => {
|
|
905
|
+
wrapper.dispatchEvent(new Event('scroll'));
|
|
906
|
+
frames.splice(0).forEach(callback => callback(performance.now()));
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
expect(wrapper).toHaveAttribute('tabindex', '0');
|
|
910
|
+
expect(wrapper).toHaveAttribute('data-scrollable-inline', 'true');
|
|
911
|
+
expect(wrapper.style.overscrollBehaviorX).toBe('contain');
|
|
912
|
+
|
|
913
|
+
Object.defineProperty(wrapper, 'scrollWidth', {
|
|
914
|
+
configurable: true,
|
|
915
|
+
value: 100,
|
|
916
|
+
writable: true,
|
|
917
|
+
});
|
|
918
|
+
act(() => {
|
|
919
|
+
wrapper.dispatchEvent(new Event('scroll'));
|
|
920
|
+
frames.splice(0).forEach(callback => callback(performance.now()));
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
expect(wrapper).not.toHaveAttribute('tabindex');
|
|
924
|
+
expect(wrapper).not.toHaveAttribute('data-scrollable-inline');
|
|
925
|
+
expect(wrapper.style.overscrollBehaviorX).toBe('auto');
|
|
926
|
+
} finally {
|
|
927
|
+
vi.unstubAllGlobals();
|
|
928
|
+
}
|
|
856
929
|
});
|
|
857
930
|
|
|
858
931
|
it('uses table-layout: auto in children mode', () => {
|
package/src/Table/Table.tsx
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
12
|
* - /packages/core/src/Table/Table.doc.mjs (props table, features, usage examples)
|
|
13
13
|
* - /packages/core/src/Table/Table.test.tsx (tests for new/changed behavior)
|
|
14
|
+
* - /packages/core/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts (browser scroll behavior)
|
|
14
15
|
* - /packages/core/src/Table/index.ts (exports if types change)
|
|
15
16
|
* - /apps/storybook/stories/Table.stories.tsx (storybook stories)
|
|
16
17
|
* - /packages/cli/assets/templates/blocks/components/Table/ (showcase blocks)
|
|
@@ -22,6 +23,7 @@ import {colorVars} from '../theme/tokens.stylex';
|
|
|
22
23
|
import {BaseTable} from './BaseTable';
|
|
23
24
|
import {TableContext} from './TableContext';
|
|
24
25
|
import {useBaseTablePlugins} from './useBaseTablePlugins';
|
|
26
|
+
import {useScrollableArea} from '../hooks/useScrollableArea';
|
|
25
27
|
import {mergeProps} from '../utils';
|
|
26
28
|
import {themeProps} from '../utils/themeProps';
|
|
27
29
|
import type {
|
|
@@ -149,30 +151,36 @@ function TableScrollWrapper({
|
|
|
149
151
|
afterTable?: React.ReactNode;
|
|
150
152
|
}) {
|
|
151
153
|
const t = useTranslator();
|
|
154
|
+
const label = t('@astryx.table.label');
|
|
155
|
+
const {getViewportProps, getContentProps, state} = useScrollableArea({
|
|
156
|
+
axis: 'inline',
|
|
157
|
+
keyboardAccess: {owner: 'content'},
|
|
158
|
+
overscroll: 'contain',
|
|
159
|
+
});
|
|
152
160
|
const {ref, ...restHtmlProps} = htmlProps ?? {};
|
|
161
|
+
const viewportProps = getViewportProps<HTMLDivElement>({
|
|
162
|
+
role: 'group',
|
|
163
|
+
'aria-label': label,
|
|
164
|
+
...(state.inline.isScrollable ? {tabIndex: 0} : {}),
|
|
165
|
+
...restHtmlProps,
|
|
166
|
+
ref,
|
|
167
|
+
...mergeProps(
|
|
168
|
+
themeProps('table-scroll-wrapper'),
|
|
169
|
+
stylex.props(
|
|
170
|
+
scrollWrapperStyles.base,
|
|
171
|
+
scrollWrapperStyles.containerBleed,
|
|
172
|
+
...(pluginStyles ?? []),
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
|
|
153
177
|
return (
|
|
154
|
-
<div
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// htmlProps.
|
|
161
|
-
tabIndex={0}
|
|
162
|
-
role="group"
|
|
163
|
-
aria-label={t('@astryx.table.label')}
|
|
164
|
-
{...restHtmlProps}
|
|
165
|
-
{...mergeProps(
|
|
166
|
-
themeProps('table-scroll-wrapper'),
|
|
167
|
-
stylex.props(
|
|
168
|
-
scrollWrapperStyles.base,
|
|
169
|
-
scrollWrapperStyles.containerBleed,
|
|
170
|
-
...(pluginStyles ?? []),
|
|
171
|
-
),
|
|
172
|
-
)}>
|
|
173
|
-
{beforeTable}
|
|
174
|
-
{children}
|
|
175
|
-
{afterTable}
|
|
178
|
+
<div {...viewportProps}>
|
|
179
|
+
<div {...getContentProps<HTMLDivElement>()}>
|
|
180
|
+
{beforeTable}
|
|
181
|
+
{children}
|
|
182
|
+
{afterTable}
|
|
183
|
+
</div>
|
|
176
184
|
</div>
|
|
177
185
|
);
|
|
178
186
|
}
|