@astryxdesign/core 0.4.0 → 0.4.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 (194) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +1 -2
  4. package/dist/AspectRatio/index.d.ts.map +1 -1
  5. package/dist/AspectRatio/index.js +1 -2
  6. package/dist/Badge/Badge.d.ts.map +1 -1
  7. package/dist/Badge/Badge.js +1 -2
  8. package/dist/Badge/index.d.ts.map +1 -1
  9. package/dist/Badge/index.js +1 -2
  10. package/dist/Banner/Banner.d.ts +3 -0
  11. package/dist/Banner/Banner.d.ts.map +1 -1
  12. package/dist/Banner/Banner.js +17 -3
  13. package/dist/Blockquote/Blockquote.d.ts.map +1 -1
  14. package/dist/Blockquote/Blockquote.js +1 -2
  15. package/dist/Blockquote/index.d.ts.map +1 -1
  16. package/dist/Blockquote/index.js +1 -2
  17. package/dist/Button/Button.d.ts.map +1 -1
  18. package/dist/Button/Button.js +2 -2
  19. package/dist/Card/Card.d.ts.map +1 -1
  20. package/dist/Card/Card.js +1 -2
  21. package/dist/Card/index.d.ts.map +1 -1
  22. package/dist/Card/index.js +1 -2
  23. package/dist/Center/Center.d.ts.map +1 -1
  24. package/dist/Center/Center.js +1 -2
  25. package/dist/Center/index.d.ts.map +1 -1
  26. package/dist/Center/index.js +1 -2
  27. package/dist/Code/Code.d.ts.map +1 -1
  28. package/dist/Code/Code.js +1 -2
  29. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPalette.js +5 -0
  31. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  32. package/dist/ComplexSelector/ComplexSelector.js +7 -8
  33. package/dist/DateInput/DateInput.d.ts.map +1 -1
  34. package/dist/DateInput/DateInput.js +23 -10
  35. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  36. package/dist/DateRangeInput/DateRangeInput.js +44 -16
  37. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  38. package/dist/DateTimeInput/DateTimeInput.js +32 -13
  39. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
  40. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
  42. package/dist/Field/PanelSearchInput.d.ts +81 -0
  43. package/dist/Field/PanelSearchInput.d.ts.map +1 -0
  44. package/dist/Field/PanelSearchInput.js +151 -0
  45. package/dist/FileInput/FileInput.d.ts.map +1 -1
  46. package/dist/FileInput/FileInput.js +21 -10
  47. package/dist/Grid/Grid.d.ts.map +1 -1
  48. package/dist/Grid/Grid.js +1 -2
  49. package/dist/Grid/index.d.ts.map +1 -1
  50. package/dist/Grid/index.js +1 -2
  51. package/dist/Icon/globalIconRegistry.d.ts +5 -4
  52. package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
  53. package/dist/Icon/globalIconRegistry.js +5 -4
  54. package/dist/Indicator/index.d.ts +1 -1
  55. package/dist/Indicator/index.d.ts.map +1 -1
  56. package/dist/Indicator/types.d.ts +10 -0
  57. package/dist/Indicator/types.d.ts.map +1 -1
  58. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  59. package/dist/Lightbox/Lightbox.js +24 -50
  60. package/dist/MultiSelector/MultiSelector.d.ts +8 -1
  61. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  62. package/dist/MultiSelector/MultiSelector.js +134 -102
  63. package/dist/Popover/usePopover.d.ts +15 -0
  64. package/dist/Popover/usePopover.d.ts.map +1 -1
  65. package/dist/Popover/usePopover.js +14 -2
  66. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  67. package/dist/ProgressBar/ProgressBar.js +2 -3
  68. package/dist/Section/Section.d.ts.map +1 -1
  69. package/dist/Section/Section.js +1 -2
  70. package/dist/Section/index.d.ts.map +1 -1
  71. package/dist/Section/index.js +1 -2
  72. package/dist/Selector/Selector.d.ts +10 -0
  73. package/dist/Selector/Selector.d.ts.map +1 -1
  74. package/dist/Selector/Selector.js +135 -95
  75. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  76. package/dist/Skeleton/Skeleton.js +1 -2
  77. package/dist/Skeleton/index.d.ts.map +1 -1
  78. package/dist/Skeleton/index.js +1 -2
  79. package/dist/Slider/Slider.d.ts.map +1 -1
  80. package/dist/Slider/Slider.js +2 -6
  81. package/dist/Switch/Switch.d.ts.map +1 -1
  82. package/dist/Switch/Switch.js +3 -3
  83. package/dist/Table/TableRow.d.ts +1 -1
  84. package/dist/Table/TableRow.d.ts.map +1 -1
  85. package/dist/Table/TableRow.js +4 -2
  86. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  87. package/dist/TimeInput/TimeInput.js +7 -2
  88. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  89. package/dist/Timestamp/Timestamp.js +14 -3
  90. package/dist/Timestamp/formatInstant.d.ts +11 -1
  91. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  92. package/dist/Timestamp/formatInstant.js +3 -1
  93. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
  94. package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
  95. package/dist/VisuallyHidden/index.d.ts.map +1 -1
  96. package/dist/VisuallyHidden/index.js +1 -2
  97. package/dist/astryx.css +18 -17
  98. package/dist/astryx.umd.js +45 -45
  99. package/dist/astryx.umd.js.map +4 -4
  100. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  101. package/dist/hooks/useInputStatusIcon.js +17 -4
  102. package/dist/theme/defineTheme.d.ts +2 -2
  103. package/dist/theme/defineTheme.d.ts.map +1 -1
  104. package/dist/theme/defineTheme.js +2 -1
  105. package/dist/theme/index.d.ts +1 -1
  106. package/dist/theme/index.d.ts.map +1 -1
  107. package/dist/theme/index.js +1 -1
  108. package/dist/theme/tokens.stylex.d.ts +12 -0
  109. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  110. package/dist/theme/tokens.stylex.js +23 -0
  111. package/dist/utils/focusOutline.stylex.d.ts +34 -19
  112. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  113. package/dist/utils/focusOutline.stylex.js +46 -33
  114. package/dist/utils/index.d.ts +1 -0
  115. package/dist/utils/index.d.ts.map +1 -1
  116. package/dist/utils/index.js +5 -1
  117. package/dist/utils/interactionModality.d.ts +34 -0
  118. package/dist/utils/interactionModality.d.ts.map +1 -0
  119. package/dist/utils/interactionModality.js +70 -0
  120. package/locales/en.json +40 -0
  121. package/locales/pseudo.json +30 -0
  122. package/package.json +2 -2
  123. package/src/AppShell/AppShell.doc.mjs +3 -0
  124. package/src/AspectRatio/AspectRatio.tsx +0 -2
  125. package/src/AspectRatio/index.ts +0 -2
  126. package/src/Badge/Badge.tsx +0 -2
  127. package/src/Badge/index.ts +0 -2
  128. package/src/Banner/Banner.test.tsx +58 -1
  129. package/src/Banner/Banner.tsx +24 -5
  130. package/src/Blockquote/Blockquote.tsx +0 -2
  131. package/src/Blockquote/index.ts +0 -2
  132. package/src/Button/Button.doc.mjs +2 -2
  133. package/src/Button/Button.tsx +5 -4
  134. package/src/Card/Card.tsx +0 -2
  135. package/src/Card/index.ts +0 -2
  136. package/src/Center/Center.tsx +0 -2
  137. package/src/Center/index.ts +0 -2
  138. package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
  139. package/src/Code/Code.tsx +0 -2
  140. package/src/CommandPalette/CommandPalette.test.tsx +54 -0
  141. package/src/CommandPalette/CommandPalette.tsx +5 -0
  142. package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
  143. package/src/ComplexSelector/ComplexSelector.tsx +6 -10
  144. package/src/DateInput/DateInput.test.tsx +19 -0
  145. package/src/DateInput/DateInput.tsx +3 -6
  146. package/src/DateRangeInput/DateRangeInput.tsx +3 -9
  147. package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
  148. package/src/DateTimeInput/DateTimeInput.tsx +23 -9
  149. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
  150. package/src/Field/PanelSearchInput.tsx +271 -0
  151. package/src/FileInput/FileInput.tsx +21 -8
  152. package/src/Grid/Grid.tsx +0 -2
  153. package/src/Grid/index.ts +0 -2
  154. package/src/Icon/globalIconRegistry.tsx +5 -4
  155. package/src/Indicator/Indicator.doc.mjs +1 -1
  156. package/src/Indicator/index.ts +1 -0
  157. package/src/Indicator/types.ts +11 -0
  158. package/src/Lightbox/Lightbox.tsx +2 -11
  159. package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
  160. package/src/MultiSelector/MultiSelector.test.tsx +135 -5
  161. package/src/MultiSelector/MultiSelector.tsx +170 -127
  162. package/src/Popover/Popover.doc.mjs +2 -0
  163. package/src/Popover/usePopover.tsx +37 -4
  164. package/src/ProgressBar/ProgressBar.tsx +2 -8
  165. package/src/Section/Section.tsx +0 -2
  166. package/src/Section/index.ts +0 -2
  167. package/src/Selector/Selector.doc.mjs +10 -0
  168. package/src/Selector/Selector.test.tsx +386 -2
  169. package/src/Selector/Selector.tsx +182 -122
  170. package/src/Skeleton/Skeleton.tsx +0 -2
  171. package/src/Skeleton/index.ts +0 -2
  172. package/src/Slider/Slider.tsx +2 -11
  173. package/src/Switch/Switch.tsx +10 -2
  174. package/src/Table/Table.test.tsx +31 -0
  175. package/src/Table/TableRow.tsx +6 -0
  176. package/src/TimeInput/TimeInput.test.tsx +54 -0
  177. package/src/TimeInput/TimeInput.tsx +17 -2
  178. package/src/Timestamp/Timestamp.test.tsx +91 -14
  179. package/src/Timestamp/Timestamp.tsx +14 -2
  180. package/src/Timestamp/formatInstant.ts +16 -1
  181. package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
  182. package/src/TreeList/TreeList.test.tsx +3 -2
  183. package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
  184. package/src/VisuallyHidden/index.ts +0 -2
  185. package/src/hooks/useInputStatusIcon.tsx +8 -8
  186. package/src/serverSafeComponents.test.ts +665 -0
  187. package/src/theme/defineTheme.ts +3 -0
  188. package/src/theme/generateThemeRules.test.ts +17 -0
  189. package/src/theme/index.ts +2 -0
  190. package/src/theme/tokens.stylex.ts +18 -0
  191. package/src/theme/tokens.test.ts +37 -0
  192. package/src/utils/focusOutline.stylex.ts +37 -23
  193. package/src/utils/index.ts +4 -0
  194. package/src/utils/interactionModality.ts +74 -0
@@ -110,12 +110,12 @@ function getThemeIconOverrides(
110
110
  * ```
111
111
  *
112
112
  * Libraries may also register their own extension keys (any string), so a
113
- * theme can override them the same way it overrides built-in icons:
113
+ * theme can override them the same way it overrides built-in icons. A library
114
+ * that ships its own icons registers them by key, then resolves with
115
+ * `getIcon('richtext:bold')`.
114
116
  * @example
115
117
  * ```
116
- * // In a library that ships its own icons:
117
118
  * registerIcons({ 'richtext:bold': <MyBoldIcon /> });
118
- * // resolve with getIcon('richtext:bold')
119
119
  * ```
120
120
  */
121
121
  export function registerIcons(
@@ -191,9 +191,10 @@ export function getIcon(
191
191
  * override the key via {@link registerIcons} without the library having to
192
192
  * widen the core {@link IconName} union.
193
193
  *
194
+ * The `fallback` is the library default, overridable by a theme registering the
195
+ * same key (for example `'richtext:bold'`).
194
196
  * @example
195
197
  * ```
196
- * // Library default, overridable by a theme registering 'richtext:bold':
197
198
  * getExtendedIcon('richtext:bold', <BoldGlyph />)
198
199
  * ```
199
200
  */
@@ -52,7 +52,7 @@ export const docs = {
52
52
  name: 'children',
53
53
  type: 'ReactNode',
54
54
  description:
55
- 'Rendered inside the chrome INSTEAD of the state mark. CheckboxInput passes its loading Spinner through this while a change action is pending, so a replacement indicator must render children when they will actually draw something `isRenderable(children)`, never `children ?? mark`, because a host writes `children={isBusy && <Spinner/>}` and `false` slips straight past a nullish check and deletes the mark.',
55
+ 'Rendered inside the chrome INSTEAD of the state mark. CheckboxInput passes its loading Spinner through this while a change action is pending, so a replacement indicator must render children when they will actually draw something: use `isRenderable(children)`, never `children ?? mark`, because a host writes `children={isBusy && <Spinner/>}` and `false` slips straight past a nullish check and deletes the mark.',
56
56
  },
57
57
  ],
58
58
  },
@@ -38,6 +38,7 @@ export type {
38
38
  IndicatorMap,
39
39
  IndicatorName,
40
40
  IndicatorNameOfFamily,
41
+ IndicatorPosition,
41
42
  IndicatorProps,
42
43
  IndicatorRegistry,
43
44
  IndicatorSize,
@@ -67,6 +67,17 @@ export type IndicatorState<F extends IndicatorFamily = IndicatorFamily> =
67
67
  /** Indicator size scale — matches the control sizes of the owning inputs. */
68
68
  export type IndicatorSize = 'sm' | 'md';
69
69
 
70
+ /**
71
+ * Which edge of its row an indicator sits on.
72
+ *
73
+ * Logical, not physical: `start` is the left edge in LTR and the right edge in
74
+ * RTL. Owned by the host component, not by the indicator — an indicator draws a
75
+ * picture and has no say in where the row puts it — which is why this is a prop
76
+ * on the components that lay out rows rather than part of
77
+ * {@link IndicatorProps}.
78
+ */
79
+ export type IndicatorPosition = 'start' | 'end';
80
+
70
81
  /**
71
82
  * Props every indicator accepts.
72
83
  *
@@ -35,6 +35,7 @@ import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
35
35
  import {mergeProps, mergeRefs, rtlStyles} from '../utils';
36
36
  import type {BaseProps} from '../BaseProps';
37
37
  import {themeProps} from '../utils/themeProps';
38
+ import {focusOutlineStyles} from '../utils/focusOutline.stylex';
38
39
  import {useTranslator} from '../i18n';
39
40
 
40
41
  /**
@@ -158,16 +159,6 @@ const styles = stylex.create({
158
159
  '@media (hover: hover)': 'zoom-in',
159
160
  },
160
161
  },
161
- zoomTarget: {
162
- outline: {
163
- default: 'none',
164
- ':focus-visible': `2px solid ${colorVars['--color-accent']}`,
165
- },
166
- outlineOffset: {
167
- default: '0',
168
- ':focus-visible': '2px',
169
- },
170
- },
171
162
  imageWrapperZoomed: {
172
163
  cursor: 'grab',
173
164
  },
@@ -641,7 +632,7 @@ export function Lightbox({
641
632
  aria-label={isZoomTarget ? t('@astryx.lightbox.zoom') : undefined}
642
633
  {...stylex.props(
643
634
  styles.imageWrapper,
644
- isZoomTarget && styles.zoomTarget,
635
+ isZoomTarget && focusOutlineStyles.focusVisible,
645
636
  !isVideo && hasZoom && !isZoomed && styles.imageWrapperZoomable,
646
637
  !isVideo && isZoomed && styles.imageWrapperZoomed,
647
638
  !isVideo && isDragging && styles.imageWrapperDragging,
@@ -27,6 +27,8 @@ export const docs = {
27
27
  },
28
28
  {className: 'astryx-multi-selector-clear-icon', deprecatedFor: 'input-clear-icon'},
29
29
  {className: 'astryx-multi-selector-empty-state'},
30
+ {className: 'astryx-multi-selector-search'},
31
+ {className: 'astryx-multi-selector-section-heading'},
30
32
  {
31
33
  className: 'astryx-multi-selector-indicator-icon',
32
34
  states: ['state'],
@@ -192,6 +194,13 @@ export const docs = {
192
194
  description:
193
195
  'Custom render function for each selectable option in the dropdown. Not called for dividers, sections, or the select-all row.',
194
196
  },
197
+ {
198
+ name: 'indicatorPosition',
199
+ type: "'start' | 'end'",
200
+ description:
201
+ 'Which edge of the option row carries the checkbox. end pushes it to the far edge of the row, including on the select-all row.',
202
+ default: "'start'",
203
+ },
195
204
  {
196
205
  name: 'width',
197
206
  type: 'SizeValue',
@@ -1774,7 +1774,134 @@ describe('MultiSelector indicator (chevron) icon theme target', () => {
1774
1774
  });
1775
1775
  });
1776
1776
 
1777
+ describe('MultiSelector list structure', () => {
1778
+ it('does not draw a divider under select-all', async () => {
1779
+ const user = userEvent.setup();
1780
+ render(
1781
+ <MultiSelector
1782
+ label="Fruit"
1783
+ options={['Apple', 'Banana', 'Orange']}
1784
+ value={[]}
1785
+ onChange={() => {}}
1786
+ hasSelectAll
1787
+ />,
1788
+ );
1789
+ await user.click(screen.getByRole('combobox'));
1790
+ // Select-all is the first row of the list, not a section of its own. No
1791
+ // option here declares a divider and there is no search row, so the panel
1792
+ // should contain no rule at all.
1793
+ expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
1794
+ const [first] = screen.getAllByRole('option', h);
1795
+ expect(first).toHaveTextContent('Select all');
1796
+ });
1797
+
1798
+ it('renders a section title as a plain heading inside the group, not a divider', async () => {
1799
+ const user = userEvent.setup();
1800
+ render(
1801
+ <MultiSelector
1802
+ label="Fruit"
1803
+ options={[
1804
+ {
1805
+ type: 'section',
1806
+ title: 'Citrus',
1807
+ options: [
1808
+ {value: 'orange', label: 'Orange'},
1809
+ {value: 'lemon', label: 'Lemon'},
1810
+ ],
1811
+ },
1812
+ ]}
1813
+ value={[]}
1814
+ onChange={() => {}}
1815
+ />,
1816
+ );
1817
+ await user.click(screen.getByRole('combobox'));
1818
+
1819
+ // A labeled Divider used to stand in for the heading; it rendered a
1820
+ // role="separator" as a direct child of the listbox and stacked a second
1821
+ // rule under the search row's own.
1822
+ expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
1823
+
1824
+ const group = screen.getByRole('group', {name: 'Citrus', ...h});
1825
+ const heading = group.querySelector(
1826
+ '.astryx-multi-selector-section-heading',
1827
+ );
1828
+ expect(heading).toBeTruthy();
1829
+ expect(heading).toHaveTextContent('Citrus');
1830
+ // The group already carries the title as its accessible name, so the
1831
+ // visible heading must not announce it a second time.
1832
+ expect(heading).toHaveAttribute('aria-hidden', 'true');
1833
+ // ...and it precedes the options it heads.
1834
+ const [firstOption] = within(group).getAllByRole('option', h);
1835
+ expect(
1836
+ heading!.compareDocumentPosition(firstOption) &
1837
+ Node.DOCUMENT_POSITION_FOLLOWING,
1838
+ ).toBeTruthy();
1839
+ });
1840
+ });
1841
+
1777
1842
  describe('MultiSelector search affordances', () => {
1843
+ it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
1844
+ const user = userEvent.setup();
1845
+ render(
1846
+ <MultiSelector
1847
+ label="Fruit"
1848
+ options={['Apple', 'Banana', 'Orange']}
1849
+ value={[]}
1850
+ onChange={() => {}}
1851
+ hasSearch
1852
+ />,
1853
+ );
1854
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
1855
+
1856
+ const search = screen.getByRole('combobox', h);
1857
+ // The row is the outer gutter; the input sits inside the rounded field.
1858
+ const row = search.closest('.astryx-multi-selector-search');
1859
+ const field = search.parentElement;
1860
+ if (!row || !field) {
1861
+ throw new Error('search row not found');
1862
+ }
1863
+ // The panel is already a bordered surface: the field inside it must not be
1864
+ // a second bordered box (this used to render a TextInput).
1865
+ expect(row).not.toHaveClass('astryx-text-input');
1866
+ expect(search.closest('.astryx-text-input')).toBeNull();
1867
+ // The field is a rounded box inset from the panel edge, shaped like the
1868
+ // option rows under it — not a full-bleed header strip.
1869
+ expect(field).not.toBe(row);
1870
+ // ...and a divider separates it from the options.
1871
+ const separator = document.querySelector('[role="separator"]');
1872
+ if (!separator) {
1873
+ throw new Error('divider not found');
1874
+ }
1875
+ // Order: row, then divider, then the listbox.
1876
+ expect(
1877
+ row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
1878
+ ).toBeTruthy();
1879
+ const listbox = screen.getByRole('listbox', h);
1880
+ expect(
1881
+ separator.compareDocumentPosition(listbox) &
1882
+ Node.DOCUMENT_POSITION_FOLLOWING,
1883
+ ).toBeTruthy();
1884
+ });
1885
+
1886
+ it('keeps the search row outside the scrolling option list', async () => {
1887
+ const user = userEvent.setup();
1888
+ render(
1889
+ <MultiSelector
1890
+ label="Fruit"
1891
+ options={['Apple', 'Banana', 'Orange']}
1892
+ value={[]}
1893
+ onChange={() => {}}
1894
+ hasSearch
1895
+ />,
1896
+ );
1897
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
1898
+ // The options scroll under the header rather than carrying it away, so the
1899
+ // field stays reachable in a long list.
1900
+ const search = screen.getByRole('combobox', h);
1901
+ const listbox = screen.getByRole('listbox', h);
1902
+ expect(listbox.contains(search)).toBe(false);
1903
+ });
1904
+
1778
1905
  it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
1779
1906
  const user = userEvent.setup();
1780
1907
  render(
@@ -1788,8 +1915,7 @@ describe('MultiSelector search affordances', () => {
1788
1915
  );
1789
1916
  await user.click(screen.getByRole('button', {name: 'Fruit'}));
1790
1917
  const search = screen.getByRole('combobox', h);
1791
- // The search field is a TextInput; the magnifier is its startIcon, so it
1792
- // sits inside the input container as a sibling of the <input>.
1918
+ // The magnifier leads the search row, as a sibling of the <input>.
1793
1919
  const wrapper = search.parentElement;
1794
1920
  const magnifier = wrapper?.querySelector('.astryx-icon');
1795
1921
  expect(magnifier).toBeTruthy();
@@ -2088,7 +2214,7 @@ describe('MultiSelector dropdown option theme target', () => {
2088
2214
  });
2089
2215
 
2090
2216
  describe('MultiSelector popup theme target', () => {
2091
- it('puts astryx-multi-selector-popup on the dropdown box inside the layer', async () => {
2217
+ it('puts astryx-multi-selector-popup on the surface that paints, not the list inside it', async () => {
2092
2218
  const user = userEvent.setup();
2093
2219
  render(
2094
2220
  <MultiSelector
@@ -2100,13 +2226,17 @@ describe('MultiSelector popup theme target', () => {
2100
2226
  );
2101
2227
  await user.click(screen.getByRole('combobox', {name: /Fruit/}));
2102
2228
 
2103
- const layer = document.querySelector('[popover]') as HTMLElement;
2104
2229
  const popup = document.querySelector(
2105
2230
  '.astryx-multi-selector-popup',
2106
2231
  ) as HTMLElement;
2107
2232
  expect(popup).not.toBeNull();
2233
+ expect(popup).toHaveClass('astryx-popover-surface');
2234
+ // The scrolling list is a descendant, not the target itself.
2235
+ expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
2236
+ expect(popup.getAttribute('role')).toBeNull();
2237
+
2238
+ const layer = document.querySelector('[popover]') as HTMLElement;
2108
2239
  expect(popup).not.toBe(layer);
2109
2240
  expect(layer.contains(popup)).toBe(true);
2110
- expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
2111
2241
  });
2112
2242
  });