@astryxdesign/core 0.7.0-canary.fe57f54 → 0.7.0

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 (125) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +5 -4
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +1 -1
  7. package/dist/Calendar/styles.d.ts +6 -6
  8. package/dist/Chat/ChatComposerInput.d.ts +0 -11
  9. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  10. package/dist/Chat/ChatComposerInput.js +0 -11
  11. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  12. package/dist/Chat/useTriggerMenu.js +1 -1
  13. package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
  14. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  15. package/dist/CommandPalette/CommandPaletteList.js +3 -2
  16. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  17. package/dist/DateInput/TouchDateField.js +4 -1
  18. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  19. package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
  20. package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
  21. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.js +173 -11
  23. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
  25. package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
  26. package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
  27. package/dist/DropdownMenu/menuRegionsContext.js +17 -0
  28. package/dist/Field/inputStyles.stylex.d.ts +3 -3
  29. package/dist/Layout/container.stylex.d.ts +18 -20
  30. package/dist/Layout/container.stylex.d.ts.map +1 -1
  31. package/dist/Layout/container.stylex.js +15 -25
  32. package/dist/Layout/padding.stylex.d.ts +231 -231
  33. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  34. package/dist/Lightbox/Lightbox.js +15 -6
  35. package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
  36. package/dist/Markdown/plugins/protocol.js +33 -8
  37. package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
  38. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  39. package/dist/PowerSearch/PowerSearch.js +21 -1
  40. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  41. package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
  42. package/dist/Stack/stack.stylex.d.ts +22 -22
  43. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  44. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
  45. package/dist/Table/table.stylex.d.ts +6 -6
  46. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  47. package/dist/TimeInput/TouchTimeField.js +4 -1
  48. package/dist/Tokenizer/Tokenizer.d.ts +1 -12
  49. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  50. package/dist/Tokenizer/Tokenizer.js +1 -13
  51. package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
  52. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  53. package/dist/Typeahead/BaseTypeahead.js +1 -9
  54. package/dist/Typeahead/Typeahead.d.ts +1 -12
  55. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/Typeahead.js +1 -13
  57. package/dist/astryx.css +17 -12
  58. package/dist/hooks/useFocusTrap.d.ts +8 -0
  59. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  60. package/dist/hooks/useFocusTrap.js +5 -1
  61. package/dist/hooks/useRenamedProp.d.ts +6 -6
  62. package/dist/hooks/useRenamedProp.js +6 -6
  63. package/dist/reset.a11y.chromium.spec.d.ts +2 -0
  64. package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
  65. package/dist/reset.a11y.chromium.spec.js +176 -0
  66. package/dist/theme/tokens.stylex.d.ts +30 -30
  67. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  68. package/dist/theme/tokens.stylex.js +18 -15
  69. package/dist/theme/useTheme.d.ts +1 -1
  70. package/package.json +4 -4
  71. package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
  72. package/src/BottomSheet/BottomSheet.tsx +5 -4
  73. package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
  74. package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
  75. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  76. package/src/Chat/ChatComposerInput.test.tsx +7 -55
  77. package/src/Chat/ChatComposerInput.tsx +0 -36
  78. package/src/Chat/useTriggerMenu.tsx +1 -4
  79. package/src/CommandPalette/CommandPalette.spec.md +2 -0
  80. package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
  81. package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
  82. package/src/CommandPalette/CommandPaletteList.tsx +3 -2
  83. package/src/DateInput/TouchDateField.tsx +3 -1
  84. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
  85. package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
  86. package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
  87. package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
  88. package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
  89. package/src/DropdownMenu/DropdownMenu.tsx +278 -49
  90. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
  91. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
  92. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
  93. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
  94. package/src/DropdownMenu/menuRegionsContext.ts +17 -0
  95. package/src/Layout/container.stylex.ts +11 -21
  96. package/src/Lightbox/Lightbox.test.tsx +58 -0
  97. package/src/Lightbox/Lightbox.tsx +12 -6
  98. package/src/Markdown/plugins/protocol.ts +45 -12
  99. package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
  100. package/src/PowerSearch/PowerSearch.tsx +28 -1
  101. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
  102. package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
  103. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
  104. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
  105. package/src/TimeInput/TouchTimeField.tsx +3 -1
  106. package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
  107. package/src/Tokenizer/Tokenizer.test.tsx +12 -63
  108. package/src/Tokenizer/Tokenizer.tsx +1 -25
  109. package/src/Toolbar/Toolbar.test.tsx +29 -0
  110. package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
  111. package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
  112. package/src/Typeahead/BaseTypeahead.tsx +1 -21
  113. package/src/Typeahead/Typeahead.doc.mjs +0 -7
  114. package/src/Typeahead/Typeahead.test.tsx +24 -42
  115. package/src/Typeahead/Typeahead.tsx +1 -25
  116. package/src/hooks/useFocusTrap.ts +5 -1
  117. package/src/hooks/useRenamedProp.ts +6 -6
  118. package/src/reset.a11y.chromium.spec.ts +178 -0
  119. package/src/reset.css +8 -8
  120. package/src/reset.test.ts +112 -0
  121. package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
  122. package/src/theme/tokens.stylex.ts +18 -15
  123. package/src/theme/tokens.test.ts +1 -1
  124. package/src/theme/useTheme.test.tsx +4 -4
  125. package/src/theme/useTheme.ts +1 -1
@@ -1762,9 +1762,9 @@ describe('Tokenizer end-lane reserve', () => {
1762
1762
  });
1763
1763
 
1764
1764
  // `emptySearchResultsText` was renamed to `emptySearchText` and widened from
1765
- // `string` to `ReactNode` (`spec:AST-056` FR1). It is a released prop with a
1766
- // victim, so the replacement ships first and the old name keeps working
1767
- // through the overlap (`spec:AST-017` FR28, FR29).
1765
+ // `string` to `ReactNode` (`spec:AST-056` FR1). The overlap ended in a minor
1766
+ // release (`CLN-0001`–`CLN-0004`, `spec:AST-017` FR31): the old name is no
1767
+ // longer part of the type and is no longer read.
1768
1768
  describe('Tokenizer emptySearchText', () => {
1769
1769
  async function searchForNothing() {
1770
1770
  const input = screen.getByRole('combobox');
@@ -1847,33 +1847,6 @@ describe('Tokenizer emptySearchText', () => {
1847
1847
  ).not.toContain('→');
1848
1848
  });
1849
1849
 
1850
- it('treats an explicit null as not given, falling through to the old name', async () => {
1851
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1852
- render(
1853
- <Tokenizer
1854
- label="People"
1855
- searchSource={userSource}
1856
- value={[]}
1857
- onChange={() => {}}
1858
- debounceMs={0}
1859
- emptySearchResultsText="Nobody found"
1860
- emptySearchText={null}
1861
- />,
1862
- );
1863
- await searchForNothing();
1864
-
1865
- // `null` is what this prop's own `??` default already treats as absent,
1866
- // so the released name still supplies the message and the warning says
1867
- // only that it is deprecated.
1868
- await waitFor(() => {
1869
- expect(screen.getByText('Nobody found')).toBeInTheDocument();
1870
- });
1871
- expect(warn).not.toHaveBeenCalledWith(
1872
- expect.stringContaining('are both set'),
1873
- );
1874
- warn.mockRestore();
1875
- });
1876
-
1877
1850
  it('falls through to the default when null is the only value given', async () => {
1878
1851
  render(
1879
1852
  <Tokenizer
@@ -1892,8 +1865,10 @@ describe('Tokenizer emptySearchText', () => {
1892
1865
  });
1893
1866
  });
1894
1867
 
1895
- it('keeps the deprecated name working, and says it is deprecated', async () => {
1868
+ it('no longer reads the removed emptySearchResultsText (CLN-0001)', async () => {
1896
1869
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1870
+ // The unknown prop reaches the root element, which React reports.
1871
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
1897
1872
  render(
1898
1873
  <Tokenizer
1899
1874
  label="People"
@@ -1901,47 +1876,21 @@ describe('Tokenizer emptySearchText', () => {
1901
1876
  value={[]}
1902
1877
  onChange={() => {}}
1903
1878
  debounceMs={0}
1879
+ // @ts-expect-error -- removed in the minor release (CLN-0001)
1904
1880
  emptySearchResultsText="Nobody found"
1905
1881
  />,
1906
1882
  );
1907
1883
  await searchForNothing();
1908
1884
 
1909
1885
  await waitFor(() => {
1910
- expect(screen.getByText('Nobody found')).toBeInTheDocument();
1911
- });
1912
- expect(warn).toHaveBeenCalledWith(
1913
- expect.stringContaining(
1914
- 'Tokenizer: `emptySearchResultsText` is deprecated; use `emptySearchText`',
1915
- ),
1916
- );
1917
- warn.mockRestore();
1918
- });
1919
-
1920
- it('lets the new name win when both are set, and warns', async () => {
1921
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1922
- render(
1923
- <Tokenizer
1924
- label="People"
1925
- searchSource={userSource}
1926
- value={[]}
1927
- onChange={() => {}}
1928
- debounceMs={0}
1929
- emptySearchResultsText="Old copy"
1930
- emptySearchText="New copy"
1931
- />,
1932
- );
1933
- await searchForNothing();
1934
-
1935
- await waitFor(() => {
1936
- expect(screen.getByText('New copy')).toBeInTheDocument();
1886
+ expect(screen.getByText('No results found')).toBeInTheDocument();
1937
1887
  });
1938
- expect(screen.queryByText('Old copy')).not.toBeInTheDocument();
1939
- expect(warn).toHaveBeenCalledWith(
1940
- expect.stringContaining(
1941
- 'Tokenizer: `emptySearchResultsText` and `emptySearchText` are both set',
1942
- ),
1888
+ expect(screen.queryByText('Nobody found')).not.toBeInTheDocument();
1889
+ expect(warn).not.toHaveBeenCalledWith(
1890
+ expect.stringContaining('is deprecated'),
1943
1891
  );
1944
1892
  warn.mockRestore();
1893
+ error.mockRestore();
1945
1894
  });
1946
1895
  });
1947
1896
 
@@ -68,7 +68,6 @@ import type {SearchableItem, SearchSource} from '../Typeahead/types';
68
68
  import {mergeProps} from '../utils';
69
69
  import {themeProps} from '../utils/themeProps';
70
70
  import {useTranslator} from '../i18n';
71
- import {useRenamedProp} from '../hooks/useRenamedProp';
72
71
 
73
72
  // Re-export status types for convenience
74
73
  export type {
@@ -206,18 +205,6 @@ export interface TokenizerProps<T extends SearchableItem> extends Omit<
206
205
  * @default 'No results found'
207
206
  */
208
207
  emptySearchText?: ReactNode;
209
-
210
- /**
211
- * Text shown when no results found.
212
- * @default 'No results found'
213
- * @deprecated `DEP-0001`. Renamed to `emptySearchText`, which takes a
214
- * `ReactNode` rather than a `string` — every existing value stays valid
215
- * (`spec:AST-056` FR1, FR7). Still works exactly as released;
216
- * `emptySearchText` wins when both are set. Removal is `CLN-0001`, in a
217
- * later minor whose frozen manifest carries both ids (`spec:AST-017`
218
- * FR31).
219
- */
220
- emptySearchResultsText?: string;
221
208
  /** Whether the input is disabled. @default false */
222
209
  isDisabled?: boolean;
223
210
  /**
@@ -577,8 +564,7 @@ export function Tokenizer<T extends SearchableItem>({
577
564
  maxMenuItems,
578
565
  menuWidth,
579
566
  minQueryLength,
580
- emptySearchResultsText,
581
- emptySearchText: emptySearchTextFromProps,
567
+ emptySearchText,
582
568
  isDisabled = false,
583
569
  htmlName,
584
570
  disabledMessage,
@@ -633,16 +619,6 @@ export function Tokenizer<T extends SearchableItem>({
633
619
  isEnabled: showsDisabledMessage,
634
620
  });
635
621
 
636
- // The replacement wins, the released name keeps working, and development
637
- // says which one was read (`spec:AST-056` FR7, `spec:AST-017` FR28).
638
- const emptySearchText = useRenamedProp<ReactNode>({
639
- component: 'Tokenizer',
640
- deprecated: 'emptySearchResultsText',
641
- deprecatedValue: emptySearchResultsText,
642
- replacement: 'emptySearchText',
643
- value: emptySearchTextFromProps,
644
- });
645
-
646
622
  useImperativeHandle(handleRef, () => ({
647
623
  focus() {
648
624
  inputRef.current?.focus();
@@ -12,7 +12,12 @@
12
12
  import {describe, it, expect, vi} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
+ import * as stylex from '@stylexjs/stylex';
15
16
  import {Toolbar} from './Toolbar';
17
+ import {
18
+ paddingBlockStyles,
19
+ paddingInlineStyles,
20
+ } from '../Layout/padding.stylex';
16
21
 
17
22
  describe('Toolbar', () => {
18
23
  it('renders with toolbar role', () => {
@@ -148,6 +153,30 @@ describe('Toolbar', () => {
148
153
  expect(toolbar).toHaveAttribute('data-size', 'sm');
149
154
  });
150
155
 
156
+ it.each([
157
+ ['sm', 2],
158
+ ['md', 2],
159
+ ] as const)(
160
+ 'uses the %s block inset without overriding container inline padding',
161
+ (size, blockPadding) => {
162
+ const {container} = render(<Toolbar label="Actions" size={size} />);
163
+ const sectionInner = container.querySelector('.astryx-section');
164
+ const expectedBlockClasses = stylex
165
+ .props(paddingBlockStyles[blockPadding])
166
+ .className?.split(' ')
167
+ .filter(Boolean);
168
+ const compactInlineClasses = stylex
169
+ .props(paddingInlineStyles[1])
170
+ .className?.split(' ')
171
+ .filter(Boolean);
172
+
173
+ expect(sectionInner).toBeInTheDocument();
174
+ expect(expectedBlockClasses).not.toHaveLength(0);
175
+ expect(sectionInner).toHaveClass(...(expectedBlockClasses ?? []));
176
+ expect(sectionInner).not.toHaveClass(...(compactInlineClasses ?? []));
177
+ },
178
+ );
179
+
151
180
  it('defaults to md size', () => {
152
181
  render(<Toolbar label="Actions" />);
153
182
  const toolbar = screen.getByRole('toolbar');
@@ -165,13 +165,6 @@ export const docs = {
165
165
  'Content shown when the query matched nothing. Takes a ReactNode, so the dead end can carry a link or a create row. Announced in a polite live region as the text it renders, read from the DOM; aria-hidden parts stay out of both, and content that renders no text announces nothing. null counts as not given, like undefined, and falls through to the default; pass an empty string to render nothing.',
166
166
  default: "'No results found'",
167
167
  },
168
- {
169
- name: 'emptySearchResultsText',
170
- type: 'string',
171
- description:
172
- 'Deprecated: renamed to emptySearchText, which takes a ReactNode rather than a string, so every existing value stays valid. Still works exactly as released; emptySearchText wins when both are set.',
173
- default: "'No results found'",
174
- },
175
168
  {
176
169
  name: 'isDisabled',
177
170
  type: 'boolean',
@@ -331,13 +324,6 @@ export const docsZh = {
331
324
  '查询无匹配结果时显示的内容。接受 ReactNode,因此可在无结果处放置链接或创建入口。会从 DOM 读取其渲染出的文本在礼貌性实时区域中播报;aria-hidden 的部分两处都不包含,不渲染任何文本的内容则不会播报。null 与 undefined 同样视为未提供,将回退到默认值;若要不渲染任何内容,请传入空字符串。',
332
325
  default: "'No results found'",
333
326
  },
334
- {
335
- name: 'emptySearchResultsText',
336
- type: 'string',
337
- description:
338
- '已弃用:改名为 emptySearchText,其类型由 string 放宽为 ReactNode,原有取值全部仍然有效。仍按已发布行为工作;两者同时设置时以 emptySearchText 为准。',
339
- default: "'No results found'",
340
- },
341
327
  {
342
328
  name: 'isDisabled',
343
329
  type: 'boolean',
@@ -479,8 +465,6 @@ export const docsDense = {
479
465
  minQueryLength:
480
466
  'Min query length before searching. Menu stays closed below it.',
481
467
  emptySearchText: 'Content when the query matched nothing. ReactNode.',
482
- emptySearchResultsText:
483
- 'deprecated, use emptySearchText (ReactNode). still works as released; emptySearchText wins if both set.',
484
468
  isDisabled: 'Whether input disabled.',
485
469
  isFocusableDisabled:
486
470
  'Keeps disabled input focusable and blocks text entry; an already-open highlight can still be selected with Enter.',
@@ -238,7 +238,7 @@ describe('BaseTypeahead', () => {
238
238
  value={null}
239
239
  onChange={() => {}}
240
240
  debounceMs={0}
241
- emptySearchResultsText="No matching frameworks"
241
+ emptySearchText="No matching frameworks"
242
242
  />,
243
243
  );
244
244
 
@@ -33,7 +33,6 @@ import type {StyleXStyles} from '@stylexjs/stylex';
33
33
  import {usePopover} from '../Popover/usePopover';
34
34
  import {useAnnounce} from '../hooks/useAnnounce';
35
35
  import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
36
- import {useRenamedProp} from '../hooks/useRenamedProp';
37
36
  import {useHighlightedOptionScroll} from '../hooks/useHighlightedOptionScroll';
38
37
  import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
39
38
  import {isImeKeyEvent} from '../utils/ime';
@@ -143,18 +142,6 @@ export interface BaseTypeaheadProps<T extends SearchableItem> extends Omit<
143
142
  */
144
143
  emptySearchText?: ReactNode;
145
144
 
146
- /**
147
- * Text shown when no results found.
148
- * @default 'No results found'
149
- * @deprecated `DEP-0003`. Renamed to `emptySearchText`, which takes a
150
- * `ReactNode` rather than a `string` — every existing value stays valid
151
- * (`spec:AST-056` FR1, FR7). Still works exactly as released;
152
- * `emptySearchText` wins when both are set. Removal is `CLN-0003`, in a
153
- * later minor whose frozen manifest carries both ids (`spec:AST-017`
154
- * FR31).
155
- */
156
- emptySearchResultsText?: string;
157
-
158
145
  /**
159
146
  * Whether the input is disabled.
160
147
  * @default false
@@ -465,7 +452,6 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
465
452
  maxMenuItems = 10,
466
453
  menuWidth,
467
454
  minQueryLength = 1,
468
- emptySearchResultsText: deprecatedEmptySearchResultsText,
469
455
  emptySearchText: emptySearchTextFromProps,
470
456
  isDisabled = false,
471
457
  isFocusableDisabled = false,
@@ -500,13 +486,7 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
500
486
  const placeholder =
501
487
  placeholderFromProps ?? t('@astryx.typeahead.searchPlaceholder');
502
488
  const emptySearchText =
503
- useRenamedProp<ReactNode>({
504
- component: 'BaseTypeahead',
505
- deprecated: 'emptySearchResultsText',
506
- deprecatedValue: deprecatedEmptySearchResultsText,
507
- replacement: 'emptySearchText',
508
- value: emptySearchTextFromProps,
509
- }) ?? t('@astryx.typeahead.emptySearchResults');
489
+ emptySearchTextFromProps ?? t('@astryx.typeahead.emptySearchResults');
510
490
  // The empty-state row carries the message visually, and the live region has
511
491
  // to speak the same words. `emptySearchText` takes a ReactNode, so they are
512
492
  // read off the rendered row after it renders rather than guessed from the
@@ -230,13 +230,6 @@ export const docs = {
230
230
  'Content shown when the query matched nothing. Takes a ReactNode, so the dead end can carry a link or a create row. Announced in a polite live region as the text it renders, read from the DOM; aria-hidden parts stay out of both, and content that renders no text announces nothing. null counts as not given, like undefined, and falls through to the default; pass an empty string to render nothing.',
231
231
  default: "'No results found'",
232
232
  },
233
- {
234
- name: 'emptySearchResultsText',
235
- type: 'string',
236
- description:
237
- 'Deprecated: renamed to emptySearchText, which takes a ReactNode rather than a string, so every existing value stays valid. Still works exactly as released; emptySearchText wins when both are set.',
238
- default: "'No results found'",
239
- },
240
233
  {
241
234
  name: 'hasAutoFocus',
242
235
  type: 'boolean',
@@ -239,7 +239,7 @@ describe('BaseTypeahead', () => {
239
239
  value={null}
240
240
  onChange={() => {}}
241
241
  debounceMs={0}
242
- emptySearchResultsText="No results found"
242
+ emptySearchText="No results found"
243
243
  />,
244
244
  );
245
245
  const input = screen.getByRole('combobox');
@@ -260,7 +260,7 @@ describe('BaseTypeahead', () => {
260
260
  value={null}
261
261
  onChange={() => {}}
262
262
  debounceMs={0}
263
- emptySearchResultsText="No results found"
263
+ emptySearchText="No results found"
264
264
  />,
265
265
  );
266
266
  const input = screen.getByRole('combobox');
@@ -2189,9 +2189,9 @@ describe('the value is bounded by the content lane', () => {
2189
2189
  });
2190
2190
 
2191
2191
  // `emptySearchResultsText` was renamed to `emptySearchText` and widened from
2192
- // `string` to `ReactNode` (`spec:AST-056` FR1). It is a released prop with a
2193
- // victim, so the replacement ships first and the old name keeps working
2194
- // through the overlap (`spec:AST-017` FR28, FR29).
2192
+ // `string` to `ReactNode` (`spec:AST-056` FR1). The overlap ended in a minor
2193
+ // release (`CLN-0001`–`CLN-0004`, `spec:AST-017` FR31): the old name is no
2194
+ // longer part of the type and is no longer read.
2195
2195
  describe('emptySearchText rename', () => {
2196
2196
  function searchForNothing() {
2197
2197
  fireEvent.change(screen.getByRole('combobox'), {
@@ -2222,8 +2222,10 @@ describe('emptySearchText rename', () => {
2222
2222
  });
2223
2223
  });
2224
2224
 
2225
- it('keeps the deprecated name working, and says it is deprecated', async () => {
2225
+ it('no longer reads the removed emptySearchResultsText (CLN-0002)', async () => {
2226
2226
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
2227
+ // The unknown prop reaches the root element, which React reports.
2228
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
2227
2229
  render(
2228
2230
  <Typeahead
2229
2231
  label="Fruit"
@@ -2231,47 +2233,21 @@ describe('emptySearchText rename', () => {
2231
2233
  value={null}
2232
2234
  onChange={() => {}}
2233
2235
  debounceMs={0}
2236
+ // @ts-expect-error -- removed in the minor release (CLN-0002)
2234
2237
  emptySearchResultsText="Nothing ripe"
2235
2238
  />,
2236
2239
  );
2237
2240
  searchForNothing();
2238
2241
 
2239
2242
  await waitFor(() => {
2240
- expect(screen.getByText('Nothing ripe')).toBeInTheDocument();
2243
+ expect(screen.getByText('No results found')).toBeInTheDocument();
2241
2244
  });
2242
- expect(warn).toHaveBeenCalledWith(
2243
- expect.stringContaining(
2244
- 'Typeahead: `emptySearchResultsText` is deprecated; use `emptySearchText`',
2245
- ),
2246
- );
2247
- warn.mockRestore();
2248
- });
2249
-
2250
- it('lets the new name win when both are set, and warns', async () => {
2251
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
2252
- render(
2253
- <Typeahead
2254
- label="Fruit"
2255
- searchSource={fruitSource}
2256
- value={null}
2257
- onChange={() => {}}
2258
- debounceMs={0}
2259
- emptySearchResultsText="Old copy"
2260
- emptySearchText="New copy"
2261
- />,
2262
- );
2263
- searchForNothing();
2264
-
2265
- await waitFor(() => {
2266
- expect(screen.getByText('New copy')).toBeInTheDocument();
2267
- });
2268
- expect(screen.queryByText('Old copy')).not.toBeInTheDocument();
2269
- expect(warn).toHaveBeenCalledWith(
2270
- expect.stringContaining(
2271
- 'Typeahead: `emptySearchResultsText` and `emptySearchText` are both set',
2272
- ),
2245
+ expect(screen.queryByText('Nothing ripe')).not.toBeInTheDocument();
2246
+ expect(warn).not.toHaveBeenCalledWith(
2247
+ expect.stringContaining('is deprecated'),
2273
2248
  );
2274
2249
  warn.mockRestore();
2250
+ error.mockRestore();
2275
2251
  });
2276
2252
  });
2277
2253
 
@@ -2301,14 +2277,16 @@ describe('emptySearchText rename', () => {
2301
2277
  });
2302
2278
  });
2303
2279
 
2304
- it('announces the deprecated name unchanged', async () => {
2305
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
2280
+ it('no longer announces the removed emptySearchResultsText (CLN-0003)', async () => {
2281
+ // The unknown prop reaches the root element, which React reports.
2282
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
2306
2283
  render(
2307
2284
  <BaseTypeahead
2308
2285
  searchSource={fruitSource}
2309
2286
  value={null}
2310
2287
  onChange={() => {}}
2311
2288
  debounceMs={0}
2289
+ // @ts-expect-error -- removed in the minor release (CLN-0003)
2312
2290
  emptySearchResultsText="Nothing ripe"
2313
2291
  />,
2314
2292
  );
@@ -2317,9 +2295,13 @@ describe('emptySearchText rename', () => {
2317
2295
  await waitFor(() => {
2318
2296
  expect(
2319
2297
  document.querySelector('[data-astryx-live-region="polite"]'),
2320
- ).toHaveTextContent('Nothing ripe');
2298
+ ).toHaveTextContent('No results found');
2321
2299
  });
2322
- warn.mockRestore();
2300
+ expect(
2301
+ document.querySelector('[data-astryx-live-region="polite"]')
2302
+ ?.textContent,
2303
+ ).not.toContain('Nothing ripe');
2304
+ error.mockRestore();
2323
2305
  });
2324
2306
  });
2325
2307
  });
@@ -70,7 +70,6 @@ import {themeProps} from '../utils/themeProps';
70
70
  import {useTranslator} from '../i18n';
71
71
 
72
72
  import {useMergedRefs} from '../hooks/useMergedRefs';
73
- import {useRenamedProp} from '../hooks/useRenamedProp';
74
73
  export type {
75
74
  InputStatus as TypeaheadStatus,
76
75
  InputStatusType as TypeaheadStatusType,
@@ -160,18 +159,6 @@ export interface TypeaheadProps<T extends SearchableItem> extends Omit<
160
159
  * @default 'No results found'
161
160
  */
162
161
  emptySearchText?: ReactNode;
163
-
164
- /**
165
- * Text shown when no results found.
166
- * @default 'No results found'
167
- * @deprecated `DEP-0002`. Renamed to `emptySearchText`, which takes a
168
- * `ReactNode` rather than a `string` — every existing value stays valid
169
- * (`spec:AST-056` FR1, FR7). Still works exactly as released;
170
- * `emptySearchText` wins when both are set. Removal is `CLN-0002`, in a
171
- * later minor whose frozen manifest carries both ids (`spec:AST-017`
172
- * FR31).
173
- */
174
- emptySearchResultsText?: string;
175
162
  /** Whether the input is disabled. @default false */
176
163
  isDisabled?: boolean;
177
164
  /**
@@ -399,8 +386,7 @@ export function Typeahead<T extends SearchableItem>({
399
386
  hasEntriesOnFocus,
400
387
  maxMenuItems,
401
388
  minQueryLength,
402
- emptySearchResultsText,
403
- emptySearchText: emptySearchTextFromProps,
389
+ emptySearchText,
404
390
  isDisabled = false,
405
391
  disabledMessage,
406
392
  hasClear = true,
@@ -435,16 +421,6 @@ export function Typeahead<T extends SearchableItem>({
435
421
  const statusMessageId = useId();
436
422
  const inputGroup = useInputGroup();
437
423
 
438
- // The replacement wins, the released name keeps working, and development
439
- // says which one was read (`spec:AST-056` FR7, `spec:AST-017` FR28).
440
- const emptySearchText = useRenamedProp<ReactNode>({
441
- component: 'Typeahead',
442
- deprecated: 'emptySearchResultsText',
443
- deprecatedValue: emptySearchResultsText,
444
- replacement: 'emptySearchText',
445
- value: emptySearchTextFromProps,
446
- });
447
-
448
424
  const wrapperRef = useRef<HTMLDivElement>(null);
449
425
  const inputRef = useRef<HTMLInputElement>(null);
450
426
  const tokenRef = useRef<HTMLElement>(null);
@@ -91,8 +91,12 @@ function isVisiblyFocusable(el: HTMLElement): boolean {
91
91
 
92
92
  /**
93
93
  * Get all focusable elements within a container.
94
+ *
95
+ * Exported for menus that place their own Tab stops inside a trapped layer
96
+ * (DropdownMenu's header and footer), so they judge reachability exactly as
97
+ * the trap does. Not re-exported from the package's `hooks` entry.
94
98
  */
95
- function getFocusableElements(container: HTMLElement): HTMLElement[] {
99
+ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
96
100
  return Array.from(
97
101
  container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
98
102
  ).filter(isVisiblyFocusable);
@@ -56,12 +56,12 @@ export interface RenamedPropOptions<T> {
56
56
  *
57
57
  * @example
58
58
  * ```
59
- * const emptySearchText = useRenamedProp({
60
- * component: 'Tokenizer',
61
- * deprecated: 'emptySearchResultsText',
62
- * deprecatedValue: emptySearchResultsText,
63
- * replacement: 'emptySearchText',
64
- * value: emptySearchTextFromProps,
59
+ * const label = useRenamedProp({
60
+ * component: 'Example',
61
+ * deprecated: 'oldLabel',
62
+ * deprecatedValue: oldLabel,
63
+ * replacement: 'label',
64
+ * value: labelFromProps,
65
65
  * });
66
66
  * ```
67
67
  */