@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.
Files changed (132) hide show
  1. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
  2. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  3. package/dist/Breadcrumbs/BreadcrumbItem.js +4 -1
  4. package/dist/Button/Button.d.ts.map +1 -1
  5. package/dist/Button/Button.js +2 -4
  6. package/dist/Calendar/Calendar.d.ts.map +1 -1
  7. package/dist/Calendar/Calendar.js +33 -14
  8. package/dist/Calendar/hooks/index.d.ts +0 -2
  9. package/dist/Calendar/hooks/index.d.ts.map +1 -1
  10. package/dist/Calendar/hooks/index.js +1 -2
  11. package/dist/Calendar/index.d.ts +2 -2
  12. package/dist/Calendar/index.d.ts.map +1 -1
  13. package/dist/Calendar/index.js +1 -1
  14. package/dist/CheckboxList/CheckboxList.js +3 -3
  15. package/dist/Citation/Citation.d.ts.map +1 -1
  16. package/dist/Citation/Citation.js +3 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts +1 -7
  18. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  19. package/dist/ContextMenu/ContextMenu.js +7 -13
  20. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -8
  21. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.js +4 -9
  23. package/dist/Field/Field.d.ts +4 -4
  24. package/dist/Field/Field.d.ts.map +1 -1
  25. package/dist/Field/Field.js +2 -2
  26. package/dist/Field/FieldLabel.d.ts +4 -4
  27. package/dist/Field/FieldLabel.d.ts.map +1 -1
  28. package/dist/Field/FieldLabel.js +2 -2
  29. package/dist/InputGroup/InputGroup.js +3 -3
  30. package/dist/Link/Link.d.ts.map +1 -1
  31. package/dist/Link/Link.js +2 -4
  32. package/dist/MoreMenu/MoreMenu.d.ts +1 -7
  33. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  34. package/dist/MoreMenu/MoreMenu.js +0 -2
  35. package/dist/MultiSelector/MultiSelector.d.ts +25 -2
  36. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  37. package/dist/MultiSelector/MultiSelector.js +31 -6
  38. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  39. package/dist/ProgressBar/ProgressBar.js +2 -4
  40. package/dist/RadioList/RadioList.js +3 -3
  41. package/dist/SegmentedControl/SegmentedControl.d.ts +1 -1
  42. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  43. package/dist/SegmentedControl/SegmentedControl.js +51 -61
  44. package/dist/Selector/Selector.d.ts +22 -1
  45. package/dist/Selector/Selector.d.ts.map +1 -1
  46. package/dist/Selector/Selector.js +31 -6
  47. package/dist/Switch/Switch.d.ts.map +1 -1
  48. package/dist/Switch/Switch.js +2 -4
  49. package/dist/TabList/TabList.d.ts +3 -2
  50. package/dist/TabList/TabList.d.ts.map +1 -1
  51. package/dist/TabList/TabList.js +55 -33
  52. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  53. package/dist/Table/tableContextMenu.js +0 -3
  54. package/dist/TextArea/TextArea.d.ts.map +1 -1
  55. package/dist/TextArea/TextArea.js +2 -4
  56. package/dist/Toolbar/Toolbar.d.ts +3 -3
  57. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  58. package/dist/Toolbar/Toolbar.js +46 -6
  59. package/dist/TreeList/TreeList.d.ts.map +1 -1
  60. package/dist/TreeList/TreeList.js +16 -22
  61. package/dist/TreeList/TreeListItem.d.ts +3 -2
  62. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  63. package/dist/astryx.css +3 -0
  64. package/dist/astryx.umd.js +47 -47
  65. package/dist/astryx.umd.js.map +4 -4
  66. package/dist/hooks/index.d.ts +4 -2
  67. package/dist/hooks/index.d.ts.map +1 -1
  68. package/dist/hooks/index.js +1 -0
  69. package/dist/hooks/useGridFocus.d.ts +56 -0
  70. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  71. package/dist/hooks/useGridFocus.js +138 -24
  72. package/dist/hooks/useKeyboardHint.d.ts +82 -0
  73. package/dist/hooks/useKeyboardHint.d.ts.map +1 -0
  74. package/dist/hooks/useKeyboardHint.js +220 -0
  75. package/dist/hooks/useTreeFocus.d.ts +27 -3
  76. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  77. package/dist/hooks/useTreeFocus.js +69 -6
  78. package/package.json +1 -1
  79. package/src/Breadcrumbs/BreadcrumbItem.tsx +5 -2
  80. package/src/Button/Button.tsx +3 -16
  81. package/src/Calendar/Calendar.tsx +52 -22
  82. package/src/Calendar/hooks/index.ts +0 -6
  83. package/src/Calendar/index.ts +0 -3
  84. package/src/CheckboxList/CheckboxList.tsx +3 -3
  85. package/src/Citation/Citation.doc.mjs +10 -0
  86. package/src/Citation/Citation.test.tsx +117 -0
  87. package/src/Citation/Citation.tsx +9 -1
  88. package/src/ContextMenu/ContextMenu.doc.mjs +0 -6
  89. package/src/ContextMenu/ContextMenu.test.tsx +4 -6
  90. package/src/ContextMenu/ContextMenu.tsx +7 -19
  91. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -8
  92. package/src/DropdownMenu/DropdownMenu.test.tsx +0 -23
  93. package/src/DropdownMenu/DropdownMenu.tsx +4 -16
  94. package/src/Field/Field.test.tsx +2 -2
  95. package/src/Field/Field.tsx +5 -5
  96. package/src/Field/FieldLabel.tsx +5 -5
  97. package/src/InputGroup/InputGroup.tsx +3 -3
  98. package/src/Link/Link.tsx +2 -13
  99. package/src/MoreMenu/MoreMenu.doc.mjs +2 -17
  100. package/src/MoreMenu/MoreMenu.tsx +0 -9
  101. package/src/MultiSelector/MultiSelector.doc.mjs +25 -0
  102. package/src/MultiSelector/MultiSelector.test.tsx +150 -1
  103. package/src/MultiSelector/MultiSelector.tsx +55 -3
  104. package/src/ProgressBar/ProgressBar.tsx +2 -3
  105. package/src/RadioList/RadioList.tsx +3 -3
  106. package/src/SegmentedControl/SegmentedControl.tsx +51 -77
  107. package/src/Selector/Selector.doc.mjs +21 -0
  108. package/src/Selector/Selector.test.tsx +123 -1
  109. package/src/Selector/Selector.tsx +53 -3
  110. package/src/Switch/Switch.tsx +2 -16
  111. package/src/TabList/TabList.test.tsx +41 -0
  112. package/src/TabList/TabList.tsx +66 -39
  113. package/src/Table/tableContextMenu.tsx +1 -5
  114. package/src/TextArea/TextArea.tsx +3 -13
  115. package/src/Toolbar/Toolbar.test.tsx +64 -6
  116. package/src/Toolbar/Toolbar.tsx +55 -4
  117. package/src/TreeList/TreeList.tsx +17 -28
  118. package/src/TreeList/TreeListItem.tsx +3 -2
  119. package/src/VisuallyHidden/VisuallyHidden.doc.mjs +7 -7
  120. package/src/hooks/index.ts +12 -5
  121. package/src/hooks/useGridFocus.doc.mjs +40 -2
  122. package/src/hooks/useGridFocus.test.tsx +132 -0
  123. package/src/hooks/useGridFocus.ts +179 -23
  124. package/src/hooks/useKeyboardHint.doc.mjs +101 -0
  125. package/src/hooks/useKeyboardHint.test.tsx +70 -0
  126. package/src/hooks/useKeyboardHint.tsx +332 -0
  127. package/src/hooks/useTreeFocus.doc.mjs +15 -1
  128. package/src/hooks/useTreeFocus.ts +101 -6
  129. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts +0 -57
  130. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts.map +0 -1
  131. package/dist/Calendar/hooks/useCalendarRovingTabindex.js +0 -96
  132. package/src/Calendar/hooks/useCalendarRovingTabindex.ts +0 -118
@@ -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, } from './useLongPress';
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;AAC5C,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,EAAC,qBAAqB,EAAE,qBAAqB,EAAC,MAAM,yBAAyB,CAAC;AACrF,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,EACV,mBAAmB,EACnB,oBAAoB,GACrB,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"}
@@ -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":"AAiBA;;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;CACzB;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;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,CAAC,CAAC,CAuSvB"}
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.ts
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
- * Focus a cell (resolving to its focus target).
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 focusCellElement = useCallback(cell => {
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
- target.focus();
106
- return true;
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
- return false;
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
- focusCellElement(cells[target]);
255
+ focusCellWithStop(cells[target]);
154
256
  }
155
- }, [getCells, findFocusableInDirection, focusCellElement]);
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
- focusCellElement(cells[index]);
266
+ focusCellWithStop(cells[index]);
165
267
  }
166
- }, [getCells, findFocusableInDirection, focusCellElement]);
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
- focusCellElement(cells[index]);
277
+ focusCellWithStop(cells[index]);
176
278
  }
177
- }, [getCells, findFocusableInDirection, focusCellElement]);
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
- switch (e.key) {
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
- focusCellElement(cells[target]);
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
- focusCellElement(cells[target]);
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
- focusCellElement(cells[target]);
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
- focusCellElement(cells[target]);
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
- focusCellElement(cells[target]);
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
- focusCellElement(cells[target]);
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, focusCellElement, focusFirst, focusLast, getCells, getCurrentIndex, onNavigateAfter, onNavigateBefore, onPageDown, onPageUp]);
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"}