@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.
Files changed (261) hide show
  1. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  2. package/dist/Chat/ChatComposerInput.js +2 -2
  3. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  4. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  5. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  6. package/dist/Collapsible/Collapsible.js +6 -1
  7. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  8. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  9. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  10. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  11. package/dist/DateInput/DateInput.d.ts.map +1 -1
  12. package/dist/DateInput/DateInput.js +4 -4
  13. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  14. package/dist/DateInput/NativeDateField.js +16 -16
  15. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  16. package/dist/DateInput/TouchDateField.js +2 -2
  17. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  18. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  19. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  20. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  21. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  22. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  23. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  24. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  25. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  26. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  27. package/dist/Field/PanelSearchInput.js +1 -1
  28. package/dist/FileInput/FileInput.d.ts.map +1 -1
  29. package/dist/FileInput/FileInput.js +7 -7
  30. package/dist/Kbd/Kbd.d.ts +5 -3
  31. package/dist/Kbd/Kbd.d.ts.map +1 -1
  32. package/dist/Kbd/Kbd.js +36 -42
  33. package/dist/Link/Link.d.ts.map +1 -1
  34. package/dist/Link/Link.js +6 -2
  35. package/dist/Markdown/Markdown.d.ts +8 -2
  36. package/dist/Markdown/Markdown.d.ts.map +1 -1
  37. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  38. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  39. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  40. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  41. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  42. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  43. package/dist/Markdown/Markdown.js +262 -109
  44. package/dist/Markdown/ast.d.ts +162 -0
  45. package/dist/Markdown/ast.d.ts.map +1 -0
  46. package/dist/Markdown/ast.js +74 -0
  47. package/dist/Markdown/index.d.ts +5 -3
  48. package/dist/Markdown/index.d.ts.map +1 -1
  49. package/dist/Markdown/index.js +3 -2
  50. package/dist/Markdown/parser/index.d.ts +10 -0
  51. package/dist/Markdown/parser/index.d.ts.map +1 -0
  52. package/dist/Markdown/parser/index.js +10 -0
  53. package/dist/Markdown/parser.d.ts +126 -58
  54. package/dist/Markdown/parser.d.ts.map +1 -1
  55. package/dist/Markdown/parser.js +716 -167
  56. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  57. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  58. package/dist/Markdown/plugins/frontmatter.js +132 -0
  59. package/dist/Markdown/plugins/index.d.ts +16 -0
  60. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  61. package/dist/Markdown/plugins/index.js +13 -0
  62. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  63. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  64. package/dist/Markdown/plugins/protocol.js +957 -0
  65. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  66. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  67. package/dist/Markdown/plugins/semanticFence.js +213 -0
  68. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  69. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  70. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  71. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  72. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  73. package/dist/Markdown/plugins/textTransform.js +374 -0
  74. package/dist/Markdown/remark.d.ts +196 -0
  75. package/dist/Markdown/remark.d.ts.map +1 -0
  76. package/dist/Markdown/remark.js +1062 -0
  77. package/dist/Markdown/url.d.ts +4 -0
  78. package/dist/Markdown/url.d.ts.map +1 -0
  79. package/dist/Markdown/url.js +25 -0
  80. package/dist/Markdown/utils.d.ts +4 -2
  81. package/dist/Markdown/utils.d.ts.map +1 -1
  82. package/dist/Markdown/utils.js +2 -1
  83. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  84. package/dist/NumberInput/NumberInput.js +4 -4
  85. package/dist/Outline/index.d.ts +1 -0
  86. package/dist/Outline/index.d.ts.map +1 -1
  87. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  88. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  89. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  90. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  91. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  92. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  93. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  94. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  95. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  96. package/dist/ProgressBar/ProgressBar.js +36 -31
  97. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  98. package/dist/RadioList/RadioListItem.js +13 -1
  99. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  100. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  101. package/dist/Selector/Selector.d.ts.map +1 -1
  102. package/dist/Selector/Selector.js +1 -1
  103. package/dist/SideNav/SideNav.d.ts +2 -1
  104. package/dist/SideNav/SideNav.d.ts.map +1 -1
  105. package/dist/SideNav/SideNav.js +9 -3
  106. package/dist/Slider/Slider.d.ts.map +1 -1
  107. package/dist/Slider/Slider.js +14 -8
  108. package/dist/Spinner/Spinner.d.ts.map +1 -1
  109. package/dist/Spinner/Spinner.js +23 -7
  110. package/dist/Switch/Switch.d.ts.map +1 -1
  111. package/dist/Switch/Switch.js +11 -0
  112. package/dist/TabList/Tab.d.ts +1 -1
  113. package/dist/TabList/Tab.d.ts.map +1 -1
  114. package/dist/TabList/Tab.js +20 -7
  115. package/dist/Table/Table.d.ts +1 -0
  116. package/dist/Table/Table.d.ts.map +1 -1
  117. package/dist/Table/Table.js +29 -13
  118. package/dist/TextArea/TextArea.d.ts.map +1 -1
  119. package/dist/TextArea/TextArea.js +1 -1
  120. package/dist/TextInput/TextInput.d.ts.map +1 -1
  121. package/dist/TextInput/TextInput.js +2 -2
  122. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  123. package/dist/TimeInput/TimeInput.js +4 -4
  124. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  125. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  126. package/dist/ToggleButton/ToggleButton.js +7 -1
  127. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  128. package/dist/Typeahead/BaseTypeahead.js +16 -7
  129. package/dist/astryx.css +14 -6
  130. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  131. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  132. package/dist/utils/interactionOverlay.stylex.js +9 -0
  133. package/package.json +18 -3
  134. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  135. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  136. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  137. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  138. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  139. package/src/Chat/ChatComposerInput.tsx +10 -2
  140. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  141. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  142. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  143. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  144. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  145. package/src/Collapsible/Collapsible.test.tsx +21 -0
  146. package/src/Collapsible/Collapsible.tsx +5 -0
  147. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  148. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  149. package/src/DateInput/DateInput.tsx +6 -1
  150. package/src/DateInput/NativeDateField.tsx +24 -8
  151. package/src/DateInput/TouchDateField.tsx +6 -2
  152. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  153. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  154. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  155. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  156. package/src/Field/PanelSearchInput.tsx +5 -2
  157. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  158. package/src/FileInput/FileInput.tsx +11 -2
  159. package/src/Kbd/Kbd.doc.mjs +3 -3
  160. package/src/Kbd/Kbd.test.tsx +57 -1
  161. package/src/Kbd/Kbd.tsx +57 -37
  162. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  163. package/src/Link/Link.doc.mjs +11 -0
  164. package/src/Link/Link.test.tsx +24 -0
  165. package/src/Link/Link.tsx +5 -0
  166. package/src/Markdown/Markdown.doc.mjs +272 -0
  167. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  168. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  169. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  170. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  171. package/src/Markdown/Markdown.public.test.ts +126 -3
  172. package/src/Markdown/Markdown.spec.md +197 -44
  173. package/src/Markdown/Markdown.test.tsx +11 -0
  174. package/src/Markdown/Markdown.tsx +414 -135
  175. package/src/Markdown/ast.test.ts +307 -0
  176. package/src/Markdown/ast.ts +337 -0
  177. package/src/Markdown/incremental.test.ts +12 -0
  178. package/src/Markdown/index.ts +37 -1
  179. package/src/Markdown/modules/remark.spec.md +269 -0
  180. package/src/Markdown/parser/index.ts +64 -0
  181. package/src/Markdown/parser.public.test.ts +103 -0
  182. package/src/Markdown/parser.ts +1180 -235
  183. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  184. package/src/Markdown/plugins/frontmatter.ts +198 -0
  185. package/src/Markdown/plugins/index.ts +53 -0
  186. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  187. package/src/Markdown/plugins/protocol.ts +1618 -0
  188. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  189. package/src/Markdown/plugins/semanticFence.ts +424 -0
  190. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  191. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  192. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  193. package/src/Markdown/plugins/textTransform.ts +554 -0
  194. package/src/Markdown/remark.test.tsx +1877 -0
  195. package/src/Markdown/remark.ts +1574 -0
  196. package/src/Markdown/url.ts +30 -0
  197. package/src/Markdown/utils.ts +13 -1
  198. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  199. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  200. package/src/NumberInput/NumberInput.tsx +6 -1
  201. package/src/Outline/Outline.doc.mjs +7 -5
  202. package/src/Outline/Outline.spec.md +1 -1
  203. package/src/Outline/index.ts +1 -0
  204. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  205. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  206. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  207. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  208. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  209. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  210. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  211. package/src/ProgressBar/ProgressBar.tsx +59 -72
  212. package/src/RadioList/RadioList.doc.mjs +11 -0
  213. package/src/RadioList/RadioList.test.tsx +32 -0
  214. package/src/RadioList/RadioListItem.tsx +25 -1
  215. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  216. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  217. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  218. package/src/Selector/Selector.spec.md +12 -9
  219. package/src/Selector/Selector.tsx +6 -1
  220. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  221. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  222. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  223. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  224. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  225. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  226. package/src/SideNav/SideNav.doc.mjs +1 -1
  227. package/src/SideNav/SideNav.test.tsx +10 -0
  228. package/src/SideNav/SideNav.tsx +14 -2
  229. package/src/Slider/Slider.doc.mjs +11 -0
  230. package/src/Slider/Slider.test.tsx +128 -0
  231. package/src/Slider/Slider.tsx +25 -7
  232. package/src/Spinner/Spinner.test.tsx +4 -0
  233. package/src/Spinner/Spinner.tsx +31 -18
  234. package/src/Switch/Switch.doc.mjs +11 -0
  235. package/src/Switch/Switch.test.tsx +16 -0
  236. package/src/Switch/Switch.tsx +28 -0
  237. package/src/TabList/Tab.tsx +35 -7
  238. package/src/TabList/TabList.doc.mjs +11 -0
  239. package/src/TabList/TabList.test.tsx +66 -0
  240. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  241. package/src/Table/Table.doc.mjs +1 -1
  242. package/src/Table/Table.spec.md +54 -40
  243. package/src/Table/Table.test.tsx +83 -10
  244. package/src/Table/Table.tsx +30 -22
  245. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  246. package/src/TextArea/TextArea.tsx +6 -1
  247. package/src/TextInput/TextInput.test.tsx +20 -1
  248. package/src/TextInput/TextInput.tsx +6 -1
  249. package/src/TimeInput/TimeInput.tsx +6 -1
  250. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  251. package/src/ToggleButton/ToggleButton.tsx +9 -2
  252. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  253. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  254. package/src/TreeList/TreeList.spec.md +123 -75
  255. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  256. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  257. package/src/Typeahead/Typeahead.test.tsx +53 -0
  258. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  259. package/src/__tests__/pressState.ts +93 -0
  260. package/src/inputFontFloor.test.ts +106 -0
  261. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -0,0 +1,137 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file TableScroll.a11y.chromium.spec.ts
5
+ * @input Built responsive Table story and real Chromium wheel input
6
+ * @output Browser evidence for conditional inline containment and scroll chaining
7
+ * @position Table integration proof for the shared useScrollableArea behavior
8
+ */
9
+
10
+ import {expect, test, type Locator, type Page} from '@playwright/test';
11
+ import {
12
+ DEFAULT_STORYBOOK_DIR,
13
+ serveStorybook,
14
+ type StaticServer,
15
+ } from '@astryxdesign/a11y-spec/storybook';
16
+
17
+ let storybook: StaticServer;
18
+ test.beforeAll(async () => {
19
+ storybook = await serveStorybook(
20
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
21
+ );
22
+ });
23
+ test.afterAll(async () => {
24
+ await storybook?.close();
25
+ });
26
+
27
+ async function mount(page: Page) {
28
+ await page.goto(
29
+ `${storybook.origin}/iframe.html?id=core-table--responsive-scroll&viewMode=story`,
30
+ );
31
+ const wrappers = page.locator('.astryx-table-scroll-wrapper');
32
+ await expect(wrappers).toHaveCount(3);
33
+ return wrappers;
34
+ }
35
+
36
+ async function putInsideOuterScroller(
37
+ page: Page,
38
+ viewport: Locator,
39
+ viewportWidth: number,
40
+ contentWidth: number,
41
+ tableWidth: number,
42
+ ) {
43
+ await viewport.evaluate(
44
+ (node, sizes) => {
45
+ const outer = document.createElement('div');
46
+ outer.dataset.tableScrollOuter = '';
47
+ outer.style.inlineSize = `${sizes.viewportWidth}px`;
48
+ outer.style.overflowX = 'auto';
49
+ const content = document.createElement('div');
50
+ content.style.inlineSize = `${sizes.contentWidth}px`;
51
+ node.parentElement?.insertBefore(outer, node);
52
+ outer.append(content);
53
+ content.append(node);
54
+ (node as HTMLElement).style.inlineSize = `${sizes.tableWidth}px`;
55
+ },
56
+ {viewportWidth, contentWidth, tableWidth},
57
+ );
58
+ return page.locator('[data-table-scroll-outer]');
59
+ }
60
+
61
+ async function wheelInline(page: Page, target: Locator) {
62
+ const box = await target.boundingBox();
63
+ if (box == null) {
64
+ throw new Error('Table scroll viewport has no rendered bounds');
65
+ }
66
+ await page.mouse.move(box.x + Math.min(box.width / 2, 100), box.y + 20);
67
+ await page.mouse.wheel(200, 0);
68
+ }
69
+
70
+ test('an overflowing table contains horizontal wheel input at its edge', async ({
71
+ page,
72
+ }) => {
73
+ const wrappers = await mount(page);
74
+ const tableViewport = wrappers.nth(0);
75
+ await tableViewport.locator('table').evaluate(table => {
76
+ const viewport = table.closest<HTMLElement>('.astryx-table-scroll-wrapper');
77
+ if (viewport != null) {
78
+ table.style.minWidth = `${viewport.clientWidth + 400}px`;
79
+ }
80
+ });
81
+ await expect(tableViewport).toHaveAttribute('data-scrollable-inline', 'true');
82
+ await expect(tableViewport).toHaveAttribute('tabindex', '0');
83
+ await expect
84
+ .poll(async () =>
85
+ tableViewport.evaluate(
86
+ element => getComputedStyle(element).overscrollBehaviorX,
87
+ ),
88
+ )
89
+ .toBe('contain');
90
+
91
+ const outer = await putInsideOuterScroller(
92
+ page,
93
+ tableViewport,
94
+ 320,
95
+ 800,
96
+ 320,
97
+ );
98
+ await tableViewport.evaluate(element => {
99
+ element.scrollLeft = element.scrollWidth - element.clientWidth;
100
+ });
101
+ await wheelInline(page, tableViewport);
102
+ await expect
103
+ .poll(async () => outer.evaluate(element => element.scrollLeft))
104
+ .toBe(0);
105
+ });
106
+
107
+ test('a fitting table passes horizontal wheel input to its outer scroller', async ({
108
+ page,
109
+ }) => {
110
+ const wrappers = await mount(page);
111
+ const tableViewport = wrappers.nth(2);
112
+ const outer = await putInsideOuterScroller(
113
+ page,
114
+ tableViewport,
115
+ 320,
116
+ 800,
117
+ 800,
118
+ );
119
+
120
+ await expect(tableViewport).not.toHaveAttribute(
121
+ 'data-scrollable-inline',
122
+ 'true',
123
+ );
124
+ await expect(tableViewport).not.toHaveAttribute('tabindex');
125
+ await expect
126
+ .poll(async () =>
127
+ tableViewport.evaluate(
128
+ element => getComputedStyle(element).overscrollBehaviorX,
129
+ ),
130
+ )
131
+ .toBe('auto');
132
+
133
+ await wheelInline(page, tableViewport);
134
+ await expect
135
+ .poll(async () => outer.evaluate(element => element.scrollLeft))
136
+ .toBeGreaterThan(0);
137
+ });
@@ -95,9 +95,14 @@ const styles = stylex.create({
95
95
  paddingBlock: spacingVars['--spacing-1'],
96
96
  paddingInline: 'var(--_textarea-inline-padding)',
97
97
  fontFamily: typographyVars['--font-family-body'],
98
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
99
+ // control sits under 16px, and only iOS WebKit implements
100
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
98
101
  fontSize: {
99
102
  default: typeScaleVars['--text-body-size'],
100
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
103
+ '@media (pointer: coarse)': {
104
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
105
+ },
101
106
  },
102
107
  lineHeight: typeScaleVars['--text-body-leading'],
103
108
  color: colorVars['--color-text-primary'],
@@ -2,7 +2,8 @@
2
2
 
3
3
  /**
4
4
  * @file TextInput.test.tsx
5
- * @input Uses vitest, @testing-library/react, TextInput component
5
+ * @input Uses vitest, @testing-library/react, TextInput component, and the
6
+ * shared declaredValue StyleX read-back helper
6
7
  * @output Unit tests for TextInput component behavior
7
8
  * @position Testing; validates TextInput.tsx implementation
8
9
  *
@@ -13,6 +14,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest';
13
14
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
15
  import userEvent from '@testing-library/user-event';
15
16
  import {TestIcon} from '../__tests__/TestIcon';
17
+ import {declaredValue} from '../__tests__/stylexDeclarations';
16
18
  import {InputGroup} from '../InputGroup';
17
19
  import {TextInput} from './TextInput';
18
20
 
@@ -1113,3 +1115,20 @@ describe('TextInput clear button focus behavior', () => {
1113
1115
  rafSpy.mockRestore();
1114
1116
  });
1115
1117
  });
1118
+
1119
+ describe('TextInput text size', () => {
1120
+ it('renders the theme body size outside the iOS zoom gate', () => {
1121
+ // The 16px floor rides `@supports (-webkit-touch-callout: none)` inside
1122
+ // `@media (pointer: coarse)` — iOS Safari is the browser that zooms on
1123
+ // focus under 16px, so the floor targets it alone instead of every
1124
+ // coarse pointer. jsdom's CSSOM drops at-rule-wrapped declarations it
1125
+ // cannot parse, so the gate itself is pinned against the sources in
1126
+ // inputFontFloor.test.ts; what this asserts is the visible result off
1127
+ // the gate: the plain token size, not a floor.
1128
+ render(<TextInput label="Name" value="" onChange={() => {}} />);
1129
+ expect(declaredValue(screen.getByLabelText('Name'), 'font-size')).toBe(
1130
+ 'var(--text-body-size)',
1131
+ );
1132
+ });
1133
+ });
1134
+
@@ -58,9 +58,14 @@ const styles = stylex.create({
58
58
  borderStyle: 'none',
59
59
  padding: 0,
60
60
  fontFamily: typographyVars['--font-family-body'],
61
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
62
+ // control sits under 16px, and only iOS WebKit implements
63
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
61
64
  fontSize: {
62
65
  default: typeScaleVars['--text-body-size'],
63
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
66
+ '@media (pointer: coarse)': {
67
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
68
+ },
64
69
  },
65
70
  lineHeight: typeScaleVars['--text-body-leading'],
66
71
  color: colorVars['--color-text-primary'],
@@ -95,9 +95,14 @@ const styles = stylex.create({
95
95
  borderStyle: 'none',
96
96
  padding: 0,
97
97
  fontFamily: typographyVars['--font-family-body'],
98
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
99
+ // control sits under 16px, and only iOS WebKit implements
100
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
98
101
  fontSize: {
99
102
  default: typeScaleVars['--text-body-size'],
100
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
103
+ '@media (pointer: coarse)': {
104
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
105
+ },
101
106
  },
102
107
  lineHeight: typeScaleVars['--text-body-leading'],
103
108
  color: colorVars['--color-text-primary'],
@@ -570,6 +570,139 @@ describe('ToggleButtonGroup (multiple)', () => {
570
570
  });
571
571
  });
572
572
 
573
+ // =============================================================================
574
+ // Disabled state — family:buttons FR3 (disabled means non-operable)
575
+ // =============================================================================
576
+
577
+ /**
578
+ * Two ways a ToggleButton becomes unavailable, and the rule that binds them:
579
+ * a group disables everything it contains, and a member can disable itself
580
+ * while the group stays enabled. Neither source may cancel the other out.
581
+ *
582
+ * The tooltip cases are here at the attribute and callback level only. A
583
+ * tooltip'd disabled toggle carries `aria-disabled` instead of the native
584
+ * `disabled` attribute, so whether a REAL mouse press is refused is an engine
585
+ * fact that jsdom's synthetic click cannot settle — that half lives in
586
+ * ./__tests__/ToggleButton.a11y.chromium.spec.ts.
587
+ */
588
+ describe('disabled state', () => {
589
+ it('keeps a member disabled when the group disables nothing', async () => {
590
+ const user = userEvent.setup();
591
+ const handleChange = vi.fn();
592
+ render(
593
+ <ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
594
+ <ToggleButton value="list" label="List" isDisabled />
595
+ <ToggleButton value="grid" label="Grid" />
596
+ </ToggleButtonGroup>,
597
+ );
598
+
599
+ expect(screen.getByRole('button', {name: 'List'})).toBeDisabled();
600
+
601
+ await user.click(screen.getByRole('button', {name: 'List'}));
602
+ expect(handleChange).not.toHaveBeenCalled();
603
+ });
604
+
605
+ it('leaves the rest of an enabled group selectable', async () => {
606
+ const user = userEvent.setup();
607
+ const handleChange = vi.fn();
608
+ render(
609
+ <ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
610
+ <ToggleButton value="list" label="List" isDisabled />
611
+ <ToggleButton value="grid" label="Grid" />
612
+ </ToggleButtonGroup>,
613
+ );
614
+
615
+ expect(screen.getByRole('button', {name: 'Grid'})).toBeEnabled();
616
+
617
+ await user.click(screen.getByRole('button', {name: 'Grid'}));
618
+ expect(handleChange).toHaveBeenCalledWith('grid');
619
+ });
620
+
621
+ it('still disables a member that says nothing when the group is disabled', async () => {
622
+ const user = userEvent.setup();
623
+ const handleChange = vi.fn();
624
+ render(
625
+ <ToggleButtonGroup
626
+ value={null}
627
+ onChange={handleChange}
628
+ label="View mode"
629
+ isDisabled>
630
+ <ToggleButton value="list" label="List" />
631
+ </ToggleButtonGroup>,
632
+ );
633
+
634
+ expect(screen.getByRole('button', {name: 'List'})).toBeDisabled();
635
+
636
+ await user.click(screen.getByRole('button', {name: 'List'}));
637
+ expect(handleChange).not.toHaveBeenCalled();
638
+ });
639
+
640
+ it('keeps a tooltip-bearing disabled member focusable but inert', async () => {
641
+ const user = userEvent.setup();
642
+ const handleChange = vi.fn();
643
+ render(
644
+ <ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
645
+ <ToggleButton
646
+ value="list"
647
+ label="List"
648
+ tooltip="List view is unavailable for this dataset"
649
+ isDisabled
650
+ />
651
+ </ToggleButtonGroup>,
652
+ );
653
+
654
+ const member = screen.getByRole('button', {name: 'List'});
655
+ // The tooltip is the disabled reason, so the control stays reachable to
656
+ // read it: aria-disabled rather than the native attribute, which would
657
+ // take it out of the tab order along with its own explanation.
658
+ expect(member).toHaveAttribute('aria-disabled', 'true');
659
+ expect(member).not.toBeDisabled();
660
+
661
+ await user.click(member);
662
+ expect(handleChange).not.toHaveBeenCalled();
663
+ });
664
+
665
+ it('does not toggle a standalone disabled toggle that carries a tooltip', async () => {
666
+ const user = userEvent.setup();
667
+ const handleChange = vi.fn();
668
+ render(
669
+ <ToggleButton
670
+ label="Bold"
671
+ tooltip="Formatting is locked for this document"
672
+ isPressed={false}
673
+ onPressedChange={handleChange}
674
+ isDisabled
675
+ />,
676
+ );
677
+
678
+ const toggle = screen.getByRole('button', {name: 'Bold'});
679
+ expect(toggle).toHaveAttribute('aria-disabled', 'true');
680
+
681
+ await user.click(toggle);
682
+ expect(handleChange).not.toHaveBeenCalled();
683
+ expect(toggle).toHaveAttribute('aria-pressed', 'false');
684
+ });
685
+
686
+ it('still toggles an enabled toggle that carries the same tooltip', async () => {
687
+ const user = userEvent.setup();
688
+ const handleChange = vi.fn();
689
+ render(
690
+ <ToggleButton
691
+ label="Bold"
692
+ tooltip="Bold the selected text"
693
+ isPressed={false}
694
+ onPressedChange={handleChange}
695
+ />,
696
+ );
697
+
698
+ const toggle = screen.getByRole('button', {name: 'Bold'});
699
+ expect(toggle).not.toHaveAttribute('aria-disabled');
700
+
701
+ await user.click(toggle);
702
+ expect(handleChange).toHaveBeenCalledWith(true, expect.anything());
703
+ });
704
+ });
705
+
573
706
  // jsdom cannot emulate forced-colors rendering, so these assert that the
574
707
  // compiled output includes the forced-colors rules; visual behavior needs
575
708
  // manual verification under Windows High Contrast.
@@ -154,7 +154,8 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
154
154
 
155
155
  /**
156
156
  * Whether the button is disabled.
157
- * When used inside ToggleButtonGroup, the group's isDisabled overrides this.
157
+ * When used inside ToggleButtonGroup, a disabled group disables this button
158
+ * too, but an enabled group does not re-enable a button that disables itself.
158
159
  * @default false
159
160
  */
160
161
  isDisabled?: boolean;
@@ -263,7 +264,13 @@ export function ToggleButton({
263
264
  ? group.selectedValues.has(value)
264
265
  : (isPressedProp ?? false);
265
266
  const size = sizeProp ?? group?.size ?? 'md';
266
- const isDisabled = group?.isDisabled ?? isDisabledProp;
267
+ // Either source disabling this button is enough. `??` could not express that:
268
+ // the group always supplies a boolean (its own prop defaults to false), so the
269
+ // fallback never ran and an enabled group handed a member that had disabled
270
+ // itself its availability back. A group still disables everything it contains
271
+ // — that is the half `??` got right — but it cannot re-enable a member
272
+ // (family:buttons FR3).
273
+ const isDisabled = (group?.isDisabled ?? false) || isDisabledProp;
267
274
 
268
275
  // Track the pressed state optimistically so the button reflects the intended
269
276
  // state immediately while an async action is pending. The optimistic update
@@ -0,0 +1,209 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ToggleButton.a11y.chromium.spec.ts
5
+ * @input Uses @playwright/test, the a11y-spec static Storybook server, and the
6
+ * checked-in ToggleButtonDisabledA11y stories
7
+ * @output Real-browser evidence for `docs/families/buttons.md` FR3 — disabled
8
+ * means non-operable — across the two ways a ToggleButton becomes disabled,
9
+ * plus the enabled control that catches a fix which disables too much.
10
+ * @position The half of FR3 that jsdom cannot honestly reach. A disabled toggle
11
+ * kept focusable by a tooltip carries `aria-disabled` instead of the native
12
+ * `disabled` attribute, so whether a real mouse press is refused is an engine
13
+ * fact — jsdom's synthetic click does not settle it
14
+ * (`docs/specs/AST-013/spec.md`: a browser claim is made in a browser).
15
+ *
16
+ * Each state drives a checked-in story rather than a page this file builds, so
17
+ * the reproduction path for any failure is a URL a person can open
18
+ * (`docs/specs/AST-009/spec.md` FR30). Each story counts its own activations,
19
+ * which is what `activationsOn` reads — a toggle that refuses a press leaves no
20
+ * trace on itself.
21
+ *
22
+ * Presses go through `page.mouse` at the control's own centre rather than
23
+ * `locator.click()`. Playwright's actionability wait would refuse to press a
24
+ * natively disabled control at all, which would prove nothing about what the
25
+ * engine does with a press that really lands.
26
+ *
27
+ * SYNC: Fixtures live in
28
+ * /apps/storybook/stories/ToggleButtonDisabledA11y.stories.tsx.
29
+ */
30
+
31
+ import {expect, test, type Page} from '@playwright/test';
32
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
33
+ import {
34
+ DEFAULT_STORYBOOK_DIR,
35
+ serveStorybook,
36
+ type StaticServer,
37
+ } from '@astryxdesign/a11y-spec/storybook';
38
+
39
+ const STORY_BASE = 'a11y-togglebutton-disabled-state';
40
+
41
+ let storybook: StaticServer;
42
+
43
+ test.beforeAll(async () => {
44
+ storybook = await serveStorybook(
45
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
46
+ );
47
+ });
48
+
49
+ test.afterAll(async () => {
50
+ await storybook?.close();
51
+ });
52
+
53
+ /** Load a story and wait for the control the assertions press. */
54
+ async function mountStory(page: Page, storyId: string): Promise<void> {
55
+ await page.goto(
56
+ `${storybook.origin}/iframe.html?id=${STORY_BASE}--${storyId}&viewMode=story`,
57
+ {waitUntil: 'load'},
58
+ );
59
+ await holdMotionStill(page);
60
+ await page
61
+ .locator('#storybook-root')
62
+ .getByRole('button')
63
+ .first()
64
+ .waitFor({state: 'attached'});
65
+ }
66
+
67
+ /**
68
+ * How many times this story's own handler has run.
69
+ *
70
+ * The story publishes the count as an attribute, so this reads a fact the story
71
+ * renders rather than instrumenting the page from the test — a person opening
72
+ * the story sees the same number the spec does.
73
+ */
74
+ async function activationsOn(page: Page): Promise<number> {
75
+ const raw = await page
76
+ .locator('[data-a11y-activations]')
77
+ .first()
78
+ .getAttribute('data-a11y-activations');
79
+ return Number(raw ?? '0');
80
+ }
81
+
82
+ /**
83
+ * A real mouse press at the control's own centre, landing whether or not
84
+ * Playwright would consider the control actionable. This is the press the
85
+ * expectations are about: a user aiming at a control that looks unavailable.
86
+ */
87
+ async function pressWithMouse(page: Page, name: string): Promise<void> {
88
+ const control = page
89
+ .locator('#storybook-root')
90
+ .getByRole('button', {name, includeHidden: true})
91
+ .first();
92
+ const box = await control.boundingBox();
93
+ if (box == null) {
94
+ throw new Error(
95
+ `"${name}" has no layout box, so no mouse press could land on it`,
96
+ );
97
+ }
98
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
99
+ }
100
+
101
+ /**
102
+ * Whether the engine reports this control as unavailable, by either of the two
103
+ * spellings FR3 permits: the native attribute, or focusable `aria-disabled`.
104
+ */
105
+ async function isUnavailable(page: Page, name: string): Promise<boolean> {
106
+ return page
107
+ .locator('#storybook-root')
108
+ .getByRole('button', {name, includeHidden: true})
109
+ .first()
110
+ .evaluate(
111
+ element =>
112
+ (element as HTMLButtonElement).disabled ||
113
+ element.getAttribute('aria-disabled') === 'true',
114
+ );
115
+ }
116
+
117
+ test('a member that disables itself stays disabled inside an enabled group', async ({
118
+ page,
119
+ }) => {
120
+ await mountStory(page, 'group-enabled-member-disabled');
121
+
122
+ expect(
123
+ await isUnavailable(page, 'List'),
124
+ 'the member sets isDisabled, so it must be exposed as unavailable even though the group disables nothing',
125
+ ).toBe(true);
126
+
127
+ await pressWithMouse(page, 'List');
128
+ expect(
129
+ await activationsOn(page),
130
+ 'a disabled member must not invoke the group selection callback (family:buttons FR3)',
131
+ ).toBe(0);
132
+ });
133
+
134
+ test('an enabled member of the same group still toggles', async ({page}) => {
135
+ await mountStory(page, 'group-enabled-member-disabled');
136
+
137
+ expect(await isUnavailable(page, 'Grid')).toBe(false);
138
+
139
+ await pressWithMouse(page, 'Grid');
140
+ expect(
141
+ await activationsOn(page),
142
+ 'the sibling says nothing about its own availability and the group disables nothing, so it must still select',
143
+ ).toBe(1);
144
+ });
145
+
146
+ test('a group disabled state still disables a member that says nothing', async ({
147
+ page,
148
+ }) => {
149
+ await mountStory(page, 'group-disabled-member-silent');
150
+
151
+ expect(await isUnavailable(page, 'List')).toBe(true);
152
+
153
+ await pressWithMouse(page, 'List');
154
+ expect(
155
+ await activationsOn(page),
156
+ 'a group disabled state overrides member availability (family:buttons FR3)',
157
+ ).toBe(0);
158
+ });
159
+
160
+ test('a disabled toggle kept focusable by its tooltip refuses a mouse press', async ({
161
+ page,
162
+ }) => {
163
+ await mountStory(page, 'disabled-with-tooltip');
164
+
165
+ const control = page.locator('#storybook-root').getByRole('button').first();
166
+
167
+ // The tooltip is why this control is focusable rather than natively disabled,
168
+ // so the press has to be made the way a user reaches it: hover first, let the
169
+ // tip come up, then press. Pressing cold would skip the hover state the
170
+ // tooltip's own pointer listeners run in.
171
+ await control.hover();
172
+ await expect(page.getByRole('tooltip')).toBeVisible();
173
+
174
+ expect(
175
+ await control.evaluate(element => element.getAttribute('aria-disabled')),
176
+ 'a tooltip keeps the reason reachable by making the control focusable rather than natively disabled',
177
+ ).toBe('true');
178
+ expect(await control.getAttribute('aria-pressed')).toBe('false');
179
+
180
+ await pressWithMouse(page, 'Bold');
181
+
182
+ expect(
183
+ await activationsOn(page),
184
+ 'focusable is not operable: a disabled toggle must not invoke onPressedChange from a mouse press (family:buttons FR3)',
185
+ ).toBe(0);
186
+ expect(
187
+ await control.getAttribute('aria-pressed'),
188
+ 'the pressed state must not flip',
189
+ ).toBe('false');
190
+ });
191
+
192
+ test('an enabled toggle with the same tooltip still toggles on a mouse press', async ({
193
+ page,
194
+ }) => {
195
+ await mountStory(page, 'enabled-with-tooltip');
196
+
197
+ const control = page.locator('#storybook-root').getByRole('button').first();
198
+ await control.hover();
199
+ await expect(page.getByRole('tooltip')).toBeVisible();
200
+ expect(await control.getAttribute('aria-pressed')).toBe('false');
201
+
202
+ await pressWithMouse(page, 'Bold');
203
+
204
+ expect(
205
+ await activationsOn(page),
206
+ 'the enabled path must be untouched by the disabled-state fix',
207
+ ).toBe(1);
208
+ expect(await control.getAttribute('aria-pressed')).toBe('true');
209
+ });