@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1
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 +116 -0
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +5 -2
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +191 -32
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +5 -0
- package/dist/Chat/ChatMessage.d.ts +11 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +18 -9
- package/dist/Chat/useTriggerMenu.d.ts +9 -2
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +222 -50
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +158 -38
- package/dist/CodeBlock/tokenizer.d.ts +39 -0
- package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
- package/dist/CodeBlock/tokenizer.js +108 -0
- package/dist/CommandPalette/CommandPalette.d.ts +9 -5
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +38 -20
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
- package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +35 -18
- package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +8 -9
- package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
- package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteList.js +10 -5
- package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +12 -3
- package/dist/ContextMenu/ContextMenu.d.ts +3 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +62 -48
- package/dist/DateInput/DateInput.js +1 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +12 -4
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +12 -2
- package/dist/Dialog/Dialog.d.ts +1 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +16 -5
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +7 -7
- package/dist/Divider/Divider.d.ts +1 -0
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +5 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +161 -65
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
- package/dist/Field/InputClearButton.d.ts +8 -4
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +23 -7
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -0
- package/dist/Grid/Grid.d.ts +13 -4
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +7 -1
- package/dist/HoverCard/useHoverCard.d.ts +1 -1
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +67 -49
- package/dist/InputGroup/InputGroup.d.ts +2 -4
- package/dist/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/InputGroup/InputGroup.js +2 -4
- package/dist/InputGroup/groupStyles.d.ts +10 -2
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +19 -2
- package/dist/Layer/useLayerDismissal.d.ts +9 -9
- package/dist/Layer/useLayerDismissal.js +9 -9
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +25 -4
- package/dist/MobileNav/MobileNav.d.ts +7 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +45 -2
- package/dist/MultiSelector/MultiSelector.d.ts +7 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +24 -3
- package/dist/NumberInput/NumberInput.js +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
- package/dist/Resizable/ResizeHandle.d.ts +1 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +83 -15
- package/dist/Resizable/useResizable.d.ts +23 -0
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +78 -0
- package/dist/Selector/Selector.d.ts +6 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +28 -6
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +10 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +18 -7
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +16 -4
- package/dist/Toolbar/Toolbar.d.ts +2 -1
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +3 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +6 -3
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +7 -1
- package/dist/astryx.css +22 -0
- package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
- package/dist/hooks/useScrollOverflow.js +55 -12
- package/dist/hooks/useTreeFocus.d.ts +21 -0
- package/dist/hooks/useTreeFocus.d.ts.map +1 -1
- package/dist/hooks/useTreeFocus.js +40 -2
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +3 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
- package/dist/reset.a11y.chromium.spec.js +6 -1
- package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
- package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
- package/dist/reset.until-found.a11y.browser.spec.js +71 -0
- package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/SyntaxTheme.js +43 -2
- package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
- package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
- package/dist/theme/syntax/index.d.ts +1 -1
- package/dist/theme/syntax/index.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.d.ts +12 -2
- package/dist/theme/syntax/tokens.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.js +25 -4
- package/dist/utils/plainDate.d.ts +12 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +63 -10
- package/locales/en.json +13 -1
- package/locales/pseudo.json +9 -0
- package/package.json +4 -4
- package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
- package/src/AlertDialog/AlertDialog.spec.md +205 -0
- package/src/AlertDialog/AlertDialog.test.tsx +3 -14
- package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
- package/src/Button/Button.test.tsx +37 -0
- package/src/Button/Button.tsx +5 -1
- package/src/Calendar/Calendar.doc.mjs +234 -60
- package/src/Calendar/Calendar.spec.md +209 -0
- package/src/Calendar/Calendar.test.tsx +559 -20
- package/src/Calendar/Calendar.tsx +258 -41
- package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
- package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
- package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
- package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
- package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
- package/src/Carousel/Carousel.test.tsx +54 -71
- package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
- package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
- package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
- package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
- package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
- package/src/Chat/ChatComposerInput.doc.mjs +1 -1
- package/src/Chat/ChatComposerInput.spec.md +94 -55
- package/src/Chat/ChatComposerInput.test.tsx +202 -1
- package/src/Chat/ChatComposerInput.tsx +13 -0
- package/src/Chat/ChatMessage.doc.mjs +31 -16
- package/src/Chat/ChatMessage.spec.md +265 -0
- package/src/Chat/ChatMessage.test.tsx +246 -0
- package/src/Chat/ChatMessage.tsx +30 -7
- package/src/Chat/ChatSystemMessage.spec.md +36 -37
- package/src/Chat/ChatSystemMessage.test.tsx +4 -4
- package/src/Chat/useTriggerMenu.test.tsx +994 -0
- package/src/Chat/useTriggerMenu.tsx +278 -46
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
- package/src/CodeBlock/CodeBlock.spec.md +40 -41
- package/src/CodeBlock/CodeBlock.test.tsx +89 -9
- package/src/CodeBlock/CodeBlock.tsx +205 -25
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
- package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
- package/src/CodeBlock/tokenizer.test.ts +112 -0
- package/src/CodeBlock/tokenizer.ts +106 -0
- package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
- package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
- package/src/CommandPalette/CommandPalette.spec.md +100 -49
- package/src/CommandPalette/CommandPalette.test.tsx +241 -2
- package/src/CommandPalette/CommandPalette.tsx +47 -21
- package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
- package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
- package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
- package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
- package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
- package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
- package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
- package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
- package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
- package/src/CommandPalette/CommandPaletteList.tsx +10 -5
- package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
- package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
- package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
- package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
- package/src/ComplexSelector/ComplexSelector.tsx +37 -8
- package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.spec.md +51 -53
- package/src/ContextMenu/ContextMenu.test.tsx +132 -1
- package/src/ContextMenu/ContextMenu.tsx +78 -48
- package/src/DateInput/DateInput.doc.mjs +52 -0
- package/src/DateInput/DateInput.spec.md +27 -15
- package/src/DateInput/DateInput.test.tsx +38 -0
- package/src/DateInput/DateInput.tsx +1 -1
- package/src/DateInput/DateInputTouch.test.tsx +22 -0
- package/src/DateInput/MonthScroller.tsx +12 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
- package/src/DateRangeInput/DateRangeInput.tsx +20 -2
- package/src/Dialog/Dialog.test.tsx +27 -1
- package/src/Dialog/Dialog.tsx +16 -5
- package/src/Dialog/DialogHeader.test.tsx +27 -2
- package/src/Dialog/DialogHeader.tsx +7 -8
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
- package/src/Divider/Divider.doc.mjs +5 -3
- package/src/Divider/Divider.spec.md +140 -54
- package/src/Divider/Divider.test.tsx +15 -0
- package/src/Divider/Divider.tsx +6 -4
- package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
- package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
- package/src/DropdownMenu/DropdownMenu.tsx +212 -81
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
- package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
- package/src/Field/InputClearButton.test.tsx +29 -4
- package/src/Field/InputClearButton.tsx +26 -8
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
- package/src/FormLayout/FormLayout.tsx +1 -0
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
- package/src/Grid/Grid.doc.mjs +14 -11
- package/src/Grid/Grid.spec.md +50 -32
- package/src/Grid/Grid.tsx +13 -4
- package/src/HoverCard/HoverCard.test.tsx +46 -1
- package/src/HoverCard/useHoverCard.tsx +68 -48
- package/src/InputGroup/InputGroup.doc.mjs +11 -1
- package/src/InputGroup/InputGroup.tsx +2 -4
- package/src/InputGroup/groupStyles.ts +40 -2
- package/src/Layer/useLayerDismissal.ts +9 -9
- package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
- package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
- package/src/Link/__tests__/Link.a11y.states.ts +104 -0
- package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
- package/src/Markdown/Markdown.doc.mjs +1 -1
- package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
- package/src/Markdown/Markdown.spec.md +45 -23
- package/src/Markdown/Markdown.test.tsx +38 -0
- package/src/Markdown/Markdown.tsx +24 -2
- package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
- package/src/MobileNav/MobileNav.tsx +50 -2
- package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
- package/src/MoreMenu/MoreMenu.spec.md +18 -7
- package/src/MoreMenu/MoreMenu.test.tsx +38 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
- package/src/MultiSelector/MultiSelector.spec.md +1 -1
- package/src/MultiSelector/MultiSelector.test.tsx +188 -21
- package/src/MultiSelector/MultiSelector.tsx +50 -11
- package/src/NumberInput/NumberInput.test.tsx +24 -0
- package/src/NumberInput/NumberInput.tsx +1 -1
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
- package/src/Resizable/Resizable.spec.md +41 -9
- package/src/Resizable/ResizeHandle.test.tsx +565 -2
- package/src/Resizable/ResizeHandle.tsx +126 -21
- package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
- package/src/Resizable/useResizable.ts +95 -0
- package/src/Selector/Selector.doc.mjs +7 -6
- package/src/Selector/Selector.spec.md +37 -24
- package/src/Selector/Selector.test.tsx +176 -10
- package/src/Selector/Selector.tsx +59 -14
- package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
- package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
- package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
- package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/SideNav/SideNav.tsx +16 -2
- package/src/Slider/Slider.spec.md +43 -17
- package/src/Slider/Slider.test.tsx +134 -0
- package/src/Slider/Slider.tsx +29 -6
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
- package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
- package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
- package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
- package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
- package/src/Tokenizer/Tokenizer.test.tsx +87 -0
- package/src/Tokenizer/Tokenizer.tsx +31 -3
- package/src/Toolbar/Toolbar.test.tsx +131 -4
- package/src/Toolbar/Toolbar.tsx +3 -1
- package/src/TreeList/TreeList.spec.md +33 -8
- package/src/TreeList/TreeList.test.tsx +72 -0
- package/src/TreeList/TreeList.tsx +10 -6
- package/src/TreeList/TreeListItem.tsx +5 -0
- package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
- package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
- package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
- package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
- package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
- package/src/hooks/useScrollOverflow.test.tsx +222 -0
- package/src/hooks/useScrollOverflow.ts +55 -13
- package/src/hooks/useTreeFocus.test.tsx +37 -0
- package/src/hooks/useTreeFocus.ts +55 -2
- package/src/i18n/generated-locales/en.generated.ts +3 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
- package/src/reset.a11y.chromium.spec.ts +9 -1
- package/src/reset.css +8 -3
- package/src/reset.until-found.a11y.browser.spec.ts +75 -0
- package/src/reset.until-found.test.ts +32 -0
- package/src/stylexValidation.source-build.test.mjs +42 -0
- package/src/theme/syntax/SyntaxTheme.tsx +46 -5
- package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
- package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
- package/src/theme/syntax/index.ts +5 -1
- package/src/theme/syntax/tokens.ts +32 -2
- package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
- package/src/utils/plainDate.test.ts +147 -0
- package/src/utils/plainDate.ts +75 -10
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MobileNavFocus.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, MobileNav + AppShell + SideNav
|
|
6
|
+
* @output Regression tests for focus restoration when the drawer closes
|
|
7
|
+
* @position Testing; validates the focus restore in MobileNav.tsx
|
|
8
|
+
*
|
|
9
|
+
* Repro for the umbrella accessibility issue (#3343): closing the drawer
|
|
10
|
+
* dropped focus to `<body>` instead of returning it to the element that
|
|
11
|
+
* opened the drawer.
|
|
12
|
+
*
|
|
13
|
+
* jsdom has NO `<dialog>` implementation at all — `HTMLDialogElement.prototype`
|
|
14
|
+
* exposes only `open`, with no `show`/`showModal`/`close` (verified on
|
|
15
|
+
* jsdom 27.4.0). The other MobileNav suites polyfill those two methods as
|
|
16
|
+
* plain attribute toggles, which means focus never moves in the first place
|
|
17
|
+
* and the defect is invisible.
|
|
18
|
+
*
|
|
19
|
+
* These tests model the two halves of real browser behaviour explicitly:
|
|
20
|
+
* - `showModal()` runs the dialog focusing steps, so focus moves INTO the
|
|
21
|
+
* drawer. Without this the bug cannot reproduce.
|
|
22
|
+
* - `close()` does NOT hand focus back. That is the behaviour reported
|
|
23
|
+
* against Safari 26.5.2, and it is what the component must not depend on.
|
|
24
|
+
*
|
|
25
|
+
* The native close is deferred past the slide-out (#4290), and the restore has
|
|
26
|
+
* to follow it: while a modal <dialog> holds the top layer the rest of the
|
|
27
|
+
* document is inert, so `focus()` on the opener is silently dropped. jsdom has
|
|
28
|
+
* no top layer and cannot reproduce that, so these tests pin the call order
|
|
29
|
+
* and the timing directly instead.
|
|
30
|
+
*
|
|
31
|
+
* SYNC: When MobileNav.tsx's focus handling changes, update these tests.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
describe,
|
|
36
|
+
it,
|
|
37
|
+
expect,
|
|
38
|
+
vi,
|
|
39
|
+
beforeAll,
|
|
40
|
+
beforeEach,
|
|
41
|
+
afterEach,
|
|
42
|
+
} from 'vitest';
|
|
43
|
+
import {useState} from 'react';
|
|
44
|
+
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
45
|
+
import {MobileNav} from './MobileNav';
|
|
46
|
+
import {AppShell} from '../AppShell/AppShell';
|
|
47
|
+
import {SideNav, SideNavItem, SideNavSection} from '../SideNav';
|
|
48
|
+
import {stubMatchMedia} from '../__tests__/stubMatchMedia';
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
// Model the dialog focusing steps: showModal() moves focus into the dialog.
|
|
52
|
+
HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
|
|
53
|
+
this.setAttribute('open', '');
|
|
54
|
+
const target = this.querySelector<HTMLElement>('[tabindex]');
|
|
55
|
+
target?.focus();
|
|
56
|
+
};
|
|
57
|
+
// Deliberately does NOT restore focus — mirrors the reported Safari
|
|
58
|
+
// behaviour the component must not rely on.
|
|
59
|
+
HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
|
|
60
|
+
this.removeAttribute('open');
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
class MockResizeObserver {
|
|
65
|
+
observe() {}
|
|
66
|
+
unobserve() {}
|
|
67
|
+
disconnect() {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// AppShell needs its breakpoint query to match, but a blanket `matches: true`
|
|
71
|
+
// would also match `prefers-reduced-motion`, which caps the close delay at 0.
|
|
72
|
+
// The restore has to wait for that delay, so keep the real one in play.
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
|
|
75
|
+
stubMatchMedia({reduceMotion: false});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
document.documentElement.style.overflow = '';
|
|
80
|
+
vi.restoreAllMocks();
|
|
81
|
+
vi.unstubAllGlobals();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Controlled standalone harness with a real trigger button. */
|
|
85
|
+
function StandaloneHarness({onMounted}: {onMounted?: () => void} = {}) {
|
|
86
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
data-testid="trigger"
|
|
92
|
+
onClick={() => {
|
|
93
|
+
onMounted?.();
|
|
94
|
+
setIsOpen(true);
|
|
95
|
+
}}>
|
|
96
|
+
Open nav
|
|
97
|
+
</button>
|
|
98
|
+
<MobileNav
|
|
99
|
+
isOpen={isOpen}
|
|
100
|
+
onOpenChange={setIsOpen}
|
|
101
|
+
side="start"
|
|
102
|
+
data-testid="nav">
|
|
103
|
+
<a href="/home" data-testid="nav-link">
|
|
104
|
+
Home
|
|
105
|
+
</a>
|
|
106
|
+
</MobileNav>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Standalone harness whose whole subtree — trigger included — can unmount. */
|
|
112
|
+
function UnmountHarness() {
|
|
113
|
+
const [isMounted, setIsMounted] = useState(true);
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
data-testid="unmount"
|
|
119
|
+
onClick={() => setIsMounted(false)}>
|
|
120
|
+
Unmount
|
|
121
|
+
</button>
|
|
122
|
+
{isMounted ? <StandaloneHarness /> : null}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The drawer alone can unmount; the trigger stays in the document. */
|
|
128
|
+
function DrawerUnmountHarness() {
|
|
129
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
130
|
+
const [hasDrawer, setHasDrawer] = useState(true);
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
data-testid="trigger"
|
|
136
|
+
onClick={() => setIsOpen(true)}>
|
|
137
|
+
Open nav
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
data-testid="unmount-drawer"
|
|
142
|
+
onClick={() => setHasDrawer(false)}>
|
|
143
|
+
Unmount drawer
|
|
144
|
+
</button>
|
|
145
|
+
{hasDrawer ? (
|
|
146
|
+
<MobileNav
|
|
147
|
+
isOpen={isOpen}
|
|
148
|
+
onOpenChange={setIsOpen}
|
|
149
|
+
side="start"
|
|
150
|
+
data-testid="nav">
|
|
151
|
+
<span>Content</span>
|
|
152
|
+
</MobileNav>
|
|
153
|
+
) : null}
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Removes the trigger from the DOM in the same commit that closes the drawer. */
|
|
159
|
+
function DetachHarness() {
|
|
160
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
161
|
+
const [hasTrigger, setHasTrigger] = useState(true);
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
{hasTrigger ? (
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
data-testid="trigger"
|
|
168
|
+
onClick={() => setIsOpen(true)}>
|
|
169
|
+
Open nav
|
|
170
|
+
</button>
|
|
171
|
+
) : null}
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
data-testid="detach"
|
|
175
|
+
onClick={() => {
|
|
176
|
+
setHasTrigger(false);
|
|
177
|
+
setIsOpen(false);
|
|
178
|
+
}}>
|
|
179
|
+
Detach
|
|
180
|
+
</button>
|
|
181
|
+
<MobileNav isOpen={isOpen} onOpenChange={setIsOpen} side="start">
|
|
182
|
+
<span>Content</span>
|
|
183
|
+
</MobileNav>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Opens the drawer without ever focusing the button that opens it. */
|
|
189
|
+
function BodyHarness() {
|
|
190
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
data-testid="programmatic"
|
|
196
|
+
onClick={() => setIsOpen(v => !v)}>
|
|
197
|
+
Toggle
|
|
198
|
+
</button>
|
|
199
|
+
<MobileNav isOpen={isOpen} onOpenChange={setIsOpen} side="start">
|
|
200
|
+
<span>Content</span>
|
|
201
|
+
</MobileNav>
|
|
202
|
+
</>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function AppShellHarness() {
|
|
207
|
+
return (
|
|
208
|
+
<AppShell
|
|
209
|
+
sideNav={
|
|
210
|
+
<SideNav>
|
|
211
|
+
<SideNavSection title="Test" isHeaderHidden>
|
|
212
|
+
<SideNavItem label="Home" />
|
|
213
|
+
</SideNavSection>
|
|
214
|
+
</SideNav>
|
|
215
|
+
}
|
|
216
|
+
mobileNav={{breakpoint: 'md'}}>
|
|
217
|
+
<div>Content</div>
|
|
218
|
+
</AppShell>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Opens the standalone drawer from a focused trigger and returns the trigger. */
|
|
223
|
+
function openFromTrigger(): HTMLElement {
|
|
224
|
+
const trigger = screen.getByTestId('trigger');
|
|
225
|
+
trigger.focus();
|
|
226
|
+
fireEvent.click(trigger);
|
|
227
|
+
// showModal() pulled focus into the drawer.
|
|
228
|
+
expect(trigger).not.toHaveFocus();
|
|
229
|
+
return trigger;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function clickDrawerClose() {
|
|
233
|
+
fireEvent.click(screen.getByRole('button', {name: /close navigation/i}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
describe('MobileNav focus restoration', () => {
|
|
237
|
+
it('returns focus to the trigger when the drawer closes (standalone)', () => {
|
|
238
|
+
vi.useFakeTimers();
|
|
239
|
+
try {
|
|
240
|
+
render(<StandaloneHarness />);
|
|
241
|
+
const trigger = openFromTrigger();
|
|
242
|
+
|
|
243
|
+
clickDrawerClose();
|
|
244
|
+
act(() => {
|
|
245
|
+
vi.advanceTimersByTime(400);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(trigger).toHaveFocus();
|
|
249
|
+
} finally {
|
|
250
|
+
vi.useRealTimers();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns focus to the hamburger toggle when the AppShell drawer closes', () => {
|
|
255
|
+
vi.useFakeTimers();
|
|
256
|
+
try {
|
|
257
|
+
render(<AppShellHarness />);
|
|
258
|
+
|
|
259
|
+
const toggle = screen.getByRole('button', {name: /open navigation/i});
|
|
260
|
+
toggle.focus();
|
|
261
|
+
fireEvent.click(toggle);
|
|
262
|
+
|
|
263
|
+
expect(toggle).not.toHaveFocus();
|
|
264
|
+
|
|
265
|
+
// Inside AppShell the drawer sits in an <Activity> that hides on close,
|
|
266
|
+
// so the native close is the unmount-style teardown, not the delayed
|
|
267
|
+
// timer. Focus still has to come back, and still only after close().
|
|
268
|
+
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
|
|
269
|
+
const focusSpy = vi.spyOn(toggle, 'focus');
|
|
270
|
+
|
|
271
|
+
fireEvent.click(screen.getByRole('button', {name: /close navigation/i}));
|
|
272
|
+
act(() => {
|
|
273
|
+
vi.advanceTimersByTime(400);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(document.activeElement).not.toBe(document.body);
|
|
277
|
+
expect(toggle).toHaveFocus();
|
|
278
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
279
|
+
expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
280
|
+
closeSpy.mock.invocationCallOrder[0],
|
|
281
|
+
);
|
|
282
|
+
} finally {
|
|
283
|
+
vi.useRealTimers();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('restores focus only after the native dialog has closed', () => {
|
|
288
|
+
// Load-bearing ordering contract. Browsers hold the rest of the document
|
|
289
|
+
// inert while a modal <dialog> is open, so focusing the trigger before
|
|
290
|
+
// close() would be silently dropped — the exact class of failure this fix
|
|
291
|
+
// exists to remove. jsdom has no top layer and cannot reproduce that, so
|
|
292
|
+
// assert the call order directly instead. Restoring when `isOpen` flips
|
|
293
|
+
// (in an effect or its cleanup) instead of after the deferred close()
|
|
294
|
+
// breaks this.
|
|
295
|
+
vi.useFakeTimers();
|
|
296
|
+
try {
|
|
297
|
+
render(<StandaloneHarness />);
|
|
298
|
+
const trigger = openFromTrigger();
|
|
299
|
+
|
|
300
|
+
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
|
|
301
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
302
|
+
|
|
303
|
+
clickDrawerClose();
|
|
304
|
+
act(() => {
|
|
305
|
+
vi.advanceTimersByTime(400);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
309
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
310
|
+
expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
311
|
+
closeSpy.mock.invocationCallOrder[0],
|
|
312
|
+
);
|
|
313
|
+
} finally {
|
|
314
|
+
vi.useRealTimers();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('waits for the delayed close before restoring focus', () => {
|
|
319
|
+
// The close is deferred so the slide-out can play (250ms cap outside
|
|
320
|
+
// reduced motion). Until it runs the dialog is still modal and the trigger
|
|
321
|
+
// is inert, so an early restore would be a silent no-op in a browser.
|
|
322
|
+
vi.useFakeTimers();
|
|
323
|
+
try {
|
|
324
|
+
render(<StandaloneHarness />);
|
|
325
|
+
const trigger = openFromTrigger();
|
|
326
|
+
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
|
|
327
|
+
|
|
328
|
+
clickDrawerClose();
|
|
329
|
+
act(() => {
|
|
330
|
+
vi.advanceTimersByTime(200);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(closeSpy).not.toHaveBeenCalled();
|
|
334
|
+
expect(screen.getByTestId('nav')).toHaveAttribute('open');
|
|
335
|
+
expect(trigger).not.toHaveFocus();
|
|
336
|
+
|
|
337
|
+
act(() => {
|
|
338
|
+
vi.advanceTimersByTime(100);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(closeSpy).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(trigger).toHaveFocus();
|
|
343
|
+
} finally {
|
|
344
|
+
vi.useRealTimers();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('keeps focus in the drawer when it is reopened before the delayed close', () => {
|
|
349
|
+
vi.useFakeTimers();
|
|
350
|
+
try {
|
|
351
|
+
render(<StandaloneHarness />);
|
|
352
|
+
const trigger = openFromTrigger();
|
|
353
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
354
|
+
|
|
355
|
+
clickDrawerClose();
|
|
356
|
+
act(() => {
|
|
357
|
+
vi.advanceTimersByTime(100);
|
|
358
|
+
});
|
|
359
|
+
// Reopened while the close was still pending: the dialog never closed,
|
|
360
|
+
// so there is nothing to hand focus back from.
|
|
361
|
+
fireEvent.click(trigger);
|
|
362
|
+
act(() => {
|
|
363
|
+
vi.advanceTimersByTime(400);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(screen.getByTestId('nav')).toHaveAttribute('open');
|
|
367
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
368
|
+
expect(trigger).not.toHaveFocus();
|
|
369
|
+
} finally {
|
|
370
|
+
vi.useRealTimers();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('returns focus to the original opener after a reopen interrupted the close', () => {
|
|
375
|
+
vi.useFakeTimers();
|
|
376
|
+
try {
|
|
377
|
+
render(<StandaloneHarness />);
|
|
378
|
+
const trigger = openFromTrigger();
|
|
379
|
+
|
|
380
|
+
clickDrawerClose();
|
|
381
|
+
act(() => {
|
|
382
|
+
vi.advanceTimersByTime(100);
|
|
383
|
+
});
|
|
384
|
+
// The reopen happens with focus inside the drawer. That element must
|
|
385
|
+
// not replace the opener the drawer captured on the way in.
|
|
386
|
+
fireEvent.click(trigger);
|
|
387
|
+
act(() => {
|
|
388
|
+
vi.advanceTimersByTime(400);
|
|
389
|
+
});
|
|
390
|
+
expect(trigger).not.toHaveFocus();
|
|
391
|
+
|
|
392
|
+
clickDrawerClose();
|
|
393
|
+
act(() => {
|
|
394
|
+
vi.advanceTimersByTime(400);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(screen.getByTestId('nav')).not.toHaveAttribute('open');
|
|
398
|
+
expect(trigger).toHaveFocus();
|
|
399
|
+
} finally {
|
|
400
|
+
vi.useRealTimers();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('restores focus when the drawer unmounts while its delayed close is pending', () => {
|
|
405
|
+
vi.useFakeTimers();
|
|
406
|
+
try {
|
|
407
|
+
render(<DrawerUnmountHarness />);
|
|
408
|
+
const trigger = openFromTrigger();
|
|
409
|
+
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
|
|
410
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
411
|
+
|
|
412
|
+
clickDrawerClose();
|
|
413
|
+
act(() => {
|
|
414
|
+
vi.advanceTimersByTime(100);
|
|
415
|
+
});
|
|
416
|
+
expect(closeSpy).not.toHaveBeenCalled();
|
|
417
|
+
|
|
418
|
+
// Unmounting cancels the timer and closes the dialog on the spot. Focus
|
|
419
|
+
// goes back to the trigger from that close, and only after it.
|
|
420
|
+
fireEvent.click(screen.getByTestId('unmount-drawer'));
|
|
421
|
+
|
|
422
|
+
expect(screen.queryByTestId('nav')).toBeNull();
|
|
423
|
+
expect(closeSpy).toHaveBeenCalledTimes(1);
|
|
424
|
+
expect(trigger).toHaveFocus();
|
|
425
|
+
expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
426
|
+
closeSpy.mock.invocationCallOrder[0],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// The cancelled timer must not fire a second close later.
|
|
430
|
+
act(() => {
|
|
431
|
+
vi.advanceTimersByTime(400);
|
|
432
|
+
});
|
|
433
|
+
expect(closeSpy).toHaveBeenCalledTimes(1);
|
|
434
|
+
} finally {
|
|
435
|
+
vi.useRealTimers();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('returns focus to the trigger even when focus moved inside the drawer first', () => {
|
|
440
|
+
vi.useFakeTimers();
|
|
441
|
+
try {
|
|
442
|
+
render(<StandaloneHarness />);
|
|
443
|
+
const trigger = openFromTrigger();
|
|
444
|
+
|
|
445
|
+
// User tabs to a link inside the drawer before dismissing it.
|
|
446
|
+
const link = screen.getByTestId('nav-link');
|
|
447
|
+
link.focus();
|
|
448
|
+
expect(link).toHaveFocus();
|
|
449
|
+
|
|
450
|
+
clickDrawerClose();
|
|
451
|
+
act(() => {
|
|
452
|
+
vi.advanceTimersByTime(400);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(trigger).toHaveFocus();
|
|
456
|
+
} finally {
|
|
457
|
+
vi.useRealTimers();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('restores focus when the drawer is closed via Escape', () => {
|
|
462
|
+
vi.useFakeTimers();
|
|
463
|
+
try {
|
|
464
|
+
render(<StandaloneHarness />);
|
|
465
|
+
const trigger = openFromTrigger();
|
|
466
|
+
|
|
467
|
+
const dialog = screen.getByTestId('nav');
|
|
468
|
+
fireEvent(
|
|
469
|
+
dialog,
|
|
470
|
+
new Event('cancel', {bubbles: false, cancelable: true}),
|
|
471
|
+
);
|
|
472
|
+
act(() => {
|
|
473
|
+
vi.advanceTimersByTime(400);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(trigger).toHaveFocus();
|
|
477
|
+
} finally {
|
|
478
|
+
vi.useRealTimers();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('does not throw when the drawer unmounts while still open', () => {
|
|
483
|
+
vi.useFakeTimers();
|
|
484
|
+
try {
|
|
485
|
+
render(<UnmountHarness />);
|
|
486
|
+
openFromTrigger();
|
|
487
|
+
|
|
488
|
+
// Unmounting the whole drawer subtree also takes the trigger with it —
|
|
489
|
+
// the restore must not throw and must not force focus onto a detached
|
|
490
|
+
// node.
|
|
491
|
+
expect(() => {
|
|
492
|
+
fireEvent.click(screen.getByTestId('unmount'));
|
|
493
|
+
act(() => {
|
|
494
|
+
vi.advanceTimersByTime(400);
|
|
495
|
+
});
|
|
496
|
+
}).not.toThrow();
|
|
497
|
+
expect(document.activeElement).toBe(document.body);
|
|
498
|
+
} finally {
|
|
499
|
+
vi.useRealTimers();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('does not throw when the trigger is removed before the drawer closes', () => {
|
|
504
|
+
vi.useFakeTimers();
|
|
505
|
+
try {
|
|
506
|
+
render(<DetachHarness />);
|
|
507
|
+
const trigger = openFromTrigger();
|
|
508
|
+
|
|
509
|
+
// jsdom silently ignores focus() on a detached node, so assert the call
|
|
510
|
+
// is never made rather than assert on activeElement.
|
|
511
|
+
const triggerFocus = vi.spyOn(trigger, 'focus');
|
|
512
|
+
expect(() => {
|
|
513
|
+
fireEvent.click(screen.getByTestId('detach'));
|
|
514
|
+
act(() => {
|
|
515
|
+
vi.advanceTimersByTime(400);
|
|
516
|
+
});
|
|
517
|
+
}).not.toThrow();
|
|
518
|
+
expect(screen.queryByTestId('trigger')).toBeNull();
|
|
519
|
+
expect(triggerFocus).not.toHaveBeenCalled();
|
|
520
|
+
} finally {
|
|
521
|
+
vi.useRealTimers();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('leaves focus alone when nothing was focused before opening', () => {
|
|
526
|
+
vi.useFakeTimers();
|
|
527
|
+
try {
|
|
528
|
+
render(<BodyHarness />);
|
|
529
|
+
|
|
530
|
+
// fireEvent.click does not focus, so <body> is the "trigger" the drawer
|
|
531
|
+
// sees when it opens — there is nothing meaningful to restore to.
|
|
532
|
+
expect(document.activeElement).toBe(document.body);
|
|
533
|
+
fireEvent.click(screen.getByTestId('programmatic'));
|
|
534
|
+
act(() => {
|
|
535
|
+
vi.advanceTimersByTime(400);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// jsdom treats <body> as a non-focusable area, so body.focus() is a
|
|
539
|
+
// silent no-op there and an activeElement assertion cannot tell the two
|
|
540
|
+
// implementations apart. Spy on the call itself — in a real browser
|
|
541
|
+
// body.focus() DOES blur whatever is focused.
|
|
542
|
+
const bodyFocus = vi.spyOn(document.body, 'focus');
|
|
543
|
+
clickDrawerClose();
|
|
544
|
+
act(() => {
|
|
545
|
+
vi.advanceTimersByTime(400);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
expect(bodyFocus).not.toHaveBeenCalled();
|
|
549
|
+
} finally {
|
|
550
|
+
vi.useRealTimers();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
@@ -125,6 +125,16 @@ This draft does not change or extend MoreMenu's existing accessible label,
|
|
|
125
125
|
tooltip, icon-only Button semantics, focus behavior, keyboard operation, or
|
|
126
126
|
dismissal behavior.
|
|
127
127
|
|
|
128
|
+
- **AR1 — The anchored trigger is a Menu Button through DropdownMenu.** In the
|
|
129
|
+
anchored pointer presentation MoreMenu's icon-only trigger adopts the APG
|
|
130
|
+
Menu Button pattern's required parts as `component:DropdownMenu/AR4` records
|
|
131
|
+
them — `button` role, its accessible label as the name,
|
|
132
|
+
`aria-haspopup="menu"`, `aria-expanded`, `aria-controls`, Enter or Space
|
|
133
|
+
opening with focus on the first enabled row — and the same named exception
|
|
134
|
+
for ArrowDown and ArrowUp opening. MoreMenu adds nothing of its own to that
|
|
135
|
+
contract. In the bottom-sheet presentation the trigger opens a dialog and says
|
|
136
|
+
`aria-haspopup="dialog"`, outside this pattern.
|
|
137
|
+
|
|
128
138
|
## Design relationships
|
|
129
139
|
|
|
130
140
|
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
@@ -195,13 +205,14 @@ presentation.
|
|
|
195
205
|
|
|
196
206
|
## Verification map
|
|
197
207
|
|
|
198
|
-
| Contract | Verification
|
|
199
|
-
| ------------------- |
|
|
200
|
-
| FR1, FR4 | `MoreMenu.test.tsx` render, pointer, bottom-sheet, placement, and controlled suites
|
|
201
|
-
| FR2 | MoreMenu target test, source inspection, and theming target inventories
|
|
202
|
-
| FR3 | MoreMenu, DropdownMenu, Button, Icon, and List source/owner tests
|
|
203
|
-
|
|
|
204
|
-
|
|
|
208
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
209
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
|
|
210
|
+
| FR1, FR4 | `MoreMenu.test.tsx` render, pointer, bottom-sheet, placement, and controlled suites | Default, custom icon, pointer, touch | Replacing the composition or changing forwarded behavior fails existing render/interaction tests. | `audit:MoreMenu/anatomy` |
|
|
211
|
+
| FR2 | MoreMenu target test, source inspection, and theming target inventories | Closed trigger and composed menu panel | Moving `more-menu` onto the trigger or removing it from the panel fails target evidence. | `audit:MoreMenu/theming` |
|
|
212
|
+
| FR3 | MoreMenu, DropdownMenu, Button, Icon, and List source/owner tests | Trigger, icon branches, pointer rows, touch rows | A composed part loses its owner target or is documented as a new MoreMenu-owned descendant. | `audit:MoreMenu/theming` |
|
|
213
|
+
| AR1 | `MoreMenu.test.tsx` "renders trigger button with default aria-label" and "has aria-haspopup and aria-expanded attributes"; keyboard opening verified through DropdownMenu's AR4 tests; MoreMenu-bound Menu Button accessibility contract (`internal/a11y-spec`) pending | Default and custom-icon triggers; closed/open; Enter, Space, ArrowDown, ArrowUp | The trigger loses its label, `aria-haspopup="menu"`, `aria-expanded` or `aria-controls`, or a keyboard open does not land on the first (ArrowUp: last) enabled row | `audit:MoreMenu/accessibility` |
|
|
214
|
+
| Layer relationships | `MoreMenu.test.tsx`, `DropdownMenu.test.tsx`, and current layer/dismissal records | Light dismiss, Escape, pointer, touch | Documentation gives MoreMenu a parallel runtime or misstates the current dismissal owner. | `audit:MoreMenu/behavior` |
|
|
215
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:MoreMenu/theming` |
|
|
205
216
|
|
|
206
217
|
## Decision log
|
|
207
218
|
|
|
@@ -159,6 +159,44 @@ describe('MoreMenu', () => {
|
|
|
159
159
|
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
it('keeps the menu closed from every opening key and press while disabled', async () => {
|
|
163
|
+
const user = userEvent.setup();
|
|
164
|
+
render(<MoreMenu items={defaultItems} isDisabled />);
|
|
165
|
+
const button = screen.getByRole('button', {name: 'More options'});
|
|
166
|
+
button.focus();
|
|
167
|
+
for (const key of ['{Enter}', ' ', '{ArrowDown}', '{ArrowUp}']) {
|
|
168
|
+
await user.keyboard(key);
|
|
169
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
170
|
+
}
|
|
171
|
+
await user.pointer([
|
|
172
|
+
{keys: '[MouseLeft>]', target: button},
|
|
173
|
+
{keys: '[/MouseLeft]'},
|
|
174
|
+
]);
|
|
175
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('does not carry a refused ArrowUp into a later open', async () => {
|
|
179
|
+
const user = userEvent.setup();
|
|
180
|
+
const {rerender} = render(<MoreMenu items={defaultItems} isDisabled />);
|
|
181
|
+
const button = screen.getByRole('button', {name: 'More options'});
|
|
182
|
+
button.focus();
|
|
183
|
+
await user.keyboard('{ArrowUp}');
|
|
184
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
185
|
+
|
|
186
|
+
// Enabled again, a synthesized click (detail 0: screen-reader activation)
|
|
187
|
+
// opens on the FIRST row; the refused ArrowUp must not have armed "last".
|
|
188
|
+
rerender(<MoreMenu items={defaultItems} />);
|
|
189
|
+
fireEvent.click(button, {detail: 0});
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
192
|
+
});
|
|
193
|
+
await waitFor(() => {
|
|
194
|
+
expect(
|
|
195
|
+
screen.getByRole('menuitem', {name: 'Edit', hidden: true}),
|
|
196
|
+
).toHaveFocus();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
162
200
|
it('supports custom label', () => {
|
|
163
201
|
render(<MoreMenu items={defaultItems} label="Row actions" />);
|
|
164
202
|
expect(
|