@astryxdesign/core 0.1.9 → 0.2.0-canary.136a00f
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 +107 -0
- package/dist/Avatar/Avatar.d.ts +42 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +190 -59
- package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
- package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +58 -6
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
- package/dist/Banner/Banner.js +2 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
- package/dist/Breadcrumbs/index.d.ts +1 -0
- package/dist/Breadcrumbs/index.d.ts.map +1 -1
- package/dist/Breadcrumbs/index.js +2 -1
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +6 -4
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +50 -9
- package/dist/Calendar/dayCellUtils.d.ts +4 -4
- package/dist/Calendar/dayCellUtils.js +2 -2
- package/dist/Calendar/styles.d.ts +30 -31
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +30 -31
- package/dist/Card/Card.js +1 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +22 -12
- package/dist/Chat/ChatComposerInput.js +1 -1
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +3 -1
- package/dist/Chat/ChatLayout.js +4 -4
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +8 -8
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +10 -3
- package/dist/Chat/useTriggerMenu.js +2 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -6
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +19 -4
- package/dist/Citation/Citation.d.ts +22 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +20 -6
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +16 -10
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +3 -1
- package/dist/CommandPalette/CommandPalette.d.ts +1 -1
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +42 -3
- package/dist/CommandPalette/CommandPaletteItem.js +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +23 -5
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +3 -1
- package/dist/DateInput/DateInput.d.ts +10 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +33 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +10 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +23 -20
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +23 -18
- package/dist/Divider/Divider.d.ts +2 -2
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +13 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +22 -5
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +5 -2
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +8 -11
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +3 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +5 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -0
- package/dist/DropdownMenu/index.d.ts +10 -0
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +13 -0
- package/dist/DropdownMenu/menuItemHover.d.ts +23 -0
- package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemHover.js +33 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
- package/dist/DropdownMenu/menuItemRoles.js +10 -1
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +21 -6
- package/dist/Field/Field.d.ts +3 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +4 -1
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +9 -5
- package/dist/Field/inputStyles.stylex.d.ts +1 -0
- package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
- package/dist/Field/inputStyles.stylex.js +1 -0
- package/dist/FieldStatus/FieldStatus.d.ts +10 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +39 -3
- package/dist/FileInput/FileInput.d.ts +10 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +93 -65
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -4
- package/dist/Icon/Icon.d.ts +15 -1
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +19 -3
- package/dist/Item/Item.d.ts +16 -3
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +31 -6
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +10 -2
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +19 -13
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +6 -3
- package/dist/List/ListItem.d.ts +11 -1
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +2 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +7 -0
- package/dist/MetadataList/MetadataList.d.ts +1 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +4 -2
- package/dist/MultiSelector/MultiSelector.d.ts +9 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +80 -42
- package/dist/NavMenu/NavHeadingMenuItem.js +1 -1
- package/dist/NumberInput/NumberInput.d.ts +10 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +22 -17
- package/dist/Outline/Outline.js +2 -2
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +11 -3
- package/dist/PowerSearch/PowerSearch.d.ts +9 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +2 -0
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +6 -1
- package/dist/RadioList/RadioList.d.ts +2 -2
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -5
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +49 -19
- package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -3
- package/dist/Selector/Selector.d.ts +8 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +66 -17
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +5 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +13 -10
- package/dist/SideNav/SideNavItem.d.ts +1 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +12 -10
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +2 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +87 -34
- package/dist/Spinner/Spinner.d.ts +3 -2
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +11 -2
- package/dist/Switch/Switch.d.ts +8 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +171 -91
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts +2 -11
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +5 -9
- package/dist/TabList/TabListContext.d.ts +0 -5
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +0 -5
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +30 -4
- package/dist/Table/TableCell.js +3 -3
- package/dist/Table/TableHeaderCell.js +3 -3
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +37 -27
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +3 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +15 -7
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
- package/dist/Table/types.d.ts +32 -0
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/TextArea/TextArea.d.ts +10 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +18 -19
- package/dist/TextInput/TextInput.d.ts +10 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +20 -17
- package/dist/Thumbnail/Thumbnail.js +2 -2
- package/dist/TimeInput/TimeInput.d.ts +10 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +17 -17
- package/dist/Timestamp/Timestamp.d.ts +31 -3
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +47 -76
- package/dist/Timestamp/formatInstant.d.ts +41 -0
- package/dist/Timestamp/formatInstant.d.ts.map +1 -0
- package/dist/Timestamp/formatInstant.js +177 -0
- package/dist/Timestamp/index.d.ts +1 -0
- package/dist/Timestamp/index.d.ts.map +1 -1
- package/dist/Timestamp/tooltipEntries.d.ts +56 -0
- package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
- package/dist/Timestamp/tooltipEntries.js +163 -0
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +61 -9
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +3 -1
- package/dist/Token/Token.d.ts +34 -4
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +85 -45
- package/dist/Token/TokenLink.d.ts +62 -0
- package/dist/Token/TokenLink.d.ts.map +1 -0
- package/dist/Token/TokenLink.js +76 -0
- package/dist/Token/index.d.ts +1 -1
- package/dist/Token/index.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts +9 -2
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +4 -2
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +17 -1
- package/dist/TopNav/TopNavItem.d.ts +3 -1
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +30 -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 +8 -6
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +32 -14
- package/dist/Typeahead/BaseTypeahead.d.ts +9 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +22 -11
- package/dist/Typeahead/Typeahead.d.ts +9 -2
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +11 -2
- package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/Typeahead/TypeaheadItem.js +20 -10
- package/dist/astryx.css +70 -30
- package/dist/astryx.umd.js +51 -51
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/isRtlElement.d.ts +18 -0
- package/dist/hooks/isRtlElement.d.ts.map +1 -0
- package/dist/hooks/isRtlElement.js +27 -0
- package/dist/hooks/useAnnounce.d.ts +3 -0
- package/dist/hooks/useAnnounce.d.ts.map +1 -1
- package/dist/hooks/useAnnounce.js +39 -1
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +52 -11
- package/dist/hooks/useGridFocus.d.ts +6 -1
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +12 -9
- package/dist/hooks/useHotkeys.d.ts +9 -0
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +9 -0
- package/dist/hooks/useInputStatusIcon.d.ts +38 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
- package/dist/hooks/useInputStatusIcon.js +158 -0
- package/dist/hooks/useKeyboardHint.d.ts +1 -1
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
- package/dist/hooks/useKeyboardHint.js +4 -2
- package/dist/hooks/useListFocus.d.ts +39 -1
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +54 -9
- package/dist/i18n/InternationalizationContext.d.ts +1 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -0
- package/dist/i18n/InternationalizationProvider.d.ts +8 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +5 -1
- package/dist/i18n/getLocaleDirection.d.ts +18 -0
- package/dist/i18n/getLocaleDirection.d.ts.map +1 -0
- package/dist/i18n/getLocaleDirection.js +29 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +7 -1
- package/dist/i18n/useDirection.d.ts +2 -0
- package/dist/i18n/useDirection.d.ts.map +1 -0
- package/dist/i18n/useDirection.js +27 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +13 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/plainDate.d.ts +17 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +23 -4
- package/dist/utils/rtlStyles.d.ts +24 -0
- package/dist/utils/rtlStyles.d.ts.map +1 -0
- package/dist/utils/rtlStyles.js +27 -0
- package/locales/en.json +112 -0
- package/locales/pseudo.json +84 -0
- package/package.json +6 -1
- package/src/Avatar/Avatar.doc.mjs +50 -3
- package/src/Avatar/Avatar.test.tsx +218 -0
- package/src/Avatar/Avatar.tsx +285 -79
- package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
- package/src/Avatar/AvatarStatusDot.tsx +6 -4
- package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
- package/src/AvatarGroup/AvatarGroup.test.tsx +162 -2
- package/src/AvatarGroup/AvatarGroup.tsx +58 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +5 -2
- package/src/Banner/Banner.tsx +6 -6
- package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +52 -1
- package/src/Breadcrumbs/index.ts +4 -0
- package/src/Button/Button.doc.mjs +0 -4
- package/src/Button/Button.test.tsx +16 -0
- package/src/Button/Button.tsx +6 -4
- package/src/Calendar/Calendar.test.tsx +170 -20
- package/src/Calendar/Calendar.tsx +73 -16
- package/src/Calendar/dayCellUtils.test.ts +18 -18
- package/src/Calendar/dayCellUtils.ts +2 -2
- package/src/Calendar/styles.ts +33 -39
- package/src/Card/Card.tsx +1 -1
- package/src/Carousel/Carousel.tsx +25 -6
- package/src/Chat/ChatComposer.doc.mjs +2 -2
- package/src/Chat/ChatComposerInput.doc.mjs +1 -1
- package/src/Chat/ChatComposerInput.tsx +2 -2
- package/src/Chat/ChatDictationButton.tsx +7 -1
- package/src/Chat/ChatLayout.tsx +4 -4
- package/src/Chat/ChatMessageBubble.tsx +15 -10
- package/src/Chat/ChatSystemMessage.test.tsx +5 -0
- package/src/Chat/ChatToolCalls.tsx +9 -2
- package/src/Chat/useTriggerMenu.tsx +1 -1
- package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +23 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -6
- package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
- package/src/CheckboxList/CheckboxList.test.tsx +94 -3
- package/src/CheckboxList/CheckboxListItem.tsx +17 -10
- package/src/Citation/Citation.doc.mjs +4 -3
- package/src/Citation/Citation.test.tsx +97 -0
- package/src/Citation/Citation.tsx +43 -11
- package/src/CodeBlock/CodeBlock.test.tsx +17 -0
- package/src/CodeBlock/CodeBlock.tsx +38 -11
- package/src/Collapsible/Collapsible.doc.mjs +1 -0
- package/src/Collapsible/Collapsible.test.tsx +7 -0
- package/src/Collapsible/Collapsible.tsx +16 -6
- package/src/CommandPalette/CommandPalette.test.tsx +95 -1
- package/src/CommandPalette/CommandPalette.tsx +41 -2
- package/src/CommandPalette/CommandPaletteItem.tsx +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +59 -2
- package/src/ContextMenu/ContextMenu.tsx +22 -11
- package/src/ContextMenu/index.ts +5 -0
- package/src/DateInput/DateInput.doc.mjs +25 -0
- package/src/DateInput/DateInput.test.tsx +230 -0
- package/src/DateInput/DateInput.tsx +51 -29
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +140 -0
- package/src/DateRangeInput/DateRangeInput.tsx +42 -30
- package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
- package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
- package/src/DateTimeInput/DateTimeInput.tsx +44 -39
- package/src/Divider/Divider.test.tsx +32 -8
- package/src/Divider/Divider.tsx +14 -1
- package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
- package/src/DropdownMenu/DropdownMenu.tsx +31 -12
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
- package/src/DropdownMenu/DropdownMenuItem.tsx +9 -3
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +4 -12
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +9 -10
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +9 -3
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +20 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
- package/src/DropdownMenu/index.ts +16 -0
- package/src/DropdownMenu/menuItemHover.ts +39 -0
- package/src/DropdownMenu/menuItemRoles.ts +9 -0
- package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
- package/src/Field/Field.tsx +14 -9
- package/src/Field/FieldLabel.test.tsx +25 -0
- package/src/Field/FieldLabel.tsx +12 -3
- package/src/Field/inputStyles.stylex.ts +2 -0
- package/src/FieldStatus/FieldStatus.test.tsx +72 -0
- package/src/FieldStatus/FieldStatus.tsx +48 -2
- package/src/FileInput/FileInput.doc.mjs +23 -2
- package/src/FileInput/FileInput.test.tsx +104 -13
- package/src/FileInput/FileInput.tsx +106 -79
- package/src/HoverCard/useHoverCard.tsx +15 -3
- package/src/Icon/Icon.doc.mjs +13 -0
- package/src/Icon/Icon.test.tsx +89 -0
- package/src/Icon/Icon.tsx +51 -3
- package/src/Item/Item.doc.mjs +5 -0
- package/src/Item/Item.test.tsx +88 -0
- package/src/Item/Item.tsx +57 -11
- package/src/Layer/layerAnimations.stylex.ts +23 -0
- package/src/Lightbox/Lightbox.tsx +15 -7
- package/src/List/List.test.tsx +17 -29
- package/src/List/List.tsx +6 -1
- package/src/List/ListItem.doc.mjs +12 -0
- package/src/List/ListItem.tsx +13 -0
- package/src/Markdown/Markdown.test.tsx +21 -0
- package/src/Markdown/Markdown.tsx +4 -0
- package/src/MetadataList/MetadataList.test.tsx +24 -0
- package/src/MetadataList/MetadataList.tsx +6 -2
- package/src/MultiSelector/MultiSelector.doc.mjs +20 -0
- package/src/MultiSelector/MultiSelector.test.tsx +402 -0
- package/src/MultiSelector/MultiSelector.tsx +120 -59
- package/src/NavMenu/NavHeadingMenuItem.tsx +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +21 -0
- package/src/NumberInput/NumberInput.test.tsx +71 -0
- package/src/NumberInput/NumberInput.tsx +35 -26
- package/src/Outline/Outline.test.tsx +12 -12
- package/src/Outline/Outline.tsx +2 -2
- package/src/Pagination/Pagination.tsx +11 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
- package/src/PowerSearch/PowerSearch.test.tsx +23 -0
- package/src/PowerSearch/PowerSearch.tsx +10 -1
- package/src/ProgressBar/ProgressBar.test.tsx +34 -0
- package/src/ProgressBar/ProgressBar.tsx +16 -1
- package/src/RadioList/RadioList.doc.mjs +6 -0
- package/src/RadioList/RadioList.test.tsx +26 -4
- package/src/RadioList/RadioList.tsx +2 -2
- package/src/RadioList/RadioListItem.tsx +12 -5
- package/src/Resizable/ResizeHandle.test.tsx +94 -0
- package/src/Resizable/ResizeHandle.tsx +70 -20
- package/src/SegmentedControl/SegmentedControl.test.tsx +28 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +22 -2
- package/src/Selector/Selector.doc.mjs +15 -0
- package/src/Selector/Selector.test.tsx +370 -0
- package/src/Selector/Selector.tsx +88 -20
- package/src/SideNav/SideNav.tsx +1 -8
- package/src/SideNav/SideNavCollapseButton.tsx +9 -7
- package/src/SideNav/SideNavItem.tsx +5 -3
- package/src/Skeleton/Skeleton.test.tsx +16 -0
- package/src/Skeleton/Skeleton.tsx +13 -1
- package/src/Slider/Slider.doc.mjs +6 -0
- package/src/Slider/Slider.test.tsx +240 -12
- package/src/Slider/Slider.tsx +96 -23
- package/src/Spinner/Spinner.test.tsx +10 -8
- package/src/Spinner/Spinner.tsx +15 -5
- package/src/Switch/Switch.doc.mjs +142 -42
- package/src/Switch/Switch.test.tsx +111 -0
- package/src/Switch/Switch.tsx +149 -32
- package/src/TabList/Tab.tsx +2 -2
- package/src/TabList/TabList.doc.mjs +0 -6
- package/src/TabList/TabList.test.tsx +3 -15
- package/src/TabList/TabList.tsx +6 -17
- package/src/TabList/TabListContext.ts +0 -6
- package/src/TabList/TabMenu.tsx +2 -2
- package/src/Table/BaseTable.tsx +34 -2
- package/src/Table/Table.doc.mjs +11 -0
- package/src/Table/Table.test.tsx +85 -0
- package/src/Table/TableCell.tsx +3 -3
- package/src/Table/TableHeaderCell.tsx +3 -3
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +19 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +27 -18
- package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
- package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +38 -9
- package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
- package/src/Table/types.ts +33 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/TextArea/TextArea.doc.mjs +21 -0
- package/src/TextArea/TextArea.test.tsx +23 -0
- package/src/TextArea/TextArea.tsx +27 -26
- package/src/TextInput/TextInput.doc.mjs +21 -0
- package/src/TextInput/TextInput.test.tsx +150 -0
- package/src/TextInput/TextInput.tsx +28 -25
- package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.tsx +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +21 -0
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +24 -25
- package/src/Timestamp/Timestamp.doc.mjs +32 -6
- package/src/Timestamp/Timestamp.test.tsx +371 -0
- package/src/Timestamp/Timestamp.tsx +96 -93
- package/src/Timestamp/formatInstant.ts +218 -0
- package/src/Timestamp/index.ts +4 -0
- package/src/Timestamp/tooltipEntries.test.ts +552 -0
- package/src/Timestamp/tooltipEntries.ts +210 -0
- package/src/Toast/ToastViewport.test.tsx +218 -8
- package/src/Toast/ToastViewport.tsx +71 -12
- package/src/ToggleButton/ToggleButton.test.tsx +24 -0
- package/src/ToggleButton/ToggleButton.tsx +19 -1
- package/src/Token/Token.test.tsx +166 -15
- package/src/Token/Token.tsx +104 -47
- package/src/Token/TokenLink.tsx +107 -0
- package/src/Token/index.ts +1 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +23 -1
- package/src/Tokenizer/Tokenizer.test.tsx +23 -0
- package/src/Tokenizer/Tokenizer.tsx +14 -2
- package/src/Tooltip/Tooltip.test.tsx +46 -0
- package/src/Tooltip/useTooltip.tsx +20 -0
- package/src/TopNav/TopNav.test.tsx +93 -2
- package/src/TopNav/TopNavItem.doc.mjs +3 -3
- package/src/TopNav/TopNavItem.tsx +35 -6
- package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +6 -0
- package/src/TreeList/TreeList.test.tsx +50 -10
- package/src/TreeList/TreeList.tsx +5 -0
- package/src/TreeList/TreeListBranches.tsx +8 -6
- package/src/TreeList/TreeListItem.tsx +33 -11
- package/src/Typeahead/BaseTypeahead.tsx +42 -15
- package/src/Typeahead/Typeahead.doc.mjs +13 -0
- package/src/Typeahead/Typeahead.test.tsx +112 -0
- package/src/Typeahead/Typeahead.tsx +17 -1
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/__tests__/forcedColors.ts +60 -0
- package/src/__tests__/inputWidthContract.test.tsx +62 -0
- package/src/doc-mjs.d.ts +19 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/isRtlElement.ts +27 -0
- package/src/hooks/useAnnounce.test.tsx +75 -2
- package/src/hooks/useAnnounce.ts +47 -1
- package/src/hooks/useFocusTrap.test.tsx +104 -0
- package/src/hooks/useFocusTrap.ts +61 -12
- package/src/hooks/useGridFocus.doc.mjs +3 -3
- package/src/hooks/useGridFocus.test.tsx +41 -0
- package/src/hooks/useGridFocus.ts +21 -10
- package/src/hooks/useHotkeys.ts +9 -0
- package/src/hooks/useInputStatusIcon.test.tsx +162 -0
- package/src/hooks/useInputStatusIcon.tsx +224 -0
- package/src/hooks/useKeyboardHint.test.tsx +22 -0
- package/src/hooks/useKeyboardHint.tsx +6 -2
- package/src/hooks/useListFocus.doc.mjs +25 -3
- package/src/hooks/useListFocus.test.tsx +153 -1
- package/src/hooks/useListFocus.ts +102 -9
- package/src/i18n/InternationalizationContext.ts +4 -0
- package/src/i18n/InternationalizationProvider.tsx +12 -2
- package/src/i18n/__tests__/getLocaleDirection.test.ts +40 -0
- package/src/i18n/__tests__/useDirection.test.tsx +66 -0
- package/src/i18n/getLocaleDirection.ts +29 -0
- package/src/i18n/index.ts +6 -0
- package/src/i18n/useDirection.ts +29 -0
- package/src/reset.css +0 -26
- package/src/reset.test.ts +60 -0
- package/src/theme/derivedVarRegistry.test.ts +1 -0
- package/src/theme/derivedVarRegistry.ts +6 -0
- package/src/theme/generateThemeRules.test.ts +42 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/plainDate.test.ts +24 -6
- package/src/utils/plainDate.ts +18 -4
- package/src/utils/rtlStyles.ts +27 -0
package/src/hooks/useAnnounce.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* @input Uses React useCallback; DOM APIs for a singleton live-region pair
|
|
8
8
|
* @output Exports useAnnounce hook and AnnouncePoliteness type
|
|
9
9
|
* @position Core a11y hook; provides imperative screen-reader announcements
|
|
10
|
-
* via persistently-mounted polite/assertive live regions
|
|
10
|
+
* via persistently-mounted polite/assertive live regions; regions are
|
|
11
|
+
* auto-cleared shortly after announcing so stale status text does not
|
|
12
|
+
* linger in the accessibility tree
|
|
11
13
|
*
|
|
12
14
|
* SYNC: When modified, update:
|
|
13
15
|
* - /packages/core/src/hooks/index.ts
|
|
@@ -47,6 +49,40 @@ interface LiveRegions {
|
|
|
47
49
|
assertive: HTMLElement;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* How long an announcement stays in its region before being auto-cleared.
|
|
54
|
+
* Long enough for screen readers to pick the message up and finish reading
|
|
55
|
+
* it; clearing afterwards keeps stale status text out of the accessibility
|
|
56
|
+
* tree for users who browse the DOM later. Each announce resets the timer.
|
|
57
|
+
*/
|
|
58
|
+
const CLEAR_DELAY_MS = 2000;
|
|
59
|
+
|
|
60
|
+
const clearTimers: {
|
|
61
|
+
[K in AnnouncePoliteness]: ReturnType<typeof setTimeout> | null;
|
|
62
|
+
} = {
|
|
63
|
+
polite: null,
|
|
64
|
+
assertive: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function cancelScheduledClear(politeness: AnnouncePoliteness): void {
|
|
68
|
+
const timer = clearTimers[politeness];
|
|
69
|
+
if (timer != null) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
clearTimers[politeness] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scheduleClear(
|
|
76
|
+
politeness: AnnouncePoliteness,
|
|
77
|
+
target: HTMLElement,
|
|
78
|
+
): void {
|
|
79
|
+
cancelScheduledClear(politeness);
|
|
80
|
+
clearTimers[politeness] = setTimeout(() => {
|
|
81
|
+
clearTimers[politeness] = null;
|
|
82
|
+
target.textContent = '';
|
|
83
|
+
}, CLEAR_DELAY_MS);
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
// Singleton: the live regions are created ONCE and stay mounted for the
|
|
51
87
|
// lifetime of the document. This is the crux of reliable announcements — many
|
|
52
88
|
// screen readers will not announce content injected into a live region that is
|
|
@@ -103,6 +139,10 @@ function announceMessage(
|
|
|
103
139
|
requestAnimationFrame(() => {
|
|
104
140
|
target.textContent = message;
|
|
105
141
|
});
|
|
142
|
+
// Auto-clear so the last message does not linger in the accessibility tree
|
|
143
|
+
// indefinitely. Scheduled per announce, so a newer announcement always
|
|
144
|
+
// resets the countdown (the delay comfortably outlasts the rAF re-set).
|
|
145
|
+
scheduleClear(politeness, target);
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
/**
|
|
@@ -111,6 +151,7 @@ function announceMessage(
|
|
|
111
151
|
* so stale status text does not linger in the accessibility tree.
|
|
112
152
|
*/
|
|
113
153
|
function clearRegion(politeness: AnnouncePoliteness): void {
|
|
154
|
+
cancelScheduledClear(politeness);
|
|
114
155
|
if (!regions) {
|
|
115
156
|
return;
|
|
116
157
|
}
|
|
@@ -126,6 +167,9 @@ function clearRegion(politeness: AnnouncePoliteness): void {
|
|
|
126
167
|
* The polite and assertive regions are created once on first use and kept
|
|
127
168
|
* mounted, so announcements are reliable even for messages that appear
|
|
128
169
|
* immediately (unlike a live region rendered together with its content).
|
|
170
|
+
* Each message is automatically cleared a couple of seconds after being
|
|
171
|
+
* announced, so users browsing the DOM later do not encounter stale status
|
|
172
|
+
* text; announcing again before the clear simply resets the countdown.
|
|
129
173
|
*
|
|
130
174
|
* @example
|
|
131
175
|
* ```
|
|
@@ -160,6 +204,8 @@ export function useAnnounce(): AnnounceFn {
|
|
|
160
204
|
* @internal
|
|
161
205
|
*/
|
|
162
206
|
export function __resetLiveRegionsForTest(): void {
|
|
207
|
+
cancelScheduledClear('polite');
|
|
208
|
+
cancelScheduledClear('assertive');
|
|
163
209
|
if (regions) {
|
|
164
210
|
regions.polite.remove();
|
|
165
211
|
regions.assertive.remove();
|
|
@@ -50,6 +50,39 @@ function EscapeTrap({
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function NestedInnerTrap({onEscape}: {onEscape: () => void}) {
|
|
54
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({
|
|
55
|
+
isActive: true,
|
|
56
|
+
onEscape,
|
|
57
|
+
});
|
|
58
|
+
return (
|
|
59
|
+
<div ref={containerRef} data-testid="nested-inner">
|
|
60
|
+
<button type="button">inner-btn</button>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function NestedTraps({
|
|
66
|
+
onOuterEscape,
|
|
67
|
+
onInnerEscape,
|
|
68
|
+
showInner = true,
|
|
69
|
+
}: {
|
|
70
|
+
onOuterEscape: () => void;
|
|
71
|
+
onInnerEscape: () => void;
|
|
72
|
+
showInner?: boolean;
|
|
73
|
+
}) {
|
|
74
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({
|
|
75
|
+
isActive: true,
|
|
76
|
+
onEscape: onOuterEscape,
|
|
77
|
+
});
|
|
78
|
+
return (
|
|
79
|
+
<div ref={containerRef} data-testid="nested-outer">
|
|
80
|
+
<button type="button">outer-btn</button>
|
|
81
|
+
{showInner && <NestedInnerTrap onEscape={onInnerEscape} />}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
function RestoreTrap({isActive}: {isActive: boolean}) {
|
|
54
87
|
const {containerRef} = useFocusTrap<HTMLDivElement>({isActive});
|
|
55
88
|
return (
|
|
@@ -118,6 +151,47 @@ describe('useFocusTrap tabbable model (infra-8)', () => {
|
|
|
118
151
|
// Focus skips the inert button and lands on the real one.
|
|
119
152
|
expect(screen.getByTestId('real-btn')).toHaveFocus();
|
|
120
153
|
});
|
|
154
|
+
|
|
155
|
+
it('ignores an aria-hidden subtree when finding focusables', () => {
|
|
156
|
+
render(
|
|
157
|
+
<Trap>
|
|
158
|
+
<div aria-hidden="true">
|
|
159
|
+
<button type="button" data-testid="aria-hidden-btn">
|
|
160
|
+
Hidden from AT
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
<button type="button" data-testid="real-btn">
|
|
164
|
+
Real
|
|
165
|
+
</button>
|
|
166
|
+
</Trap>,
|
|
167
|
+
);
|
|
168
|
+
fireEvent.click(screen.getByTestId('focus-first'));
|
|
169
|
+
// An element AT cannot perceive must not be a trap tab stop (WCAG 4.1.2).
|
|
170
|
+
expect(screen.getByTestId('real-btn')).toHaveFocus();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('excludes an aria-hidden focusable from the Tab wrap boundary', () => {
|
|
174
|
+
render(
|
|
175
|
+
<Trap>
|
|
176
|
+
<button type="button" data-testid="first">
|
|
177
|
+
First
|
|
178
|
+
</button>
|
|
179
|
+
<button type="button" data-testid="visible-last">
|
|
180
|
+
Visible last
|
|
181
|
+
</button>
|
|
182
|
+
<div aria-hidden="true">
|
|
183
|
+
<button type="button" data-testid="aria-hidden-btn">
|
|
184
|
+
Hidden from AT
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</Trap>,
|
|
188
|
+
);
|
|
189
|
+
// The last VISIBLE-to-AT element is the wrap boundary: Tab from it wraps
|
|
190
|
+
// to the first element instead of landing on the aria-hidden button.
|
|
191
|
+
screen.getByTestId('visible-last').focus();
|
|
192
|
+
fireEvent.keyDown(document, {key: 'Tab'});
|
|
193
|
+
expect(screen.getByTestId('first')).toHaveFocus();
|
|
194
|
+
});
|
|
121
195
|
});
|
|
122
196
|
|
|
123
197
|
describe('FOCUSABLE_SELECTOR href matching', () => {
|
|
@@ -198,6 +272,36 @@ describe('useFocusTrap Escape coordination', () => {
|
|
|
198
272
|
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
199
273
|
});
|
|
200
274
|
|
|
275
|
+
it('resolves Escape to the DOM-nested inner trap when both mount in one commit', () => {
|
|
276
|
+
const outer = vi.fn();
|
|
277
|
+
const inner = vi.fn();
|
|
278
|
+
// Outer and inner mount in the SAME React commit. React runs child
|
|
279
|
+
// effects before parent effects, so the inner trap is PUSHED first —
|
|
280
|
+
// DOM containment, not push order, must decide who answers Escape.
|
|
281
|
+
render(<NestedTraps onOuterEscape={outer} onInnerEscape={inner} />);
|
|
282
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
283
|
+
expect(inner).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(outer).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('falls back to the outer trap once the nested inner trap unmounts', () => {
|
|
288
|
+
const outer = vi.fn();
|
|
289
|
+
const inner = vi.fn();
|
|
290
|
+
const {rerender} = render(
|
|
291
|
+
<NestedTraps onOuterEscape={outer} onInnerEscape={inner} />,
|
|
292
|
+
);
|
|
293
|
+
rerender(
|
|
294
|
+
<NestedTraps
|
|
295
|
+
onOuterEscape={outer}
|
|
296
|
+
onInnerEscape={inner}
|
|
297
|
+
showInner={false}
|
|
298
|
+
/>,
|
|
299
|
+
);
|
|
300
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
301
|
+
expect(outer).toHaveBeenCalledTimes(1);
|
|
302
|
+
expect(inner).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
201
305
|
it('does not respond after the trap is deactivated', () => {
|
|
202
306
|
const onEscape = vi.fn();
|
|
203
307
|
const {rerender} = render(
|
|
@@ -26,24 +26,62 @@ import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
|
26
26
|
* Every active `useFocusTrap` used to attach its own document-level `keydown`
|
|
27
27
|
* listener with no coordination, so a single Escape press closed *every* open
|
|
28
28
|
* layer at once (e.g. a popover nested inside a Dialog closed both). Tracking
|
|
29
|
-
* traps in a shared stack lets only the most
|
|
30
|
-
*
|
|
29
|
+
* traps in a shared stack lets only the top-most trap respond to Escape.
|
|
30
|
+
*
|
|
31
|
+
* "Top-most" is resolved by DOM containment first, push order second. Push
|
|
32
|
+
* order alone is not reliable: React runs child effects before parent
|
|
33
|
+
* effects, so when an outer and an inner (DOM-nested) trap mount in the SAME
|
|
34
|
+
* commit, the inner trap pushes first and the outer trap would wrongly win a
|
|
35
|
+
* pure last-pushed comparison.
|
|
31
36
|
*/
|
|
32
|
-
|
|
37
|
+
interface EscapeStackEntry {
|
|
38
|
+
handler: () => void;
|
|
39
|
+
getContainer: () => HTMLElement | null;
|
|
40
|
+
}
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
const escapeStack: EscapeStackEntry[] = [];
|
|
43
|
+
|
|
44
|
+
function pushEscapeHandler(entry: EscapeStackEntry): void {
|
|
45
|
+
escapeStack.push(entry);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function removeEscapeHandler(handler: () => void): void {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
for (let i = escapeStack.length - 1; i >= 0; i--) {
|
|
50
|
+
if (escapeStack[i].handler === handler) {
|
|
51
|
+
escapeStack.splice(i, 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the top-most trap: walk the stack in push order, keeping the
|
|
59
|
+
* deepest container by DOM containment. When a later entry's container
|
|
60
|
+
* contains the current candidate's container, the candidate is nested inside
|
|
61
|
+
* it and stays on top; otherwise the later push wins (containment for nested
|
|
62
|
+
* traps, push order as the tiebreaker for unrelated ones).
|
|
63
|
+
*/
|
|
45
64
|
function isTopEscapeHandler(handler: () => void): boolean {
|
|
46
|
-
|
|
65
|
+
if (escapeStack.length === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
let top = escapeStack[0];
|
|
69
|
+
for (let i = 1; i < escapeStack.length; i++) {
|
|
70
|
+
const entry = escapeStack[i];
|
|
71
|
+
const topContainer = top.getContainer();
|
|
72
|
+
const entryContainer = entry.getContainer();
|
|
73
|
+
if (
|
|
74
|
+
topContainer != null &&
|
|
75
|
+
entryContainer != null &&
|
|
76
|
+
entryContainer !== topContainer &&
|
|
77
|
+
entryContainer.contains(topContainer)
|
|
78
|
+
) {
|
|
79
|
+
// The current top is nested inside this entry — it stays on top.
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
top = entry;
|
|
83
|
+
}
|
|
84
|
+
return top.handler === handler;
|
|
47
85
|
}
|
|
48
86
|
|
|
49
87
|
/**
|
|
@@ -73,7 +111,9 @@ export function isImeKeyEvent(event: {
|
|
|
73
111
|
/**
|
|
74
112
|
* Whether an element is currently perceivable/focusable — excludes ones hidden
|
|
75
113
|
* via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
|
|
76
|
-
* which the browser skips for Tab
|
|
114
|
+
* which the browser skips for Tab, and ones inside an `aria-hidden="true"`
|
|
115
|
+
* subtree, which sighted-keyboard users could Tab to while AT skips them
|
|
116
|
+
* (WCAG 4.1.2 — focusable content must be exposed to assistive tech).
|
|
77
117
|
*/
|
|
78
118
|
function isVisiblyFocusable(el: HTMLElement): boolean {
|
|
79
119
|
if (el.hasAttribute('inert') || el.closest('[inert]')) {
|
|
@@ -82,6 +122,10 @@ function isVisiblyFocusable(el: HTMLElement): boolean {
|
|
|
82
122
|
if (el.hidden || el.closest('[hidden]')) {
|
|
83
123
|
return false;
|
|
84
124
|
}
|
|
125
|
+
// closest() matches the element itself as well as any ancestor.
|
|
126
|
+
if (el.closest('[aria-hidden="true"]')) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
85
129
|
// offsetParent is null for display:none (and fixed elements); pair with a
|
|
86
130
|
// visibility check via getComputedStyle when available.
|
|
87
131
|
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
@@ -323,12 +367,17 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
323
367
|
|
|
324
368
|
// Register this trap on the shared Escape stack so only the top-most
|
|
325
369
|
// active trap responds to Escape. A stable identity per active period is
|
|
326
|
-
// enough — we push on activate and remove on cleanup.
|
|
370
|
+
// enough — we push on activate and remove on cleanup. The container is
|
|
371
|
+
// read lazily: the ref may not be attached yet at effect time, and the
|
|
372
|
+
// stack resolves top-most by DOM containment at keydown time.
|
|
327
373
|
const escapeHandler = () => {
|
|
328
374
|
onEscape?.();
|
|
329
375
|
};
|
|
330
376
|
if (onEscape) {
|
|
331
|
-
pushEscapeHandler(
|
|
377
|
+
pushEscapeHandler({
|
|
378
|
+
handler: escapeHandler,
|
|
379
|
+
getContainer: () => containerRef.current,
|
|
380
|
+
});
|
|
332
381
|
}
|
|
333
382
|
|
|
334
383
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
@@ -64,8 +64,8 @@ export const docs = {
|
|
|
64
64
|
{
|
|
65
65
|
name: 'options.isRtl',
|
|
66
66
|
type: 'boolean',
|
|
67
|
-
description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts.',
|
|
68
|
-
default: '
|
|
67
|
+
description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts. When omitted, auto-detected from the container computed direction on keydown.',
|
|
68
|
+
default: "undefined (auto-detect from the container's computed direction)",
|
|
69
69
|
required: false,
|
|
70
70
|
},
|
|
71
71
|
{
|
|
@@ -138,7 +138,7 @@ export const docsDense = {
|
|
|
138
138
|
'options.onNavigateAfter': 'callback when navigation would go after last cell. Receives column index + offset.',
|
|
139
139
|
'options.onPageUp': 'callback for Page Up key (e.g. navigate to previous month in calendars).',
|
|
140
140
|
'options.onPageDown': 'callback for Page Down key (e.g. navigate to next month in calendars).',
|
|
141
|
-
'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default
|
|
141
|
+
'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default: auto-detect from container computed direction; explicit boolean wins.',
|
|
142
142
|
'options.hasRovingTabIndex': 'own a single roving tab stop across the grid (one focus target tabindex=0, rest -1); repaired on render + moved with arrows. Attach handleFocus to onFocus. default false.',
|
|
143
143
|
},
|
|
144
144
|
returnDescriptions: {
|
|
@@ -23,10 +23,12 @@ const NO_DISABLED: number[] = [];
|
|
|
23
23
|
function Grid({
|
|
24
24
|
disabled = NO_DISABLED,
|
|
25
25
|
seed = 0,
|
|
26
|
+
dir,
|
|
26
27
|
...opts
|
|
27
28
|
}: {
|
|
28
29
|
disabled?: number[];
|
|
29
30
|
seed?: number;
|
|
31
|
+
dir?: 'ltr' | 'rtl';
|
|
30
32
|
} & Partial<Parameters<typeof useGridFocus>[0]>) {
|
|
31
33
|
const {gridRef, handleKeyDown, handleFocus} = useGridFocus<HTMLDivElement>({
|
|
32
34
|
columns: 3,
|
|
@@ -41,6 +43,7 @@ function Grid({
|
|
|
41
43
|
<div
|
|
42
44
|
ref={gridRef}
|
|
43
45
|
role="grid"
|
|
46
|
+
dir={dir}
|
|
44
47
|
onKeyDown={handleKeyDown}
|
|
45
48
|
onFocus={handleFocus}>
|
|
46
49
|
{Array.from({length: 9}, (_, i) => (
|
|
@@ -130,3 +133,41 @@ describe('useGridFocus roving tabindex (hasRovingTabIndex)', () => {
|
|
|
130
133
|
expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
|
|
131
134
|
});
|
|
132
135
|
});
|
|
136
|
+
|
|
137
|
+
describe('useGridFocus RTL auto-detection (WCAG 1.3.2)', () => {
|
|
138
|
+
// jsdom reflects the `dir` attribute into computed style only on the element
|
|
139
|
+
// that carries it, so tests set dir="rtl" on the grid container itself — the
|
|
140
|
+
// element the hook reads via gridRef.
|
|
141
|
+
|
|
142
|
+
it('auto-detects dir="rtl": ArrowLeft moves forward (next cell)', () => {
|
|
143
|
+
render(<Grid seed={1} dir="rtl" />);
|
|
144
|
+
const grid = screen.getByRole('grid');
|
|
145
|
+
screen.getByTestId('cell-1').focus();
|
|
146
|
+
fireEvent.keyDown(grid, {key: 'ArrowLeft'});
|
|
147
|
+
expect(screen.getByTestId('cell-2')).toHaveFocus();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('auto-detects dir="rtl": ArrowRight moves backward (previous cell)', () => {
|
|
151
|
+
render(<Grid seed={1} dir="rtl" />);
|
|
152
|
+
const grid = screen.getByRole('grid');
|
|
153
|
+
screen.getByTestId('cell-1').focus();
|
|
154
|
+
fireEvent.keyDown(grid, {key: 'ArrowRight'});
|
|
155
|
+
expect(screen.getByTestId('cell-0')).toHaveFocus();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('leaves vertical navigation unaffected under dir="rtl"', () => {
|
|
159
|
+
render(<Grid seed={0} dir="rtl" />);
|
|
160
|
+
const grid = screen.getByRole('grid');
|
|
161
|
+
screen.getByTestId('cell-0').focus();
|
|
162
|
+
fireEvent.keyDown(grid, {key: 'ArrowDown'});
|
|
163
|
+
expect(screen.getByTestId('cell-3')).toHaveFocus();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('explicit isRtl={false} overrides a dir="rtl" container', () => {
|
|
167
|
+
render(<Grid seed={1} dir="rtl" isRtl={false} />);
|
|
168
|
+
const grid = screen.getByRole('grid');
|
|
169
|
+
screen.getByTestId('cell-1').focus();
|
|
170
|
+
fireEvent.keyDown(grid, {key: 'ArrowRight'});
|
|
171
|
+
expect(screen.getByTestId('cell-2')).toHaveFocus();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useGridFocus.ts
|
|
7
|
-
* @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
|
|
7
|
+
* @input Uses React useCallback, useRef, useIsomorphicLayoutEffect,
|
|
8
|
+
* isRtlElement
|
|
8
9
|
* @output Exports useGridFocus hook for grid keyboard navigation
|
|
9
|
-
* @position Core hook; used by Calendar for date grid navigation
|
|
10
|
+
* @position Core hook; used by Calendar for date grid navigation. Auto-detects
|
|
11
|
+
* RTL from the container's computed direction so horizontal arrow keys
|
|
12
|
+
* follow visual direction (WCAG 1.3.2).
|
|
10
13
|
*
|
|
11
14
|
* SYNC: When modified, update:
|
|
12
15
|
* - /packages/core/src/hooks/index.ts
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import {useCallback, useRef} from 'react';
|
|
20
|
+
import {isRtlElement} from './isRtlElement';
|
|
17
21
|
import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -88,7 +92,12 @@ export interface UseGridFocusOptions {
|
|
|
88
92
|
/**
|
|
89
93
|
* Whether the grid is in a right-to-left context. When true, ArrowLeft and
|
|
90
94
|
* ArrowRight are swapped so horizontal navigation follows visual direction.
|
|
91
|
-
*
|
|
95
|
+
*
|
|
96
|
+
* When omitted, the direction is auto-detected from the container's computed
|
|
97
|
+
* `direction` (read lazily on keydown, only for horizontal arrow keys), so
|
|
98
|
+
* grids inside `dir="rtl"` subtrees flip automatically. Pass an explicit
|
|
99
|
+
* boolean to override detection.
|
|
100
|
+
* @default undefined (auto-detect from the container)
|
|
92
101
|
*/
|
|
93
102
|
isRtl?: boolean;
|
|
94
103
|
|
|
@@ -217,7 +226,7 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
|
|
|
217
226
|
onNavigateAfter,
|
|
218
227
|
onPageUp,
|
|
219
228
|
onPageDown,
|
|
220
|
-
isRtl
|
|
229
|
+
isRtl,
|
|
221
230
|
hasRovingTabIndex = false,
|
|
222
231
|
} = options;
|
|
223
232
|
|
|
@@ -455,13 +464,15 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
|
|
|
455
464
|
|
|
456
465
|
// In RTL, ArrowLeft/ArrowRight are swapped so horizontal navigation
|
|
457
466
|
// follows visual direction. Vertical keys (Up/Down) are unaffected.
|
|
467
|
+
// Direction is resolved lazily — getComputedStyle runs only when a
|
|
468
|
+
// horizontal arrow key is actually pressed (SSR-safe, no layout thrash
|
|
469
|
+
// on unrelated keys) — and an explicit `isRtl` always wins.
|
|
458
470
|
let key = e.key;
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
471
|
+
if (
|
|
472
|
+
(key === 'ArrowLeft' || key === 'ArrowRight') &&
|
|
473
|
+
(isRtl ?? isRtlElement(gridRef.current))
|
|
474
|
+
) {
|
|
475
|
+
key = key === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft';
|
|
465
476
|
}
|
|
466
477
|
|
|
467
478
|
switch (key) {
|
package/src/hooks/useHotkeys.ts
CHANGED
|
@@ -155,6 +155,15 @@ function matchesCombo(
|
|
|
155
155
|
* definitions live in a ref, so re-renders update behavior without
|
|
156
156
|
* re-subscribing. First matching hotkey wins per event.
|
|
157
157
|
*
|
|
158
|
+
* Accessibility (WCAG 2.1.4 — Character Key Shortcuts): a shortcut whose
|
|
159
|
+
* combo is a single unmodified character (letter, digit, punctuation or
|
|
160
|
+
* symbol — e.g. 'c' or '/') is easily triggered by accident by speech-input
|
|
161
|
+
* users and keyboard users with motor impairments. If you register one, you
|
|
162
|
+
* MUST also provide at least one of: (a) a way to turn the shortcut off,
|
|
163
|
+
* (b) a way to remap it to include a modifier, or (c) scope it so it is only
|
|
164
|
+
* active while the relevant component has focus. Prefer `mod`-based combos
|
|
165
|
+
* where possible; the `isDisabled` flag can back a user-facing off switch.
|
|
166
|
+
*
|
|
158
167
|
* @param hotkeys - Shortcut registrations to listen for.
|
|
159
168
|
*
|
|
160
169
|
* @example
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import type {ReactElement} from 'react';
|
|
5
|
+
import {render, screen} from '@testing-library/react';
|
|
6
|
+
import {TextInput} from '../TextInput/TextInput';
|
|
7
|
+
import {TextArea} from '../TextArea/TextArea';
|
|
8
|
+
import {NumberInput} from '../NumberInput/NumberInput';
|
|
9
|
+
import {DateInput} from '../DateInput/DateInput';
|
|
10
|
+
import {DateRangeInput} from '../DateRangeInput/DateRangeInput';
|
|
11
|
+
import {TimeInput} from '../TimeInput/TimeInput';
|
|
12
|
+
import {FileInput} from '../FileInput/FileInput';
|
|
13
|
+
import type {FieldStatusVariant} from '../FieldStatus/FieldStatus';
|
|
14
|
+
|
|
15
|
+
// Every id referenced by an aria-describedby must resolve to an element in the
|
|
16
|
+
// document (WCAG 1.3.1) — a described-by pointing at a non-rendered node is a
|
|
17
|
+
// dangling reference. The tooltip variant is the interesting case: it swaps the
|
|
18
|
+
// message box for a tooltip layer, so the referenced ids change per variant.
|
|
19
|
+
function expectNoDanglingDescribedBy(root: HTMLElement) {
|
|
20
|
+
for (const el of Array.from(root.querySelectorAll('[aria-describedby]'))) {
|
|
21
|
+
const value = el.getAttribute('aria-describedby') ?? '';
|
|
22
|
+
for (const id of value.split(/\s+/).filter(Boolean)) {
|
|
23
|
+
expect(
|
|
24
|
+
document.getElementById(id),
|
|
25
|
+
`dangling aria-describedby id "${id}" on <${el.tagName.toLowerCase()}>`,
|
|
26
|
+
).not.toBeNull();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VARIANTS: FieldStatusVariant[] = ['attached', 'detached', 'tooltip'];
|
|
32
|
+
|
|
33
|
+
const INPUTS: [string, (variant: FieldStatusVariant) => ReactElement][] = [
|
|
34
|
+
[
|
|
35
|
+
'TextInput',
|
|
36
|
+
v => (
|
|
37
|
+
<TextInput
|
|
38
|
+
label="Field"
|
|
39
|
+
value=""
|
|
40
|
+
onChange={() => {}}
|
|
41
|
+
status={{type: 'error', message: 'Message'}}
|
|
42
|
+
statusVariant={v}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
'TextArea',
|
|
48
|
+
v => (
|
|
49
|
+
<TextArea
|
|
50
|
+
label="Field"
|
|
51
|
+
value=""
|
|
52
|
+
onChange={() => {}}
|
|
53
|
+
status={{type: 'error', message: 'Message'}}
|
|
54
|
+
statusVariant={v}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
'NumberInput',
|
|
60
|
+
v => (
|
|
61
|
+
<NumberInput
|
|
62
|
+
label="Field"
|
|
63
|
+
value={undefined}
|
|
64
|
+
onChange={() => {}}
|
|
65
|
+
status={{type: 'error', message: 'Message'}}
|
|
66
|
+
statusVariant={v}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
'DateInput',
|
|
72
|
+
v => (
|
|
73
|
+
<DateInput
|
|
74
|
+
label="Field"
|
|
75
|
+
value={undefined}
|
|
76
|
+
onChange={() => {}}
|
|
77
|
+
status={{type: 'error', message: 'Message'}}
|
|
78
|
+
statusVariant={v}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
'DateRangeInput',
|
|
84
|
+
v => (
|
|
85
|
+
<DateRangeInput
|
|
86
|
+
label="Field"
|
|
87
|
+
value={null}
|
|
88
|
+
onChange={() => {}}
|
|
89
|
+
status={{type: 'error', message: 'Message'}}
|
|
90
|
+
statusVariant={v}
|
|
91
|
+
/>
|
|
92
|
+
),
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
'TimeInput',
|
|
96
|
+
v => (
|
|
97
|
+
<TimeInput
|
|
98
|
+
label="Field"
|
|
99
|
+
value={undefined}
|
|
100
|
+
onChange={() => {}}
|
|
101
|
+
status={{type: 'error', message: 'Message'}}
|
|
102
|
+
statusVariant={v}
|
|
103
|
+
/>
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
'FileInput',
|
|
108
|
+
v => (
|
|
109
|
+
<FileInput
|
|
110
|
+
label="Field"
|
|
111
|
+
value={null}
|
|
112
|
+
onChange={() => {}}
|
|
113
|
+
status={{type: 'error', message: 'Message'}}
|
|
114
|
+
statusVariant={v}
|
|
115
|
+
/>
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
describe('useInputStatusIcon — no dangling aria-describedby (WCAG 1.3.1)', () => {
|
|
121
|
+
for (const [name, make] of INPUTS) {
|
|
122
|
+
for (const variant of VARIANTS) {
|
|
123
|
+
it(`${name} / statusVariant="${variant}"`, () => {
|
|
124
|
+
const {container} = render(make(variant));
|
|
125
|
+
expectNoDanglingDescribedBy(container);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
it('tooltip variant links the input to the rendered tooltip element', () => {
|
|
131
|
+
render(
|
|
132
|
+
<TextInput
|
|
133
|
+
label="Field"
|
|
134
|
+
value=""
|
|
135
|
+
onChange={() => {}}
|
|
136
|
+
status={{type: 'error', message: 'Message'}}
|
|
137
|
+
statusVariant="tooltip"
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
const input = screen.getByRole('textbox');
|
|
141
|
+
const tooltip = screen.getByRole('tooltip', {hidden: true});
|
|
142
|
+
expect(input.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not render a tooltip (or reference one) when there is no message', () => {
|
|
146
|
+
// No message → the tooltip variant renders no tooltip layer and no
|
|
147
|
+
// described-by should point at a non-existent status element.
|
|
148
|
+
const {container} = render(
|
|
149
|
+
<TextInput
|
|
150
|
+
label="Field"
|
|
151
|
+
value=""
|
|
152
|
+
onChange={() => {}}
|
|
153
|
+
status={{type: 'error'}}
|
|
154
|
+
statusVariant="tooltip"
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
expect(
|
|
158
|
+
screen.queryByRole('tooltip', {hidden: true}),
|
|
159
|
+
).not.toBeInTheDocument();
|
|
160
|
+
expectNoDanglingDescribedBy(container);
|
|
161
|
+
});
|
|
162
|
+
});
|