@astryxdesign/core 0.7.1 → 0.7.2-canary.01eab6a
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/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- 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/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -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.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +47 -4
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- 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.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
- 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/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- 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/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- 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/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +71 -9
- 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/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- 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/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +3 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/dist/astryx.css +47 -13
- 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/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- 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/Avatar/Avatar.doc.mjs +5 -3
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- 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/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- 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/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -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 +62 -13
- package/src/DropdownMenu/DropdownMenu.tsx +47 -4
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- 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/Grid/GridSpan.tsx +3 -0
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- 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.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioList.tsx +4 -1
- 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/SideNav/SideNav.test.tsx +179 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +96 -17
- package/src/Slider/Slider.spec.md +39 -18
- 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/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- 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/TopNav/TopNavMegaMenu.test.tsx +22 -0
- package/src/TopNav/TopNavMegaMenu.tsx +7 -3
- package/src/TopNav/TopNavMenu.test.tsx +19 -0
- package/src/TopNav/TopNavMenu.tsx +11 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.spec.md +29 -17
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
- package/src/docPropLiterals.test.ts +36 -10
- 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/hooks/useMenuHover.ts +18 -13
- 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 +7 -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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/** @vitest-environment jsdom */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @file Menu.a11y.test.tsx
|
|
6
|
+
* @input MENU_PATTERN bound to the open DropdownMenu pointer menu
|
|
7
|
+
* @output jsdom-lane proof of the DOM expectations and an honest "unrun" report for the browser layers
|
|
8
|
+
* @position Binding test for the DropdownMenu menu surface (the Chromium spec covers the rest)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {cleanup, render, screen} from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import {describe, expect, it} from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
MENU_PATTERN,
|
|
16
|
+
checkAccessibilitySpec,
|
|
17
|
+
createJsdomHarness,
|
|
18
|
+
expectAccessibilitySpec,
|
|
19
|
+
summarize,
|
|
20
|
+
type BindingResult,
|
|
21
|
+
} from '@astryxdesign/a11y-spec';
|
|
22
|
+
import {MENU_KNOWN_FAILURES} from './Menu.a11y.known-failures';
|
|
23
|
+
import {renderMenuState} from './Menu.a11y.renders';
|
|
24
|
+
import {
|
|
25
|
+
MENU_BINDING_STATES,
|
|
26
|
+
MENU_EXCLUSIONS,
|
|
27
|
+
type MenuBindingState,
|
|
28
|
+
} from './Menu.a11y.states';
|
|
29
|
+
|
|
30
|
+
function trigger(state: MenuBindingState): HTMLElement {
|
|
31
|
+
return screen.getByRole('button', {name: state.label});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function menuFor(state: MenuBindingState): HTMLElement {
|
|
35
|
+
const id = trigger(state).getAttribute('aria-controls');
|
|
36
|
+
const menu = id == null ? null : document.getElementById(id);
|
|
37
|
+
if (menu == null) {
|
|
38
|
+
throw new Error(`menu state "${state.id}" has no controlled menu`);
|
|
39
|
+
}
|
|
40
|
+
return menu;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function relatedFor(
|
|
44
|
+
state: MenuBindingState,
|
|
45
|
+
): Readonly<Record<string, Element>> {
|
|
46
|
+
const related: Record<string, Element> = {opener: trigger(state)};
|
|
47
|
+
for (const [relation, [role, name]] of Object.entries(state.items)) {
|
|
48
|
+
related[relation] = screen.getByRole(role, {name, hidden: true});
|
|
49
|
+
}
|
|
50
|
+
return related;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function open(state: MenuBindingState): Promise<void> {
|
|
54
|
+
render(renderMenuState(state));
|
|
55
|
+
await userEvent.setup().click(trigger(state));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function checkState(state: MenuBindingState): Promise<BindingResult> {
|
|
59
|
+
return checkAccessibilitySpec({
|
|
60
|
+
spec: MENU_PATTERN,
|
|
61
|
+
binding: 'DropdownMenu.menu',
|
|
62
|
+
state: state.id,
|
|
63
|
+
facts: state.facts,
|
|
64
|
+
knownFailures: MENU_KNOWN_FAILURES,
|
|
65
|
+
mount: async () => {
|
|
66
|
+
await open(state);
|
|
67
|
+
return createJsdomHarness({
|
|
68
|
+
subject: menuFor(state),
|
|
69
|
+
related: relatedFor(state),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
unmount: cleanup,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('DropdownMenu pointer menu — menu pattern, jsdom lane', () => {
|
|
77
|
+
it.each(
|
|
78
|
+
MENU_BINDING_STATES.map(state => [state.id, state.summary, state] as const),
|
|
79
|
+
)('%s — %s', async (_id, _summary, state) => {
|
|
80
|
+
await expectAccessibilitySpec({
|
|
81
|
+
spec: MENU_PATTERN,
|
|
82
|
+
binding: 'DropdownMenu.menu',
|
|
83
|
+
state: state.id,
|
|
84
|
+
facts: state.facts,
|
|
85
|
+
knownFailures: MENU_KNOWN_FAILURES,
|
|
86
|
+
render: async () => {
|
|
87
|
+
await open(state);
|
|
88
|
+
},
|
|
89
|
+
subject: () => menuFor(state),
|
|
90
|
+
related: () => relatedFor(state),
|
|
91
|
+
cleanup,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('runs the DOM layer and reports the browser-owned outcomes as unrun', async () => {
|
|
96
|
+
const results: BindingResult[] = [];
|
|
97
|
+
for (const state of MENU_BINDING_STATES) {
|
|
98
|
+
results.push(await checkState(state));
|
|
99
|
+
}
|
|
100
|
+
const report = summarize(MENU_PATTERN, results);
|
|
101
|
+
expect(report.counts.pass).toBeGreaterThan(0);
|
|
102
|
+
expect(report.unrunLayers).toEqual(['accessibility-tree', 'real-browser']);
|
|
103
|
+
expect(report.counts.unexpectedPass).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('menu binding inventory', () => {
|
|
108
|
+
it('gives every representative state a distinct checked-in story', () => {
|
|
109
|
+
const stories = MENU_BINDING_STATES.map(state => state.storyId);
|
|
110
|
+
expect(new Set(stories).size).toBe(stories.length);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('keeps the opening control, submenus and dialog presentations explicit', () => {
|
|
114
|
+
expect(MENU_EXCLUSIONS.map(row => row.id)).toEqual([
|
|
115
|
+
'opening-control',
|
|
116
|
+
'submenus',
|
|
117
|
+
'bottom-sheet-and-header-footer',
|
|
118
|
+
]);
|
|
119
|
+
for (const exclusion of MENU_EXCLUSIONS) {
|
|
120
|
+
expect(exclusion.owner.length, exclusion.id).toBeGreaterThan(20);
|
|
121
|
+
expect(exclusion.reason.length, exclusion.id).toBeGreaterThan(80);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('renders the menu and every item relation it names for every state', async () => {
|
|
126
|
+
for (const state of MENU_BINDING_STATES) {
|
|
127
|
+
await open(state);
|
|
128
|
+
expect(menuFor(state).getAttribute('role'), state.id).toBe('menu');
|
|
129
|
+
expect(Object.keys(relatedFor(state)), state.id).toEqual([
|
|
130
|
+
'opener',
|
|
131
|
+
...Object.keys(state.items),
|
|
132
|
+
]);
|
|
133
|
+
cleanup();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -12,7 +12,7 @@ export const docs = {
|
|
|
12
12
|
name: 'title',
|
|
13
13
|
type: 'string',
|
|
14
14
|
description:
|
|
15
|
-
'Primary message rendered as
|
|
15
|
+
'Primary message rendered as the configured heading level inside the empty state.',
|
|
16
16
|
required: true,
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -81,6 +81,7 @@ export const docs = {
|
|
|
81
81
|
{ guidance: false, description: 'Use an EmptyState for error messages that require immediate action; use a Banner instead.' },
|
|
82
82
|
],
|
|
83
83
|
anatomy: [
|
|
84
|
+
{name: 'Container', required: true, description: 'The centered status region that owns spacing, reading order, and the compact layout variant.'},
|
|
84
85
|
{name: 'Icon', required: false, description: 'A visual cue above the title that reinforces the context, like a search icon for no results.'},
|
|
85
86
|
{name: 'Title', required: true, description: 'Primary message explaining what is empty: "No projects yet" not "No data".'},
|
|
86
87
|
{name: 'Description', required: false, description: 'Additional context explaining why it is empty or what the user can do.'},
|
|
@@ -98,7 +99,7 @@ export const docsZh = {
|
|
|
98
99
|
name: 'title',
|
|
99
100
|
type: 'string',
|
|
100
101
|
description:
|
|
101
|
-
'
|
|
102
|
+
'在空状态内部按配置的标题级别渲染主要信息。',
|
|
102
103
|
required: true,
|
|
103
104
|
},
|
|
104
105
|
{
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:EmptyState
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, layout, theming, accessibility, testing]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/EmptyState/EmptyState.test.tsx,
|
|
16
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
17
|
+
packages/core/src/docPropReferences.test.ts,
|
|
18
|
+
]
|
|
19
|
+
modules: []
|
|
20
|
+
families: []
|
|
21
|
+
design_specs: []
|
|
22
|
+
architecture:
|
|
23
|
+
[
|
|
24
|
+
architecture:public-component-api,
|
|
25
|
+
architecture:component-theming-surface,
|
|
26
|
+
architecture:component-test-sufficiency,
|
|
27
|
+
]
|
|
28
|
+
contributing: []
|
|
29
|
+
system_specs: [spec:AST-020/FR1, spec:AST-029/FR3]
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# EmptyState component contract
|
|
33
|
+
|
|
34
|
+
## Contract at a glance
|
|
35
|
+
|
|
36
|
+
| Area | Contract |
|
|
37
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
38
|
+
| Public contract | `EmptyState` renders a required title plus optional description, decorative icon, and caller-owned actions in one status region; callers choose heading level and compact layout. |
|
|
39
|
+
| Behavior | Null, undefined, boolean, and empty-string optional values create no slot wrapper. Rendered content follows icon, text, then actions order; compact layout reduces spacing and stacks actions. |
|
|
40
|
+
| End-user impact | A person reaches a named empty-content message and any next action without blank gaps from shallow-empty optional values; the title fits the surrounding document outline. |
|
|
41
|
+
| Builder impact | Callers supply content and actions, select heading semantics, and may compose supported root props and styling. No additional setup is required. |
|
|
42
|
+
| Compatibility/readiness | The released default is preserved. This draft observes shipped behavior plus the narrow scalar-slot remediation; it adds no API, default, or compatibility promise. |
|
|
43
|
+
| Review checks | Reject a missing heading, an informative icon hidden as decorative, a shallow-empty optional scalar that still changes layout, a dropped root prop/ref/style, or a theme target without anatomy. |
|
|
44
|
+
| Governing rules | `architecture:public-component-api` INV1 and INV5–INV9; `architecture:component-theming-surface` INV2–INV6; `architecture:component-test-sufficiency` INV1–INV7, INV10–INV11; WCAG 2.2. |
|
|
45
|
+
|
|
46
|
+
This table is a review projection; the body below is authoritative.
|
|
47
|
+
|
|
48
|
+
## Intent
|
|
49
|
+
|
|
50
|
+
`EmptyState` gives an otherwise empty content area a semantic heading, supporting
|
|
51
|
+
context, optional decorative visual, and optional caller-owned next actions.
|
|
52
|
+
|
|
53
|
+
## Compatibility and migration
|
|
54
|
+
|
|
55
|
+
- Released default preserved: yes
|
|
56
|
+
- Compatibility class: observational and additive documentation; runtime remediation removes wrappers for nullish, boolean, and empty-string optional values
|
|
57
|
+
- Controlled/uncontrolled behavior: not applicable
|
|
58
|
+
- Migration decision: none
|
|
59
|
+
|
|
60
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
61
|
+
|
|
62
|
+
## Ownership boundary
|
|
63
|
+
|
|
64
|
+
**Owns**
|
|
65
|
+
|
|
66
|
+
- the status-region root, content order, heading element, and default/compact layout;
|
|
67
|
+
- hiding the icon slot from assistive technology as decorative; and
|
|
68
|
+
- whether optional slots create component-owned wrappers and spacing.
|
|
69
|
+
|
|
70
|
+
**Does not own / non-goals**
|
|
71
|
+
|
|
72
|
+
- the message copy, icon meaning, action controls, or action behavior — those belong to the product callsite and the components it supplies;
|
|
73
|
+
- page-level heading order — the caller chooses `headingLevel` for its document; or
|
|
74
|
+
- error, loading, or populated-content behavior.
|
|
75
|
+
|
|
76
|
+
## Public concepts
|
|
77
|
+
|
|
78
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
79
|
+
| -------------- | ----------------------- | ------------------------------------------------------------------------- | ----------------------------------------- | -------- | ---------------------- | --------- | ------------------------------------------------------- |
|
|
80
|
+
| title | string | The empty region's heading content. | all | required | `component:EmptyState` | stable | type rejection |
|
|
81
|
+
| description | string or absent | Optional supporting context below the title. | all | absent | `component:EmptyState` | stable | nullish and empty-string values omit the part |
|
|
82
|
+
| icon | React node or absent | Optional caller visual, exposed as decorative beside the textual message. | all | absent | product callsite | stable | nullish, boolean, and empty-string values omit the slot |
|
|
83
|
+
| actions | React node or absent | Optional caller-owned next actions below the message. | all | absent | product callsite | stable | nullish, boolean, and empty-string values omit the slot |
|
|
84
|
+
| heading level | `1` through `6` | The title's HTML heading level in the surrounding document outline. | all | `3` | product callsite | stable | type rejection |
|
|
85
|
+
| layout variant | default, compact | Compact reduces spacing and stacks the action slot vertically. | all | default | `component:EmptyState` | stable | type rejection |
|
|
86
|
+
|
|
87
|
+
## Behavioral and layout contract
|
|
88
|
+
|
|
89
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
90
|
+
intentional decision. A `current` contract contains no unresolved rows.
|
|
91
|
+
|
|
92
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
93
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ |
|
|
94
|
+
| FR1 | The component MUST render one status-region root containing the icon, text group, and actions in that reading order when present. | Current implementation and consumer docs; WCAG 2.2 SC 1.3.2 and SC 4.1.3 | verify |
|
|
95
|
+
| FR2 | The required title MUST render at the caller-selected heading level without changing the component's visual type role. | Current implementation, tests, and consumer docs; WCAG 2.2 SC 1.3.1 and SC 2.4.6 | verify |
|
|
96
|
+
| FR3 | Null, undefined, boolean, and empty-string optional values MUST NOT create a wrapper, layout gap, or assistive-technology node; containers that are shallowly non-empty remain wrapped. | React's direct-child rendering semantics; established public optional-slot interface | verify |
|
|
97
|
+
| FR4 | The decorative icon wrapper MUST be absent when the slot is absent and hidden from assistive technology when rendered. | Current implementation and tests; WCAG 2.2 SC 1.1.1 | verify |
|
|
98
|
+
| FR5 | Root DOM/data/ARIA/event props, ref, `className`, `style`, and `xstyle` MUST reach and compose on the status-region root. | `architecture:public-component-api` INV5–INV8 | verify |
|
|
99
|
+
| FR6 | Compact layout MUST reduce component spacing and stack the actions slot while preserving content order and root semantics. | Current implementation, tests, and consumer docs | verify |
|
|
100
|
+
|
|
101
|
+
### Allowed variation
|
|
102
|
+
|
|
103
|
+
- **AV1 — Caller content.** Message wording, decorative artwork, and action
|
|
104
|
+
composition may vary at the product callsite.
|
|
105
|
+
- **AV2 — Theme paint.** Theme-controlled paint and typography may vary through
|
|
106
|
+
the documented targets while anatomy and content order remain stable.
|
|
107
|
+
|
|
108
|
+
### Representative states
|
|
109
|
+
|
|
110
|
+
| State | Required invariant | Allowed variation |
|
|
111
|
+
| ----------------------------- | --------------------------------------------------------- | -------------------------------------- |
|
|
112
|
+
| title only | status root and selected heading level remain present | message wording |
|
|
113
|
+
| description | supporting text follows the title | text length and wrapping |
|
|
114
|
+
| decorative icon | visual precedes text and is hidden from assistive tech | artwork supplied by the caller |
|
|
115
|
+
| actions | caller controls follow the text group | one or more caller-owned controls |
|
|
116
|
+
| compact | reduced spacing and vertical actions preserve semantics | theme paint and caller content |
|
|
117
|
+
| shallow-empty optional scalar | nullish, boolean, and empty-string values emit no wrapper | which optional slot receives the value |
|
|
118
|
+
|
|
119
|
+
### Transformation and precedence order
|
|
120
|
+
|
|
121
|
+
- **ORD1 — Root composition.** Component semantics and theme metadata combine
|
|
122
|
+
with component styles, then consumer `xstyle`, `className`, and `style` under
|
|
123
|
+
the shared merge contract; component-owned `role="status"` remains authoritative.
|
|
124
|
+
|
|
125
|
+
### Performance and resources
|
|
126
|
+
|
|
127
|
+
- **PR1 — Render-only surface.** EmptyState owns no retained state, listener,
|
|
128
|
+
observer, timer, or external resource.
|
|
129
|
+
|
|
130
|
+
## Accessibility contract
|
|
131
|
+
|
|
132
|
+
- **AR1 — Heading semantics.** The title is a heading at the caller-selected level.
|
|
133
|
+
- **AR2 — Decorative visual.** The icon slot is hidden from assistive technology;
|
|
134
|
+
callers keep all necessary meaning in the title or description.
|
|
135
|
+
- **AR3 — Status exposure.** The root exposes the empty message as a status region
|
|
136
|
+
without moving focus; action controls keep their own names, roles, and behavior.
|
|
137
|
+
|
|
138
|
+
## Design relationships
|
|
139
|
+
|
|
140
|
+
No current design record governs EmptyState-specific representation. Visual
|
|
141
|
+
judgment remains evidence, not product authority.
|
|
142
|
+
|
|
143
|
+
### Theming anatomy
|
|
144
|
+
|
|
145
|
+
<!-- anatomy-theming:v1 -->
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"Container": {"target": "empty-state"},
|
|
150
|
+
"Icon": {
|
|
151
|
+
"none": {
|
|
152
|
+
"reason": "intentional: caller-supplied decorative content keeps its own styling and has no component-owned paint target"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"Title": {"target": "empty-state-title"},
|
|
156
|
+
"Description": {"target": "empty-state-description"},
|
|
157
|
+
"Actions": {
|
|
158
|
+
"none": {
|
|
159
|
+
"reason": "intentional: caller-supplied controls keep their own component targets and behavior"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Family and system relationships
|
|
166
|
+
|
|
167
|
+
No current family record owns EmptyState-specific behavior.
|
|
168
|
+
|
|
169
|
+
## Verification map
|
|
170
|
+
|
|
171
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
172
|
+
| -------------- | ------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------- | ------------------------- |
|
|
173
|
+
| FR1–FR4, AR1–3 | focused DOM tests plus required exact-head accessibility evidence | title only, all slots, absent/shallow-empty slots | order, heading, status, or decorative treatment regresses | `audit:EmptyState/a11y` |
|
|
174
|
+
| FR5, ORD1 | focused root composition tests plus shared API guards | DOM/data/event/style inputs, ref, root target | accepted input is dropped or component semantics/styles are replaced | `audit:EmptyState/api` |
|
|
175
|
+
| FR6 | compact target/DOM tests plus required exact-head rendered evidence | default and compact, wide and narrow | compact state, spacing, or action direction no longer applies | `audit:EmptyState/layout` |
|
|
176
|
+
| PR1 | source review | every render path | a retained resource appears without a named owner | `audit:EmptyState/code` |
|
|
177
|
+
|
|
178
|
+
## Decision log
|
|
179
|
+
|
|
180
|
+
No component-local decision is approved in this draft.
|
|
181
|
+
|
|
182
|
+
## Open questions
|
|
183
|
+
|
|
184
|
+
None.
|
|
185
|
+
|
|
186
|
+
## Content boundary
|
|
187
|
+
|
|
188
|
+
This file does not duplicate consumer prop tables/examples, current audit
|
|
189
|
+
results, implementation steps, or family/system rules. It links to their owners.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {render, screen} from '@testing-library/react';
|
|
3
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
4
|
+
import {fireEvent, render, screen} from '@testing-library/react';
|
|
5
|
+
import * as stylex from '@stylexjs/stylex';
|
|
5
6
|
import {EmptyState} from './EmptyState';
|
|
6
7
|
import {defineTheme} from '../theme/defineTheme';
|
|
7
8
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
@@ -11,6 +12,12 @@ function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
|
11
12
|
return [prose, component].filter(Boolean).join('\n\n');
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const testStyles = stylex.create({
|
|
16
|
+
root: {
|
|
17
|
+
minWidth: '123px',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
14
21
|
describe('EmptyState', () => {
|
|
15
22
|
it('renders with title', () => {
|
|
16
23
|
render(<EmptyState title="No results found" />);
|
|
@@ -52,7 +59,7 @@ describe('EmptyState', () => {
|
|
|
52
59
|
/>,
|
|
53
60
|
);
|
|
54
61
|
expect(screen.getByText('Try adjusting your search.')).toBeInTheDocument();
|
|
55
|
-
// Description
|
|
62
|
+
// Description keeps its stable target element instead of paragraph semantics.
|
|
56
63
|
expect(screen.getByText('Try adjusting your search.').tagName).toBe('DIV');
|
|
57
64
|
});
|
|
58
65
|
|
|
@@ -132,6 +139,70 @@ describe('EmptyState', () => {
|
|
|
132
139
|
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
|
133
140
|
});
|
|
134
141
|
|
|
142
|
+
it('does not render empty optional slot wrappers', () => {
|
|
143
|
+
const {container} = render(
|
|
144
|
+
<EmptyState title="No results" description="" icon={false} actions="" />,
|
|
145
|
+
);
|
|
146
|
+
const root = screen.getByRole('status');
|
|
147
|
+
expect(root).toHaveTextContent('No results');
|
|
148
|
+
expect(root).toHaveTextContent(/^No results$/);
|
|
149
|
+
expect(root.children).toHaveLength(1);
|
|
150
|
+
expect(container.querySelector('[aria-hidden="true"]')).toBeNull();
|
|
151
|
+
expect(
|
|
152
|
+
container.querySelector('.astryx-empty-state-description'),
|
|
153
|
+
).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('composes root props and keeps component semantics authoritative', () => {
|
|
157
|
+
const onClick = vi.fn();
|
|
158
|
+
const expectedXstyleClass = stylex.props(testStyles.root).className;
|
|
159
|
+
if (expectedXstyleClass == null) {
|
|
160
|
+
throw new Error('Expected the test xstyle to produce a class name.');
|
|
161
|
+
}
|
|
162
|
+
render(
|
|
163
|
+
<EmptyState
|
|
164
|
+
title="No results"
|
|
165
|
+
role="region"
|
|
166
|
+
data-owner="consumer"
|
|
167
|
+
className="consumer-class"
|
|
168
|
+
style={{width: '321px'}}
|
|
169
|
+
xstyle={testStyles.root}
|
|
170
|
+
onClick={onClick}
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const root = screen.getByRole('status');
|
|
175
|
+
expect(root).toHaveClass('astryx-empty-state', 'consumer-class');
|
|
176
|
+
expect(root).toHaveClass(expectedXstyleClass);
|
|
177
|
+
expect(root).toHaveAttribute('data-owner', 'consumer');
|
|
178
|
+
expect(root).toHaveStyle({width: '321px'});
|
|
179
|
+
fireEvent.click(root);
|
|
180
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('reflects compact layout on the root and preserves slot order', () => {
|
|
184
|
+
render(
|
|
185
|
+
<EmptyState
|
|
186
|
+
title="No results"
|
|
187
|
+
description="Try again."
|
|
188
|
+
icon={<span data-testid="icon">Icon</span>}
|
|
189
|
+
actions={<button type="button">Retry</button>}
|
|
190
|
+
isCompact
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const root = screen.getByRole('status');
|
|
195
|
+
expect(root).toHaveAttribute('data-variant', 'compact');
|
|
196
|
+
expect(root.children).toHaveLength(3);
|
|
197
|
+
expect(root.children[0]).toContainElement(screen.getByTestId('icon'));
|
|
198
|
+
expect(root.children[1]).toContainElement(
|
|
199
|
+
screen.getByRole('heading', {name: 'No results'}),
|
|
200
|
+
);
|
|
201
|
+
expect(root.children[2]).toContainElement(
|
|
202
|
+
screen.getByRole('button', {name: 'Retry'}),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
135
206
|
it('renders all slots together', () => {
|
|
136
207
|
render(
|
|
137
208
|
<EmptyState
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
typeScaleVars,
|
|
24
24
|
} from '../theme/tokens.stylex';
|
|
25
25
|
import type {BaseProps} from '../BaseProps';
|
|
26
|
-
import {mergeProps} from '../utils';
|
|
26
|
+
import {isRenderable, mergeProps} from '../utils';
|
|
27
27
|
import {themeProps} from '../utils/themeProps';
|
|
28
28
|
|
|
29
29
|
const styles = stylex.create({
|
|
@@ -169,7 +169,7 @@ export function EmptyState({
|
|
|
169
169
|
className,
|
|
170
170
|
style,
|
|
171
171
|
)}>
|
|
172
|
-
{icon
|
|
172
|
+
{isRenderable(icon) && <div aria-hidden="true">{icon}</div>}
|
|
173
173
|
<div {...stylex.props(styles.textGroup)}>
|
|
174
174
|
{createElement(
|
|
175
175
|
HeadingTag,
|
|
@@ -181,11 +181,7 @@ export function EmptyState({
|
|
|
181
181
|
),
|
|
182
182
|
title,
|
|
183
183
|
)}
|
|
184
|
-
{description
|
|
185
|
-
// Rendered as <div> (not <p>): description accepts ReactNode and a
|
|
186
|
-
// <p> cannot legally contain block children, which causes hydration
|
|
187
|
-
// mismatches. The StyleX style sets margin: 0, so appearance is
|
|
188
|
-
// unchanged.
|
|
184
|
+
{isRenderable(description) && (
|
|
189
185
|
<div
|
|
190
186
|
{...mergeProps(
|
|
191
187
|
themeProps('empty-state-description', {
|
|
@@ -200,7 +196,7 @@ export function EmptyState({
|
|
|
200
196
|
</div>
|
|
201
197
|
)}
|
|
202
198
|
</div>
|
|
203
|
-
{actions
|
|
199
|
+
{isRenderable(actions) && (
|
|
204
200
|
<div
|
|
205
201
|
{...stylex.props(styles.actions, isCompact && styles.actionsCompact)}>
|
|
206
202
|
{actions}
|
package/src/Field/Field.doc.mjs
CHANGED
|
@@ -114,7 +114,7 @@ export const docs = {
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
name: 'labelIcon',
|
|
117
|
-
type: 'IconType',
|
|
117
|
+
type: 'ReactNode | IconType',
|
|
118
118
|
description: 'Icon to display before the label text. See `astryx docs icons` for valid semantic names.',
|
|
119
119
|
},
|
|
120
120
|
{
|
|
@@ -129,8 +129,8 @@ export const docs = {
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
name: 'statusVariant',
|
|
132
|
-
type: "'attached' | 'detached'",
|
|
133
|
-
description: 'How
|
|
132
|
+
type: "'attached' | 'detached' | 'tooltip'",
|
|
133
|
+
description: 'How status feedback is placed. Attached overlaps an eligible opaque, bordered, fixed-height control; detached renders a message below custom or non-bordered controls; tooltip leaves the visible affordance to the wrapped input and announces its message without a FieldStatus box.',
|
|
134
134
|
default: "'attached'",
|
|
135
135
|
},
|
|
136
136
|
{
|
|
@@ -170,17 +170,23 @@ export const docs = {
|
|
|
170
170
|
{ guidance: true, description: 'Always provide a label for accessibility, even if visually hidden with isLabelHidden.' },
|
|
171
171
|
{ guidance: true, description: 'Use inputID and descriptionID to connect the label and description to the inner control with htmlFor and aria-describedby.' },
|
|
172
172
|
{ guidance: false, description: 'Nest Field around styled inputs such as TextInput, Typeahead, Select, DateInput, or TextArea; those components already render their own Field shell.' },
|
|
173
|
-
{ guidance:
|
|
173
|
+
{ guidance: true, description: 'Use attached status only when the wrapped control is opaque, bordered, and fixed-height; use detached for custom, tall, translucent, or non-bordered controls.' },
|
|
174
174
|
{ guidance: false, description: 'Set both isOptional and isRequired on the same field.' },
|
|
175
175
|
{ guidance: false, description: 'Hide the label without providing an alternative way for the user to understand the field purpose.' },
|
|
176
176
|
],
|
|
177
177
|
anatomy: [
|
|
178
|
+
{name: 'Container', required: true, description: 'Root field box that owns layout, width, and local stacking for the label, control, and status.'},
|
|
178
179
|
{name: 'Label', required: true, description: 'Text identifying the field. Always rendered for accessibility, optionally hidden visually.'},
|
|
179
|
-
{name: '
|
|
180
|
+
{name: 'Ordinary description', required: false, description: 'Helper text under the label in the ordinary field layout.'},
|
|
181
|
+
{name: 'Horizontal-label description', required: false, description: 'Helper text in the control column when FormLayout owns horizontal labels.'},
|
|
180
182
|
{name: 'Control slot', required: true, description: 'A custom, native, or third-party control that does not already render a field shell.'},
|
|
181
183
|
{name: 'Status message', required: false, description: 'Inline validation feedback showing error, warning, or success with a message.'},
|
|
184
|
+
{name: 'Input status icon', required: false, description: 'Shared status affordance used by Field-composed input controls.'},
|
|
185
|
+
{name: 'Input clear button', required: false, description: 'Shared clear action used by Field-composed input controls.'},
|
|
186
|
+
{name: 'Input clear icon', required: false, description: 'Glyph inside the shared input clear action.'},
|
|
182
187
|
{name: 'Optional/Required indicator', required: false, description: 'Badge next to the label showing whether the field is optional or required.'},
|
|
183
|
-
{name: 'Label tooltip', required: false, description: 'Info icon
|
|
188
|
+
{name: 'Label tooltip trigger', required: false, description: 'Info icon beside the label that opens explanatory content.'},
|
|
189
|
+
{name: 'Label tooltip surface', required: false, description: 'Layer-hosted explanatory content for the label tooltip.'},
|
|
184
190
|
],
|
|
185
191
|
},
|
|
186
192
|
examples: [
|
|
@@ -279,7 +285,7 @@ export const docsZh = {
|
|
|
279
285
|
{ guidance: true, description: 'Always provide a label for accessibility, even if visually hidden with isLabelHidden.' },
|
|
280
286
|
{ guidance: true, description: 'Use inputID and descriptionID to connect the label and description to the inner control with htmlFor and aria-describedby.' },
|
|
281
287
|
{ guidance: false, description: 'Nest Field around styled inputs such as TextInput, Typeahead, Select, DateInput, or TextArea; those components already render their own Field shell.' },
|
|
282
|
-
{ guidance:
|
|
288
|
+
{ guidance: true, description: 'Use attached status only when the wrapped control is opaque, bordered, and fixed-height; use detached for custom, tall, translucent, or non-bordered controls.' },
|
|
283
289
|
{ guidance: false, description: 'Set both isOptional and isRequired on the same field.' },
|
|
284
290
|
{ guidance: false, description: 'Hide the label without providing an alternative way for the user to understand the field purpose.' },
|
|
285
291
|
],
|
|
@@ -297,7 +303,7 @@ export const docsDense = {
|
|
|
297
303
|
{ guidance: true, description: 'Always provide a label; visually hide it only when context is clear.' },
|
|
298
304
|
{ guidance: true, description: 'Wire inputID/descriptionID to htmlFor and aria-describedby on the inner control.' },
|
|
299
305
|
{ guidance: false, description: 'Nest Field around styled inputs; it double-renders labels and status UI.' },
|
|
300
|
-
{ guidance:
|
|
306
|
+
{ guidance: true, description: 'Use attached status only for opaque, bordered, fixed-height controls; use detached for custom, tall, translucent, or non-bordered controls.' },
|
|
301
307
|
{ guidance: false, description: 'Set both isOptional and isRequired on the same field.' },
|
|
302
308
|
{ guidance: false, description: 'Hide the label without another way to understand field purpose.' },
|
|
303
309
|
],
|