@astryxdesign/core 0.1.2-canary.eb7243a → 0.1.2-canary.f03e2cc
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/README.md +12 -12
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +5 -11
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -24
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +28 -43
- package/dist/Calendar/styles.d.ts +9 -3
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +11 -3
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +3 -17
- package/dist/Chat/ChatMessageList.d.ts +1 -14
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +0 -2
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +1 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -4
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +4 -12
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +37 -49
- package/dist/ContextMenu/ContextMenu.d.ts +5 -4
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +11 -93
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +2 -17
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +4 -9
- package/dist/DateTimeInput/DateTimeInput.d.ts +1 -7
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +3 -15
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +0 -12
- package/dist/DropdownMenu/DropdownMenu.d.ts +8 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +14 -42
- package/dist/Field/Field.d.ts +2 -19
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +0 -4
- package/dist/Field/FieldLabel.d.ts +2 -21
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +26 -41
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +6 -27
- package/dist/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/InputGroup/InputGroup.js +1 -4
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +1 -33
- package/dist/Layer/layerAnimations.stylex.d.ts +0 -4
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +4 -8
- package/dist/Layer/useLayer.d.ts +0 -10
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +6 -41
- package/dist/Layout/padding.stylex.d.ts +0 -51
- package/dist/Layout/padding.stylex.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.js +0 -63
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +2 -4
- package/dist/Link/Link.d.ts +1 -7
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +7 -15
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +0 -5
- package/dist/MoreMenu/MoreMenu.d.ts +7 -1
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +17 -73
- package/dist/MultiSelector/hooks.d.ts +1 -12
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +1 -14
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +3 -34
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +1 -7
- package/dist/Pagination/Pagination.d.ts +1 -1
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +1 -15
- package/dist/Popover/usePopover.d.ts +2 -25
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +6 -18
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +1 -4
- package/dist/ProgressBar/ProgressBar.js +1 -1
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -76
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +0 -22
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -6
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +18 -54
- package/dist/Selector/hooks.d.ts +1 -8
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +1 -14
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +1 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +2 -8
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +1 -1
- package/dist/Stack/Stack.d.ts +1 -28
- package/dist/Stack/Stack.d.ts.map +1 -1
- package/dist/Stack/Stack.js +1 -17
- package/dist/Stack/StackItem.d.ts +1 -12
- package/dist/Stack/StackItem.d.ts.map +1 -1
- package/dist/Stack/StackItem.js +1 -8
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +0 -6
- package/dist/TabList/TabList.d.ts +4 -15
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +4 -58
- package/dist/TabList/TabListContext.d.ts +0 -5
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +0 -5
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -6
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +1 -7
- package/dist/Table/plugins/pagination/paginateData.d.ts +1 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +3 -8
- package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/useTablePagination.js +1 -6
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +3 -0
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +0 -14
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +3 -132
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +3 -7
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +9 -56
- package/dist/TreeList/TreeList.d.ts +1 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +2 -78
- package/dist/TreeList/TreeListItem.d.ts +1 -10
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +12 -51
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +4 -58
- package/dist/astryx.css +0 -7
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +2 -10
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -6
- package/dist/hooks/useClickableContainer.d.ts +1 -16
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +4 -16
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +4 -4
- package/dist/hooks/useFocusTrap.d.ts +0 -18
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +3 -87
- package/dist/hooks/useGridFocus.d.ts +1 -32
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +58 -143
- package/dist/hooks/useListFocus.d.ts +7 -78
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +52 -305
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +0 -1
- package/package.json +5 -11
- package/src/Avatar/Avatar.tsx +5 -10
- package/src/Breadcrumbs/BreadcrumbItem.tsx +7 -25
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +10 -31
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +4 -147
- package/src/Calendar/Calendar.tsx +43 -52
- package/src/Calendar/styles.ts +9 -4
- package/src/Carousel/Carousel.test.tsx +0 -20
- package/src/Carousel/Carousel.tsx +1 -16
- package/src/Chat/ChatMessageList.doc.mjs +0 -8
- package/src/Chat/ChatMessageList.test.tsx +0 -18
- package/src/Chat/ChatMessageList.tsx +0 -16
- package/src/Chat/useTriggerMenu.tsx +0 -3
- package/src/CheckboxInput/CheckboxInput.test.tsx +25 -14
- package/src/CheckboxInput/CheckboxInput.tsx +2 -4
- package/src/CheckboxList/CheckboxList.test.tsx +14 -30
- package/src/CheckboxList/CheckboxList.tsx +3 -18
- package/src/CodeBlock/CodeBlock.tsx +33 -61
- package/src/ContextMenu/ContextMenu.doc.mjs +4 -4
- package/src/ContextMenu/ContextMenu.test.tsx +1 -155
- package/src/ContextMenu/ContextMenu.tsx +16 -113
- package/src/DateInput/DateInput.test.tsx +0 -59
- package/src/DateInput/DateInput.tsx +7 -24
- package/src/DateRangeInput/DateRangeInput.test.tsx +18 -69
- package/src/DateRangeInput/DateRangeInput.tsx +3 -7
- package/src/DateTimeInput/DateTimeInput.doc.mjs +0 -7
- package/src/DateTimeInput/DateTimeInput.test.tsx +6 -99
- package/src/DateTimeInput/DateTimeInput.tsx +3 -32
- package/src/Dialog/Dialog.tsx +0 -12
- package/src/DropdownMenu/DropdownMenu.doc.mjs +8 -1
- package/src/DropdownMenu/DropdownMenu.test.tsx +42 -56
- package/src/DropdownMenu/DropdownMenu.tsx +18 -52
- package/src/Field/Field.test.tsx +0 -32
- package/src/Field/Field.tsx +5 -23
- package/src/Field/FieldLabel.tsx +17 -55
- package/src/FileInput/FileInput.test.tsx +49 -104
- package/src/FileInput/FileInput.tsx +3 -27
- package/src/InputGroup/InputGroup.test.tsx +63 -21
- package/src/InputGroup/InputGroup.tsx +1 -4
- package/src/Kbd/Kbd.test.tsx +4 -22
- package/src/Kbd/Kbd.tsx +3 -35
- package/src/Layer/layerAnimations.stylex.ts +4 -20
- package/src/Layer/useLayer.doc.mjs +0 -5
- package/src/Layer/useLayer.tsx +6 -67
- package/src/Layout/padding.stylex.ts +0 -52
- package/src/Lightbox/Lightbox.test.tsx +5 -53
- package/src/Lightbox/Lightbox.tsx +4 -9
- package/src/Link/Link.doc.mjs +0 -14
- package/src/Link/Link.test.tsx +0 -32
- package/src/Link/Link.tsx +4 -41
- package/src/Markdown/Markdown.test.tsx +0 -11
- package/src/Markdown/Markdown.tsx +0 -5
- package/src/MetadataList/MetadataListItem.doc.mjs +0 -4
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -2
- package/src/MoreMenu/MoreMenu.tsx +9 -0
- package/src/MultiSelector/MultiSelector.test.tsx +8 -226
- package/src/MultiSelector/MultiSelector.tsx +19 -91
- package/src/MultiSelector/hooks.ts +0 -27
- package/src/NavMenu/NavHeadingMenu.test.tsx +1 -77
- package/src/NavMenu/NavHeadingMenu.tsx +3 -50
- package/src/NumberInput/NumberInput.test.tsx +38 -72
- package/src/NumberInput/NumberInput.tsx +1 -12
- package/src/Pagination/Pagination.doc.mjs +3 -3
- package/src/Pagination/Pagination.test.tsx +5 -117
- package/src/Pagination/Pagination.tsx +1 -21
- package/src/Popover/usePopover.doc.mjs +3 -7
- package/src/Popover/usePopover.tsx +6 -49
- package/src/PowerSearch/PowerSearch.tsx +0 -3
- package/src/ProgressBar/ProgressBar.test.tsx +26 -34
- package/src/ProgressBar/ProgressBar.tsx +1 -1
- package/src/RadioList/RadioList.test.tsx +14 -129
- package/src/RadioList/RadioList.tsx +8 -103
- package/src/SegmentedControl/SegmentedControl.test.tsx +40 -84
- package/src/SegmentedControl/SegmentedControl.tsx +0 -26
- package/src/SegmentedControl/SegmentedControlItem.tsx +1 -4
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/Selector/Selector.test.tsx +14 -199
- package/src/Selector/Selector.tsx +38 -74
- package/src/Selector/hooks.ts +0 -23
- package/src/SideNav/SideNav.test.tsx +0 -16
- package/src/SideNav/SideNavItem.tsx +4 -2
- package/src/Skeleton/Skeleton.tsx +1 -11
- package/src/Spinner/Spinner.tsx +1 -7
- package/src/Stack/Stack.doc.mjs +0 -57
- package/src/Stack/Stack.test.tsx +5 -55
- package/src/Stack/Stack.tsx +1 -53
- package/src/Stack/StackItem.test.tsx +0 -23
- package/src/Stack/StackItem.tsx +0 -20
- package/src/TabList/Tab.tsx +0 -6
- package/src/TabList/TabList.doc.mjs +0 -6
- package/src/TabList/TabList.test.tsx +0 -167
- package/src/TabList/TabList.tsx +9 -72
- package/src/TabList/TabListContext.ts +3 -7
- package/src/TabList/TabMenu.tsx +3 -6
- package/src/Table/Table.test.tsx +0 -10
- package/src/Table/Table.tsx +0 -5
- package/src/Table/plugins/pagination/paginateData.ts +3 -10
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -16
- package/src/Table/plugins/pagination/useTablePagination.tsx +1 -8
- package/src/Table/tableContextMenu.tsx +5 -1
- package/src/Toast/Toast.tsx +0 -14
- package/src/Toast/ToastViewport.tsx +21 -167
- package/src/Toolbar/Toolbar.test.tsx +0 -45
- package/src/Toolbar/Toolbar.tsx +2 -5
- package/src/Tooltip/Tooltip.test.tsx +0 -68
- package/src/Tooltip/useTooltip.tsx +8 -55
- package/src/TreeList/TreeList.test.tsx +6 -295
- package/src/TreeList/TreeList.tsx +1 -84
- package/src/TreeList/TreeListItem.tsx +5 -50
- package/src/Typeahead/BaseTypeahead.tsx +8 -69
- package/src/Typeahead/Typeahead.test.tsx +0 -160
- package/src/hooks/index.ts +2 -21
- package/src/hooks/useClickableContainer.ts +4 -16
- package/src/hooks/useEntryAnimation.ts +4 -16
- package/src/hooks/useFocusTrap.ts +4 -98
- package/src/hooks/useGridFocus.ts +64 -184
- package/src/hooks/useListFocus.doc.mjs +10 -49
- package/src/hooks/useListFocus.ts +57 -395
- package/src/index.ts +0 -1
- package/src/utils/index.ts +0 -7
- 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/dist/VisuallyHidden/VisuallyHidden.d.ts +0 -61
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +0 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +0 -65
- package/dist/VisuallyHidden/index.d.ts +0 -11
- package/dist/VisuallyHidden/index.d.ts.map +0 -1
- package/dist/VisuallyHidden/index.js +0 -13
- package/dist/hooks/focusableSelector.d.ts +0 -22
- package/dist/hooks/focusableSelector.d.ts.map +0 -1
- package/dist/hooks/focusableSelector.js +0 -24
- package/dist/hooks/useAnnounce.d.ts +0 -40
- package/dist/hooks/useAnnounce.d.ts.map +0 -1
- package/dist/hooks/useAnnounce.js +0 -142
- package/dist/hooks/useLongPress.d.ts +0 -21
- package/dist/hooks/useLongPress.d.ts.map +0 -1
- package/dist/hooks/useLongPress.js +0 -89
- package/dist/hooks/useTreeFocus.d.ts +0 -133
- package/dist/hooks/useTreeFocus.d.ts.map +0 -1
- package/dist/hooks/useTreeFocus.js +0 -274
- package/dist/hooks/useTypeahead.d.ts +0 -63
- package/dist/hooks/useTypeahead.d.ts.map +0 -1
- package/dist/hooks/useTypeahead.js +0 -117
- package/src/Avatar/Avatar.test.tsx +0 -37
- package/src/CodeBlock/CodeBlock.test.tsx +0 -115
- package/src/Skeleton/Skeleton.test.tsx +0 -25
- package/src/Toast/ToastViewport.test.tsx +0 -204
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
- package/src/VisuallyHidden/VisuallyHidden.doc.mjs +0 -80
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +0 -76
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -113
- package/src/VisuallyHidden/index.ts +0 -15
- package/src/hooks/focusableSelector.ts +0 -25
- package/src/hooks/useAnnounce.test.tsx +0 -101
- package/src/hooks/useAnnounce.ts +0 -168
- package/src/hooks/useFocusTrap.test.tsx +0 -132
- package/src/hooks/useListFocus.test.tsx +0 -375
- package/src/hooks/useLongPress.test.tsx +0 -134
- package/src/hooks/useLongPress.ts +0 -119
- package/src/hooks/useTreeFocus.doc.mjs +0 -126
- package/src/hooks/useTreeFocus.test.tsx +0 -214
- package/src/hooks/useTreeFocus.ts +0 -464
- package/src/hooks/useTypeahead.test.tsx +0 -109
- package/src/hooks/useTypeahead.ts +0 -183
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file useTreeFocus.ts
|
|
7
|
-
* @input Uses React useCallback, useRef
|
|
8
|
-
* @output Exports useTreeFocus hook for WAI-ARIA tree keyboard navigation
|
|
9
|
-
* @position Core hook; used by TreeList for roving tabindex + APG tree keyboard model
|
|
10
|
-
*
|
|
11
|
-
* SYNC: When modified, update:
|
|
12
|
-
* - /packages/core/src/hooks/index.ts
|
|
13
|
-
* - /packages/core/src/hooks/useTreeFocus.doc.mjs
|
|
14
|
-
* - /packages/core/src/hooks/useTreeFocus.test.tsx
|
|
15
|
-
*/
|
|
16
|
-
import { useCallback, useRef } from 'react';
|
|
17
|
-
|
|
18
|
-
/** Keys handled by the tree keyboard model (used to gate typeahead). */
|
|
19
|
-
const NAVIGATION_KEYS = new Set(['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End', 'Enter', ' ']);
|
|
20
|
-
|
|
21
|
-
/** Default reset delay for the typeahead buffer. */
|
|
22
|
-
const DEFAULT_TYPEAHEAD_RESET_MS = 500;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Configuration for tree focus behavior.
|
|
26
|
-
*
|
|
27
|
-
* The tree keyboard model differs from a linear list (useListFocus): while
|
|
28
|
-
* ArrowUp/ArrowDown/Home/End roam linearly over the *visible* treeitems,
|
|
29
|
-
* ArrowRight/ArrowLeft carry tree semantics (expand/collapse, move to
|
|
30
|
-
* first-child / parent). The hook stays generic by taking callbacks for the
|
|
31
|
-
* tree-specific bits (expansion toggling, activation) — mirroring how
|
|
32
|
-
* useGridFocus takes `isCellFocusable` rather than hardcoding disabled logic.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Return type for useTreeFocus hook.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Hook for managing roving-tabindex focus + the WAI-ARIA tree keyboard model.
|
|
41
|
-
*
|
|
42
|
-
* A tree is not a linear list: ArrowUp/ArrowDown/Home/End roam linearly over
|
|
43
|
-
* the *visible* treeitems (skipping disabled ones), but ArrowRight/ArrowLeft
|
|
44
|
-
* carry tree semantics. This is why a tree needs its own hook rather than
|
|
45
|
-
* reusing `useListFocus` — the same reason 2D grids get `useGridFocus`.
|
|
46
|
-
*
|
|
47
|
-
* - ArrowDown / ArrowUp: move to next/previous visible treeitem (skip disabled)
|
|
48
|
-
* - ArrowRight: collapsed parent → expand; expanded parent → first child;
|
|
49
|
-
* leaf → no-op
|
|
50
|
-
* - ArrowLeft: expanded parent → collapse; otherwise → move to parent treeitem
|
|
51
|
-
* - Home / End: first / last visible treeitem
|
|
52
|
-
* - Enter / Space: activate (`onActivate`), falling back to expansion toggle
|
|
53
|
-
* - Printable characters: typeahead to the next matching treeitem
|
|
54
|
-
*
|
|
55
|
-
* The hook is DOM-query based (reads aria-expanded/aria-level and data-*
|
|
56
|
-
* attributes) and stays generic via option callbacks for the tree-specific
|
|
57
|
-
* bits (`onToggleExpand`, `onActivate`, `getLevel`, `isItemDisabled`, …).
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```
|
|
61
|
-
* const {treeRef, handleKeyDown} = useTreeFocus<HTMLUListElement>({
|
|
62
|
-
* onToggleExpand: id => toggle(id),
|
|
63
|
-
* onActiveChange: id => setActiveId(id),
|
|
64
|
-
* });
|
|
65
|
-
*
|
|
66
|
-
* <ul ref={treeRef} role="tree" onKeyDown={handleKeyDown}>
|
|
67
|
-
* {items.map(item => <li role="treeitem" tabIndex={-1}>{item.label}</li>)}
|
|
68
|
-
* </ul>
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export function useTreeFocus(options = {}) {
|
|
72
|
-
const {
|
|
73
|
-
itemSelector = '[role="treeitem"]',
|
|
74
|
-
isItemDisabled,
|
|
75
|
-
getLevel,
|
|
76
|
-
isExpanded,
|
|
77
|
-
isCollapsed,
|
|
78
|
-
getItemId,
|
|
79
|
-
onToggleExpand,
|
|
80
|
-
onActivate,
|
|
81
|
-
typeahead = true,
|
|
82
|
-
typeaheadResetMs = DEFAULT_TYPEAHEAD_RESET_MS,
|
|
83
|
-
onActiveChange
|
|
84
|
-
} = options;
|
|
85
|
-
const treeRef = useRef(null);
|
|
86
|
-
const typeaheadRef = useRef({
|
|
87
|
-
buffer: '',
|
|
88
|
-
timer: null
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
/** Visible treeitems in DOM order (collapsed subtrees are not rendered). */
|
|
92
|
-
const getItems = useCallback(() => {
|
|
93
|
-
const root = treeRef.current;
|
|
94
|
-
if (root == null) {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
return Array.from(root.querySelectorAll(itemSelector));
|
|
98
|
-
}, [itemSelector]);
|
|
99
|
-
const itemDisabled = useCallback(el => isItemDisabled ? isItemDisabled(el) : el.dataset.treeDisabled != null || el.getAttribute('aria-disabled') === 'true', [isItemDisabled]);
|
|
100
|
-
const levelOf = useCallback(el => getLevel ? getLevel(el) : Number(el.getAttribute('aria-level') ?? '1'), [getLevel]);
|
|
101
|
-
const expandedOf = useCallback(el => isExpanded ? isExpanded(el) : el.getAttribute('aria-expanded') === 'true', [isExpanded]);
|
|
102
|
-
const collapsedOf = useCallback(el => isCollapsed ? isCollapsed(el) : el.getAttribute('aria-expanded') === 'false', [isCollapsed]);
|
|
103
|
-
const idOf = useCallback(el => getItemId ? getItemId(el) : el.dataset.treeId, [getItemId]);
|
|
104
|
-
|
|
105
|
-
/** Move focus to a treeitem and notify the active-change listener. */
|
|
106
|
-
const focusItem = useCallback(el => {
|
|
107
|
-
if (el == null) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
onActiveChange?.(idOf(el));
|
|
111
|
-
el.focus();
|
|
112
|
-
}, [idOf, onActiveChange]);
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Focus the first enabled treeitem from `start`, moving by `dir`. No wrap —
|
|
116
|
-
* a tree's linear roam clamps at the ends.
|
|
117
|
-
*/
|
|
118
|
-
const focusEnabledFrom = useCallback((items, start, dir) => {
|
|
119
|
-
for (let i = start; i >= 0 && i < items.length; i += dir) {
|
|
120
|
-
const candidate = items[i];
|
|
121
|
-
if (candidate != null && !itemDisabled(candidate)) {
|
|
122
|
-
focusItem(candidate);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}, [itemDisabled, focusItem]);
|
|
127
|
-
const focusFirst = useCallback(() => {
|
|
128
|
-
focusEnabledFrom(getItems(), 0, 1);
|
|
129
|
-
}, [getItems, focusEnabledFrom]);
|
|
130
|
-
const focusLast = useCallback(() => {
|
|
131
|
-
const items = getItems();
|
|
132
|
-
focusEnabledFrom(items, items.length - 1, -1);
|
|
133
|
-
}, [getItems, focusEnabledFrom]);
|
|
134
|
-
const runTypeahead = useCallback((e, items, currentIndex) => {
|
|
135
|
-
const state = typeaheadRef.current;
|
|
136
|
-
if (state.timer != null) {
|
|
137
|
-
clearTimeout(state.timer);
|
|
138
|
-
}
|
|
139
|
-
state.buffer += e.key.toLowerCase();
|
|
140
|
-
state.timer = setTimeout(() => {
|
|
141
|
-
typeaheadRef.current.buffer = '';
|
|
142
|
-
}, typeaheadResetMs);
|
|
143
|
-
const query = state.buffer;
|
|
144
|
-
const start = currentIndex < 0 ? 0 : currentIndex;
|
|
145
|
-
const ordered = [...items.slice(start + 1), ...items.slice(0, start + 1)];
|
|
146
|
-
const match = ordered.find(item => !itemDisabled(item) && (item.textContent ?? '').trim().toLowerCase().startsWith(query));
|
|
147
|
-
if (match != null) {
|
|
148
|
-
e.preventDefault();
|
|
149
|
-
focusItem(match);
|
|
150
|
-
}
|
|
151
|
-
}, [typeaheadResetMs, itemDisabled, focusItem]);
|
|
152
|
-
const handleKeyDown = useCallback(e => {
|
|
153
|
-
const items = getItems();
|
|
154
|
-
if (items.length === 0) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const active = document.activeElement;
|
|
158
|
-
// Resolve the treeitem that owns focus: the nearest treeitem ancestor of
|
|
159
|
-
// the active element (never an outer treeitem that merely contains it).
|
|
160
|
-
const activeItem = active instanceof Element ? active.closest('[role="treeitem"]') : null;
|
|
161
|
-
const currentIndex = items.findIndex(item => item === activeItem);
|
|
162
|
-
const current = currentIndex >= 0 ? items[currentIndex] : undefined;
|
|
163
|
-
|
|
164
|
-
// Typeahead: printable single characters jump to the next matching item.
|
|
165
|
-
if (typeahead && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey && NAVIGATION_KEYS.has(e.key) === false) {
|
|
166
|
-
runTypeahead(e, items, currentIndex);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (!NAVIGATION_KEYS.has(e.key)) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
switch (e.key) {
|
|
173
|
-
case 'ArrowDown':
|
|
174
|
-
{
|
|
175
|
-
e.preventDefault();
|
|
176
|
-
focusEnabledFrom(items, currentIndex < 0 ? 0 : currentIndex + 1, 1);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
case 'ArrowUp':
|
|
180
|
-
{
|
|
181
|
-
e.preventDefault();
|
|
182
|
-
focusEnabledFrom(items, currentIndex < 0 ? items.length - 1 : currentIndex - 1, -1);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
case 'ArrowRight':
|
|
186
|
-
{
|
|
187
|
-
if (current == null) {
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
e.preventDefault();
|
|
191
|
-
if (collapsedOf(current)) {
|
|
192
|
-
// Collapsed parent → expand.
|
|
193
|
-
const id = idOf(current);
|
|
194
|
-
if (id != null) {
|
|
195
|
-
onToggleExpand?.(id);
|
|
196
|
-
}
|
|
197
|
-
} else if (expandedOf(current)) {
|
|
198
|
-
// Expanded parent → move to first child.
|
|
199
|
-
const next = items[currentIndex + 1];
|
|
200
|
-
if (next != null && levelOf(next) > levelOf(current)) {
|
|
201
|
-
focusItem(next);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// Leaf → no-op.
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
case 'ArrowLeft':
|
|
208
|
-
{
|
|
209
|
-
if (current == null) {
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
if (expandedOf(current)) {
|
|
214
|
-
// Expanded parent → collapse.
|
|
215
|
-
const id = idOf(current);
|
|
216
|
-
if (id != null) {
|
|
217
|
-
onToggleExpand?.(id);
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
// Otherwise → move to parent treeitem (nearest shallower item
|
|
221
|
-
// scanning upward in visible order).
|
|
222
|
-
const currentLevel = levelOf(current);
|
|
223
|
-
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
224
|
-
const candidate = items[i];
|
|
225
|
-
if (candidate != null && levelOf(candidate) < currentLevel) {
|
|
226
|
-
focusItem(candidate);
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
case 'Home':
|
|
234
|
-
{
|
|
235
|
-
e.preventDefault();
|
|
236
|
-
focusEnabledFrom(items, 0, 1);
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
case 'End':
|
|
240
|
-
{
|
|
241
|
-
e.preventDefault();
|
|
242
|
-
focusEnabledFrom(items, items.length - 1, -1);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case 'Enter':
|
|
246
|
-
case ' ':
|
|
247
|
-
{
|
|
248
|
-
if (current == null || itemDisabled(current)) {
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
e.preventDefault();
|
|
252
|
-
const id = idOf(current);
|
|
253
|
-
// Activate the item: prefer the consumer's onActivate (e.g. click an
|
|
254
|
-
// inner link/button). Fall back to toggling expansion for a parent
|
|
255
|
-
// without its own action.
|
|
256
|
-
const handled = onActivate ? onActivate(current, id) === true : false;
|
|
257
|
-
if (!handled && current.getAttribute('aria-expanded') != null) {
|
|
258
|
-
if (id != null) {
|
|
259
|
-
onToggleExpand?.(id);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
default:
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
}, [getItems, typeahead, runTypeahead, focusEnabledFrom, focusItem, collapsedOf, expandedOf, idOf, levelOf, itemDisabled, onToggleExpand, onActivate]);
|
|
268
|
-
return {
|
|
269
|
-
treeRef,
|
|
270
|
-
handleKeyDown,
|
|
271
|
-
focusFirst,
|
|
272
|
-
focusLast
|
|
273
|
-
};
|
|
274
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export interface UseTypeaheadOptions {
|
|
2
|
-
/**
|
|
3
|
-
* Returns the current, in-DOM-order list of item labels to match against.
|
|
4
|
-
* An entry may be `null`/empty to mark a non-matchable slot (its index is
|
|
5
|
-
* preserved so it maps 1:1 to the caller's items).
|
|
6
|
-
*/
|
|
7
|
-
getItemLabels: () => ReadonlyArray<string | null | undefined>;
|
|
8
|
-
/**
|
|
9
|
-
* Called with the index of the matched item so the caller can focus/select
|
|
10
|
-
* it (e.g. `useListFocus`'s `focusItem`).
|
|
11
|
-
*/
|
|
12
|
-
onMatch: (index: number) => void;
|
|
13
|
-
/**
|
|
14
|
-
* The index to start searching from — typically the currently focused item,
|
|
15
|
-
* so repeated presses of the same letter cycle through matches. Defaults to
|
|
16
|
-
* -1 (search from the top).
|
|
17
|
-
*/
|
|
18
|
-
getCurrentIndex?: () => number;
|
|
19
|
-
/**
|
|
20
|
-
* Milliseconds of inactivity after which the typed buffer resets.
|
|
21
|
-
* @default 750
|
|
22
|
-
*/
|
|
23
|
-
resetMs?: number;
|
|
24
|
-
/**
|
|
25
|
-
* Whether an index should be skipped (e.g. disabled items).
|
|
26
|
-
*/
|
|
27
|
-
isDisabled?: (index: number) => boolean;
|
|
28
|
-
}
|
|
29
|
-
export interface UseTypeaheadReturn {
|
|
30
|
-
/**
|
|
31
|
-
* Keydown handler. Call it (or let it fall through) from the collection's
|
|
32
|
-
* own key handler. Returns `true` when it handled a printable character (so
|
|
33
|
-
* the caller can `preventDefault`/stop further handling), `false` otherwise.
|
|
34
|
-
*/
|
|
35
|
-
onKeyDown: (e: React.KeyboardEvent | KeyboardEvent) => boolean;
|
|
36
|
-
/** Clear the pending buffer (e.g. on close). */
|
|
37
|
-
reset: () => void;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* First-character ("type-ahead") search for a collection.
|
|
41
|
-
*
|
|
42
|
-
* Buffers printable keystrokes (resetting after `resetMs` of inactivity),
|
|
43
|
-
* matches items whose label starts with the buffer, and cycles from the
|
|
44
|
-
* current index — so pressing "s" repeatedly walks through the "s" items
|
|
45
|
-
* instead of sticking on the first. Space is reserved for activation and is
|
|
46
|
-
* only treated as typeahead while a buffer is already active.
|
|
47
|
-
*
|
|
48
|
-
* Additive and collection-agnostic: it does not move focus itself; the caller
|
|
49
|
-
* wires `onMatch` to its own focus/selection (e.g. `useListFocus.focusItem`).
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```
|
|
53
|
-
* const {listRef, handleKeyDown, focusItem, getItems} = useListFocus(...);
|
|
54
|
-
* const typeahead = useTypeahead({
|
|
55
|
-
* getItemLabels: () => getItems().map(el => el.textContent),
|
|
56
|
-
* onMatch: focusItem,
|
|
57
|
-
* getCurrentIndex: () => getItems().findIndex(el => el === document.activeElement),
|
|
58
|
-
* });
|
|
59
|
-
* const onKeyDown = (e) => { if (!typeahead.onKeyDown(e)) handleKeyDown(e); };
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
export declare function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn;
|
|
63
|
-
//# sourceMappingURL=useTypeahead.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../src/hooks/useTypeahead.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAE9D;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAE/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,KAAK,OAAO,CAAC;IAC/D,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAiBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAkF7E"}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file useTypeahead.ts
|
|
7
|
-
* @input Uses React useCallback/useRef
|
|
8
|
-
* @output Exports useTypeahead hook for first-character (type-to-focus) search
|
|
9
|
-
* @position Core hook; adds APG typeahead to menus, listboxes, and other
|
|
10
|
-
* collections. Composes with useListFocus/useGridFocus (or any collection)
|
|
11
|
-
* via a caller-supplied getItemLabels + onMatch.
|
|
12
|
-
*
|
|
13
|
-
* SYNC: When modified, update:
|
|
14
|
-
* - /packages/core/src/hooks/index.ts
|
|
15
|
-
*/
|
|
16
|
-
import { useCallback, useRef } from 'react';
|
|
17
|
-
/**
|
|
18
|
-
* Whether a key event represents a single printable character (a type-to-focus
|
|
19
|
-
* candidate) rather than a control/navigation key or a shortcut chord.
|
|
20
|
-
*/
|
|
21
|
-
function isPrintableCharacter(e) {
|
|
22
|
-
return e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey &&
|
|
23
|
-
// A lone space is used for activation in menus, not typeahead-from-empty.
|
|
24
|
-
e.key !== ' ';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* First-character ("type-ahead") search for a collection.
|
|
29
|
-
*
|
|
30
|
-
* Buffers printable keystrokes (resetting after `resetMs` of inactivity),
|
|
31
|
-
* matches items whose label starts with the buffer, and cycles from the
|
|
32
|
-
* current index — so pressing "s" repeatedly walks through the "s" items
|
|
33
|
-
* instead of sticking on the first. Space is reserved for activation and is
|
|
34
|
-
* only treated as typeahead while a buffer is already active.
|
|
35
|
-
*
|
|
36
|
-
* Additive and collection-agnostic: it does not move focus itself; the caller
|
|
37
|
-
* wires `onMatch` to its own focus/selection (e.g. `useListFocus.focusItem`).
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```
|
|
41
|
-
* const {listRef, handleKeyDown, focusItem, getItems} = useListFocus(...);
|
|
42
|
-
* const typeahead = useTypeahead({
|
|
43
|
-
* getItemLabels: () => getItems().map(el => el.textContent),
|
|
44
|
-
* onMatch: focusItem,
|
|
45
|
-
* getCurrentIndex: () => getItems().findIndex(el => el === document.activeElement),
|
|
46
|
-
* });
|
|
47
|
-
* const onKeyDown = (e) => { if (!typeahead.onKeyDown(e)) handleKeyDown(e); };
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
export function useTypeahead(options) {
|
|
51
|
-
const {
|
|
52
|
-
getItemLabels,
|
|
53
|
-
onMatch,
|
|
54
|
-
getCurrentIndex,
|
|
55
|
-
resetMs = 750,
|
|
56
|
-
isDisabled
|
|
57
|
-
} = options;
|
|
58
|
-
const bufferRef = useRef('');
|
|
59
|
-
const timeoutRef = useRef(undefined);
|
|
60
|
-
const reset = useCallback(() => {
|
|
61
|
-
bufferRef.current = '';
|
|
62
|
-
if (timeoutRef.current) {
|
|
63
|
-
clearTimeout(timeoutRef.current);
|
|
64
|
-
timeoutRef.current = undefined;
|
|
65
|
-
}
|
|
66
|
-
}, []);
|
|
67
|
-
const scheduleReset = useCallback(() => {
|
|
68
|
-
if (timeoutRef.current) {
|
|
69
|
-
clearTimeout(timeoutRef.current);
|
|
70
|
-
}
|
|
71
|
-
timeoutRef.current = setTimeout(() => {
|
|
72
|
-
bufferRef.current = '';
|
|
73
|
-
timeoutRef.current = undefined;
|
|
74
|
-
}, resetMs);
|
|
75
|
-
}, [resetMs]);
|
|
76
|
-
const onKeyDown = useCallback(e => {
|
|
77
|
-
// A bare Space with no active buffer is not typeahead (menus activate on
|
|
78
|
-
// Space); once the user is mid-typing, Space extends the query.
|
|
79
|
-
const isSpaceMidType = e.key === ' ' && bufferRef.current.length > 0;
|
|
80
|
-
if (!isPrintableCharacter(e) && !isSpaceMidType) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
const labels = getItemLabels();
|
|
84
|
-
if (labels.length === 0) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Pressing the SAME single character repeatedly cycles to the next match
|
|
89
|
-
// rather than filtering deeper (APG behavior).
|
|
90
|
-
const char = e.key.toLowerCase();
|
|
91
|
-
const isRepeatSameChar = bufferRef.current.length > 0 && bufferRef.current.split('').every(c => c === char);
|
|
92
|
-
const nextBuffer = isRepeatSameChar ? char : bufferRef.current + char;
|
|
93
|
-
bufferRef.current = nextBuffer;
|
|
94
|
-
scheduleReset();
|
|
95
|
-
const start = getCurrentIndex?.() ?? -1;
|
|
96
|
-
const count = labels.length;
|
|
97
|
-
// When repeating a single char, start the search AFTER the current item
|
|
98
|
-
// so we advance; otherwise include the current item.
|
|
99
|
-
const offset = isRepeatSameChar ? 1 : 0;
|
|
100
|
-
for (let i = 0; i < count; i++) {
|
|
101
|
-
const index = (start + offset + i + count) % count;
|
|
102
|
-
if (isDisabled?.(index)) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const label = labels[index];
|
|
106
|
-
if (label != null && label.trim().toLowerCase().startsWith(nextBuffer)) {
|
|
107
|
-
onMatch(index);
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}, [getItemLabels, onMatch, getCurrentIndex, scheduleReset, isDisabled]);
|
|
113
|
-
return {
|
|
114
|
-
onKeyDown,
|
|
115
|
-
reset
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {render, screen} from '@testing-library/react';
|
|
5
|
-
import {Avatar} from './Avatar';
|
|
6
|
-
|
|
7
|
-
describe('Avatar', () => {
|
|
8
|
-
it('exposes role="img" with the name as accessible name', () => {
|
|
9
|
-
render(<Avatar name="Ada Lovelace" data-testid="a" />);
|
|
10
|
-
expect(screen.getByRole('img', {name: 'Ada Lovelace'})).toBeInTheDocument();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('uses alt over name for the accessible name', () => {
|
|
14
|
-
render(<Avatar name="Ada" alt="Ada Lovelace, profile photo" />);
|
|
15
|
-
expect(
|
|
16
|
-
screen.getByRole('img', {name: 'Ada Lovelace, profile photo'}),
|
|
17
|
-
).toBeInTheDocument();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('is decorative (presentation + aria-hidden) when it has no name or alt (obs-9)', () => {
|
|
21
|
-
render(<Avatar data-testid="a" />);
|
|
22
|
-
const el = screen.getByTestId('a');
|
|
23
|
-
// No meaningful name → not announced as a generic "Avatar".
|
|
24
|
-
expect(el).toHaveAttribute('aria-hidden', 'true');
|
|
25
|
-
expect(el).not.toHaveAttribute('aria-label');
|
|
26
|
-
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('does not double-announce: the inner img is decorative when the wrapper is named', () => {
|
|
30
|
-
render(<Avatar name="Ada" src="https://example.com/ada.jpg" />);
|
|
31
|
-
const wrapper = screen.getByRole('img', {name: 'Ada'});
|
|
32
|
-
const innerImg = wrapper.querySelector('img');
|
|
33
|
-
expect(innerImg).not.toBeNull();
|
|
34
|
-
// The inner <img> carries an empty alt so it isn't announced separately.
|
|
35
|
-
expect(innerImg).toHaveAttribute('alt', '');
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file CodeBlock.test.tsx
|
|
5
|
-
* @input Uses vitest, @testing-library/react, CodeBlock component
|
|
6
|
-
* @output Unit tests for CodeBlock (copy button, collapse, scroll region a11y)
|
|
7
|
-
* @position Testing; validates CodeBlock implementation
|
|
8
|
-
*
|
|
9
|
-
* SYNC: When CodeBlock.tsx changes, update tests to match new behavior
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
13
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
|
-
import {CodeBlock} from './CodeBlock';
|
|
15
|
-
|
|
16
|
-
// A code sample long enough to exceed the default collapsible threshold (10).
|
|
17
|
-
const LONG_CODE = Array.from(
|
|
18
|
-
{length: 15},
|
|
19
|
-
(_, i) => `const line${i} = ${i};`,
|
|
20
|
-
).join('\n');
|
|
21
|
-
|
|
22
|
-
describe('CodeBlock', () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
// jsdom does not implement the async Clipboard API.
|
|
25
|
-
Object.assign(navigator, {
|
|
26
|
-
clipboard: {writeText: vi.fn().mockResolvedValue(undefined)},
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('renders the code', () => {
|
|
31
|
-
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
32
|
-
expect(screen.getByText(/const/)).toBeInTheDocument();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('makes the scroll container keyboard-focusable', () => {
|
|
36
|
-
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
37
|
-
const region = screen.getByRole('region');
|
|
38
|
-
expect(region).toHaveAttribute('tabindex', '0');
|
|
39
|
-
expect(region).toHaveAttribute('aria-label', 'javascript');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('labels the scroll region "Code" when no language label is shown', () => {
|
|
43
|
-
render(<CodeBlock code="hello" hasLanguageLabel={false} />);
|
|
44
|
-
const region = screen.getByRole('region');
|
|
45
|
-
expect(region).toHaveAttribute('tabindex', '0');
|
|
46
|
-
expect(region).toHaveAttribute('aria-label', 'Code');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('copies code when the copy button is clicked', () => {
|
|
50
|
-
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
51
|
-
const copyButton = screen.getByRole('button', {name: 'Copy code'});
|
|
52
|
-
fireEvent.click(copyButton);
|
|
53
|
-
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('const x = 1;');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('does NOT collapse the block when the copy button is clicked', () => {
|
|
57
|
-
render(
|
|
58
|
-
<CodeBlock
|
|
59
|
-
code={LONG_CODE}
|
|
60
|
-
language="javascript"
|
|
61
|
-
title="example"
|
|
62
|
-
isCollapsible
|
|
63
|
-
/>,
|
|
64
|
-
);
|
|
65
|
-
// The collapsible header exposes aria-expanded.
|
|
66
|
-
const header = screen
|
|
67
|
-
.getAllByRole('button')
|
|
68
|
-
.find(el => el.hasAttribute('aria-expanded'));
|
|
69
|
-
expect(header).toBeTruthy();
|
|
70
|
-
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
71
|
-
|
|
72
|
-
const copyButton = screen.getByRole('button', {name: 'Copy code'});
|
|
73
|
-
fireEvent.click(copyButton);
|
|
74
|
-
|
|
75
|
-
// Clicking Copy must not toggle the collapsible header.
|
|
76
|
-
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
77
|
-
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('does not nest the copy button inside the collapsible header role="button"', () => {
|
|
81
|
-
render(
|
|
82
|
-
<CodeBlock
|
|
83
|
-
code={LONG_CODE}
|
|
84
|
-
language="javascript"
|
|
85
|
-
title="example"
|
|
86
|
-
isCollapsible
|
|
87
|
-
/>,
|
|
88
|
-
);
|
|
89
|
-
const header = screen
|
|
90
|
-
.getAllByRole('button')
|
|
91
|
-
.find(el => el.hasAttribute('aria-expanded'));
|
|
92
|
-
const copyButton = screen.getByRole('button', {name: 'Copy code'});
|
|
93
|
-
expect(header).toBeTruthy();
|
|
94
|
-
// The copy button must be a sibling, not a descendant of the interactive
|
|
95
|
-
// header — nested interactive controls are invalid ARIA.
|
|
96
|
-
expect(header!.contains(copyButton)).toBe(false);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('still toggles collapse when the header itself is clicked', () => {
|
|
100
|
-
render(
|
|
101
|
-
<CodeBlock
|
|
102
|
-
code={LONG_CODE}
|
|
103
|
-
language="javascript"
|
|
104
|
-
title="example"
|
|
105
|
-
isCollapsible
|
|
106
|
-
/>,
|
|
107
|
-
);
|
|
108
|
-
const header = screen
|
|
109
|
-
.getAllByRole('button')
|
|
110
|
-
.find(el => el.hasAttribute('aria-expanded'))!;
|
|
111
|
-
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
112
|
-
fireEvent.click(header);
|
|
113
|
-
expect(header).toHaveAttribute('aria-expanded', 'false');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {render, screen} from '@testing-library/react';
|
|
5
|
-
import {Skeleton} from './Skeleton';
|
|
6
|
-
|
|
7
|
-
describe('Skeleton', () => {
|
|
8
|
-
it('renders a placeholder element', () => {
|
|
9
|
-
render(<Skeleton width={200} height={20} data-testid="sk" />);
|
|
10
|
-
expect(screen.getByTestId('sk')).toBeInTheDocument();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('is hidden from assistive tech by default (complex-20)', () => {
|
|
14
|
-
render(<Skeleton width={200} height={20} data-testid="sk" />);
|
|
15
|
-
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'true');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('allows the aria-hidden default to be overridden', () => {
|
|
19
|
-
render(
|
|
20
|
-
<Skeleton width={200} height={20} data-testid="sk" aria-hidden={false} />,
|
|
21
|
-
);
|
|
22
|
-
// Consumer opt-out: not hidden when explicitly set false.
|
|
23
|
-
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'false');
|
|
24
|
-
});
|
|
25
|
-
});
|