@astryxdesign/core 0.1.4-canary.37c19d3 → 0.1.4-canary.39ffd21

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 (226) hide show
  1. package/dist/Banner/Banner.d.ts.map +1 -1
  2. package/dist/Banner/Banner.js +1 -8
  3. package/dist/Calendar/Calendar.d.ts.map +1 -1
  4. package/dist/Calendar/Calendar.js +11 -31
  5. package/dist/Calendar/styles.d.ts +2 -2
  6. package/dist/Calendar/styles.d.ts.map +1 -1
  7. package/dist/Calendar/styles.js +2 -2
  8. package/dist/Carousel/Carousel.d.ts.map +1 -1
  9. package/dist/Carousel/Carousel.js +0 -1
  10. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  11. package/dist/Chat/ChatToolCalls.js +1 -8
  12. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  13. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  14. package/dist/CheckboxInput/CheckboxInput.js +1 -3
  15. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  16. package/dist/ClickableCard/ClickableCard.js +3 -26
  17. package/dist/CodeBlock/CodeBlock.d.ts +1 -10
  18. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  19. package/dist/CodeBlock/CodeBlock.js +5 -23
  20. package/dist/Collapsible/Collapsible.d.ts +3 -9
  21. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  22. package/dist/Collapsible/Collapsible.js +13 -101
  23. package/dist/Collapsible/CollapsibleGroup.d.ts +7 -39
  24. package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
  25. package/dist/Collapsible/CollapsibleGroup.js +9 -70
  26. package/dist/Collapsible/CollapsibleGroupContext.d.ts +0 -31
  27. package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
  28. package/dist/Collapsible/CollapsibleGroupContext.js +3 -33
  29. package/dist/Collapsible/index.d.ts +0 -1
  30. package/dist/Collapsible/index.d.ts.map +1 -1
  31. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  32. package/dist/ContextMenu/ContextMenu.js +0 -2
  33. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  34. package/dist/DateTimeInput/DateTimeInput.js +0 -2
  35. package/dist/Grid/Grid.d.ts +3 -4
  36. package/dist/Grid/Grid.d.ts.map +1 -1
  37. package/dist/Grid/Grid.js +9 -26
  38. package/dist/Icon/Icon.d.ts.map +1 -1
  39. package/dist/Icon/Icon.js +1 -5
  40. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  41. package/dist/Lightbox/Lightbox.js +2 -26
  42. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  43. package/dist/NumberInput/NumberInput.js +16 -32
  44. package/dist/Pagination/Pagination.d.ts.map +1 -1
  45. package/dist/Pagination/Pagination.js +36 -87
  46. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  47. package/dist/PowerSearch/PowerSearch.js +21 -39
  48. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  49. package/dist/ProgressBar/ProgressBar.js +1 -2
  50. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  51. package/dist/Resizable/ResizeHandle.js +2 -10
  52. package/dist/SideNav/SideNav.d.ts.map +1 -1
  53. package/dist/SideNav/SideNav.js +2 -6
  54. package/dist/TabList/TabMenu.d.ts.map +1 -1
  55. package/dist/TabList/TabMenu.js +7 -34
  56. package/dist/Table/BaseTable.d.ts.map +1 -1
  57. package/dist/Table/BaseTable.js +2 -12
  58. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  59. package/dist/Table/plugins/columnResize/useTableColumnResize.js +2 -5
  60. package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
  61. package/dist/Table/plugins/pagination/paginateData.js +1 -5
  62. package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
  63. package/dist/Table/plugins/selection/useTableSelectionState.js +1 -11
  64. package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
  65. package/dist/Table/plugins/sortable/useTableSortableState.js +4 -8
  66. package/dist/Thumbnail/Thumbnail.d.ts +2 -3
  67. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  68. package/dist/Thumbnail/Thumbnail.js +0 -1
  69. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  70. package/dist/TimeInput/TimeInput.js +1 -6
  71. package/dist/Toast/Toast.d.ts.map +1 -1
  72. package/dist/Toast/Toast.js +3 -11
  73. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  74. package/dist/Toast/ToastViewport.js +0 -12
  75. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  76. package/dist/Typeahead/BaseTypeahead.js +2 -5
  77. package/dist/astryx.css +0 -7
  78. package/dist/astryx.umd.js +48 -48
  79. package/dist/astryx.umd.js.map +4 -4
  80. package/dist/hooks/focusableSelector.d.ts +1 -1
  81. package/dist/hooks/focusableSelector.d.ts.map +1 -1
  82. package/dist/hooks/focusableSelector.js +1 -1
  83. package/dist/hooks/useFocusTrap.d.ts +0 -3
  84. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  85. package/dist/hooks/useFocusTrap.js +1 -38
  86. package/dist/theme/defineTheme.d.ts +1 -1
  87. package/dist/theme/defineTheme.d.ts.map +1 -1
  88. package/dist/theme/expandColorScale.d.ts.map +1 -1
  89. package/dist/theme/expandColorScale.js +3 -10
  90. package/dist/theme/hct.d.ts +15 -0
  91. package/dist/theme/hct.d.ts.map +1 -1
  92. package/dist/theme/hct.js +9 -9
  93. package/dist/theme/tokens.d.ts +2 -10
  94. package/dist/theme/tokens.d.ts.map +1 -1
  95. package/dist/theme/tokens.js +4 -220
  96. package/dist/utils/index.d.ts +0 -2
  97. package/dist/utils/index.d.ts.map +1 -1
  98. package/dist/utils/index.js +1 -2
  99. package/dist/utils/timeParser.d.ts.map +1 -1
  100. package/dist/utils/timeParser.js +5 -8
  101. package/package.json +1 -1
  102. package/src/Avatar/Avatar.doc.mjs +0 -11
  103. package/src/Banner/Banner.doc.mjs +0 -8
  104. package/src/Banner/Banner.test.tsx +0 -38
  105. package/src/Banner/Banner.tsx +1 -8
  106. package/src/Calendar/Calendar.doc.mjs +1 -1
  107. package/src/Calendar/Calendar.test.tsx +4 -112
  108. package/src/Calendar/Calendar.tsx +5 -26
  109. package/src/Calendar/styles.ts +2 -12
  110. package/src/Carousel/Carousel.doc.mjs +12 -12
  111. package/src/Carousel/Carousel.test.tsx +0 -13
  112. package/src/Carousel/Carousel.tsx +0 -1
  113. package/src/Chat/Chat.doc.mjs +1 -1
  114. package/src/Chat/ChatToolCalls.test.tsx +0 -55
  115. package/src/Chat/ChatToolCalls.tsx +12 -10
  116. package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
  117. package/src/CheckboxInput/CheckboxInput.test.tsx +4 -71
  118. package/src/CheckboxInput/CheckboxInput.tsx +0 -2
  119. package/src/Citation/Citation.doc.mjs +0 -5
  120. package/src/ClickableCard/ClickableCard.tsx +2 -45
  121. package/src/CodeBlock/CodeBlock.doc.mjs +2 -7
  122. package/src/CodeBlock/CodeBlock.test.tsx +3 -87
  123. package/src/CodeBlock/CodeBlock.tsx +3 -43
  124. package/src/Collapsible/Collapsible.doc.mjs +4 -8
  125. package/src/Collapsible/Collapsible.tsx +10 -86
  126. package/src/Collapsible/CollapsibleGroup.doc.mjs +3 -27
  127. package/src/Collapsible/CollapsibleGroup.test.tsx +0 -288
  128. package/src/Collapsible/CollapsibleGroup.tsx +9 -107
  129. package/src/Collapsible/CollapsibleGroupContext.tsx +2 -42
  130. package/src/Collapsible/index.ts +0 -4
  131. package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +0 -5
  132. package/src/CommandPalette/CommandPaletteGroup.doc.mjs +0 -10
  133. package/src/CommandPalette/CommandPaletteInput.doc.mjs +0 -6
  134. package/src/CommandPalette/CommandPaletteItem.doc.mjs +0 -6
  135. package/src/CommandPalette/CommandPaletteList.doc.mjs +0 -9
  136. package/src/ContextMenu/ContextMenu.doc.mjs +0 -15
  137. package/src/ContextMenu/ContextMenu.test.tsx +0 -15
  138. package/src/ContextMenu/ContextMenu.tsx +0 -2
  139. package/src/ContextMenu/ContextMenuItem.doc.mjs +0 -6
  140. package/src/DateTimeInput/DateTimeInput.test.tsx +0 -70
  141. package/src/DateTimeInput/DateTimeInput.tsx +0 -2
  142. package/src/Dialog/DialogHeader.doc.mjs +0 -45
  143. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
  144. package/src/Field/FieldLabel.test.tsx +2 -2
  145. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -0
  146. package/src/Grid/Grid.test.tsx +18 -43
  147. package/src/Grid/Grid.tsx +9 -26
  148. package/src/Icon/Icon.test.tsx +29 -41
  149. package/src/Icon/Icon.tsx +2 -6
  150. package/src/Layout/LayoutContent.doc.mjs +0 -12
  151. package/src/Layout/LayoutPanel.doc.mjs +0 -40
  152. package/src/Lightbox/Lightbox.doc.mjs +0 -5
  153. package/src/Lightbox/Lightbox.test.tsx +2 -123
  154. package/src/Lightbox/Lightbox.tsx +1 -29
  155. package/src/NavMenu/NavMenu.doc.mjs +0 -15
  156. package/src/NumberInput/NumberInput.test.tsx +3 -48
  157. package/src/NumberInput/NumberInput.tsx +15 -32
  158. package/src/Overlay/Overlay.doc.mjs +0 -6
  159. package/src/Pagination/Pagination.test.tsx +0 -143
  160. package/src/Pagination/Pagination.tsx +23 -75
  161. package/src/Popover/Popover.test.tsx +0 -65
  162. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  163. package/src/PowerSearch/PowerSearch.test.tsx +1 -109
  164. package/src/PowerSearch/PowerSearch.tsx +16 -37
  165. package/src/ProgressBar/ProgressBar.test.tsx +0 -10
  166. package/src/ProgressBar/ProgressBar.tsx +1 -2
  167. package/src/Resizable/ResizeHandle.tsx +2 -10
  168. package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
  169. package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
  170. package/src/SideNav/SideNav.doc.mjs +1 -7
  171. package/src/SideNav/SideNav.test.tsx +0 -20
  172. package/src/SideNav/SideNav.tsx +2 -6
  173. package/src/TabList/TabList.test.tsx +1 -185
  174. package/src/TabList/TabMenu.tsx +9 -45
  175. package/src/Table/BaseTable.tsx +1 -11
  176. package/src/Table/Table.test.tsx +0 -51
  177. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +0 -13
  178. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +1 -3
  179. package/src/Table/plugins/pagination/paginateData.ts +1 -5
  180. package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -13
  181. package/src/Table/plugins/selection/useTableSelectionState.test.tsx +0 -23
  182. package/src/Table/plugins/selection/useTableSelectionState.tsx +6 -11
  183. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +1 -35
  184. package/src/Table/plugins/sortable/useTableSortableState.tsx +4 -8
  185. package/src/TextArea/TextArea.test.tsx +3 -3
  186. package/src/TextInput/TextInput.test.tsx +3 -3
  187. package/src/Thumbnail/Thumbnail.test.tsx +2 -36
  188. package/src/Thumbnail/Thumbnail.tsx +2 -4
  189. package/src/TimeInput/TimeInput.test.tsx +0 -37
  190. package/src/TimeInput/TimeInput.tsx +1 -11
  191. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  192. package/src/Toast/Toast.tsx +3 -11
  193. package/src/Toast/ToastViewport.test.tsx +0 -64
  194. package/src/Toast/ToastViewport.tsx +0 -12
  195. package/src/ToggleButton/ToggleButton.doc.mjs +0 -1
  196. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  197. package/src/TopNav/TopNav.doc.mjs +1 -1
  198. package/src/TreeList/TreeList.doc.mjs +2 -2
  199. package/src/Typeahead/BaseTypeahead.doc.mjs +0 -19
  200. package/src/Typeahead/BaseTypeahead.tsx +2 -5
  201. package/src/Typeahead/Typeahead.doc.mjs +1 -1
  202. package/src/Typeahead/Typeahead.test.tsx +1 -45
  203. package/src/hooks/focusableSelector.ts +1 -1
  204. package/src/hooks/useFocusTrap.doc.mjs +2 -4
  205. package/src/hooks/useFocusTrap.test.tsx +1 -149
  206. package/src/hooks/useFocusTrap.ts +1 -50
  207. package/src/theme/SyntaxTheme.doc.mjs +4 -4
  208. package/src/theme/defineTheme.ts +13 -4
  209. package/src/theme/derivedVarRegistry.test.ts +2 -2
  210. package/src/theme/expandColorScale.test.ts +0 -29
  211. package/src/theme/expandColorScale.ts +6 -11
  212. package/src/theme/hct.ts +17 -9
  213. package/src/theme/tokens.test.ts +9 -182
  214. package/src/theme/tokens.ts +4 -239
  215. package/src/theme/useTheme.test.tsx +0 -20
  216. package/src/utils/index.ts +0 -3
  217. package/src/utils/timeParser.test.ts +0 -13
  218. package/src/utils/timeParser.ts +5 -8
  219. package/dist/utils/color.d.ts +0 -42
  220. package/dist/utils/color.d.ts.map +0 -1
  221. package/dist/utils/color.js +0 -150
  222. package/src/Resizable/ResizeHandle.test.tsx +0 -180
  223. package/src/__tests__/TestIcon.tsx +0 -26
  224. package/src/docPropReferences.test.ts +0 -178
  225. package/src/utils/color.test.ts +0 -133
  226. package/src/utils/color.ts +0 -148
@@ -10,11 +10,10 @@
10
10
  */
11
11
 
12
12
  import {useState} from 'react';
13
- import {describe, it, expect, vi, beforeAll, afterAll, afterEach} from 'vitest';
13
+ import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
14
14
  import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
16
  import {PowerSearch} from './PowerSearch';
17
- import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
18
17
  import type {PowerSearchConfig, PowerSearchFilter} from './types';
19
18
 
20
19
  // =============================================================================
@@ -60,12 +59,6 @@ afterAll(() => {
60
59
  (HTMLElement.prototype as any).matches = originalMatches;
61
60
  });
62
61
 
63
- // Reset the singleton live regions between tests so result-count
64
- // announcements from one test don't leak into the next.
65
- afterEach(() => {
66
- __resetLiveRegionsForTest();
67
- });
68
-
69
62
  // =============================================================================
70
63
  // Fixtures
71
64
  // =============================================================================
@@ -302,105 +295,4 @@ describe('PowerSearch', () => {
302
295
  expect(screen.getByRole('combobox')).toBeDisabled();
303
296
  });
304
297
  });
305
-
306
- describe('result count announcements', () => {
307
- const politeRegion = () =>
308
- document.querySelector('[data-astryx-live-region="polite"]');
309
-
310
- it('announces the result count to a polite live region when it changes', async () => {
311
- const {rerender} = render(
312
- <PowerSearch
313
- config={config}
314
- filters={[]}
315
- onChange={() => {}}
316
- resultCount={0}
317
- />,
318
- );
319
- rerender(
320
- <PowerSearch
321
- config={config}
322
- filters={[]}
323
- onChange={() => {}}
324
- resultCount={5}
325
- />,
326
- );
327
- await waitFor(() => {
328
- expect(politeRegion()).toHaveTextContent('5 results');
329
- });
330
- });
331
-
332
- it('announces "1 result" (singular) for a single match', async () => {
333
- const {rerender} = render(
334
- <PowerSearch
335
- config={config}
336
- filters={[]}
337
- onChange={() => {}}
338
- resultCount={0}
339
- />,
340
- );
341
- rerender(
342
- <PowerSearch
343
- config={config}
344
- filters={[]}
345
- onChange={() => {}}
346
- resultCount={1}
347
- />,
348
- );
349
- await waitFor(() => {
350
- expect(politeRegion()).toHaveTextContent('1 result');
351
- });
352
- expect(politeRegion()?.textContent).not.toMatch(/results/);
353
- });
354
-
355
- it('announces a string result count verbatim', async () => {
356
- const {rerender} = render(
357
- <PowerSearch
358
- config={config}
359
- filters={[]}
360
- onChange={() => {}}
361
- resultCount="0 items"
362
- />,
363
- );
364
- rerender(
365
- <PowerSearch
366
- config={config}
367
- filters={[]}
368
- onChange={() => {}}
369
- resultCount="Showing 1.2k matches"
370
- />,
371
- );
372
- await waitFor(() => {
373
- expect(politeRegion()).toHaveTextContent('Showing 1.2k matches');
374
- });
375
- });
376
-
377
- it('does not announce the result count present on initial mount', async () => {
378
- render(
379
- <PowerSearch
380
- config={config}
381
- filters={[]}
382
- onChange={() => {}}
383
- resultCount={42}
384
- />,
385
- );
386
- // Flush effects and any pending live-region rAF writes.
387
- await act(async () => {
388
- await new Promise(resolve => setTimeout(resolve, 50));
389
- });
390
- expect(politeRegion()?.textContent ?? '').not.toContain('42');
391
- });
392
-
393
- it('leaves Typeahead dropdown announcements intact and stays silent when no resultCount is set', async () => {
394
- const user = userEvent.setup();
395
- render(<PowerSearchWrapper config={config} />);
396
- const input = screen.getByRole('combobox');
397
- await user.click(input);
398
- await user.type(input, 'Status');
399
- // BaseTypeahead announces the dropdown suggestion count; PowerSearch adds
400
- // no result-count announcement because resultCount is unset.
401
- await waitFor(() => {
402
- expect(politeRegion()?.textContent).toMatch(/\d+ results?/);
403
- });
404
- });
405
- });
406
298
  });
@@ -16,7 +16,6 @@
16
16
 
17
17
  import React, {
18
18
  useCallback,
19
- useEffect,
20
19
  useImperativeHandle,
21
20
  useMemo,
22
21
  useRef,
@@ -39,7 +38,6 @@ import type {IconType} from '../Icon';
39
38
  import type {IconName} from '../Icon/globalIconRegistry';
40
39
  import type {InputStatus} from '../Field';
41
40
  import {usePopover} from '../Popover/usePopover';
42
- import {useAnnounce} from '../hooks/useAnnounce';
43
41
  import {
44
42
  spacingVars,
45
43
  colorVars,
@@ -947,42 +945,23 @@ export function PowerSearch({
947
945
  isReadOnly,
948
946
  ]);
949
947
 
950
- // Plain-text form of the result count, shared by the visible label and the
951
- // screen-reader announcement so the two never drift.
952
- const resultCountText = useMemo((): string | null => {
953
- if (resultCount == null) {
954
- return null;
955
- }
956
- if (typeof resultCount === 'number') {
957
- const formatted = new Intl.NumberFormat().format(resultCount);
958
- return `${formatted} ${resultCount === 1 ? 'result' : 'results'}`;
959
- }
960
- return resultCount;
961
- }, [resultCount]);
962
-
963
- // Announce result-count changes to screen readers through a polite live
964
- // region, mirroring the way Typeahead announces its dropdown result count.
965
- // The count is otherwise only shown visually and stays silent to assistive
966
- // tech. Skip the first run so the count already present on mount isn't
967
- // announced unprompted — only user-driven changes are spoken.
968
- const announce = useAnnounce();
969
- const hasMountedRef = useRef(false);
970
- useEffect(() => {
971
- if (!hasMountedRef.current) {
972
- hasMountedRef.current = true;
973
- return;
974
- }
975
- if (resultCountText != null) {
976
- announce(resultCountText);
977
- }
978
- }, [resultCountText, announce]);
979
-
980
948
  // Build combined endContent from resultCount + endContent props
981
949
  const combinedEndContent = useMemo((): React.ReactNode => {
982
- const resultCountNode =
983
- resultCountText != null ? (
984
- <span {...stylex.props(resultCountStyles.text)}>{resultCountText}</span>
985
- ) : null;
950
+ let resultCountNode: React.ReactNode = null;
951
+ if (resultCount != null) {
952
+ if (typeof resultCount === 'number') {
953
+ const formatted = new Intl.NumberFormat().format(resultCount);
954
+ resultCountNode = (
955
+ <span {...stylex.props(resultCountStyles.text)}>
956
+ {formatted} {resultCount === 1 ? 'result' : 'results'}
957
+ </span>
958
+ );
959
+ } else {
960
+ resultCountNode = (
961
+ <span {...stylex.props(resultCountStyles.text)}>{resultCount}</span>
962
+ );
963
+ }
964
+ }
986
965
 
987
966
  if (resultCountNode && endContent) {
988
967
  return (
@@ -993,7 +972,7 @@ export function PowerSearch({
993
972
  );
994
973
  }
995
974
  return resultCountNode || endContent || undefined;
996
- }, [resultCountText, endContent]);
975
+ }, [resultCount, endContent]);
997
976
 
998
977
  return (
999
978
  <>
@@ -140,16 +140,6 @@ describe('ProgressBar', () => {
140
140
  expect(progressbar).toHaveAttribute('aria-valuemax', '0');
141
141
  });
142
142
 
143
- it('does not render NaN in the value label when max is zero', () => {
144
- render(<ProgressBar value={0} max={0} label="Empty" hasValueLabel />);
145
- expect(screen.queryByText(/NaN|Infinity/)).not.toBeInTheDocument();
146
- const progressbar = screen.getByRole('progressbar');
147
- expect(progressbar.getAttribute('aria-valuetext') ?? '').not.toMatch(
148
- /NaN|Infinity/,
149
- );
150
- expect(screen.getByText('0%')).toBeInTheDocument();
151
- });
152
-
153
143
  // Disabled state
154
144
  describe('disabled state', () => {
155
145
  it('renders with isDisabled', () => {
@@ -228,8 +228,7 @@ const variantStyles = stylex.create({
228
228
  });
229
229
 
230
230
  function defaultFormatValueLabel(value: number, max: number): string {
231
- const pct = max > 0 ? Math.round((value / max) * 100) : 0;
232
- return `${pct}%`;
231
+ return `${Math.round((value / max) * 100)}%`;
233
232
  }
234
233
 
235
234
  /**
@@ -510,16 +510,7 @@ export function ResizeHandle({
510
510
  ),
511
511
  className,
512
512
  )}
513
- {...props}
514
- // Keyboard resizing must fire from the focusable separator, not a child:
515
- // keydown fires on the focused element and bubbles up, so a handler on a
516
- // descendant never runs (per the WAI-ARIA window-splitter pattern).
517
- // Placed after {...props} and composed with any consumer onKeyDown so
518
- // this accessibility-critical handler can't be clobbered by a passed prop.
519
- onKeyDown={e => {
520
- props.onKeyDown?.(e);
521
- handleKeyDown(e);
522
- }}>
513
+ {...props}>
523
514
  {/* Wider invisible hit area for pointer interaction */}
524
515
  <div
525
516
  {...stylex.props(
@@ -537,6 +528,7 @@ export function ResizeHandle({
537
528
  setIsHovered(false);
538
529
  }
539
530
  }}
531
+ onKeyDown={handleKeyDown}
540
532
  />
541
533
  {/* Pill grip indicator — themed via .astryx-resize-handle-pill */}
542
534
  {children ?? (
@@ -16,7 +16,7 @@ export const docs = {
16
16
  theming: {
17
17
  targets: [
18
18
  {className: 'astryx-segmented-control', visualProps: ['size']},
19
- {className: 'astryx-segmented-control-item', visualProps: ['size'], states: ['selected', 'disabled']},
19
+ {className: 'astryx-segmented-control-item'},
20
20
  ],
21
21
  vars: [
22
22
  {name: '--_segmented-control-radius', description: 'Border radius of the segmented control', default: 'var(--radius-element)', private: true},
@@ -34,7 +34,7 @@ export const docs = {
34
34
  ],
35
35
  theming: {
36
36
  container: true,
37
- targets: [{className: 'astryx-selectable-card', visualProps: ['selected', 'variant']}],
37
+ targets: [{className: 'astryx-selectable-card', visualProps: ['selected']}],
38
38
  },
39
39
  playground: {
40
40
  defaults: {
@@ -21,7 +21,7 @@ export const docs = {
21
21
  targets: [
22
22
  {className: 'astryx-side-nav', visualProps: ['mode']},
23
23
  {className: 'astryx-side-nav-heading'},
24
- {className: 'astryx-side-nav-item', visualProps: ['size'], states: ['selected']},
24
+ {className: 'astryx-side-nav-item'},
25
25
  {className: 'astryx-side-nav-section'},
26
26
  ],
27
27
  },
@@ -102,12 +102,6 @@ export const docs = {
102
102
  description: 'Enables collapse behavior. true for uncontrolled with default toggle button, or an object for controlled mode and advanced config (defaultIsCollapsed, isCollapsed + onCollapsedChange, hasButton, buttonLabel).',
103
103
  default: 'false',
104
104
  },
105
- {
106
- name: 'resizable',
107
- type: 'boolean | { defaultWidth?: number; minWidth?: number; maxWidth?: number; autoSaveId?: string; onWidthChange?: (width: number) => void }',
108
- description: 'Enables a resize handle at the inline-end edge. true for defaults (260px initial, 180-480px range), or a ResizableConfig object (defaultWidth, minWidth, maxWidth, autoSaveId for localStorage persistence, onWidthChange). The handle is hidden while collapsed.',
109
- default: 'false',
110
- },
111
105
  {
112
106
  name: 'handleRef',
113
107
  type: 'Ref<SideNavImperativeCollapseHandle>',
@@ -162,26 +162,6 @@ describe('SideNav', () => {
162
162
  screen.getByRole('button', {name: 'Expand sidebar'}),
163
163
  ).toBeInTheDocument();
164
164
  });
165
-
166
- it('does not render an empty footer container when collapsible.hasButton is false', () => {
167
- render(
168
- <SideNav data-testid="nav" collapsible={{hasButton: false}}>
169
- Content
170
- </SideNav>,
171
- );
172
-
173
- // The built-in collapse button is opted out (consumers render their own
174
- // SideNavCollapseButton in the header), so it must not appear...
175
- expect(
176
- screen.queryByRole('button', {name: 'Collapse sidebar'}),
177
- ).not.toBeInTheDocument();
178
-
179
- // ...and no empty sticky-bottom container should be left behind. With no
180
- // footer/footerIcons and no built-in button, the scrollable content region
181
- // is the nav's only child.
182
- const nav = screen.getByTestId('nav');
183
- expect(nav.children).toHaveLength(1);
184
- });
185
165
  });
186
166
 
187
167
  // =============================================================================
@@ -490,10 +490,6 @@ export function SideNav({
490
490
  // =========================================================================
491
491
  const hasStickyTop = !!(header || topContent);
492
492
  const hasStickyBottom = !!(footer || footerIcons);
493
- // The built-in collapse button only renders when collapse is enabled and it
494
- // hasn't been opted out via `collapsible.hasButton: false` (e.g. when the
495
- // consumer places a SideNavCollapseButton in the header instead).
496
- const showCollapseButton = isCollapsible && hasCollapseButton;
497
493
 
498
494
  // When resizable, override the nav width via inline style
499
495
  const resizableNavStyle: React.CSSProperties | undefined = isResizable
@@ -536,7 +532,7 @@ export function SideNav({
536
532
  )}>
537
533
  {children}
538
534
  </div>
539
- {(hasStickyBottom || showCollapseButton) && (
535
+ {(hasStickyBottom || isCollapsible) && (
540
536
  <div
541
537
  {...stylex.props(
542
538
  styles.stickyBottom,
@@ -548,7 +544,7 @@ export function SideNav({
548
544
  styles.footerRow,
549
545
  collapsed && styles.footerRowCollapsed,
550
546
  )}>
551
- {showCollapseButton && <SideNavCollapseButton />}
547
+ {isCollapsible && hasCollapseButton && <SideNavCollapseButton />}
552
548
  {footerIcons}
553
549
  </div>
554
550
  </div>
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
13
- import {render, screen, fireEvent} from '@testing-library/react';
13
+ import {render, screen} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TabList} from './TabList';
16
16
  import {Tab} from './Tab';
@@ -702,187 +702,3 @@ describe('TabMenu', () => {
702
702
  expect(reportsItem).not.toHaveAttribute('aria-current');
703
703
  });
704
704
  });
705
-
706
- describe('TabMenu keyboard navigation (roving tabindex)', () => {
707
- const menuOptions = [
708
- {value: 'analytics', label: 'Analytics'},
709
- {value: 'reports', label: 'Reports'},
710
- ];
711
-
712
- it('exposes the overflow menu as a single Tab stop (one item tabbable, rest -1)', async () => {
713
- const user = userEvent.setup();
714
- render(
715
- <TabList value="home" onChange={() => {}}>
716
- <Tab value="home" label="Home" />
717
- <TabMenu label="More" options={menuOptions} />
718
- </TabList>,
719
- );
720
-
721
- await user.click(screen.getByRole('button', {name: /More/}));
722
-
723
- const analytics = screen.getByRole('menuitem', {
724
- name: 'Analytics',
725
- hidden: true,
726
- });
727
- const reports = screen.getByRole('menuitem', {
728
- name: 'Reports',
729
- hidden: true,
730
- });
731
-
732
- // Exactly one menu item is in the Tab sequence; arrow keys reach the rest.
733
- expect(analytics).toHaveAttribute('tabindex', '0');
734
- expect(reports).toHaveAttribute('tabindex', '-1');
735
-
736
- const tabbable = [analytics, reports].filter(
737
- el => el.getAttribute('tabindex') === '0',
738
- );
739
- expect(tabbable).toHaveLength(1);
740
- });
741
-
742
- it('moves focus between items with ArrowDown and ArrowUp', async () => {
743
- const user = userEvent.setup();
744
- render(
745
- <TabList value="home" onChange={() => {}}>
746
- <Tab value="home" label="Home" />
747
- <TabMenu label="More" options={menuOptions} />
748
- </TabList>,
749
- );
750
-
751
- await user.click(screen.getByRole('button', {name: /More/}));
752
- const menu = screen.getByRole('menu', {hidden: true});
753
- const analytics = screen.getByRole('menuitem', {
754
- name: 'Analytics',
755
- hidden: true,
756
- });
757
- const reports = screen.getByRole('menuitem', {
758
- name: 'Reports',
759
- hidden: true,
760
- });
761
-
762
- fireEvent.keyDown(menu, {key: 'ArrowDown'});
763
- expect(analytics).toHaveFocus();
764
-
765
- fireEvent.keyDown(menu, {key: 'ArrowDown'});
766
- expect(reports).toHaveFocus();
767
-
768
- fireEvent.keyDown(menu, {key: 'ArrowUp'});
769
- expect(analytics).toHaveFocus();
770
- });
771
-
772
- it('moves the roving tab stop with arrow navigation', async () => {
773
- const user = userEvent.setup();
774
- render(
775
- <TabList value="home" onChange={() => {}}>
776
- <Tab value="home" label="Home" />
777
- <TabMenu label="More" options={menuOptions} />
778
- </TabList>,
779
- );
780
-
781
- await user.click(screen.getByRole('button', {name: /More/}));
782
- const menu = screen.getByRole('menu', {hidden: true});
783
- const analytics = screen.getByRole('menuitem', {
784
- name: 'Analytics',
785
- hidden: true,
786
- });
787
- const reports = screen.getByRole('menuitem', {
788
- name: 'Reports',
789
- hidden: true,
790
- });
791
-
792
- fireEvent.keyDown(menu, {key: 'ArrowDown'}); // focus analytics
793
- fireEvent.keyDown(menu, {key: 'ArrowDown'}); // focus reports
794
-
795
- // The tab stop follows focus, so it is still a single stop after moving.
796
- expect(reports).toHaveAttribute('tabindex', '0');
797
- expect(analytics).toHaveAttribute('tabindex', '-1');
798
- });
799
-
800
- it('jumps to first and last item with Home and End', async () => {
801
- const user = userEvent.setup();
802
- const options = [
803
- {value: 'analytics', label: 'Analytics'},
804
- {value: 'reports', label: 'Reports'},
805
- {value: 'exports', label: 'Exports'},
806
- ];
807
- render(
808
- <TabList value="home" onChange={() => {}}>
809
- <Tab value="home" label="Home" />
810
- <TabMenu label="More" options={options} />
811
- </TabList>,
812
- );
813
-
814
- await user.click(screen.getByRole('button', {name: /More/}));
815
- const menu = screen.getByRole('menu', {hidden: true});
816
- const analytics = screen.getByRole('menuitem', {
817
- name: 'Analytics',
818
- hidden: true,
819
- });
820
- const exports = screen.getByRole('menuitem', {
821
- name: 'Exports',
822
- hidden: true,
823
- });
824
-
825
- fireEvent.keyDown(menu, {key: 'End'});
826
- expect(exports).toHaveFocus();
827
-
828
- fireEvent.keyDown(menu, {key: 'Home'});
829
- expect(analytics).toHaveFocus();
830
- });
831
-
832
- it('selects an item with Enter and calls onChange', async () => {
833
- const user = userEvent.setup();
834
- const handleChange = vi.fn();
835
- render(
836
- <TabList value="home" onChange={handleChange}>
837
- <Tab value="home" label="Home" />
838
- <TabMenu label="More" options={menuOptions} />
839
- </TabList>,
840
- );
841
-
842
- await user.click(screen.getByRole('button', {name: /More/}));
843
- const analytics = screen.getByRole('menuitem', {
844
- name: 'Analytics',
845
- hidden: true,
846
- });
847
- analytics.focus();
848
- fireEvent.keyDown(analytics, {key: 'Enter'});
849
-
850
- expect(handleChange).toHaveBeenCalledWith('analytics');
851
- });
852
-
853
- it('closes the menu when Tab is pressed inside it (APG menu-button)', async () => {
854
- const user = userEvent.setup();
855
- render(
856
- <TabList value="home" onChange={() => {}}>
857
- <Tab value="home" label="Home" />
858
- <TabMenu label="More" options={menuOptions} />
859
- </TabList>,
860
- );
861
-
862
- await user.click(screen.getByRole('button', {name: /More/}));
863
- const menu = screen.getByRole('menu', {hidden: true});
864
-
865
- const hidePopover = vi.mocked(HTMLElement.prototype.hidePopover);
866
- hidePopover.mockClear();
867
- fireEvent.keyDown(menu, {key: 'Tab'});
868
- expect(hidePopover).toHaveBeenCalled();
869
- });
870
-
871
- it('closes the menu when Escape is pressed', async () => {
872
- const user = userEvent.setup();
873
- render(
874
- <TabList value="home" onChange={() => {}}>
875
- <Tab value="home" label="Home" />
876
- <TabMenu label="More" options={menuOptions} />
877
- </TabList>,
878
- );
879
-
880
- await user.click(screen.getByRole('button', {name: /More/}));
881
- const menu = screen.getByRole('menu', {hidden: true});
882
-
883
- const hidePopover = vi.mocked(HTMLElement.prototype.hidePopover);
884
- hidePopover.mockClear();
885
- fireEvent.keyDown(menu, {key: 'Escape'});
886
- expect(hidePopover).toHaveBeenCalled();
887
- });
888
- });
@@ -16,7 +16,7 @@
16
16
  * - /packages/cli/templates/blocks/components/TabList/ (showcase blocks)
17
17
  */
18
18
 
19
- import React, {useCallback, useId, useRef, type ReactNode} from 'react';
19
+ import React, {useCallback, useId, type ReactNode} from 'react';
20
20
  import * as stylex from '@stylexjs/stylex';
21
21
  import {Icon} from '../Icon';
22
22
  import {renderIconSlot, type IconType} from '../Icon';
@@ -262,7 +262,6 @@ export function TabMenu({
262
262
  }: TabMenuProps) {
263
263
  const tabListCtx = useTabListContext();
264
264
  const menuId = useId();
265
- const buttonRef = useRef<HTMLButtonElement>(null);
266
265
 
267
266
  const popover = usePopover({
268
267
  hasLightDismiss: true,
@@ -271,50 +270,20 @@ export function TabMenu({
271
270
  // The popup's own role="menu" is the exposed semantics; a modal dialog
272
271
  // wrapper would announce an unnamed dialog around the menu.
273
272
  role: 'none',
274
- // Return focus to the trigger when the menu closes (Escape, Tab, select,
275
- // or light dismiss) so keyboard focus is never dropped to <body>.
276
- onHide: useCallback(() => {
277
- buttonRef.current?.focus();
278
- }, []),
279
273
  });
280
274
 
281
- // The overflow menu is a composite widget: a single roving tab stop with
282
- // arrow-key navigation between items, per the APG menu pattern. The hook owns
283
- // item tabindex — items render tabIndex={-1} and exactly one is promoted to 0.
284
- const {
285
- listRef,
286
- handleKeyDown: handleListKeyDown,
287
- handleFocus,
288
- focusFirst,
289
- } = useListFocus<HTMLDivElement>({
290
- hasRovingTabIndex: true,
291
- onEscape: () => popover.hide(),
292
- });
275
+ const {listRef, handleKeyDown: handleListKeyDown} =
276
+ useListFocus<HTMLDivElement>({
277
+ onEscape: () => popover.hide(),
278
+ });
293
279
 
294
280
  const handleToggle = useCallback(() => {
295
281
  if (popover.isOpen) {
296
282
  popover.hide();
297
283
  } else {
298
284
  popover.show();
299
- // Move focus into the menu on open so arrow navigation works and the
300
- // roving tab stop lands on a real item (APG menu-button focus-on-open).
301
- requestAnimationFrame(() => focusFirst());
302
285
  }
303
- }, [popover, focusFirst]);
304
-
305
- const handleMenuKeyDown = useCallback(
306
- (e: React.KeyboardEvent) => {
307
- // APG menu-button: Tab leaves the menu rather than walking its items.
308
- // Close and let onHide return focus to the trigger, from which the
309
- // browser's default Tab continues to the next element.
310
- if (e.key === 'Tab') {
311
- popover.hide();
312
- return;
313
- }
314
- handleListKeyDown(e);
315
- },
316
- [handleListKeyDown, popover],
317
- );
286
+ }, [popover]);
318
287
 
319
288
  const selectedOption = options.find(o => o.value === tabListCtx.value);
320
289
  const triggerLabel = selectedOption?.label ?? label;
@@ -330,11 +299,7 @@ export function TabMenu({
330
299
  [tabListCtx, popover],
331
300
  );
332
301
 
333
- const setButtonRef = mergeRefs<HTMLButtonElement>(
334
- popover.triggerRef,
335
- buttonRef,
336
- ref,
337
- );
302
+ const setButtonRef = mergeRefs<HTMLButtonElement>(popover.triggerRef, ref);
338
303
 
339
304
  return (
340
305
  <>
@@ -392,8 +357,7 @@ export function TabMenu({
392
357
  id={menuId}
393
358
  role="menu"
394
359
  aria-label={label}
395
- onKeyDown={handleMenuKeyDown}
396
- onFocus={handleFocus}
360
+ onKeyDown={handleListKeyDown}
397
361
  {...mergeProps(
398
362
  themeProps('tab-menu-dropdown'),
399
363
  stylex.props(styles.dropdown),
@@ -407,7 +371,7 @@ export function TabMenu({
407
371
  <div
408
372
  key={option.value}
409
373
  role="menuitem"
410
- tabIndex={-1}
374
+ tabIndex={0}
411
375
  aria-current={isSelected ? 'true' : undefined}
412
376
  onClick={() => handleSelect(option.value)}
413
377
  onKeyDown={e => {