@proyecto-viviana/solidaria 0.2.2 → 0.2.3

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 (210) hide show
  1. package/dist/autocomplete/createAutocomplete.d.ts +2 -2
  2. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  3. package/dist/index.js +233 -234
  4. package/dist/index.js.map +2 -2
  5. package/dist/index.ssr.js +233 -234
  6. package/dist/index.ssr.js.map +2 -2
  7. package/dist/interactions/PressEvent.d.ts +13 -10
  8. package/dist/interactions/PressEvent.d.ts.map +1 -1
  9. package/dist/interactions/createPress.d.ts.map +1 -1
  10. package/dist/interactions/index.d.ts +1 -1
  11. package/dist/interactions/index.d.ts.map +1 -1
  12. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  13. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  14. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  15. package/package.json +9 -7
  16. package/src/autocomplete/createAutocomplete.ts +341 -0
  17. package/src/autocomplete/index.ts +9 -0
  18. package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
  19. package/src/breadcrumbs/index.ts +8 -0
  20. package/src/button/createButton.ts +142 -0
  21. package/src/button/createToggleButton.ts +101 -0
  22. package/src/button/index.ts +4 -0
  23. package/src/button/types.ts +78 -0
  24. package/src/calendar/createCalendar.ts +138 -0
  25. package/src/calendar/createCalendarCell.ts +187 -0
  26. package/src/calendar/createCalendarGrid.ts +140 -0
  27. package/src/calendar/createRangeCalendar.ts +136 -0
  28. package/src/calendar/createRangeCalendarCell.ts +186 -0
  29. package/src/calendar/index.ts +34 -0
  30. package/src/checkbox/createCheckbox.ts +135 -0
  31. package/src/checkbox/createCheckboxGroup.ts +137 -0
  32. package/src/checkbox/createCheckboxGroupItem.ts +117 -0
  33. package/src/checkbox/createCheckboxGroupState.ts +193 -0
  34. package/src/checkbox/index.ts +13 -0
  35. package/src/color/createColorArea.ts +314 -0
  36. package/src/color/createColorField.ts +137 -0
  37. package/src/color/createColorSlider.ts +197 -0
  38. package/src/color/createColorSwatch.ts +40 -0
  39. package/src/color/createColorWheel.ts +208 -0
  40. package/src/color/index.ts +24 -0
  41. package/src/color/types.ts +116 -0
  42. package/src/combobox/createComboBox.ts +647 -0
  43. package/src/combobox/index.ts +6 -0
  44. package/src/combobox/intl/en-US.json +7 -0
  45. package/src/combobox/intl/es-ES.json +7 -0
  46. package/src/combobox/intl/index.ts +23 -0
  47. package/src/datepicker/createDateField.ts +154 -0
  48. package/src/datepicker/createDatePicker.ts +206 -0
  49. package/src/datepicker/createDateSegment.ts +229 -0
  50. package/src/datepicker/createTimeField.ts +154 -0
  51. package/src/datepicker/index.ts +28 -0
  52. package/src/dialog/createDialog.ts +120 -0
  53. package/src/dialog/index.ts +2 -0
  54. package/src/dialog/types.ts +19 -0
  55. package/src/disclosure/createDisclosure.ts +131 -0
  56. package/src/disclosure/createDisclosureGroup.ts +62 -0
  57. package/src/disclosure/index.ts +11 -0
  58. package/src/dnd/createDrag.ts +209 -0
  59. package/src/dnd/createDraggableCollection.ts +63 -0
  60. package/src/dnd/createDraggableItem.ts +243 -0
  61. package/src/dnd/createDrop.ts +321 -0
  62. package/src/dnd/createDroppableCollection.ts +293 -0
  63. package/src/dnd/createDroppableItem.ts +213 -0
  64. package/src/dnd/index.ts +47 -0
  65. package/src/dnd/types.ts +89 -0
  66. package/src/dnd/utils.ts +294 -0
  67. package/src/focus/FocusScope.tsx +408 -0
  68. package/src/focus/createAutoFocus.ts +321 -0
  69. package/src/focus/createFocusRestore.ts +313 -0
  70. package/src/focus/createVirtualFocus.ts +396 -0
  71. package/src/focus/index.ts +35 -0
  72. package/src/form/createFormReset.ts +51 -0
  73. package/src/form/createFormValidation.ts +224 -0
  74. package/src/form/index.ts +11 -0
  75. package/src/grid/GridKeyboardDelegate.ts +429 -0
  76. package/src/grid/createGrid.ts +261 -0
  77. package/src/grid/createGridCell.ts +182 -0
  78. package/src/grid/createGridRow.ts +153 -0
  79. package/src/grid/index.ts +18 -0
  80. package/src/grid/types.ts +133 -0
  81. package/src/gridlist/createGridList.ts +185 -0
  82. package/src/gridlist/createGridListItem.ts +180 -0
  83. package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
  84. package/src/gridlist/index.ts +16 -0
  85. package/src/gridlist/types.ts +81 -0
  86. package/src/i18n/NumberFormatter.ts +266 -0
  87. package/src/i18n/createCollator.ts +79 -0
  88. package/src/i18n/createDateFormatter.ts +83 -0
  89. package/src/i18n/createFilter.ts +131 -0
  90. package/src/i18n/createNumberFormatter.ts +52 -0
  91. package/src/i18n/createStringFormatter.ts +87 -0
  92. package/src/i18n/index.ts +40 -0
  93. package/src/i18n/locale.tsx +188 -0
  94. package/src/i18n/utils.ts +99 -0
  95. package/src/index.ts +670 -0
  96. package/src/interactions/FocusableProvider.tsx +44 -0
  97. package/src/interactions/PressEvent.ts +126 -0
  98. package/src/interactions/createFocus.ts +163 -0
  99. package/src/interactions/createFocusRing.ts +89 -0
  100. package/src/interactions/createFocusWithin.ts +206 -0
  101. package/src/interactions/createFocusable.ts +168 -0
  102. package/src/interactions/createHover.ts +254 -0
  103. package/src/interactions/createInteractionModality.ts +424 -0
  104. package/src/interactions/createKeyboard.ts +82 -0
  105. package/src/interactions/createLongPress.ts +174 -0
  106. package/src/interactions/createMove.ts +289 -0
  107. package/src/interactions/createPress.ts +834 -0
  108. package/src/interactions/index.ts +78 -0
  109. package/src/label/createField.ts +145 -0
  110. package/src/label/createLabel.ts +117 -0
  111. package/src/label/createLabels.ts +50 -0
  112. package/src/label/index.ts +19 -0
  113. package/src/landmark/createLandmark.ts +377 -0
  114. package/src/landmark/index.ts +8 -0
  115. package/src/link/createLink.ts +182 -0
  116. package/src/link/index.ts +1 -0
  117. package/src/listbox/createListBox.ts +269 -0
  118. package/src/listbox/createOption.ts +151 -0
  119. package/src/listbox/index.ts +12 -0
  120. package/src/live-announcer/announce.ts +322 -0
  121. package/src/live-announcer/index.ts +9 -0
  122. package/src/menu/createMenu.ts +396 -0
  123. package/src/menu/createMenuItem.ts +149 -0
  124. package/src/menu/createMenuTrigger.ts +88 -0
  125. package/src/menu/index.ts +18 -0
  126. package/src/meter/createMeter.ts +75 -0
  127. package/src/meter/index.ts +1 -0
  128. package/src/numberfield/createNumberField.ts +268 -0
  129. package/src/numberfield/index.ts +5 -0
  130. package/src/overlays/ariaHideOutside.ts +219 -0
  131. package/src/overlays/createInteractOutside.ts +149 -0
  132. package/src/overlays/createModal.tsx +202 -0
  133. package/src/overlays/createOverlay.ts +155 -0
  134. package/src/overlays/createOverlayTrigger.ts +85 -0
  135. package/src/overlays/createPreventScroll.ts +266 -0
  136. package/src/overlays/index.ts +44 -0
  137. package/src/popover/calculatePosition.ts +766 -0
  138. package/src/popover/createOverlayPosition.ts +356 -0
  139. package/src/popover/createPopover.ts +170 -0
  140. package/src/popover/index.ts +24 -0
  141. package/src/progress/createProgressBar.ts +128 -0
  142. package/src/progress/index.ts +5 -0
  143. package/src/radio/createRadio.ts +287 -0
  144. package/src/radio/createRadioGroup.ts +189 -0
  145. package/src/radio/createRadioGroupState.ts +201 -0
  146. package/src/radio/index.ts +23 -0
  147. package/src/searchfield/createSearchField.ts +186 -0
  148. package/src/searchfield/index.ts +2 -0
  149. package/src/select/createHiddenSelect.tsx +236 -0
  150. package/src/select/createSelect.ts +395 -0
  151. package/src/select/index.ts +14 -0
  152. package/src/selection/createTypeSelect.ts +201 -0
  153. package/src/selection/index.ts +6 -0
  154. package/src/separator/createSeparator.ts +82 -0
  155. package/src/separator/index.ts +6 -0
  156. package/src/slider/createSlider.ts +349 -0
  157. package/src/slider/index.ts +2 -0
  158. package/src/ssr/index.tsx +370 -0
  159. package/src/switch/createSwitch.ts +70 -0
  160. package/src/switch/index.ts +1 -0
  161. package/src/table/createTable.ts +526 -0
  162. package/src/table/createTableCell.ts +147 -0
  163. package/src/table/createTableColumnHeader.ts +115 -0
  164. package/src/table/createTableHeaderRow.ts +40 -0
  165. package/src/table/createTableRow.ts +155 -0
  166. package/src/table/createTableRowGroup.ts +32 -0
  167. package/src/table/createTableSelectAllCheckbox.ts +73 -0
  168. package/src/table/createTableSelectionCheckbox.ts +59 -0
  169. package/src/table/index.ts +30 -0
  170. package/src/table/types.ts +165 -0
  171. package/src/tabs/createTabs.ts +472 -0
  172. package/src/tabs/index.ts +14 -0
  173. package/src/tag/createTag.ts +194 -0
  174. package/src/tag/createTagGroup.ts +154 -0
  175. package/src/tag/index.ts +12 -0
  176. package/src/textfield/createTextField.ts +198 -0
  177. package/src/textfield/index.ts +5 -0
  178. package/src/toast/createToast.ts +118 -0
  179. package/src/toast/createToastRegion.ts +100 -0
  180. package/src/toast/index.ts +11 -0
  181. package/src/toggle/createToggle.ts +223 -0
  182. package/src/toggle/createToggleState.ts +94 -0
  183. package/src/toggle/index.ts +7 -0
  184. package/src/toolbar/createToolbar.ts +369 -0
  185. package/src/toolbar/index.ts +6 -0
  186. package/src/tooltip/createTooltip.ts +79 -0
  187. package/src/tooltip/createTooltipTrigger.ts +222 -0
  188. package/src/tooltip/index.ts +6 -0
  189. package/src/tree/createTree.ts +246 -0
  190. package/src/tree/createTreeItem.ts +233 -0
  191. package/src/tree/createTreeSelectionCheckbox.ts +68 -0
  192. package/src/tree/index.ts +16 -0
  193. package/src/tree/types.ts +87 -0
  194. package/src/utils/createDescription.ts +137 -0
  195. package/src/utils/dom.ts +327 -0
  196. package/src/utils/env.ts +54 -0
  197. package/src/utils/events.ts +106 -0
  198. package/src/utils/filterDOMProps.ts +116 -0
  199. package/src/utils/focus.ts +151 -0
  200. package/src/utils/geometry.ts +115 -0
  201. package/src/utils/globalListeners.ts +142 -0
  202. package/src/utils/index.ts +80 -0
  203. package/src/utils/mergeProps.ts +52 -0
  204. package/src/utils/platform.ts +52 -0
  205. package/src/utils/reactivity.ts +36 -0
  206. package/src/utils/textSelection.ts +114 -0
  207. package/src/visually-hidden/createVisuallyHidden.ts +124 -0
  208. package/src/visually-hidden/index.ts +6 -0
  209. package/dist/index.jsx +0 -15845
  210. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,408 @@
1
+ /**
2
+ * FocusScope component for managing focus containment, restoration, and auto-focus.
3
+ * Based on @react-aria/focus FocusScope.
4
+ */
5
+
6
+ import {
7
+ createContext,
8
+ useContext,
9
+ createEffect,
10
+ onCleanup,
11
+ type JSX,
12
+ type Accessor,
13
+ type ParentComponent,
14
+ createSignal,
15
+ onMount,
16
+ } from 'solid-js'
17
+ import { isServer } from 'solid-js/web'
18
+ import { getOwnerDocument, isFocusable } from '../utils'
19
+ import { focusSafely } from '../utils/focus'
20
+
21
+ // ============================================
22
+ // TYPES
23
+ // ============================================
24
+
25
+ export interface FocusScopeProps {
26
+ /** The contents of the focus scope. */
27
+ children: JSX.Element
28
+ /**
29
+ * Whether to contain focus inside the scope, so users cannot
30
+ * move focus outside, for example in a modal dialog.
31
+ */
32
+ contain?: boolean
33
+ /**
34
+ * Whether to restore focus back to the element that was focused
35
+ * when the focus scope mounted, after the focus scope unmounts.
36
+ */
37
+ restoreFocus?: boolean
38
+ /** Whether to auto focus the first focusable element in the focus scope on mount. */
39
+ autoFocus?: boolean
40
+ }
41
+
42
+ export interface FocusManagerOptions {
43
+ /** The element to start searching from. The currently focused element by default. */
44
+ from?: Element
45
+ /** Whether to only include tabbable elements, or all focusable elements. */
46
+ tabbable?: boolean
47
+ /** Whether focus should wrap around when it reaches the end of the scope. */
48
+ wrap?: boolean
49
+ /** A callback that determines whether the given element is focused. */
50
+ accept?: (node: Element) => boolean
51
+ }
52
+
53
+ export interface FocusManager {
54
+ /** Moves focus to the next focusable or tabbable element in the focus scope. */
55
+ focusNext(opts?: FocusManagerOptions): HTMLElement | null
56
+ /** Moves focus to the previous focusable or tabbable element in the focus scope. */
57
+ focusPrevious(opts?: FocusManagerOptions): HTMLElement | null
58
+ /** Moves focus to the first focusable or tabbable element in the focus scope. */
59
+ focusFirst(opts?: FocusManagerOptions): HTMLElement | null
60
+ /** Moves focus to the last focusable or tabbable element in the focus scope. */
61
+ focusLast(opts?: FocusManagerOptions): HTMLElement | null
62
+ }
63
+
64
+ // ============================================
65
+ // CONTEXT
66
+ // ============================================
67
+
68
+ interface FocusScopeContextValue {
69
+ focusManager: FocusManager
70
+ scopeRef: Accessor<Element[]>
71
+ }
72
+
73
+ const FocusScopeContext = createContext<FocusScopeContextValue | null>(null)
74
+
75
+ /**
76
+ * Returns a FocusManager interface for the parent FocusScope.
77
+ * A FocusManager can be used to programmatically move focus within
78
+ * a FocusScope, e.g. in response to user events like keyboard navigation.
79
+ */
80
+ export function useFocusManager(): FocusManager | undefined {
81
+ return useContext(FocusScopeContext)?.focusManager
82
+ }
83
+
84
+ // ============================================
85
+ // UTILITIES
86
+ // ============================================
87
+
88
+ /**
89
+ * Checks if an element is tabbable (focusable via Tab key).
90
+ */
91
+ function isTabbable(element: Element): boolean {
92
+ if (!isFocusable(element)) {
93
+ return false
94
+ }
95
+
96
+ // Check tabIndex
97
+ const tabIndex = element.getAttribute('tabindex')
98
+ if (tabIndex != null) {
99
+ return parseInt(tabIndex, 10) >= 0
100
+ }
101
+
102
+ return true
103
+ }
104
+
105
+ /**
106
+ * Gets all focusable elements within a scope.
107
+ */
108
+ function getFocusableElements(scope: Element[], tabbable = false): HTMLElement[] {
109
+ const elements: HTMLElement[] = []
110
+ const filter = tabbable ? isTabbable : isFocusable
111
+
112
+ for (const scopeElement of scope) {
113
+ // Check the element itself
114
+ if (filter(scopeElement)) {
115
+ elements.push(scopeElement as HTMLElement)
116
+ }
117
+
118
+ // Check all descendants
119
+ const descendants = scopeElement.querySelectorAll('*')
120
+ for (let i = 0; i < descendants.length; i++) {
121
+ const el = descendants[i]
122
+ if (filter(el)) {
123
+ elements.push(el as HTMLElement)
124
+ }
125
+ }
126
+ }
127
+
128
+ return elements
129
+ }
130
+
131
+ /**
132
+ * Checks if an element is within a scope.
133
+ */
134
+ function isElementInScope(element: Element | null, scope: Element[]): boolean {
135
+ if (!element) return false
136
+ return scope.some(node => node.contains(element))
137
+ }
138
+
139
+ /**
140
+ * Gets the active element, accounting for shadow DOM.
141
+ */
142
+ function getActiveElement(doc: Document): Element | null {
143
+ let activeElement = doc.activeElement
144
+ while (activeElement?.shadowRoot?.activeElement) {
145
+ activeElement = activeElement.shadowRoot.activeElement
146
+ }
147
+ return activeElement
148
+ }
149
+
150
+ // ============================================
151
+ // FOCUS SCOPE COMPONENT
152
+ // ============================================
153
+
154
+ /**
155
+ * A FocusScope manages focus for its descendants. It supports containing focus inside
156
+ * the scope, restoring focus to the previously focused element on unmount, and auto
157
+ * focusing children on mount. It also acts as a container for a programmatic focus
158
+ * management interface that can be used to move focus forward and back in response
159
+ * to user events.
160
+ */
161
+ export const FocusScope: ParentComponent<FocusScopeProps> = (props) => {
162
+ if (isServer) {
163
+ return <>{props.children}</>
164
+ }
165
+
166
+ let startRef: HTMLSpanElement | undefined
167
+ let endRef: HTMLSpanElement | undefined
168
+ const [scopeElements, setScopeElements] = createSignal<Element[]>([])
169
+
170
+ // Store the element that was focused when the scope mounted
171
+ let nodeToRestore: Element | null = null
172
+
173
+ // Create focus manager
174
+ const focusManager: FocusManager = {
175
+ focusNext(opts = {}) {
176
+ const scope = scopeElements()
177
+ if (scope.length === 0) return null
178
+
179
+ const { from, tabbable = true, wrap = false, accept } = opts
180
+ const elements = getFocusableElements(scope, tabbable).filter(el => !accept || accept(el))
181
+ const doc = getOwnerDocument(scope[0])
182
+ const current = from || getActiveElement(doc)
183
+
184
+ if (!current || elements.length === 0) return null
185
+
186
+ const currentIndex = elements.indexOf(current as HTMLElement)
187
+ let nextIndex = currentIndex + 1
188
+
189
+ if (nextIndex >= elements.length) {
190
+ if (wrap) {
191
+ nextIndex = 0
192
+ } else {
193
+ return null
194
+ }
195
+ }
196
+
197
+ const nextElement = elements[nextIndex]
198
+ if (nextElement) {
199
+ focusSafely(nextElement)
200
+ return nextElement
201
+ }
202
+
203
+ return null
204
+ },
205
+
206
+ focusPrevious(opts = {}) {
207
+ const scope = scopeElements()
208
+ if (scope.length === 0) return null
209
+
210
+ const { from, tabbable = true, wrap = false, accept } = opts
211
+ const elements = getFocusableElements(scope, tabbable).filter(el => !accept || accept(el))
212
+ const doc = getOwnerDocument(scope[0])
213
+ const current = from || getActiveElement(doc)
214
+
215
+ if (!current || elements.length === 0) return null
216
+
217
+ const currentIndex = elements.indexOf(current as HTMLElement)
218
+ let prevIndex = currentIndex - 1
219
+
220
+ if (prevIndex < 0) {
221
+ if (wrap) {
222
+ prevIndex = elements.length - 1
223
+ } else {
224
+ return null
225
+ }
226
+ }
227
+
228
+ const prevElement = elements[prevIndex]
229
+ if (prevElement) {
230
+ focusSafely(prevElement)
231
+ return prevElement
232
+ }
233
+
234
+ return null
235
+ },
236
+
237
+ focusFirst(opts = {}) {
238
+ const scope = scopeElements()
239
+ if (scope.length === 0) return null
240
+
241
+ const { tabbable = true, accept } = opts
242
+ const elements = getFocusableElements(scope, tabbable).filter(el => !accept || accept(el))
243
+
244
+ if (elements.length > 0) {
245
+ focusSafely(elements[0])
246
+ return elements[0]
247
+ }
248
+
249
+ return null
250
+ },
251
+
252
+ focusLast(opts = {}) {
253
+ const scope = scopeElements()
254
+ if (scope.length === 0) return null
255
+
256
+ const { tabbable = true, accept } = opts
257
+ const elements = getFocusableElements(scope, tabbable).filter(el => !accept || accept(el))
258
+
259
+ if (elements.length > 0) {
260
+ const lastElement = elements[elements.length - 1]
261
+ focusSafely(lastElement)
262
+ return lastElement
263
+ }
264
+
265
+ return null
266
+ },
267
+ }
268
+
269
+ // Collect scope elements after render
270
+ onMount(() => {
271
+ if (!startRef || !endRef) return
272
+
273
+ const nodes: Element[] = []
274
+ let node = startRef.nextSibling
275
+ while (node && node !== endRef) {
276
+ if (node.nodeType === Node.ELEMENT_NODE) {
277
+ nodes.push(node as Element)
278
+ }
279
+ node = node.nextSibling
280
+ }
281
+ setScopeElements(nodes)
282
+ })
283
+
284
+ // Save the currently focused element for restoration (must happen before autoFocus/contain effects run).
285
+ onMount(() => {
286
+ if (!props.restoreFocus) return
287
+
288
+ // Focus can be in the main document, or inside this iframe's document.
289
+ const scopeDoc = startRef ? getOwnerDocument(startRef) : document
290
+ const scopeActive = getActiveElement(scopeDoc)
291
+ const topActive = getActiveElement(document)
292
+
293
+ // If the scope is in an iframe and that iframe is currently focused, prefer the iframe document's active element.
294
+ if (
295
+ scopeDoc !== document &&
296
+ document.activeElement instanceof HTMLIFrameElement &&
297
+ document.activeElement.contentDocument === scopeDoc &&
298
+ scopeActive &&
299
+ scopeActive !== scopeDoc.body
300
+ ) {
301
+ nodeToRestore = scopeActive
302
+ return
303
+ }
304
+
305
+ nodeToRestore = topActive
306
+ })
307
+
308
+ // Auto-focus first element
309
+ createEffect(() => {
310
+ if (!props.autoFocus) return
311
+
312
+ const scope = scopeElements()
313
+ if (scope.length === 0) return
314
+
315
+ const doc = getOwnerDocument(scope[0])
316
+ const activeElement = getActiveElement(doc)
317
+
318
+ // Only auto-focus if focus is not already inside the scope
319
+ if (!isElementInScope(activeElement, scope)) {
320
+ focusManager.focusFirst()
321
+ }
322
+ })
323
+
324
+ // Focus containment
325
+ createEffect(() => {
326
+ if (!props.contain) return
327
+
328
+ const scope = scopeElements()
329
+ if (scope.length === 0) return
330
+
331
+ const doc = getOwnerDocument(scope[0])
332
+ let focusedNode: Element | null = null
333
+
334
+ const onKeyDown = (e: KeyboardEvent) => {
335
+ if (e.key !== 'Tab' || e.altKey || e.ctrlKey || e.metaKey) {
336
+ return
337
+ }
338
+
339
+ const scope = scopeElements()
340
+ const activeElement = getActiveElement(doc)
341
+ if (!isElementInScope(activeElement, scope)) {
342
+ return
343
+ }
344
+
345
+ const elements = getFocusableElements(scope, true)
346
+ if (elements.length === 0) return
347
+
348
+ const firstElement = elements[0]
349
+ const lastElement = elements[elements.length - 1]
350
+
351
+ if (e.shiftKey && activeElement === firstElement) {
352
+ e.preventDefault()
353
+ focusSafely(lastElement)
354
+ } else if (!e.shiftKey && activeElement === lastElement) {
355
+ e.preventDefault()
356
+ focusSafely(firstElement)
357
+ }
358
+ }
359
+
360
+ const onFocusIn = (e: FocusEvent) => {
361
+ const scope = scopeElements()
362
+ const target = e.target as Element
363
+
364
+ if (isElementInScope(target, scope)) {
365
+ focusedNode = target
366
+ } else if (focusedNode) {
367
+ // Focus escaped the scope, bring it back
368
+ focusSafely(focusedNode as HTMLElement)
369
+ } else {
370
+ // No previous focus, focus first element
371
+ focusManager.focusFirst()
372
+ }
373
+ }
374
+
375
+ doc.addEventListener('keydown', onKeyDown, true)
376
+ doc.addEventListener('focusin', onFocusIn, true)
377
+
378
+ onCleanup(() => {
379
+ doc.removeEventListener('keydown', onKeyDown, true)
380
+ doc.removeEventListener('focusin', onFocusIn, true)
381
+ })
382
+ })
383
+
384
+ // Restore focus on unmount
385
+ onCleanup(() => {
386
+ if (props.restoreFocus && nodeToRestore && (nodeToRestore as HTMLElement).focus) {
387
+ const doc = getOwnerDocument(nodeToRestore as Element)
388
+ const win = doc.defaultView ?? window
389
+
390
+ // Use requestAnimationFrame to ensure the element is still in the DOM
391
+ win.requestAnimationFrame(() => {
392
+ if (nodeToRestore && doc.body.contains(nodeToRestore as Node)) {
393
+ ;(nodeToRestore as HTMLElement).focus()
394
+ }
395
+ })
396
+ }
397
+ })
398
+
399
+ return (
400
+ <FocusScopeContext.Provider value={{ focusManager, scopeRef: scopeElements }}>
401
+ <span data-focus-scope-start hidden ref={startRef} />
402
+ {props.children}
403
+ <span data-focus-scope-end hidden ref={endRef} />
404
+ </FocusScopeContext.Provider>
405
+ )
406
+ }
407
+
408
+ export default FocusScope
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Auto-focus management for solidaria
3
+ *
4
+ * Provides priority-based auto-focus with deferred execution
5
+ * and conflict resolution for multiple auto-focus elements.
6
+ */
7
+
8
+ import { createEffect, onCleanup, onMount } from 'solid-js';
9
+ import { isServer } from 'solid-js/web';
10
+ import { focusSafely } from '../utils/focus';
11
+
12
+ // ============================================
13
+ // TYPES
14
+ // ============================================
15
+
16
+ export interface AutoFocusOptions {
17
+ /**
18
+ * Whether auto-focus is enabled.
19
+ * @default true
20
+ */
21
+ isEnabled?: boolean;
22
+ /**
23
+ * Priority level (higher = more important).
24
+ * When multiple elements request auto-focus, the highest priority wins.
25
+ * @default 0
26
+ */
27
+ priority?: number;
28
+ /**
29
+ * Delay in milliseconds before focusing.
30
+ * Useful for animations or transitions.
31
+ * @default 0
32
+ */
33
+ delay?: number;
34
+ /**
35
+ * Whether to focus even if another element is already focused.
36
+ * @default false
37
+ */
38
+ force?: boolean;
39
+ /**
40
+ * Whether to prevent scrolling when focusing.
41
+ * @default true
42
+ */
43
+ preventScroll?: boolean;
44
+ /**
45
+ * Callback when focus is applied.
46
+ */
47
+ onFocus?: (element: HTMLElement) => void;
48
+ /**
49
+ * Callback when focus is skipped (due to lower priority or other reasons).
50
+ */
51
+ onSkip?: () => void;
52
+ }
53
+
54
+ export interface AutoFocusResult {
55
+ /**
56
+ * Manually trigger the auto-focus.
57
+ */
58
+ focus: () => void;
59
+ /**
60
+ * Cancel any pending auto-focus.
61
+ */
62
+ cancel: () => void;
63
+ }
64
+
65
+ // ============================================
66
+ // AUTO-FOCUS QUEUE
67
+ // ============================================
68
+
69
+ interface QueuedFocus {
70
+ ref: () => HTMLElement | null | undefined;
71
+ priority: number;
72
+ delay: number;
73
+ force: boolean;
74
+ preventScroll: boolean;
75
+ onFocus?: (element: HTMLElement) => void;
76
+ onSkip?: () => void;
77
+ }
78
+
79
+ // Global queue for managing auto-focus requests
80
+ let autoFocusQueue: QueuedFocus[] = [];
81
+ let processingTimeout: ReturnType<typeof setTimeout> | null = null;
82
+
83
+ /**
84
+ * Process the auto-focus queue and focus the highest priority element.
85
+ */
86
+ function processAutoFocusQueue(): void {
87
+ if (processingTimeout) {
88
+ clearTimeout(processingTimeout);
89
+ processingTimeout = null;
90
+ }
91
+
92
+ if (autoFocusQueue.length === 0) return;
93
+
94
+ // Sort by priority (highest first)
95
+ autoFocusQueue.sort((a, b) => b.priority - a.priority);
96
+
97
+ // Get the highest priority item
98
+ const winner = autoFocusQueue[0];
99
+ const losers = autoFocusQueue.slice(1);
100
+
101
+ // Clear the queue
102
+ autoFocusQueue = [];
103
+
104
+ // Notify losers
105
+ for (const loser of losers) {
106
+ loser.onSkip?.();
107
+ }
108
+
109
+ // Focus the winner
110
+ const element = winner.ref();
111
+ if (!element) {
112
+ winner.onSkip?.();
113
+ return;
114
+ }
115
+
116
+ // Check if we should focus
117
+ const activeElement = document.activeElement;
118
+ const shouldFocus =
119
+ winner.force ||
120
+ !activeElement ||
121
+ activeElement === document.body ||
122
+ activeElement === document.documentElement;
123
+
124
+ if (!shouldFocus) {
125
+ winner.onSkip?.();
126
+ return;
127
+ }
128
+
129
+ // Apply focus with optional delay
130
+ if (winner.delay > 0) {
131
+ setTimeout(() => {
132
+ const el = winner.ref();
133
+ if (el && document.body.contains(el)) {
134
+ if (winner.preventScroll) {
135
+ focusSafely(el);
136
+ } else {
137
+ el.focus();
138
+ }
139
+ winner.onFocus?.(el);
140
+ }
141
+ }, winner.delay);
142
+ } else {
143
+ if (winner.preventScroll) {
144
+ focusSafely(element);
145
+ } else {
146
+ element.focus();
147
+ }
148
+ winner.onFocus?.(element);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Queue an element for auto-focus.
154
+ */
155
+ function queueAutoFocus(item: QueuedFocus): void {
156
+ autoFocusQueue.push(item);
157
+
158
+ // Schedule processing on next frame to allow all components to register
159
+ if (processingTimeout === null) {
160
+ processingTimeout = setTimeout(processAutoFocusQueue, 0);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Remove an item from the auto-focus queue.
166
+ */
167
+ function removeFromQueue(ref: () => HTMLElement | null | undefined): void {
168
+ autoFocusQueue = autoFocusQueue.filter((item) => item.ref !== ref);
169
+ }
170
+
171
+ // ============================================
172
+ // HOOK
173
+ // ============================================
174
+
175
+ /**
176
+ * Creates auto-focus behavior for an element.
177
+ *
178
+ * This hook registers the element for auto-focus when mounted. If multiple
179
+ * elements request auto-focus, the one with the highest priority wins.
180
+ *
181
+ * @param ref - Accessor for the element to focus
182
+ * @param options - Auto-focus options
183
+ *
184
+ * @example
185
+ * ```tsx
186
+ * function Dialog(props) {
187
+ * let contentRef: HTMLDivElement | undefined;
188
+ *
189
+ * createAutoFocus(() => contentRef, {
190
+ * priority: 10, // High priority for dialogs
191
+ * onFocus: () => console.log('Dialog focused'),
192
+ * });
193
+ *
194
+ * return (
195
+ * <div ref={contentRef} tabIndex={-1}>
196
+ * {props.children}
197
+ * </div>
198
+ * );
199
+ * }
200
+ * ```
201
+ *
202
+ * @example
203
+ * ```tsx
204
+ * // With delay for animations
205
+ * function AnimatedPanel() {
206
+ * let panelRef: HTMLDivElement | undefined;
207
+ *
208
+ * createAutoFocus(() => panelRef, {
209
+ * delay: 300, // Wait for animation
210
+ * });
211
+ *
212
+ * return <div ref={panelRef} class="animated-panel">...</div>;
213
+ * }
214
+ * ```
215
+ *
216
+ * @example
217
+ * ```tsx
218
+ * // Conditional auto-focus
219
+ * function Input(props) {
220
+ * let inputRef: HTMLInputElement | undefined;
221
+ *
222
+ * createAutoFocus(() => inputRef, {
223
+ * isEnabled: props.autoFocus,
224
+ * });
225
+ *
226
+ * return <input ref={inputRef} />;
227
+ * }
228
+ * ```
229
+ */
230
+ export function createAutoFocus(
231
+ ref: () => HTMLElement | null | undefined,
232
+ options: AutoFocusOptions = {}
233
+ ): AutoFocusResult {
234
+ const {
235
+ isEnabled = true,
236
+ priority = 0,
237
+ delay = 0,
238
+ force = false,
239
+ preventScroll = true,
240
+ onFocus,
241
+ onSkip,
242
+ } = options;
243
+
244
+ // During SSR, return no-op functions
245
+ if (isServer) {
246
+ return {
247
+ focus: () => {},
248
+ cancel: () => {},
249
+ };
250
+ }
251
+
252
+ let canceled = false;
253
+
254
+ // Queue auto-focus on mount
255
+ onMount(() => {
256
+ if (!isEnabled || canceled) return;
257
+
258
+ queueAutoFocus({
259
+ ref,
260
+ priority,
261
+ delay,
262
+ force,
263
+ preventScroll,
264
+ onFocus,
265
+ onSkip,
266
+ });
267
+ });
268
+
269
+ // Remove from queue on cleanup
270
+ onCleanup(() => {
271
+ removeFromQueue(ref);
272
+ });
273
+
274
+ const focus = (): void => {
275
+ if (canceled) return;
276
+
277
+ const element = ref();
278
+ if (!element) return;
279
+
280
+ if (preventScroll) {
281
+ focusSafely(element);
282
+ } else {
283
+ element.focus();
284
+ }
285
+ onFocus?.(element);
286
+ };
287
+
288
+ const cancel = (): void => {
289
+ canceled = true;
290
+ removeFromQueue(ref);
291
+ };
292
+
293
+ return {
294
+ focus,
295
+ cancel,
296
+ };
297
+ }
298
+
299
+ // ============================================
300
+ // UTILITIES
301
+ // ============================================
302
+
303
+ /**
304
+ * Clears all pending auto-focus requests.
305
+ * Useful for testing or when navigating away.
306
+ */
307
+ export function clearAutoFocusQueue(): void {
308
+ if (processingTimeout) {
309
+ clearTimeout(processingTimeout);
310
+ processingTimeout = null;
311
+ }
312
+ autoFocusQueue = [];
313
+ }
314
+
315
+ /**
316
+ * Gets the current auto-focus queue length.
317
+ * Useful for debugging.
318
+ */
319
+ export function getAutoFocusQueueLength(): number {
320
+ return autoFocusQueue.length;
321
+ }