@astryxdesign/core 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
  9. package/dist/Center/Center.d.ts +23 -16
  10. package/dist/Center/Center.d.ts.map +1 -1
  11. package/dist/Center/Center.js +7 -5
  12. package/dist/CodeBlock/CodeBlock.js +2 -2
  13. package/dist/DateInput/DateInput.d.ts.map +1 -1
  14. package/dist/DateInput/DateInput.js +12 -2
  15. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  16. package/dist/DateTimeInput/DateTimeInput.js +12 -2
  17. package/dist/Field/Field.d.ts.map +1 -1
  18. package/dist/Field/Field.js +1 -0
  19. package/dist/Field/InputClearButton.d.ts +2 -2
  20. package/dist/Field/InputClearButton.d.ts.map +1 -1
  21. package/dist/Field/InputClearButton.js +5 -1
  22. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  23. package/dist/Field/PanelSearchInput.js +16 -4
  24. package/dist/FileInput/FileInput.d.ts.map +1 -1
  25. package/dist/FileInput/FileInput.js +12 -1
  26. package/dist/HoverCard/useHoverCard.js +2 -2
  27. package/dist/Indicator/CheckboxIndicator.js +2 -2
  28. package/dist/Indicator/RadioIndicator.js +2 -2
  29. package/dist/Layer/layerStack.d.ts +10 -0
  30. package/dist/Layer/layerStack.d.ts.map +1 -1
  31. package/dist/Layer/layerStack.js +21 -9
  32. package/dist/Layer/useLayerDismissal.d.ts +2 -3
  33. package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
  34. package/dist/Layer/useLayerDismissal.js +2 -3
  35. package/dist/NavIcon/NavIcon.js +2 -2
  36. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  37. package/dist/NumberInput/NumberInput.js +12 -2
  38. package/dist/Popover/usePopover.d.ts +3 -2
  39. package/dist/Popover/usePopover.d.ts.map +1 -1
  40. package/dist/Popover/usePopover.js +4 -2
  41. package/dist/ProgressBar/ProgressBar.js +2 -2
  42. package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
  43. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
  44. package/dist/ScrollableArea/ScrollableArea.js +144 -0
  45. package/dist/ScrollableArea/index.d.ts +11 -0
  46. package/dist/ScrollableArea/index.d.ts.map +1 -0
  47. package/dist/ScrollableArea/index.js +11 -0
  48. package/dist/StatusDot/StatusDot.js +2 -2
  49. package/dist/TextArea/TextArea.js +2 -2
  50. package/dist/TextInput/TextInput.d.ts.map +1 -1
  51. package/dist/TextInput/TextInput.js +19 -4
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +12 -2
  54. package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
  55. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/BaseTypeahead.js +56 -20
  57. package/dist/astryx.css +15 -0
  58. package/dist/hooks/index.d.ts +2 -0
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/index.js +1 -0
  61. package/dist/hooks/scrollGeometry.d.ts +24 -0
  62. package/dist/hooks/scrollGeometry.d.ts.map +1 -0
  63. package/dist/hooks/scrollGeometry.js +86 -0
  64. package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
  65. package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
  66. package/dist/hooks/scrollOwnerRegistry.js +24 -0
  67. package/dist/hooks/useFocusTrap.d.ts +8 -0
  68. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  69. package/dist/hooks/useFocusTrap.js +22 -11
  70. package/dist/hooks/useScrollableArea.d.ts +51 -0
  71. package/dist/hooks/useScrollableArea.d.ts.map +1 -0
  72. package/dist/hooks/useScrollableArea.js +287 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/theme/defineTheme.d.ts +2 -6
  77. package/dist/theme/defineTheme.d.ts.map +1 -1
  78. package/dist/theme/defineTheme.js +1 -1
  79. package/dist/theme/derivedVarRegistry.js +1 -1
  80. package/dist/theme/localTokens.d.ts +8 -11
  81. package/dist/theme/localTokens.d.ts.map +1 -1
  82. package/dist/theme/localTokens.js +17 -71
  83. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  84. package/dist/theme/themeAdaptations.js +4 -4
  85. package/dist/utils/themeProps.d.ts +10 -10
  86. package/dist/utils/themeProps.d.ts.map +1 -1
  87. package/dist/utils/themeProps.js +27 -10
  88. package/locales/en.json +16 -0
  89. package/locales/pseudo.json +12 -0
  90. package/package.json +7 -2
  91. package/src/AppShell/AppShell.test.tsx +36 -0
  92. package/src/AppShell/AppShell.tsx +4 -1
  93. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  94. package/src/Banner/Banner.test.tsx +3 -1
  95. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
  96. package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
  97. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
  98. package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
  99. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
  100. package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
  101. package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
  102. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
  103. package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
  104. package/src/Center/Center.doc.mjs +32 -28
  105. package/src/Center/Center.spec.md +225 -0
  106. package/src/Center/Center.test.tsx +42 -4
  107. package/src/Center/Center.tsx +24 -17
  108. package/src/Chat/ChatSystemMessage.test.tsx +2 -9
  109. package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
  110. package/src/CodeBlock/CodeBlock.tsx +2 -2
  111. package/src/DateInput/DateInput.test.tsx +4 -4
  112. package/src/DateInput/DateInput.tsx +15 -4
  113. package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
  114. package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
  115. package/src/DateTimeInput/DateTimeInput.tsx +18 -7
  116. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
  117. package/src/Field/Field.test.tsx +42 -0
  118. package/src/Field/Field.tsx +6 -0
  119. package/src/Field/InputClearButton.test.tsx +35 -1
  120. package/src/Field/InputClearButton.tsx +7 -3
  121. package/src/Field/PanelSearchInput.tsx +21 -8
  122. package/src/FieldStatus/FieldStatus.spec.md +27 -17
  123. package/src/FieldStatus/FieldStatus.test.tsx +7 -5
  124. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
  125. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
  126. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
  127. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
  128. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
  129. package/src/FileInput/FileInput.tsx +10 -1
  130. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
  131. package/src/HoverCard/HoverCard.doc.mjs +4 -4
  132. package/src/HoverCard/useHoverCard.tsx +2 -2
  133. package/src/Indicator/CheckboxIndicator.tsx +2 -2
  134. package/src/Indicator/Indicator.doc.mjs +2 -2
  135. package/src/Indicator/Indicator.test.tsx +1 -1
  136. package/src/Indicator/RadioIndicator.tsx +2 -2
  137. package/src/Layer/layerStack.ts +20 -9
  138. package/src/Layer/useLayerDismissal.ts +2 -3
  139. package/src/MultiSelector/MultiSelector.test.tsx +4 -4
  140. package/src/NavIcon/NavIcon.doc.mjs +4 -4
  141. package/src/NavIcon/NavIcon.tsx +2 -2
  142. package/src/NumberInput/NumberInput.tsx +18 -7
  143. package/src/Popover/Popover.doc.mjs +10 -10
  144. package/src/Popover/Popover.spec.md +55 -65
  145. package/src/Popover/Popover.test.tsx +29 -0
  146. package/src/Popover/usePopover.doc.mjs +4 -4
  147. package/src/Popover/usePopover.tsx +7 -4
  148. package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
  149. package/src/ProgressBar/ProgressBar.test.tsx +1 -31
  150. package/src/ProgressBar/ProgressBar.tsx +2 -2
  151. package/src/RadioList/RadioList.test.tsx +5 -144
  152. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
  153. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
  154. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
  155. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
  156. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
  157. package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
  158. package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
  159. package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
  160. package/src/ScrollableArea/ScrollableArea.tsx +259 -0
  161. package/src/ScrollableArea/index.ts +26 -0
  162. package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
  163. package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
  164. package/src/Selector/Selector.test.tsx +4 -4
  165. package/src/Spinner/Spinner.test.tsx +0 -18
  166. package/src/StatusDot/StatusDot.doc.mjs +4 -4
  167. package/src/StatusDot/StatusDot.tsx +2 -2
  168. package/src/TabList/TabList.test.tsx +5 -9
  169. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
  170. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
  171. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
  172. package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
  173. package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
  174. package/src/Table/Table.doc.mjs +2 -2
  175. package/src/TextArea/TextArea.doc.mjs +4 -4
  176. package/src/TextArea/TextArea.tsx +2 -2
  177. package/src/TextInput/TextInput.doc.mjs +2 -1
  178. package/src/TextInput/TextInput.test.tsx +94 -0
  179. package/src/TextInput/TextInput.tsx +22 -6
  180. package/src/TimeInput/TimeInput.tsx +18 -7
  181. package/src/Toast/ToastViewport.test.tsx +1 -39
  182. package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
  183. package/src/Typeahead/BaseTypeahead.spec.md +269 -0
  184. package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
  185. package/src/Typeahead/BaseTypeahead.tsx +99 -30
  186. package/src/hooks/index.ts +13 -0
  187. package/src/hooks/scrollGeometry.ts +155 -0
  188. package/src/hooks/scrollOwnerRegistry.ts +47 -0
  189. package/src/hooks/useFocusTrap.ts +22 -11
  190. package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
  191. package/src/hooks/useScrollableArea.doc.mjs +108 -0
  192. package/src/hooks/useScrollableArea.test.tsx +437 -0
  193. package/src/hooks/useScrollableArea.ts +469 -0
  194. package/src/index.ts +1 -0
  195. package/src/theme/defineTheme.test.ts +65 -105
  196. package/src/theme/defineTheme.ts +3 -9
  197. package/src/theme/derivedVarRegistry.ts +1 -1
  198. package/src/theme/localTokens.ts +25 -96
  199. package/src/theme/publicThemeHelperContract.test.ts +2 -2
  200. package/src/theme/themeAdaptations.test.ts +16 -42
  201. package/src/theme/themeAdaptations.ts +6 -9
  202. package/src/utils/themeProps.test.ts +29 -10
  203. package/src/utils/themeProps.ts +36 -17
@@ -560,6 +560,51 @@ describe('TextInput', () => {
560
560
  expect(handleEnter).toHaveBeenCalledTimes(1);
561
561
  });
562
562
 
563
+ it('does not call onEnter when Enter commits an IME conversion', () => {
564
+ // The browser fires this composing keydown for the Enter that commits an
565
+ // IME candidate (isComposing: true, or the legacy keyCode 229) before
566
+ // compositionend writes the conversion. It must NOT trigger onEnter —
567
+ // submit/save actions would fire before the user intends to submit.
568
+ // See utils/ime.ts and #6082.
569
+ const handleEnter = vi.fn();
570
+ const {container} = render(
571
+ <TextInput
572
+ label="Name"
573
+ value="にほんご"
574
+ onChange={() => {}}
575
+ onEnter={handleEnter}
576
+ />,
577
+ );
578
+ const input = container.querySelector('input')!;
579
+ fireEvent.keyDown(input, {key: 'Enter', isComposing: true});
580
+ expect(handleEnter).not.toHaveBeenCalled();
581
+ fireEvent.keyDown(input, {key: 'Enter', keyCode: 229});
582
+ expect(handleEnter).not.toHaveBeenCalled();
583
+
584
+ // A real, non-composing Enter after composition ends still submits.
585
+ fireEvent.keyDown(input, {key: 'Enter'});
586
+ expect(handleEnter).toHaveBeenCalledTimes(1);
587
+ });
588
+
589
+ it('still calls onKeyDown for composing keydowns', () => {
590
+ // onKeyDown is the raw escape hatch: it keeps receiving IME keydowns so
591
+ // consumers with custom composition handling are unaffected by the
592
+ // onEnter guard.
593
+ const handleKeyDown = vi.fn();
594
+ const {container} = render(
595
+ <TextInput
596
+ label="Name"
597
+ value="にほんご"
598
+ onChange={() => {}}
599
+ onKeyDown={handleKeyDown}
600
+ onEnter={() => {}}
601
+ />,
602
+ );
603
+ const input = container.querySelector('input')!;
604
+ fireEvent.keyDown(input, {key: 'Enter', isComposing: true});
605
+ expect(handleKeyDown).toHaveBeenCalledTimes(1);
606
+ });
607
+
563
608
  it('does not call onEnter for other keys', async () => {
564
609
  const user = userEvent.setup();
565
610
  const handleEnter = vi.fn();
@@ -1019,3 +1064,52 @@ describe('TextInput readonly theme state', () => {
1019
1064
  expect(root).not.toHaveAttribute('data-readonly');
1020
1065
  });
1021
1066
  });
1067
+
1068
+ describe('TextInput clear button focus behavior', () => {
1069
+ it('synchronously restores focus to the input on keyboard activation (detail === 0)', () => {
1070
+ const handleChange = vi.fn();
1071
+ render(
1072
+ <TextInput
1073
+ label="Search"
1074
+ value="test"
1075
+ hasClear
1076
+ onChange={handleChange}
1077
+ />,
1078
+ );
1079
+
1080
+ const input = screen.getByRole('textbox');
1081
+ const clearButton = screen.getByRole('button', {name: /clear/i});
1082
+
1083
+ clearButton.focus();
1084
+ expect(document.activeElement).toBe(clearButton);
1085
+
1086
+ // Keyboard activation (e.g. Enter / Space on focused button yields detail 0)
1087
+ fireEvent.click(clearButton, {detail: 0});
1088
+
1089
+ expect(handleChange).toHaveBeenCalledWith('', expect.any(Object));
1090
+ // Must be synchronously focused without waiting for animation frames
1091
+ expect(document.activeElement).toBe(input);
1092
+ });
1093
+
1094
+ it('defers focus restoration via requestAnimationFrame on pointer activation', () => {
1095
+ const handleChange = vi.fn();
1096
+ const rafSpy = vi.spyOn(window, 'requestAnimationFrame');
1097
+ render(
1098
+ <TextInput
1099
+ label="Search"
1100
+ value="test"
1101
+ hasClear
1102
+ onChange={handleChange}
1103
+ />,
1104
+ );
1105
+
1106
+ const clearButton = screen.getByRole('button', {name: /clear/i});
1107
+
1108
+ // Pointer activation (detail > 0)
1109
+ fireEvent.click(clearButton, {detail: 1});
1110
+
1111
+ expect(handleChange).toHaveBeenCalledWith('', expect.any(Object));
1112
+ expect(rafSpy).toHaveBeenCalled();
1113
+ rafSpy.mockRestore();
1114
+ });
1115
+ });
@@ -47,7 +47,7 @@ import {renderIconSlot, type IconType} from '../Icon';
47
47
  import {Spinner} from '../Spinner';
48
48
  import {useTooltip} from '../Tooltip';
49
49
  import {VisuallyHidden} from '../VisuallyHidden';
50
- import {getInputARIA} from '../utils';
50
+ import {getInputARIA, isImeKeyEvent} from '../utils';
51
51
 
52
52
  const styles = stylex.create({
53
53
  input: {
@@ -381,10 +381,21 @@ export function TextInput({
381
381
  };
382
382
 
383
383
  // Handle clear button click
384
- const handleClear = useCallback(() => {
385
- onChange?.('', null as unknown as ChangeEvent<HTMLInputElement>);
386
- inputRef.current?.focus();
387
- }, [onChange]);
384
+ const handleClear = useCallback(
385
+ (e?: React.MouseEvent<HTMLButtonElement>) => {
386
+ onChange?.('', null as unknown as ChangeEvent<HTMLInputElement>);
387
+ if (!e || e.detail === 0) {
388
+ inputRef.current?.focus();
389
+ } else {
390
+ // Defer focus restoration past the button's unmount task so iOS Safari
391
+ // and touch browsers don't jump the page scroll to 0 on tap.
392
+ requestAnimationFrame(() => {
393
+ inputRef.current?.focus({preventScroll: true});
394
+ });
395
+ }
396
+ },
397
+ [onChange],
398
+ );
388
399
 
389
400
  // Focus input when clicking anywhere on the wrapper (icons, padding, etc.)
390
401
  const {onClick: handleWrapperClick, onMouseUp: handleWrapperMouseUp} =
@@ -438,7 +449,12 @@ export function TextInput({
438
449
  onKeyDown={
439
450
  onEnter || onKeyDown
440
451
  ? e => {
441
- if (e.key === 'Enter') {
452
+ // The composing keydown fires before compositionend, so without
453
+ // this guard pressing Enter to commit a Japanese/Chinese/Korean
454
+ // IME conversion would trigger onEnter (submit/save actions)
455
+ // before the user intends to submit. onKeyDown still receives
456
+ // the raw event. See utils/ime.ts (#6082).
457
+ if (e.key === 'Enter' && !isImeKeyEvent(e.nativeEvent)) {
442
458
  onEnter?.();
443
459
  }
444
460
  onKeyDown?.(e);
@@ -622,13 +622,24 @@ export function TimeInput({
622
622
  );
623
623
 
624
624
  // Handle clear button click
625
- const handleClear = useCallback(() => {
626
- fireChange(undefined);
627
- // Focusing a native time control reopens the OS picker on iOS.
628
- if (!usesNativeTimePicker) {
629
- inputRef.current?.focus();
630
- }
631
- }, [fireChange, usesNativeTimePicker]);
625
+ const handleClear = useCallback(
626
+ (e?: React.MouseEvent<HTMLButtonElement>) => {
627
+ fireChange(undefined);
628
+ // Focusing a native time control reopens the OS picker on iOS.
629
+ if (!usesNativeTimePicker) {
630
+ if (!e || e.detail === 0) {
631
+ inputRef.current?.focus();
632
+ } else {
633
+ // Defer focus restoration past the button's unmount task so iOS Safari
634
+ // and touch browsers don't jump the page scroll to 0 on tap.
635
+ requestAnimationFrame(() => {
636
+ inputRef.current?.focus({preventScroll: true});
637
+ });
638
+ }
639
+ }
640
+ },
641
+ [fireChange, usesNativeTimePicker],
642
+ );
632
643
 
633
644
  // Focus input when clicking anywhere on the wrapper (icons, padding, etc.)
634
645
  const {onClick: handleWrapperClick, onMouseUp: handleWrapperMouseUp} =
@@ -34,7 +34,7 @@ import React from 'react';
34
34
  import {readFileSync} from 'node:fs';
35
35
  import {type AnnounceFn, __resetLiveRegionsForTest} from '../hooks/useAnnounce';
36
36
  import {Button} from '../Button';
37
- import {Toast, type ToastProps} from './Toast';
37
+ import type {ToastProps} from './Toast';
38
38
  import {ToastViewport} from './ToastViewport';
39
39
  import {useToast} from './useToast';
40
40
  import type {ToastOptions} from './types';
@@ -1720,44 +1720,6 @@ describe('Toast swipe dismissal', () => {
1720
1720
  });
1721
1721
  });
1722
1722
 
1723
- describe('Toast live-region fallback semantics', () => {
1724
- it('keeps standalone info Toast content in a polite status region', () => {
1725
- render(
1726
- <Toast
1727
- type="info"
1728
- body="Saved"
1729
- isAutoHide={false}
1730
- autoHideDuration={5000}
1731
- onDismiss={() => {}}
1732
- />,
1733
- );
1734
-
1735
- const visualToast = screen.getByText('Saved').closest('[data-type]');
1736
- expect(visualToast).toHaveAttribute('role', 'status');
1737
- expect(visualToast).toHaveAttribute('aria-live', 'polite');
1738
- expect(visualToast).toHaveAttribute('aria-atomic', 'true');
1739
- });
1740
-
1741
- it('keeps standalone error Toast content in an assertive alert region', () => {
1742
- render(
1743
- <Toast
1744
- type="error"
1745
- body="Upload failed"
1746
- isAutoHide={false}
1747
- autoHideDuration={5000}
1748
- onDismiss={() => {}}
1749
- />,
1750
- );
1751
-
1752
- const visualToast = screen
1753
- .getByText('Upload failed')
1754
- .closest('[data-type]');
1755
- expect(visualToast).toHaveAttribute('role', 'alert');
1756
- expect(visualToast).toHaveAttribute('aria-live', 'assertive');
1757
- expect(visualToast).toHaveAttribute('aria-atomic', 'true');
1758
- });
1759
- });
1760
-
1761
1723
  describe('ToastViewport region ARIA', () => {
1762
1724
  it('does not expose an empty notifications landmark', () => {
1763
1725
  renderViewport(<ShowToastButton />);
@@ -1,5 +1,62 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Input',
7
+ required: true,
8
+ description:
9
+ 'Bare combobox input. The caller supplies its visible field chrome and accessible name.',
10
+ },
11
+ {
12
+ name: 'Loading status',
13
+ required: false,
14
+ description:
15
+ 'Named Spinner shown beside the input while an asynchronous source is pending, unless a composed owner takes over the busy indicator lane.',
16
+ },
17
+ {
18
+ name: 'Dropdown',
19
+ required: false,
20
+ description:
21
+ 'Anchored listbox surface containing current search or bootstrap results.',
22
+ },
23
+ {
24
+ name: 'Empty state',
25
+ required: false,
26
+ description:
27
+ 'Disabled listbox option shown after a completed search returns no results.',
28
+ },
29
+ {
30
+ name: 'Result group heading',
31
+ required: false,
32
+ description: 'Visible label for a group of result options.',
33
+ },
34
+ {
35
+ name: 'Result row',
36
+ required: false,
37
+ description:
38
+ 'Option wrapper that owns highlight, selection, pointer, and keyboard behavior.',
39
+ },
40
+ {
41
+ name: 'Default item content',
42
+ required: false,
43
+ description:
44
+ 'TypeaheadItem label and optional supporting content rendered inside a result row.',
45
+ },
46
+ {
47
+ name: 'Caller-rendered item content',
48
+ required: false,
49
+ description:
50
+ 'Caller content supplied through renderItem or item.element inside the stable result row.',
51
+ },
52
+ {
53
+ name: 'Selected result state',
54
+ required: false,
55
+ description:
56
+ 'Selected row weight and trailing check shown when a result matches value.',
57
+ },
58
+ ];
59
+
3
60
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
61
 
5
62
  export const docs = {
@@ -7,15 +64,43 @@ export const docs = {
7
64
  subComponentOf: 'Typeahead',
8
65
  displayName: 'Base Typeahead',
9
66
  isHiddenFromOverview: true,
10
- description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
67
+ description:
68
+ 'Composable combobox engine providing a bare input, search, keyboard navigation, and a styled result dropdown. It renders no input wrapper, border, or selected-value token. Typeahead and Tokenizer compose it for standard fields.',
11
69
  usage: {
12
- description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
70
+ anatomy,
71
+ description:
72
+ 'Composable combobox engine providing a bare input, search, keyboard navigation, and a styled result dropdown. It renders no input wrapper, border, or selected-value token. Typeahead and Tokenizer compose it for standard fields.',
13
73
  bestPractices: [
14
- { guidance: true, description: 'Use Typeahead or Tokenizer for standard fields; they wrap BaseTypeahead with the wrapper div, border styling, and token rendering it intentionally omits.' },
15
- { guidance: true, description: 'Provide your own wrapper div with border and layout when composing directly, since BaseTypeahead renders no visual chrome of its own.' },
16
- { guidance: true, description: 'Pass anchorRef pointing to your wrapper so the dropdown positions against your custom input chrome, not just the bare input element.' },
17
- { guidance: false, description: 'Expect a wrapper div, border, or token rendering. BaseTypeahead is an engine only; all visual chrome is the caller\'s responsibility.' },
18
- { guidance: false, description: 'Use BaseTypeahead when Typeahead or Tokenizer would suffice; the extra wrapper and styling work is only justified for truly custom compositions.' },
74
+ {
75
+ guidance: true,
76
+ description:
77
+ 'Use Typeahead or Tokenizer for standard fields; they wrap BaseTypeahead with input chrome and selected-value rendering it intentionally omits.',
78
+ },
79
+ {
80
+ guidance: true,
81
+ description:
82
+ 'Provide your own visible label or aria-label and custom input wrapper so the bare combobox has an accessible name, focus treatment, border, and layout.',
83
+ },
84
+ {
85
+ guidance: true,
86
+ description:
87
+ 'Pass anchorRef pointing to your wrapper so the dropdown positions against your custom input chrome, not just the bare input element.',
88
+ },
89
+ {
90
+ guidance: false,
91
+ description:
92
+ 'Expect input chrome or selected-value rendering. BaseTypeahead is an engine; the caller owns those visible parts.',
93
+ },
94
+ {
95
+ guidance: false,
96
+ description:
97
+ 'Use BaseTypeahead when Typeahead or Tokenizer would suffice; the extra wrapper and styling work is only justified for truly custom compositions.',
98
+ },
99
+ {
100
+ guidance: false,
101
+ description:
102
+ 'Treat Escape as cancellation of pending source work. It hides the current popup, but a late response can reopen it.',
103
+ },
19
104
  ],
20
105
  },
21
106
  props: [
@@ -46,7 +131,7 @@ export const docs = {
46
131
  name: 'placeholder',
47
132
  type: 'string',
48
133
  description: 'Input placeholder text.',
49
- default: "'Search...'",
134
+ default: "'Search…'",
50
135
  },
51
136
  {
52
137
  name: 'hasEntriesOnFocus',
@@ -63,12 +148,14 @@ export const docs = {
63
148
  {
64
149
  name: 'menuWidth',
65
150
  type: 'number',
66
- description: 'Fixed dropdown width in pixels. The menu never shrinks below its anchor width.',
151
+ description:
152
+ 'Requested dropdown width in pixels before viewport clamping.',
67
153
  },
68
154
  {
69
155
  name: 'minQueryLength',
70
156
  type: 'number',
71
- description: 'Minimum query length before the search source is queried. Below it no search runs and the menu stays closed.',
157
+ description:
158
+ 'Minimum query length before the search source is queried. Below it no search runs and the menu stays closed.',
72
159
  default: '1',
73
160
  },
74
161
  {
@@ -83,6 +170,13 @@ export const docs = {
83
170
  description: 'Whether the input is disabled.',
84
171
  default: 'false',
85
172
  },
173
+ {
174
+ name: 'isFocusableDisabled',
175
+ type: 'boolean',
176
+ description:
177
+ 'Keep a disabled input focusable with aria-disabled and readOnly so a caller-owned disabled reason remains discoverable. It blocks text entry, but when applied after results are already open, Enter can still select the highlighted option.',
178
+ default: 'false',
179
+ },
86
180
  {
87
181
  name: 'hasAutoFocus',
88
182
  type: 'boolean',
@@ -92,23 +186,44 @@ export const docs = {
92
186
  {
93
187
  name: 'debounceMs',
94
188
  type: 'number',
95
- description: 'Debounce delay in ms before triggering search. Set to 0 for synchronous sources.',
189
+ description:
190
+ 'Debounce delay in ms before triggering search. Set to 0 for synchronous sources.',
96
191
  default: '150',
97
192
  },
193
+ {
194
+ name: 'size',
195
+ type: "'sm' | 'md' | 'lg'",
196
+ description: 'Size used to scale dropdown option padding.',
197
+ default: "'md'",
198
+ },
98
199
  {
99
200
  name: 'anchorRef',
100
201
  type: 'RefObject<HTMLElement | null>',
101
- description: 'Ref to the anchor element for dropdown positioning. If not provided, the input itself is used.',
202
+ description:
203
+ 'Ref to the anchor element for dropdown positioning. If not provided, the input itself is used.',
102
204
  },
103
205
  {
104
206
  name: 'inputXStyle',
105
207
  type: 'StyleXStyles',
106
208
  description: 'Additional StyleX styles for the input element.',
107
209
  },
210
+ {
211
+ name: 'xstyle',
212
+ type: 'StyleXStyles',
213
+ description:
214
+ 'Standard BaseProps StyleX styles applied to the input. Must be a stylex.create() value, not an inline style object.',
215
+ },
216
+ {
217
+ name: 'inputTabIndex',
218
+ type: 'number',
219
+ description:
220
+ 'Legacy input-specific alias for native tabIndex. When provided, it takes precedence; otherwise native tabIndex is preserved.',
221
+ },
108
222
  {
109
223
  name: 'onKeyDown',
110
224
  type: '(e: React.KeyboardEvent<HTMLInputElement>) => void',
111
- description: 'Additional keydown handler called before internal keyboard navigation. Call e.preventDefault() to skip internal handling.',
225
+ description:
226
+ 'Additional keydown handler called before internal keyboard navigation. Call e.preventDefault() to skip internal handling.',
112
227
  },
113
228
  {
114
229
  name: 'onChangeQuery',
@@ -123,12 +238,20 @@ export const docs = {
123
238
  {
124
239
  name: 'inputId',
125
240
  type: 'string',
126
- description: 'ID for the input element (for label association).',
241
+ description:
242
+ 'Legacy input-specific alias for native id. When provided, it takes precedence; otherwise native id is preserved.',
127
243
  },
128
244
  {
129
245
  name: 'ariaDescribedBy',
130
246
  type: 'string',
131
- description: 'Additional aria-describedby IDs.',
247
+ description:
248
+ 'Legacy input-specific alias for native aria-describedby. When provided, it takes precedence; otherwise the native attribute is preserved.',
249
+ },
250
+ {
251
+ name: 'ariaLabelledBy',
252
+ type: 'string',
253
+ description:
254
+ 'Legacy input-specific alias for native aria-labelledby. When provided, it takes precedence; otherwise the native attribute is preserved.',
132
255
  },
133
256
  ],
134
257
  };
@@ -137,7 +260,8 @@ export const docsZh = {
137
260
  name: 'BaseTypeahead',
138
261
  isHiddenFromOverview: true,
139
262
  displayName: 'Base Typeahead',
140
- description: '无样式的组合框引擎,提供输入、搜索、键盘导航和下拉列表。无包装 div,无边框样式,无标记渲染。由 Typeahead 和 Tokenizer 用于自定义组合。',
263
+ description:
264
+ '可组合的组合框引擎,提供裸输入框、搜索、键盘导航和带样式的结果下拉列表。它不渲染输入框包装、边框或已选值标记;Typeahead 和 Tokenizer 将其组合成标准字段。',
141
265
  props: [
142
266
  {
143
267
  name: 'searchSource',
@@ -166,7 +290,7 @@ export const docsZh = {
166
290
  name: 'placeholder',
167
291
  type: 'string',
168
292
  description: '输入框占位文本。',
169
- default: "'Search...'",
293
+ default: "'Search…'",
170
294
  },
171
295
  {
172
296
  name: 'hasEntriesOnFocus',
@@ -183,12 +307,13 @@ export const docsZh = {
183
307
  {
184
308
  name: 'menuWidth',
185
309
  type: 'number',
186
- description: '下拉菜单的固定像素宽度。菜单不会小于其锚点宽度。',
310
+ description: '视口限制前请求的下拉菜单像素宽度。',
187
311
  },
188
312
  {
189
313
  name: 'minQueryLength',
190
314
  type: 'number',
191
- description: '查询搜索源前的最小查询长度。低于该长度不会发起搜索,菜单保持关闭。',
315
+ description:
316
+ '查询搜索源前的最小查询长度。低于该长度不会发起搜索,菜单保持关闭。',
192
317
  default: '1',
193
318
  },
194
319
  {
@@ -203,6 +328,13 @@ export const docsZh = {
203
328
  description: '输入框是否被禁用。',
204
329
  default: 'false',
205
330
  },
331
+ {
332
+ name: 'isFocusableDisabled',
333
+ type: 'boolean',
334
+ description:
335
+ '使用 aria-disabled 和只读状态保持禁用输入框可聚焦,以便访问调用方提供的禁用原因。它会阻止文本输入,但如果结果已打开,按 Enter 仍可选择高亮选项。',
336
+ default: 'false',
337
+ },
206
338
  {
207
339
  name: 'hasAutoFocus',
208
340
  type: 'boolean',
@@ -215,6 +347,12 @@ export const docsZh = {
215
347
  description: '触发搜索前的防抖延迟(毫秒)。同步数据源设置为 0。',
216
348
  default: '150',
217
349
  },
350
+ {
351
+ name: 'size',
352
+ type: "'sm' | 'md' | 'lg'",
353
+ description: '用于调整下拉选项内边距的尺寸。',
354
+ default: "'md'",
355
+ },
218
356
  {
219
357
  name: 'anchorRef',
220
358
  type: 'RefObject<HTMLElement | null>',
@@ -225,10 +363,22 @@ export const docsZh = {
225
363
  type: 'StyleXStyles',
226
364
  description: '输入元素的附加 StyleX 样式。',
227
365
  },
366
+ {
367
+ name: 'xstyle',
368
+ type: 'StyleXStyles',
369
+ description: '应用于输入元素的标准 BaseProps StyleX 样式。',
370
+ },
371
+ {
372
+ name: 'inputTabIndex',
373
+ type: 'number',
374
+ description:
375
+ '原生 tabIndex 的旧输入专用别名。提供时优先;未提供时保留原生属性。',
376
+ },
228
377
  {
229
378
  name: 'onKeyDown',
230
379
  type: '(e: React.KeyboardEvent<HTMLInputElement>) => void',
231
- description: '在内部键盘导航之前调用的附加 keydown 处理函数。调用 e.preventDefault() 可跳过内部处理。',
380
+ description:
381
+ '在内部键盘导航之前调用的附加 keydown 处理函数。调用 e.preventDefault() 可跳过内部处理。',
232
382
  },
233
383
  {
234
384
  name: 'onChangeQuery',
@@ -243,12 +393,20 @@ export const docsZh = {
243
393
  {
244
394
  name: 'inputId',
245
395
  type: 'string',
246
- description: '输入元素的 ID(用于标签关联)。',
396
+ description:
397
+ '原生 id 的旧输入专用别名。提供时优先;未提供时保留原生属性。',
247
398
  },
248
399
  {
249
400
  name: 'ariaDescribedBy',
250
401
  type: 'string',
251
- description: '附加的 aria-describedby ID。',
402
+ description:
403
+ '原生 aria-describedby 的旧输入专用别名。提供时优先;未提供时保留原生属性。',
404
+ },
405
+ {
406
+ name: 'ariaLabelledBy',
407
+ type: 'string',
408
+ description:
409
+ '原生 aria-labelledby 的旧输入专用别名。提供时优先;未提供时保留原生属性。',
252
410
  },
253
411
  ],
254
412
  };
@@ -257,14 +415,40 @@ export const docsDense = {
257
415
  name: 'BaseTypeahead',
258
416
  isHiddenFromOverview: true,
259
417
  displayName: 'Base Typeahead',
260
- description: 'Unstyled combobox engine; input+search+keyboard nav+dropdown. No wrapper/border/token. Used by Typeahead+Tokenizer.',
418
+ description:
419
+ 'Composable combobox engine providing a bare input and a styled result dropdown. Callers own input chrome and selected-value presentation.',
261
420
  usage: {
262
421
  bestPractices: [
263
- { guidance: true, description: 'Use Typeahead or Tokenizer for standard fields; they add the wrapper, border, and token rendering BaseTypeahead omits.' },
264
- { guidance: true, description: 'Provide your own wrapper div with border and layout when composing directly; BaseTypeahead renders no chrome.' },
265
- { guidance: true, description: 'Pass anchorRef to your wrapper so the dropdown positions against your input chrome, not the bare input.' },
266
- { guidance: false, description: 'Expect wrapper, border, or token rendering. Engine only; chrome is the caller\'s job.' },
267
- { guidance: false, description: 'Use BaseTypeahead when Typeahead or Tokenizer suffice; extra work only pays off for custom compositions.' },
422
+ {
423
+ guidance: true,
424
+ description:
425
+ 'Use Typeahead or Tokenizer for standard fields; they add the input chrome and selected-value rendering BaseTypeahead omits.',
426
+ },
427
+ {
428
+ guidance: true,
429
+ description:
430
+ 'Provide a visible label or aria-label plus a custom wrapper with focus treatment, border, and layout.',
431
+ },
432
+ {
433
+ guidance: true,
434
+ description:
435
+ 'Pass anchorRef to your wrapper so the dropdown positions against your input chrome, not the bare input.',
436
+ },
437
+ {
438
+ guidance: false,
439
+ description:
440
+ 'Expect input chrome or selected-value rendering. The caller owns those visible parts.',
441
+ },
442
+ {
443
+ guidance: false,
444
+ description:
445
+ 'Use BaseTypeahead when Typeahead or Tokenizer suffice; extra work only pays off for custom compositions.',
446
+ },
447
+ {
448
+ guidance: false,
449
+ description:
450
+ 'Treat Escape as pending-work cancellation. It hides the popup, but a late response can reopen it.',
451
+ },
268
452
  ],
269
453
  },
270
454
  propDescriptions: {
@@ -275,18 +459,30 @@ export const docsDense = {
275
459
  placeholder: 'Input placeholder.',
276
460
  hasEntriesOnFocus: 'Bootstrap results on focus.',
277
461
  maxMenuItems: 'Max dropdown items.',
278
- menuWidth: 'Fixed dropdown width in pixels.',
279
- minQueryLength: 'Min query length before searching. Menu stays closed below it.',
462
+ menuWidth: 'Requested px width before viewport clamping.',
463
+ minQueryLength:
464
+ 'Min query length before searching. Menu stays closed below it.',
280
465
  emptySearchResultsText: 'Text when no results.',
281
466
  isDisabled: 'Whether input disabled.',
467
+ isFocusableDisabled:
468
+ 'Keeps disabled input focusable and blocks text entry; an already-open highlight can still be selected with Enter.',
282
469
  hasAutoFocus: 'Auto-focus on mount.',
283
470
  debounceMs: 'Search debounce ms. 0 for sync.',
471
+ size: 'Dropdown option padding size.',
284
472
  anchorRef: 'Anchor for dropdown positioning. Defaults to input.',
285
473
  inputXStyle: 'Additional StyleX styles for input.',
286
- onKeyDown: 'Keydown before internal nav. preventDefault() skips internal handling.',
474
+ xstyle: 'Standard BaseProps StyleX styles for input.',
475
+ inputTabIndex:
476
+ 'Legacy tabIndex alias; defined alias wins, otherwise native tabIndex passes through.',
477
+ onKeyDown:
478
+ 'Keydown before internal nav. preventDefault() skips internal handling.',
287
479
  onChangeQuery: 'Fired on query text change.',
288
480
  onOpenChange: 'Fired on dropdown open/close.',
289
- inputId: 'Input ID for label association.',
290
- ariaDescribedBy: 'Additional aria-describedby IDs.',
481
+ inputId:
482
+ 'Legacy id alias; defined alias wins, otherwise native id passes through.',
483
+ ariaDescribedBy:
484
+ 'Legacy aria-describedby alias; defined alias wins, otherwise native attribute passes through.',
485
+ ariaLabelledBy:
486
+ 'Legacy aria-labelledby alias; defined alias wins, otherwise native attribute passes through.',
291
487
  },
292
488
  };