@astryxdesign/core 0.6.2 → 0.6.3-canary.01fa439
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 +81 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +10 -5
- package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
- package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
- package/dist/Chat/ChatComposerDrawer.js +48 -23
- package/dist/Chat/ChatComposerInput.d.ts +11 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +63 -16
- package/dist/Chat/ChatDictationButton.d.ts +3 -2
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +6 -4
- package/dist/Chat/ChatSendButton.js +1 -1
- package/dist/Chat/index.d.ts +1 -1
- package/dist/Chat/index.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +1 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +4 -4
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +16 -16
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +2 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +4 -4
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +8 -8
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
- package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
- package/dist/Dialog/DialogHeader.d.ts +8 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -1
- package/dist/Field/InputClearButton.d.ts +4 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +4 -2
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +1 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +7 -7
- package/dist/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- package/dist/Markdown/Markdown.d.ts +8 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.bench.js +150 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
- package/dist/Markdown/Markdown.js +262 -109
- package/dist/Markdown/ast.d.ts +162 -0
- package/dist/Markdown/ast.d.ts.map +1 -0
- package/dist/Markdown/ast.js +74 -0
- package/dist/Markdown/index.d.ts +5 -3
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/index.js +3 -2
- package/dist/Markdown/parser/index.d.ts +10 -0
- package/dist/Markdown/parser/index.d.ts.map +1 -0
- package/dist/Markdown/parser/index.js +10 -0
- package/dist/Markdown/parser.d.ts +126 -58
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +716 -167
- package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
- package/dist/Markdown/plugins/frontmatter.js +132 -0
- package/dist/Markdown/plugins/index.d.ts +16 -0
- package/dist/Markdown/plugins/index.d.ts.map +1 -0
- package/dist/Markdown/plugins/index.js +13 -0
- package/dist/Markdown/plugins/protocol.d.ts +191 -0
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
- package/dist/Markdown/plugins/protocol.js +957 -0
- package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
- package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
- package/dist/Markdown/plugins/semanticFence.js +213 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
- package/dist/Markdown/plugins/sourceDecoration.js +393 -0
- package/dist/Markdown/plugins/textTransform.d.ts +30 -0
- package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
- package/dist/Markdown/plugins/textTransform.js +374 -0
- package/dist/Markdown/remark.d.ts +196 -0
- package/dist/Markdown/remark.d.ts.map +1 -0
- package/dist/Markdown/remark.js +1062 -0
- package/dist/Markdown/url.d.ts +4 -0
- package/dist/Markdown/url.d.ts.map +1 -0
- package/dist/Markdown/url.js +25 -0
- package/dist/Markdown/utils.d.ts +4 -2
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/Markdown/utils.js +2 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +4 -4
- package/dist/Outline/index.d.ts +1 -0
- package/dist/Outline/index.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
- package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +8 -3
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +36 -31
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/Selector/Selector.d.ts +9 -7
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +7 -5
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +28 -12
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -7
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- package/dist/Table/Table.d.ts +1 -0
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +29 -13
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +1 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +2 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +4 -4
- package/dist/ToggleButton/ToggleButton.d.ts +9 -6
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +15 -27
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +3 -0
- package/dist/TreeList/TreeListItem.d.ts +6 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +12 -2
- package/dist/TreeList/TreeListTypes.d.ts +8 -1
- package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -7
- package/dist/astryx.css +18 -7
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +10 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/locales/af-ZA.json +530 -1
- package/locales/ar-SA.json +490 -1
- package/locales/ca-ES.json +514 -1
- package/locales/cs-CZ.json +498 -1
- package/locales/da-DK.json +522 -1
- package/locales/de-DE.json +522 -1
- package/locales/el-GR.json +498 -1
- package/locales/es-ES.json +510 -1
- package/locales/fi-FI.json +498 -1
- package/locales/fr-FR.json +534 -1
- package/locales/he-IL.json +498 -1
- package/locales/hu-HU.json +498 -1
- package/locales/it-IT.json +510 -1
- package/locales/ja-JP.json +490 -1
- package/locales/ko-KR.json +498 -1
- package/locales/nl-NL.json +534 -1
- package/locales/no-NO.json +510 -1
- package/locales/pl-PL.json +506 -1
- package/locales/pt-BR.json +506 -1
- package/locales/pt-PT.json +506 -1
- package/locales/ro-RO.json +506 -1
- package/locales/ru-RU.json +498 -1
- package/locales/sr-SP.json +498 -1
- package/locales/sv-SE.json +502 -1
- package/locales/tr-TR.json +498 -1
- package/locales/uk-UA.json +498 -1
- package/locales/vi-VN.json +502 -1
- package/locales/zh-CN.json +490 -1
- package/locales/zh-TW.json +490 -1
- package/package.json +19 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/Chat/ChatComposer.doc.mjs +14 -0
- package/src/Chat/ChatComposer.spec.md +230 -0
- package/src/Chat/ChatComposer.test.tsx +57 -0
- package/src/Chat/ChatComposer.tsx +21 -9
- package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
- package/src/Chat/ChatComposerDrawer.spec.md +267 -0
- package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
- package/src/Chat/ChatComposerDrawer.tsx +38 -9
- package/src/Chat/ChatComposerInput.doc.mjs +114 -26
- package/src/Chat/ChatComposerInput.spec.md +185 -0
- package/src/Chat/ChatComposerInput.test.tsx +113 -3
- package/src/Chat/ChatComposerInput.tsx +78 -13
- package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
- package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
- package/src/Chat/ChatDictationButton.doc.mjs +3 -3
- package/src/Chat/ChatDictationButton.spec.md +160 -0
- package/src/Chat/ChatDictationButton.test.tsx +92 -27
- package/src/Chat/ChatDictationButton.tsx +8 -6
- package/src/Chat/ChatSendButton.test.tsx +1 -4
- package/src/Chat/ChatSendButton.tsx +1 -1
- package/src/Chat/index.ts +1 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
- package/src/ComplexSelector/ComplexSelector.tsx +6 -1
- package/src/DateInput/DateInput.tsx +6 -1
- package/src/DateInput/NativeDateField.tsx +24 -8
- package/src/DateInput/TouchDateField.tsx +6 -2
- package/src/DateRangeInput/DateRangeInput.tsx +6 -1
- package/src/DateTimeInput/DateTimeInput.tsx +6 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
- package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
- package/src/Dialog/DialogHeader.doc.mjs +12 -0
- package/src/Dialog/DialogHeader.test.tsx +97 -0
- package/src/Dialog/DialogHeader.tsx +25 -3
- package/src/Dialog/modules/DialogHeader.spec.md +75 -38
- package/src/Field/InputClearButton.test.tsx +17 -3
- package/src/Field/InputClearButton.tsx +5 -2
- package/src/Field/PanelSearchInput.tsx +5 -2
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
- package/src/FileInput/FileInput.tsx +11 -2
- package/src/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Layout/LayoutFooter.doc.mjs +17 -0
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/List/List.spec.md +200 -0
- package/src/Markdown/Markdown.doc.mjs +272 -0
- package/src/Markdown/Markdown.fr23.bench.ts +174 -0
- package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
- package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
- package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
- package/src/Markdown/Markdown.public.test.ts +126 -3
- package/src/Markdown/Markdown.spec.md +197 -44
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +414 -135
- package/src/Markdown/ast.test.ts +307 -0
- package/src/Markdown/ast.ts +337 -0
- package/src/Markdown/incremental.test.ts +12 -0
- package/src/Markdown/index.ts +37 -1
- package/src/Markdown/modules/remark.spec.md +269 -0
- package/src/Markdown/parser/index.ts +64 -0
- package/src/Markdown/parser.public.test.ts +103 -0
- package/src/Markdown/parser.ts +1180 -235
- package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
- package/src/Markdown/plugins/frontmatter.ts +198 -0
- package/src/Markdown/plugins/index.ts +53 -0
- package/src/Markdown/plugins/protocol.test.tsx +1022 -0
- package/src/Markdown/plugins/protocol.ts +1618 -0
- package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
- package/src/Markdown/plugins/semanticFence.ts +424 -0
- package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
- package/src/Markdown/plugins/sourceDecoration.ts +539 -0
- package/src/Markdown/plugins/textTransform.test.ts +467 -0
- package/src/Markdown/plugins/textTransform.ts +554 -0
- package/src/Markdown/remark.test.tsx +1877 -0
- package/src/Markdown/remark.ts +1574 -0
- package/src/Markdown/url.ts +30 -0
- package/src/Markdown/utils.ts +13 -1
- package/src/MultiSelector/MultiSelector.test.tsx +1 -35
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
- package/src/NumberInput/NumberInput.tsx +6 -1
- package/src/Outline/Outline.doc.mjs +7 -5
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/index.ts +1 -0
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
- package/src/Outline/useOutlineFromMarkdown.ts +15 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ProgressBar/ProgressBar.test.tsx +29 -11
- package/src/ProgressBar/ProgressBar.tsx +59 -72
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/Selector/Selector.doc.mjs +2 -2
- package/src/Selector/Selector.spec.md +51 -49
- package/src/Selector/Selector.test.tsx +61 -55
- package/src/Selector/Selector.tsx +23 -14
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
- package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +17 -3
- package/src/Slider/Slider.test.tsx +221 -0
- package/src/Slider/Slider.tsx +40 -9
- package/src/Spinner/Spinner.test.tsx +4 -0
- package/src/Spinner/Spinner.tsx +31 -18
- package/src/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.spec.md +204 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- package/src/Table/Table.doc.mjs +1 -1
- package/src/Table/Table.spec.md +54 -40
- package/src/Table/Table.test.tsx +83 -10
- package/src/Table/Table.tsx +30 -22
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
- package/src/TextArea/TextArea.tsx +6 -1
- package/src/TextInput/TextInput.test.tsx +20 -1
- package/src/TextInput/TextInput.tsx +6 -1
- package/src/TimeInput/TimeInput.tsx +6 -1
- package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
- package/src/ToggleButton/ToggleButton.test.tsx +299 -4
- package/src/ToggleButton/ToggleButton.tsx +26 -33
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/TreeList/TreeList.doc.mjs +3 -3
- package/src/TreeList/TreeList.spec.md +123 -75
- package/src/TreeList/TreeList.test.tsx +29 -0
- package/src/TreeList/TreeList.tsx +3 -0
- package/src/TreeList/TreeListItem.tsx +15 -1
- package/src/TreeList/TreeListTypes.ts +11 -1
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +21 -7
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -0
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/inputFontFloor.test.ts +106 -0
- package/src/tailwind-theme.css +0 -6
- package/src/theme/generateThemeRules.test.ts +30 -0
- package/src/theme/generateThemeRules.ts +10 -0
- package/src/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file pressState.ts
|
|
5
|
+
* @input Uses document.styleSheets (populated by StyleX runtime injection)
|
|
6
|
+
* @output Exports rulesDeclaredFor and expectPressedArm test helpers
|
|
7
|
+
* @position Shared test helper for asserting a control paints a pressed state
|
|
8
|
+
*
|
|
9
|
+
* jsdom does not compute `:active` (there is no real pointer), so a component
|
|
10
|
+
* test asserts the next-best thing: that the StyleX dev runtime injected a
|
|
11
|
+
* rule, keyed to the element's own classes, that paints the design system's
|
|
12
|
+
* pressed overlay token while the control is pressed. The selector shape is
|
|
13
|
+
* deliberately not pinned — a press may be read off the element itself
|
|
14
|
+
* (`.x:active`) or off an ancestor scope marker (`.x:where(.marker:active *)`).
|
|
15
|
+
*
|
|
16
|
+
* SYNC: When modified, update this header.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const PRESSED_TOKEN = '--color-overlay-pressed';
|
|
20
|
+
|
|
21
|
+
function walk(list: CSSRuleList, visit: (rule: CSSRule) => void): void {
|
|
22
|
+
for (const rule of Array.from(list)) {
|
|
23
|
+
visit(rule);
|
|
24
|
+
const nested = (rule as CSSGroupingRule).cssRules;
|
|
25
|
+
if (nested != null) {
|
|
26
|
+
walk(nested, visit);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Every injected style rule (rules nested in `@media` included) whose selector
|
|
33
|
+
* names one of the element's classes, as `selector {declarations}` text.
|
|
34
|
+
*/
|
|
35
|
+
export function rulesDeclaredFor(el: Element): string[] {
|
|
36
|
+
const classes = el.className
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map(name => `.${name}`);
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
42
|
+
let rules: CSSRuleList;
|
|
43
|
+
try {
|
|
44
|
+
rules = sheet.cssRules;
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
walk(rules, rule => {
|
|
49
|
+
if (!(rule instanceof CSSStyleRule)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const selector = rule.selectorText;
|
|
53
|
+
if (classes.some(cls => selector.startsWith(cls))) {
|
|
54
|
+
out.push(`${selector} {${rule.style.cssText}}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The rules on `el` whose selector matches `pseudo` (e.g. `:active`). */
|
|
62
|
+
export function rulesWithSelector(el: Element, pseudo: string): string[] {
|
|
63
|
+
return rulesDeclaredFor(el).filter(rule => {
|
|
64
|
+
const selector = rule.slice(0, rule.indexOf('{'));
|
|
65
|
+
return selector.includes(pseudo);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Does one of the element's own rules paint the pressed overlay token while
|
|
71
|
+
* `pseudo` matches? `pseudo` defaults to `:active`, the arm a mouse press
|
|
72
|
+
* takes.
|
|
73
|
+
*/
|
|
74
|
+
export function hasPressedArm(el: Element, pseudo = ':active'): boolean {
|
|
75
|
+
return rulesWithSelector(el, pseudo).some(rule =>
|
|
76
|
+
rule.includes(PRESSED_TOKEN),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Does one of the element's own classes paint the pressed overlay token
|
|
82
|
+
* unconditionally — no pseudo-class, no ancestor scope — because the component
|
|
83
|
+
* applies the class while it holds the press itself (a dragged slider thumb)?
|
|
84
|
+
* StyleX's specificity padding (`:not(#\#)`) is not a condition.
|
|
85
|
+
*/
|
|
86
|
+
export function declaresPressedOverlay(el: Element): boolean {
|
|
87
|
+
return rulesDeclaredFor(el).some(rule => {
|
|
88
|
+
const selector = rule
|
|
89
|
+
.slice(0, rule.indexOf('{'))
|
|
90
|
+
.replaceAll(':not(#\\#)', '');
|
|
91
|
+
return !selector.includes(':') && rule.includes(PRESSED_TOKEN);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file scrollKeyboardDelegation.test.ts
|
|
5
|
+
* @input Private delegation behavior, explicit visibility, and focus event sequences
|
|
6
|
+
* @output Eligibility, keyboard intent, observer cost, and cleanup regressions
|
|
7
|
+
* @position DOM-level policy proof; browser tests own native Tab and scroll evidence
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
11
|
+
import {attachScrollKeyboardDelegation} from './scrollKeyboardDelegation';
|
|
12
|
+
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
13
|
+
|
|
14
|
+
let detach: (() => void) | undefined;
|
|
15
|
+
let before: HTMLButtonElement;
|
|
16
|
+
let viewport: HTMLDivElement;
|
|
17
|
+
let content: HTMLDivElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.spyOn(HTMLElement.prototype, 'getClientRects').mockReturnValue([
|
|
21
|
+
new DOMRect(0, 0, 20, 20),
|
|
22
|
+
] as unknown as DOMRectList);
|
|
23
|
+
before = document.createElement('button');
|
|
24
|
+
viewport = document.createElement('div');
|
|
25
|
+
viewport.tabIndex = 0;
|
|
26
|
+
content = document.createElement('div');
|
|
27
|
+
viewport.append(content);
|
|
28
|
+
// The pinned jsdom selector engine orders this comma-separated selector by
|
|
29
|
+
// selector group. Browsers return document order; normalize only the emulator.
|
|
30
|
+
const query = content.querySelectorAll.bind(content);
|
|
31
|
+
vi.spyOn(content, 'querySelectorAll').mockImplementation(selector => {
|
|
32
|
+
const result = query(selector);
|
|
33
|
+
return selector === FOCUSABLE_SELECTOR
|
|
34
|
+
? ([...result].sort((a, b) =>
|
|
35
|
+
a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
36
|
+
? -1
|
|
37
|
+
: 1,
|
|
38
|
+
) as unknown as NodeListOf<Element>)
|
|
39
|
+
: result;
|
|
40
|
+
});
|
|
41
|
+
document.body.append(before, viewport);
|
|
42
|
+
detach = attachScrollKeyboardDelegation(viewport, content);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
detach?.();
|
|
47
|
+
before.remove();
|
|
48
|
+
viewport.remove();
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// jsdom supplies no Tab default action. Complete dispatch before focusing to
|
|
53
|
+
// model the browser's event boundary; real traversal is tested in both engines.
|
|
54
|
+
function enter() {
|
|
55
|
+
before.focus();
|
|
56
|
+
before.dispatchEvent(
|
|
57
|
+
new KeyboardEvent('keydown', {key: 'Tab', bubbles: true}),
|
|
58
|
+
);
|
|
59
|
+
viewport.focus();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('scroll keyboard delegation', () => {
|
|
63
|
+
it('inspects content only on keyboard entry and leaves mutation observation untouched', () => {
|
|
64
|
+
content.innerHTML = '<button>Action</button>';
|
|
65
|
+
const scan = vi.spyOn(content, 'querySelectorAll');
|
|
66
|
+
const observe = vi.spyOn(MutationObserver.prototype, 'observe');
|
|
67
|
+
viewport.focus();
|
|
68
|
+
content.firstElementChild?.setAttribute('data-state', 'changed');
|
|
69
|
+
viewport.dispatchEvent(new Event('scroll'));
|
|
70
|
+
expect(scan).not.toHaveBeenCalled();
|
|
71
|
+
expect(observe).not.toHaveBeenCalled();
|
|
72
|
+
enter();
|
|
73
|
+
expect(scan).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(document.activeElement).toBe(content.firstElementChild);
|
|
75
|
+
expect(viewport.tabIndex).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each([
|
|
79
|
+
'<input aria-label="Input"><button>Later action</button>',
|
|
80
|
+
'<button role="radio">Radio</button><button>Later action</button>',
|
|
81
|
+
'<div role="toolbar"><button>Toolbar action</button></div>',
|
|
82
|
+
'<div contenteditable="true"><button>Editor action</button></div>',
|
|
83
|
+
'<button aria-haspopup="menu">Open menu</button>',
|
|
84
|
+
'<button aria-disabled="true">Unavailable</button>',
|
|
85
|
+
'<button aria-hidden="true">Hidden from AT</button><button>Later action</button>',
|
|
86
|
+
'<button>Zero</button><button tabindex="1">Earlier positive stop</button>',
|
|
87
|
+
'<div tabindex="0">Custom target</div><button>Later action</button>',
|
|
88
|
+
])('does not skip an excluded first target: %s', markup => {
|
|
89
|
+
content.innerHTML = markup;
|
|
90
|
+
enter();
|
|
91
|
+
expect(document.activeElement).toBe(viewport);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each(['disabled', 'hidden', 'tabindex="-2"', 'style="visibility:hidden"'])(
|
|
95
|
+
'skips a nonsequential target (%s)',
|
|
96
|
+
attributes => {
|
|
97
|
+
content.innerHTML = `<button ${attributes}>Skipped</button><button>Action</button>`;
|
|
98
|
+
enter();
|
|
99
|
+
expect(document.activeElement).toBe(content.lastElementChild);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it('rechecks ancestor eligibility on the next entry', () => {
|
|
104
|
+
content.innerHTML = '<div><button>Action</button></div>';
|
|
105
|
+
enter();
|
|
106
|
+
expect(document.activeElement).toBe(content.querySelector('button'));
|
|
107
|
+
content.firstElementChild?.setAttribute('role', 'menu');
|
|
108
|
+
enter();
|
|
109
|
+
expect(document.activeElement).toBe(viewport);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('retains programmatic focus during a Tab handler even without preventDefault', () => {
|
|
113
|
+
content.innerHTML = '<button>Action</button>';
|
|
114
|
+
before.addEventListener('keydown', () => viewport.focus(), {once: true});
|
|
115
|
+
before.focus();
|
|
116
|
+
before.dispatchEvent(
|
|
117
|
+
new KeyboardEvent('keydown', {key: 'Tab', bubbles: true}),
|
|
118
|
+
);
|
|
119
|
+
expect(document.activeElement).toBe(viewport);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not retain canceled keyboard intent', () => {
|
|
123
|
+
content.innerHTML = '<button>Action</button>';
|
|
124
|
+
before.addEventListener('keydown', event => event.preventDefault(), {
|
|
125
|
+
once: true,
|
|
126
|
+
});
|
|
127
|
+
before.focus();
|
|
128
|
+
before.dispatchEvent(
|
|
129
|
+
new KeyboardEvent('keydown', {
|
|
130
|
+
key: 'Tab',
|
|
131
|
+
bubbles: true,
|
|
132
|
+
cancelable: true,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
viewport.focus();
|
|
136
|
+
expect(document.activeElement).toBe(viewport);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('restores the skipped viewport on cancellation and cleans up pending work', () => {
|
|
140
|
+
content.innerHTML = '<button>Action</button><button>Last</button>';
|
|
141
|
+
enter();
|
|
142
|
+
const first = content.firstElementChild as HTMLElement;
|
|
143
|
+
(content.lastElementChild as HTMLElement).focus();
|
|
144
|
+
first.focus();
|
|
145
|
+
first.dispatchEvent(
|
|
146
|
+
new KeyboardEvent('keydown', {key: 'Tab', shiftKey: true, bubbles: true}),
|
|
147
|
+
);
|
|
148
|
+
expect(viewport.tabIndex).toBe(-1);
|
|
149
|
+
detach?.();
|
|
150
|
+
detach = undefined;
|
|
151
|
+
expect(viewport.tabIndex).toBe(0);
|
|
152
|
+
enter();
|
|
153
|
+
expect(document.activeElement).toBe(viewport);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file scrollKeyboardDelegation.ts
|
|
5
|
+
* @input A named, overflowing viewport and its real content box
|
|
6
|
+
* @output Focus-time delegation with native forward and reverse Tab traversal
|
|
7
|
+
* @position Private useScrollableArea keyboard behavior; no DOM observers or scrolling
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
11
|
+
import {getRegisteredScrollOwnerState} from './scrollOwnerRegistry';
|
|
12
|
+
|
|
13
|
+
// Structural roles may contain ordinary links/buttons. Unknown or interactive
|
|
14
|
+
// roles are deliberately excluded until their navigation contract is proven.
|
|
15
|
+
const PASSIVE_ROLES = new Set([
|
|
16
|
+
'article',
|
|
17
|
+
'banner',
|
|
18
|
+
'complementary',
|
|
19
|
+
'contentinfo',
|
|
20
|
+
'definition',
|
|
21
|
+
'directory',
|
|
22
|
+
'document',
|
|
23
|
+
'figure',
|
|
24
|
+
'form',
|
|
25
|
+
'generic',
|
|
26
|
+
'group',
|
|
27
|
+
'heading',
|
|
28
|
+
'list',
|
|
29
|
+
'listitem',
|
|
30
|
+
'main',
|
|
31
|
+
'navigation',
|
|
32
|
+
'none',
|
|
33
|
+
'note',
|
|
34
|
+
'paragraph',
|
|
35
|
+
'presentation',
|
|
36
|
+
'region',
|
|
37
|
+
'section',
|
|
38
|
+
'status',
|
|
39
|
+
'table',
|
|
40
|
+
'term',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function isSequential(element: HTMLElement): boolean {
|
|
44
|
+
if (
|
|
45
|
+
element.tabIndex < 0 ||
|
|
46
|
+
element.matches(':disabled') ||
|
|
47
|
+
element.closest('[inert], [hidden]') != null ||
|
|
48
|
+
element.getClientRects().length === 0
|
|
49
|
+
) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const visibility = getComputedStyle(element).visibility;
|
|
53
|
+
return visibility !== 'hidden' && visibility !== 'collapse';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function firstDelegationTarget(
|
|
57
|
+
viewport: HTMLElement,
|
|
58
|
+
content: HTMLElement,
|
|
59
|
+
): HTMLElement | null {
|
|
60
|
+
const candidates = content.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
61
|
+
let first =
|
|
62
|
+
content.matches(FOCUSABLE_SELECTOR) && isSequential(content)
|
|
63
|
+
? content
|
|
64
|
+
: null;
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
// After finding the first target, only positive tabindex can precede it.
|
|
67
|
+
// Ordinary trailing controls need no visibility or layout reads.
|
|
68
|
+
if (first != null && candidate.tabIndex <= 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!isSequential(candidate)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Positive tabindex participates before the zero-index viewport. Do not
|
|
75
|
+
// revisit it or skip its ordering by delegating to a later zero-index child.
|
|
76
|
+
if (candidate.tabIndex > 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
first ??= candidate;
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
first == null ||
|
|
83
|
+
first.tabIndex !== 0 ||
|
|
84
|
+
!first.matches('a[href], button') ||
|
|
85
|
+
first.closest('[aria-hidden="true"]') != null ||
|
|
86
|
+
first.matches(
|
|
87
|
+
'[aria-disabled="true"], [aria-haspopup]:not([aria-haspopup="false"])',
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let ancestor: HTMLElement | null = first;
|
|
94
|
+
while (ancestor != null && ancestor !== viewport) {
|
|
95
|
+
const role: string | undefined = ancestor
|
|
96
|
+
.getAttribute('role')
|
|
97
|
+
?.trim()
|
|
98
|
+
.toLowerCase();
|
|
99
|
+
const nativeRole: boolean =
|
|
100
|
+
ancestor === first &&
|
|
101
|
+
((role === 'button' && first.localName === 'button') ||
|
|
102
|
+
(role === 'link' && first.localName === 'a'));
|
|
103
|
+
if (
|
|
104
|
+
(role != null &&
|
|
105
|
+
role !== '' &&
|
|
106
|
+
!nativeRole &&
|
|
107
|
+
!PASSIVE_ROLES.has(role)) ||
|
|
108
|
+
(ancestor === first && role != null && role !== '' && !nativeRole) ||
|
|
109
|
+
ancestor.hasAttribute('aria-activedescendant') ||
|
|
110
|
+
ancestor.isContentEditable ||
|
|
111
|
+
ancestor.matches('[contenteditable]:not([contenteditable="false"])') ||
|
|
112
|
+
getRegisteredScrollOwnerState(ancestor) != null
|
|
113
|
+
) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Also respect native scroll owners that have not adopted the shared hook.
|
|
117
|
+
const style = getComputedStyle(ancestor);
|
|
118
|
+
if (
|
|
119
|
+
(/^(auto|scroll|overlay)$/.test(style.overflowX) &&
|
|
120
|
+
ancestor.scrollWidth > ancestor.clientWidth + 1) ||
|
|
121
|
+
(/^(auto|scroll|overlay)$/.test(style.overflowY) &&
|
|
122
|
+
ancestor.scrollHeight > ancestor.clientHeight + 1)
|
|
123
|
+
) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
ancestor = ancestor.parentElement;
|
|
127
|
+
}
|
|
128
|
+
return ancestor === viewport ? first : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Attach only while automatic ownership has an effective scroll axis. */
|
|
132
|
+
export function attachScrollKeyboardDelegation(
|
|
133
|
+
viewport: HTMLElement,
|
|
134
|
+
content: HTMLElement,
|
|
135
|
+
): () => void {
|
|
136
|
+
const document = viewport.ownerDocument;
|
|
137
|
+
const window = document.defaultView;
|
|
138
|
+
if (window == null) {
|
|
139
|
+
return () => {};
|
|
140
|
+
}
|
|
141
|
+
let entry: KeyboardEvent | null = null;
|
|
142
|
+
let origin: EventTarget | null = null;
|
|
143
|
+
let delegated: HTMLElement | null = null;
|
|
144
|
+
let timer: number | undefined;
|
|
145
|
+
let restoreTabStop: (() => void) | undefined;
|
|
146
|
+
|
|
147
|
+
const reset = () => {
|
|
148
|
+
entry = null;
|
|
149
|
+
origin = null;
|
|
150
|
+
window.clearTimeout(timer);
|
|
151
|
+
restoreTabStop?.();
|
|
152
|
+
restoreTabStop = undefined;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
156
|
+
reset();
|
|
157
|
+
if (event.key !== 'Tab' || event.ctrlKey || event.metaKey) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Keep the actual event: focus() during keydown is programmatic (nonzero
|
|
161
|
+
// eventPhase), whereas native Tab focus follows completed event dispatch.
|
|
162
|
+
entry = event;
|
|
163
|
+
origin = event.target;
|
|
164
|
+
if (
|
|
165
|
+
event.shiftKey &&
|
|
166
|
+
delegated != null &&
|
|
167
|
+
document.activeElement === delegated &&
|
|
168
|
+
viewport.contains(delegated)
|
|
169
|
+
) {
|
|
170
|
+
const tabIndex = viewport.getAttribute('tabindex');
|
|
171
|
+
viewport.tabIndex = -1;
|
|
172
|
+
restoreTabStop = () => {
|
|
173
|
+
if (viewport.getAttribute('tabindex') === '-1') {
|
|
174
|
+
if (tabIndex == null) {
|
|
175
|
+
viewport.removeAttribute('tabindex');
|
|
176
|
+
} else {
|
|
177
|
+
viewport.setAttribute('tabindex', tabIndex);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// A canceled Tab, focus leaving the document, or a held key must not leave
|
|
183
|
+
// stale keyboard intent for a later pointer/programmatic focus operation.
|
|
184
|
+
timer = window.setTimeout(reset, 0);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const onFocusIn = (event: FocusEvent) => {
|
|
188
|
+
const keyboardEntry = entry;
|
|
189
|
+
const previous = origin;
|
|
190
|
+
reset();
|
|
191
|
+
if (event.target !== viewport) {
|
|
192
|
+
if (!viewport.contains(event.target as Node | null)) {
|
|
193
|
+
delegated = null;
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
delegated = null;
|
|
198
|
+
if (
|
|
199
|
+
keyboardEntry == null ||
|
|
200
|
+
keyboardEntry.shiftKey ||
|
|
201
|
+
keyboardEntry.defaultPrevented ||
|
|
202
|
+
keyboardEntry.eventPhase !== 0 ||
|
|
203
|
+
(event.relatedTarget != null && event.relatedTarget !== previous)
|
|
204
|
+
) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const target = firstDelegationTarget(viewport, content);
|
|
208
|
+
if (target == null) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
delegated = target;
|
|
212
|
+
// Let native focus reveal the action. Arrow/Page scrolling remains entirely
|
|
213
|
+
// browser-owned, including scroll chaining and the full viewport range.
|
|
214
|
+
target.focus();
|
|
215
|
+
if (document.activeElement !== target) {
|
|
216
|
+
delegated = null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
221
|
+
document.addEventListener('keyup', reset, true);
|
|
222
|
+
document.addEventListener('pointerdown', reset, true);
|
|
223
|
+
document.addEventListener('focusin', onFocusIn, true);
|
|
224
|
+
window.addEventListener('blur', reset);
|
|
225
|
+
return () => {
|
|
226
|
+
reset();
|
|
227
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
228
|
+
document.removeEventListener('keyup', reset, true);
|
|
229
|
+
document.removeEventListener('pointerdown', reset, true);
|
|
230
|
+
document.removeEventListener('focusin', onFocusIn, true);
|
|
231
|
+
window.removeEventListener('blur', reset);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file useScrollableArea.doc.mjs
|
|
5
|
+
* @input Shared scroll hook's fixed and entry-time keyboard policies
|
|
6
|
+
* @output Consumer guidance for named viewports and safe focus delegation
|
|
7
|
+
* @position Hook documentation consumed by the CLI and docsite
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
/** @type {import('@astryxdesign/cli/authoring').HookDoc} */
|
|
4
11
|
export const docs = {
|
|
5
12
|
name: 'useScrollableArea',
|
|
@@ -18,7 +25,7 @@ export const docs = {
|
|
|
18
25
|
name: 'options',
|
|
19
26
|
type: 'UseScrollableAreaOptions',
|
|
20
27
|
description:
|
|
21
|
-
'Logical scroll intent, keyboard owner, overscroll policy, and fitting Sticky containment.',
|
|
28
|
+
'Logical scroll intent, fixed or automatic keyboard owner, overscroll policy, and fitting Sticky containment.',
|
|
22
29
|
required: true,
|
|
23
30
|
},
|
|
24
31
|
],
|
|
@@ -59,7 +66,12 @@ export const docs = {
|
|
|
59
66
|
{
|
|
60
67
|
guidance: true,
|
|
61
68
|
description:
|
|
62
|
-
'Use
|
|
69
|
+
'Use contentOrViewport to delegate forward Tab entry to the first sequential native link or button when it preserves native scroll keys. Inputs, composite widgets, and nested scroll areas retain the named viewport stop. Shift+Tab from the delegated first child skips the viewport; pointer and programmatic focus stay on it.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
guidance: true,
|
|
73
|
+
description:
|
|
74
|
+
'Use content keyboard ownership when your integration already supplies keyboard access to the full scroll range. Automatic delegation checks current content at each keyboard entry without continuously tracking its focusability.',
|
|
63
75
|
},
|
|
64
76
|
{
|
|
65
77
|
guidance: true,
|
|
@@ -85,7 +97,7 @@ export const docsDense = {
|
|
|
85
97
|
'Composes logical-axis scrolling into caller-owned viewport/content elements with stable effective-axis and edge state.',
|
|
86
98
|
paramDescriptions: {
|
|
87
99
|
options:
|
|
88
|
-
'axis, keyboard owner, allow/contain overscroll policy, and fitting Sticky containment.',
|
|
100
|
+
'axis, fixed/automatic keyboard owner, allow/contain overscroll policy, and fitting Sticky containment.',
|
|
89
101
|
},
|
|
90
102
|
returnDescriptions: {
|
|
91
103
|
getViewportProps:
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file useScrollableArea.test.tsx
|
|
5
|
+
* @input Shared scroll hook, mocked geometry, and observed DOM changes
|
|
6
|
+
* @output Regression coverage for measurement, composition, and stable viewport access
|
|
7
|
+
* @position DOM-level hook contract; native traversal is verified in browser tests
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
import {act, render, screen} from '@testing-library/react';
|
|
4
11
|
import * as stylex from '@stylexjs/stylex';
|
|
5
|
-
import {useRef, type Ref} from 'react';
|
|
12
|
+
import {useRef, type ReactNode, type Ref} from 'react';
|
|
6
13
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
7
14
|
import {
|
|
8
15
|
useScrollableArea,
|
|
@@ -194,6 +201,57 @@ describe('useScrollableArea', () => {
|
|
|
194
201
|
expect(viewport.getAttribute('style')).toContain('--x-overflowY: hidden');
|
|
195
202
|
});
|
|
196
203
|
|
|
204
|
+
it('keeps the named viewport available while content eligibility changes', () => {
|
|
205
|
+
function AdaptiveFixture({children}: {children?: ReactNode}) {
|
|
206
|
+
const area = useScrollableArea({
|
|
207
|
+
axis: 'block',
|
|
208
|
+
keyboardAccess: {
|
|
209
|
+
owner: 'contentOrViewport',
|
|
210
|
+
label: 'Adaptive results',
|
|
211
|
+
role: 'region',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
return (
|
|
215
|
+
<div data-testid="adaptive-viewport" {...area.getViewportProps()}>
|
|
216
|
+
<div data-testid="adaptive-content" {...area.getContentProps()}>
|
|
217
|
+
{children}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
vi.spyOn(HTMLElement.prototype, 'getClientRects').mockReturnValue([
|
|
224
|
+
new DOMRect(0, 0, 20, 20),
|
|
225
|
+
] as unknown as DOMRectList);
|
|
226
|
+
const {rerender} = render(<AdaptiveFixture>Plain text</AdaptiveFixture>);
|
|
227
|
+
const viewport = screen.getByTestId('adaptive-viewport');
|
|
228
|
+
const content = screen.getByTestId('adaptive-content');
|
|
229
|
+
makeMeasurable(viewport);
|
|
230
|
+
setGeometry(viewport, {scrollHeight: 180});
|
|
231
|
+
|
|
232
|
+
void act(() => viewport.dispatchEvent(new Event('scroll')));
|
|
233
|
+
flushFrame();
|
|
234
|
+
expect(viewport).toHaveAttribute('role', 'region');
|
|
235
|
+
expect(viewport).toHaveAccessibleName('Adaptive results');
|
|
236
|
+
expect(viewport).toHaveAttribute('tabindex', '0');
|
|
237
|
+
|
|
238
|
+
viewport.focus();
|
|
239
|
+
rerender(
|
|
240
|
+
<AdaptiveFixture>
|
|
241
|
+
<button type="button">Use existing action</button>
|
|
242
|
+
</AdaptiveFixture>,
|
|
243
|
+
);
|
|
244
|
+
void act(() => content.dispatchEvent(new Event('transitionend')));
|
|
245
|
+
flushFrame();
|
|
246
|
+
expect(viewport).toHaveAttribute('tabindex', '0');
|
|
247
|
+
expect(viewport).toHaveFocus();
|
|
248
|
+
|
|
249
|
+
rerender(<AdaptiveFixture>Plain text again</AdaptiveFixture>);
|
|
250
|
+
void act(() => content.dispatchEvent(new Event('transitionend')));
|
|
251
|
+
flushFrame();
|
|
252
|
+
expect(viewport).toHaveAttribute('tabindex', '0');
|
|
253
|
+
});
|
|
254
|
+
|
|
197
255
|
it('requires scroll-capable computed overflow and more than 1px excess geometry', () => {
|
|
198
256
|
render(<Fixture />);
|
|
199
257
|
const viewport = screen.getByTestId('viewport');
|