@versini/ui-hooks 5.3.1 → 5.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -33
- package/dist/index.js +4 -159
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -55,39 +55,6 @@ export declare interface StorageProperties<T> {
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function useClickOutside<T extends HTMLElement = any>(handler: () => void, events?: string[] | null, nodes?: (HTMLElement | null)[]): RefObject<T | null>;
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* Custom hook for trapping focus within a container element. Implements W3C
|
|
60
|
-
* WAI-ARIA dialog focus management patterns:
|
|
61
|
-
* - Tab/Shift+Tab cycles through focusable elements within the container
|
|
62
|
-
* - Focus is trapped and cannot escape to elements outside
|
|
63
|
-
* - Initial focus is set based on the initialFocus option
|
|
64
|
-
*
|
|
65
|
-
* @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
66
|
-
*
|
|
67
|
-
*/
|
|
68
|
-
export declare function useFocusTrap({ enabled, initialFocus, }: UseFocusTrapOptions): UseFocusTrapReturn;
|
|
69
|
-
|
|
70
|
-
declare interface UseFocusTrapOptions {
|
|
71
|
-
/**
|
|
72
|
-
* Whether the focus trap is active.
|
|
73
|
-
*/
|
|
74
|
-
enabled: boolean;
|
|
75
|
-
/**
|
|
76
|
-
* Which element to initially focus when the trap activates. Can be a number
|
|
77
|
-
* (tabbable index, 0 = first), a ref to an element, or -1 to disable initial
|
|
78
|
-
* focus.
|
|
79
|
-
* @default 0
|
|
80
|
-
*/
|
|
81
|
-
initialFocus?: number | React.RefObject<HTMLElement | null>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
declare interface UseFocusTrapReturn {
|
|
85
|
-
/**
|
|
86
|
-
* Ref to attach to the container element that should trap focus.
|
|
87
|
-
*/
|
|
88
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
58
|
/**
|
|
92
59
|
* Custom hook providing imperative haptic feedback for mobile devices. Uses
|
|
93
60
|
* navigator.vibrate when available, falls back to iOS switch element trick for
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-hooks v5.3.
|
|
2
|
+
@versini/ui-hooks v5.3.2
|
|
3
3
|
© 2025 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
try {
|
|
6
6
|
if (!window.__VERSINI_UI_HOOKS__) {
|
|
7
7
|
window.__VERSINI_UI_HOOKS__ = {
|
|
8
|
-
version: "5.3.
|
|
9
|
-
buildTime: "12/
|
|
8
|
+
version: "5.3.2",
|
|
9
|
+
buildTime: "12/16/2025 01:48 PM EST",
|
|
10
10
|
homepage: "https://www.npmjs.com/package/@versini/ui-hooks",
|
|
11
11
|
license: "MIT",
|
|
12
12
|
};
|
|
@@ -65,160 +65,6 @@ const DEFAULT_EVENTS = [
|
|
|
65
65
|
return ref;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
;// CONCATENATED MODULE: ./src/hooks/useFocusTrap.ts
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Selector for all focusable elements within a container. Based on W3C WAI-ARIA
|
|
72
|
-
* practices for dialog focus management.
|
|
73
|
-
*/ const FOCUSABLE_SELECTOR = [
|
|
74
|
-
'a[href]:not([disabled]):not([tabindex="-1"])',
|
|
75
|
-
'button:not([disabled]):not([tabindex="-1"])',
|
|
76
|
-
'textarea:not([disabled]):not([tabindex="-1"])',
|
|
77
|
-
'input:not([disabled]):not([tabindex="-1"])',
|
|
78
|
-
'select:not([disabled]):not([tabindex="-1"])',
|
|
79
|
-
'[tabindex]:not([tabindex="-1"]):not([disabled])',
|
|
80
|
-
'audio[controls]:not([tabindex="-1"])',
|
|
81
|
-
'video[controls]:not([tabindex="-1"])',
|
|
82
|
-
'details:not([tabindex="-1"])'
|
|
83
|
-
].join(", ");
|
|
84
|
-
/**
|
|
85
|
-
* Custom hook for trapping focus within a container element. Implements W3C
|
|
86
|
-
* WAI-ARIA dialog focus management patterns:
|
|
87
|
-
* - Tab/Shift+Tab cycles through focusable elements within the container
|
|
88
|
-
* - Focus is trapped and cannot escape to elements outside
|
|
89
|
-
* - Initial focus is set based on the initialFocus option
|
|
90
|
-
*
|
|
91
|
-
* @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
92
|
-
*
|
|
93
|
-
*/ function useFocusTrap({ enabled, initialFocus = 0 }) {
|
|
94
|
-
const containerRef = useRef(null);
|
|
95
|
-
const previouslyFocusedRef = useRef(null);
|
|
96
|
-
/**
|
|
97
|
-
* Get all focusable elements within the container.
|
|
98
|
-
*/ const getFocusableElements = useCallback(()=>{
|
|
99
|
-
/* c8 ignore next 3 - defensive check, containerRef is always set when enabled */ if (!containerRef.current) {
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
const elements = containerRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
103
|
-
return Array.from(elements).filter((el)=>el.offsetParent !== null);
|
|
104
|
-
}, []);
|
|
105
|
-
/**
|
|
106
|
-
* Focus a specific element by index, or the element referenced by a ref.
|
|
107
|
-
*/ const focusElement = useCallback((target)=>{
|
|
108
|
-
if (typeof target === "number") {
|
|
109
|
-
if (target === -1) {
|
|
110
|
-
// -1 means don't focus anything.
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const focusableElements = getFocusableElements();
|
|
114
|
-
if (focusableElements.length > 0) {
|
|
115
|
-
const index = Math.min(target, focusableElements.length - 1);
|
|
116
|
-
focusableElements[index]?.focus();
|
|
117
|
-
}
|
|
118
|
-
} else if (target?.current) {
|
|
119
|
-
target.current.focus();
|
|
120
|
-
}
|
|
121
|
-
}, [
|
|
122
|
-
getFocusableElements
|
|
123
|
-
]);
|
|
124
|
-
/**
|
|
125
|
-
* Handle keyboard events for focus trapping. Tab cycles forward, Shift+Tab
|
|
126
|
-
* cycles backward.
|
|
127
|
-
*/ const handleKeyDown = useCallback((event)=>{
|
|
128
|
-
if (event.key !== "Tab" || !containerRef.current) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const activeElement = document.activeElement;
|
|
132
|
-
// If focus is in a nested dialog or menu, let that element handle Tab.
|
|
133
|
-
if (activeElement && !containerRef.current.contains(activeElement) && activeElement.closest('[role="dialog"], [role="menu"]')) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const focusableElements = getFocusableElements();
|
|
137
|
-
if (focusableElements.length === 0) {
|
|
138
|
-
event.preventDefault();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const firstElement = focusableElements[0];
|
|
142
|
-
const lastElement = focusableElements[focusableElements.length - 1];
|
|
143
|
-
// Shift+Tab from first element -> go to last.
|
|
144
|
-
if (event.shiftKey && activeElement === firstElement) {
|
|
145
|
-
event.preventDefault();
|
|
146
|
-
lastElement?.focus();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
// Tab from last element -> go to first.
|
|
150
|
-
if (!event.shiftKey && activeElement === lastElement) {
|
|
151
|
-
event.preventDefault();
|
|
152
|
-
firstElement?.focus();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
/* c8 ignore next 9 - defensive check for focus escaping, hard to trigger in tests */ // If focus is outside the container, bring it back.
|
|
156
|
-
if (!containerRef.current.contains(activeElement)) {
|
|
157
|
-
event.preventDefault();
|
|
158
|
-
if (event.shiftKey) {
|
|
159
|
-
lastElement?.focus();
|
|
160
|
-
} else {
|
|
161
|
-
firstElement?.focus();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}, [
|
|
165
|
-
getFocusableElements
|
|
166
|
-
]);
|
|
167
|
-
/**
|
|
168
|
-
* Handle focus events to ensure focus stays within the container. This catches
|
|
169
|
-
* focus that escapes via mouse clicks or other means.
|
|
170
|
-
*/ const handleFocusIn = useCallback((event)=>{
|
|
171
|
-
if (!containerRef.current || containerRef.current.contains(event.target)) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Allow focus to go to nested dialogs (child panels) or menus (dropdown menus).
|
|
175
|
-
const target = event.target;
|
|
176
|
-
if (target?.closest('[role="dialog"], [role="menu"]')) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Focus escaped the container, bring it back.
|
|
180
|
-
const focusableElements = getFocusableElements();
|
|
181
|
-
if (focusableElements.length > 0) {
|
|
182
|
-
focusableElements[0]?.focus();
|
|
183
|
-
}
|
|
184
|
-
}, [
|
|
185
|
-
getFocusableElements
|
|
186
|
-
]);
|
|
187
|
-
// Set up focus trap when enabled.
|
|
188
|
-
useEffect(()=>{
|
|
189
|
-
if (!enabled) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
// Store the currently focused element to restore later.
|
|
193
|
-
previouslyFocusedRef.current = document.activeElement;
|
|
194
|
-
// Set initial focus after a small delay to ensure the DOM is ready.
|
|
195
|
-
const focusTimer = setTimeout(()=>{
|
|
196
|
-
focusElement(initialFocus);
|
|
197
|
-
}, 0);
|
|
198
|
-
// Add event listeners for focus trapping.
|
|
199
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
200
|
-
document.addEventListener("focusin", handleFocusIn);
|
|
201
|
-
return ()=>{
|
|
202
|
-
clearTimeout(focusTimer);
|
|
203
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
204
|
-
document.removeEventListener("focusin", handleFocusIn);
|
|
205
|
-
// Restore focus to the previously focused element if it's still in the DOM.
|
|
206
|
-
if (previouslyFocusedRef.current?.isConnected) {
|
|
207
|
-
previouslyFocusedRef.current.focus();
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
}, [
|
|
211
|
-
enabled,
|
|
212
|
-
initialFocus,
|
|
213
|
-
focusElement,
|
|
214
|
-
handleKeyDown,
|
|
215
|
-
handleFocusIn
|
|
216
|
-
]);
|
|
217
|
-
return {
|
|
218
|
-
containerRef
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
68
|
;// CONCATENATED MODULE: ./src/hooks/useHaptic.ts
|
|
223
69
|
|
|
224
70
|
const HAPTIC_DURATION_MS = 50;
|
|
@@ -1109,5 +955,4 @@ function useWindowEvent(type, listener) {
|
|
|
1109
955
|
|
|
1110
956
|
|
|
1111
957
|
|
|
1112
|
-
|
|
1113
|
-
export { getHotkeyHandler, shouldFireEvent, useClickOutside, useFocusTrap, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|
|
958
|
+
export { getHotkeyHandler, shouldFireEvent, useClickOutside, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-hooks",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"test:watch": "vitest",
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "ef4323fa1f14f9f2a5471d71f063519230762aec"
|
|
40
40
|
}
|