@astryxdesign/core 0.7.1 → 0.7.2-canary.151434a

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 (251) hide show
  1. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +44 -3
  4. package/dist/Button/Button.d.ts.map +1 -1
  5. package/dist/Button/Button.js +38 -14
  6. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  7. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  8. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  9. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  10. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  11. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  12. package/dist/Dialog/Dialog.d.ts.map +1 -1
  13. package/dist/Dialog/Dialog.js +20 -8
  14. package/dist/Drawer/Drawer.d.ts +191 -0
  15. package/dist/Drawer/Drawer.d.ts.map +1 -0
  16. package/dist/Drawer/Drawer.js +382 -0
  17. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  18. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  19. package/dist/Drawer/DrawerHeader.js +134 -0
  20. package/dist/Drawer/index.d.ts +11 -0
  21. package/dist/Drawer/index.d.ts.map +1 -0
  22. package/dist/Drawer/index.js +11 -0
  23. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  24. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  25. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  26. package/dist/DropdownMenu/DropdownMenu.js +1 -1
  27. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  28. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  29. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +4 -4
  31. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  32. package/dist/EmptyState/EmptyState.js +4 -10
  33. package/dist/Field/Field.d.ts +1 -1
  34. package/dist/Field/Field.d.ts.map +1 -1
  35. package/dist/Field/Field.js +8 -4
  36. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  37. package/dist/HoverCard/useHoverCard.js +10 -2
  38. package/dist/Item/Item.d.ts +6 -2
  39. package/dist/Item/Item.d.ts.map +1 -1
  40. package/dist/Item/Item.js +19 -4
  41. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  42. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  43. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  44. package/dist/Layer/useLayer.d.ts.map +1 -1
  45. package/dist/Layer/useLayer.js +13 -7
  46. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  47. package/dist/Layer/useLayerDismissal.js +1 -1
  48. package/dist/Layout/container.stylex.d.ts +5 -5
  49. package/dist/Layout/container.stylex.js +4 -4
  50. package/dist/List/List.d.ts +46 -19
  51. package/dist/List/List.d.ts.map +1 -1
  52. package/dist/List/List.js +28 -10
  53. package/dist/List/ListContext.d.ts +8 -3
  54. package/dist/List/ListContext.d.ts.map +1 -1
  55. package/dist/List/ListContext.js +7 -2
  56. package/dist/List/ListItem.d.ts +13 -5
  57. package/dist/List/ListItem.d.ts.map +1 -1
  58. package/dist/List/ListItem.js +21 -8
  59. package/dist/List/index.d.ts +1 -1
  60. package/dist/List/index.d.ts.map +1 -1
  61. package/dist/Markdown/Markdown.d.ts.map +1 -1
  62. package/dist/Markdown/Markdown.js +8 -1
  63. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  64. package/dist/Markdown/plugins/frontmatter.js +7 -3
  65. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  66. package/dist/MetadataList/MetadataList.js +5 -1
  67. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  68. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  69. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  70. package/dist/MultiSelector/MultiSelector.js +22 -8
  71. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  72. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  73. package/dist/MultiSelector/hooks.js +3 -1
  74. package/dist/Pagination/Pagination.d.ts +8 -5
  75. package/dist/Pagination/Pagination.d.ts.map +1 -1
  76. package/dist/Pagination/Pagination.js +21 -8
  77. package/dist/Popover/Popover.js +2 -2
  78. package/dist/PowerSearch/PowerSearch.js +1 -1
  79. package/dist/RadioList/RadioListItem.d.ts +9 -1
  80. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  81. package/dist/RadioList/RadioListItem.js +2 -0
  82. package/dist/Selector/hooks.d.ts.map +1 -1
  83. package/dist/Selector/hooks.js +6 -2
  84. package/dist/Switch/Switch.d.ts +1 -1
  85. package/dist/Switch/Switch.d.ts.map +1 -1
  86. package/dist/Switch/Switch.js +13 -1
  87. package/dist/TabList/TabMenu.d.ts +2 -2
  88. package/dist/TabList/TabMenu.d.ts.map +1 -1
  89. package/dist/TabList/TabMenu.js +6 -3
  90. package/dist/Text/useTruncation.d.ts +7 -5
  91. package/dist/Text/useTruncation.d.ts.map +1 -1
  92. package/dist/Text/useTruncation.js +16 -5
  93. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  94. package/dist/Tooltip/useTooltip.js +11 -0
  95. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  96. package/dist/Typeahead/BaseTypeahead.js +26 -0
  97. package/dist/astryx.css +35 -12
  98. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  99. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  100. package/dist/hooks/useEntryAnimation.js +14 -12
  101. package/dist/hooks/useIsHydrating.d.ts +6 -0
  102. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  103. package/dist/hooks/useIsHydrating.js +30 -0
  104. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  105. package/dist/i18n/generated-locales/en.generated.js +112 -1
  106. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  107. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +1 -0
  111. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  112. package/dist/theme/derivedVarRegistry.js +0 -3
  113. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  114. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  115. package/dist/utils/focusOutline.stylex.js +4 -0
  116. package/dist/utils/plainDate.d.ts.map +1 -1
  117. package/dist/utils/plainDate.js +15 -9
  118. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  119. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  120. package/dist/utils/sharedMutationObserver.js +69 -0
  121. package/locales/en.json +444 -0
  122. package/locales/pseudo.json +333 -0
  123. package/package.json +9 -4
  124. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  125. package/src/AspectRatio/AspectRatio.tsx +38 -18
  126. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  127. package/src/Button/Button.doc.mjs +2 -2
  128. package/src/Button/Button.test.tsx +155 -9
  129. package/src/Button/Button.tsx +39 -16
  130. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  131. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  132. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  133. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  134. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  135. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  136. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  137. package/src/Dialog/Dialog.test.tsx +46 -0
  138. package/src/Dialog/Dialog.tsx +20 -7
  139. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  140. package/src/Drawer/Drawer.doc.mjs +472 -0
  141. package/src/Drawer/Drawer.spec.md +357 -0
  142. package/src/Drawer/Drawer.test.tsx +1300 -0
  143. package/src/Drawer/Drawer.tsx +613 -0
  144. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  145. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  146. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  147. package/src/Drawer/DrawerHeader.tsx +235 -0
  148. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  149. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  150. package/src/Drawer/index.ts +13 -0
  151. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  152. package/src/DropdownMenu/DropdownMenu.test.tsx +21 -12
  153. package/src/DropdownMenu/DropdownMenu.tsx +2 -2
  154. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  155. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  156. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +2 -2
  157. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  158. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  159. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  160. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  161. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  162. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  163. package/src/EmptyState/EmptyState.spec.md +189 -0
  164. package/src/EmptyState/EmptyState.test.tsx +74 -3
  165. package/src/EmptyState/EmptyState.tsx +4 -8
  166. package/src/Field/Field.doc.mjs +14 -8
  167. package/src/Field/Field.spec.md +217 -0
  168. package/src/Field/Field.test.tsx +60 -2
  169. package/src/Field/Field.tsx +17 -8
  170. package/src/HoverCard/HoverCard.test.tsx +58 -0
  171. package/src/HoverCard/useHoverCard.tsx +10 -2
  172. package/src/IconButton/IconButton.doc.mjs +2 -2
  173. package/src/IconButton/IconButton.test.tsx +3 -1
  174. package/src/Item/Item.doc.mjs +12 -8
  175. package/src/Item/Item.spec.md +226 -0
  176. package/src/Item/Item.test.tsx +61 -0
  177. package/src/Item/Item.tsx +40 -4
  178. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  179. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  180. package/src/Layer/layerViewportInset.test.ts +1 -0
  181. package/src/Layer/useLayer.tsx +24 -13
  182. package/src/Layer/useLayerDismissal.ts +1 -1
  183. package/src/Layout/container.stylex.ts +6 -6
  184. package/src/List/List.doc.mjs +14 -1
  185. package/src/List/List.spec.md +129 -77
  186. package/src/List/List.test.tsx +163 -94
  187. package/src/List/List.tsx +74 -18
  188. package/src/List/ListContext.tsx +9 -3
  189. package/src/List/ListItem.doc.mjs +24 -5
  190. package/src/List/ListItem.tsx +56 -27
  191. package/src/List/index.ts +1 -1
  192. package/src/Markdown/Markdown.tsx +14 -1
  193. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  194. package/src/Markdown/plugins/frontmatter.ts +7 -3
  195. package/src/MetadataList/MetadataList.doc.mjs +2 -2
  196. package/src/MetadataList/MetadataList.test.tsx +38 -0
  197. package/src/MetadataList/MetadataList.tsx +8 -1
  198. package/src/MetadataList/MetadataListContext.tsx +1 -1
  199. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  200. package/src/MultiSelector/MultiSelector.tsx +40 -7
  201. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  202. package/src/MultiSelector/hooks.ts +3 -1
  203. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  204. package/src/Pagination/Pagination.doc.mjs +7 -7
  205. package/src/Pagination/Pagination.spec.md +9 -9
  206. package/src/Pagination/Pagination.test.tsx +242 -0
  207. package/src/Pagination/Pagination.tsx +27 -11
  208. package/src/Popover/Popover.test.tsx +2 -2
  209. package/src/Popover/Popover.tsx +4 -4
  210. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  211. package/src/PowerSearch/PowerSearch.tsx +2 -2
  212. package/src/RadioList/RadioList.test.tsx +22 -0
  213. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  214. package/src/RadioList/RadioListItem.tsx +10 -1
  215. package/src/Selector/hooks.ts +6 -2
  216. package/src/Selector/useCombobox.doc.mjs +237 -0
  217. package/src/Switch/Switch.test.tsx +49 -0
  218. package/src/Switch/Switch.tsx +11 -0
  219. package/src/TabList/TabList.spec.md +57 -44
  220. package/src/TabList/TabList.test.tsx +67 -0
  221. package/src/TabList/TabMenu.doc.mjs +4 -3
  222. package/src/TabList/TabMenu.tsx +7 -4
  223. package/src/Text/useTruncation.test.tsx +69 -0
  224. package/src/Text/useTruncation.ts +16 -5
  225. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  226. package/src/Tooltip/Tooltip.test.tsx +31 -0
  227. package/src/Tooltip/useTooltip.test.tsx +13 -0
  228. package/src/Tooltip/useTooltip.tsx +11 -0
  229. package/src/TreeList/TreeList.spec.md +29 -17
  230. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  231. package/src/Typeahead/Typeahead.test.tsx +260 -2
  232. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  233. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  234. package/src/hooks/useEntryAnimation.ts +15 -16
  235. package/src/hooks/useIsHydrating.test.tsx +70 -0
  236. package/src/hooks/useIsHydrating.ts +31 -0
  237. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  238. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  239. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  240. package/src/i18n/generated-locales/en.generated.ts +121 -0
  241. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  242. package/src/i18n/useTranslator.test.tsx +37 -0
  243. package/src/index.ts +1 -0
  244. package/src/reset.css +0 -29
  245. package/src/theme/derivedVarRegistry.test.ts +3 -3
  246. package/src/theme/derivedVarRegistry.ts +0 -3
  247. package/src/utils/focusOutline.stylex.ts +4 -0
  248. package/src/utils/plainDate.test.ts +37 -1
  249. package/src/utils/plainDate.ts +15 -5
  250. package/src/utils/sharedMutationObserver.test.ts +53 -0
  251. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @file Field.test.tsx
5
- * @input Uses vitest, @testing-library/react, Field component
5
+ * @input Uses Vitest, Testing Library, StyleX, Field, FormLayoutContext
6
6
  * @output Unit tests for Field component behavior
7
7
  * @position Testing; validates Field.tsx implementation
8
8
  *
@@ -10,11 +10,18 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, afterEach} from 'vitest';
13
- import {render, screen, waitFor} from '@testing-library/react';
13
+ import {render, screen, waitFor, fireEvent} from '@testing-library/react';
14
+ import * as stylex from '@stylexjs/stylex';
14
15
  import {Field} from './Field';
15
16
  import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
16
17
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
17
18
 
19
+ const testStyles = stylex.create({
20
+ root: {
21
+ minWidth: '123px',
22
+ },
23
+ });
24
+
18
25
  function politeRegion(): HTMLElement | null {
19
26
  return document.querySelector('[data-astryx-live-region="polite"]');
20
27
  }
@@ -572,6 +579,57 @@ describe('Field', () => {
572
579
  </FormLayoutContext>
573
580
  );
574
581
 
582
+ it('composes root props without letting callers replace layout semantics', () => {
583
+ const onClick = vi.fn();
584
+ const expectedXstyleClass = stylex.props(testStyles.root).className;
585
+ if (expectedXstyleClass == null) {
586
+ throw new Error('Expected the test xstyle to produce a class name.');
587
+ }
588
+
589
+ render(
590
+ <Field
591
+ label="Name"
592
+ inputID="name-input"
593
+ data-testid="field"
594
+ data-layout="consumer"
595
+ data-owner="consumer"
596
+ className="consumer-class"
597
+ style={{width: '321px'}}
598
+ xstyle={testStyles.root}
599
+ onClick={onClick}>
600
+ <input id="name-input" />
601
+ </Field>,
602
+ {wrapper: horizontalLabelsWrapper},
603
+ );
604
+
605
+ const field = screen.getByTestId('field');
606
+ expect(field).toHaveClass('astryx-field', 'consumer-class');
607
+ expect(field).toHaveClass(expectedXstyleClass);
608
+ expect(field).toHaveAttribute('data-layout', 'horizontal-labels');
609
+ expect(field).toHaveAttribute('data-owner', 'consumer');
610
+ expect(field).toHaveStyle({width: '321px'});
611
+ fireEvent.click(field);
612
+ expect(onClick).toHaveBeenCalledOnce();
613
+ });
614
+
615
+ it('visually hides the description in horizontal-label mode when the label is hidden', () => {
616
+ render(
617
+ <Field
618
+ label="Search"
619
+ inputID="search-input"
620
+ isLabelHidden
621
+ description="Search for items"
622
+ descriptionID="search-desc">
623
+ <input id="search-input" aria-describedby="search-desc" />
624
+ </Field>,
625
+ {wrapper: horizontalLabelsWrapper},
626
+ );
627
+
628
+ const description = screen.getByText('Search for items');
629
+ expect(description.className).toContain('visuallyHidden');
630
+ expect(description).toHaveAttribute('id', 'search-desc');
631
+ });
632
+
575
633
  it('applies display:contents when in horizontal-labels context', () => {
576
634
  const {container} = render(
577
635
  <Field label="Name" inputID="name-input">
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Field.tsx
7
- * @input Uses React, HTMLAttributes, ReactNode, FieldLabel, IconType, useFieldStatusAnnouncement
7
+ * @input Uses React, StyleX, BaseProps, FieldLabel/FieldStatus, FormLayoutContext, Text, VisuallyHidden, theme/status utilities
8
8
  * @output Exports Field component, FieldProps
9
9
  * @position Core implementation; consumed by index.ts, tested by Field.test.tsx
10
10
  *
@@ -30,6 +30,7 @@ import {mergeProps} from '../utils';
30
30
  import {useDevWarning} from '../hooks/useDevWarning';
31
31
  import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
32
32
  import {Text} from '../Text';
33
+ import {VisuallyHidden} from '../VisuallyHidden';
33
34
  import {themeProps} from '../utils/themeProps';
34
35
 
35
36
  const styles = stylex.create({
@@ -298,20 +299,28 @@ export function Field({
298
299
  return (
299
300
  <div
300
301
  ref={ref}
302
+ {...props}
301
303
  {...mergeProps(
302
304
  themeProps('field', {layout: 'horizontal-labels'}),
303
305
  stylex.props(styles.horizontalLabels, xstyle),
304
306
  className,
305
307
  style,
306
- )}
307
- {...props}>
308
+ )}>
308
309
  <div {...stylex.props(styles.horizontalLabelAlign)}>{labelNode}</div>
309
310
  <div {...stylex.props(styles.inputStatusWrapper)}>
310
- {description && (
311
- <Text type="supporting" display="block" id={resolvedDescriptionID}>
312
- {description}
313
- </Text>
314
- )}
311
+ {description &&
312
+ (isLabelHidden ? (
313
+ <VisuallyHidden id={resolvedDescriptionID}>
314
+ {description}
315
+ </VisuallyHidden>
316
+ ) : (
317
+ <Text
318
+ type="supporting"
319
+ display="block"
320
+ id={resolvedDescriptionID}>
321
+ {description}
322
+ </Text>
323
+ ))}
315
324
  {children}
316
325
  {statusNode}
317
326
  </div>
@@ -109,6 +109,64 @@ describe('useHoverCard identity', () => {
109
109
  });
110
110
  });
111
111
 
112
+ // A card closed while the pointer rests on it unmounts without a mouseleave,
113
+ // so the hook must not keep believing the pointer is over the card: if it
114
+ // does, the next open of the same trigger never closes on leave.
115
+ describe('useHoverCard closing while the pointer is on the card', () => {
116
+ function Harness() {
117
+ const card = useHoverCard({delay: 0, hideDelay: 0});
118
+ return (
119
+ <div>
120
+ <span ref={card.ref}>Trigger</span>
121
+ <output data-testid="state">{card.isOpen ? 'open' : 'closed'}</output>
122
+ {card.renderHoverCard(
123
+ <button type="button" onClick={card.hide}>
124
+ Pick
125
+ </button>,
126
+ )}
127
+ </div>
128
+ );
129
+ }
130
+
131
+ async function openThenRestOnCard() {
132
+ const state = screen.getByTestId('state');
133
+ fireEvent.mouseEnter(screen.getByText('Trigger'));
134
+ await waitFor(() => expect(state).toHaveTextContent('open'));
135
+ const pick = screen.getByRole('button', {name: 'Pick', hidden: true});
136
+ fireEvent.mouseLeave(screen.getByText('Trigger'));
137
+ fireEvent.mouseEnter(pick.parentElement!);
138
+ return {state, pick};
139
+ }
140
+
141
+ async function expectNextOpenClosesOnLeave(state: HTMLElement) {
142
+ const trigger = screen.getByText('Trigger');
143
+ fireEvent.mouseEnter(trigger);
144
+ await waitFor(() => expect(state).toHaveTextContent('open'));
145
+ fireEvent.mouseLeave(trigger);
146
+ await waitFor(() => expect(state).toHaveTextContent('closed'));
147
+ }
148
+
149
+ it('closes the next open on leave after hide() from inside the card', async () => {
150
+ render(<Harness />);
151
+ const {state, pick} = await openThenRestOnCard();
152
+
153
+ fireEvent.click(pick);
154
+ await waitFor(() => expect(state).toHaveTextContent('closed'));
155
+
156
+ await expectNextOpenClosesOnLeave(state);
157
+ });
158
+
159
+ it('closes the next open on leave after Escape dismisses the card', async () => {
160
+ render(<Harness />);
161
+ const {state} = await openThenRestOnCard();
162
+
163
+ fireEvent.keyDown(document.body, {key: 'Escape'});
164
+ await waitFor(() => expect(state).toHaveTextContent('closed'));
165
+
166
+ await expectNextOpenClosesOnLeave(state);
167
+ });
168
+ });
169
+
112
170
  describe('HoverCard', () => {
113
171
  it('renders trigger element', () => {
114
172
  render(
@@ -345,6 +345,14 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
345
345
  hideLayer();
346
346
  }, [clearTimeouts, hideLayer]);
347
347
 
348
+ // The imperative hide. A card closed while the pointer rests on it unmounts
349
+ // without a mouseleave, so the content-hover latch is cleared here; left set,
350
+ // it would keep the trigger's next hover-open from closing on leave.
351
+ const hide = useCallback(() => {
352
+ isHoveringContentRef.current = false;
353
+ hideLayer();
354
+ }, [hideLayer]);
355
+
348
356
  const {
349
357
  isTouchPointerRef,
350
358
  isTouchInteraction,
@@ -508,7 +516,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
508
516
  ? null
509
517
  : document.getElementById(layerId);
510
518
  const hadFocus = card?.contains(document.activeElement) ?? false;
511
- hideLayer();
519
+ hide();
512
520
  if (hadFocus) {
513
521
  isEscapeDismissingRef.current = true;
514
522
  triggerRef.current?.focus();
@@ -720,6 +728,6 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
720
728
  isOpen: isLayerOpen,
721
729
  renderHoverCard,
722
730
  show: showLayer,
723
- hide: hideLayer,
731
+ hide,
724
732
  };
725
733
  }
@@ -46,7 +46,7 @@ export const docs = {
46
46
  {
47
47
  name: 'isLoading',
48
48
  type: 'boolean',
49
- description: 'Shows a loading spinner and disables interaction.',
49
+ description: 'Shows a loading spinner and blocks re-activation while staying focusable (aria-busy + aria-disabled, not native disabled).',
50
50
  default: 'false',
51
51
  },
52
52
  {
@@ -141,7 +141,7 @@ export const docsDense = {
141
141
  variant: 'visual style variant',
142
142
  size: 'size variant',
143
143
  elevation: 'resting shadow depth: none|low|med|high; raise for a floating action button (FAB)',
144
- isLoading: 'shows loading spinner + disables interaction',
144
+ isLoading: 'shows loading spinner, blocks re-activation but stays focusable (aria-disabled, not native disabled)',
145
145
  isDisabled: 'disables button',
146
146
  tooltip: 'tooltip text shown on hover',
147
147
  onClick: 'standard click handler',
@@ -124,7 +124,9 @@ describe('IconButton', () => {
124
124
  it('shows loading state', () => {
125
125
  render(<IconButton label="Save" icon={<span>💾</span>} isLoading />);
126
126
  const button = screen.getByRole('button');
127
- expect(button).toBeDisabled();
127
+ // Busy is aria-disabled (focusable), not native disabled — see #4871.
128
+ expect(button).not.toBeDisabled();
129
+ expect(button).toHaveAttribute('aria-disabled', 'true');
128
130
  });
129
131
 
130
132
  it('forwards ref correctly', () => {
@@ -49,7 +49,7 @@ export const docs = {
49
49
  {
50
50
  name: '--_item-inset-inline',
51
51
  description:
52
- 'Inline inset of the row. Item derives its paddingInline from this variable, and List reads it to cancel the inset when edgeCompensation="inline". Set paddingInline on `item` in a theme and both stay in sync.',
52
+ 'Inline inset of the row. Item derives its paddingInline from this variable, and List reads it to place rows for its edge treatments (edge="flush", "outset", and "bleed"). Set paddingInline on `item` in a theme and both stay in sync.',
53
53
  default: 'var(--spacing-2) (var(--spacing-3) for density="spacious")',
54
54
  private: true,
55
55
  },
@@ -118,8 +118,9 @@ export const docs = {
118
118
  },
119
119
  {
120
120
  name: 'align',
121
- type: "'center' | 'start'",
122
- description: 'Vertical alignment of start/end content slots.',
121
+ type: "'center' | 'start' | 'first-line'",
122
+ description:
123
+ "Vertical alignment of start/end content slots. 'center' centers each slot on the whole text block; 'start' lines up slot tops with the text's top; 'first-line' centers each slot on the label's first line, so a small mark stays level with the first line of a wrapped label. A slot taller than that line moves the text down. The `item` theme target reflects the value as `align`.",
123
124
  default: "'center'",
124
125
  },
125
126
  {
@@ -248,7 +249,7 @@ export const docs = {
248
249
  {
249
250
  guidance: true,
250
251
  description:
251
- 'Use align="start" when start or end content is taller than a single line of text.',
252
+ 'Use align="first-line" when a wrapping label sits beside a small mark (a status dot, a radio, a timestamp) that belongs to its first line. Use align="start" to line up the tops of a tall slot and the text.',
252
253
  },
253
254
  {
254
255
  guidance: true,
@@ -321,7 +322,8 @@ export const docsZh = {
321
322
  endContent:
322
323
  '在标签/描述区域之后渲染的内容,例如徽章、元数据、时间戳或操作按钮。',
323
324
  as: '根元素的 HTML 元素。',
324
- align: '起始/结束内容插槽的垂直对齐方式。',
325
+ align:
326
+ "起始/结束内容插槽的垂直对齐方式:'center'(默认)、'start'(顶部对齐)或 'first-line'(以标签第一行为中心)。",
325
327
  density:
326
328
  '间距密度。"compact" 使用 4px 块内距,"balanced" 使用 8px,"spacious" 使用 12px 块内距和内联内距。',
327
329
  labelLines: '标签截断前的最大行数。',
@@ -363,7 +365,8 @@ export const docsZh = {
363
365
  },
364
366
  {
365
367
  guidance: true,
366
- description: '当起始或结束内容高于单行文本时使用 align="start"。',
368
+ description:
369
+ '当换行标签旁边是属于其第一行的小标记(状态点、单选按钮、时间戳)时,使用 align="first-line"。使用 align="start" 让较高插槽与文本顶部对齐。',
367
370
  },
368
371
  {
369
372
  guidance: false,
@@ -426,7 +429,7 @@ export const docsDense = {
426
429
  {
427
430
  guidance: true,
428
431
  description:
429
- 'align="start" when start/end content is taller than one text line.',
432
+ 'align="first-line" for a small mark beside a wrapping label; align="start" to top-align a tall slot with the text.',
430
433
  },
431
434
  {
432
435
  guidance: false,
@@ -458,7 +461,8 @@ export const docsDense = {
458
461
  endContent:
459
462
  'Content after label/description: badges, timestamps, actions.',
460
463
  as: 'Root HTML element.',
461
- align: 'Vertical alignment of start/end content slots.',
464
+ align:
465
+ "Start/end slot alignment: 'center' (default), 'start' (tops), or 'first-line' (centered on the label's first line).",
462
466
  density:
463
467
  'Spacing: "compact" (4px), "balanced" (8px), or "spacious" (12px).',
464
468
  labelLines: 'Max label lines before truncation.',
@@ -0,0 +1,226 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 7
4
+ kind: component
5
+ id: component:Item
6
+ authority: current
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-10-16
11
+ owners: [cixzhang]
12
+ review_triggers: [public-api, layout, theming]
13
+ verified_by:
14
+ [
15
+ packages/core/src/Item/Item.test.tsx,
16
+ packages/core/src/List/List.test.tsx,
17
+ packages/core/src/RadioList/RadioList.test.tsx,
18
+ packages/core/src/CheckboxList/CheckboxList.test.tsx,
19
+ apps/storybook/stories/Item.stories.tsx,
20
+ scripts/check-knowledge.mjs,
21
+ ]
22
+ modules: []
23
+ families: [family:navigation-destinations]
24
+ design_specs: []
25
+ architecture:
26
+ [
27
+ architecture:component-theming-surface,
28
+ architecture:public-component-api,
29
+ architecture:internationalization,
30
+ ]
31
+ contributing: [contributing:api-conventions]
32
+ system_specs: [spec:AST-017/FR3, spec:AST-057]
33
+ ---
34
+
35
+ # Item component contract
36
+
37
+ <!-- Describe the system, not the project: present tense, what it does. No proposals, history, pull requests, or research in the record; see docs/contributing/spec-writing.md and report its rubric results in the pull request. -->
38
+
39
+ This record governs how Item aligns its start and end content against the
40
+ label. Item's swipe actions are governed by `spec:AST-057`, and its link and
41
+ navigation semantics by `family:navigation-destinations`; other Item behavior
42
+ remains described by its source, tests, and `Item.doc.mjs` until a record
43
+ claims it.
44
+
45
+ ## Contract at a glance
46
+
47
+ | Area | Contract |
48
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | Public contract | `align?: 'center' \| 'start' \| 'first-line'` on Item, ListItem, RadioListItem, and CheckboxListItem; default `center`. |
50
+ | Behavior | `first-line` centers each start and end slot on the label's first line, for any slot height; a slot taller than that line moves the text down rather than leaving the row. |
51
+ | End-user impact | A small status dot, radio, or age stays level with the first line of a wrapped label instead of drifting to the middle of the row (`center`) or riding above the line (`start`). |
52
+ | Builder impact | None by default. A builder opts in per row with `align="first-line"`; theme authors may style the `item` target's `first-line` alignment. |
53
+ | Compatibility/readiness | Additive: a new value on an existing closed axis and typed forwarding of an existing runtime pass-through. Rows that omit `align` are unchanged. |
54
+ | Review checks | Reject a changed default, changed `center`/`start` geometry, JavaScript measurement, a slot pushed outside the row, a second theme target, or a composing row that drops the value. |
55
+ | Governing rules | FR1–FR8 below; `architecture:component-theming-surface/INV6`; `spec:AST-017/FR3`. |
56
+
57
+ This table is a review projection; the body below is authoritative.
58
+
59
+ ## Intent
60
+
61
+ Item is the shared row behind lists, menus, pickers, and option rows. Its label
62
+ can wrap, while its start and end content is often a single-line-sized mark: a
63
+ status dot, a radio or checkbox, an icon, a short timestamp. Those marks belong
64
+ to the first line of text they annotate. Item owns the alignment because only
65
+ Item knows where its label's first line sits; a caller cannot place a slot
66
+ against it without copying Item's typography.
67
+
68
+ ## Compatibility and migration
69
+
70
+ - Released default preserved: `yes`; omitted `align` resolves to `center`.
71
+ - Compatibility class: additive closed-axis value; additive typed prop on three
72
+ composing rows that already forward unknown props to Item at runtime.
73
+ - Controlled/uncontrolled behavior: not applicable.
74
+ - Migration decision: `component:Item/DEC-1`.
75
+
76
+ Consumer migration instructions belong in consumer docs and release notes.
77
+
78
+ ## Ownership boundary
79
+
80
+ **Owns**
81
+
82
+ - The `align` axis: its values, default, and the geometry of each value for
83
+ the start and end content slots.
84
+ - Reflecting the resolved alignment on the `item` theme target.
85
+ - The forwarding rule for rows that compose Item and expose `align`.
86
+
87
+ **Does not own / non-goals**
88
+
89
+ - The `marker` slot's alignment — owned by whoever renders the marker; ListItem
90
+ positions its own markers against the first line.
91
+ - Typography of caller content inside `label` — owned by the caller.
92
+ - A theme-wide default alignment — themes style values; they do not choose a
93
+ component's prop default (`architecture:component-theming-surface`).
94
+ - Alignment inside a slot's own content (for example, an avatar stack) — owned
95
+ by that content.
96
+
97
+ ## Public concepts
98
+
99
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
100
+ | --------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------- | ---------------- | --------- | -------------------------------------------- |
101
+ | Alignment | `center`, `start`, `first-line` | `center`: slots centered on the whole text block. `start`: slot tops on the text block's top. `first-line`: slot centers on the first line's center. | Every density, layout, interaction mode, and direction; start and end slots | `center` | `component:Item` | stable | Other values are rejected by the public type |
102
+
103
+ ## Behavioral and layout contract
104
+
105
+ | ID | Invariant | Basis | Draft review state |
106
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------ |
107
+ | FR1 | Item MUST accept `align` with exactly `center`, `start`, and `first-line`, and MUST resolve an omitted value to `center`. The geometry of `center` and `start` MUST NOT change. | `component:Item/DEC-1` | settled |
108
+ | FR2 | Under `first-line`, the block-axis center of each rendered start-content and end-content slot MUST coincide with the block-axis center of the label's first line box, for every slot block size. | Owner direction | settled |
109
+ | FR3 | Under `first-line`, a slot taller than the label's first line MUST stay inside the row's content box: the label and description move down by the difference, and the row grows to fit. No slot is clipped or painted into the row's block padding. | Owner direction | settled |
110
+ | FR4 | A row whose label renders a single line and which has no description MUST render identically under `first-line` and `center`, for every slot block size. | Follows from FR2 and FR3 | settled |
111
+ | FR5 | The first line box is the label line as Item styles it, from the active theme's body text size and leading. A theme that changes those tokens moves the alignment with them. Caller content inside `label` that sets its own font size or line height is outside the FR2 guarantee. | Shared typography source | settled |
112
+ | FR6 | `first-line` MUST hold in every density, in `stacked` and `inline` layouts, with `labelLines` truncation, and in plain, button, link, link-root, delegated, and parent-role rows. In right-to-left text it MUST produce the same block-axis placement with the slots mirrored by logical order and no DOM reordering. | `architecture:internationalization` | settled |
113
+ | FR7 | The `item` theme target MUST reflect the resolved alignment as its existing `align` selector axis, including `first-line`, and Item's public theming metadata MUST list that value. No new target, state, or theme-level default is added. | `architecture:component-theming-surface/INV6`; `component:Item/DEC-2` | settled |
114
+ | FR8 | ListItem, RadioListItem, and CheckboxListItem MUST expose `align` with Item's values and default and forward it unchanged to the Item row they render. A composing row MUST NOT drop, remap, or narrow the value. | Owner direction | settled |
115
+
116
+ ### Allowed variation
117
+
118
+ - **AV1 — Slot content.** Any slot content and size may appear; FR2 holds for
119
+ each slot independently, so a start slot and an end slot of different heights
120
+ each center on the first line.
121
+ - **AV2 — Theme typography.** Body text size and leading may vary by theme; the
122
+ first line box follows them (FR5).
123
+ - **AV3 — Row height.** Under `first-line` a row may be taller than the same
124
+ row under `center` when a slot is taller than the first line and a
125
+ description or second label line is present (FR3).
126
+
127
+ ### Representative states
128
+
129
+ | State | Required invariant | Allowed variation |
130
+ | -------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------- |
131
+ | Wrapped label, 6px dot start, short-text end | Both slot centers on the first line's center. | Density, theme typography, direction. |
132
+ | One-line label, no description, any slot | Identical to `center`. | Slot size. |
133
+ | Label and description, 40px avatar start | Avatar center on the first line's center; text moves down; row grows. | Avatar size. |
134
+ | Radio or checkbox row with a wrapped label | The control centers on the first line. | Control size by Field size. |
135
+ | Right-to-left | Same block-axis placement; slots mirrored by logical order. | Writing direction. |
136
+ | Omitted `align` | Identical to today's `center`. | None. |
137
+
138
+ ### Transformation and precedence order
139
+
140
+ - **ORD1 — Resolution.** An explicit `align` on a composing row forwards to
141
+ Item unchanged; Item resolves an omitted value to `center`; the resolved value
142
+ drives both geometry and the `item` target's selector axis, so the two never
143
+ disagree.
144
+
145
+ ### Performance and resources
146
+
147
+ - **PR1 — CSS geometry only.** Every alignment value MUST be achieved in CSS:
148
+ no measurement, observer, listener, state, Effect, or extra render pass.
149
+
150
+ ## Accessibility contract
151
+
152
+ - **AR1 — Presentation only.** Alignment MUST NOT change the DOM order, role,
153
+ accessible name or description, focus target, tab order, or hit target of the
154
+ row or its slots.
155
+ - **AR2 — No exposed text.** Any construct that implements `first-line` MUST
156
+ contribute no text to the accessibility tree.
157
+
158
+ ## Design relationships
159
+
160
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
161
+ | --------------------- | ----------------------------------------------------- | ------------------------ | -------------- | ------------------ |
162
+ | Start and end content | Annotates the first line of the label it sits beside. | Owner direction | Supporting | FR2–FR4 |
163
+ | Label first line | The reference line the slots align to. | Item typography | Prominent | FR5 |
164
+
165
+ ## Family and system relationships
166
+
167
+ - `spec:AST-057` owns Item's swipe actions and their panels; alignment does
168
+ not change them, because the panels sit outside the row's flow.
169
+ - `family:navigation-destinations` owns Item's link and navigation semantics;
170
+ alignment does not change them.
171
+ - `architecture:component-theming-surface` owns how visual axes reach theme
172
+ targets; this record adds one value to Item's existing `align` axis.
173
+ - `architecture:public-component-api` owns prop admission; FR8 types an existing
174
+ runtime pass-through rather than adding a parallel concept.
175
+ - `architecture:internationalization` owns logical-direction behavior; FR6
176
+ applies it.
177
+ - `spec:AST-017/FR3` classifies this as a non-breaking feature: it adds a value
178
+ and a typed prop, narrows nothing, and a caller who never sets `align` sees no
179
+ change.
180
+
181
+ ## Verification map
182
+
183
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
184
+ | -------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------- |
185
+ | FR1, FR7 | `Item.test.tsx` alignment and theme-reflection suites; public type test | Omitted, `center`, `start`, `first-line` | A changed default, a missing `data-align="first-line"`, or a widened type fails. | `audit:Item/api` |
186
+ | FR2–FR6 | Real-Chromium geometry assertions over the Item alignment stories | Wrapped label with dot and age; avatar; densities; RTL; theme | A slot center off the first line center by more than 0.5px, or a clipped slot, fails. | `audit:Item/layout` |
187
+ | FR8 | `List.test.tsx`, `RadioList.test.tsx`, `CheckboxList.test.tsx` forwarding cases | Each composing row with `align="first-line"` | A row whose Item root lacks `data-align="first-line"` fails. | `audit:Item/api` |
188
+ | AR1, AR2 | Accessibility-tree snapshot of a `first-line` row against a `center` row | Plain and option rows | Any difference in name, role, description, or order fails. | `audit:Item/a11y` |
189
+ | PR1 | Source review of Item's alignment styles | All values | A ref, observer, or Effect added for alignment fails review. | `audit:Item/layout` |
190
+
191
+ ## Decision log
192
+
193
+ <!-- Record the boundary or requirement, not how it was reached. A rejected alternative is at most one line here, kept only when it is consequential and likely to recur. -->
194
+
195
+ ### DEC-1 — `first-line` is an opt-in value; `center` stays the default
196
+
197
+ **Reference:** `component:Item/DEC-1`
198
+ **Decider:** cixzhang, 2026-10-16
199
+
200
+ A wrapped label with a single-line-sized mark needs the mark on its first line,
201
+ and neither existing value does that: `center` drops the mark to the middle of
202
+ the row and `start` pins its top, which leaves a small mark above the line's
203
+ center. A new value fixes those rows without moving any row that relies on
204
+ today's default.
205
+
206
+ Rejected: making `first-line` the default, because a slot sized to the whole
207
+ text block, such as an avatar beside a label and description, is correct under
208
+ `center`.
209
+
210
+ ### DEC-2 — The theme setting is the `item` target's alignment axis
211
+
212
+ **Reference:** `component:Item/DEC-2`
213
+ **Decider:** cixzhang, 2026-10-16
214
+
215
+ Themes reach `first-line` rows through the `item` target's existing `align`
216
+ selector axis, the same way they reach `center` and `start`. A theme can style
217
+ those rows; it does not choose which value a row receives.
218
+
219
+ ## Open questions
220
+
221
+ None.
222
+
223
+ ## Content boundary
224
+
225
+ This file does not duplicate consumer prop tables/examples, current audit
226
+ results, implementation steps, or family/system rules. It links to their owners.
@@ -531,6 +531,67 @@ describe('Item', () => {
531
531
  expect(screen.getByTestId('item')).toBeInTheDocument();
532
532
  });
533
533
 
534
+ it('reflects the resolved alignment on the item theme target', () => {
535
+ const {rerender} = render(<Item label="Item" data-testid="item" />);
536
+ expect(screen.getByTestId('item')).toHaveAttribute('data-align', 'center');
537
+ for (const align of ['center', 'start', 'first-line'] as const) {
538
+ rerender(<Item label="Item" align={align} data-testid="item" />);
539
+ const item = screen.getByTestId('item');
540
+ expect(item).toHaveClass('astryx-item');
541
+ expect(item).toHaveAttribute('data-align', align);
542
+ }
543
+ });
544
+
545
+ it('aligns the row on first baselines only under first-line', () => {
546
+ const {rerender} = render(<Item label="Item" data-testid="item" />);
547
+ const declares = (value: string) =>
548
+ rulesDeclaredFor(screen.getByTestId('item')).some(rule =>
549
+ rule.includes(`align-items: ${value}`),
550
+ );
551
+ expect(declares('baseline')).toBe(false);
552
+ rerender(<Item label="Item" align="start" data-testid="item" />);
553
+ expect(declares('baseline')).toBe(false);
554
+ rerender(<Item label="Item" align="first-line" data-testid="item" />);
555
+ expect(declares('baseline')).toBe(true);
556
+ });
557
+
558
+ it('gives each first-line slot a strut with no accessible text', () => {
559
+ render(
560
+ <Item
561
+ label="Item"
562
+ align="first-line"
563
+ startContent={<span data-testid="start">S</span>}
564
+ endContent={<span data-testid="end">E</span>}
565
+ />,
566
+ );
567
+ for (const id of ['start', 'end']) {
568
+ const slot = screen.getByTestId(id).parentElement!;
569
+ const before = rulesDeclaredFor(slot).filter(rule =>
570
+ rule.slice(0, rule.indexOf('{')).includes('::before'),
571
+ );
572
+ expect(before.some(rule => rule.includes('/ ""'))).toBe(true);
573
+ }
574
+ });
575
+
576
+ it('leaves center and start slots without a strut', () => {
577
+ const {rerender} = render(
578
+ <Item label="Item" startContent={<span data-testid="start">S</span>} />,
579
+ );
580
+ const hasStrut = () =>
581
+ rulesDeclaredFor(screen.getByTestId('start').parentElement!).some(rule =>
582
+ rule.slice(0, rule.indexOf('{')).includes('::before'),
583
+ );
584
+ expect(hasStrut()).toBe(false);
585
+ rerender(
586
+ <Item
587
+ label="Item"
588
+ align="start"
589
+ startContent={<span data-testid="start">S</span>}
590
+ />,
591
+ );
592
+ expect(hasStrut()).toBe(false);
593
+ });
594
+
534
595
  // ===========================================================================
535
596
  // Description rendering
536
597
  // ===========================================================================