@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.
- package/dist/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +38 -14
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/Drawer/Drawer.d.ts +191 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -0
- package/dist/Drawer/Drawer.js +382 -0
- package/dist/Drawer/DrawerHeader.d.ts +85 -0
- package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
- package/dist/Drawer/DrawerHeader.js +134 -0
- package/dist/Drawer/index.d.ts +11 -0
- package/dist/Drawer/index.d.ts.map +1 -0
- package/dist/Drawer/index.js +11 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
- package/dist/Drawer/useDrawerDialogPresence.js +236 -0
- package/dist/DropdownMenu/DropdownMenu.js +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +4 -4
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -2
- package/dist/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -4
- package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
- package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
- package/dist/Layer/layerViewportInset.stylex.js +6 -2
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +13 -7
- package/dist/Layer/useLayerDismissal.d.ts +1 -1
- package/dist/Layer/useLayerDismissal.js +1 -1
- package/dist/Layout/container.stylex.d.ts +5 -5
- package/dist/Layout/container.stylex.js +4 -4
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +13 -5
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +21 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
- package/dist/Markdown/plugins/frontmatter.js +7 -3
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +22 -8
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -1
- package/dist/Pagination/Pagination.d.ts +8 -5
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +21 -8
- package/dist/Popover/Popover.js +2 -2
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/RadioList/RadioListItem.d.ts +9 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +2 -0
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +6 -2
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -1
- package/dist/TabList/TabMenu.d.ts +2 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +6 -3
- package/dist/Text/useTruncation.d.ts +7 -5
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +16 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/dist/astryx.css +35 -12
- package/dist/hooks/useEntryAnimation.d.ts +2 -1
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +14 -12
- package/dist/hooks/useIsHydrating.d.ts +6 -0
- package/dist/hooks/useIsHydrating.d.ts.map +1 -0
- package/dist/hooks/useIsHydrating.js +30 -0
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +112 -1
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +0 -3
- package/dist/utils/focusOutline.stylex.d.ts +4 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +4 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/dist/utils/sharedMutationObserver.d.ts +21 -0
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
- package/dist/utils/sharedMutationObserver.js +69 -0
- package/locales/en.json +444 -0
- package/locales/pseudo.json +333 -0
- package/package.json +9 -4
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.test.tsx +155 -9
- package/src/Button/Button.tsx +39 -16
- package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
- package/src/Button/__tests__/Button.a11y.states.ts +0 -10
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/Drawer/Drawer.doc.mjs +472 -0
- package/src/Drawer/Drawer.spec.md +357 -0
- package/src/Drawer/Drawer.test.tsx +1300 -0
- package/src/Drawer/Drawer.tsx +613 -0
- package/src/Drawer/DrawerDirection.test.tsx +127 -0
- package/src/Drawer/DrawerHeader.doc.mjs +293 -0
- package/src/Drawer/DrawerHeader.test.tsx +102 -0
- package/src/Drawer/DrawerHeader.tsx +235 -0
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
- package/src/Drawer/index.ts +13 -0
- package/src/Drawer/useDrawerDialogPresence.ts +284 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +21 -12
- package/src/DropdownMenu/DropdownMenu.tsx +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +2 -2
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/HoverCard/HoverCard.test.tsx +58 -0
- package/src/HoverCard/useHoverCard.tsx +10 -2
- package/src/IconButton/IconButton.doc.mjs +2 -2
- package/src/IconButton/IconButton.test.tsx +3 -1
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.spec.md +226 -0
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
- package/src/Layer/layerViewportInset.stylex.ts +6 -2
- package/src/Layer/layerViewportInset.test.ts +1 -0
- package/src/Layer/useLayer.tsx +24 -13
- package/src/Layer/useLayerDismissal.ts +1 -1
- package/src/Layout/container.stylex.ts +6 -6
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +163 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.doc.mjs +24 -5
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
- package/src/Markdown/plugins/frontmatter.ts +7 -3
- package/src/MetadataList/MetadataList.doc.mjs +2 -2
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MultiSelector/MultiSelector.test.tsx +159 -0
- package/src/MultiSelector/MultiSelector.tsx +40 -7
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
- package/src/MultiSelector/hooks.ts +3 -1
- package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
- package/src/Pagination/Pagination.doc.mjs +7 -7
- package/src/Pagination/Pagination.spec.md +9 -9
- package/src/Pagination/Pagination.test.tsx +242 -0
- package/src/Pagination/Pagination.tsx +27 -11
- package/src/Popover/Popover.test.tsx +2 -2
- package/src/Popover/Popover.tsx +4 -4
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioListItem.doc.mjs +16 -0
- package/src/RadioList/RadioListItem.tsx +10 -1
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/Switch/Switch.test.tsx +49 -0
- package/src/Switch/Switch.tsx +11 -0
- package/src/TabList/TabList.spec.md +57 -44
- package/src/TabList/TabList.test.tsx +67 -0
- package/src/TabList/TabMenu.doc.mjs +4 -3
- package/src/TabList/TabMenu.tsx +7 -4
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Tooltip/Tooltip.test.tsx +31 -0
- package/src/Tooltip/useTooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +11 -0
- package/src/TreeList/TreeList.spec.md +29 -17
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
- package/src/hooks/useEntryAnimation.doc.mjs +4 -4
- package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
- package/src/hooks/useEntryAnimation.ts +15 -16
- package/src/hooks/useIsHydrating.test.tsx +70 -0
- package/src/hooks/useIsHydrating.ts +31 -0
- package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
- package/src/i18n/InternationalizationProvider.spec.md +252 -0
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
- package/src/i18n/generated-locales/en.generated.ts +121 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
- package/src/i18n/useTranslator.test.tsx +37 -0
- package/src/index.ts +1 -0
- package/src/reset.css +0 -29
- package/src/theme/derivedVarRegistry.test.ts +3 -3
- package/src/theme/derivedVarRegistry.ts +0 -3
- package/src/utils/focusOutline.stylex.ts +4 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
- package/src/utils/sharedMutationObserver.test.ts +53 -0
- package/src/utils/sharedMutationObserver.ts +78 -0
package/src/Field/Field.test.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Field.test.tsx
|
|
5
|
-
* @input Uses
|
|
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">
|
package/src/Field/Field.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Field.tsx
|
|
7
|
-
* @input Uses React,
|
|
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
|
-
|
|
312
|
-
{
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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', () => {
|
package/src/Item/Item.doc.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
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="
|
|
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:
|
|
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="
|
|
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:
|
|
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.
|
package/src/Item/Item.test.tsx
CHANGED
|
@@ -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
|
// ===========================================================================
|