@proyecto-viviana/solidaria 0.2.4 → 0.2.8

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actiongroup/createActionGroup.d.ts +29 -0
  3. package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
  4. package/dist/actiongroup/index.d.ts +2 -0
  5. package/dist/actiongroup/index.d.ts.map +1 -0
  6. package/dist/autocomplete/createAutocomplete.d.ts +6 -2
  7. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  8. package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
  9. package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
  10. package/dist/button/createToggleButtonGroup.d.ts +32 -0
  11. package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
  12. package/dist/button/index.d.ts +2 -0
  13. package/dist/button/index.d.ts.map +1 -1
  14. package/dist/calendar/createCalendarCell.d.ts +2 -0
  15. package/dist/calendar/createCalendarCell.d.ts.map +1 -1
  16. package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
  17. package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
  18. package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
  19. package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
  20. package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
  21. package/dist/collections/index.d.ts +56 -0
  22. package/dist/collections/index.d.ts.map +1 -0
  23. package/dist/color/createColorArea.d.ts.map +1 -1
  24. package/dist/color/createColorSlider.d.ts.map +1 -1
  25. package/dist/color/createColorWheel.d.ts.map +1 -1
  26. package/dist/combobox/createComboBox.d.ts +6 -0
  27. package/dist/combobox/createComboBox.d.ts.map +1 -1
  28. package/dist/datepicker/createDatePicker.d.ts +6 -0
  29. package/dist/datepicker/createDatePicker.d.ts.map +1 -1
  30. package/dist/datepicker/createDateRangePicker.d.ts +40 -0
  31. package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
  32. package/dist/datepicker/createDateSegment.d.ts +1 -1
  33. package/dist/datepicker/createDateSegment.d.ts.map +1 -1
  34. package/dist/datepicker/createTimeSegment.d.ts +29 -0
  35. package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
  36. package/dist/datepicker/index.d.ts +2 -0
  37. package/dist/datepicker/index.d.ts.map +1 -1
  38. package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
  39. package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
  40. package/dist/dnd/createDrag.d.ts.map +1 -1
  41. package/dist/dnd/createDraggableCollection.d.ts +4 -0
  42. package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
  43. package/dist/dnd/createDraggableItem.d.ts.map +1 -1
  44. package/dist/dnd/createDrop.d.ts.map +1 -1
  45. package/dist/dnd/createDroppableCollection.d.ts +32 -1
  46. package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
  47. package/dist/dnd/createDroppableItem.d.ts.map +1 -1
  48. package/dist/dnd/index.d.ts +1 -1
  49. package/dist/dnd/index.d.ts.map +1 -1
  50. package/dist/grid/createGrid.d.ts.map +1 -1
  51. package/dist/gridlist/createGridList.d.ts.map +1 -1
  52. package/dist/index.d.ts +6 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4659 -3452
  55. package/dist/index.js.map +1 -7
  56. package/dist/index.ssr.js +4659 -3452
  57. package/dist/index.ssr.js.map +1 -7
  58. package/dist/interactions/createFocus.d.ts.map +1 -1
  59. package/dist/interactions/createFocusWithin.d.ts.map +1 -1
  60. package/dist/link/createLink.d.ts +10 -0
  61. package/dist/link/createLink.d.ts.map +1 -1
  62. package/dist/listbox/createListBox.d.ts +1 -0
  63. package/dist/listbox/createListBox.d.ts.map +1 -1
  64. package/dist/listbox/createOption.d.ts.map +1 -1
  65. package/dist/menu/createMenu.d.ts +1 -0
  66. package/dist/menu/createMenu.d.ts.map +1 -1
  67. package/dist/meter/createMeter.d.ts.map +1 -1
  68. package/dist/numberfield/createNumberField.d.ts +18 -0
  69. package/dist/numberfield/createNumberField.d.ts.map +1 -1
  70. package/dist/overlays/createModal.d.ts +16 -0
  71. package/dist/overlays/createModal.d.ts.map +1 -1
  72. package/dist/overlays/createOverlay.d.ts.map +1 -1
  73. package/dist/overlays/index.d.ts +1 -1
  74. package/dist/overlays/index.d.ts.map +1 -1
  75. package/dist/popover/createOverlayPosition.d.ts.map +1 -1
  76. package/dist/popover/createPopover.d.ts.map +1 -1
  77. package/dist/progress/createProgressBar.d.ts.map +1 -1
  78. package/dist/radio/createRadioGroup.d.ts +2 -2
  79. package/dist/radio/createRadioGroup.d.ts.map +1 -1
  80. package/dist/searchfield/createSearchField.d.ts.map +1 -1
  81. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  82. package/dist/select/createSelect.d.ts.map +1 -1
  83. package/dist/slider/createSlider.d.ts.map +1 -1
  84. package/dist/table/createTable.d.ts.map +1 -1
  85. package/dist/tabs/createTabs.d.ts +1 -1
  86. package/dist/tabs/createTabs.d.ts.map +1 -1
  87. package/dist/tag/createTag.d.ts.map +1 -1
  88. package/dist/tag/createTagGroup.d.ts.map +1 -1
  89. package/dist/toast/createToast.d.ts +4 -0
  90. package/dist/toast/createToast.d.ts.map +1 -1
  91. package/dist/toast/createToastRegion.d.ts.map +1 -1
  92. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  93. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  94. package/dist/tree/createTree.d.ts.map +1 -1
  95. package/dist/tree/createTreeItem.d.ts.map +1 -1
  96. package/dist/tree/types.d.ts +4 -0
  97. package/dist/tree/types.d.ts.map +1 -1
  98. package/dist/utils/env.d.ts +1 -1
  99. package/dist/utils/env.d.ts.map +1 -1
  100. package/dist/utils/platform.d.ts.map +1 -1
  101. package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
  102. package/package.json +8 -6
  103. package/src/actiongroup/createActionGroup.ts +324 -0
  104. package/src/actiongroup/index.ts +8 -0
  105. package/src/autocomplete/createAutocomplete.ts +32 -9
  106. package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
  107. package/src/button/createButton.ts +1 -1
  108. package/src/button/createToggleButtonGroup.ts +128 -0
  109. package/src/button/index.ts +9 -0
  110. package/src/calendar/createCalendarCell.ts +6 -4
  111. package/src/calendar/createCalendarGrid.ts +27 -18
  112. package/src/calendar/createRangeCalendarCell.ts +26 -9
  113. package/src/checkbox/createCheckboxGroup.ts +21 -4
  114. package/src/collections/index.ts +242 -0
  115. package/src/color/createColorArea.ts +380 -314
  116. package/src/color/createColorField.ts +137 -137
  117. package/src/color/createColorSlider.ts +286 -197
  118. package/src/color/createColorSwatch.ts +40 -40
  119. package/src/color/createColorWheel.ts +218 -208
  120. package/src/color/index.ts +24 -24
  121. package/src/color/types.ts +116 -116
  122. package/src/combobox/createComboBox.ts +670 -647
  123. package/src/combobox/index.ts +6 -6
  124. package/src/datepicker/createDatePicker.ts +54 -16
  125. package/src/datepicker/createDateRangePicker.ts +246 -0
  126. package/src/datepicker/createDateSegment.ts +185 -31
  127. package/src/datepicker/createTimeSegment.ts +370 -0
  128. package/src/datepicker/index.ts +14 -0
  129. package/src/dialog/createDialog.ts +120 -120
  130. package/src/dialog/index.ts +2 -2
  131. package/src/dialog/types.ts +19 -19
  132. package/src/disclosure/createDisclosureGroup.ts +5 -2
  133. package/src/dnd/createDrag.ts +224 -209
  134. package/src/dnd/createDraggableCollection.ts +96 -63
  135. package/src/dnd/createDraggableItem.ts +259 -243
  136. package/src/dnd/createDrop.ts +322 -321
  137. package/src/dnd/createDroppableCollection.ts +682 -293
  138. package/src/dnd/createDroppableItem.ts +215 -213
  139. package/src/dnd/index.ts +55 -47
  140. package/src/dnd/types.ts +89 -89
  141. package/src/dnd/utils.ts +294 -294
  142. package/src/focus/createAutoFocus.ts +321 -321
  143. package/src/focus/createFocusRestore.ts +313 -313
  144. package/src/focus/createVirtualFocus.ts +396 -396
  145. package/src/form/createFormValidation.ts +224 -224
  146. package/src/form/index.ts +11 -11
  147. package/src/grid/createGrid.ts +3 -1
  148. package/src/gridlist/createGridList.ts +16 -0
  149. package/src/gridlist/createGridListItem.ts +1 -1
  150. package/src/i18n/NumberFormatter.ts +266 -266
  151. package/src/i18n/createCollator.ts +79 -79
  152. package/src/i18n/createDateFormatter.ts +83 -83
  153. package/src/i18n/createFilter.ts +131 -131
  154. package/src/i18n/createNumberFormatter.ts +52 -52
  155. package/src/i18n/index.ts +40 -40
  156. package/src/i18n/locale.tsx +188 -188
  157. package/src/i18n/utils.ts +99 -99
  158. package/src/index.ts +51 -0
  159. package/src/interactions/createFocus.ts +6 -5
  160. package/src/interactions/createFocusWithin.ts +6 -5
  161. package/src/interactions/createLongPress.ts +174 -174
  162. package/src/interactions/createMove.ts +289 -289
  163. package/src/interactions/createPress.ts +5 -5
  164. package/src/landmark/createLandmark.ts +377 -377
  165. package/src/landmark/index.ts +8 -8
  166. package/src/link/createLink.ts +23 -8
  167. package/src/listbox/createListBox.ts +308 -269
  168. package/src/listbox/createOption.ts +162 -151
  169. package/src/listbox/index.ts +12 -12
  170. package/src/live-announcer/announce.ts +322 -322
  171. package/src/live-announcer/index.ts +9 -9
  172. package/src/menu/createMenu.ts +405 -396
  173. package/src/menu/createMenuItem.ts +149 -149
  174. package/src/menu/createMenuTrigger.ts +88 -88
  175. package/src/menu/index.ts +18 -18
  176. package/src/meter/createMeter.ts +1 -6
  177. package/src/numberfield/createNumberField.ts +311 -268
  178. package/src/numberfield/index.ts +5 -5
  179. package/src/overlays/ariaHideOutside.ts +219 -219
  180. package/src/overlays/createInteractOutside.ts +149 -149
  181. package/src/overlays/createModal.tsx +238 -202
  182. package/src/overlays/createOverlay.ts +165 -155
  183. package/src/overlays/createOverlayTrigger.ts +85 -85
  184. package/src/overlays/createPreventScroll.ts +266 -266
  185. package/src/overlays/index.ts +48 -44
  186. package/src/popover/calculatePosition.ts +6 -6
  187. package/src/popover/createOverlayPosition.ts +7 -4
  188. package/src/popover/createPopover.ts +21 -7
  189. package/src/progress/createProgressBar.ts +6 -1
  190. package/src/radio/createRadioGroup.ts +88 -14
  191. package/src/searchfield/createSearchField.ts +241 -186
  192. package/src/searchfield/index.ts +2 -2
  193. package/src/select/createHiddenSelect.tsx +263 -236
  194. package/src/select/createSelect.ts +373 -395
  195. package/src/select/index.ts +14 -14
  196. package/src/slider/createSlider.ts +364 -349
  197. package/src/slider/index.ts +2 -2
  198. package/src/ssr/index.tsx +370 -370
  199. package/src/table/createTable.ts +3 -1
  200. package/src/table/createTableColumnHeader.ts +1 -1
  201. package/src/table/createTableRow.ts +1 -1
  202. package/src/tabs/createTabs.ts +80 -51
  203. package/src/tag/createTag.ts +135 -6
  204. package/src/tag/createTagGroup.ts +7 -2
  205. package/src/toast/createToast.ts +8 -2
  206. package/src/toast/createToastRegion.ts +0 -1
  207. package/src/toolbar/createToolbar.ts +75 -1
  208. package/src/tooltip/createTooltip.ts +79 -79
  209. package/src/tooltip/createTooltipTrigger.ts +226 -222
  210. package/src/tooltip/index.ts +6 -6
  211. package/src/tree/createTree.ts +261 -246
  212. package/src/tree/createTreeItem.ts +282 -233
  213. package/src/tree/createTreeSelectionCheckbox.ts +68 -68
  214. package/src/tree/index.ts +16 -16
  215. package/src/tree/types.ts +91 -87
  216. package/src/utils/env.ts +55 -54
  217. package/src/utils/platform.ts +16 -6
  218. package/src/visually-hidden/createVisuallyHidden.ts +139 -124
  219. package/src/visually-hidden/index.ts +6 -6
@@ -1,313 +1,313 @@
1
- /**
2
- * Focus restoration utilities for solidaria
3
- *
4
- * Provides enhanced focus restoration with retry logic, cross-scope tracking,
5
- * and safe restoration patterns.
6
- */
7
-
8
- import { createEffect, onCleanup, onMount } from 'solid-js';
9
- import { isServer } from 'solid-js/web';
10
- import { getOwnerDocument } from '../utils';
11
- import { focusSafely } from '../utils/focus';
12
-
13
- // ============================================
14
- // TYPES
15
- // ============================================
16
-
17
- export interface FocusRestoreOptions {
18
- /**
19
- * Whether to restore focus when the component unmounts.
20
- * @default true
21
- */
22
- restoreOnUnmount?: boolean;
23
- /**
24
- * Maximum number of retries if the element is not in the DOM.
25
- * @default 3
26
- */
27
- maxRetries?: number;
28
- /**
29
- * Delay between retries in milliseconds.
30
- * @default 50
31
- */
32
- retryDelay?: number;
33
- /**
34
- * Callback when focus is successfully restored.
35
- */
36
- onRestore?: (element: HTMLElement) => void;
37
- /**
38
- * Callback when focus restoration fails.
39
- */
40
- onRestoreFailed?: () => void;
41
- /**
42
- * Whether to prevent scrolling when restoring focus.
43
- * @default true
44
- */
45
- preventScroll?: boolean;
46
- }
47
-
48
- export interface FocusRestoreResult {
49
- /**
50
- * Manually restore focus to the saved element.
51
- */
52
- restore: () => boolean;
53
- /**
54
- * Get the saved element (if any).
55
- */
56
- getSavedElement: () => HTMLElement | null;
57
- /**
58
- * Save the currently focused element.
59
- */
60
- saveCurrentFocus: () => void;
61
- /**
62
- * Clear the saved element without restoring.
63
- */
64
- clear: () => void;
65
- }
66
-
67
- // ============================================
68
- // GLOBAL FOCUS STACK
69
- // ============================================
70
-
71
- // Stack to track focus history across scopes
72
- const focusStack: HTMLElement[] = [];
73
-
74
- /**
75
- * Push an element onto the focus stack.
76
- */
77
- export function pushFocusStack(element: HTMLElement): void {
78
- focusStack.push(element);
79
- }
80
-
81
- /**
82
- * Pop the last element from the focus stack.
83
- */
84
- export function popFocusStack(): HTMLElement | undefined {
85
- return focusStack.pop();
86
- }
87
-
88
- /**
89
- * Get the current focus stack length.
90
- */
91
- export function getFocusStackLength(): number {
92
- return focusStack.length;
93
- }
94
-
95
- /**
96
- * Clear the entire focus stack.
97
- */
98
- export function clearFocusStack(): void {
99
- focusStack.length = 0;
100
- }
101
-
102
- // ============================================
103
- // UTILITIES
104
- // ============================================
105
-
106
- /**
107
- * Gets the active element, accounting for shadow DOM.
108
- */
109
- function getActiveElement(doc: Document): HTMLElement | null {
110
- let activeElement = doc.activeElement as HTMLElement | null;
111
- while (activeElement?.shadowRoot?.activeElement) {
112
- activeElement = activeElement.shadowRoot.activeElement as HTMLElement;
113
- }
114
- return activeElement;
115
- }
116
-
117
- /**
118
- * Checks if an element is still valid for focus restoration.
119
- */
120
- function isValidForRestore(element: HTMLElement | null): boolean {
121
- if (!element) return false;
122
- if (!document.body.contains(element)) return false;
123
- if (element.hasAttribute('disabled')) return false;
124
- if (element.getAttribute('aria-disabled') === 'true') return false;
125
- if (element.getAttribute('aria-hidden') === 'true') return false;
126
- return true;
127
- }
128
-
129
- /**
130
- * Attempts to restore focus with retries.
131
- */
132
- function tryRestoreFocus(
133
- element: HTMLElement | null,
134
- options: Required<Pick<FocusRestoreOptions, 'maxRetries' | 'retryDelay' | 'preventScroll' | 'onRestore' | 'onRestoreFailed'>>
135
- ): void {
136
- const { maxRetries, retryDelay, preventScroll, onRestore, onRestoreFailed } = options;
137
- let attempts = 0;
138
-
139
- const attempt = () => {
140
- if (!element) {
141
- onRestoreFailed?.();
142
- return;
143
- }
144
-
145
- if (isValidForRestore(element)) {
146
- if (preventScroll) {
147
- focusSafely(element);
148
- } else {
149
- element.focus();
150
- }
151
- onRestore?.(element);
152
- return;
153
- }
154
-
155
- attempts++;
156
- if (attempts < maxRetries) {
157
- setTimeout(attempt, retryDelay);
158
- } else {
159
- onRestoreFailed?.();
160
- }
161
- };
162
-
163
- // Use requestAnimationFrame for the first attempt to ensure DOM is ready
164
- requestAnimationFrame(attempt);
165
- }
166
-
167
- // ============================================
168
- // HOOK
169
- // ============================================
170
-
171
- /**
172
- * Creates a focus restoration manager.
173
- *
174
- * This hook saves the currently focused element when mounted and provides
175
- * methods to restore focus later, with retry logic for reliability.
176
- *
177
- * @example
178
- * ```tsx
179
- * function Modal(props) {
180
- * const focusRestore = createFocusRestore({
181
- * restoreOnUnmount: true,
182
- * onRestore: () => console.log('Focus restored'),
183
- * });
184
- *
185
- * return (
186
- * <div role="dialog">
187
- * {props.children}
188
- * <button onClick={() => focusRestore.restore()}>
189
- * Close
190
- * </button>
191
- * </div>
192
- * );
193
- * }
194
- * ```
195
- *
196
- * @example
197
- * ```tsx
198
- * // Manual focus management
199
- * function Dropdown() {
200
- * const focusRestore = createFocusRestore({ restoreOnUnmount: false });
201
- *
202
- * const onOpen = () => {
203
- * focusRestore.saveCurrentFocus();
204
- * // Focus dropdown content
205
- * };
206
- *
207
- * const onClose = () => {
208
- * focusRestore.restore();
209
- * };
210
- *
211
- * return <div>...</div>;
212
- * }
213
- * ```
214
- */
215
- export function createFocusRestore(
216
- options: FocusRestoreOptions = {}
217
- ): FocusRestoreResult {
218
- const {
219
- restoreOnUnmount = true,
220
- maxRetries = 3,
221
- retryDelay = 50,
222
- onRestore,
223
- onRestoreFailed,
224
- preventScroll = true,
225
- } = options;
226
-
227
- // During SSR, return no-op functions
228
- if (isServer) {
229
- return {
230
- restore: () => false,
231
- getSavedElement: () => null,
232
- saveCurrentFocus: () => {},
233
- clear: () => {},
234
- };
235
- }
236
-
237
- let savedElement: HTMLElement | null = null;
238
-
239
- // Save focus on mount
240
- onMount(() => {
241
- saveCurrentFocus();
242
- });
243
-
244
- // Restore focus on cleanup
245
- onCleanup(() => {
246
- if (restoreOnUnmount && savedElement) {
247
- tryRestoreFocus(savedElement, {
248
- maxRetries,
249
- retryDelay,
250
- preventScroll,
251
- onRestore: onRestore ?? (() => {}),
252
- onRestoreFailed: onRestoreFailed ?? (() => {}),
253
- });
254
- }
255
- });
256
-
257
- function saveCurrentFocus(): void {
258
- const doc = typeof document !== 'undefined' ? document : null;
259
- if (!doc) return;
260
-
261
- const active = getActiveElement(doc);
262
- if (active && active !== doc.body) {
263
- savedElement = active;
264
- pushFocusStack(active);
265
- }
266
- }
267
-
268
- function restore(): boolean {
269
- if (!savedElement) return false;
270
-
271
- if (isValidForRestore(savedElement)) {
272
- if (preventScroll) {
273
- focusSafely(savedElement);
274
- } else {
275
- savedElement.focus();
276
- }
277
- onRestore?.(savedElement);
278
- return true;
279
- }
280
-
281
- // Try the focus stack
282
- while (focusStack.length > 0) {
283
- const stackElement = popFocusStack();
284
- if (stackElement && isValidForRestore(stackElement)) {
285
- if (preventScroll) {
286
- focusSafely(stackElement);
287
- } else {
288
- stackElement.focus();
289
- }
290
- onRestore?.(stackElement);
291
- return true;
292
- }
293
- }
294
-
295
- onRestoreFailed?.();
296
- return false;
297
- }
298
-
299
- function getSavedElement(): HTMLElement | null {
300
- return savedElement;
301
- }
302
-
303
- function clear(): void {
304
- savedElement = null;
305
- }
306
-
307
- return {
308
- restore,
309
- getSavedElement,
310
- saveCurrentFocus,
311
- clear,
312
- };
313
- }
1
+ /**
2
+ * Focus restoration utilities for solidaria
3
+ *
4
+ * Provides enhanced focus restoration with retry logic, cross-scope tracking,
5
+ * and safe restoration patterns.
6
+ */
7
+
8
+ import { createEffect, onCleanup, onMount } from 'solid-js';
9
+ import { isServer } from 'solid-js/web';
10
+ import { getOwnerDocument } from '../utils';
11
+ import { focusSafely } from '../utils/focus';
12
+
13
+ // ============================================
14
+ // TYPES
15
+ // ============================================
16
+
17
+ export interface FocusRestoreOptions {
18
+ /**
19
+ * Whether to restore focus when the component unmounts.
20
+ * @default true
21
+ */
22
+ restoreOnUnmount?: boolean;
23
+ /**
24
+ * Maximum number of retries if the element is not in the DOM.
25
+ * @default 3
26
+ */
27
+ maxRetries?: number;
28
+ /**
29
+ * Delay between retries in milliseconds.
30
+ * @default 50
31
+ */
32
+ retryDelay?: number;
33
+ /**
34
+ * Callback when focus is successfully restored.
35
+ */
36
+ onRestore?: (element: HTMLElement) => void;
37
+ /**
38
+ * Callback when focus restoration fails.
39
+ */
40
+ onRestoreFailed?: () => void;
41
+ /**
42
+ * Whether to prevent scrolling when restoring focus.
43
+ * @default true
44
+ */
45
+ preventScroll?: boolean;
46
+ }
47
+
48
+ export interface FocusRestoreResult {
49
+ /**
50
+ * Manually restore focus to the saved element.
51
+ */
52
+ restore: () => boolean;
53
+ /**
54
+ * Get the saved element (if any).
55
+ */
56
+ getSavedElement: () => HTMLElement | null;
57
+ /**
58
+ * Save the currently focused element.
59
+ */
60
+ saveCurrentFocus: () => void;
61
+ /**
62
+ * Clear the saved element without restoring.
63
+ */
64
+ clear: () => void;
65
+ }
66
+
67
+ // ============================================
68
+ // GLOBAL FOCUS STACK
69
+ // ============================================
70
+
71
+ // Stack to track focus history across scopes
72
+ const focusStack: HTMLElement[] = [];
73
+
74
+ /**
75
+ * Push an element onto the focus stack.
76
+ */
77
+ export function pushFocusStack(element: HTMLElement): void {
78
+ focusStack.push(element);
79
+ }
80
+
81
+ /**
82
+ * Pop the last element from the focus stack.
83
+ */
84
+ export function popFocusStack(): HTMLElement | undefined {
85
+ return focusStack.pop();
86
+ }
87
+
88
+ /**
89
+ * Get the current focus stack length.
90
+ */
91
+ export function getFocusStackLength(): number {
92
+ return focusStack.length;
93
+ }
94
+
95
+ /**
96
+ * Clear the entire focus stack.
97
+ */
98
+ export function clearFocusStack(): void {
99
+ focusStack.length = 0;
100
+ }
101
+
102
+ // ============================================
103
+ // UTILITIES
104
+ // ============================================
105
+
106
+ /**
107
+ * Gets the active element, accounting for shadow DOM.
108
+ */
109
+ function getActiveElement(doc: Document): HTMLElement | null {
110
+ let activeElement = doc.activeElement as HTMLElement | null;
111
+ while (activeElement?.shadowRoot?.activeElement) {
112
+ activeElement = activeElement.shadowRoot.activeElement as HTMLElement;
113
+ }
114
+ return activeElement;
115
+ }
116
+
117
+ /**
118
+ * Checks if an element is still valid for focus restoration.
119
+ */
120
+ function isValidForRestore(element: HTMLElement | null): boolean {
121
+ if (!element) return false;
122
+ if (!document.body.contains(element)) return false;
123
+ if (element.hasAttribute('disabled')) return false;
124
+ if (element.getAttribute('aria-disabled') === 'true') return false;
125
+ if (element.getAttribute('aria-hidden') === 'true') return false;
126
+ return true;
127
+ }
128
+
129
+ /**
130
+ * Attempts to restore focus with retries.
131
+ */
132
+ function tryRestoreFocus(
133
+ element: HTMLElement | null,
134
+ options: Required<Pick<FocusRestoreOptions, 'maxRetries' | 'retryDelay' | 'preventScroll' | 'onRestore' | 'onRestoreFailed'>>
135
+ ): void {
136
+ const { maxRetries, retryDelay, preventScroll, onRestore, onRestoreFailed } = options;
137
+ let attempts = 0;
138
+
139
+ const attempt = () => {
140
+ if (!element) {
141
+ onRestoreFailed?.();
142
+ return;
143
+ }
144
+
145
+ if (isValidForRestore(element)) {
146
+ if (preventScroll) {
147
+ focusSafely(element);
148
+ } else {
149
+ element.focus();
150
+ }
151
+ onRestore?.(element);
152
+ return;
153
+ }
154
+
155
+ attempts++;
156
+ if (attempts < maxRetries) {
157
+ setTimeout(attempt, retryDelay);
158
+ } else {
159
+ onRestoreFailed?.();
160
+ }
161
+ };
162
+
163
+ // Use requestAnimationFrame for the first attempt to ensure DOM is ready
164
+ requestAnimationFrame(attempt);
165
+ }
166
+
167
+ // ============================================
168
+ // HOOK
169
+ // ============================================
170
+
171
+ /**
172
+ * Creates a focus restoration manager.
173
+ *
174
+ * This hook saves the currently focused element when mounted and provides
175
+ * methods to restore focus later, with retry logic for reliability.
176
+ *
177
+ * @example
178
+ * ```tsx
179
+ * function Modal(props) {
180
+ * const focusRestore = createFocusRestore({
181
+ * restoreOnUnmount: true,
182
+ * onRestore: () => console.log('Focus restored'),
183
+ * });
184
+ *
185
+ * return (
186
+ * <div role="dialog">
187
+ * {props.children}
188
+ * <button onClick={() => focusRestore.restore()}>
189
+ * Close
190
+ * </button>
191
+ * </div>
192
+ * );
193
+ * }
194
+ * ```
195
+ *
196
+ * @example
197
+ * ```tsx
198
+ * // Manual focus management
199
+ * function Dropdown() {
200
+ * const focusRestore = createFocusRestore({ restoreOnUnmount: false });
201
+ *
202
+ * const onOpen = () => {
203
+ * focusRestore.saveCurrentFocus();
204
+ * // Focus dropdown content
205
+ * };
206
+ *
207
+ * const onClose = () => {
208
+ * focusRestore.restore();
209
+ * };
210
+ *
211
+ * return <div>...</div>;
212
+ * }
213
+ * ```
214
+ */
215
+ export function createFocusRestore(
216
+ options: FocusRestoreOptions = {}
217
+ ): FocusRestoreResult {
218
+ const {
219
+ restoreOnUnmount = true,
220
+ maxRetries = 3,
221
+ retryDelay = 50,
222
+ onRestore,
223
+ onRestoreFailed,
224
+ preventScroll = true,
225
+ } = options;
226
+
227
+ // During SSR, return no-op functions
228
+ if (isServer) {
229
+ return {
230
+ restore: () => false,
231
+ getSavedElement: () => null,
232
+ saveCurrentFocus: () => {},
233
+ clear: () => {},
234
+ };
235
+ }
236
+
237
+ let savedElement: HTMLElement | null = null;
238
+
239
+ // Save focus on mount
240
+ onMount(() => {
241
+ saveCurrentFocus();
242
+ });
243
+
244
+ // Restore focus on cleanup
245
+ onCleanup(() => {
246
+ if (restoreOnUnmount && savedElement) {
247
+ tryRestoreFocus(savedElement, {
248
+ maxRetries,
249
+ retryDelay,
250
+ preventScroll,
251
+ onRestore: onRestore ?? (() => {}),
252
+ onRestoreFailed: onRestoreFailed ?? (() => {}),
253
+ });
254
+ }
255
+ });
256
+
257
+ function saveCurrentFocus(): void {
258
+ const doc = typeof document !== 'undefined' ? document : null;
259
+ if (!doc) return;
260
+
261
+ const active = getActiveElement(doc);
262
+ if (active && active !== doc.body) {
263
+ savedElement = active;
264
+ pushFocusStack(active);
265
+ }
266
+ }
267
+
268
+ function restore(): boolean {
269
+ if (!savedElement) return false;
270
+
271
+ if (isValidForRestore(savedElement)) {
272
+ if (preventScroll) {
273
+ focusSafely(savedElement);
274
+ } else {
275
+ savedElement.focus();
276
+ }
277
+ onRestore?.(savedElement);
278
+ return true;
279
+ }
280
+
281
+ // Try the focus stack
282
+ while (focusStack.length > 0) {
283
+ const stackElement = popFocusStack();
284
+ if (stackElement && isValidForRestore(stackElement)) {
285
+ if (preventScroll) {
286
+ focusSafely(stackElement);
287
+ } else {
288
+ stackElement.focus();
289
+ }
290
+ onRestore?.(stackElement);
291
+ return true;
292
+ }
293
+ }
294
+
295
+ onRestoreFailed?.();
296
+ return false;
297
+ }
298
+
299
+ function getSavedElement(): HTMLElement | null {
300
+ return savedElement;
301
+ }
302
+
303
+ function clear(): void {
304
+ savedElement = null;
305
+ }
306
+
307
+ return {
308
+ restore,
309
+ getSavedElement,
310
+ saveCurrentFocus,
311
+ clear,
312
+ };
313
+ }