@astryxdesign/core 0.1.8 → 0.1.9
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/CHANGELOG.md +100 -0
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +6 -2
- package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
- package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
- package/dist/Avatar/Avatar.d.ts +18 -2
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +63 -7
- package/dist/Avatar/AvatarStatusDot.d.ts +33 -5
- package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
- package/dist/Avatar/AvatarStatusDot.js +144 -19
- package/dist/Banner/Banner.d.ts +9 -1
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +31 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +31 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +208 -6
- package/dist/Breadcrumbs/index.d.ts +5 -0
- package/dist/Breadcrumbs/index.d.ts.map +1 -1
- package/dist/Breadcrumbs/index.js +9 -1
- package/dist/Button/Button.d.ts +8 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +31 -2
- package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/ButtonGroup/ButtonGroup.js +30 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +30 -2
- package/dist/Calendar/dayCellUtils.js +1 -1
- package/dist/Card/Card.d.ts +8 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +28 -2
- package/dist/Carousel/Carousel.d.ts +4 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +24 -4
- package/dist/Chat/ChatComposer.d.ts +8 -0
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +46 -7
- package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
- package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
- package/dist/Chat/ChatComposerDrawer.js +11 -1
- package/dist/Chat/ChatComposerInput.d.ts +20 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +35 -3
- package/dist/Chat/ChatContext.d.ts +33 -0
- package/dist/Chat/ChatContext.d.ts.map +1 -1
- package/dist/Chat/ChatContext.js +16 -2
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +1 -1
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +4 -4
- package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
- package/dist/Chat/ChatSendButton.d.ts.map +1 -1
- package/dist/Chat/ChatSendButton.js +7 -1
- package/dist/Chat/ChatToolCalls.d.ts +5 -1
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +18 -9
- package/dist/Chat/index.d.ts +2 -1
- package/dist/Chat/index.d.ts.map +1 -1
- package/dist/Chat/index.js +1 -1
- package/dist/Chat/useChatStreamScroll.d.ts +2 -1
- package/dist/Chat/useChatStreamScroll.d.ts.map +1 -1
- package/dist/Chat/useChatStreamScroll.js +22 -8
- package/dist/Chat/useSpeechRecognition.d.ts +0 -1
- package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
- package/dist/Chat/useSpeechRecognition.js +1 -1
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +1 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts +20 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +11 -1
- package/dist/ClickableCard/ClickableCard.d.ts +9 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +5 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +8 -1
- package/dist/CommandPalette/CommandPalette.d.ts +1 -1
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +3 -1
- package/dist/CommandPalette/CommandPaletteGroup.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteGroup.js +9 -2
- package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +6 -1
- package/dist/DateInput/DateInput.d.ts +47 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +26 -2
- package/dist/DateInput/index.d.ts +1 -1
- package/dist/DateInput/index.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +10 -1
- package/dist/Dialog/Dialog.d.ts +3 -0
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +57 -6
- package/dist/Dialog/DialogContext.d.ts +7 -0
- package/dist/Dialog/DialogContext.d.ts.map +1 -1
- package/dist/Dialog/DialogContext.js +2 -2
- package/dist/Dialog/DialogHeader.d.ts +5 -2
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +6 -1
- package/dist/Divider/Divider.js +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -2
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +1 -2
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -2
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +2 -3
- package/dist/Field/FieldLabel.d.ts +1 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +42 -15
- package/dist/FieldStatus/FieldStatus.d.ts +8 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +22 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +10 -2
- package/dist/HoverCard/HoverCard.d.ts +9 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +2 -0
- package/dist/HoverCard/useHoverCard.d.ts +8 -0
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +7 -2
- package/dist/Icon/Icon.d.ts +7 -2
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Item/Item.d.ts +6 -1
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +22 -1
- package/dist/Layer/useLayer.d.ts +6 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +2 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +6 -1
- package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
- package/dist/MobileNav/MobileNavToggle.js +4 -0
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +39 -12
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +16 -0
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/Outline/Outline.d.ts +0 -1
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +0 -1
- package/dist/OverflowList/OverflowList.d.ts +20 -3
- package/dist/OverflowList/OverflowList.d.ts.map +1 -1
- package/dist/OverflowList/OverflowList.js +29 -4
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +3 -9
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.d.ts +9 -2
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +16 -13
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +4 -2
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +16 -0
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +21 -8
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +19 -2
- package/dist/Stack/stack.stylex.d.ts +9 -9
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +3 -2
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
- package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/index.js +3 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts +13 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +4 -1
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +16 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +87 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +6 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeState.js +22 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +5 -3
- package/dist/Thumbnail/Thumbnail.d.ts +26 -5
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +97 -54
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +9 -0
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +1 -0
- package/dist/Thumbnail/thumbnail.markers.stylex.js +14 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +15 -4
- package/dist/Timestamp/Timestamp.d.ts +3 -1
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +8 -6
- package/dist/Toast/useToast.d.ts.map +1 -1
- package/dist/Toast/useToast.js +2 -5
- package/dist/ToggleButton/ToggleButtonGroup.d.ts +0 -1
- package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButtonGroup.js +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +16 -4
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +18 -8
- package/dist/TopNav/TopNavMegaMenu.js +10 -4
- package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts +9 -1
- package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuFeaturedCard.js +8 -1
- package/dist/TopNav/TopNavMenu.d.ts +2 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +68 -6
- package/dist/TreeList/TreeList.d.ts +12 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +2 -0
- package/dist/TreeList/TreeListBranches.d.ts +5 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +10 -4
- package/dist/TreeList/TreeListItem.d.ts +8 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +27 -7
- package/dist/TreeList/TreeListTypes.d.ts +25 -1
- package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
- package/dist/TreeList/index.d.ts +2 -2
- package/dist/TreeList/index.d.ts.map +1 -1
- package/dist/astryx.css +28 -18
- package/dist/astryx.umd.js +51 -51
- package/dist/astryx.umd.js.map +4 -4
- package/dist/authoring/doc.d.ts.map +1 -1
- package/dist/docs-types.d.ts +8 -0
- package/dist/docs-types.d.ts.map +1 -1
- package/dist/hooks/computeOverflow.d.ts +47 -0
- package/dist/hooks/computeOverflow.d.ts.map +1 -0
- package/dist/hooks/computeOverflow.js +213 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useDevWarning.d.ts +18 -0
- package/dist/hooks/useDevWarning.d.ts.map +1 -0
- package/dist/hooks/useDevWarning.js +49 -0
- package/dist/hooks/useOverflow.d.ts +19 -0
- package/dist/hooks/useOverflow.d.ts.map +1 -1
- package/dist/hooks/useOverflow.js +36 -25
- package/dist/i18n/resolve.d.ts.map +1 -1
- package/dist/i18n/resolve.js +7 -18
- package/dist/theme/Theme.d.ts.map +1 -1
- package/dist/theme/Theme.js +2 -7
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +51 -0
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +2 -1
- package/dist/utils/devWarning.d.ts +42 -0
- package/dist/utils/devWarning.d.ts.map +1 -0
- package/dist/utils/devWarning.js +88 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -2
- package/dist/utils/plainDate.d.ts +30 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +53 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/types.d.ts.map +1 -1
- package/locales/en.json +24 -0
- package/locales/fr-FR.json +14 -0
- package/locales/pseudo.json +18 -0
- package/package.json +1 -1
- package/src/AppShell/AppShell.tsx +6 -0
- package/src/AppShell/AppShellMobileContext.tsx +8 -0
- package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
- package/src/Avatar/Avatar.doc.mjs +15 -3
- package/src/Avatar/Avatar.test.tsx +156 -1
- package/src/Avatar/Avatar.tsx +96 -4
- package/src/Avatar/AvatarStatusDot.doc.mjs +23 -12
- package/src/Avatar/AvatarStatusDot.test.tsx +284 -0
- package/src/Avatar/AvatarStatusDot.tsx +178 -21
- package/src/Banner/Banner.doc.mjs +9 -0
- package/src/Banner/Banner.test.tsx +30 -0
- package/src/Banner/Banner.tsx +47 -3
- package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +26 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +333 -14
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -0
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +284 -2
- package/src/Breadcrumbs/index.ts +27 -0
- package/src/Button/Button.doc.mjs +9 -0
- package/src/Button/Button.test.tsx +49 -0
- package/src/Button/Button.tsx +25 -1
- package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
- package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
- package/src/ButtonGroup/ButtonGroup.tsx +32 -0
- package/src/Calendar/Calendar.doc.mjs +12 -1
- package/src/Calendar/Calendar.test.tsx +193 -0
- package/src/Calendar/Calendar.tsx +38 -16
- package/src/Calendar/dayCellUtils.test.ts +5 -0
- package/src/Calendar/dayCellUtils.ts +1 -1
- package/src/Card/Card.doc.mjs +9 -0
- package/src/Card/Card.test.tsx +21 -3
- package/src/Card/Card.tsx +35 -2
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +53 -0
- package/src/Carousel/Carousel.tsx +34 -4
- package/src/Chat/ChatComposer.doc.mjs +12 -3
- package/src/Chat/ChatComposer.test.tsx +115 -0
- package/src/Chat/ChatComposer.tsx +73 -9
- package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
- package/src/Chat/ChatComposerDrawer.tsx +11 -1
- package/src/Chat/ChatComposerInput.doc.mjs +7 -0
- package/src/Chat/ChatComposerInput.test.tsx +103 -0
- package/src/Chat/ChatComposerInput.tsx +49 -3
- package/src/Chat/ChatContext.tsx +37 -10
- package/src/Chat/ChatDictationButton.tsx +4 -1
- package/src/Chat/ChatLayout.tsx +3 -1
- package/src/Chat/ChatLayoutScrollButton.tsx +7 -2
- package/src/Chat/ChatMessageMetadata.tsx +1 -2
- package/src/Chat/ChatSendButton.tsx +11 -1
- package/src/Chat/ChatToolCalls.test.tsx +64 -0
- package/src/Chat/ChatToolCalls.tsx +25 -4
- package/src/Chat/index.ts +5 -1
- package/src/Chat/useChatStreamScroll.test.tsx +49 -0
- package/src/Chat/useChatStreamScroll.ts +27 -10
- package/src/Chat/useSpeechRecognition.ts +1 -1
- package/src/Chat/useTriggerMenu.tsx +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
- package/src/CheckboxInput/CheckboxInput.tsx +1 -1
- package/src/CheckboxList/CheckboxList.test.tsx +81 -3
- package/src/CheckboxList/CheckboxListItem.doc.mjs +33 -5
- package/src/CheckboxList/CheckboxListItem.tsx +41 -5
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
- package/src/ClickableCard/ClickableCard.test.tsx +34 -0
- package/src/ClickableCard/ClickableCard.tsx +13 -3
- package/src/CodeBlock/CodeBlock.test.tsx +56 -0
- package/src/CodeBlock/CodeBlock.tsx +9 -1
- package/src/CommandPalette/CommandPalette.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.tsx +3 -1
- package/src/CommandPalette/CommandPaletteGroup.test.tsx +71 -0
- package/src/CommandPalette/CommandPaletteGroup.tsx +12 -1
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
- package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
- package/src/ContextMenu/ContextMenu.test.tsx +193 -0
- package/src/DateInput/DateInput.doc.mjs +16 -0
- package/src/DateInput/DateInput.test.tsx +108 -0
- package/src/DateInput/DateInput.tsx +68 -3
- package/src/DateInput/index.ts +1 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +35 -0
- package/src/DateTimeInput/DateTimeInput.tsx +14 -2
- package/src/Dialog/Dialog.doc.mjs +2 -2
- package/src/Dialog/Dialog.test.tsx +77 -4
- package/src/Dialog/Dialog.tsx +89 -5
- package/src/Dialog/DialogContext.ts +9 -2
- package/src/Dialog/DialogHeader.doc.mjs +2 -2
- package/src/Dialog/DialogHeader.tsx +9 -2
- package/src/Divider/Divider.tsx +2 -2
- package/src/DropdownMenu/DropdownMenu.test.tsx +169 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -2
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +1 -2
- package/src/Field/Field.test.tsx +57 -12
- package/src/Field/Field.tsx +6 -5
- package/src/Field/FieldLabel.tsx +8 -0
- package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
- package/src/FieldStatus/FieldStatus.test.tsx +78 -40
- package/src/FieldStatus/FieldStatus.tsx +22 -4
- package/src/FileInput/FileInput.test.tsx +52 -3
- package/src/FileInput/FileInput.tsx +13 -2
- package/src/HoverCard/HoverCard.doc.mjs +13 -0
- package/src/HoverCard/HoverCard.test.tsx +21 -2
- package/src/HoverCard/HoverCard.tsx +11 -0
- package/src/HoverCard/useHoverCard.doc.mjs +2 -0
- package/src/HoverCard/useHoverCard.tsx +24 -2
- package/src/Icon/Icon.doc.mjs +3 -2
- package/src/Icon/Icon.tsx +7 -2
- package/src/IconButton/IconButton.doc.mjs +8 -0
- package/src/IconButton/IconButton.test.tsx +15 -18
- package/src/Item/Item.test.tsx +40 -18
- package/src/Item/Item.tsx +38 -2
- package/src/Layer/useLayer.tsx +8 -0
- package/src/List/List.test.tsx +9 -8
- package/src/MobileNav/MobileNav.tsx +12 -1
- package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
- package/src/MobileNav/MobileNavToggle.tsx +9 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
- package/src/MultiSelector/MultiSelector.test.tsx +172 -3
- package/src/MultiSelector/MultiSelector.tsx +55 -12
- package/src/MultiSelector/hooks.ts +17 -0
- package/src/NumberInput/NumberInput.test.tsx +11 -7
- package/src/NumberInput/NumberInput.tsx +1 -2
- package/src/Outline/Outline.tsx +0 -1
- package/src/OverflowList/OverflowList.doc.mjs +39 -6
- package/src/OverflowList/OverflowList.test.tsx +111 -0
- package/src/OverflowList/OverflowList.tsx +48 -8
- package/src/Popover/Popover.tsx +7 -4
- package/src/Popover/usePopover.tsx +9 -13
- package/src/PowerSearch/PowerSearch.doc.mjs +14 -0
- package/src/PowerSearch/PowerSearch.test.tsx +20 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/usePowerSearchSource.ts +1 -2
- package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
- package/src/SelectableCard/SelectableCard.test.tsx +46 -4
- package/src/SelectableCard/SelectableCard.tsx +29 -16
- package/src/Selector/Selector.test.tsx +69 -0
- package/src/Selector/Selector.tsx +5 -1
- package/src/Selector/hooks.ts +17 -0
- package/src/SideNav/SideNav.test.tsx +119 -3
- package/src/SideNav/SideNavCollapseButton.tsx +6 -1
- package/src/SideNav/SideNavHeading.tsx +24 -8
- package/src/SideNav/SideNavItem.tsx +5 -1
- package/src/Slider/Slider.test.tsx +75 -2
- package/src/Slider/Slider.tsx +21 -5
- package/src/Switch/Switch.test.tsx +32 -1
- package/src/Table/BaseTable.tsx +3 -5
- package/src/Table/index.ts +5 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
- package/src/Table/plugins/rowStatus/index.ts +7 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +19 -1
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +135 -3
- package/src/Table/plugins/tree/useTableTreeData.tsx +128 -1
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +104 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +46 -2
- package/src/Table/useBaseTablePlugins.ts +10 -6
- package/src/Table/useTableRowStatus.doc.mjs +30 -0
- package/src/Table/useTableSelection.doc.mjs +65 -14
- package/src/Table/useTableTreeData.doc.mjs +29 -0
- package/src/Table/useTableTreeState.doc.mjs +2 -2
- package/src/TextArea/TextArea.test.tsx +9 -1
- package/src/Thumbnail/Thumbnail.doc.mjs +19 -10
- package/src/Thumbnail/Thumbnail.test.tsx +98 -7
- package/src/Thumbnail/Thumbnail.tsx +141 -41
- package/src/Thumbnail/thumbnail.markers.stylex.ts +12 -0
- package/src/TimeInput/TimeInput.test.tsx +76 -1
- package/src/TimeInput/TimeInput.tsx +19 -6
- package/src/Timestamp/Timestamp.doc.mjs +4 -4
- package/src/Timestamp/Timestamp.test.tsx +32 -0
- package/src/Timestamp/Timestamp.tsx +28 -8
- package/src/Toast/useToast.tsx +7 -8
- package/src/ToggleButton/ToggleButtonGroup.tsx +1 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +15 -1
- package/src/Tokenizer/Tokenizer.test.tsx +183 -2
- package/src/Tokenizer/Tokenizer.tsx +16 -4
- package/src/TopNav/TopNav.test.tsx +125 -1
- package/src/TopNav/TopNavHeading.tsx +24 -8
- package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
- package/src/TopNav/TopNavMegaMenu.tsx +10 -4
- package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +3 -3
- package/src/TopNav/TopNavMegaMenuFeaturedCard.tsx +19 -2
- package/src/TopNav/TopNavMenu.test.tsx +118 -4
- package/src/TopNav/TopNavMenu.tsx +83 -5
- package/src/TreeList/TreeList.doc.mjs +22 -0
- package/src/TreeList/TreeList.test.tsx +232 -0
- package/src/TreeList/TreeList.tsx +18 -2
- package/src/TreeList/TreeListBranches.tsx +16 -2
- package/src/TreeList/TreeListItem.tsx +51 -11
- package/src/TreeList/TreeListTypes.ts +27 -1
- package/src/TreeList/index.ts +2 -2
- package/src/authoring/doc.ts +1 -3
- package/src/docs-types.ts +5 -0
- package/src/hooks/computeOverflow.test.ts +422 -0
- package/src/hooks/computeOverflow.ts +290 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useDevWarning.test.tsx +45 -0
- package/src/hooks/useDevWarning.ts +54 -0
- package/src/hooks/useOverflow.test.ts +96 -4
- package/src/hooks/useOverflow.ts +74 -33
- package/src/i18n/InternationalizationProvider.doc.mjs +99 -2
- package/src/i18n/resolve.ts +9 -21
- package/src/theme/Theme.tsx +11 -14
- package/src/theme/generateThemeRules.test.ts +65 -0
- package/src/theme/generateThemeRules.ts +56 -0
- package/src/theme/hct.ts +1 -1
- package/src/theme/syntax/defineSyntaxTheme.ts +5 -6
- package/src/utils/devWarning.test.ts +56 -0
- package/src/utils/devWarning.ts +102 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/plainDate.test.ts +22 -0
- package/src/utils/plainDate.ts +61 -0
- package/src/utils/types.ts +14 -0
package/src/Dialog/Dialog.tsx
CHANGED
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
* - /packages/cli/templates/blocks/components/Dialog/ (showcase blocks)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useId,
|
|
23
|
+
useMemo,
|
|
24
|
+
useRef,
|
|
25
|
+
type ReactNode,
|
|
26
|
+
} from 'react';
|
|
20
27
|
import type {BaseProps} from '../BaseProps';
|
|
21
28
|
import * as stylex from '@stylexjs/stylex';
|
|
22
29
|
import {useScrollLock} from '../hooks/useScrollLock';
|
|
@@ -39,6 +46,7 @@ import {
|
|
|
39
46
|
} from '../Layout/padding.stylex';
|
|
40
47
|
import type {SpacingStep} from '../utils/types';
|
|
41
48
|
import {mergeProps, mergeRefs} from '../utils';
|
|
49
|
+
import {devWarn} from '../utils/devWarning';
|
|
42
50
|
import {DialogContext} from './DialogContext';
|
|
43
51
|
import {themeProps} from '../utils/themeProps';
|
|
44
52
|
|
|
@@ -152,7 +160,13 @@ const styles = stylex.create({
|
|
|
152
160
|
open: {
|
|
153
161
|
display: 'flex',
|
|
154
162
|
opacity: 1,
|
|
155
|
-
|
|
163
|
+
// Disable the entry keyframe animation under
|
|
164
|
+
// `prefers-reduced-motion: reduce` so the dialog appears instantly
|
|
165
|
+
// instead of translating/scaling in (same pattern as layerAnimations).
|
|
166
|
+
animationName: {
|
|
167
|
+
default: enterDirectional,
|
|
168
|
+
'@media (prefers-reduced-motion: reduce)': 'none',
|
|
169
|
+
},
|
|
156
170
|
},
|
|
157
171
|
// Backdrop using ::backdrop pseudo-element
|
|
158
172
|
backdrop: {
|
|
@@ -312,6 +326,9 @@ export interface DialogProps extends BaseProps<HTMLDialogElement> {
|
|
|
312
326
|
*
|
|
313
327
|
* Designed to be used with Layout as its child for structured content.
|
|
314
328
|
* Uses the browser's built-in modal behavior for optimal accessibility.
|
|
329
|
+
* When a DialogHeader is rendered inside, its title automatically names the
|
|
330
|
+
* dialog via aria-labelledby; pass `aria-label` or `aria-labelledby` to
|
|
331
|
+
* override.
|
|
315
332
|
*
|
|
316
333
|
* @example
|
|
317
334
|
* ```
|
|
@@ -348,10 +365,47 @@ export function Dialog({
|
|
|
348
365
|
const paddingToken = spacingStepToToken[effectivePadding] as SpacingToken;
|
|
349
366
|
|
|
350
367
|
const isFullscreen = variant === 'fullscreen';
|
|
351
|
-
|
|
368
|
+
|
|
369
|
+
// Default accessible name: publish a title id through DialogContext so a
|
|
370
|
+
// DialogHeader applies it to its heading (mirrors AlertDialog's explicit
|
|
371
|
+
// useId wiring). Whether to emit the default aria-labelledby is decided
|
|
372
|
+
// imperatively in the callback ref below by checking whether the title
|
|
373
|
+
// element actually rendered — so a dialog with no header never points at a
|
|
374
|
+
// missing id, and we avoid the extra render a registration-via-state effect
|
|
375
|
+
// would cost at the dialog level.
|
|
376
|
+
const titleId = useId();
|
|
377
|
+
|
|
378
|
+
const dialogContextValue = useMemo(
|
|
379
|
+
() => ({isInline, titleId}),
|
|
380
|
+
[isInline, titleId],
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Consumer-provided labels always win over the DialogHeader default.
|
|
384
|
+
const hasConsumerName =
|
|
385
|
+
props['aria-label'] != null || props['aria-labelledby'] != null;
|
|
352
386
|
|
|
353
387
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
354
388
|
|
|
389
|
+
// Callback ref on the <dialog>: sync the default aria-labelledby from the
|
|
390
|
+
// DialogHeader title (if one rendered) in the same commit, with no second
|
|
391
|
+
// render. Consumer aria-label/aria-labelledby win — when present we leave
|
|
392
|
+
// the attribute to the {...safeProps} spread and never touch it here.
|
|
393
|
+
const attachDialog = useCallback(
|
|
394
|
+
(node: HTMLDialogElement | null) => {
|
|
395
|
+
dialogRef.current = node;
|
|
396
|
+
if (!node || hasConsumerName) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const hasTitle = node.querySelector(`#${CSS.escape(titleId)}`) != null;
|
|
400
|
+
if (hasTitle) {
|
|
401
|
+
node.setAttribute('aria-labelledby', titleId);
|
|
402
|
+
} else {
|
|
403
|
+
node.removeAttribute('aria-labelledby');
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
[titleId, hasConsumerName],
|
|
407
|
+
);
|
|
408
|
+
|
|
355
409
|
// Capture the element that was focused when the dialog opened,
|
|
356
410
|
// for directional animation origin and focus restoration on close.
|
|
357
411
|
const triggerElementRef = useRef<HTMLElement | null>(null);
|
|
@@ -440,6 +494,29 @@ export function Dialog({
|
|
|
440
494
|
return () => dialog.removeEventListener('keydown', handleKeyDown);
|
|
441
495
|
}, [isOpen, isInline, allowEscape, onOpenChange]);
|
|
442
496
|
|
|
497
|
+
// Dev-time guardrail: an open modal should always have an accessible name.
|
|
498
|
+
// The header-title check reads the DOM, so this stays in an effect; the ref
|
|
499
|
+
// keeps it to one warning per component instance.
|
|
500
|
+
const warnedUnnamedDialogRef = useRef(false);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
const hasHeaderTitle =
|
|
503
|
+
dialogRef.current?.querySelector(`#${CSS.escape(titleId)}`) != null;
|
|
504
|
+
if (
|
|
505
|
+
isOpen &&
|
|
506
|
+
!isInline &&
|
|
507
|
+
!hasConsumerName &&
|
|
508
|
+
!hasHeaderTitle &&
|
|
509
|
+
!warnedUnnamedDialogRef.current
|
|
510
|
+
) {
|
|
511
|
+
warnedUnnamedDialogRef.current = true;
|
|
512
|
+
devWarn(
|
|
513
|
+
'Dialog',
|
|
514
|
+
'open dialog has no accessible name. Add a DialogHeader ' +
|
|
515
|
+
'with a `title`, or pass `aria-label`/`aria-labelledby`.',
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}, [isOpen, isInline, hasConsumerName, titleId]);
|
|
519
|
+
|
|
443
520
|
// Handle backdrop click — when the user clicks the ::backdrop pseudo-element,
|
|
444
521
|
// the event target is the <dialog> element itself; clicks on child content
|
|
445
522
|
// always target the child. This avoids false positives from native browser
|
|
@@ -531,7 +608,11 @@ export function Dialog({
|
|
|
531
608
|
),
|
|
532
609
|
className,
|
|
533
610
|
style,
|
|
534
|
-
)}
|
|
611
|
+
)}
|
|
612
|
+
data-testid={
|
|
613
|
+
(props as Record<string, unknown>)['data-testid'] as
|
|
614
|
+
string | undefined
|
|
615
|
+
}>
|
|
535
616
|
{innerContent}
|
|
536
617
|
</div>
|
|
537
618
|
);
|
|
@@ -541,7 +622,7 @@ export function Dialog({
|
|
|
541
622
|
|
|
542
623
|
return (
|
|
543
624
|
<dialog
|
|
544
|
-
ref={mergeRefs(ref,
|
|
625
|
+
ref={mergeRefs(ref, attachDialog)}
|
|
545
626
|
{...safeProps}
|
|
546
627
|
{...mergeProps(
|
|
547
628
|
themeProps('dialog', {variant}),
|
|
@@ -566,6 +647,9 @@ export function Dialog({
|
|
|
566
647
|
onClick={handleClick}
|
|
567
648
|
onCancel={handleCancel}
|
|
568
649
|
aria-modal="true"
|
|
650
|
+
// The default aria-labelledby (from a DialogHeader title) is set
|
|
651
|
+
// imperatively in `attachDialog`; a consumer-provided aria-labelledby
|
|
652
|
+
// flows through {...safeProps} above and wins.
|
|
569
653
|
{...(purpose === 'required' ? {role: 'alertdialog'} : undefined)}>
|
|
570
654
|
{innerContent}
|
|
571
655
|
</dialog>
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file DialogContext.ts
|
|
7
7
|
* @input React context
|
|
8
|
-
* @output Internal dialog context for child focus behavior
|
|
9
|
-
* @position Dialog internals; consumed by focus-managing children
|
|
8
|
+
* @output Internal dialog context for child focus behavior and default labelling
|
|
9
|
+
* @position Dialog internals; consumed by focus-managing children and DialogHeader
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {createContext, use} from 'react';
|
|
@@ -14,6 +14,13 @@ import {createContext, use} from 'react';
|
|
|
14
14
|
export interface DialogContextValue {
|
|
15
15
|
/** Whether the dialog is rendered inline for docs/showcases. */
|
|
16
16
|
isInline: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Id the DialogHeader title should render with so the dialog can name
|
|
19
|
+
* itself via aria-labelledby. The dialog detects the title element's
|
|
20
|
+
* presence directly (via a callback ref), so it only emits aria-labelledby
|
|
21
|
+
* when the title actually rendered — never pointing at a nonexistent id.
|
|
22
|
+
*/
|
|
23
|
+
titleId?: string;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export const DialogContext = createContext<DialogContextValue | null>(null);
|
|
@@ -12,7 +12,7 @@ export const docs = {
|
|
|
12
12
|
{
|
|
13
13
|
name: 'title',
|
|
14
14
|
type: 'string',
|
|
15
|
-
description: 'Dialog title (receives focus on open).',
|
|
15
|
+
description: 'Dialog title (receives focus on open and labels the dialog via aria-labelledby).',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: 'subtitle',
|
|
@@ -158,7 +158,7 @@ export const docsDense = {
|
|
|
158
158
|
displayName: 'Dialog Header',
|
|
159
159
|
description: 'dialog header w/ title, optional subtitle, close button, start/end content slots',
|
|
160
160
|
propDescriptions: {
|
|
161
|
-
title: 'dialog title (
|
|
161
|
+
title: 'dialog title (focused on open; labels dialog via aria-labelledby)',
|
|
162
162
|
subtitle: 'subtitle below title',
|
|
163
163
|
onOpenChange: 'close button callback (omit=no button)',
|
|
164
164
|
startContent: 'content before title (e.g. back button)',
|
|
@@ -65,7 +65,9 @@ export interface DialogHeaderProps extends BaseProps<HTMLDivElement> {
|
|
|
65
65
|
ref?: React.Ref<HTMLDivElement>;
|
|
66
66
|
/**
|
|
67
67
|
* The title of the dialog.
|
|
68
|
-
* This title receives focus when the dialog opens for screen reader
|
|
68
|
+
* This title receives focus when the dialog opens for screen reader
|
|
69
|
+
* accessibility, and names the parent Dialog via aria-labelledby unless the
|
|
70
|
+
* consumer passes an explicit aria-label/aria-labelledby to the Dialog.
|
|
69
71
|
*/
|
|
70
72
|
title: string;
|
|
71
73
|
|
|
@@ -105,7 +107,8 @@ export interface DialogHeaderProps extends BaseProps<HTMLDivElement> {
|
|
|
105
107
|
* Renders a title that receives focus when a modal dialog opens (for screen reader accessibility)
|
|
106
108
|
* and an optional close button. Inline documentation previews suppress this autofocus.
|
|
107
109
|
* The title is an h2 element with tabIndex={-1} so it can be programmatically focused but
|
|
108
|
-
* doesn't appear in the tab order.
|
|
110
|
+
* doesn't appear in the tab order. The title also names the parent Dialog via
|
|
111
|
+
* aria-labelledby (unless the Dialog receives an explicit aria-label/aria-labelledby).
|
|
109
112
|
*
|
|
110
113
|
* Uses LayoutHeader internally for consistent styling with other layout headers.
|
|
111
114
|
*
|
|
@@ -137,10 +140,13 @@ export function DialogHeader({
|
|
|
137
140
|
const titleRef = useRef<HTMLHeadingElement>(null);
|
|
138
141
|
const dialogContext = useDialogContext();
|
|
139
142
|
const shouldAutoFocus = dialogContext?.isInline !== true;
|
|
143
|
+
const titleId = dialogContext?.titleId;
|
|
140
144
|
|
|
141
145
|
// Auto-focus the title when mounted for screen reader accessibility.
|
|
142
146
|
// Inline dialogs are documentation/showcase previews, so suppress focus to
|
|
143
147
|
// avoid stealing scroll position from the surrounding page.
|
|
148
|
+
// The parent Dialog detects this title (by `titleId`) via a callback ref to
|
|
149
|
+
// set its default aria-labelledby — no registration handshake needed here.
|
|
144
150
|
useEffect(() => {
|
|
145
151
|
if (shouldAutoFocus && titleRef.current) {
|
|
146
152
|
titleRef.current.focus();
|
|
@@ -162,6 +168,7 @@ export function DialogHeader({
|
|
|
162
168
|
<div {...stylex.props(styles.titleWrapper)}>
|
|
163
169
|
<Heading
|
|
164
170
|
ref={titleRef}
|
|
171
|
+
id={titleId}
|
|
165
172
|
level={2}
|
|
166
173
|
tabIndex={-1}
|
|
167
174
|
xstyle={styles.titleFocusable}>
|
package/src/Divider/Divider.tsx
CHANGED
|
@@ -172,6 +172,7 @@ export function Divider({
|
|
|
172
172
|
return (
|
|
173
173
|
<div
|
|
174
174
|
ref={ref}
|
|
175
|
+
{...props}
|
|
175
176
|
role="separator"
|
|
176
177
|
aria-orientation={orientation}
|
|
177
178
|
{...mergeProps(
|
|
@@ -186,8 +187,7 @@ export function Divider({
|
|
|
186
187
|
),
|
|
187
188
|
className,
|
|
188
189
|
style,
|
|
189
|
-
)}
|
|
190
|
-
{...props}>
|
|
190
|
+
)}>
|
|
191
191
|
<div
|
|
192
192
|
{...stylex.props(
|
|
193
193
|
isHorizontal ? lineStyles.horizontalLine : lineStyles.verticalLine,
|
|
@@ -613,3 +613,172 @@ describe('DropdownMenu compound mode', () => {
|
|
|
613
613
|
).toBeInTheDocument();
|
|
614
614
|
});
|
|
615
615
|
});
|
|
616
|
+
|
|
617
|
+
describe('DropdownMenu keyboard access for menuitemradio/menuitemcheckbox (#3829)', () => {
|
|
618
|
+
it('arrow navigation reaches consumer-rendered menuitemradio and menuitemcheckbox items', async () => {
|
|
619
|
+
const user = userEvent.setup();
|
|
620
|
+
render(
|
|
621
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
622
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
623
|
+
<div role="menuitemradio" tabIndex={-1} aria-checked="false">
|
|
624
|
+
Newest
|
|
625
|
+
</div>
|
|
626
|
+
<div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
|
|
627
|
+
Archived
|
|
628
|
+
</div>
|
|
629
|
+
</DropdownMenu>,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
633
|
+
const menu = screen.getByRole('menu', {hidden: true});
|
|
634
|
+
screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
|
|
635
|
+
|
|
636
|
+
fireEvent.keyDown(menu, {key: 'ArrowDown'});
|
|
637
|
+
expect(
|
|
638
|
+
screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}),
|
|
639
|
+
).toHaveFocus();
|
|
640
|
+
|
|
641
|
+
fireEvent.keyDown(menu, {key: 'ArrowDown'});
|
|
642
|
+
expect(
|
|
643
|
+
screen.getByRole('menuitemcheckbox', {name: 'Archived', hidden: true}),
|
|
644
|
+
).toHaveFocus();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('activates a focused menuitemradio with Enter and a menuitemcheckbox with Space', async () => {
|
|
648
|
+
const user = userEvent.setup();
|
|
649
|
+
const onSelect = vi.fn();
|
|
650
|
+
const onToggle = vi.fn();
|
|
651
|
+
render(
|
|
652
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
653
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
654
|
+
<div
|
|
655
|
+
role="menuitemradio"
|
|
656
|
+
tabIndex={-1}
|
|
657
|
+
aria-checked="false"
|
|
658
|
+
onClick={onSelect}>
|
|
659
|
+
Newest
|
|
660
|
+
</div>
|
|
661
|
+
<div
|
|
662
|
+
role="menuitemcheckbox"
|
|
663
|
+
tabIndex={-1}
|
|
664
|
+
aria-checked="false"
|
|
665
|
+
onClick={onToggle}>
|
|
666
|
+
Archived
|
|
667
|
+
</div>
|
|
668
|
+
</DropdownMenu>,
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
672
|
+
const menu = screen.getByRole('menu', {hidden: true});
|
|
673
|
+
|
|
674
|
+
screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}).focus();
|
|
675
|
+
fireEvent.keyDown(menu, {key: 'Enter'});
|
|
676
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
677
|
+
|
|
678
|
+
screen
|
|
679
|
+
.getByRole('menuitemcheckbox', {name: 'Archived', hidden: true})
|
|
680
|
+
.focus();
|
|
681
|
+
fireEvent.keyDown(menu, {key: ' '});
|
|
682
|
+
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('typeahead matches a menuitemradio label', async () => {
|
|
686
|
+
const user = userEvent.setup();
|
|
687
|
+
render(
|
|
688
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
689
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
690
|
+
<div role="menuitemradio" tabIndex={-1} aria-checked="false">
|
|
691
|
+
Newest
|
|
692
|
+
</div>
|
|
693
|
+
<div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
|
|
694
|
+
Archived
|
|
695
|
+
</div>
|
|
696
|
+
</DropdownMenu>,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
700
|
+
fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'n'});
|
|
701
|
+
expect(
|
|
702
|
+
screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}),
|
|
703
|
+
).toHaveFocus();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('typeahead matches a menuitemcheckbox label', async () => {
|
|
707
|
+
const user = userEvent.setup();
|
|
708
|
+
render(
|
|
709
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
710
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
711
|
+
<div role="menuitemradio" tabIndex={-1} aria-checked="false">
|
|
712
|
+
Newest
|
|
713
|
+
</div>
|
|
714
|
+
<div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
|
|
715
|
+
Archived
|
|
716
|
+
</div>
|
|
717
|
+
</DropdownMenu>,
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
721
|
+
fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'a'});
|
|
722
|
+
expect(
|
|
723
|
+
screen.getByRole('menuitemcheckbox', {name: 'Archived', hidden: true}),
|
|
724
|
+
).toHaveFocus();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('typeahead skips an aria-disabled item and matches the next enabled label', async () => {
|
|
728
|
+
const user = userEvent.setup();
|
|
729
|
+
render(
|
|
730
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
731
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
732
|
+
<div role="menuitemradio" tabIndex={-1} aria-disabled="true">
|
|
733
|
+
Newest
|
|
734
|
+
</div>
|
|
735
|
+
<div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
|
|
736
|
+
Nightly
|
|
737
|
+
</div>
|
|
738
|
+
</DropdownMenu>,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
742
|
+
// Anchor the search on 'Edit' so typeahead scans forward and meets the
|
|
743
|
+
// disabled 'Newest' (also an 'n' match) before the enabled 'Nightly'.
|
|
744
|
+
// This pins the `:not([aria-disabled="true"])` in MENU_ITEM_SELECTOR: the
|
|
745
|
+
// menus never pass useTypeahead's `isDisabled` option, so that clause is
|
|
746
|
+
// the only thing keeping disabled rows out of the typeahead list. An
|
|
747
|
+
// arrow-key test cannot cover it — useListFocus re-filters disabled items
|
|
748
|
+
// independently, so arrow navigation is guarded twice over.
|
|
749
|
+
// The disabled row keeps tabIndex={-1} on purpose: it stays focusable, so
|
|
750
|
+
// the selector clause is the sole reason focus skips it.
|
|
751
|
+
screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
|
|
752
|
+
fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'n'});
|
|
753
|
+
expect(
|
|
754
|
+
screen.getByRole('menuitemcheckbox', {name: 'Nightly', hidden: true}),
|
|
755
|
+
).toHaveFocus();
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('skips aria-disabled menuitemradio and menuitemcheckbox items during arrow navigation', async () => {
|
|
759
|
+
const user = userEvent.setup();
|
|
760
|
+
render(
|
|
761
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
762
|
+
<DropdownMenuItem label="Edit" onClick={() => {}} />
|
|
763
|
+
<div role="menuitemradio" tabIndex={-1} aria-disabled="true">
|
|
764
|
+
Newest
|
|
765
|
+
</div>
|
|
766
|
+
<div role="menuitemcheckbox" tabIndex={-1} aria-disabled="true">
|
|
767
|
+
Archived
|
|
768
|
+
</div>
|
|
769
|
+
<div role="menuitemradio" tabIndex={-1} aria-checked="false">
|
|
770
|
+
Oldest
|
|
771
|
+
</div>
|
|
772
|
+
</DropdownMenu>,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
776
|
+
const menu = screen.getByRole('menu', {hidden: true});
|
|
777
|
+
screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
|
|
778
|
+
|
|
779
|
+
fireEvent.keyDown(menu, {key: 'ArrowDown'});
|
|
780
|
+
expect(
|
|
781
|
+
screen.getByRole('menuitemradio', {name: 'Oldest', hidden: true}),
|
|
782
|
+
).toHaveFocus();
|
|
783
|
+
});
|
|
784
|
+
});
|
|
@@ -152,8 +152,7 @@ export interface DropdownMenuCheckboxItemProps extends Omit<
|
|
|
152
152
|
*
|
|
153
153
|
* @example
|
|
154
154
|
* ```
|
|
155
|
-
*
|
|
156
|
-
*
|
|
155
|
+
* import {DropdownMenuCheckboxItem} from '@astryxdesign/core/DropdownMenu';
|
|
157
156
|
* <DropdownMenu button={{label: 'View'}}>
|
|
158
157
|
* <DropdownMenuCheckboxItem
|
|
159
158
|
* label="Show archived"
|
|
@@ -77,11 +77,10 @@ export interface DropdownMenuRadioGroupProps extends Omit<
|
|
|
77
77
|
*
|
|
78
78
|
* @example
|
|
79
79
|
* ```
|
|
80
|
-
*
|
|
80
|
+
* import {
|
|
81
81
|
* DropdownMenuRadioGroup,
|
|
82
82
|
* DropdownMenuRadioItem,
|
|
83
83
|
* } from '@astryxdesign/core/DropdownMenu';
|
|
84
|
-
*
|
|
85
84
|
* <DropdownMenu button={{label: 'Sort'}}>
|
|
86
85
|
* <DropdownMenuRadioGroup value={sort} onChange={setSort} aria-label="Sort by">
|
|
87
86
|
* <DropdownMenuRadioItem value="newest" label="Newest" />
|
package/src/Field/Field.test.tsx
CHANGED
|
@@ -9,10 +9,22 @@
|
|
|
9
9
|
* SYNC: When Field.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen} from '@testing-library/react';
|
|
12
|
+
import {describe, it, expect, vi, afterEach} from 'vitest';
|
|
13
|
+
import {render, screen, waitFor} from '@testing-library/react';
|
|
14
14
|
import {Field} from './Field';
|
|
15
15
|
import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
|
|
16
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
17
|
+
|
|
18
|
+
function politeRegion(): HTMLElement | null {
|
|
19
|
+
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
20
|
+
}
|
|
21
|
+
function assertiveRegion(): HTMLElement | null {
|
|
22
|
+
return document.querySelector('[data-astryx-live-region="assertive"]');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
__resetLiveRegionsForTest();
|
|
27
|
+
});
|
|
16
28
|
|
|
17
29
|
describe('Field', () => {
|
|
18
30
|
it('renders with label', () => {
|
|
@@ -228,7 +240,11 @@ describe('Field', () => {
|
|
|
228
240
|
expect(document.querySelector('svg')).not.toBeInTheDocument();
|
|
229
241
|
});
|
|
230
242
|
|
|
231
|
-
|
|
243
|
+
// The status message is announced through the persistent useAnnounce live
|
|
244
|
+
// regions rather than role/aria-live on the (conditionally mounted)
|
|
245
|
+
// FieldStatus element itself — regions born with their content are not
|
|
246
|
+
// reliably announced by assistive technology.
|
|
247
|
+
it('announces error status assertively via the persistent live region', async () => {
|
|
232
248
|
render(
|
|
233
249
|
<Field
|
|
234
250
|
label="Email"
|
|
@@ -237,12 +253,13 @@ describe('Field', () => {
|
|
|
237
253
|
<input id="email-input" />
|
|
238
254
|
</Field>,
|
|
239
255
|
);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
expect(screen.getByText('Invalid email')).not.toHaveAttribute('role');
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
expect(assertiveRegion()).toHaveTextContent('Invalid email');
|
|
259
|
+
});
|
|
243
260
|
});
|
|
244
261
|
|
|
245
|
-
it('
|
|
262
|
+
it('announces warning status politely via the persistent live region', async () => {
|
|
246
263
|
render(
|
|
247
264
|
<Field
|
|
248
265
|
label="Email"
|
|
@@ -251,9 +268,34 @@ describe('Field', () => {
|
|
|
251
268
|
<input id="email-input" />
|
|
252
269
|
</Field>,
|
|
253
270
|
);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
expect(screen.getByText('Check this')).not.toHaveAttribute('aria-live');
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
expect(politeRegion()).toHaveTextContent('Check this');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Regression: the status live region must NOT be born together with its
|
|
278
|
+
// content. A message that appears after mount (the common validation flow)
|
|
279
|
+
// has to land in the persistent announce region.
|
|
280
|
+
it('announces a status message that appears after mount', async () => {
|
|
281
|
+
const {rerender} = render(
|
|
282
|
+
<Field label="Email" inputID="email-input">
|
|
283
|
+
<input id="email-input" />
|
|
284
|
+
</Field>,
|
|
285
|
+
);
|
|
286
|
+
expect(assertiveRegion()).toBeNull();
|
|
287
|
+
|
|
288
|
+
rerender(
|
|
289
|
+
<Field
|
|
290
|
+
label="Email"
|
|
291
|
+
inputID="email-input"
|
|
292
|
+
status={{type: 'error', message: 'Email is required'}}>
|
|
293
|
+
<input id="email-input" />
|
|
294
|
+
</Field>,
|
|
295
|
+
);
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
expect(assertiveRegion()).toHaveTextContent('Email is required');
|
|
298
|
+
});
|
|
257
299
|
});
|
|
258
300
|
|
|
259
301
|
it('auto-generates description ID as {inputID}-desc when descriptionID is not provided', () => {
|
|
@@ -277,7 +319,10 @@ describe('Field', () => {
|
|
|
277
319
|
<input id="my-input" />
|
|
278
320
|
</Field>,
|
|
279
321
|
);
|
|
280
|
-
expect(screen.
|
|
322
|
+
expect(screen.getByText('Required')).toHaveAttribute(
|
|
323
|
+
'id',
|
|
324
|
+
'my-input-status',
|
|
325
|
+
);
|
|
281
326
|
});
|
|
282
327
|
|
|
283
328
|
it('warns when isOptional and isRequired are both set', () => {
|
|
@@ -421,7 +466,7 @@ describe('Field', () => {
|
|
|
421
466
|
</Field>,
|
|
422
467
|
{wrapper: horizontalLabelsWrapper},
|
|
423
468
|
);
|
|
424
|
-
const statusEl = screen.
|
|
469
|
+
const statusEl = screen.getByText('Required');
|
|
425
470
|
const inputEl = screen.getByTestId('email');
|
|
426
471
|
// Both status and input should be inside the same wrapper div (column 2)
|
|
427
472
|
expect(statusEl.parentElement).toBe(inputEl.parentElement);
|
package/src/Field/Field.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import {FieldStatus} from '../FieldStatus/FieldStatus';
|
|
|
25
25
|
import {spacingVars, borderVars} from '../theme/tokens.stylex';
|
|
26
26
|
import type {IconType} from '../Icon';
|
|
27
27
|
import {mergeProps} from '../utils';
|
|
28
|
+
import {useDevWarning} from '../hooks/useDevWarning';
|
|
28
29
|
import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
|
|
29
30
|
import {Text} from '../Text';
|
|
30
31
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -215,11 +216,11 @@ export function Field({
|
|
|
215
216
|
const resolvedMessageID =
|
|
216
217
|
status?.messageID ?? (status?.message ? `${inputID}-status` : undefined);
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
useDevWarning(
|
|
220
|
+
'Field',
|
|
221
|
+
'isOptional and isRequired are mutually exclusive. isOptional takes precedence.',
|
|
222
|
+
isOptional && isRequired,
|
|
223
|
+
);
|
|
223
224
|
|
|
224
225
|
const labelNode = (
|
|
225
226
|
<FieldLabel
|
package/src/Field/FieldLabel.tsx
CHANGED
|
@@ -171,7 +171,11 @@ export function FieldLabel({
|
|
|
171
171
|
labelTooltip,
|
|
172
172
|
description,
|
|
173
173
|
descriptionID,
|
|
174
|
+
className,
|
|
175
|
+
style,
|
|
176
|
+
xstyle,
|
|
174
177
|
ref,
|
|
178
|
+
...rest
|
|
175
179
|
}: FieldLabelProps) {
|
|
176
180
|
const statusText = isOptional ? 'Optional' : isRequired ? 'Required' : null;
|
|
177
181
|
|
|
@@ -207,13 +211,17 @@ export function FieldLabel({
|
|
|
207
211
|
// `htmlFor` only applies to a real `<label>` associating with a single
|
|
208
212
|
// control; a group label (span) has no `htmlFor`.
|
|
209
213
|
htmlFor={isGroupLabel ? undefined : inputID}
|
|
214
|
+
{...rest}
|
|
210
215
|
{...mergeProps(
|
|
211
216
|
themeProps('field-label'),
|
|
212
217
|
stylex.props(
|
|
213
218
|
styles.label,
|
|
214
219
|
isDisabled && styles.labelDisabled,
|
|
215
220
|
isLabelHidden && styles.srOnly,
|
|
221
|
+
xstyle,
|
|
216
222
|
),
|
|
223
|
+
className,
|
|
224
|
+
style,
|
|
217
225
|
)}>
|
|
218
226
|
{labelContent}
|
|
219
227
|
</LabelElement>
|
|
@@ -8,7 +8,7 @@ export const docs = {
|
|
|
8
8
|
category: 'Data Input',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
10
|
description:
|
|
11
|
-
'Status message component for form field validation feedback.',
|
|
11
|
+
'Status message component for form field validation feedback. Messages are announced to screen readers through persistent live regions (assertive for errors, polite otherwise), so conditional mounting is safe.',
|
|
12
12
|
theming: {
|
|
13
13
|
targets: [
|
|
14
14
|
{className: 'astryx-field-status', visualProps: ['type', 'variant']},
|
|
@@ -66,12 +66,24 @@ export const docs = {
|
|
|
66
66
|
/** @type {import('../docs-types').TranslationDoc} */
|
|
67
67
|
export const docsDense = {
|
|
68
68
|
description:
|
|
69
|
-
'Validation feedback message for fields/custom controls. Supports error, warning, success and attached/detached variants.',
|
|
69
|
+
'Validation feedback message for fields/custom controls. Supports error, warning, success and attached/detached variants. Announced via persistent live regions (assertive for errors, polite otherwise).',
|
|
70
70
|
usage: {
|
|
71
71
|
bestPractices: [
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
{
|
|
73
|
+
guidance: true,
|
|
74
|
+
description:
|
|
75
|
+
'Use attached status below bordered inputs when message belongs to that input.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
guidance: true,
|
|
79
|
+
description:
|
|
80
|
+
'Use detached status for checkboxes, switches, and custom controls where overlap is visually awkward.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
guidance: false,
|
|
84
|
+
description:
|
|
85
|
+
'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
|
|
86
|
+
},
|
|
75
87
|
],
|
|
76
88
|
},
|
|
77
89
|
propDescriptions: {
|