@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,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:Field
|
|
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]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/Field/Field.test.tsx,
|
|
16
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
17
|
+
packages/core/src/docPropLiterals.test.ts,
|
|
18
|
+
packages/core/src/docPropReferences.test.ts,
|
|
19
|
+
]
|
|
20
|
+
modules: []
|
|
21
|
+
families: [family:input-fields]
|
|
22
|
+
design_specs:
|
|
23
|
+
[design:spatial-hierarchy, design:typography-hierarchy, design:system-states]
|
|
24
|
+
architecture:
|
|
25
|
+
[
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
architecture:component-theming-surface,
|
|
28
|
+
architecture:component-test-sufficiency,
|
|
29
|
+
]
|
|
30
|
+
contributing: []
|
|
31
|
+
system_specs:
|
|
32
|
+
[spec:AST-027/FR1, spec:AST-027/FR2, spec:AST-027/FR3, spec:AST-027/FR11]
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Field component contract
|
|
36
|
+
|
|
37
|
+
## Contract at a glance
|
|
38
|
+
|
|
39
|
+
| Area | Contract |
|
|
40
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Public contract | `Field` wraps one caller-owned control with a label, optional description and status, optionality indicators, an explicit width, and root `BaseProps`; it also consumes FormLayout's horizontal-label mode. |
|
|
42
|
+
| Behavior | Single controls use a native label association; groups use a labelled span. Status messages receive stable IDs and use persistent live regions. Horizontal-label mode exposes one protected `data-layout` theme axis. |
|
|
43
|
+
| End-user impact | People can identify a custom control, hear its description and changing status, and keep label/control/status relationships together across ordinary and horizontal form layouts. |
|
|
44
|
+
| Builder impact | Builders provide the control and its ID, wire descriptions/status IDs to that control, choose group semantics when needed, and use a status placement that is safe for the wrapped surface. |
|
|
45
|
+
| Compatibility/readiness | This draft records released behavior. It adds no API or policy. The released default `statusVariant="attached"` for arbitrary children conflicts with the capability boundary in `family:input-fields/FR8` and remains open. |
|
|
46
|
+
| Review checks | Reject a lost label/group association, dropped root props, caller replacement of `data-layout`, duplicate/missing status announcement, escaped local z-index, undocumented public literals, or a claim that all custom controls safely support attached status. |
|
|
47
|
+
| Governing rules | Established public `Field` interface; `family:input-fields` shared ownership and FR8; `architecture:public-component-api/INV5–INV8`; `spec:AST-027/FR1–FR3`; HTML label and ARIA naming/description semantics. |
|
|
48
|
+
|
|
49
|
+
This table is a review projection; the body below is authoritative.
|
|
50
|
+
|
|
51
|
+
## Intent
|
|
52
|
+
|
|
53
|
+
`Field` supplies shared label, description, status, width, and layout chrome for a native, custom, or third-party control that does not already own an Astryx field shell. The caller continues to own the control's value, interaction model, and control-specific accessibility semantics.
|
|
54
|
+
|
|
55
|
+
## Compatibility and migration
|
|
56
|
+
|
|
57
|
+
- Released default preserved: yes; `statusVariant` still defaults to `attached` in the current implementation.
|
|
58
|
+
- Compatibility class: observational only; this draft changes no public prop, default, or behavior.
|
|
59
|
+
- Controlled/uncontrolled behavior: caller-owned and unchanged.
|
|
60
|
+
- Migration decision: none; resolving OQ1 requires a separate compatibility decision.
|
|
61
|
+
|
|
62
|
+
## Ownership boundary
|
|
63
|
+
|
|
64
|
+
**Owns**
|
|
65
|
+
|
|
66
|
+
- The root field box, explicit width, local stacking boundary, and composition order.
|
|
67
|
+
- Label and description placement around the caller-owned control.
|
|
68
|
+
- Single-control versus grouped-label semantics.
|
|
69
|
+
- Status-message placement and persistent live-region announcement.
|
|
70
|
+
- The FormLayout horizontal-label projection.
|
|
71
|
+
|
|
72
|
+
**Does not own / non-goals**
|
|
73
|
+
|
|
74
|
+
- The wrapped control's value, focus model, keyboard behavior, disabled behavior, or event semantics — owned by the caller or wrapped component.
|
|
75
|
+
- Applying `aria-describedby` to the wrapped control — the caller supplies the control and wires the generated or explicit IDs.
|
|
76
|
+
- The visual and semantic contract of `FieldLabel`, `FieldStatus`, `Tooltip`, or caller-owned children — owned by those components.
|
|
77
|
+
- Input-family member APIs and control-box part props — owned by `family:input-fields` and each member.
|
|
78
|
+
|
|
79
|
+
## Public concepts
|
|
80
|
+
|
|
81
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
82
|
+
| ---------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------ | --------------------- | ------------------------- | ------------------------------------- |
|
|
83
|
+
| label mode | single control; group | Chooses a native `<label htmlFor>` or a `<span>` named through `aria-labelledby`. | All layouts. | single control | `component:Field` | released | Type-constrained |
|
|
84
|
+
| label visibility | visible; visually hidden | Hides label and description visually while preserving them for assistive technology. | All layouts. | visible | `component:Field` | released | Type-constrained |
|
|
85
|
+
| optionality marker | none; optional; required | Shows the localized exception to FormLayout's optionality default. | Label presentation. | none | `component:Field` | released | Both set warns; optional wins visibly |
|
|
86
|
+
| status placement | `attached`; `detached`; `tooltip` | Places a status box behind the control, below it, or leaves visible tooltip affordance to input. | Message required for a rendered box; tooltip still announces messages. | `attached` (see OQ1) | `family:input-fields` | released; conformance gap | Type-constrained |
|
|
87
|
+
| width | absent; number; CSS size string | Sizes the complete ordinary field box. | Ordinary box layout; horizontal-label mode uses `display: contents`. | containing layout | `component:Field` | released | Type-constrained |
|
|
88
|
+
| field layout | ordinary box; `horizontal-labels` from FormLayout | Stacks content or projects label and input group into the parent grid. | Resolved from `FormLayoutContext`. | ordinary box | `component:Field` | released | Context-constrained |
|
|
89
|
+
| description/status IDs | explicit; generated from `inputID`; absent | Names stable description and message elements for caller-owned `aria-describedby` wiring. | Generated only when the corresponding text is present. | generated when text is present | `component:Field` | released | Empty text omits output |
|
|
90
|
+
|
|
91
|
+
## Behavioral and layout contract
|
|
92
|
+
|
|
93
|
+
Draft requirements identify their basis so observed code is not mistaken for an intentional decision.
|
|
94
|
+
|
|
95
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
96
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------- |
|
|
97
|
+
| FR1 | A single-control label renders as `<label htmlFor={inputID}>`; a group label renders as a non-label element with `labelID` for caller-owned `aria-labelledby`. | Established public interface; HTML and ARIA semantics | settled observation |
|
|
98
|
+
| FR2 | Visually hidden labels and descriptions remain in the DOM and out of layout flow. | Established public interface and accessibility behavior | settled observation |
|
|
99
|
+
| FR3 | Description and status IDs use explicit values when supplied and otherwise resolve to `${inputID}-desc` and `${inputID}-status` only when text exists. | Established public interface | settled observation |
|
|
100
|
+
| FR4 | Error status messages announce assertively and warning/success messages politely through persistent live regions; tooltip placement renders no `FieldStatus` box but preserves the announcement. | Existing tests; family ownership | settled observation |
|
|
101
|
+
| FR5 | Ordinary layout orders label, control, then optional status. Horizontal-label mode places the label wrapper before one description/control/status group and reflects `data-layout="horizontal-labels"`; caller props cannot replace that owned theme marker. | Established layout interface; `architecture:public-component-api/INV5` | settled observation |
|
|
102
|
+
| FR6 | `width` applies to the ordinary root field box; it does not style the caller-owned control and has no box to target in horizontal-label mode. | Established public interface; `family:input-fields` shared owner | settled observation |
|
|
103
|
+
| FR7 | Root ref, data/ARIA/DOM inputs, events, `xstyle`, class name, and inline style reach or compose on the root without replacing component-owned semantics. | `architecture:public-component-api/INV5–INV8` | settled observation |
|
|
104
|
+
| FR8 | The Field root isolates every local attached-status paint relationship without assigning the root a page-level z-index. | `spec:AST-027/FR1–FR3`, `FR11` | settled observation |
|
|
105
|
+
| FR9 | Attached status is safe only for an opaque, bordered, fixed-height control with reliable size reflection; custom, tall, wrapped, translucent, or non-bordered controls use detached or component-owned placement. | `family:input-fields/FR8`; current default mismatch | human decision for released default |
|
|
106
|
+
|
|
107
|
+
### Allowed variation
|
|
108
|
+
|
|
109
|
+
- **AV1 — Caller-owned control.** Any control markup may occupy the slot; Field does not inspect, clone, or change it.
|
|
110
|
+
- **AV2 — Form arrangement.** FormLayout may choose ordinary or horizontal-label arrangement without changing label, description, status, or announcement ownership.
|
|
111
|
+
- **AV3 — Status surface.** Eligible controls may use attached placement; other controls use detached or a member-owned tooltip presentation.
|
|
112
|
+
|
|
113
|
+
### Representative states
|
|
114
|
+
|
|
115
|
+
| State | Required invariant | Allowed variation |
|
|
116
|
+
| ----------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------- |
|
|
117
|
+
| label only | One associated label precedes the caller-owned control. | Visible or visually hidden. |
|
|
118
|
+
| description | One description element receives the resolved ID. | Caller may use an explicit ID or the generated value. |
|
|
119
|
+
| group | Group label is not a literal `<label>` and is referenceable by ID. | Caller owns the group role and `aria-labelledby`. |
|
|
120
|
+
| status | Message receives resolved ID and is announced once on its urgency channel. | Attached/detached box or tooltip-owned visible affordance. |
|
|
121
|
+
| horizontal labels | Label and input group participate in the parent grid and retain `data-layout`. | Description/status may be absent. |
|
|
122
|
+
| explicit width | Ordinary root receives the requested size and remains shrinkable. | Number or CSS size string. |
|
|
123
|
+
|
|
124
|
+
### Transformation and precedence order
|
|
125
|
+
|
|
126
|
+
- **ORD1 — Root props.** Caller DOM/data/event props are forwarded first; Field-owned visual-state reflection is applied afterward; StyleX, caller class, and caller inline style compose in the documented order.
|
|
127
|
+
- **ORD2 — IDs.** Explicit description/status ID wins; otherwise text presence derives an ID from `inputID`; absent text produces no ID-bearing element.
|
|
128
|
+
- **ORD3 — Optionality.** FormLayout default optionality suppresses the matching visible marker; if both local booleans are true, Field warns and the optional marker wins visibly.
|
|
129
|
+
|
|
130
|
+
### Performance and resources
|
|
131
|
+
|
|
132
|
+
- **PR1 — No per-field global resource.** Field owns no listener, observer, timer, or external store. Status announcement delegates to the shared persistent live-region runtime.
|
|
133
|
+
|
|
134
|
+
## Accessibility contract
|
|
135
|
+
|
|
136
|
+
- **AR1 — Label association.** Single controls receive a native label association; grouped controls use caller-wired `aria-labelledby` and never a misleading literal `<label>`.
|
|
137
|
+
- **AR2 — Description association.** Description text remains separate from the accessible name and exposes a stable ID for caller-owned `aria-describedby`.
|
|
138
|
+
- **AR3 — Status announcement.** Changing status text is announced through the persistent live region, including tooltip placement where no message box renders.
|
|
139
|
+
- **AR4 — Hidden labels.** Visually hidden label and description text remain available to assistive technology.
|
|
140
|
+
|
|
141
|
+
## Design relationships
|
|
142
|
+
|
|
143
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
144
|
+
| ----------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | ------------------------- | ------------------ |
|
|
145
|
+
| Label and description | `design:typography-hierarchy/DR4`, `DR6` | prescribed roles; exact tuning remains token-owned | label and supporting text | FR1–FR3 |
|
|
146
|
+
| Label/control/status grouping | `design:spatial-hierarchy` local and group relationships | prescribed relationship; exact gap remains token-owned | one field group | FR5 |
|
|
147
|
+
| Semantic status | `design:system-states/DR2` | delegated to FieldStatus and wrapped input | supporting feedback | FR4, FR9 |
|
|
148
|
+
|
|
149
|
+
### Theming anatomy
|
|
150
|
+
|
|
151
|
+
<!-- anatomy-theming:v1 -->
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"Container": {"target": "field"},
|
|
156
|
+
"Label": {"target": "field-label"},
|
|
157
|
+
"Ordinary description": {
|
|
158
|
+
"none": {
|
|
159
|
+
"reason": "reachability-gap: The ordinary-layout description paints supporting text outside the field-label target and has no current public target of its own."
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"Horizontal-label description": {
|
|
163
|
+
"delegatesTo": {"owner": "component:Text", "target": "text"}
|
|
164
|
+
},
|
|
165
|
+
"Control slot": {
|
|
166
|
+
"none": {
|
|
167
|
+
"reason": "intentional: The wrapped control is caller-owned or another Astryx component and retains its own theming surface."
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"Status message": {"target": "field-status"},
|
|
171
|
+
"Input status icon": {"target": "input-status-icon"},
|
|
172
|
+
"Input clear button": {"target": "input-clear-button"},
|
|
173
|
+
"Input clear icon": {"target": "input-clear-icon"},
|
|
174
|
+
"Optional/Required indicator": {"inherits": "field-label"},
|
|
175
|
+
"Label tooltip trigger": {
|
|
176
|
+
"delegatesTo": {"owner": "component:Icon", "target": "icon"}
|
|
177
|
+
},
|
|
178
|
+
"Label tooltip surface": {
|
|
179
|
+
"delegatesTo": {"owner": "component:Tooltip", "target": "tooltip"}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The ordinary description remains stable consumer anatomy without a Field-owned target. A visible horizontal-label description delegates to `Text/text`; when `isLabelHidden` is true, the same content delegates to `VisuallyHidden` and exposes no painted target. The label tooltip splits its visible `Icon/icon` trigger from its layer-hosted `Tooltip/tooltip` surface.
|
|
185
|
+
|
|
186
|
+
## Family and system relationships
|
|
187
|
+
|
|
188
|
+
- `family:input-fields` owns shared status-placement capability and identifies Field as the standalone label, description, status-placement, and explicit-width owner.
|
|
189
|
+
- `architecture:public-component-api` owns root prop, style, event, and ref composition.
|
|
190
|
+
- `architecture:component-theming-surface` owns anatomy qualification and target disposition.
|
|
191
|
+
- `architecture:component-test-sufficiency` owns mutation-sensitive evidence and bounded partitions.
|
|
192
|
+
- `spec:AST-027` owns the isolated local paint relationship between the control and attached status.
|
|
193
|
+
|
|
194
|
+
## Verification map
|
|
195
|
+
|
|
196
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
197
|
+
| ------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
|
|
198
|
+
| FR1–FR3, AR1–AR2, AR4 | `Field.test.tsx` DOM/accessibility tests | single control, group, visible/hidden label, explicit/generated IDs | A lost association, wrong element, missing hidden text, or wrong ID fails. | `audit:Field/accessibility` |
|
|
199
|
+
| FR4, AR3 | `Field.test.tsx` announcement tests plus exact-head browser accessibility evidence | first mount, update, repeat, error, warning, attached, detached, tooltip | A missing, duplicate, stale, or wrong-urgency announcement fails. | `audit:Field/accessibility` |
|
|
200
|
+
| FR5–FR7, ORD1 | `Field.test.tsx`, BaseProps lint, theming-target tests, and exact-head visual evidence | ordinary/horizontal layout; class/style/xstyle/data/event/ref; description/status | Dropped caller input, replaceable layout marker, wrong order, or lost target fails. | `audit:Field/api` |
|
|
201
|
+
| FR8 | computed-style tests and exact-head browser rendering | attached, detached, tooltip, no status | Removing isolation or ranking the root outside its parent fails. | `audit:Field/code-health` |
|
|
202
|
+
| FR9 | family contract review and component-owned visual evidence | eligible bordered control; custom/tall/non-bordered control | Attached placement on an ineligible surface remains visible as the current conformance gap. | `audit:Field/behavior` |
|
|
203
|
+
| Theming anatomy | knowledge validation and theming-target tests | Field root, label, description, caller control, status, shared input affordances, indicator, tooltip | Missing, duplicate, or wrongly owned dispositions fail. | `audit:Field/theming` |
|
|
204
|
+
| Documentation projection | docs typecheck and prop literal/reference tests | labelIcon; all status placements; anatomy and usage guidance | Docs omit or misstate a public type/literal/owner and validation fails or review rejects it. | `audit:Field/docs` |
|
|
205
|
+
| RTL applicability | exact-head RTL audit and verified-N/A registry | all checked-in Field stories and source | A directional glyph, physical inline placement, order, scroll/drag, overlay, or directional keyboard behavior makes N/A stale. | `audit:Field/i18n-rtl` |
|
|
206
|
+
|
|
207
|
+
## Decision log
|
|
208
|
+
|
|
209
|
+
No component-local decision is asserted. This draft records released behavior and leaves the current family-owned status-placement conflict in OQ1.
|
|
210
|
+
|
|
211
|
+
## Open questions
|
|
212
|
+
|
|
213
|
+
- **OQ1 — How should Field migrate the released `attached` default so arbitrary custom controls cannot receive a placement that `family:input-fields/FR8` says they may not safely support?** (`human-api`)
|
|
214
|
+
|
|
215
|
+
## Content boundary
|
|
216
|
+
|
|
217
|
+
This draft records verified shipped behavior and the current family-owned conformance gap. It does not approve a new default, migration, theme target, or compatibility promise, and it does not duplicate audit scores or run evidence.
|
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>
|
package/src/Grid/GridSpan.tsx
CHANGED
|
@@ -47,6 +47,9 @@ const baseStyles = stylex.create({
|
|
|
47
47
|
minWidth: 0, // Prevent overflow in grid
|
|
48
48
|
// Make span fill grid cell and stretch children
|
|
49
49
|
display: 'grid',
|
|
50
|
+
// The implicit `auto` column would grow to a child's min-content width;
|
|
51
|
+
// a zero minimum keeps it inside the span.
|
|
52
|
+
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
50
53
|
height: '100%',
|
|
51
54
|
},
|
|
52
55
|
});
|
|
@@ -269,3 +269,60 @@ describe('Heading', () => {
|
|
|
269
269
|
expect(element).not.toHaveAttribute('data-type', 'editorial');
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
// With hasTruncateTooltip={false} nothing reads the measured overflow, so a
|
|
274
|
+
// long table of truncated cells must not pay a layout read per cell.
|
|
275
|
+
function countScrollWidthReads(run: () => void): number {
|
|
276
|
+
const proto = window.HTMLElement.prototype;
|
|
277
|
+
const original = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
|
|
278
|
+
let reads = 0;
|
|
279
|
+
Object.defineProperty(proto, 'scrollWidth', {
|
|
280
|
+
configurable: true,
|
|
281
|
+
get() {
|
|
282
|
+
reads += 1;
|
|
283
|
+
return 0;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
run();
|
|
288
|
+
} finally {
|
|
289
|
+
if (original) {
|
|
290
|
+
Object.defineProperty(proto, 'scrollWidth', original);
|
|
291
|
+
} else {
|
|
292
|
+
delete (proto as {scrollWidth?: number}).scrollWidth;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return reads;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
describe('Heading maxLines without a truncation tooltip', () => {
|
|
299
|
+
it('takes no truncation measurement when hasTruncateTooltip is false', () => {
|
|
300
|
+
const reads = countScrollWidthReads(() => {
|
|
301
|
+
render(
|
|
302
|
+
<>
|
|
303
|
+
{Array.from({length: 20}, (_, index) => (
|
|
304
|
+
<Heading
|
|
305
|
+
level={3}
|
|
306
|
+
maxLines={1}
|
|
307
|
+
hasTruncateTooltip={false}
|
|
308
|
+
key={index}>
|
|
309
|
+
Cell {index} with text far wider than its column
|
|
310
|
+
</Heading>
|
|
311
|
+
))}
|
|
312
|
+
</>,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
expect(reads).toBe(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('still measures when the tooltip is enabled', () => {
|
|
319
|
+
const reads = countScrollWidthReads(() => {
|
|
320
|
+
render(
|
|
321
|
+
<Heading level={3} maxLines={1}>
|
|
322
|
+
Text far wider than its column
|
|
323
|
+
</Heading>,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
expect(reads).toBeGreaterThan(0);
|
|
327
|
+
});
|
|
328
|
+
});
|
package/src/Heading/Heading.tsx
CHANGED
|
@@ -253,8 +253,12 @@ export function Heading({
|
|
|
253
253
|
// the closed built-in StyleX maps with an unknown name.
|
|
254
254
|
const builtinType = type && isBuiltinHeadingType(type) ? type : undefined;
|
|
255
255
|
|
|
256
|
-
// Truncation detection
|
|
257
|
-
|
|
256
|
+
// Truncation detection. Only the tooltip reads the measured overflow, so
|
|
257
|
+
// with the tooltip off the text is clipped by CSS alone and nothing is
|
|
258
|
+
// measured or observed.
|
|
259
|
+
const truncation = useTruncation({
|
|
260
|
+
maxLines: hasTruncateTooltip === false ? 0 : maxLines,
|
|
261
|
+
});
|
|
258
262
|
|
|
259
263
|
// Tooltip for truncated text
|
|
260
264
|
const tooltipPlacement: LayerPlacement =
|
|
@@ -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', () => {
|