@astryxdesign/core 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
  9. package/dist/Center/Center.d.ts +23 -16
  10. package/dist/Center/Center.d.ts.map +1 -1
  11. package/dist/Center/Center.js +7 -5
  12. package/dist/CodeBlock/CodeBlock.js +2 -2
  13. package/dist/DateInput/DateInput.d.ts.map +1 -1
  14. package/dist/DateInput/DateInput.js +12 -2
  15. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  16. package/dist/DateTimeInput/DateTimeInput.js +12 -2
  17. package/dist/Field/Field.d.ts.map +1 -1
  18. package/dist/Field/Field.js +1 -0
  19. package/dist/Field/InputClearButton.d.ts +2 -2
  20. package/dist/Field/InputClearButton.d.ts.map +1 -1
  21. package/dist/Field/InputClearButton.js +5 -1
  22. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  23. package/dist/Field/PanelSearchInput.js +16 -4
  24. package/dist/FileInput/FileInput.d.ts.map +1 -1
  25. package/dist/FileInput/FileInput.js +12 -1
  26. package/dist/HoverCard/useHoverCard.js +2 -2
  27. package/dist/Indicator/CheckboxIndicator.js +2 -2
  28. package/dist/Indicator/RadioIndicator.js +2 -2
  29. package/dist/Layer/layerStack.d.ts +10 -0
  30. package/dist/Layer/layerStack.d.ts.map +1 -1
  31. package/dist/Layer/layerStack.js +21 -9
  32. package/dist/Layer/useLayerDismissal.d.ts +2 -3
  33. package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
  34. package/dist/Layer/useLayerDismissal.js +2 -3
  35. package/dist/NavIcon/NavIcon.js +2 -2
  36. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  37. package/dist/NumberInput/NumberInput.js +12 -2
  38. package/dist/Popover/usePopover.d.ts +3 -2
  39. package/dist/Popover/usePopover.d.ts.map +1 -1
  40. package/dist/Popover/usePopover.js +4 -2
  41. package/dist/ProgressBar/ProgressBar.js +2 -2
  42. package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
  43. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
  44. package/dist/ScrollableArea/ScrollableArea.js +144 -0
  45. package/dist/ScrollableArea/index.d.ts +11 -0
  46. package/dist/ScrollableArea/index.d.ts.map +1 -0
  47. package/dist/ScrollableArea/index.js +11 -0
  48. package/dist/StatusDot/StatusDot.js +2 -2
  49. package/dist/TextArea/TextArea.js +2 -2
  50. package/dist/TextInput/TextInput.d.ts.map +1 -1
  51. package/dist/TextInput/TextInput.js +19 -4
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +12 -2
  54. package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
  55. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/BaseTypeahead.js +56 -20
  57. package/dist/astryx.css +15 -0
  58. package/dist/hooks/index.d.ts +2 -0
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/index.js +1 -0
  61. package/dist/hooks/scrollGeometry.d.ts +24 -0
  62. package/dist/hooks/scrollGeometry.d.ts.map +1 -0
  63. package/dist/hooks/scrollGeometry.js +86 -0
  64. package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
  65. package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
  66. package/dist/hooks/scrollOwnerRegistry.js +24 -0
  67. package/dist/hooks/useFocusTrap.d.ts +8 -0
  68. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  69. package/dist/hooks/useFocusTrap.js +22 -11
  70. package/dist/hooks/useScrollableArea.d.ts +51 -0
  71. package/dist/hooks/useScrollableArea.d.ts.map +1 -0
  72. package/dist/hooks/useScrollableArea.js +287 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/theme/defineTheme.d.ts +2 -6
  77. package/dist/theme/defineTheme.d.ts.map +1 -1
  78. package/dist/theme/defineTheme.js +1 -1
  79. package/dist/theme/derivedVarRegistry.js +1 -1
  80. package/dist/theme/localTokens.d.ts +8 -11
  81. package/dist/theme/localTokens.d.ts.map +1 -1
  82. package/dist/theme/localTokens.js +17 -71
  83. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  84. package/dist/theme/themeAdaptations.js +4 -4
  85. package/dist/utils/themeProps.d.ts +10 -10
  86. package/dist/utils/themeProps.d.ts.map +1 -1
  87. package/dist/utils/themeProps.js +27 -10
  88. package/locales/en.json +16 -0
  89. package/locales/pseudo.json +12 -0
  90. package/package.json +7 -2
  91. package/src/AppShell/AppShell.test.tsx +36 -0
  92. package/src/AppShell/AppShell.tsx +4 -1
  93. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  94. package/src/Banner/Banner.test.tsx +3 -1
  95. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
  96. package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
  97. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
  98. package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
  99. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
  100. package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
  101. package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
  102. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
  103. package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
  104. package/src/Center/Center.doc.mjs +32 -28
  105. package/src/Center/Center.spec.md +225 -0
  106. package/src/Center/Center.test.tsx +42 -4
  107. package/src/Center/Center.tsx +24 -17
  108. package/src/Chat/ChatSystemMessage.test.tsx +2 -9
  109. package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
  110. package/src/CodeBlock/CodeBlock.tsx +2 -2
  111. package/src/DateInput/DateInput.test.tsx +4 -4
  112. package/src/DateInput/DateInput.tsx +15 -4
  113. package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
  114. package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
  115. package/src/DateTimeInput/DateTimeInput.tsx +18 -7
  116. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
  117. package/src/Field/Field.test.tsx +42 -0
  118. package/src/Field/Field.tsx +6 -0
  119. package/src/Field/InputClearButton.test.tsx +35 -1
  120. package/src/Field/InputClearButton.tsx +7 -3
  121. package/src/Field/PanelSearchInput.tsx +21 -8
  122. package/src/FieldStatus/FieldStatus.spec.md +27 -17
  123. package/src/FieldStatus/FieldStatus.test.tsx +7 -5
  124. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
  125. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
  126. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
  127. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
  128. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
  129. package/src/FileInput/FileInput.tsx +10 -1
  130. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
  131. package/src/HoverCard/HoverCard.doc.mjs +4 -4
  132. package/src/HoverCard/useHoverCard.tsx +2 -2
  133. package/src/Indicator/CheckboxIndicator.tsx +2 -2
  134. package/src/Indicator/Indicator.doc.mjs +2 -2
  135. package/src/Indicator/Indicator.test.tsx +1 -1
  136. package/src/Indicator/RadioIndicator.tsx +2 -2
  137. package/src/Layer/layerStack.ts +20 -9
  138. package/src/Layer/useLayerDismissal.ts +2 -3
  139. package/src/MultiSelector/MultiSelector.test.tsx +4 -4
  140. package/src/NavIcon/NavIcon.doc.mjs +4 -4
  141. package/src/NavIcon/NavIcon.tsx +2 -2
  142. package/src/NumberInput/NumberInput.tsx +18 -7
  143. package/src/Popover/Popover.doc.mjs +10 -10
  144. package/src/Popover/Popover.spec.md +55 -65
  145. package/src/Popover/Popover.test.tsx +29 -0
  146. package/src/Popover/usePopover.doc.mjs +4 -4
  147. package/src/Popover/usePopover.tsx +7 -4
  148. package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
  149. package/src/ProgressBar/ProgressBar.test.tsx +1 -31
  150. package/src/ProgressBar/ProgressBar.tsx +2 -2
  151. package/src/RadioList/RadioList.test.tsx +5 -144
  152. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
  153. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
  154. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
  155. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
  156. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
  157. package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
  158. package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
  159. package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
  160. package/src/ScrollableArea/ScrollableArea.tsx +259 -0
  161. package/src/ScrollableArea/index.ts +26 -0
  162. package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
  163. package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
  164. package/src/Selector/Selector.test.tsx +4 -4
  165. package/src/Spinner/Spinner.test.tsx +0 -18
  166. package/src/StatusDot/StatusDot.doc.mjs +4 -4
  167. package/src/StatusDot/StatusDot.tsx +2 -2
  168. package/src/TabList/TabList.test.tsx +5 -9
  169. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
  170. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
  171. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
  172. package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
  173. package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
  174. package/src/Table/Table.doc.mjs +2 -2
  175. package/src/TextArea/TextArea.doc.mjs +4 -4
  176. package/src/TextArea/TextArea.tsx +2 -2
  177. package/src/TextInput/TextInput.doc.mjs +2 -1
  178. package/src/TextInput/TextInput.test.tsx +94 -0
  179. package/src/TextInput/TextInput.tsx +22 -6
  180. package/src/TimeInput/TimeInput.tsx +18 -7
  181. package/src/Toast/ToastViewport.test.tsx +1 -39
  182. package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
  183. package/src/Typeahead/BaseTypeahead.spec.md +269 -0
  184. package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
  185. package/src/Typeahead/BaseTypeahead.tsx +99 -30
  186. package/src/hooks/index.ts +13 -0
  187. package/src/hooks/scrollGeometry.ts +155 -0
  188. package/src/hooks/scrollOwnerRegistry.ts +47 -0
  189. package/src/hooks/useFocusTrap.ts +22 -11
  190. package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
  191. package/src/hooks/useScrollableArea.doc.mjs +108 -0
  192. package/src/hooks/useScrollableArea.test.tsx +437 -0
  193. package/src/hooks/useScrollableArea.ts +469 -0
  194. package/src/index.ts +1 -0
  195. package/src/theme/defineTheme.test.ts +65 -105
  196. package/src/theme/defineTheme.ts +3 -9
  197. package/src/theme/derivedVarRegistry.ts +1 -1
  198. package/src/theme/localTokens.ts +25 -96
  199. package/src/theme/publicThemeHelperContract.test.ts +2 -2
  200. package/src/theme/themeAdaptations.test.ts +16 -42
  201. package/src/theme/themeAdaptations.ts +6 -9
  202. package/src/utils/themeProps.test.ts +29 -10
  203. package/src/utils/themeProps.ts +36 -17
@@ -0,0 +1,155 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @vitest-environment jsdom */
3
+
4
+ /**
5
+ * @file StatusMessage.a11y.test.tsx
6
+ * @input Uses the shared status-message contract and current Core binding render map
7
+ * @output Component-facing jsdom evidence through expectAccessibilitySpec
8
+ * @position Fast migration lane; browser-owned outcomes remain explicitly unrun
9
+ */
10
+
11
+ import {afterEach, describe, expect, it} from 'vitest';
12
+ import {
13
+ act,
14
+ cleanup,
15
+ fireEvent,
16
+ render,
17
+ screen,
18
+ waitFor,
19
+ } from '@testing-library/react';
20
+ import {
21
+ STATUS_MESSAGE_PATTERN,
22
+ expectAccessibilitySpec,
23
+ } from '@astryxdesign/a11y-spec';
24
+ import {__resetLiveRegionsForTest} from '../../hooks/useAnnounce';
25
+ import {CORE_STATUS_MESSAGE_KNOWN_FAILURES} from './StatusMessage.a11y.known-failures';
26
+ import {
27
+ CORE_STATUS_MESSAGE_STATE_RENDERS,
28
+ transitionTestId,
29
+ } from './StatusMessage.a11y.renders';
30
+ import {
31
+ CORE_STATUS_MESSAGE_BINDING_STATES,
32
+ CORE_STATUS_MESSAGE_EXCLUSIONS,
33
+ type CoreStatusMessageBindingState,
34
+ } from './StatusMessage.a11y.states';
35
+
36
+ async function twoFrames(): Promise<void> {
37
+ await new Promise<void>(resolve => {
38
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
39
+ });
40
+ }
41
+
42
+ async function renderState(
43
+ state: CoreStatusMessageBindingState,
44
+ ): Promise<void> {
45
+ render(CORE_STATUS_MESSAGE_STATE_RENDERS[state.id]());
46
+ await waitFor(() =>
47
+ expect(document.querySelector('[data-a11y-ready="true"]')).not.toBeNull(),
48
+ );
49
+ }
50
+
51
+ function subjectFor(state: CoreStatusMessageBindingState): Element {
52
+ const facts = state.facts;
53
+ const role = facts.kind === 'progressbar' ? 'progressbar' : facts.role;
54
+ if (role == null) {
55
+ throw new Error(
56
+ `${state.id}: this component binding declares no public role`,
57
+ );
58
+ }
59
+ const matches =
60
+ facts.kind === 'progressbar'
61
+ ? screen.getAllByRole(role, {hidden: true, name: facts.name})
62
+ : facts.messageSource === 'accessible-name'
63
+ ? screen.getAllByRole(role, {
64
+ hidden: true,
65
+ name: facts.initialMessage,
66
+ })
67
+ : screen
68
+ .getAllByRole(role, {hidden: true})
69
+ .filter(
70
+ element =>
71
+ (element.textContent ?? '').replace(/\s+/g, ' ').trim() ===
72
+ facts.initialMessage,
73
+ );
74
+ if (matches.length !== 1) {
75
+ throw new Error(
76
+ `${state.id}: expected one ${role} subject in initial state, found ${matches.length}`,
77
+ );
78
+ }
79
+ return matches[0];
80
+ }
81
+
82
+ async function transition(name: string): Promise<void> {
83
+ fireEvent.click(screen.getByTestId(transitionTestId(name)));
84
+ await act(twoFrames);
85
+ }
86
+
87
+ function reset(): void {
88
+ cleanup();
89
+ __resetLiveRegionsForTest();
90
+ }
91
+
92
+ afterEach(reset);
93
+
94
+ describe('Core status-message bindings — jsdom lane', () => {
95
+ it.each(
96
+ CORE_STATUS_MESSAGE_BINDING_STATES.map(
97
+ state =>
98
+ [`${state.binding} [${state.id}]`, state.summary, state] as const,
99
+ ),
100
+ )('%s — %s', async (_id, _summary, state) => {
101
+ await expectAccessibilitySpec({
102
+ spec: STATUS_MESSAGE_PATTERN,
103
+ binding: state.binding,
104
+ state: state.id,
105
+ facts: state.facts,
106
+ knownFailures: CORE_STATUS_MESSAGE_KNOWN_FAILURES,
107
+ render: async () => renderState(state),
108
+ subject: () => subjectFor(state),
109
+ transition,
110
+ cleanup: reset,
111
+ });
112
+ });
113
+ });
114
+
115
+ describe('Core status-message binding inventory', () => {
116
+ it('names a distinct checked-in story for every bound state', () => {
117
+ const stories = CORE_STATUS_MESSAGE_BINDING_STATES.map(
118
+ state => state.storyId,
119
+ );
120
+ expect(new Set(stories).size).toBe(stories.length);
121
+ });
122
+
123
+ it('binds every named Core status-message owner', () => {
124
+ expect(
125
+ [
126
+ ...new Set(
127
+ CORE_STATUS_MESSAGE_BINDING_STATES.map(state => state.binding),
128
+ ),
129
+ ].sort(),
130
+ ).toEqual([
131
+ 'ChatSystemMessage',
132
+ 'FieldStatus announcement channel',
133
+ 'ProgressBar',
134
+ 'Spinner',
135
+ 'Toast announcement channel',
136
+ 'Toast card',
137
+ ]);
138
+ });
139
+
140
+ it('records every adjacent non-status or separately owned surface explicitly', () => {
141
+ expect(CORE_STATUS_MESSAGE_EXCLUSIONS.map(entry => entry.part)).toEqual([
142
+ 'visible validation text',
143
+ 'aria-hidden decorative spinner',
144
+ 'date or section separator',
145
+ 'role=log transcript updates',
146
+ 'composer errors and tool-run states',
147
+ 'busy announcement',
148
+ 'focusable target-mark triggers and labels',
149
+ 'visual fill, track, animation, and status cadence',
150
+ ]);
151
+ expect(
152
+ CORE_STATUS_MESSAGE_EXCLUSIONS.every(entry => entry.reason.length > 0),
153
+ ).toBe(true);
154
+ });
155
+ });
@@ -589,7 +589,16 @@ export function FileInput({
589
589
  onChange(null);
590
590
  if (inputRef.current) {
591
591
  inputRef.current.value = '';
592
- inputRef.current.focus();
592
+ const targetInput = inputRef.current;
593
+ if (e.detail === 0) {
594
+ targetInput.focus();
595
+ } else {
596
+ // Defer focus restoration past the button's unmount task so iOS Safari
597
+ // and touch browsers don't jump the page scroll to 0 on tap.
598
+ requestAnimationFrame(() => {
599
+ targetInput.focus({preventScroll: true});
600
+ });
601
+ }
593
602
  }
594
603
  },
595
604
  [onChange],
@@ -2,7 +2,7 @@
2
2
 
3
3
  exports[`FormLayout > matches snapshot for horizontal direction 1`] = `
4
4
  <div
5
- class="astryx-form-layout FormLayout__styles.base xdt5ytf x18g69wz FormLayout__styles.horizontal xrvj5dj x1mt1orb xu6a5m6"
5
+ class="astryx-form-layout horizontal FormLayout__styles.base xdt5ytf x18g69wz FormLayout__styles.horizontal xrvj5dj x1mt1orb xu6a5m6"
6
6
  data-direction="horizontal"
7
7
  data-testid="layout"
8
8
  >
@@ -17,7 +17,7 @@ exports[`FormLayout > matches snapshot for horizontal direction 1`] = `
17
17
 
18
18
  exports[`FormLayout > matches snapshot for horizontal-labels direction 1`] = `
19
19
  <div
20
- class="astryx-form-layout FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff"
20
+ class="astryx-form-layout horizontal-labels FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff"
21
21
  data-direction="horizontal-labels"
22
22
  data-testid="layout"
23
23
  >
@@ -32,7 +32,7 @@ exports[`FormLayout > matches snapshot for horizontal-labels direction 1`] = `
32
32
 
33
33
  exports[`FormLayout > matches snapshot for vertical direction 1`] = `
34
34
  <div
35
- class="astryx-form-layout FormLayout__styles.base x78zum5 xdt5ytf x18g69wz"
35
+ class="astryx-form-layout vertical FormLayout__styles.base x78zum5 xdt5ytf x18g69wz"
36
36
  data-direction="vertical"
37
37
  data-testid="layout"
38
38
  >
@@ -17,8 +17,8 @@ export const docs = {
17
17
  theming: {
18
18
  targets: [
19
19
  {className: 'astryx-hover-card'},
20
- // Still emitted beside the names above, so themes written against
21
- // them keep working. Drop in the next major.
20
+ // Retained beside the canonical names for backwards compatibility.
21
+ // New themes use the canonical targets above.
22
22
  {className: 'astryx-hovercard', deprecatedFor: 'hover-card'},
23
23
  ],
24
24
  vars: [
@@ -148,8 +148,8 @@ export const docsZh = {
148
148
  theming: {
149
149
  targets: [
150
150
  {className: 'astryx-hover-card'},
151
- // Still emitted beside the names above, so themes written against
152
- // them keep working. Drop in the next major.
151
+ // Retained beside the canonical names for backwards compatibility.
152
+ // New themes use the canonical targets above.
153
153
  {className: 'astryx-hovercard', deprecatedFor: 'hover-card'},
154
154
  ],
155
155
  vars: [
@@ -609,8 +609,8 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
609
609
  ): ReactNode => {
610
610
  const renderPlacement = props?.placement ?? placement;
611
611
  const themeClassName = themeProps('hover-card', undefined, {
612
- // `hovercard` ran the compound name together; themes styling it keep
613
- // working until the next major.
612
+ // `hovercard` ran the compound name together; keep it emitted so
613
+ // existing themes continue to work.
614
614
  legacyNames: ['hovercard'],
615
615
  }).className;
616
616
  const renderProps = {
@@ -206,8 +206,8 @@ export function CheckboxIndicator({
206
206
  : null,
207
207
  disabled: isDisabled ? 'disabled' : null,
208
208
  },
209
- // `checkbox` was the target before indicators existed; themes
210
- // styling it keep working until the next major.
209
+ // `checkbox` was the target before indicators existed; keep it
210
+ // emitted so existing themes continue to work.
211
211
  {legacyNames: ['checkbox']},
212
212
  ),
213
213
  stylex.props(
@@ -143,8 +143,8 @@ export const docs = {
143
143
  states: ['checked', 'disabled'],
144
144
  },
145
145
  {className: 'astryx-radio-indicator-dot', visualProps: ['size']},
146
- // Still emitted beside the names above, so themes written against them
147
- // keep working. Drop in the next major.
146
+ // Retained beside the canonical names for backwards compatibility.
147
+ // New themes use the canonical targets above.
148
148
  {
149
149
  className: 'astryx-checkbox',
150
150
  visualProps: ['size'],
@@ -344,7 +344,7 @@ describe('useIndicator', () => {
344
344
  * A theme target is public API. Renaming one to follow the
345
345
  * `<component>-kebab` convention (`checkbox` → `checkbox-indicator`) would
346
346
  * silently break every theme styling the old name — the CSS still compiles, it
347
- * just stops matching. So both names are emitted for a deprecation window, and
347
+ * just stops matching. So both names remain emitted for compatibility, and
348
348
  * these tests pin that promise from both ends: the new name exists, and the
349
349
  * old one has not quietly disappeared.
350
350
  */
@@ -170,8 +170,8 @@ export function RadioIndicator({
170
170
  checked: isChecked ? 'checked' : null,
171
171
  disabled: isDisabled ? 'disabled' : null,
172
172
  },
173
- // `radio` was the target before indicators existed; themes styling
174
- // it keep working until the next major.
173
+ // `radio` was the target before indicators existed; keep it emitted
174
+ // so existing themes continue to work.
175
175
  {legacyNames: ['radio']},
176
176
  ),
177
177
  stylex.props(
@@ -6,7 +6,8 @@
6
6
  * @file layerStack.ts
7
7
  * @input Uses the isImeKeyEvent predicate from utils/ime
8
8
  * @output Exports the shared layer dismissal stack: registration, top-most
9
- * ordering, and the single document-level Escape listener
9
+ * ordering, the document-level Escape listener, and its internal local-event
10
+ * dispatcher
10
11
  * @position Internal to the Layer system; consumed by useLayerDismissal, which
11
12
  * is what overlays actually call. Not exported from the package root.
12
13
  *
@@ -34,11 +35,12 @@
34
35
  * ## Bubble phase, not capture
35
36
  *
36
37
  * The listener is on the BUBBLE phase so content inside a layer can claim the
37
- * press first, either by `stopPropagation()` (the press never reaches us) or by
38
- * `preventDefault()` (we see it and stand down). Editors are the motivating
39
- * case: Monaco and the rich-text editor use Escape to close their own find
40
- * widget or autocomplete, and that must win over dismissing the Dialog they sit
41
- * in. A capture-phase listener would take the press away from them.
38
+ * press first, either by `stopPropagation()` (the press never reaches the
39
+ * document listener) or by `preventDefault()` (we see it and stand down).
40
+ * A host whose public event prop can stop propagation may route the still-
41
+ * unprevented event through `dispatchLayerEscapeKeyDown` at its own boundary;
42
+ * the same stack still chooses the owner. Editors are why `preventDefault()`
43
+ * remains the explicit cancellation signal: their own Escape action must win.
42
44
  *
43
45
  * ## The stack, not the browser, decides
44
46
  *
@@ -275,7 +277,16 @@ function dispatchEscape(): boolean {
275
277
  return true;
276
278
  }
277
279
 
278
- function handleKeyDown(event: KeyboardEvent): void {
280
+ /**
281
+ * Resolve a keydown through the shared stack immediately.
282
+ *
283
+ * The document listener is the normal path. A host may call this at its own
284
+ * element boundary after a consumer handler when that handler can stop
285
+ * propagation before the event reaches `document`. The same stack still picks
286
+ * the owner, and `preventDefault()` records the decision so a bubbling event is
287
+ * not handled twice.
288
+ */
289
+ export function dispatchLayerEscapeKeyDown(event: KeyboardEvent): void {
279
290
  if (event.key !== 'Escape') {
280
291
  return;
281
292
  }
@@ -309,7 +320,7 @@ function startListening(): void {
309
320
  if (isListening || typeof document === 'undefined') {
310
321
  return;
311
322
  }
312
- document.addEventListener('keydown', handleKeyDown);
323
+ document.addEventListener('keydown', dispatchLayerEscapeKeyDown);
313
324
  document.addEventListener('compositionstart', handleCompositionStart, true);
314
325
  document.addEventListener('compositionend', handleCompositionEnd, true);
315
326
  document.addEventListener('blur', handleCompositionEnd, true);
@@ -320,7 +331,7 @@ function stopListening(): void {
320
331
  if (!isListening || typeof document === 'undefined') {
321
332
  return;
322
333
  }
323
- document.removeEventListener('keydown', handleKeyDown);
334
+ document.removeEventListener('keydown', dispatchLayerEscapeKeyDown);
324
335
  document.removeEventListener(
325
336
  'compositionstart',
326
337
  handleCompositionStart,
@@ -97,9 +97,8 @@ export interface UseLayerDismissalReturn {
97
97
  * The layer does NOT attach a key listener — the stack owns one listener and
98
98
  * routes each Escape press to the top-most REGISTERED layer, so one press
99
99
  * dismisses exactly one of them. Dialog (and what is built on it), Popover and
100
- * the menus built on it, Tooltip, HoverCard, Lightbox and MobileNav register
101
- * today; `BottomSheetSwitcher` registers through its focus trap when modal and
102
- * still handles its own press when not.
100
+ * the menus built on it, Tooltip, HoverCard, Lightbox, MobileNav and
101
+ * BottomSheetSwitcher register today.
103
102
  *
104
103
  * `BottomSheet`, `CommandPalette`, `ContextMenu`, `DropdownMenuSubMenu`,
105
104
  * `PowerSearchEditPopover` and lab's `Drawer` still run their own Escape
@@ -2433,8 +2433,8 @@ describe('MultiSelector clear icon theme target', () => {
2433
2433
  // The canonical target lands on the icon element itself (not the button),
2434
2434
  // so a theme can restyle just this glyph (color, size, hover) via
2435
2435
  // `defineTheme` — a button-level target could not reach the icon's own
2436
- // color/size. The original per-component name rides along for a
2437
- // deprecation window.
2436
+ // color/size. The original per-component name remains as a compatibility
2437
+ // alias.
2438
2438
  const icon = getClearIcon();
2439
2439
  expect(icon).toHaveClass('astryx-input-clear-icon');
2440
2440
  expect(icon).toHaveClass('astryx-multi-selector-clear-icon');
@@ -2461,8 +2461,8 @@ describe('MultiSelector clear icon theme target', () => {
2461
2461
  it('routes the clear glyph through the shared clear button, keeping the legacy target', () => {
2462
2462
  // The clear affordance now composes the shared InputClearButton (a ghost
2463
2463
  // Button with a secondary/sm glyph), so the icon carries the canonical
2464
- // `astryx-input-clear-icon` target and — for a deprecation window — the
2465
- // original `astryx-multi-selector-clear-icon`. Aside from those target
2464
+ // `astryx-input-clear-icon` target plus the supported compatibility alias
2465
+ // `astryx-multi-selector-clear-icon`. Aside from those target
2466
2466
  // classes it matches the shared button's own `close`/`sm`/`secondary`
2467
2467
  // glyph exactly, so the default look is defined in one place.
2468
2468
  render(
@@ -37,8 +37,8 @@ export const docs = {
37
37
  theming: {
38
38
  targets: [
39
39
  {className: 'astryx-nav-icon'},
40
- // Still emitted beside the names above, so themes written against
41
- // them keep working. Drop in the next major.
40
+ // Retained beside the canonical names for backwards compatibility.
41
+ // New themes use the canonical targets above.
42
42
  {className: 'astryx-navicon', deprecatedFor: 'nav-icon'},
43
43
  ],
44
44
  },
@@ -70,8 +70,8 @@ export const docsZh = {
70
70
  theming: {
71
71
  targets: [
72
72
  {className: 'astryx-nav-icon'},
73
- // Still emitted beside the names above, so themes written against
74
- // them keep working. Drop in the next major.
73
+ // Retained beside the canonical names for backwards compatibility.
74
+ // New themes use the canonical targets above.
75
75
  {className: 'astryx-navicon', deprecatedFor: 'nav-icon'},
76
76
  ],
77
77
  },
@@ -80,8 +80,8 @@ export function NavIcon({
80
80
  ref={ref}
81
81
  {...mergeProps(
82
82
  themeProps('nav-icon', undefined, {
83
- // `navicon` ran the compound name together; themes styling it keep
84
- // working until the next major.
83
+ // `navicon` ran the compound name together; keep it emitted so
84
+ // existing themes continue to work.
85
85
  legacyNames: ['navicon'],
86
86
  }),
87
87
  stylex.props(styles.base, xstyle),
@@ -829,13 +829,24 @@ export function NumberInput({
829
829
  const canDecrement = getNextValue(-1) !== valueForStepping;
830
830
 
831
831
  // Handle clear button click
832
- const handleClear = useCallback(() => {
833
- if (hasClear) {
834
- onChange(null);
835
- }
836
- setPendingInput(null);
837
- inputRef.current?.focus();
838
- }, [hasClear, onChange]);
832
+ const handleClear = useCallback(
833
+ (e?: React.MouseEvent<HTMLButtonElement>) => {
834
+ if (hasClear) {
835
+ onChange(null);
836
+ }
837
+ setPendingInput(null);
838
+ if (!e || e.detail === 0) {
839
+ inputRef.current?.focus();
840
+ } else {
841
+ // Defer focus restoration past the button's unmount task so iOS Safari
842
+ // and touch browsers don't jump the page scroll to 0 on tap.
843
+ requestAnimationFrame(() => {
844
+ inputRef.current?.focus({preventScroll: true});
845
+ });
846
+ }
847
+ },
848
+ [hasClear, onChange],
849
+ );
839
850
 
840
851
  // Focus input when clicking anywhere on the wrapper (icons, padding, etc.)
841
852
  const {onClick: handleWrapperClick, onMouseUp: handleWrapperMouseUp} =
@@ -168,8 +168,8 @@ export const docs = {
168
168
  targets: [
169
169
  // Canonical broad target for the painted Popover surface.
170
170
  {className: 'astryx-popover'},
171
- // Deprecated compatibility alias. Existing themes keep working during
172
- // migration; new themes target `popover`.
171
+ // Deprecated compatibility alias. Existing themes remain supported;
172
+ // new themes target `popover`.
173
173
  {
174
174
  className: 'astryx-popover-surface',
175
175
  deprecatedFor: 'popover',
@@ -202,7 +202,7 @@ export const docs = {
202
202
  {
203
203
  guidance: true,
204
204
  description:
205
- 'Theme the painted surface through popover. Existing popover-surface overrides remain supported during migration, but new themes should not depend on that deprecated alias.',
205
+ 'Theme the painted surface through popover. Existing popover-surface overrides remain supported for compatibility, while new themes use the canonical target.',
206
206
  },
207
207
  {
208
208
  guidance: false,
@@ -231,7 +231,7 @@ export const docs = {
231
231
  name: 'Popover surface',
232
232
  required: true,
233
233
  description:
234
- 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains only as a deprecated compatibility alias during migration.',
234
+ 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains supported as a deprecated compatibility alias.',
235
235
  },
236
236
  {
237
237
  name: 'Popover content',
@@ -371,8 +371,8 @@ export const docsZh = {
371
371
  targets: [
372
372
  // Canonical broad target for the painted Popover surface.
373
373
  {className: 'astryx-popover'},
374
- // Deprecated compatibility alias. Existing themes keep working during
375
- // migration; new themes target `popover`.
374
+ // Deprecated compatibility alias. Existing themes remain supported;
375
+ // new themes target `popover`.
376
376
  {
377
377
  className: 'astryx-popover-surface',
378
378
  deprecatedFor: 'popover',
@@ -405,7 +405,7 @@ export const docsZh = {
405
405
  {
406
406
  guidance: true,
407
407
  description:
408
- 'Theme the painted surface through popover. Existing popover-surface overrides remain supported during migration, but new themes should not depend on that deprecated alias.',
408
+ 'Theme the painted surface through popover. Existing popover-surface overrides remain supported for compatibility, while new themes use the canonical target.',
409
409
  },
410
410
  {
411
411
  guidance: false,
@@ -434,7 +434,7 @@ export const docsZh = {
434
434
  name: 'Popover surface',
435
435
  required: true,
436
436
  description:
437
- 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains only as a deprecated compatibility alias during migration.',
437
+ 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains supported as a deprecated compatibility alias.',
438
438
  },
439
439
  {
440
440
  name: 'Popover content',
@@ -472,7 +472,7 @@ export const docsDense = {
472
472
  {
473
473
  guidance: true,
474
474
  description:
475
- 'Theme the painted surface through popover. Existing popover-surface overrides remain supported during migration, but new themes should not depend on that deprecated alias.',
475
+ 'Theme the painted surface through popover. Existing popover-surface overrides remain supported for compatibility, while new themes use the canonical target.',
476
476
  },
477
477
  {
478
478
  guidance: false,
@@ -501,7 +501,7 @@ export const docsDense = {
501
501
  name: 'Popover surface',
502
502
  required: true,
503
503
  description:
504
- 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains only as a deprecated compatibility alias during migration.',
504
+ 'Painted surface owned by Popover. Theme it through the canonical popover target; popover-surface remains supported as a deprecated compatibility alias.',
505
505
  },
506
506
  {
507
507
  name: 'Popover content',