@astryxdesign/core 0.1.7 → 0.1.8-canary.0c4d8d7
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 +65 -0
- package/README.md +30 -19
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +6 -2
- package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
- package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -8
- package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
- package/dist/Avatar/AvatarSizeContext.js +1 -1
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
- package/dist/Avatar/AvatarStatusDot.js +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -2
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
- package/dist/Banner/Banner.d.ts +9 -1
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +31 -2
- package/dist/Button/Button.d.ts +8 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +31 -2
- package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/ButtonGroup/ButtonGroup.js +30 -1
- package/dist/Card/Card.d.ts +8 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +28 -2
- package/dist/Carousel/Carousel.d.ts +6 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +55 -5
- package/dist/Chat/ChatComposer.d.ts +8 -0
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +31 -6
- package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
- package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
- package/dist/Chat/ChatComposerDrawer.js +11 -1
- package/dist/Chat/ChatComposerInput.d.ts +20 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +19 -3
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.d.ts +1 -1
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +1 -1
- package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
- package/dist/Chat/ChatSendButton.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.d.ts +5 -1
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +12 -3
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- package/dist/ClickableCard/ClickableCard.d.ts +9 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +3 -0
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +8 -1
- package/dist/Collapsible/Collapsible.d.ts +11 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +36 -4
- package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +6 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/ContextMenu/index.d.ts +3 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +7 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +10 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +88 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +169 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +68 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
- package/dist/DropdownMenu/index.d.ts +5 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +10 -1
- package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemRoles.js +15 -0
- package/dist/FieldStatus/FieldStatus.d.ts +8 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +22 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +10 -2
- package/dist/HoverCard/HoverCard.d.ts +9 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +2 -0
- package/dist/HoverCard/useHoverCard.d.ts +8 -0
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +7 -2
- package/dist/Icon/Icon.d.ts +43 -13
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +62 -27
- package/dist/Layer/useLayer.d.ts +6 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +2 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +6 -1
- package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
- package/dist/MobileNav/MobileNavToggle.js +4 -0
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +39 -12
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +16 -0
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/Outline/Outline.d.ts +68 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +128 -26
- package/dist/Outline/useScrollSpy.d.ts +27 -7
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +202 -70
- package/dist/OverflowList/OverflowList.d.ts +20 -3
- package/dist/OverflowList/OverflowList.d.ts.map +1 -1
- package/dist/OverflowList/OverflowList.js +29 -4
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.d.ts +9 -2
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +14 -11
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +4 -2
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +16 -0
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +21 -8
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +15 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +2 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/Table.d.ts +1 -1
- package/dist/Table/Table.js +1 -1
- package/dist/Table/index.d.ts +4 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +2 -0
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
- package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/index.js +3 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts +12 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +4 -1
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
- package/dist/Table/plugins/tree/index.d.ts +5 -0
- package/dist/Table/plugins/tree/index.d.ts.map +1 -0
- package/dist/Table/plugins/tree/index.js +4 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +6 -3
- package/dist/Thumbnail/Thumbnail.d.ts +8 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +46 -8
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +15 -4
- package/dist/Tokenizer/Tokenizer.d.ts +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +16 -4
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +8 -1
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +18 -8
- package/dist/TopNav/TopNavMegaMenu.js +10 -4
- package/dist/TopNav/TopNavMenu.d.ts +2 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +68 -6
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +5 -14
- package/dist/astryx.css +51 -16
- package/dist/astryx.umd.js +56 -56
- package/dist/astryx.umd.js.map +4 -4
- package/dist/authoring/doc.d.ts.map +1 -1
- package/dist/hooks/computeOverflow.d.ts +47 -0
- package/dist/hooks/computeOverflow.d.ts.map +1 -0
- package/dist/hooks/computeOverflow.js +213 -0
- package/dist/hooks/useOverflow.d.ts +19 -0
- package/dist/hooks/useOverflow.d.ts.map +1 -1
- package/dist/hooks/useOverflow.js +37 -25
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +51 -0
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +0 -3
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/types.d.ts.map +1 -1
- package/docs.mjs +32 -461
- package/locales/en.json +212 -172
- package/locales/fr-FR.json +10 -0
- package/locales/pseudo.json +30 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/AppShell/AppShell.tsx +6 -0
- package/src/AppShell/AppShellMobileContext.tsx +8 -0
- package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
- package/src/Avatar/Avatar.doc.mjs +6 -6
- package/src/Avatar/Avatar.tsx +16 -11
- package/src/Avatar/AvatarSizeContext.ts +1 -1
- package/src/Avatar/AvatarStatusDot.tsx +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
- package/src/AvatarGroup/AvatarGroup.tsx +3 -3
- package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
- package/src/Banner/Banner.doc.mjs +9 -0
- package/src/Banner/Banner.test.tsx +30 -0
- package/src/Banner/Banner.tsx +47 -3
- package/src/Button/Button.doc.mjs +9 -0
- package/src/Button/Button.test.tsx +49 -0
- package/src/Button/Button.tsx +25 -1
- package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
- package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
- package/src/ButtonGroup/ButtonGroup.tsx +32 -0
- package/src/Card/Card.doc.mjs +9 -0
- package/src/Card/Card.test.tsx +21 -3
- package/src/Card/Card.tsx +35 -2
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +149 -1
- package/src/Carousel/Carousel.tsx +62 -5
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatComposer.doc.mjs +9 -0
- package/src/Chat/ChatComposer.test.tsx +35 -0
- package/src/Chat/ChatComposer.tsx +58 -9
- package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
- package/src/Chat/ChatComposerDrawer.tsx +11 -1
- package/src/Chat/ChatComposerInput.doc.mjs +7 -0
- package/src/Chat/ChatComposerInput.test.tsx +87 -0
- package/src/Chat/ChatComposerInput.tsx +35 -3
- package/src/Chat/ChatLayout.tsx +3 -1
- package/src/Chat/ChatLayoutScrollButton.tsx +3 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/Chat/ChatMessageMetadata.tsx +1 -2
- package/src/Chat/ChatSendButton.tsx +5 -1
- package/src/Chat/ChatToolCalls.test.tsx +64 -0
- package/src/Chat/ChatToolCalls.tsx +13 -1
- package/src/Chat/useTriggerMenu.tsx +3 -1
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
- package/src/CheckboxList/CheckboxList.test.tsx +9 -1
- package/src/Citation/Citation.test.tsx +51 -0
- package/src/Citation/Citation.tsx +8 -2
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
- package/src/ClickableCard/ClickableCard.test.tsx +34 -0
- package/src/ClickableCard/ClickableCard.tsx +11 -1
- package/src/CodeBlock/CodeBlock.test.tsx +56 -0
- package/src/CodeBlock/CodeBlock.tsx +9 -1
- package/src/Collapsible/Collapsible.doc.mjs +7 -0
- package/src/Collapsible/Collapsible.test.tsx +80 -0
- package/src/Collapsible/Collapsible.tsx +58 -5
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
- package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
- package/src/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +35 -0
- package/src/DateTimeInput/DateTimeInput.tsx +14 -2
- package/src/Dialog/Dialog.tsx +12 -2
- package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
- package/src/DropdownMenu/DropdownMenu.tsx +11 -6
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +236 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +118 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
- package/src/DropdownMenu/index.ts +23 -3
- package/src/DropdownMenu/menuItemRoles.ts +22 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/Field.test.tsx +57 -12
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
- package/src/FieldStatus/FieldStatus.test.tsx +78 -40
- package/src/FieldStatus/FieldStatus.tsx +22 -4
- package/src/FileInput/FileInput.test.tsx +52 -3
- package/src/FileInput/FileInput.tsx +13 -2
- package/src/HoverCard/HoverCard.doc.mjs +13 -0
- package/src/HoverCard/HoverCard.test.tsx +21 -2
- package/src/HoverCard/HoverCard.tsx +11 -0
- package/src/HoverCard/useHoverCard.doc.mjs +2 -0
- package/src/HoverCard/useHoverCard.tsx +24 -2
- package/src/Icon/Icon.doc.mjs +12 -0
- package/src/Icon/Icon.test.tsx +142 -0
- package/src/Icon/Icon.tsx +98 -30
- package/src/IconButton/IconButton.doc.mjs +8 -0
- package/src/IconButton/IconButton.test.tsx +15 -18
- package/src/Item/Item.doc.mjs +1 -1
- package/src/Layer/useLayer.tsx +8 -0
- package/src/MobileNav/MobileNav.tsx +12 -1
- package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
- package/src/MobileNav/MobileNavToggle.tsx +9 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
- package/src/MultiSelector/MultiSelector.test.tsx +162 -1
- package/src/MultiSelector/MultiSelector.tsx +55 -12
- package/src/MultiSelector/hooks.ts +17 -0
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/NumberInput/NumberInput.test.tsx +11 -7
- package/src/NumberInput/NumberInput.tsx +1 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +183 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/OverflowList/OverflowList.doc.mjs +39 -6
- package/src/OverflowList/OverflowList.test.tsx +111 -0
- package/src/OverflowList/OverflowList.tsx +48 -8
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/usePowerSearchSource.ts +1 -2
- package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
- package/src/SelectableCard/SelectableCard.test.tsx +46 -4
- package/src/SelectableCard/SelectableCard.tsx +27 -14
- package/src/Selector/Selector.test.tsx +69 -0
- package/src/Selector/Selector.tsx +5 -1
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/Selector/hooks.ts +17 -0
- package/src/SideNav/SideNav.test.tsx +119 -3
- package/src/SideNav/SideNavCollapseButton.tsx +6 -1
- package/src/SideNav/SideNavHeading.tsx +24 -8
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +5 -1
- package/src/Slider/Slider.test.tsx +62 -2
- package/src/Slider/Slider.tsx +15 -1
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/Switch/Switch.test.tsx +32 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/TabList.tsx +8 -0
- package/src/TabList/TabMenu.tsx +4 -1
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/Table.tsx +1 -1
- package/src/Table/index.ts +12 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/rowStatus/index.ts +7 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +18 -1
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
- package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
- package/src/Table/plugins/tree/index.ts +12 -0
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
- package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
- package/src/Table/useBaseTablePlugins.ts +6 -2
- package/src/Table/useTableRowStatus.doc.mjs +30 -0
- package/src/Table/useTableSelection.doc.mjs +65 -14
- package/src/Table/useTableTreeData.doc.mjs +75 -0
- package/src/Table/useTableTreeState.doc.mjs +98 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +9 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.doc.mjs +9 -0
- package/src/Thumbnail/Thumbnail.test.tsx +38 -8
- package/src/Thumbnail/Thumbnail.tsx +28 -1
- package/src/TimeInput/TimeInput.test.tsx +76 -1
- package/src/TimeInput/TimeInput.tsx +19 -6
- package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
- package/src/Tokenizer/Tokenizer.test.tsx +137 -2
- package/src/Tokenizer/Tokenizer.tsx +16 -4
- package/src/Toolbar/Toolbar.tsx +10 -3
- package/src/TopNav/TopNav.test.tsx +80 -1
- package/src/TopNav/TopNavHeading.tsx +24 -8
- package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
- package/src/TopNav/TopNavMegaMenu.tsx +10 -4
- package/src/TopNav/TopNavMenu.test.tsx +118 -4
- package/src/TopNav/TopNavMenu.tsx +83 -5
- package/src/TreeList/TreeList.test.tsx +32 -1
- package/src/TreeList/TreeListItem.tsx +19 -10
- package/src/authoring/doc.ts +1 -3
- package/src/docs-script.test.mjs +18 -13
- package/src/hooks/computeOverflow.test.ts +422 -0
- package/src/hooks/computeOverflow.ts +290 -0
- package/src/hooks/useOverflow.test.ts +96 -4
- package/src/hooks/useOverflow.ts +77 -33
- package/src/postinstall.test.mjs +71 -0
- package/src/theme/Theme.doc.mjs +4 -4
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/generateThemeRules.test.ts +65 -0
- package/src/theme/generateThemeRules.ts +56 -0
- package/src/theme/hct.ts +0 -4
- package/src/utils/types.ts +14 -0
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
- package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
package/dist/Outline/Outline.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Outline.tsx
|
|
7
|
-
* @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types
|
|
7
|
+
* @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types, useListFocus
|
|
8
8
|
* @output Exports Outline component and OutlineProps type
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - Sliding indicator track (vertical divider + animated active bar)
|
|
13
13
|
* - Density variant (default/compact)
|
|
14
14
|
* - Scroll-spy active state when uncontrolled
|
|
15
|
+
* - Roving-tabindex keyboard navigation (Arrow/Home/End, Enter/Space activate)
|
|
16
|
+
* - onNavigateStart / onNavigateEnd callbacks around the smooth scroll
|
|
17
|
+
* - Scroll scoping (offset, scrollContainerRef, hasScrollOnClick)
|
|
15
18
|
*
|
|
16
19
|
* SYNC: When modified, update these files to stay in sync:
|
|
17
20
|
* - /packages/core/src/Outline/Outline.doc.mjs
|
|
@@ -24,6 +27,8 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
24
27
|
import "../theme/tokens.stylex.js";
|
|
25
28
|
import { colorVars, durationVars, easeVars, radiusVars, spacingVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js";
|
|
26
29
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
30
|
+
import { useListFocus } from "../hooks/useListFocus.js";
|
|
31
|
+
import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js";
|
|
27
32
|
import { mergeProps, mergeRefs } from "../utils/index.js";
|
|
28
33
|
import { useScrollSpy } from "./useScrollSpy.js";
|
|
29
34
|
import { themeProps } from "../utils/themeProps.js";
|
|
@@ -135,8 +140,15 @@ function getIndentStyle(level) {
|
|
|
135
140
|
* track that animates to the active item.
|
|
136
141
|
*
|
|
137
142
|
* When `activeId` is omitted, it tracks scroll position and marks the last
|
|
138
|
-
* heading whose top has passed its activation line
|
|
139
|
-
*
|
|
143
|
+
* heading whose top has passed its activation line — which is exactly where
|
|
144
|
+
* navigating to that heading lands it: `offset` (a fixed header overlaying the
|
|
145
|
+
* scroll root) plus the heading's own `scroll-margin-top`. It defaults to the
|
|
146
|
+
* first item at the top and the last at the bottom.
|
|
147
|
+
*
|
|
148
|
+
* Keyboard: the list is a single tab stop (roving tabindex), seated on the
|
|
149
|
+
* active heading. Arrow keys move between headings, Home/End jump to the ends,
|
|
150
|
+
* and Enter/Space activate — so a long table of contents costs one Tab press,
|
|
151
|
+
* not one per heading.
|
|
140
152
|
*
|
|
141
153
|
* @example
|
|
142
154
|
* ```
|
|
@@ -148,6 +160,20 @@ function getIndentStyle(level) {
|
|
|
148
160
|
* ]}
|
|
149
161
|
* />
|
|
150
162
|
* ```
|
|
163
|
+
*
|
|
164
|
+
* Scoped to a custom scroll container, under a fixed header:
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```
|
|
168
|
+
* const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
169
|
+
* <div ref={scrollContainerRef} style={{overflowY: 'auto'}}>...</div>
|
|
170
|
+
* <Outline
|
|
171
|
+
* items={items}
|
|
172
|
+
* scrollContainerRef={scrollContainerRef}
|
|
173
|
+
* offset={64}
|
|
174
|
+
* onNavigateEnd={id => flashHeading(id)}
|
|
175
|
+
* />
|
|
176
|
+
* ```
|
|
151
177
|
*/
|
|
152
178
|
export function Outline({
|
|
153
179
|
items,
|
|
@@ -155,6 +181,11 @@ export function Outline({
|
|
|
155
181
|
onActiveIdChange,
|
|
156
182
|
label: labelFromProps,
|
|
157
183
|
density = 'default',
|
|
184
|
+
onNavigateStart,
|
|
185
|
+
onNavigateEnd,
|
|
186
|
+
offset = 0,
|
|
187
|
+
scrollContainerRef,
|
|
188
|
+
hasScrollOnClick = true,
|
|
158
189
|
xstyle,
|
|
159
190
|
className,
|
|
160
191
|
style,
|
|
@@ -166,42 +197,109 @@ export function Outline({
|
|
|
166
197
|
const label = labelFromProps ?? t('@astryx.outline.label');
|
|
167
198
|
const rootRef = useRef(null);
|
|
168
199
|
const LinkComponent = useLinkComponent();
|
|
169
|
-
const isControlled = activeId !== undefined;
|
|
170
200
|
const {
|
|
171
201
|
activeId: resolvedActiveId,
|
|
172
|
-
|
|
173
|
-
lockActiveId
|
|
202
|
+
scrollTo
|
|
174
203
|
} = useScrollSpy({
|
|
175
204
|
activeId,
|
|
176
205
|
items,
|
|
177
206
|
onActiveIdChange,
|
|
178
|
-
rootRef
|
|
207
|
+
rootRef,
|
|
208
|
+
offset,
|
|
209
|
+
scrollContainerRef,
|
|
210
|
+
hasScrollOnClick,
|
|
211
|
+
onNavigateStart,
|
|
212
|
+
onNavigateEnd
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Roving tabindex over the links: the whole outline is one tab stop, and
|
|
216
|
+
// arrows move between headings. The hook owns `tabindex` on the items.
|
|
217
|
+
const {
|
|
218
|
+
listRef,
|
|
219
|
+
handleKeyDown: handleListKeyDown,
|
|
220
|
+
handleFocus
|
|
221
|
+
} = useListFocus({
|
|
222
|
+
itemSelector: 'a[href]',
|
|
223
|
+
orientation: 'vertical',
|
|
224
|
+
hasRovingTabIndex: true
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Seat the tab stop on the *active* heading. WAI-ARIA puts the single tab
|
|
228
|
+
// stop on the current item, so tabbing into a table of contents while
|
|
229
|
+
// reading section 7 lands on section 7 — not back at section 1, which is
|
|
230
|
+
// where useListFocus would otherwise leave it (it promotes the first item on
|
|
231
|
+
// mount and then keeps whichever item already holds the stop).
|
|
232
|
+
//
|
|
233
|
+
// Only while focus is outside the list: once the reader has arrowed to an
|
|
234
|
+
// item, the stop is theirs and scroll-spy must not yank it away. Sets
|
|
235
|
+
// `tabindex` only — it never steals focus. Registered after useListFocus so
|
|
236
|
+
// it runs after the hook's own layout effect and wins; on later commits the
|
|
237
|
+
// hook then finds this item already holding the stop and keeps it.
|
|
238
|
+
useIsomorphicLayoutEffect(() => {
|
|
239
|
+
const list = listRef.current;
|
|
240
|
+
if (list == null || list.contains(document.activeElement)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const links = Array.from(list.querySelectorAll('a[href]'));
|
|
244
|
+
const active = links.find(link => link.getAttribute('aria-current') === 'true');
|
|
245
|
+
if (active == null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
for (const link of links) {
|
|
249
|
+
const tabIndex = link === active ? '0' : '-1';
|
|
250
|
+
if (link.getAttribute('tabindex') !== tabIndex) {
|
|
251
|
+
link.setAttribute('tabindex', tabIndex);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
179
254
|
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The single navigation path, shared by click and keyboard activation:
|
|
258
|
+
* push the hash, then hand off to the scroll-spy's `scrollTo`, which fires
|
|
259
|
+
* the navigate callbacks and moves the indicator when the scroll settles.
|
|
260
|
+
*/
|
|
261
|
+
const navigate = id => {
|
|
262
|
+
if (scrollTo(id)) {
|
|
263
|
+
window.history.pushState(null, '', `#${id}`);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/** Whether an event carries a modifier chord we must leave to the browser. */
|
|
268
|
+
const hasModifier = event => event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
|
|
180
269
|
const handleClick = id => event => {
|
|
181
|
-
|
|
270
|
+
// Let the browser handle modified clicks (open in new tab, etc.) without
|
|
271
|
+
// touching the active state.
|
|
272
|
+
if (event.defaultPrevented || hasModifier(event)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
182
275
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
276
|
+
// With no target in the DOM — lazily-rendered or virtualized content —
|
|
277
|
+
// there is nothing to scroll to and nothing to make active. Leave the
|
|
278
|
+
// anchor to the browser's native fragment navigation (which still
|
|
279
|
+
// updates the URL) rather than swallowing the click into a dead link.
|
|
280
|
+
if (document.getElementById(id) == null) {
|
|
186
281
|
return;
|
|
187
282
|
}
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
window.history.pushState(null, '', `#${id}`);
|
|
190
283
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
284
|
+
// Suppress the anchor's default jump: we own the scroll (or, with
|
|
285
|
+
// `hasScrollOnClick={false}`, deliberately leave it to the consumer).
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
navigate(id);
|
|
288
|
+
};
|
|
289
|
+
const handleKeyDown = id => event => {
|
|
290
|
+
// Enter is a link's native activation and already produces a click.
|
|
291
|
+
// Space is not, so wire it up here to match the button-like affordance.
|
|
292
|
+
if (event.key !== ' ' && event.key !== 'Spacebar') {
|
|
293
|
+
return;
|
|
200
294
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
295
|
+
if (event.defaultPrevented || hasModifier(event)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// preventDefault does double duty: it stops the page from scrolling, and
|
|
300
|
+
// it tells useScrollSpy this Space is an activation, not a manual scroll.
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
navigate(id);
|
|
205
303
|
};
|
|
206
304
|
return /*#__PURE__*/_jsxs("nav", {
|
|
207
305
|
...props,
|
|
@@ -215,7 +313,10 @@ export function Outline({
|
|
|
215
313
|
...{
|
|
216
314
|
className: "x78zum5 xdt5ytf x1lsbc85 x1ghz6dp x1717udv xe8uvvx x98rzlu xeuugli"
|
|
217
315
|
},
|
|
316
|
+
ref: listRef,
|
|
218
317
|
role: "list",
|
|
318
|
+
onKeyDown: handleListKeyDown,
|
|
319
|
+
onFocus: handleFocus,
|
|
219
320
|
children: items.map(item => {
|
|
220
321
|
const isActive = item.id === resolvedActiveId;
|
|
221
322
|
return /*#__PURE__*/_jsx("li", {
|
|
@@ -227,6 +328,7 @@ export function Outline({
|
|
|
227
328
|
href: `#${item.id}`,
|
|
228
329
|
"aria-current": isActive ? 'true' : undefined,
|
|
229
330
|
onClick: handleClick(item.id),
|
|
331
|
+
onKeyDown: handleKeyDown(item.id),
|
|
230
332
|
...mergeProps(themeProps('outline-item', {
|
|
231
333
|
active: isActive ? 'active' : null,
|
|
232
334
|
level: item.level
|
|
@@ -4,20 +4,40 @@ interface UseScrollSpyOptions {
|
|
|
4
4
|
items: OutlineItem[];
|
|
5
5
|
onActiveIdChange?: (id: string) => void;
|
|
6
6
|
rootRef: React.RefObject<HTMLElement | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Height in px of a fixed header overlaying the top of the scroll root.
|
|
9
|
+
* Shifts both the activation line and the scroll landing by the same amount,
|
|
10
|
+
* on top of each heading's own `scroll-margin-top`.
|
|
11
|
+
*/
|
|
12
|
+
offset?: number;
|
|
13
|
+
/** Scroll container to track, instead of the nearest scrollable ancestor. */
|
|
14
|
+
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
|
15
|
+
/** Whether {@link UseScrollSpyResult.scrollTo} performs the smooth scroll. */
|
|
16
|
+
hasScrollOnClick?: boolean;
|
|
17
|
+
/** Called when a navigation begins, before the scroll starts. */
|
|
18
|
+
onNavigateStart?: (id: string) => void;
|
|
19
|
+
/** Called once per navigation, when the scroll settles or is interrupted. */
|
|
20
|
+
onNavigateEnd?: (id: string) => void;
|
|
7
21
|
}
|
|
8
22
|
interface UseScrollSpyResult {
|
|
9
23
|
activeId: string | undefined;
|
|
10
24
|
/** Set the active id (notifies onActiveIdChange). For controlled consumers. */
|
|
11
25
|
setActiveId: (id: string) => void;
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* scroll
|
|
27
|
+
* Navigate to the item with id `id` — the single path shared by click and
|
|
28
|
+
* keyboard activation.
|
|
29
|
+
*
|
|
30
|
+
* Fires `onNavigateStart(id)`, scrolls (unless `hasScrollOnClick` is false),
|
|
31
|
+
* and fires `onNavigateEnd(id)` exactly once when the scroll settles or the
|
|
32
|
+
* user interrupts it. While uncontrolled, scroll-spy is suppressed for the
|
|
33
|
+
* duration so the indicator doesn't chase the scroll through intervening
|
|
34
|
+
* sections; it lands on the target once the scroll settles, or resumes
|
|
35
|
+
* position tracking if the user scrolls away mid-flight.
|
|
36
|
+
*
|
|
37
|
+
* Returns false (and does nothing) when no element with `id` exists.
|
|
18
38
|
*/
|
|
19
|
-
|
|
39
|
+
scrollTo: (id: string) => boolean;
|
|
20
40
|
}
|
|
21
|
-
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, }: UseScrollSpyOptions): UseScrollSpyResult;
|
|
41
|
+
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, offset, scrollContainerRef, hasScrollOnClick, onNavigateStart, onNavigateEnd, }: UseScrollSpyOptions): UseScrollSpyResult;
|
|
22
42
|
export {};
|
|
23
43
|
//# sourceMappingURL=useScrollSpy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAsJzC,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzD,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,+EAA+E;IAC/E,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,OAAO,EACP,MAAU,EACV,kBAAkB,EAClB,gBAAuB,EACvB,eAAe,EACf,aAAa,GACd,EAAE,mBAAmB,GAAG,kBAAkB,CAoN1C"}
|
|
@@ -10,15 +10,38 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Drives the active outline item from scroll position. On each scroll
|
|
12
12
|
* (rAF-throttled) it reads live heading positions and marks the last heading
|
|
13
|
-
* whose top has passed its activation line
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* whose top has passed its activation line. This is stable — it never compares
|
|
14
|
+
* stale cached positions — so the indicator moves monotonically instead of
|
|
15
|
+
* jumping. Defaults to the first item at the top and the last item at the
|
|
16
|
+
* bottom so short final sections still activate.
|
|
17
|
+
*
|
|
18
|
+
* The activation line and the scroll landing are the same number, computed once
|
|
19
|
+
* in `getRestingTop`: scroll-root top + `offset` (a fixed header overlaying the
|
|
20
|
+
* root) + the heading's own `scroll-margin-top`. A heading therefore activates
|
|
21
|
+
* exactly where navigating to it puts it. `offset` and `scroll-margin-top`
|
|
22
|
+
* compose — the header, then the breathing room below it — they do not
|
|
23
|
+
* duplicate each other.
|
|
24
|
+
*
|
|
25
|
+
* It also owns the single navigation path (`scrollTo`), shared by click and
|
|
26
|
+
* keyboard activation: it announces the jump, suppresses scroll-spy for the
|
|
27
|
+
* duration of the programmatic scroll (so the indicator doesn't chase it), and
|
|
28
|
+
* resolves when the scroll settles — via `scrollend` where supported, or a
|
|
29
|
+
* timeout fallback where it is not.
|
|
30
|
+
*
|
|
31
|
+
* The scroll root is the `scrollContainerRef` element when provided, otherwise
|
|
32
|
+
* the nearest scrollable ancestor of the outline, otherwise the viewport.
|
|
18
33
|
*
|
|
19
34
|
* SYNC: When modified, update /packages/core/src/Outline/Outline.tsx
|
|
20
35
|
*/
|
|
21
36
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
37
|
+
/**
|
|
38
|
+
* How long to wait for a programmatic smooth scroll to settle when the
|
|
39
|
+
* `scrollend` event never arrives — either because the browser does not
|
|
40
|
+
* support it (Safari at time of writing) or because the target was already in
|
|
41
|
+
* position, so no scroll happened at all and no events were emitted.
|
|
42
|
+
*/
|
|
43
|
+
const SCROLL_SETTLE_TIMEOUT_MS = 1200;
|
|
44
|
+
|
|
22
45
|
/** Keys that scroll the viewport — used to detect a manual scroll intent. */
|
|
23
46
|
const SCROLL_KEYS = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Spacebar']);
|
|
24
47
|
function getScrollableAncestor(element) {
|
|
@@ -35,20 +58,69 @@ function getScrollableAncestor(element) {
|
|
|
35
58
|
return null;
|
|
36
59
|
}
|
|
37
60
|
|
|
61
|
+
/** The element's own `scroll-margin-top` in px (0 when unset). */
|
|
62
|
+
function getScrollMarginTop(element) {
|
|
63
|
+
return Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The top of the scroll root in viewport coordinates (0 for the viewport). */
|
|
67
|
+
function getRootTop(scrollRoot) {
|
|
68
|
+
return scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Where a heading comes to rest, in viewport coordinates: below the fixed
|
|
73
|
+
* header (`offset`) and below its own `scroll-margin-top` breathing room.
|
|
74
|
+
*
|
|
75
|
+
* This is the single source of truth shared by the activation line and the
|
|
76
|
+
* scroll landing — the two must agree, or a heading activates somewhere other
|
|
77
|
+
* than where navigating to it puts it.
|
|
78
|
+
*/
|
|
79
|
+
function getRestingTop(target, scrollRoot, offset) {
|
|
80
|
+
return getRootTop(scrollRoot) + offset + getScrollMarginTop(target);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bring `target` to rest at the top of the scroll root, below any fixed header.
|
|
85
|
+
*
|
|
86
|
+
* With no `offset`, this is the CSS-native path: `scrollIntoView` already
|
|
87
|
+
* honors the heading's `scroll-margin-top`, and it walks *every* scrollable
|
|
88
|
+
* ancestor, so the browser does it better than we can.
|
|
89
|
+
*
|
|
90
|
+
* An `offset` describes a fixed header overlaying the top of the scroll root —
|
|
91
|
+
* which the browser cannot know about, so `scrollIntoView` would park the
|
|
92
|
+
* heading underneath it, hidden. Compute the landing explicitly instead, from
|
|
93
|
+
* the same {@link getRestingTop} the activation line uses.
|
|
94
|
+
*/
|
|
95
|
+
function scrollToTarget(target, scrollRoot, scrollTarget, offset) {
|
|
96
|
+
if (offset === 0) {
|
|
97
|
+
target.scrollIntoView({
|
|
98
|
+
behavior: 'smooth',
|
|
99
|
+
block: 'start'
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const delta = target.getBoundingClientRect().top - getRestingTop(target, scrollRoot, offset);
|
|
104
|
+
scrollTarget.scrollBy({
|
|
105
|
+
top: delta,
|
|
106
|
+
behavior: 'smooth'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
38
110
|
/**
|
|
39
111
|
* Resolve the active heading id from current scroll position.
|
|
40
112
|
*
|
|
41
|
-
* A heading is "passed" once its top reaches its activation line —
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
113
|
+
* A heading is "passed" once its top reaches its activation line — exactly
|
|
114
|
+
* where navigating to it would land it ({@link getRestingTop}): the scroll
|
|
115
|
+
* root's top, plus `offset` (a fixed header overlaying the scroll root), plus
|
|
116
|
+
* the heading's own scroll-margin-top. The active heading is the last passed
|
|
117
|
+
* one (headings are in document order). When none have passed (scrolled above
|
|
118
|
+
* the first), the first item is active; at the bottom, the last item is active.
|
|
46
119
|
*/
|
|
47
|
-
function resolveActiveId(items, scrollRoot) {
|
|
120
|
+
function resolveActiveId(items, scrollRoot, offset) {
|
|
48
121
|
if (items.length === 0) {
|
|
49
122
|
return undefined;
|
|
50
123
|
}
|
|
51
|
-
const rootTop = scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
|
|
52
124
|
const atBottom = scrollRoot != null ? scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 2 : window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
|
|
53
125
|
if (atBottom) {
|
|
54
126
|
return items[items.length - 1].id;
|
|
@@ -59,9 +131,10 @@ function resolveActiveId(items, scrollRoot) {
|
|
|
59
131
|
if (element == null) {
|
|
60
132
|
continue;
|
|
61
133
|
}
|
|
134
|
+
|
|
135
|
+
// 1px of tolerance absorbs sub-pixel rounding after a scroll lands.
|
|
62
136
|
const top = element.getBoundingClientRect().top;
|
|
63
|
-
|
|
64
|
-
if (top <= rootTop + marginTop + 1) {
|
|
137
|
+
if (top <= getRestingTop(element, scrollRoot, offset) + 1) {
|
|
65
138
|
activeId = item.id;
|
|
66
139
|
} else {
|
|
67
140
|
break;
|
|
@@ -73,17 +146,23 @@ export function useScrollSpy({
|
|
|
73
146
|
activeId,
|
|
74
147
|
items,
|
|
75
148
|
onActiveIdChange,
|
|
76
|
-
rootRef
|
|
149
|
+
rootRef,
|
|
150
|
+
offset = 0,
|
|
151
|
+
scrollContainerRef,
|
|
152
|
+
hasScrollOnClick = true,
|
|
153
|
+
onNavigateStart,
|
|
154
|
+
onNavigateEnd
|
|
77
155
|
}) {
|
|
78
156
|
const isControlled = activeId !== undefined;
|
|
79
157
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(items[0]?.id);
|
|
80
158
|
const activeIdRef = useRef(activeId);
|
|
81
|
-
// While true, scroll-spy ignores scroll updates because a
|
|
82
|
-
// programmatic scroll. Released when
|
|
159
|
+
// While true, scroll-spy ignores scroll updates because a navigation is
|
|
160
|
+
// driving a programmatic scroll. Released when it settles or the user scrolls.
|
|
83
161
|
const suppressRef = useRef(false);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
162
|
+
// The navigation currently in flight, if any.
|
|
163
|
+
const navigationRef = useRef(null);
|
|
164
|
+
// Latest scroll-position resolver, so a navigation can resume tracking when
|
|
165
|
+
// the user scrolls during the programmatic scroll.
|
|
87
166
|
const syncRef = useRef(null);
|
|
88
167
|
// Keep latest items/callback in refs so the scroll listener effect doesn't
|
|
89
168
|
// re-subscribe on every render (items is a fresh array each render).
|
|
@@ -93,11 +172,20 @@ export function useScrollSpy({
|
|
|
93
172
|
onActiveIdChangeRef.current = onActiveIdChange;
|
|
94
173
|
const itemIds = items.map(item => item.id).join('\n');
|
|
95
174
|
activeIdRef.current = isControlled ? activeId : uncontrolledActiveId;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The element whose scroll position drives the outline: the explicit
|
|
178
|
+
* container when scoped, else the nearest scrollable ancestor, else null
|
|
179
|
+
* (meaning the viewport).
|
|
180
|
+
*/
|
|
181
|
+
const getScrollRoot = useCallback(() => {
|
|
182
|
+
return scrollContainerRef?.current ?? getScrollableAncestor(rootRef.current);
|
|
183
|
+
}, [rootRef, scrollContainerRef]);
|
|
96
184
|
useEffect(() => {
|
|
97
185
|
if (isControlled || typeof window === 'undefined') {
|
|
98
186
|
return;
|
|
99
187
|
}
|
|
100
|
-
const scrollRoot =
|
|
188
|
+
const scrollRoot = getScrollRoot();
|
|
101
189
|
const scrollTarget = scrollRoot ?? window;
|
|
102
190
|
let frame = 0;
|
|
103
191
|
const update = () => {
|
|
@@ -105,7 +193,7 @@ export function useScrollSpy({
|
|
|
105
193
|
if (suppressRef.current) {
|
|
106
194
|
return;
|
|
107
195
|
}
|
|
108
|
-
const nextActiveId = resolveActiveId(itemsRef.current, scrollRoot);
|
|
196
|
+
const nextActiveId = resolveActiveId(itemsRef.current, scrollRoot, offset);
|
|
109
197
|
if (nextActiveId != null && nextActiveId !== activeIdRef.current) {
|
|
110
198
|
activeIdRef.current = nextActiveId;
|
|
111
199
|
setUncontrolledActiveId(nextActiveId);
|
|
@@ -133,12 +221,12 @@ export function useScrollSpy({
|
|
|
133
221
|
cancelAnimationFrame(frame);
|
|
134
222
|
}
|
|
135
223
|
};
|
|
136
|
-
}, [isControlled, itemIds,
|
|
224
|
+
}, [isControlled, itemIds, offset, getScrollRoot]);
|
|
137
225
|
|
|
138
|
-
// Tear down any
|
|
226
|
+
// Tear down any in-flight navigation's listeners when the Outline unmounts.
|
|
139
227
|
useEffect(() => {
|
|
140
228
|
return () => {
|
|
141
|
-
|
|
229
|
+
navigationRef.current?.teardown();
|
|
142
230
|
};
|
|
143
231
|
}, []);
|
|
144
232
|
const setActiveId = nextActiveId => {
|
|
@@ -147,68 +235,112 @@ export function useScrollSpy({
|
|
|
147
235
|
}
|
|
148
236
|
onActiveIdChange?.(nextActiveId);
|
|
149
237
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
238
|
+
|
|
239
|
+
// Defined in render (not memoized) so it always closes over the current props
|
|
240
|
+
// — it only ever runs from an event handler, never during render.
|
|
241
|
+
const scrollTo = id => {
|
|
242
|
+
const target = typeof document !== 'undefined' ? document.getElementById(id) : null;
|
|
243
|
+
if (target == null) {
|
|
244
|
+
return false;
|
|
156
245
|
}
|
|
157
246
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
247
|
+
// A second navigation replaces the first: end the old one (balancing its
|
|
248
|
+
// onNavigateStart) without resuming tracking, since we are about to
|
|
249
|
+
// suppress it again anyway.
|
|
250
|
+
navigationRef.current?.supersede();
|
|
251
|
+
onNavigateStart?.(id);
|
|
252
|
+
if (isControlled) {
|
|
253
|
+
// The consumer owns the active state — notify only.
|
|
254
|
+
setActiveId(id);
|
|
255
|
+
} else {
|
|
256
|
+
// Freeze the indicator during the programmatic scroll instead of moving
|
|
257
|
+
// it immediately: it lands on the target once the scroll settles, so it
|
|
258
|
+
// doesn't chase the scroll through intervening sections.
|
|
259
|
+
suppressRef.current = true;
|
|
260
|
+
}
|
|
261
|
+
const scrollRoot = getScrollRoot();
|
|
262
|
+
const scrollTarget = scrollRoot ?? window;
|
|
164
263
|
let settleTimer = 0;
|
|
264
|
+
let isSettled = false;
|
|
165
265
|
const cleanup = () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
266
|
+
scrollTarget.removeEventListener('scrollend', onSettle);
|
|
267
|
+
scrollTarget.removeEventListener('wheel', onManual);
|
|
268
|
+
scrollTarget.removeEventListener('touchmove', onManual);
|
|
169
269
|
window.removeEventListener('keydown', onKeyDown);
|
|
170
270
|
if (settleTimer !== 0) {
|
|
171
271
|
clearTimeout(settleTimer);
|
|
172
272
|
settleTimer = 0;
|
|
173
273
|
}
|
|
174
|
-
|
|
175
|
-
};
|
|
176
|
-
// Programmatic scroll finished: move the indicator to the clicked item.
|
|
177
|
-
const onSettle = () => {
|
|
178
|
-
cleanup();
|
|
179
|
-
suppressRef.current = false;
|
|
180
|
-
setUncontrolledActiveId(clickedId);
|
|
181
|
-
activeIdRef.current = clickedId;
|
|
182
|
-
onActiveIdChangeRef.current?.(clickedId);
|
|
274
|
+
navigationRef.current = null;
|
|
183
275
|
};
|
|
184
|
-
|
|
185
|
-
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* End the navigation exactly once. `didArrive` is false when the user took
|
|
279
|
+
* over with a manual scroll mid-flight. `onNavigateEnd` fires either way,
|
|
280
|
+
* so every `onNavigateStart` is balanced and a consumer's "navigating"
|
|
281
|
+
* state can never leak.
|
|
282
|
+
*/
|
|
283
|
+
const finish = (didArrive, shouldResume = true) => {
|
|
284
|
+
if (isSettled) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
isSettled = true;
|
|
186
288
|
cleanup();
|
|
187
|
-
|
|
188
|
-
|
|
289
|
+
if (!isControlled) {
|
|
290
|
+
suppressRef.current = false;
|
|
291
|
+
if (didArrive) {
|
|
292
|
+
setUncontrolledActiveId(id);
|
|
293
|
+
activeIdRef.current = id;
|
|
294
|
+
onActiveIdChangeRef.current?.(id);
|
|
295
|
+
} else if (shouldResume) {
|
|
296
|
+
// Hand control back to scroll-position tracking.
|
|
297
|
+
syncRef.current?.();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
onNavigateEnd?.(id);
|
|
189
301
|
};
|
|
302
|
+
const onSettle = () => finish(true);
|
|
303
|
+
const onManual = () => finish(false);
|
|
190
304
|
const onKeyDown = event => {
|
|
191
|
-
|
|
192
|
-
|
|
305
|
+
// A key the outline itself consumed (arrow roving focus, Space
|
|
306
|
+
// activation) is prevented, so the browser will not scroll — it is not a
|
|
307
|
+
// manual scroll intent and must not cancel the navigation it just began.
|
|
308
|
+
if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) {
|
|
309
|
+
finish(false);
|
|
193
310
|
}
|
|
194
311
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
312
|
+
if (hasScrollOnClick) {
|
|
313
|
+
// Arm settle detection *before* scrolling so an instant jump (a target
|
|
314
|
+
// already in position, or prefers-reduced-motion collapsing the smooth
|
|
315
|
+
// scroll) cannot land before anyone is listening.
|
|
316
|
+
scrollTarget.addEventListener('scrollend', onSettle, {
|
|
317
|
+
once: true
|
|
318
|
+
});
|
|
319
|
+
scrollTarget.addEventListener('wheel', onManual, {
|
|
320
|
+
passive: true
|
|
321
|
+
});
|
|
322
|
+
scrollTarget.addEventListener('touchmove', onManual, {
|
|
323
|
+
passive: true
|
|
324
|
+
});
|
|
325
|
+
window.addEventListener('keydown', onKeyDown);
|
|
326
|
+
settleTimer = window.setTimeout(onSettle, SCROLL_SETTLE_TIMEOUT_MS);
|
|
327
|
+
navigationRef.current = {
|
|
328
|
+
supersede: () => finish(false, false),
|
|
329
|
+
teardown: cleanup
|
|
330
|
+
};
|
|
331
|
+
scrollToTarget(target, scrollRoot, scrollTarget, offset);
|
|
332
|
+
} else {
|
|
333
|
+
// The consumer owns scrolling — there is no scroll to wait for, so the
|
|
334
|
+
// navigation is already at rest. Resolving immediately (instead of
|
|
335
|
+
// waiting on the settle timeout) keeps onNavigateEnd usable for arrival
|
|
336
|
+
// effects even when hasScrollOnClick is false.
|
|
337
|
+
finish(true);
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
};
|
|
209
341
|
return {
|
|
210
342
|
activeId: isControlled ? activeId : uncontrolledActiveId,
|
|
211
343
|
setActiveId,
|
|
212
|
-
|
|
344
|
+
scrollTo
|
|
213
345
|
};
|
|
214
346
|
}
|