@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
@@ -284,12 +284,6 @@ function areRowPropsEqual<T extends Record<string, unknown>>(
284
284
  const nextItem = nextProps.item;
285
285
  const keys = Object.keys(nextItem);
286
286
 
287
- // A key deleted from nextItem would never be visited by the loop below,
288
- // and the row would keep rendering the removed field's stale value.
289
- if (Object.keys(prevItem).length !== keys.length) {
290
- return false;
291
- }
292
-
293
287
  for (const key of keys) {
294
288
  if (prevItem[key] !== nextItem[key]) {
295
289
  return false;
@@ -371,13 +365,9 @@ function BaseTableInner<T extends Record<string, unknown>>({
371
365
  const headerCells = resolvedColumns.map((col, columnIndex) => {
372
366
  const headerContent = col.header ?? col.key;
373
367
 
374
- // Build initial htmlProps with column alignment if specified.
375
- // `scope: 'col'` is the default so every column header associates its
376
- // data cells with the correct column for screen readers; a plugin's
377
- // transformHeaderCell can override it via htmlProps.scope.
368
+ // Build initial htmlProps with column alignment if specified
378
369
  const initialHeaderHtmlProps: Record<string, unknown> = {
379
370
  'data-column-key': col.key,
380
- scope: 'col',
381
371
  };
382
372
  if (col.align) {
383
373
  initialHeaderHtmlProps.style = {textAlign: col.align};
@@ -229,34 +229,6 @@ describe('BaseTable', () => {
229
229
  expect(headers[2]).toHaveTextContent('Email');
230
230
  });
231
231
 
232
- it('renders every column header with scope="col"', () => {
233
- render(<BaseTable data={users} columns={columns} />);
234
- const headers = screen.getAllByRole('columnheader');
235
- expect(headers).toHaveLength(3);
236
- for (const header of headers) {
237
- expect(header).toHaveAttribute('scope', 'col');
238
- }
239
- });
240
-
241
- it('lets a consumer override scope via header html props', () => {
242
- // A `<th scope="row">` maps to the `rowheader` role, so query the DOM
243
- // directly to assert the attribute regardless of the resolved ARIA role.
244
- const plugin: TablePlugin<User> = {
245
- transformHeaderCell: (props, column) =>
246
- column.key === 'name'
247
- ? {...props, htmlProps: {...props.htmlProps, scope: 'row'}}
248
- : props,
249
- };
250
- const {container} = render(
251
- <BaseTable data={users} columns={columns} plugins={[plugin]} />,
252
- );
253
- const headerCells = container.querySelectorAll('thead th');
254
- // columns fixture order: name, age, email — plugin overrides name → 'row'
255
- expect(headerCells[0]).toHaveAttribute('scope', 'row');
256
- expect(headerCells[1]).toHaveAttribute('scope', 'col');
257
- expect(headerCells[2]).toHaveAttribute('scope', 'col');
258
- });
259
-
260
232
  it('renders data cells', () => {
261
233
  render(<BaseTable data={users} columns={columns} />);
262
234
  const cells = screen.getAllByRole('cell');
@@ -645,29 +617,6 @@ describe('BaseTable', () => {
645
617
  // =============================================================================
646
618
 
647
619
  describe('Table', () => {
648
- it('clears a cell when the field is removed from the row object (#3595)', () => {
649
- const cols: TableColumn<Record<string, unknown>>[] = [
650
- {key: 'name', header: 'Name'},
651
- {key: 'status', header: 'Status'},
652
- ];
653
- const {rerender} = render(
654
- <Table
655
- data={[{id: '1', name: 'Alice', status: 'active'}]}
656
- columns={cols}
657
- idKey="id"
658
- />,
659
- );
660
- expect(screen.getByText('active')).toBeInTheDocument();
661
-
662
- // Clearing a field by omitting it (optimistic update / server response)
663
- // must re-render the row — the memo previously only compared the keys of
664
- // the NEW item, so the deleted field's stale value kept rendering.
665
- rerender(
666
- <Table data={[{id: '1', name: 'Alice'}]} columns={cols} idKey="id" />,
667
- );
668
- expect(screen.queryByText('active')).not.toBeInTheDocument();
669
- });
670
-
671
620
  it('renders a table with correct structure', () => {
672
621
  render(<Table data={users} columns={columns} />);
673
622
  expect(screen.getByRole('table')).toBeInTheDocument();
@@ -551,19 +551,6 @@ describe('useTableColumnResize', () => {
551
551
  expect(handle).toHaveAttribute('aria-valuenow', '200');
552
552
  });
553
553
 
554
- it('handle has a numeric aria-valuenow on initial render (before any width is measured)', () => {
555
- // A focusable role="separator" requires aria-valuenow per the ARIA spec.
556
- // Before the column width has been measured there is no override width, so
557
- // the handle must fall back to a defined numeric value (the column
558
- // minWidth) rather than omitting the attribute.
559
- render(<ResizeTable minWidth={80} />);
560
- const handle = getResizeHandles()[0];
561
- const valueNow = handle.getAttribute('aria-valuenow');
562
- expect(valueNow).not.toBeNull();
563
- expect(Number.isNaN(Number(valueNow))).toBe(false);
564
- expect(valueNow).toBe('80');
565
- });
566
-
567
554
  it('handle has aria-label with column header text', () => {
568
555
  render(<ResizeTable />);
569
556
  const handle = getResizeHandles()[0];
@@ -676,9 +676,7 @@ function ResizeHandle({
676
676
  <div
677
677
  role="separator"
678
678
  aria-orientation="vertical"
679
- // A focusable role="separator" requires a numeric aria-valuenow, so fall
680
- // back to the column's minWidth before the width has been measured.
681
- aria-valuenow={currentWidth ?? minWidth}
679
+ aria-valuenow={currentWidth ?? undefined}
682
680
  aria-valuemin={minWidth}
683
681
  aria-valuemax={maxWidth === Infinity ? undefined : maxWidth}
684
682
  aria-label={ariaLabel}
@@ -38,10 +38,6 @@ export function paginateData<T>(
38
38
  const size = Number.isFinite(pageSize)
39
39
  ? Math.max(1, Math.floor(pageSize))
40
40
  : 10;
41
- // page needs the same coercion: a negative start index makes Array.slice
42
- // count from the END of the data (page -1 returns the tail dressed up as a
43
- // page), and a fractional page returns a slice straddling two pages.
44
- const p = Number.isFinite(page) ? Math.max(1, Math.floor(page)) : 1;
45
- const start = (p - 1) * size;
41
+ const start = (page - 1) * size;
46
42
  return data.slice(start, start + size);
47
43
  }
@@ -132,19 +132,6 @@ describe('paginateData', () => {
132
132
  expect(paginateData([], 1, 10)).toEqual([]);
133
133
  });
134
134
 
135
- it('clamps invalid page numbers to the first page (#3593)', () => {
136
- const data = generateItems(30);
137
- // Negative pages previously fed a negative index to Array.slice, which
138
- // counts from the END of the data — the tail dressed up as a page.
139
- expect(paginateData(data, -1, 10)[0].id).toBe('1');
140
- expect(paginateData(data, 0, 10)[0].id).toBe('1');
141
- expect(paginateData(data, NaN, 10)[0].id).toBe('1');
142
- // Fractional pages floor to the containing page instead of straddling two.
143
- expect(paginateData(data, 1.5, 10).map(i => i.id)).toEqual(
144
- paginateData(data, 1, 10).map(i => i.id),
145
- );
146
- });
147
-
148
135
  it('returns empty array when page exceeds data', () => {
149
136
  const data = generateItems(10);
150
137
  expect(paginateData(data, 5, 10)).toEqual([]);
@@ -302,29 +302,6 @@ describe('useTableSelectionState with filtered data', () => {
302
302
  expect(screen.getByTestId('selected-keys')).toHaveTextContent('1,3');
303
303
  });
304
304
 
305
- it('select-all is unchecked when the filter matches nothing (#3591)', async () => {
306
- const user = userEvent.setup();
307
- render(<FilteredSelectionTable />);
308
-
309
- // Select Bob, then filter to an empty result set.
310
- const checkboxes = screen.getAllByLabelText('Select row');
311
- await user.click(checkboxes[1]);
312
- expect(screen.getByTestId('selected-keys')).toHaveTextContent('2');
313
-
314
- const input = screen.getByTestId('filter-input');
315
- await user.clear(input);
316
- await user.type(input, 'zzz');
317
-
318
- // Zero visible rows must not read as "all selected" — previously the
319
- // header checkbox rendered checked here and deselect-all was a no-op.
320
- const selectAll = screen.getByLabelText('Select all rows');
321
- expect(selectAll).not.toBeChecked();
322
-
323
- // The hidden selection itself is preserved (frozen), as designed.
324
- await user.clear(input);
325
- expect(screen.getByTestId('selected-keys')).toHaveTextContent('2');
326
- });
327
-
328
305
  it('selections persist when filter changes', async () => {
329
306
  const user = userEvent.setup();
330
307
  render(<FilteredSelectionTable />);
@@ -125,17 +125,12 @@ export function useTableSelectionState<T extends Record<string, unknown>>(
125
125
  [selectedKeys, selectableIDs],
126
126
  );
127
127
 
128
- const getAllSelected = useCallback(() => {
129
- // Require at least one actionable row: with zero visible selectable
130
- // rows (e.g. a filter with no matches while a selection exists), the
131
- // union-based size comparison would read any non-empty selection as
132
- // "all selected" — a checked header checkbox over an empty table that
133
- // deselect-all can't clear, since the invisible keys count as frozen.
134
- if (selectableIDs.size === 0) {
135
- return false;
136
- }
137
- return allSelectableIDs.size === selectedKeys.size;
138
- }, [allSelectableIDs, selectableIDs, selectedKeys]);
128
+ const getAllSelected = useCallback(
129
+ () =>
130
+ allSelectableIDs.size === selectedKeys.size &&
131
+ allSelectableIDs.size !== 0,
132
+ [allSelectableIDs, selectedKeys],
133
+ );
139
134
 
140
135
  // Use refs to keep onSelectAll stable
141
136
  const frozenSelectedIDsRef = useRef(frozenSelectedIDs);
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import {describe, it, expect, vi} from 'vitest';
11
- import {render, renderHook, screen, within} from '@testing-library/react';
11
+ import {render, screen, within} from '@testing-library/react';
12
12
  import userEvent from '@testing-library/user-event';
13
13
  import {useState} from 'react';
14
14
  import {Table} from '../../Table';
@@ -421,40 +421,6 @@ describe('useTableSortableState', () => {
421
421
  });
422
422
  });
423
423
 
424
- describe('NaN handling', () => {
425
- it('keeps valid numbers sorted when a NaN cell is present', () => {
426
- const data: Employee[] = [
427
- {id: '1', name: 'A', age: 5, department: 'X', salary: 1},
428
- {id: '2', name: 'B', age: NaN, department: 'X', salary: 1},
429
- {id: '3', name: 'C', age: 1, department: 'X', salary: 1},
430
- {id: '4', name: 'D', age: 3, department: 'X', salary: 1},
431
- ];
432
- const {result} = renderHook(() =>
433
- useTableSortableState({
434
- data,
435
- defaultSort: [{sortKey: 'age', direction: 'ascending'}],
436
- }),
437
- );
438
- // NaN groups at the end like null; the valid numbers stay ordered.
439
- expect(result.current.sortedData.map(e => e.age)).toEqual([1, 3, 5, NaN]);
440
- });
441
-
442
- it('sorts NaN to the start in descending order, like null', () => {
443
- const data: Employee[] = [
444
- {id: '1', name: 'A', age: 5, department: 'X', salary: 1},
445
- {id: '2', name: 'B', age: NaN, department: 'X', salary: 1},
446
- {id: '3', name: 'C', age: 1, department: 'X', salary: 1},
447
- ];
448
- const {result} = renderHook(() =>
449
- useTableSortableState({
450
- data,
451
- defaultSort: [{sortKey: 'age', direction: 'descending'}],
452
- }),
453
- );
454
- expect(result.current.sortedData.map(e => e.age)).toEqual([NaN, 5, 1]);
455
- });
456
- });
457
-
458
424
  describe('null/undefined handling', () => {
459
425
  it('sorts null values to the end', () => {
460
426
  const data: Employee[] = [
@@ -168,18 +168,14 @@ function defaultCompare<T extends Record<string, unknown>>(
168
168
  const aVal = a[sortKey];
169
169
  const bVal = b[sortKey];
170
170
 
171
- // null/undefined/NaN sort to the end. NaN must not reach the numeric fast
172
- // path: a NaN comparator result reads as "equal" to Array.sort, which makes
173
- // the comparator inconsistent and corrupts the order of the other rows.
174
- const aMissing = aVal == null || (typeof aVal === 'number' && Number.isNaN(aVal));
175
- const bMissing = bVal == null || (typeof bVal === 'number' && Number.isNaN(bVal));
176
- if (aMissing && bMissing) {
171
+ // null/undefined sort to the end
172
+ if (aVal == null && bVal == null) {
177
173
  return 0;
178
174
  }
179
- if (aMissing) {
175
+ if (aVal == null) {
180
176
  return 1;
181
177
  }
182
- if (bMissing) {
178
+ if (bVal == null) {
183
179
  return -1;
184
180
  }
185
181
 
@@ -13,7 +13,7 @@ import {useState} from 'react';
13
13
  import {describe, it, expect, vi, beforeEach} from 'vitest';
14
14
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
- import {TestIcon} from '../__tests__/TestIcon';
16
+ import {MagnifyingGlassIcon} from '@heroicons/react/24/outline';
17
17
  import {TextArea} from './TextArea';
18
18
 
19
19
  // Mock showPopover/hidePopover since jsdom does not implement them. Used by the
@@ -193,7 +193,7 @@ describe('TextArea', () => {
193
193
  label="Description"
194
194
  value=""
195
195
  onChange={() => {}}
196
- startIcon={TestIcon}
196
+ startIcon={MagnifyingGlassIcon}
197
197
  />,
198
198
  );
199
199
  expect(screen.getByRole('textbox')).toBeInTheDocument();
@@ -577,7 +577,7 @@ describe('TextArea', () => {
577
577
  label="Notes"
578
578
  value=""
579
579
  onChange={() => {}}
580
- startIcon={<TestIcon />}
580
+ startIcon={<MagnifyingGlassIcon />}
581
581
  />,
582
582
  );
583
583
 
@@ -12,7 +12,7 @@
12
12
  import {describe, it, expect, vi, beforeEach} from 'vitest';
13
13
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
- import {TestIcon} from '../__tests__/TestIcon';
15
+ import {MagnifyingGlassIcon} from '@heroicons/react/24/outline';
16
16
  import {TextInput} from './TextInput';
17
17
 
18
18
  // Mock showPopover/hidePopover since jsdom does not implement them. Used by the
@@ -163,7 +163,7 @@ describe('TextInput', () => {
163
163
  label="Search"
164
164
  value=""
165
165
  onChange={() => {}}
166
- startIcon={TestIcon}
166
+ startIcon={MagnifyingGlassIcon}
167
167
  />,
168
168
  );
169
169
  expect(screen.getByRole('textbox')).toBeInTheDocument();
@@ -482,7 +482,7 @@ describe('TextInput', () => {
482
482
  label="Search"
483
483
  value=""
484
484
  onChange={() => {}}
485
- startIcon={<TestIcon />}
485
+ startIcon={<MagnifyingGlassIcon />}
486
486
  />,
487
487
  );
488
488
 
@@ -37,45 +37,11 @@ describe('Thumbnail', () => {
37
37
  expect(screen.getByRole('status')).toBeInTheDocument();
38
38
  });
39
39
 
40
- it('exposes the label as an accessible name via a valid group role', () => {
40
+ it('uses label as accessible name on root', () => {
41
41
  render(<Thumbnail label="photo.png" data-testid="thumb" />);
42
- // The accessible name must be carried by a valid named role (group),
43
- // not by a bare aria-label on a generic div (aria-prohibited-attr).
44
- const group = screen.getByRole('group', {name: 'photo.png'});
45
- expect(group).toBe(screen.getByTestId('thumb'));
42
+ expect(screen.getByTestId('thumb')).toHaveAttribute('aria-label', 'photo.png');
46
43
  });
47
44
 
48
- it('does not put aria-label on a generic (roleless) element', () => {
49
- render(<Thumbnail label="photo.png" data-testid="thumb" />);
50
- const thumb = screen.getByTestId('thumb');
51
- // The labeled element must declare a role so the name is legitimate.
52
- expect(thumb).toHaveAttribute('role', 'group');
53
- });
54
-
55
- it('keeps interactive children accessible while exposing the group name', () => {
56
- const onRemove = vi.fn();
57
- render(
58
- <Thumbnail
59
- src="/img.jpg"
60
- alt="Clickable"
61
- label="file.png"
62
- onClick={vi.fn()}
63
- onRemove={onRemove}
64
- />,
65
- );
66
- // A group role (unlike img) must not hide descendant controls.
67
- expect(
68
- screen.getByRole('group', {name: 'file.png — Clickable'}),
69
- ).toBeInTheDocument();
70
- expect(
71
- screen.getByRole('button', {name: 'Open file.png — Clickable'}),
72
- ).toBeInTheDocument();
73
- expect(
74
- screen.getByRole('button', {name: 'Remove file.png — Clickable'}),
75
- ).toBeInTheDocument();
76
- });
77
-
78
-
79
45
  it('label is shown via tooltip, not as inline text', () => {
80
46
  render(<Thumbnail label="photo.png" data-testid="thumb" />);
81
47
  // Label should exist in DOM (tooltip) but not as a direct child text node
@@ -58,9 +58,8 @@ export interface ThumbnailProps extends BaseProps<HTMLDivElement> {
58
58
  alt?: string;
59
59
  /**
60
60
  * Accessible label for the thumbnail (e.g. file name).
61
- * Not rendered visually — shown in a tooltip on hover, exposed as the
62
- * accessible name of the thumbnail group, and used as the accessible
63
- * name for the remove button.
61
+ * Not rendered visually — shown in a tooltip on hover and used
62
+ * as accessible name for the remove button.
64
63
  */
65
64
  label?: string;
66
65
  /**
@@ -297,7 +296,6 @@ export function Thumbnail({
297
296
  <div
298
297
  ref={ref}
299
298
  data-testid={testId}
300
- role="group"
301
299
  aria-label={accessibleName}
302
300
  {...mergeProps(
303
301
  themeProps('thumbnail'),
@@ -160,43 +160,6 @@ describe('TimeInput', () => {
160
160
  expect(onChange).not.toHaveBeenCalled();
161
161
  });
162
162
 
163
- it('sets aria-invalid="true" when typed input is out of range', () => {
164
- render(<TimeInput label="Time" onChange={() => {}} />);
165
-
166
- const input = screen.getByRole('textbox');
167
- fireEvent.change(input, {target: {value: '25:99'}});
168
-
169
- expect(input).toHaveAttribute('aria-invalid', 'true');
170
- });
171
-
172
- it('does not set aria-invalid when typed input is a valid time', () => {
173
- render(<TimeInput label="Time" onChange={() => {}} />);
174
-
175
- const input = screen.getByRole('textbox');
176
- fireEvent.change(input, {target: {value: '3:45 pm'}});
177
-
178
- expect(input).not.toHaveAttribute('aria-invalid');
179
- });
180
-
181
- it('announces an alert message when typed input is invalid', () => {
182
- render(<TimeInput label="Time" onChange={() => {}} />);
183
-
184
- const input = screen.getByRole('textbox');
185
- fireEvent.change(input, {target: {value: '25:99'}});
186
-
187
- expect(screen.getByRole('alert')).toHaveTextContent('Invalid time');
188
- });
189
-
190
- it('does not announce an alert message when input is valid', () => {
191
- render(<TimeInput label="Time" onChange={() => {}} />);
192
-
193
- const input = screen.getByRole('textbox');
194
- fireEvent.change(input, {target: {value: '3:45 pm'}});
195
-
196
- expect(screen.getByRole('alert')).toHaveTextContent('');
197
- expect(screen.queryByText('Invalid time')).not.toBeInTheDocument();
198
- });
199
-
200
163
  it('calls onChange on blur when input is valid', async () => {
201
164
  const user = userEvent.setup();
202
165
  const onChange = vi.fn();
@@ -635,9 +635,7 @@ export function TimeInput({
635
635
  data-autofocus={hasAutoFocus || undefined}
636
636
  aria-describedby={ariaDescribedBy}
637
637
  aria-required={isRequired === true ? 'true' : undefined}
638
- aria-invalid={
639
- status?.type === 'error' || !isInputValid ? 'true' : undefined
640
- }
638
+ aria-invalid={status?.type === 'error' ? 'true' : undefined}
641
639
  aria-busy={isBusy || undefined}
642
640
  aria-labelledby={ariaLabelledBy}
643
641
  {...stylex.props(
@@ -646,14 +644,6 @@ export function TimeInput({
646
644
  !isInputValid && styles.inputInvalid,
647
645
  )}
648
646
  />
649
- {/*
650
- Live region announcing invalid typed input to assistive technology.
651
- The value silently reverts on blur, so without this a screen-reader
652
- user would get no feedback that their entry was rejected (WCAG 3.3.1).
653
- */}
654
- <VisuallyHidden as="div" role="alert" aria-live="assertive">
655
- {!isInputValid ? 'Invalid time' : ''}
656
- </VisuallyHidden>
657
647
  {isBusy && <Spinner size="sm" />}
658
648
  {hasClear && value && !isDisabled && (
659
649
  <button
@@ -84,7 +84,7 @@ export const docs = {
84
84
  ],
85
85
  theming: {
86
86
  targets: [
87
- {className: 'astryx-timestamp', visualProps: ['type', 'color', 'format']},
87
+ {className: 'astryx-timestamp', visualProps: ['type', 'color']},
88
88
  ],
89
89
  },
90
90
  usage: {
@@ -121,14 +121,8 @@ export function Toast({
121
121
  // Will be initialized by startTimer when actually used
122
122
  const startTimeRef = useRef<number | null>(null);
123
123
 
124
- // Read onDismiss through a ref: the viewport re-creates it on every render
125
- // (another toast arriving/exiting), and a startTimer that depends on it
126
- // would restart — and un-pause — this toast's timer on unrelated renders.
127
- const onDismissRef = useRef(onDismiss);
128
- onDismissRef.current = onDismiss;
129
-
130
124
  const startTimer = useCallback(() => {
131
- if (!isAutoHide || isPausedRef.current) {
125
+ if (!isAutoHide) {
132
126
  return;
133
127
  }
134
128
  if (timerRef.current) {
@@ -136,9 +130,9 @@ export function Toast({
136
130
  }
137
131
  startTimeRef.current = Date.now();
138
132
  timerRef.current = setTimeout(() => {
139
- onDismissRef.current('auto');
133
+ onDismiss('auto');
140
134
  }, remainingRef.current);
141
- }, [isAutoHide]);
135
+ }, [isAutoHide, onDismiss]);
142
136
 
143
137
  const pauseTimer = useCallback(() => {
144
138
  if (!isAutoHide || isPausedRef.current) {
@@ -171,8 +165,6 @@ export function Toast({
171
165
  clearTimeout(timerRef.current);
172
166
  }
173
167
  };
174
- // startTimer's identity is stable per isAutoHide, so this runs on mount
175
- // and on a genuine duration change — not on unrelated viewport renders.
176
168
  }, [autoHideDuration, startTimer]);
177
169
 
178
170
  // Pause the auto-hide timer while the window is not focused, so a toast
@@ -212,67 +212,3 @@ describe('ToastViewport region ARIA', () => {
212
212
  expect(region).not.toHaveAttribute('aria-modal');
213
213
  });
214
214
  });
215
-
216
- describe('toast timer lifecycle (#3589)', () => {
217
- it('fires onHide exactly once when dismissed twice during the exit window', () => {
218
- const onHide = vi.fn();
219
- renderViewport(
220
- <ShowToastButton options={{body: 'Once', onHide}} triggerLabel="Show" />,
221
- );
222
- act(() => {
223
- fireEvent.click(screen.getByText('Show'));
224
- });
225
- const dismiss = screen.getByRole('button', {name: 'Dismiss notification'});
226
- act(() => {
227
- fireEvent.click(dismiss);
228
- });
229
- // The toast stays mounted during its exit transition; a second click
230
- // lands on the same still-mounted button.
231
- act(() => {
232
- fireEvent.click(dismiss);
233
- });
234
- expect(onHide).toHaveBeenCalledTimes(1);
235
- });
236
-
237
- it('keeps a window-blur pause alive when another toast arrives', () => {
238
- vi.useFakeTimers();
239
- try {
240
- const onHide = vi.fn();
241
- renderViewport(
242
- <>
243
- <ShowToastButton
244
- options={{body: 'Paused', autoHideDuration: 3000, onHide}}
245
- triggerLabel="Show A"
246
- />
247
- <ShowToastButton options={INFO_B} triggerLabel="Show B" />
248
- </>,
249
- );
250
- act(() => {
251
- fireEvent.click(screen.getByText('Show A'));
252
- });
253
- act(() => {
254
- fireEvent.blur(window);
255
- });
256
- // A second toast arriving re-renders the viewport; the paused timer
257
- // must not silently restart.
258
- act(() => {
259
- fireEvent.click(screen.getByText('Show B'));
260
- });
261
- act(() => {
262
- vi.advanceTimersByTime(60_000);
263
- });
264
- expect(onHide).not.toHaveBeenCalled();
265
- // Focus returns: the remaining time resumes and completes normally.
266
- act(() => {
267
- fireEvent.focus(window);
268
- });
269
- act(() => {
270
- vi.advanceTimersByTime(60_000);
271
- });
272
- expect(onHide).toHaveBeenCalledTimes(1);
273
- expect(onHide).toHaveBeenCalledWith('auto');
274
- } finally {
275
- vi.useRealTimers();
276
- }
277
- });
278
- });
@@ -114,9 +114,6 @@ export function ToastViewport({
114
114
 
115
115
  // Show the popover on mount so it enters the top layer.
116
116
  const viewportRef = useRef<HTMLDivElement>(null);
117
- // Toast ids whose exit has begun — guards onHide from double-firing (see
118
- // removeToast). Mirrors exitingIds state, readable synchronously.
119
- const exitingIdsRef = useRef<Set<string>>(new Set());
120
117
  // The element that was focused before the user jumped into the viewport
121
118
  // (via F6). Used to restore focus once all toasts are dismissed so focus
122
119
  // never falls to <body>.
@@ -174,14 +171,6 @@ export function ToastViewport({
174
171
  }, []);
175
172
 
176
173
  const removeToast = useCallback((id: string, reason: ToastDismissReason) => {
177
- // An exiting toast stays in toastsRef until its exit transition ends, so
178
- // a second dismissal inside that window (double-click, auto-timer plus
179
- // manual dismiss()) would re-fire onHide. Track exiting ids in a ref —
180
- // the exitingIds state dedupe below runs too late to protect onHide.
181
- if (exitingIdsRef.current.has(id)) {
182
- return;
183
- }
184
- exitingIdsRef.current.add(id);
185
174
  const entry = toastsRef.current.find(t => t.id === id);
186
175
  if (entry) {
187
176
  entry.options.onHide?.(reason);
@@ -221,7 +210,6 @@ export function ToastViewport({
221
210
  }, []);
222
211
 
223
212
  const handleExited = useCallback((id: string) => {
224
- exitingIdsRef.current.delete(id);
225
213
  setExitingIds(prev => {
226
214
  if (!prev.has(id)) {
227
215
  return prev;
@@ -17,7 +17,6 @@ export const docs = {
17
17
  theming: {
18
18
  targets: [
19
19
  {className: 'astryx-toggle-button-group'},
20
- {className: 'astryx-toggle-button', states: ['isPressed']},
21
20
  ],
22
21
  },
23
22
  description: 'A button that toggles between pressed and unpressed states. Thin wrapper over Button with controlled toggle pattern, icon swap, and font weight emphasis.',
@@ -189,7 +189,7 @@ export const docs = {
189
189
  ],
190
190
  theming: {
191
191
  targets: [
192
- {className: 'astryx-tokenizer', visualProps: ['size', 'status']},
192
+ {className: 'astryx-tokenizer', visualProps: ['size']},
193
193
  ],
194
194
  },
195
195
  usage: {
@@ -390,7 +390,7 @@ export const docsZh = {
390
390
  ],
391
391
  theming: {
392
392
  targets: [
393
- {className: 'astryx-tokenizer', visualProps: ['size', 'status']},
393
+ {className: 'astryx-tokenizer', visualProps: ['size']},
394
394
  ],
395
395
  },
396
396
  usage: {