@astryxdesign/core 0.6.0 → 0.6.1

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.
Files changed (203) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
  9. package/dist/Center/Center.d.ts +23 -16
  10. package/dist/Center/Center.d.ts.map +1 -1
  11. package/dist/Center/Center.js +7 -5
  12. package/dist/CodeBlock/CodeBlock.js +2 -2
  13. package/dist/DateInput/DateInput.d.ts.map +1 -1
  14. package/dist/DateInput/DateInput.js +12 -2
  15. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  16. package/dist/DateTimeInput/DateTimeInput.js +12 -2
  17. package/dist/Field/Field.d.ts.map +1 -1
  18. package/dist/Field/Field.js +1 -0
  19. package/dist/Field/InputClearButton.d.ts +2 -2
  20. package/dist/Field/InputClearButton.d.ts.map +1 -1
  21. package/dist/Field/InputClearButton.js +5 -1
  22. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  23. package/dist/Field/PanelSearchInput.js +16 -4
  24. package/dist/FileInput/FileInput.d.ts.map +1 -1
  25. package/dist/FileInput/FileInput.js +12 -1
  26. package/dist/HoverCard/useHoverCard.js +2 -2
  27. package/dist/Indicator/CheckboxIndicator.js +2 -2
  28. package/dist/Indicator/RadioIndicator.js +2 -2
  29. package/dist/Layer/layerStack.d.ts +10 -0
  30. package/dist/Layer/layerStack.d.ts.map +1 -1
  31. package/dist/Layer/layerStack.js +21 -9
  32. package/dist/Layer/useLayerDismissal.d.ts +2 -3
  33. package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
  34. package/dist/Layer/useLayerDismissal.js +2 -3
  35. package/dist/NavIcon/NavIcon.js +2 -2
  36. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  37. package/dist/NumberInput/NumberInput.js +12 -2
  38. package/dist/Popover/usePopover.d.ts +3 -2
  39. package/dist/Popover/usePopover.d.ts.map +1 -1
  40. package/dist/Popover/usePopover.js +4 -2
  41. package/dist/ProgressBar/ProgressBar.js +2 -2
  42. package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
  43. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
  44. package/dist/ScrollableArea/ScrollableArea.js +144 -0
  45. package/dist/ScrollableArea/index.d.ts +11 -0
  46. package/dist/ScrollableArea/index.d.ts.map +1 -0
  47. package/dist/ScrollableArea/index.js +11 -0
  48. package/dist/StatusDot/StatusDot.js +2 -2
  49. package/dist/TextArea/TextArea.js +2 -2
  50. package/dist/TextInput/TextInput.d.ts.map +1 -1
  51. package/dist/TextInput/TextInput.js +19 -4
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +12 -2
  54. package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
  55. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/BaseTypeahead.js +56 -20
  57. package/dist/astryx.css +15 -0
  58. package/dist/hooks/index.d.ts +2 -0
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/index.js +1 -0
  61. package/dist/hooks/scrollGeometry.d.ts +24 -0
  62. package/dist/hooks/scrollGeometry.d.ts.map +1 -0
  63. package/dist/hooks/scrollGeometry.js +86 -0
  64. package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
  65. package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
  66. package/dist/hooks/scrollOwnerRegistry.js +24 -0
  67. package/dist/hooks/useFocusTrap.d.ts +8 -0
  68. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  69. package/dist/hooks/useFocusTrap.js +22 -11
  70. package/dist/hooks/useScrollableArea.d.ts +51 -0
  71. package/dist/hooks/useScrollableArea.d.ts.map +1 -0
  72. package/dist/hooks/useScrollableArea.js +287 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/theme/defineTheme.d.ts +2 -6
  77. package/dist/theme/defineTheme.d.ts.map +1 -1
  78. package/dist/theme/defineTheme.js +1 -1
  79. package/dist/theme/derivedVarRegistry.js +1 -1
  80. package/dist/theme/localTokens.d.ts +8 -11
  81. package/dist/theme/localTokens.d.ts.map +1 -1
  82. package/dist/theme/localTokens.js +17 -71
  83. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  84. package/dist/theme/themeAdaptations.js +4 -4
  85. package/dist/utils/themeProps.d.ts +10 -10
  86. package/dist/utils/themeProps.d.ts.map +1 -1
  87. package/dist/utils/themeProps.js +27 -10
  88. package/locales/en.json +16 -0
  89. package/locales/pseudo.json +12 -0
  90. package/package.json +7 -2
  91. package/src/AppShell/AppShell.test.tsx +36 -0
  92. package/src/AppShell/AppShell.tsx +4 -1
  93. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  94. package/src/Banner/Banner.test.tsx +3 -1
  95. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
  96. package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
  97. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
  98. package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
  99. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
  100. package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
  101. package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
  102. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
  103. package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
  104. package/src/Center/Center.doc.mjs +32 -28
  105. package/src/Center/Center.spec.md +225 -0
  106. package/src/Center/Center.test.tsx +42 -4
  107. package/src/Center/Center.tsx +24 -17
  108. package/src/Chat/ChatSystemMessage.test.tsx +2 -9
  109. package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
  110. package/src/CodeBlock/CodeBlock.tsx +2 -2
  111. package/src/DateInput/DateInput.test.tsx +4 -4
  112. package/src/DateInput/DateInput.tsx +15 -4
  113. package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
  114. package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
  115. package/src/DateTimeInput/DateTimeInput.tsx +18 -7
  116. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
  117. package/src/Field/Field.test.tsx +42 -0
  118. package/src/Field/Field.tsx +6 -0
  119. package/src/Field/InputClearButton.test.tsx +35 -1
  120. package/src/Field/InputClearButton.tsx +7 -3
  121. package/src/Field/PanelSearchInput.tsx +21 -8
  122. package/src/FieldStatus/FieldStatus.spec.md +27 -17
  123. package/src/FieldStatus/FieldStatus.test.tsx +7 -5
  124. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
  125. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
  126. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
  127. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
  128. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
  129. package/src/FileInput/FileInput.tsx +10 -1
  130. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
  131. package/src/HoverCard/HoverCard.doc.mjs +4 -4
  132. package/src/HoverCard/useHoverCard.tsx +2 -2
  133. package/src/Indicator/CheckboxIndicator.tsx +2 -2
  134. package/src/Indicator/Indicator.doc.mjs +2 -2
  135. package/src/Indicator/Indicator.test.tsx +1 -1
  136. package/src/Indicator/RadioIndicator.tsx +2 -2
  137. package/src/Layer/layerStack.ts +20 -9
  138. package/src/Layer/useLayerDismissal.ts +2 -3
  139. package/src/MultiSelector/MultiSelector.test.tsx +4 -4
  140. package/src/NavIcon/NavIcon.doc.mjs +4 -4
  141. package/src/NavIcon/NavIcon.tsx +2 -2
  142. package/src/NumberInput/NumberInput.tsx +18 -7
  143. package/src/Popover/Popover.doc.mjs +10 -10
  144. package/src/Popover/Popover.spec.md +55 -65
  145. package/src/Popover/Popover.test.tsx +29 -0
  146. package/src/Popover/usePopover.doc.mjs +4 -4
  147. package/src/Popover/usePopover.tsx +7 -4
  148. package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
  149. package/src/ProgressBar/ProgressBar.test.tsx +1 -31
  150. package/src/ProgressBar/ProgressBar.tsx +2 -2
  151. package/src/RadioList/RadioList.test.tsx +5 -144
  152. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
  153. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
  154. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
  155. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
  156. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
  157. package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
  158. package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
  159. package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
  160. package/src/ScrollableArea/ScrollableArea.tsx +259 -0
  161. package/src/ScrollableArea/index.ts +26 -0
  162. package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
  163. package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
  164. package/src/Selector/Selector.test.tsx +4 -4
  165. package/src/Spinner/Spinner.test.tsx +0 -18
  166. package/src/StatusDot/StatusDot.doc.mjs +4 -4
  167. package/src/StatusDot/StatusDot.tsx +2 -2
  168. package/src/TabList/TabList.test.tsx +5 -9
  169. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
  170. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
  171. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
  172. package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
  173. package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
  174. package/src/Table/Table.doc.mjs +2 -2
  175. package/src/TextArea/TextArea.doc.mjs +4 -4
  176. package/src/TextArea/TextArea.tsx +2 -2
  177. package/src/TextInput/TextInput.doc.mjs +2 -1
  178. package/src/TextInput/TextInput.test.tsx +94 -0
  179. package/src/TextInput/TextInput.tsx +22 -6
  180. package/src/TimeInput/TimeInput.tsx +18 -7
  181. package/src/Toast/ToastViewport.test.tsx +1 -39
  182. package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
  183. package/src/Typeahead/BaseTypeahead.spec.md +269 -0
  184. package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
  185. package/src/Typeahead/BaseTypeahead.tsx +99 -30
  186. package/src/hooks/index.ts +13 -0
  187. package/src/hooks/scrollGeometry.ts +155 -0
  188. package/src/hooks/scrollOwnerRegistry.ts +47 -0
  189. package/src/hooks/useFocusTrap.ts +22 -11
  190. package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
  191. package/src/hooks/useScrollableArea.doc.mjs +108 -0
  192. package/src/hooks/useScrollableArea.test.tsx +437 -0
  193. package/src/hooks/useScrollableArea.ts +469 -0
  194. package/src/index.ts +1 -0
  195. package/src/theme/defineTheme.test.ts +65 -105
  196. package/src/theme/defineTheme.ts +3 -9
  197. package/src/theme/derivedVarRegistry.ts +1 -1
  198. package/src/theme/localTokens.ts +25 -96
  199. package/src/theme/publicThemeHelperContract.test.ts +2 -2
  200. package/src/theme/themeAdaptations.test.ts +16 -42
  201. package/src/theme/themeAdaptations.ts +6 -9
  202. package/src/utils/themeProps.test.ts +29 -10
  203. package/src/utils/themeProps.ts +36 -17
@@ -0,0 +1,45 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tabs.a11y.known-failures.ts
5
+ * @input Uses KnownFailure from @astryxdesign/a11y-spec
6
+ * @output Exact public-safe Tabs migration failures
7
+ * @position AST-021 debt records; operational ownership remains outside public source.
8
+ */
9
+
10
+ import type {KnownFailure} from '@astryxdesign/a11y-spec';
11
+
12
+ const COMMON = {
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
+ ];
@@ -0,0 +1,92 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file Tabs.a11y.renders.tsx
7
+ * @input Uses TabList, Tab, React state, and the checked Tabs state inventory
8
+ * @output Consumer-realistic explicit Tabs render functions shared by jsdom and Storybook
9
+ * @position Binding fixture layer; navigation, callbacks, styling, and TabMenu stay local.
10
+ */
11
+
12
+ import {useState, type ReactElement} from 'react';
13
+ import {Tab, TabList} from '../index';
14
+ import type {TabsStateId} from './Tabs.a11y.states';
15
+
16
+ function TabsFixture({
17
+ direction = 'ltr',
18
+ disabledActivity = false,
19
+ hiddenSelectedLabel = false,
20
+ }: {
21
+ direction?: 'ltr' | 'rtl';
22
+ disabledActivity?: boolean;
23
+ hiddenSelectedLabel?: boolean;
24
+ }) {
25
+ const [value, setValue] = useState('overview');
26
+ const panels = {
27
+ overview: 'Everything at a glance.',
28
+ activity: 'What happened recently.',
29
+ members: 'Who has access.',
30
+ } as const;
31
+
32
+ return (
33
+ <div dir={direction}>
34
+ <button type="button">Before tabs</button>
35
+ <TabList
36
+ value={value}
37
+ onChange={setValue}
38
+ role="tablist"
39
+ aria-label="Project views">
40
+ <Tab
41
+ value="overview"
42
+ label="Overview"
43
+ id="tabs-a11y-tab-overview"
44
+ panelId="tabs-a11y-panel-overview"
45
+ isLabelHidden={hiddenSelectedLabel}
46
+ icon={
47
+ hiddenSelectedLabel ? <span aria-hidden="true">▣</span> : undefined
48
+ }
49
+ />
50
+ <Tab
51
+ value="activity"
52
+ label="Activity"
53
+ id="tabs-a11y-tab-activity"
54
+ panelId="tabs-a11y-panel-activity"
55
+ aria-disabled={disabledActivity || undefined}
56
+ />
57
+ <Tab
58
+ value="members"
59
+ label="Members"
60
+ id="tabs-a11y-tab-members"
61
+ panelId="tabs-a11y-panel-members"
62
+ />
63
+ </TabList>
64
+ {Object.entries(panels).map(([key, content]) => (
65
+ <div
66
+ key={key}
67
+ id={`tabs-a11y-panel-${key}`}
68
+ role="tabpanel"
69
+ aria-labelledby={`tabs-a11y-tab-${key}`}
70
+ tabIndex={0}
71
+ hidden={value !== key}>
72
+ {content}
73
+ </div>
74
+ ))}
75
+ <button type="button">After tabs</button>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export type TabsStateRender = () => ReactElement;
81
+
82
+ export const TABS_STATE_RENDERS: Record<TabsStateId, TabsStateRender> = {
83
+ 'tab-list-selected-ltr': () => <TabsFixture />,
84
+ 'tab-list-selected-rtl': () => <TabsFixture direction="rtl" />,
85
+ 'tab-list-disabled-skip': () => <TabsFixture disabledActivity />,
86
+ 'tab-selected-visible-label': () => <TabsFixture />,
87
+ 'tab-unselected-visible-label': () => <TabsFixture />,
88
+ 'tab-disabled-unselected': () => <TabsFixture disabledActivity />,
89
+ 'tab-selected-hidden-label': () => <TabsFixture hiddenSelectedLabel />,
90
+ 'tabpanel-active': () => <TabsFixture />,
91
+ 'tabpanel-inactive': () => <TabsFixture />,
92
+ };
@@ -0,0 +1,247 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tabs.a11y.states.ts
5
+ * @input Uses TabsStateFacts from @astryxdesign/a11y-spec
6
+ * @output The AST-021 binding inventory for explicit TabList Tabs parts and states
7
+ * @position Data-only inventory shared by jsdom, Storybook, and Chromium bindings.
8
+ */
9
+
10
+ import type {TabsStateFacts} from '@astryxdesign/a11y-spec';
11
+
12
+ export type TabsBinding = 'TabList' | 'Tab' | 'TabsComposition';
13
+
14
+ export interface TabsRelation {
15
+ readonly selector: string;
16
+ }
17
+
18
+ export interface TabsBindingState {
19
+ readonly id: string;
20
+ readonly binding: TabsBinding;
21
+ readonly summary: string;
22
+ readonly facts: TabsStateFacts;
23
+ readonly subject:
24
+ | {readonly role: 'tablist' | 'tab'; readonly name: string}
25
+ | {readonly selector: string};
26
+ readonly relations?: Readonly<Record<string, TabsRelation>>;
27
+ readonly storyId: string;
28
+ }
29
+
30
+ export type TabsBindingRow = (typeof TABS_BINDING_STATES)[number];
31
+ export type TabsStateId = (typeof TABS_BINDING_STATES)[number]['id'];
32
+
33
+ const RELATIONS = {
34
+ first: {selector: '[role="tab"][data-tab-value="overview"]'},
35
+ second: {selector: '[role="tab"][data-tab-value="activity"]'},
36
+ third: {selector: '[role="tab"][data-tab-value="members"]'},
37
+ panel: {selector: '#tabs-a11y-panel-activity[role="tabpanel"]'},
38
+ tab: {selector: '#tabs-a11y-tab-overview[role="tab"]'},
39
+ 'panel-first': {selector: '#tabs-a11y-panel-overview[role="tabpanel"]'},
40
+ 'panel-second': {selector: '#tabs-a11y-panel-activity[role="tabpanel"]'},
41
+ 'panel-third': {selector: '#tabs-a11y-panel-members[role="tabpanel"]'},
42
+ } as const;
43
+
44
+ function tablistFacts(overrides: Partial<TabsStateFacts> = {}): TabsStateFacts {
45
+ return {
46
+ part: 'tablist',
47
+ selected: null,
48
+ active: null,
49
+ disabled: null,
50
+ focusable: null,
51
+ visibleLabel: true,
52
+ controlsPanel: false,
53
+ controlledPanel: null,
54
+ labelledByTab: false,
55
+ labellingTab: null,
56
+ tabRelations: ['first', 'second', 'third'],
57
+ panelRelations: ['panel-first', 'panel-second', 'panel-third'],
58
+ disabledTabs: [],
59
+ selectedTab: 'first',
60
+ direction: 'ltr',
61
+ manualActivation: true,
62
+ tabReachable: true,
63
+ pointerSelection: true,
64
+ pointerCancellation: false,
65
+ ...overrides,
66
+ };
67
+ }
68
+
69
+ function tabFacts(overrides: Partial<TabsStateFacts> = {}): TabsStateFacts {
70
+ return tablistFacts({
71
+ part: 'tab',
72
+ selected: true,
73
+ disabled: false,
74
+ focusable: true,
75
+ controlsPanel: true,
76
+ controlledPanel: 'panel',
77
+ tabRelations: [],
78
+ panelRelations: [],
79
+ selectedTab: null,
80
+ pointerSelection: false,
81
+ pointerCancellation: true,
82
+ ...overrides,
83
+ });
84
+ }
85
+
86
+ function panelFacts(overrides: Partial<TabsStateFacts> = {}): TabsStateFacts {
87
+ return tablistFacts({
88
+ part: 'tabpanel',
89
+ selected: null,
90
+ active: true,
91
+ disabled: null,
92
+ focusable: null,
93
+ controlsPanel: false,
94
+ controlledPanel: null,
95
+ labelledByTab: true,
96
+ labellingTab: 'tab',
97
+ tabRelations: [],
98
+ panelRelations: [],
99
+ selectedTab: null,
100
+ tabReachable: false,
101
+ pointerSelection: false,
102
+ pointerCancellation: false,
103
+ ...overrides,
104
+ });
105
+ }
106
+
107
+ const story = (id: string) => `a11y-tabs-pattern--${id}`;
108
+
109
+ export const TABS_BINDING_STATES = [
110
+ {
111
+ id: 'tab-list-selected-ltr',
112
+ binding: 'TabList',
113
+ summary: 'an explicit horizontal LTR tablist with manual activation',
114
+ facts: tablistFacts(),
115
+ subject: {role: 'tablist', name: 'Project views'},
116
+ relations: RELATIONS,
117
+ storyId: story('tab-list-selected-ltr'),
118
+ },
119
+ {
120
+ id: 'tab-list-selected-rtl',
121
+ binding: 'TabList',
122
+ summary: 'an explicit horizontal RTL tablist with logical arrow movement',
123
+ facts: tablistFacts({direction: 'rtl'}),
124
+ subject: {role: 'tablist', name: 'Project views'},
125
+ relations: RELATIONS,
126
+ storyId: story('tab-list-selected-rtl'),
127
+ },
128
+ {
129
+ id: 'tab-list-disabled-skip',
130
+ binding: 'TabList',
131
+ summary: 'a tablist whose unavailable middle tab is skipped by arrows',
132
+ facts: tablistFacts({disabledTabs: ['second']}),
133
+ subject: {role: 'tablist', name: 'Project views'},
134
+ relations: RELATIONS,
135
+ storyId: story('tab-list-disabled-skip'),
136
+ },
137
+ {
138
+ id: 'tab-selected-visible-label',
139
+ binding: 'Tab',
140
+ summary: 'the selected available tab with a visible label',
141
+ facts: tabFacts(),
142
+ subject: {role: 'tab', name: 'Overview'},
143
+ relations: {
144
+ panel: {selector: '#tabs-a11y-panel-overview[role="tabpanel"]'},
145
+ },
146
+ storyId: story('tab-selected-visible-label'),
147
+ },
148
+ {
149
+ id: 'tab-unselected-visible-label',
150
+ binding: 'Tab',
151
+ summary: 'an available unselected tab with a visible label',
152
+ facts: tabFacts({selected: false, tabReachable: false}),
153
+ subject: {role: 'tab', name: 'Activity'},
154
+ relations: {panel: RELATIONS.panel},
155
+ storyId: story('tab-unselected-visible-label'),
156
+ },
157
+ {
158
+ id: 'tab-disabled-unselected',
159
+ binding: 'Tab',
160
+ summary: 'an unavailable unselected tab outside the roving order',
161
+ facts: tabFacts({
162
+ selected: false,
163
+ disabled: true,
164
+ tabReachable: false,
165
+ pointerCancellation: false,
166
+ }),
167
+ subject: {role: 'tab', name: 'Activity'},
168
+ relations: {panel: RELATIONS.panel},
169
+ storyId: story('tab-disabled-unselected'),
170
+ },
171
+ {
172
+ id: 'tab-selected-hidden-label',
173
+ binding: 'Tab',
174
+ summary: 'a selected icon-only tab named programmatically',
175
+ facts: tabFacts({visibleLabel: false}),
176
+ subject: {role: 'tab', name: 'Overview'},
177
+ relations: {
178
+ panel: {selector: '#tabs-a11y-panel-overview[role="tabpanel"]'},
179
+ },
180
+ storyId: story('tab-selected-hidden-label'),
181
+ },
182
+ {
183
+ id: 'tabpanel-active',
184
+ binding: 'TabsComposition',
185
+ summary: 'the visible panel labelled by the selected tab',
186
+ facts: panelFacts(),
187
+ subject: {selector: '#tabs-a11y-panel-overview[role="tabpanel"]'},
188
+ relations: {tab: RELATIONS.tab},
189
+ storyId: story('tabpanel-active'),
190
+ },
191
+ {
192
+ id: 'tabpanel-inactive',
193
+ binding: 'TabsComposition',
194
+ summary: 'an inactive hidden panel labelled by its tab',
195
+ facts: panelFacts({active: false}),
196
+ subject: {selector: '#tabs-a11y-panel-activity[role="tabpanel"]'},
197
+ relations: {
198
+ tab: {selector: '#tabs-a11y-tab-activity[role="tab"]'},
199
+ },
200
+ storyId: story('tabpanel-inactive'),
201
+ },
202
+ ] as const satisfies ReadonlyArray<TabsBindingState>;
203
+
204
+ export const TABS_EXCLUSIONS = [
205
+ {
206
+ owner: 'TabList and Tab without role="tablist"',
207
+ classification: 'out-of-scope',
208
+ reason:
209
+ 'The default navigation pattern remains a nav landmark with aria-current and belongs to navigation semantics, not the ARIA Tabs contract.',
210
+ },
211
+ {
212
+ owner: 'TabMenu trigger, popup, and options',
213
+ classification: 'out-of-scope',
214
+ reason:
215
+ 'TabMenu is a menu-button, menu, and radio-menu composition and is invalid as a child of an explicit tablist.',
216
+ },
217
+ {
218
+ owner: 'The table-filter page template’s narrow-view Tabs composition',
219
+ classification: 'out-of-scope',
220
+ reason:
221
+ 'The public template currently points every tab at one shared panel and labels that panel independently. Its page-owned composition mismatch is recorded separately rather than changing a template inside this pattern-migration pull request.',
222
+ },
223
+ {
224
+ owner: 'Home and End shortcuts',
225
+ classification: 'preserved',
226
+ reason:
227
+ 'The component keeps its local coverage; these APG-optional shortcuts are not promoted into the shared contract.',
228
+ },
229
+ {
230
+ owner: 'Vertical Tabs and automatic activation',
231
+ classification: 'needs-human',
232
+ reason:
233
+ 'Neither mode is currently exposed by TabList, and adopting either changes public behavior and interaction policy.',
234
+ },
235
+ {
236
+ owner: 'A public TabPanel primitive or generated relationship ids',
237
+ classification: 'needs-human',
238
+ reason:
239
+ 'The current API leaves panel elements and ids with the caller; introducing a primitive or ownership change is public API design.',
240
+ },
241
+ {
242
+ owner: 'No-selection, multi-selection, or selected-disabled policy',
243
+ classification: 'needs-human',
244
+ reason:
245
+ 'The current controlled API can be given invalid values, but no current record defines the supported recovery policy.',
246
+ },
247
+ ] as const;
@@ -0,0 +1,153 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @vitest-environment jsdom */
3
+
4
+ /**
5
+ * @file Tabs.a11y.test.tsx
6
+ * @input Uses the shared Tabs contract and complete explicit-Tabs binding inventory
7
+ * @output DOM-layer binding evidence; browser-owned outcomes remain explicitly unrun
8
+ * @position Fast migration lane for TabList, Tab, and caller-authored panel composition.
9
+ */
10
+
11
+ import {cleanup, render, screen} from '@testing-library/react';
12
+ import {describe, expect, it} from 'vitest';
13
+ import {
14
+ TABS_PATTERN,
15
+ checkAccessibilitySpec,
16
+ createJsdomHarness,
17
+ expectAccessibilitySpec,
18
+ summarize,
19
+ type BindingResult,
20
+ } from '@astryxdesign/a11y-spec';
21
+ import {TABS_KNOWN_FAILURES} from './Tabs.a11y.known-failures';
22
+ import {TABS_STATE_RENDERS} from './Tabs.a11y.renders';
23
+ import {
24
+ TABS_BINDING_STATES,
25
+ TABS_EXCLUSIONS,
26
+ type TabsBindingRow,
27
+ } from './Tabs.a11y.states';
28
+
29
+ function subjectFor(state: TabsBindingRow): Element {
30
+ if ('selector' in state.subject) {
31
+ const element = document.querySelector(state.subject.selector);
32
+ if (element == null) {
33
+ throw new Error(`no subject for ${state.id}`);
34
+ }
35
+ return element;
36
+ }
37
+ return screen.getByRole(state.subject.role, {
38
+ name: state.subject.name,
39
+ hidden: true,
40
+ });
41
+ }
42
+
43
+ function relatedFor(state: TabsBindingRow): Readonly<Record<string, Element>> {
44
+ return Object.fromEntries(
45
+ Object.entries(state.relations ?? {}).map(([name, relation]) => {
46
+ const element = document.querySelector(relation.selector);
47
+ if (element == null) {
48
+ throw new Error(`no related subject "${name}" for ${state.id}`);
49
+ }
50
+ return [name, element];
51
+ }),
52
+ );
53
+ }
54
+
55
+ async function expectState(state: TabsBindingRow): Promise<void> {
56
+ await expectAccessibilitySpec({
57
+ spec: TABS_PATTERN,
58
+ binding: state.binding,
59
+ state: state.id,
60
+ facts: state.facts,
61
+ knownFailures: TABS_KNOWN_FAILURES,
62
+ render: () => {
63
+ render(TABS_STATE_RENDERS[state.id]());
64
+ },
65
+ subject: () => subjectFor(state),
66
+ related: () => relatedFor(state),
67
+ cleanup,
68
+ });
69
+ }
70
+
71
+ async function checkState(state: TabsBindingRow): Promise<BindingResult> {
72
+ return checkAccessibilitySpec({
73
+ spec: TABS_PATTERN,
74
+ binding: state.binding,
75
+ state: state.id,
76
+ facts: state.facts,
77
+ knownFailures: TABS_KNOWN_FAILURES,
78
+ mount: async () => {
79
+ render(TABS_STATE_RENDERS[state.id]());
80
+ return createJsdomHarness({
81
+ subject: subjectFor(state),
82
+ related: relatedFor(state),
83
+ });
84
+ },
85
+ unmount: cleanup,
86
+ });
87
+ }
88
+
89
+ describe('the shared Tabs pattern, jsdom lane', () => {
90
+ it.each(
91
+ TABS_BINDING_STATES.map(
92
+ state =>
93
+ [`${state.binding} [${state.id}]`, state.summary, state] as const,
94
+ ),
95
+ )('%s — %s', async (_id, _summary, state) => {
96
+ await expectState(state);
97
+ });
98
+
99
+ it('runs the DOM layer and reports higher layers as unrun', async () => {
100
+ const results: BindingResult[] = [];
101
+ for (const state of TABS_BINDING_STATES) {
102
+ results.push(await checkState(state));
103
+ }
104
+ const report = summarize(TABS_PATTERN, results);
105
+ expect(report.counts.pass).toBeGreaterThan(0);
106
+ expect(report.unrunLayers).toEqual(['accessibility-tree', 'real-browser']);
107
+ expect(report.counts.unexpectedPass).toBe(0);
108
+ });
109
+ });
110
+
111
+ describe('the explicit Tabs binding inventory', () => {
112
+ it('names a distinct checked-in story for every state', () => {
113
+ const ids = TABS_BINDING_STATES.map(state => state.storyId);
114
+ expect(new Set(ids).size).toBe(ids.length);
115
+ });
116
+
117
+ it('binds TabList, Tab, and caller-authored panel composition', () => {
118
+ expect(
119
+ [...new Set(TABS_BINDING_STATES.map(state => state.binding))].sort(),
120
+ ).toEqual(['Tab', 'TabList', 'TabsComposition']);
121
+ });
122
+
123
+ it('records excluded and unsettled surfaces explicitly', () => {
124
+ expect(TABS_EXCLUSIONS.map(row => row.classification)).toEqual([
125
+ 'out-of-scope',
126
+ 'out-of-scope',
127
+ 'out-of-scope',
128
+ 'preserved',
129
+ 'needs-human',
130
+ 'needs-human',
131
+ 'needs-human',
132
+ ]);
133
+ expect(TABS_EXCLUSIONS.every(row => row.reason.length > 0)).toBe(true);
134
+ });
135
+
136
+ it('binds only explicit horizontal manual-activation Tabs', () => {
137
+ const wrong = TABS_BINDING_STATES.filter(state => {
138
+ if (state.facts.part !== 'tablist') {
139
+ return false;
140
+ }
141
+ return !state.facts.manualActivation;
142
+ });
143
+ expect(wrong).toEqual([]);
144
+ });
145
+
146
+ it('renders exactly one subject for every state', () => {
147
+ for (const state of TABS_BINDING_STATES) {
148
+ render(TABS_STATE_RENDERS[state.id]());
149
+ expect(subjectFor(state), state.id).toBeTruthy();
150
+ cleanup();
151
+ }
152
+ });
153
+ });
@@ -131,8 +131,8 @@ export const docs = {
131
131
  {className: 'astryx-table-row'},
132
132
  {className: 'astryx-table-cell', visualProps: ['density']},
133
133
  {className: 'astryx-table-header-cell', visualProps: ['density']},
134
- // Still emitted beside the names above, so themes written against
135
- // them keep working. Drop in the next major.
134
+ // Retained beside the canonical names for backwards compatibility.
135
+ // New themes use the canonical targets above.
136
136
  {className: 'astryx-base-table', deprecatedFor: 'table'},
137
137
  ],
138
138
  },
@@ -277,8 +277,8 @@ export const docs = {
277
277
  },
278
278
  {className: 'astryx-text-area-control'},
279
279
  {className: 'astryx-text-area-counter'},
280
- // Still emitted beside the names above, so themes written against
281
- // them keep working. Drop in the next major.
280
+ // Retained beside the canonical names for backwards compatibility.
281
+ // New themes use the canonical targets above.
282
282
  {
283
283
  className: 'astryx-textarea',
284
284
  visualProps: ['size', 'status'],
@@ -533,8 +533,8 @@ export const docsZh = {
533
533
  },
534
534
  {className: 'astryx-text-area-control'},
535
535
  {className: 'astryx-text-area-counter'},
536
- // Still emitted beside the names above, so themes written against
537
- // them keep working. Drop in the next major.
536
+ // Retained beside the canonical names for backwards compatibility.
537
+ // New themes use the canonical targets above.
538
538
  {
539
539
  className: 'astryx-textarea',
540
540
  visualProps: ['size', 'status'],
@@ -571,8 +571,8 @@ export function TextArea({
571
571
  disabled: isDisabled ? 'disabled' : null,
572
572
  readonly: isReadOnly ? 'readonly' : null,
573
573
  },
574
- // `textarea` ran the compound name together; themes styling it
575
- // keep working until the next major.
574
+ // `textarea` ran the compound name together; keep it emitted so
575
+ // existing themes continue to work.
576
576
  {legacyNames: ['textarea']},
577
577
  ),
578
578
  stylex.props(
@@ -74,7 +74,8 @@ export const docs = {
74
74
  {
75
75
  name: 'onEnter',
76
76
  type: '() => void',
77
- description: 'Callback fired when the user presses the Enter key.',
77
+ description:
78
+ 'Callback fired when the user presses the Enter key. IME-safe: Enter used to commit a Japanese/Chinese/Korean conversion does not fire it.',
78
79
  },
79
80
  {
80
81
  name: 'onKeyDown',