@astryxdesign/core 0.1.2-canary.bfcbf64 → 0.1.2-canary.c395fca
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/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +4 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -4
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +33 -14
- package/dist/Calendar/hooks/index.d.ts +0 -2
- package/dist/Calendar/hooks/index.d.ts.map +1 -1
- package/dist/Calendar/hooks/index.js +1 -2
- package/dist/Calendar/index.d.ts +2 -2
- package/dist/Calendar/index.d.ts.map +1 -1
- package/dist/Calendar/index.js +1 -1
- package/dist/CheckboxList/CheckboxList.js +3 -3
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +3 -3
- package/dist/ContextMenu/ContextMenu.d.ts +1 -7
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +7 -13
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +4 -9
- package/dist/Field/Field.d.ts +4 -4
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +2 -2
- package/dist/Field/FieldLabel.d.ts +4 -4
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +2 -2
- package/dist/InputGroup/InputGroup.js +3 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +2 -4
- package/dist/MoreMenu/MoreMenu.d.ts +1 -7
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +0 -2
- package/dist/MultiSelector/MultiSelector.d.ts +25 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +31 -6
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -4
- package/dist/RadioList/RadioList.js +3 -3
- package/dist/SegmentedControl/SegmentedControl.d.ts +1 -1
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +51 -61
- package/dist/Selector/Selector.d.ts +22 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +31 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +2 -4
- package/dist/TabList/TabList.d.ts +3 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +55 -33
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +0 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +2 -4
- package/dist/Toolbar/Toolbar.d.ts +3 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +46 -6
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +16 -22
- package/dist/TreeList/TreeListItem.d.ts +3 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/astryx.css +3 -0
- package/dist/astryx.umd.js +47 -47
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +4 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useGridFocus.d.ts +56 -0
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +138 -24
- package/dist/hooks/useKeyboardHint.d.ts +82 -0
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -0
- package/dist/hooks/useKeyboardHint.js +220 -0
- package/dist/hooks/useTreeFocus.d.ts +27 -3
- package/dist/hooks/useTreeFocus.d.ts.map +1 -1
- package/dist/hooks/useTreeFocus.js +69 -6
- package/package.json +1 -1
- package/src/Breadcrumbs/BreadcrumbItem.tsx +5 -2
- package/src/Button/Button.tsx +3 -16
- package/src/Calendar/Calendar.tsx +52 -22
- package/src/Calendar/hooks/index.ts +0 -6
- package/src/Calendar/index.ts +0 -3
- package/src/CheckboxList/CheckboxList.tsx +3 -3
- package/src/Citation/Citation.doc.mjs +10 -0
- package/src/Citation/Citation.test.tsx +117 -0
- package/src/Citation/Citation.tsx +9 -1
- package/src/ContextMenu/ContextMenu.doc.mjs +0 -6
- package/src/ContextMenu/ContextMenu.test.tsx +4 -6
- package/src/ContextMenu/ContextMenu.tsx +7 -19
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -8
- package/src/DropdownMenu/DropdownMenu.test.tsx +0 -23
- package/src/DropdownMenu/DropdownMenu.tsx +4 -16
- package/src/Field/Field.test.tsx +2 -2
- package/src/Field/Field.tsx +5 -5
- package/src/Field/FieldLabel.tsx +5 -5
- package/src/InputGroup/InputGroup.tsx +3 -3
- package/src/Link/Link.tsx +2 -13
- package/src/MoreMenu/MoreMenu.doc.mjs +2 -17
- package/src/MoreMenu/MoreMenu.tsx +0 -9
- package/src/MultiSelector/MultiSelector.doc.mjs +25 -0
- package/src/MultiSelector/MultiSelector.test.tsx +150 -1
- package/src/MultiSelector/MultiSelector.tsx +55 -3
- package/src/ProgressBar/ProgressBar.tsx +2 -3
- package/src/RadioList/RadioList.tsx +3 -3
- package/src/SegmentedControl/SegmentedControl.tsx +51 -77
- package/src/Selector/Selector.doc.mjs +21 -0
- package/src/Selector/Selector.test.tsx +123 -1
- package/src/Selector/Selector.tsx +53 -3
- package/src/Switch/Switch.tsx +2 -16
- package/src/TabList/TabList.test.tsx +41 -0
- package/src/TabList/TabList.tsx +66 -39
- package/src/Table/tableContextMenu.tsx +1 -5
- package/src/TextArea/TextArea.tsx +3 -13
- package/src/Toolbar/Toolbar.test.tsx +64 -6
- package/src/Toolbar/Toolbar.tsx +55 -4
- package/src/TreeList/TreeList.tsx +17 -28
- package/src/TreeList/TreeListItem.tsx +3 -2
- package/src/VisuallyHidden/VisuallyHidden.doc.mjs +7 -7
- package/src/hooks/index.ts +12 -5
- package/src/hooks/useGridFocus.doc.mjs +40 -2
- package/src/hooks/useGridFocus.test.tsx +132 -0
- package/src/hooks/useGridFocus.ts +179 -23
- package/src/hooks/useKeyboardHint.doc.mjs +101 -0
- package/src/hooks/useKeyboardHint.test.tsx +70 -0
- package/src/hooks/useKeyboardHint.tsx +332 -0
- package/src/hooks/useTreeFocus.doc.mjs +15 -1
- package/src/hooks/useTreeFocus.ts +101 -6
- package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts +0 -57
- package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts.map +0 -1
- package/dist/Calendar/hooks/useCalendarRovingTabindex.js +0 -96
- package/src/Calendar/hooks/useCalendarRovingTabindex.ts +0 -118
package/dist/hooks/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type { UseListFocusOptions, UseListFocusReturn, ListFocusOrientation, } f
|
|
|
17
17
|
export { useTreeFocus } from './useTreeFocus';
|
|
18
18
|
export type { UseTreeFocusOptions, UseTreeFocusReturn } from './useTreeFocus';
|
|
19
19
|
export { useTypeahead } from './useTypeahead';
|
|
20
|
+
export { useKeyboardHint } from './useKeyboardHint';
|
|
21
|
+
export type { UseKeyboardHintOptions, UseKeyboardHintReturn, KeyboardHintOrientation, } from './useKeyboardHint';
|
|
20
22
|
export type { UseTypeaheadOptions, UseTypeaheadReturn } from './useTypeahead';
|
|
21
23
|
export { useMediaQuery } from './useMediaQuery';
|
|
22
24
|
export { useOverflow } from './useOverflow';
|
|
@@ -30,12 +32,12 @@ export { useStreamingText } from './useStreamingText';
|
|
|
30
32
|
export type { StreamingTextSpeed, UseStreamingTextOptions, } from './useStreamingText';
|
|
31
33
|
export { useImageMode } from './useImageMode';
|
|
32
34
|
export type { ImageSampleRegion, UseImageModeOptions } from './useImageMode';
|
|
33
|
-
export { useClickableContainer, INTERACTIVE_SELECTORS } from './useClickableContainer';
|
|
35
|
+
export { useClickableContainer, INTERACTIVE_SELECTORS, } from './useClickableContainer';
|
|
34
36
|
export type { UseClickableContainerOptions, ClickableContainerResult, } from './useClickableContainer';
|
|
35
37
|
export { useInputContainer } from './useInputContainer';
|
|
36
38
|
export type { UseInputContainerOptions } from './useInputContainer';
|
|
37
39
|
export { useInteractiveRole } from './useInteractiveRole';
|
|
38
40
|
export type { InteractiveRole, UseInteractiveRoleOptions, } from './useInteractiveRole';
|
|
39
41
|
export { useLongPress } from './useLongPress';
|
|
40
|
-
export type { UseLongPressOptions, UseLongPressHandlers
|
|
42
|
+
export type { UseLongPressOptions, UseLongPressHandlers } from './useLongPress';
|
|
41
43
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAC,UAAU,EAAE,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAElE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAC,UAAU,EAAE,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAElE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAClD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAC,mBAAmB,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAEzE,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAE7D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAC,oBAAoB,EAAC,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EACL,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,4BAA4B,EAC5B,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAC,wBAAwB,EAAC,MAAM,qBAAqB,CAAC;AAElE,OAAO,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AACxD,YAAY,EACV,eAAe,EACf,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,oBAAoB,EAAC,MAAM,gBAAgB,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { useGridFocus } from "./useGridFocus.js";
|
|
|
16
16
|
export { useListFocus } from "./useListFocus.js";
|
|
17
17
|
export { useTreeFocus } from "./useTreeFocus.js";
|
|
18
18
|
export { useTypeahead } from "./useTypeahead.js";
|
|
19
|
+
export { useKeyboardHint } from "./useKeyboardHint.js";
|
|
19
20
|
export { useMediaQuery } from "./useMediaQuery.js";
|
|
20
21
|
export { useOverflow } from "./useOverflow.js";
|
|
21
22
|
export { useScrollOverflow } from "./useScrollOverflow.js";
|
|
@@ -59,6 +59,32 @@ export interface UseGridFocusOptions {
|
|
|
59
59
|
* Callback for Page Down key (e.g., next month).
|
|
60
60
|
*/
|
|
61
61
|
onPageDown?: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the grid is in a right-to-left context. When true, ArrowLeft and
|
|
64
|
+
* ArrowRight are swapped so horizontal navigation follows visual direction.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
isRtl?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Roving-tabindex ownership. When true, the hook manages a single tab stop
|
|
70
|
+
* across the grid: exactly one focusable cell carries `tabindex="0"` and the
|
|
71
|
+
* rest `tabindex="-1"`. The tab stop is stamped on mount and repaired
|
|
72
|
+
* whenever cells mount/unmount or toggle focusable, and moves with arrow
|
|
73
|
+
* navigation. Attach the returned {@link UseGridFocusReturn.handleFocus} to
|
|
74
|
+
* the container's `onFocus` to keep the stop in sync after clicks or
|
|
75
|
+
* programmatic focus.
|
|
76
|
+
*
|
|
77
|
+
* The tab stop is stamped on the resolved focus target (see
|
|
78
|
+
* {@link UseGridFocusOptions.getFocusTarget}), not the cell wrapper, so it
|
|
79
|
+
* works when the focusable element is a descendant of the cell. An existing
|
|
80
|
+
* `tabindex="0"` on a focus target is honored, letting the caller seed which
|
|
81
|
+
* cell is initially tabbable.
|
|
82
|
+
*
|
|
83
|
+
* When false (the default), the hook only *moves* focus (`.focus()`) and
|
|
84
|
+
* never touches `tabindex` — the caller owns tab-stop management.
|
|
85
|
+
* @default false
|
|
86
|
+
*/
|
|
87
|
+
hasRovingTabIndex?: boolean;
|
|
62
88
|
}
|
|
63
89
|
/**
|
|
64
90
|
* Return type for useGridFocus hook
|
|
@@ -72,6 +98,12 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
|
|
|
72
98
|
* Key down handler to attach to the grid container.
|
|
73
99
|
*/
|
|
74
100
|
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Focus handler to attach to the container's `onFocus`. Keeps the roving tab
|
|
103
|
+
* stop in sync when `hasRovingTabIndex` is enabled; a no-op otherwise, so it
|
|
104
|
+
* is always safe to attach.
|
|
105
|
+
*/
|
|
106
|
+
handleFocus: (e: React.FocusEvent) => void;
|
|
75
107
|
/**
|
|
76
108
|
* Focus a specific cell by index.
|
|
77
109
|
*/
|
|
@@ -102,6 +134,12 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
|
|
|
102
134
|
* non-focusable cell (per `isCellFocusable`), it continues in the same
|
|
103
135
|
* direction to the next focusable cell.
|
|
104
136
|
*
|
|
137
|
+
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
138
|
+
* the caller. Opt into {@link UseGridFocusOptions.hasRovingTabIndex} for a hook
|
|
139
|
+
* that owns a single tab stop (roving tabindex) across the grid — stamping and
|
|
140
|
+
* repairing it as cells mount/unmount or toggle focusable, and moving it with
|
|
141
|
+
* arrow navigation.
|
|
142
|
+
*
|
|
105
143
|
* @example
|
|
106
144
|
* ```
|
|
107
145
|
* const {gridRef, handleKeyDown} = useGridFocus({
|
|
@@ -114,6 +152,24 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
|
|
|
114
152
|
* {cells.map(cell => <button role="gridcell">{cell}</button>)}
|
|
115
153
|
* </div>
|
|
116
154
|
* ```
|
|
155
|
+
*
|
|
156
|
+
* Roving-tabindex grid (e.g. a date picker where the cell is a wrapper around
|
|
157
|
+
* a focusable button):
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```
|
|
161
|
+
* const {gridRef, handleKeyDown, handleFocus} = useGridFocus({
|
|
162
|
+
* columns: 7,
|
|
163
|
+
* cellSelector: '[role="gridcell"]',
|
|
164
|
+
* isCellFocusable: cell => cell.querySelector('button:not([disabled])') != null,
|
|
165
|
+
* getFocusTarget: cell => cell.querySelector('button'),
|
|
166
|
+
* hasRovingTabIndex: true,
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* <div ref={gridRef} role="grid" onKeyDown={handleKeyDown} onFocus={handleFocus}>
|
|
170
|
+
* {cells}
|
|
171
|
+
* </div>
|
|
172
|
+
* ```
|
|
117
173
|
*/
|
|
118
174
|
export declare function useGridFocus<T extends HTMLElement = HTMLElement>(options: UseGridFocusOptions): UseGridFocusReturn<T>;
|
|
119
175
|
//# sourceMappingURL=useGridFocus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGridFocus.d.ts","sourceRoot":"","sources":["../../src/hooks/useGridFocus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useGridFocus.d.ts","sourceRoot":"","sources":["../../src/hooks/useGridFocus.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC;IAEjD;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,WAAW,GAAG,IAAI,CAAC;IAE3D;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE5D;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE3D;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IAExB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IACrE;;OAEG;IACH,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAEnC;;OAEG;IACH,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;IAEhD;;;;OAIG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAE3C;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,CAAC,CAAC,CAuYvB"}
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useGridFocus.ts
|
|
7
|
-
* @input Uses React useCallback, useRef
|
|
7
|
+
* @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
|
|
8
8
|
* @output Exports useGridFocus hook for grid keyboard navigation
|
|
9
9
|
* @position Core hook; used by Calendar for date grid navigation
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update:
|
|
12
12
|
* - /packages/core/src/hooks/index.ts
|
|
13
|
-
* - /packages/core/src/hooks/useGridFocus.test.
|
|
13
|
+
* - /packages/core/src/hooks/useGridFocus.test.tsx
|
|
14
14
|
*/
|
|
15
15
|
import { useCallback, useRef } from 'react';
|
|
16
|
+
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Configuration for grid focus behavior
|
|
@@ -39,6 +40,12 @@ import { useCallback, useRef } from 'react';
|
|
|
39
40
|
* non-focusable cell (per `isCellFocusable`), it continues in the same
|
|
40
41
|
* direction to the next focusable cell.
|
|
41
42
|
*
|
|
43
|
+
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
44
|
+
* the caller. Opt into {@link UseGridFocusOptions.hasRovingTabIndex} for a hook
|
|
45
|
+
* that owns a single tab stop (roving tabindex) across the grid — stamping and
|
|
46
|
+
* repairing it as cells mount/unmount or toggle focusable, and moving it with
|
|
47
|
+
* arrow navigation.
|
|
48
|
+
*
|
|
42
49
|
* @example
|
|
43
50
|
* ```
|
|
44
51
|
* const {gridRef, handleKeyDown} = useGridFocus({
|
|
@@ -51,6 +58,24 @@ import { useCallback, useRef } from 'react';
|
|
|
51
58
|
* {cells.map(cell => <button role="gridcell">{cell}</button>)}
|
|
52
59
|
* </div>
|
|
53
60
|
* ```
|
|
61
|
+
*
|
|
62
|
+
* Roving-tabindex grid (e.g. a date picker where the cell is a wrapper around
|
|
63
|
+
* a focusable button):
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```
|
|
67
|
+
* const {gridRef, handleKeyDown, handleFocus} = useGridFocus({
|
|
68
|
+
* columns: 7,
|
|
69
|
+
* cellSelector: '[role="gridcell"]',
|
|
70
|
+
* isCellFocusable: cell => cell.querySelector('button:not([disabled])') != null,
|
|
71
|
+
* getFocusTarget: cell => cell.querySelector('button'),
|
|
72
|
+
* hasRovingTabIndex: true,
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* <div ref={gridRef} role="grid" onKeyDown={handleKeyDown} onFocus={handleFocus}>
|
|
76
|
+
* {cells}
|
|
77
|
+
* </div>
|
|
78
|
+
* ```
|
|
54
79
|
*/
|
|
55
80
|
export function useGridFocus(options) {
|
|
56
81
|
const {
|
|
@@ -61,7 +86,9 @@ export function useGridFocus(options) {
|
|
|
61
86
|
onNavigateBefore,
|
|
62
87
|
onNavigateAfter,
|
|
63
88
|
onPageUp,
|
|
64
|
-
onPageDown
|
|
89
|
+
onPageDown,
|
|
90
|
+
isRtl = false,
|
|
91
|
+
hasRovingTabIndex = false
|
|
65
92
|
} = options;
|
|
66
93
|
const gridRef = useRef(null);
|
|
67
94
|
|
|
@@ -96,17 +123,92 @@ export function useGridFocus(options) {
|
|
|
96
123
|
return getFocusTarget ? getFocusTarget(cell) : cell;
|
|
97
124
|
}, [getFocusTarget]);
|
|
98
125
|
|
|
126
|
+
// --- Roving tabindex ownership (opt-in via `hasRovingTabIndex`) -------------
|
|
127
|
+
|
|
99
128
|
/**
|
|
100
|
-
*
|
|
129
|
+
* Set `tabindex` on an element, but only when it differs (avoids redundant
|
|
130
|
+
* DOM writes). Uses setAttribute so the value reflects even for elements
|
|
131
|
+
* (like `<button>`) whose default tabIndex is already 0.
|
|
101
132
|
*/
|
|
102
|
-
const
|
|
133
|
+
const setTabIndex = useCallback((el, value) => {
|
|
134
|
+
if (el.getAttribute('tabindex') !== String(value)) {
|
|
135
|
+
el.setAttribute('tabindex', String(value));
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The resolved focus targets of all focusable cells, in DOM order. These are
|
|
141
|
+
* the elements that carry the roving tab stop (a descendant of each cell when
|
|
142
|
+
* `getFocusTarget` is provided, otherwise the cell itself).
|
|
143
|
+
*/
|
|
144
|
+
const getFocusTargets = useCallback(() => {
|
|
145
|
+
return getCells().filter(cell => cellFocusable(cell)).map(cell => resolveFocusTarget(cell)).filter(el => el !== null);
|
|
146
|
+
}, [getCells, cellFocusable, resolveFocusTarget]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stamp the roving tab stop: exactly one focus target is tabbable (0), the
|
|
150
|
+
* rest are -1. Prefer keeping the currently-tabbable target if it is still
|
|
151
|
+
* focusable; otherwise promote the first focusable one (tab-stop repair).
|
|
152
|
+
*/
|
|
153
|
+
const syncTabStops = useCallback(() => {
|
|
154
|
+
const targets = getFocusTargets();
|
|
155
|
+
if (targets.length === 0) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const current = targets.find(el => el.getAttribute('tabindex') === '0');
|
|
159
|
+
const tabbable = current ?? targets[0];
|
|
160
|
+
for (const el of targets) {
|
|
161
|
+
setTabIndex(el, el === tabbable ? 0 : -1);
|
|
162
|
+
}
|
|
163
|
+
}, [getFocusTargets, setTabIndex]);
|
|
164
|
+
|
|
165
|
+
// Keep the tab stop valid across renders (cells added/removed, focusability
|
|
166
|
+
// toggled). Runs after every commit but only when roving tabindex is on.
|
|
167
|
+
useIsomorphicLayoutEffect(() => {
|
|
168
|
+
if (hasRovingTabIndex) {
|
|
169
|
+
syncTabStops();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Move the roving tab stop so `target` becomes the sole tabbable focus
|
|
175
|
+
* target, then focus it. No-op on tab stops unless roving tabindex is on.
|
|
176
|
+
*/
|
|
177
|
+
const focusTarget = useCallback(target => {
|
|
178
|
+
if (!target) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (hasRovingTabIndex) {
|
|
182
|
+
for (const el of getFocusTargets()) {
|
|
183
|
+
setTabIndex(el, el === target ? 0 : -1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
target.focus();
|
|
187
|
+
}, [hasRovingTabIndex, getFocusTargets, setTabIndex]);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Focus a cell by resolving and focusing its target, moving the roving tab
|
|
191
|
+
* stop when enabled.
|
|
192
|
+
*/
|
|
193
|
+
const focusCellWithStop = useCallback(cell => {
|
|
103
194
|
const target = resolveFocusTarget(cell);
|
|
104
|
-
if (target) {
|
|
105
|
-
|
|
106
|
-
|
|
195
|
+
if (!target) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
focusTarget(target);
|
|
199
|
+
return true;
|
|
200
|
+
}, [resolveFocusTarget, focusTarget]);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Keep the roving stop pointing at whatever ended up focused (e.g. a click
|
|
204
|
+
* or programmatic focus) so the next Tab behaves correctly. No-op unless
|
|
205
|
+
* roving tabindex is enabled.
|
|
206
|
+
*/
|
|
207
|
+
const handleFocus = useCallback(() => {
|
|
208
|
+
if (hasRovingTabIndex) {
|
|
209
|
+
syncTabStops();
|
|
107
210
|
}
|
|
108
|
-
|
|
109
|
-
}, [resolveFocusTarget]);
|
|
211
|
+
}, [hasRovingTabIndex, syncTabStops]);
|
|
110
212
|
|
|
111
213
|
/**
|
|
112
214
|
* Get the currently focused cell index within the full cell set.
|
|
@@ -150,9 +252,9 @@ export function useGridFocus(options) {
|
|
|
150
252
|
target = findFocusableInDirection(cells, clampedIndex, -1);
|
|
151
253
|
}
|
|
152
254
|
if (target !== -1) {
|
|
153
|
-
|
|
255
|
+
focusCellWithStop(cells[target]);
|
|
154
256
|
}
|
|
155
|
-
}, [getCells, findFocusableInDirection,
|
|
257
|
+
}, [getCells, findFocusableInDirection, focusCellWithStop]);
|
|
156
258
|
|
|
157
259
|
/**
|
|
158
260
|
* Focus the first focusable cell.
|
|
@@ -161,9 +263,9 @@ export function useGridFocus(options) {
|
|
|
161
263
|
const cells = getCells();
|
|
162
264
|
const index = findFocusableInDirection(cells, 0, 1);
|
|
163
265
|
if (index !== -1) {
|
|
164
|
-
|
|
266
|
+
focusCellWithStop(cells[index]);
|
|
165
267
|
}
|
|
166
|
-
}, [getCells, findFocusableInDirection,
|
|
268
|
+
}, [getCells, findFocusableInDirection, focusCellWithStop]);
|
|
167
269
|
|
|
168
270
|
/**
|
|
169
271
|
* Focus the last focusable cell.
|
|
@@ -172,9 +274,9 @@ export function useGridFocus(options) {
|
|
|
172
274
|
const cells = getCells();
|
|
173
275
|
const index = findFocusableInDirection(cells, cells.length - 1, -1);
|
|
174
276
|
if (index !== -1) {
|
|
175
|
-
|
|
277
|
+
focusCellWithStop(cells[index]);
|
|
176
278
|
}
|
|
177
|
-
}, [getCells, findFocusableInDirection,
|
|
279
|
+
}, [getCells, findFocusableInDirection, focusCellWithStop]);
|
|
178
280
|
|
|
179
281
|
/**
|
|
180
282
|
* Handle keyboard navigation.
|
|
@@ -192,13 +294,24 @@ export function useGridFocus(options) {
|
|
|
192
294
|
const currentCol = currentIndex % columns;
|
|
193
295
|
const totalRows = Math.ceil(cells.length / columns);
|
|
194
296
|
let handled = true;
|
|
195
|
-
|
|
297
|
+
|
|
298
|
+
// In RTL, ArrowLeft/ArrowRight are swapped so horizontal navigation
|
|
299
|
+
// follows visual direction. Vertical keys (Up/Down) are unaffected.
|
|
300
|
+
let key = e.key;
|
|
301
|
+
if (isRtl) {
|
|
302
|
+
if (key === 'ArrowLeft') {
|
|
303
|
+
key = 'ArrowRight';
|
|
304
|
+
} else if (key === 'ArrowRight') {
|
|
305
|
+
key = 'ArrowLeft';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
switch (key) {
|
|
196
309
|
case 'ArrowRight':
|
|
197
310
|
{
|
|
198
311
|
// Move right, skipping non-focusable cells in the same direction.
|
|
199
312
|
const target = findFocusableInDirection(cells, currentIndex + 1, 1);
|
|
200
313
|
if (target !== -1) {
|
|
201
|
-
|
|
314
|
+
focusCellWithStop(cells[target]);
|
|
202
315
|
} else {
|
|
203
316
|
onNavigateAfter?.((currentCol + 1) % columns, 1);
|
|
204
317
|
}
|
|
@@ -208,7 +321,7 @@ export function useGridFocus(options) {
|
|
|
208
321
|
{
|
|
209
322
|
const target = findFocusableInDirection(cells, currentIndex - 1, -1);
|
|
210
323
|
if (target !== -1) {
|
|
211
|
-
|
|
324
|
+
focusCellWithStop(cells[target]);
|
|
212
325
|
} else {
|
|
213
326
|
onNavigateBefore?.(currentCol === 0 ? columns - 1 : currentCol - 1, 1);
|
|
214
327
|
}
|
|
@@ -221,7 +334,7 @@ export function useGridFocus(options) {
|
|
|
221
334
|
// (by whole rows) to the next focusable cell in that column.
|
|
222
335
|
const target = findFocusableInDirection(cells, currentIndex + columns, columns);
|
|
223
336
|
if (target !== -1) {
|
|
224
|
-
|
|
337
|
+
focusCellWithStop(cells[target]);
|
|
225
338
|
} else {
|
|
226
339
|
onNavigateAfter?.(currentCol, columns);
|
|
227
340
|
}
|
|
@@ -235,7 +348,7 @@ export function useGridFocus(options) {
|
|
|
235
348
|
if (currentRow > 0) {
|
|
236
349
|
const target = findFocusableInDirection(cells, currentIndex - columns, -columns);
|
|
237
350
|
if (target !== -1) {
|
|
238
|
-
|
|
351
|
+
focusCellWithStop(cells[target]);
|
|
239
352
|
} else {
|
|
240
353
|
onNavigateBefore?.(currentCol, columns);
|
|
241
354
|
}
|
|
@@ -254,7 +367,7 @@ export function useGridFocus(options) {
|
|
|
254
367
|
const rowEnd = Math.min(rowStart + columns - 1, cells.length - 1);
|
|
255
368
|
const target = findFocusableInDirection(cells, rowStart, 1);
|
|
256
369
|
if (target !== -1 && target <= rowEnd) {
|
|
257
|
-
|
|
370
|
+
focusCellWithStop(cells[target]);
|
|
258
371
|
}
|
|
259
372
|
}
|
|
260
373
|
break;
|
|
@@ -268,7 +381,7 @@ export function useGridFocus(options) {
|
|
|
268
381
|
const rowEnd = Math.min(rowStart + columns - 1, cells.length - 1);
|
|
269
382
|
const target = findFocusableInDirection(cells, rowEnd, -1);
|
|
270
383
|
if (target !== -1 && target >= rowStart) {
|
|
271
|
-
|
|
384
|
+
focusCellWithStop(cells[target]);
|
|
272
385
|
}
|
|
273
386
|
}
|
|
274
387
|
break;
|
|
@@ -285,10 +398,11 @@ export function useGridFocus(options) {
|
|
|
285
398
|
e.preventDefault();
|
|
286
399
|
e.stopPropagation();
|
|
287
400
|
}
|
|
288
|
-
}, [columns, findFocusableInDirection,
|
|
401
|
+
}, [columns, findFocusableInDirection, focusCellWithStop, focusFirst, focusLast, getCells, getCurrentIndex, isRtl, onNavigateAfter, onNavigateBefore, onPageDown, onPageUp]);
|
|
289
402
|
return {
|
|
290
403
|
gridRef,
|
|
291
404
|
handleKeyDown,
|
|
405
|
+
handleFocus,
|
|
292
406
|
focusCell,
|
|
293
407
|
focusFirst,
|
|
294
408
|
focusLast
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useKeyboardHint.tsx
|
|
3
|
+
* @input Uses React, StyleX, Kbd, useLayer
|
|
4
|
+
* @output Exports useKeyboardHint hook — ephemeral arrow-key navigation hint
|
|
5
|
+
* @position Core hook; shows sighted keyboard users how to navigate composite
|
|
6
|
+
* widgets that use roving tabindex (single Tab stop, arrows inside)
|
|
7
|
+
*
|
|
8
|
+
* SYNC: When modified, update:
|
|
9
|
+
* - /packages/core/src/hooks/index.ts
|
|
10
|
+
* - /packages/core/src/hooks/useKeyboardHint.doc.mjs
|
|
11
|
+
* - /apps/storybook/stories/useKeyboardHint.stories.tsx
|
|
12
|
+
* - /packages/cli/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx
|
|
13
|
+
*/
|
|
14
|
+
import React, { type ReactNode } from 'react';
|
|
15
|
+
export type KeyboardHintOrientation = 'horizontal' | 'vertical' | 'both';
|
|
16
|
+
export interface UseKeyboardHintOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Orientation of the arrow-key navigation. Controls which arrow icons are
|
|
19
|
+
* shown in the hint badge.
|
|
20
|
+
* - `'horizontal'` → ← →
|
|
21
|
+
* - `'vertical'` → ↑ ↓
|
|
22
|
+
* - `'both'` → ← → ↑ ↓
|
|
23
|
+
* @default 'horizontal'
|
|
24
|
+
*/
|
|
25
|
+
orientation?: KeyboardHintOrientation;
|
|
26
|
+
/**
|
|
27
|
+
* Milliseconds before the hint auto-dismisses after appearing.
|
|
28
|
+
* @default 3000
|
|
29
|
+
*/
|
|
30
|
+
dismissAfterMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the hint is enabled. Set to false to disable for a specific
|
|
33
|
+
* instance (e.g. when the widget is read-only or the user has dismissed
|
|
34
|
+
* globally).
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
isEnabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface UseKeyboardHintReturn {
|
|
40
|
+
/**
|
|
41
|
+
* The popover hint element to render inside your component tree (portals to
|
|
42
|
+
* top layer via `popover="manual"`). Render unconditionally — it manages its
|
|
43
|
+
* own visibility.
|
|
44
|
+
*/
|
|
45
|
+
hintElement: ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Attach to the composite container's `onFocus`. Shows the hint on the first
|
|
48
|
+
* keyboard-focus (`:focus-visible`) entry from outside.
|
|
49
|
+
*/
|
|
50
|
+
onFocus: (e: React.FocusEvent) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Attach to the composite container's `onBlur`. Hides the hint when focus
|
|
53
|
+
* leaves the composite entirely.
|
|
54
|
+
*/
|
|
55
|
+
onBlur: (e: React.FocusEvent) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Attach to the composite container's `onKeyDown`. Dismisses the hint on the
|
|
58
|
+
* first arrow press (the user discovered the interaction).
|
|
59
|
+
*/
|
|
60
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Shows an ephemeral visual hint ("← → to navigate") anchored to the focused
|
|
64
|
+
* item when a composite widget first receives keyboard focus. Teaches sighted
|
|
65
|
+
* keyboard users that arrows navigate within the group.
|
|
66
|
+
*
|
|
67
|
+
* The hint renders in the top layer (popover="manual") and is CSS-anchor-
|
|
68
|
+
* positioned to the currently focused element, so it is never clipped by
|
|
69
|
+
* overflow containers. It auto-dismisses on first arrow press, timeout, or
|
|
70
|
+
* blur, and does not re-show for that instance.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```
|
|
74
|
+
* const hint = useKeyboardHint({orientation: 'horizontal'});
|
|
75
|
+
* <div role="toolbar" onFocus={hint.onFocus} onBlur={hint.onBlur} onKeyDown={hint.onKeyDown}>
|
|
76
|
+
* {children}
|
|
77
|
+
* {hint.hintElement}
|
|
78
|
+
* </div>
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function useKeyboardHint(options?: UseKeyboardHintOptions): UseKeyboardHintReturn;
|
|
82
|
+
//# sourceMappingURL=useKeyboardHint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useKeyboardHint.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHint.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgB5E,MAAM,MAAM,uBAAuB,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,WAAW,EAAE,SAAS,CAAC;IAEvB;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEvC;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEtC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;CAC7C;AAkED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CA6JvB"}
|