@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.
- package/LICENSE +21 -0
- package/dist/actiongroup/createActionGroup.d.ts +29 -0
- package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
- package/dist/actiongroup/index.d.ts +2 -0
- package/dist/actiongroup/index.d.ts.map +1 -0
- package/dist/autocomplete/createAutocomplete.d.ts +6 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
- package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
- package/dist/button/createToggleButtonGroup.d.ts +32 -0
- package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
- package/dist/button/index.d.ts +2 -0
- package/dist/button/index.d.ts.map +1 -1
- package/dist/calendar/createCalendarCell.d.ts +2 -0
- package/dist/calendar/createCalendarCell.d.ts.map +1 -1
- package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
- package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
- package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
- package/dist/collections/index.d.ts +56 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/color/createColorArea.d.ts.map +1 -1
- package/dist/color/createColorSlider.d.ts.map +1 -1
- package/dist/color/createColorWheel.d.ts.map +1 -1
- package/dist/combobox/createComboBox.d.ts +6 -0
- package/dist/combobox/createComboBox.d.ts.map +1 -1
- package/dist/datepicker/createDatePicker.d.ts +6 -0
- package/dist/datepicker/createDatePicker.d.ts.map +1 -1
- package/dist/datepicker/createDateRangePicker.d.ts +40 -0
- package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
- package/dist/datepicker/createDateSegment.d.ts +1 -1
- package/dist/datepicker/createDateSegment.d.ts.map +1 -1
- package/dist/datepicker/createTimeSegment.d.ts +29 -0
- package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
- package/dist/datepicker/index.d.ts +2 -0
- package/dist/datepicker/index.d.ts.map +1 -1
- package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
- package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
- package/dist/dnd/createDrag.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollection.d.ts +4 -0
- package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
- package/dist/dnd/createDraggableItem.d.ts.map +1 -1
- package/dist/dnd/createDrop.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollection.d.ts +32 -1
- package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
- package/dist/dnd/createDroppableItem.d.ts.map +1 -1
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/index.d.ts.map +1 -1
- package/dist/grid/createGrid.d.ts.map +1 -1
- package/dist/gridlist/createGridList.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4659 -3452
- package/dist/index.js.map +1 -7
- package/dist/index.ssr.js +4659 -3452
- package/dist/index.ssr.js.map +1 -7
- package/dist/interactions/createFocus.d.ts.map +1 -1
- package/dist/interactions/createFocusWithin.d.ts.map +1 -1
- package/dist/link/createLink.d.ts +10 -0
- package/dist/link/createLink.d.ts.map +1 -1
- package/dist/listbox/createListBox.d.ts +1 -0
- package/dist/listbox/createListBox.d.ts.map +1 -1
- package/dist/listbox/createOption.d.ts.map +1 -1
- package/dist/menu/createMenu.d.ts +1 -0
- package/dist/menu/createMenu.d.ts.map +1 -1
- package/dist/meter/createMeter.d.ts.map +1 -1
- package/dist/numberfield/createNumberField.d.ts +18 -0
- package/dist/numberfield/createNumberField.d.ts.map +1 -1
- package/dist/overlays/createModal.d.ts +16 -0
- package/dist/overlays/createModal.d.ts.map +1 -1
- package/dist/overlays/createOverlay.d.ts.map +1 -1
- package/dist/overlays/index.d.ts +1 -1
- package/dist/overlays/index.d.ts.map +1 -1
- package/dist/popover/createOverlayPosition.d.ts.map +1 -1
- package/dist/popover/createPopover.d.ts.map +1 -1
- package/dist/progress/createProgressBar.d.ts.map +1 -1
- package/dist/radio/createRadioGroup.d.ts +2 -2
- package/dist/radio/createRadioGroup.d.ts.map +1 -1
- package/dist/searchfield/createSearchField.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/select/createSelect.d.ts.map +1 -1
- package/dist/slider/createSlider.d.ts.map +1 -1
- package/dist/table/createTable.d.ts.map +1 -1
- package/dist/tabs/createTabs.d.ts +1 -1
- package/dist/tabs/createTabs.d.ts.map +1 -1
- package/dist/tag/createTag.d.ts.map +1 -1
- package/dist/tag/createTagGroup.d.ts.map +1 -1
- package/dist/toast/createToast.d.ts +4 -0
- package/dist/toast/createToast.d.ts.map +1 -1
- package/dist/toast/createToastRegion.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/dist/tree/createTree.d.ts.map +1 -1
- package/dist/tree/createTreeItem.d.ts.map +1 -1
- package/dist/tree/types.d.ts +4 -0
- package/dist/tree/types.d.ts.map +1 -1
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/platform.d.ts.map +1 -1
- package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/actiongroup/createActionGroup.ts +324 -0
- package/src/actiongroup/index.ts +8 -0
- package/src/autocomplete/createAutocomplete.ts +32 -9
- package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
- package/src/button/createButton.ts +1 -1
- package/src/button/createToggleButtonGroup.ts +128 -0
- package/src/button/index.ts +9 -0
- package/src/calendar/createCalendarCell.ts +6 -4
- package/src/calendar/createCalendarGrid.ts +27 -18
- package/src/calendar/createRangeCalendarCell.ts +26 -9
- package/src/checkbox/createCheckboxGroup.ts +21 -4
- package/src/collections/index.ts +242 -0
- package/src/color/createColorArea.ts +380 -314
- package/src/color/createColorField.ts +137 -137
- package/src/color/createColorSlider.ts +286 -197
- package/src/color/createColorSwatch.ts +40 -40
- package/src/color/createColorWheel.ts +218 -208
- package/src/color/index.ts +24 -24
- package/src/color/types.ts +116 -116
- package/src/combobox/createComboBox.ts +670 -647
- package/src/combobox/index.ts +6 -6
- package/src/datepicker/createDatePicker.ts +54 -16
- package/src/datepicker/createDateRangePicker.ts +246 -0
- package/src/datepicker/createDateSegment.ts +185 -31
- package/src/datepicker/createTimeSegment.ts +370 -0
- package/src/datepicker/index.ts +14 -0
- package/src/dialog/createDialog.ts +120 -120
- package/src/dialog/index.ts +2 -2
- package/src/dialog/types.ts +19 -19
- package/src/disclosure/createDisclosureGroup.ts +5 -2
- package/src/dnd/createDrag.ts +224 -209
- package/src/dnd/createDraggableCollection.ts +96 -63
- package/src/dnd/createDraggableItem.ts +259 -243
- package/src/dnd/createDrop.ts +322 -321
- package/src/dnd/createDroppableCollection.ts +682 -293
- package/src/dnd/createDroppableItem.ts +215 -213
- package/src/dnd/index.ts +55 -47
- package/src/dnd/types.ts +89 -89
- package/src/dnd/utils.ts +294 -294
- package/src/focus/createAutoFocus.ts +321 -321
- package/src/focus/createFocusRestore.ts +313 -313
- package/src/focus/createVirtualFocus.ts +396 -396
- package/src/form/createFormValidation.ts +224 -224
- package/src/form/index.ts +11 -11
- package/src/grid/createGrid.ts +3 -1
- package/src/gridlist/createGridList.ts +16 -0
- package/src/gridlist/createGridListItem.ts +1 -1
- package/src/i18n/NumberFormatter.ts +266 -266
- package/src/i18n/createCollator.ts +79 -79
- package/src/i18n/createDateFormatter.ts +83 -83
- package/src/i18n/createFilter.ts +131 -131
- package/src/i18n/createNumberFormatter.ts +52 -52
- package/src/i18n/index.ts +40 -40
- package/src/i18n/locale.tsx +188 -188
- package/src/i18n/utils.ts +99 -99
- package/src/index.ts +51 -0
- package/src/interactions/createFocus.ts +6 -5
- package/src/interactions/createFocusWithin.ts +6 -5
- package/src/interactions/createLongPress.ts +174 -174
- package/src/interactions/createMove.ts +289 -289
- package/src/interactions/createPress.ts +5 -5
- package/src/landmark/createLandmark.ts +377 -377
- package/src/landmark/index.ts +8 -8
- package/src/link/createLink.ts +23 -8
- package/src/listbox/createListBox.ts +308 -269
- package/src/listbox/createOption.ts +162 -151
- package/src/listbox/index.ts +12 -12
- package/src/live-announcer/announce.ts +322 -322
- package/src/live-announcer/index.ts +9 -9
- package/src/menu/createMenu.ts +405 -396
- package/src/menu/createMenuItem.ts +149 -149
- package/src/menu/createMenuTrigger.ts +88 -88
- package/src/menu/index.ts +18 -18
- package/src/meter/createMeter.ts +1 -6
- package/src/numberfield/createNumberField.ts +311 -268
- package/src/numberfield/index.ts +5 -5
- package/src/overlays/ariaHideOutside.ts +219 -219
- package/src/overlays/createInteractOutside.ts +149 -149
- package/src/overlays/createModal.tsx +238 -202
- package/src/overlays/createOverlay.ts +165 -155
- package/src/overlays/createOverlayTrigger.ts +85 -85
- package/src/overlays/createPreventScroll.ts +266 -266
- package/src/overlays/index.ts +48 -44
- package/src/popover/calculatePosition.ts +6 -6
- package/src/popover/createOverlayPosition.ts +7 -4
- package/src/popover/createPopover.ts +21 -7
- package/src/progress/createProgressBar.ts +6 -1
- package/src/radio/createRadioGroup.ts +88 -14
- package/src/searchfield/createSearchField.ts +241 -186
- package/src/searchfield/index.ts +2 -2
- package/src/select/createHiddenSelect.tsx +263 -236
- package/src/select/createSelect.ts +373 -395
- package/src/select/index.ts +14 -14
- package/src/slider/createSlider.ts +364 -349
- package/src/slider/index.ts +2 -2
- package/src/ssr/index.tsx +370 -370
- package/src/table/createTable.ts +3 -1
- package/src/table/createTableColumnHeader.ts +1 -1
- package/src/table/createTableRow.ts +1 -1
- package/src/tabs/createTabs.ts +80 -51
- package/src/tag/createTag.ts +135 -6
- package/src/tag/createTagGroup.ts +7 -2
- package/src/toast/createToast.ts +8 -2
- package/src/toast/createToastRegion.ts +0 -1
- package/src/toolbar/createToolbar.ts +75 -1
- package/src/tooltip/createTooltip.ts +79 -79
- package/src/tooltip/createTooltipTrigger.ts +226 -222
- package/src/tooltip/index.ts +6 -6
- package/src/tree/createTree.ts +261 -246
- package/src/tree/createTreeItem.ts +282 -233
- package/src/tree/createTreeSelectionCheckbox.ts +68 -68
- package/src/tree/index.ts +16 -16
- package/src/tree/types.ts +91 -87
- package/src/utils/env.ts +55 -54
- package/src/utils/platform.ts +16 -6
- package/src/visually-hidden/createVisuallyHidden.ts +139 -124
- package/src/visually-hidden/index.ts +6 -6
|
@@ -1,321 +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
|
-
}
|
|
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
|
+
}
|