@negative-space/roving-focus 1.1.0 → 1.2.0
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.mts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +97 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface Item {
|
|
1
|
+
interface RovingFocusItem {
|
|
4
2
|
id: string;
|
|
5
3
|
ref: React.RefObject<HTMLElement>;
|
|
6
4
|
disabled?: boolean;
|
|
7
5
|
}
|
|
8
|
-
|
|
6
|
+
interface UseRovingFocusOptions {
|
|
7
|
+
wrap?: boolean;
|
|
8
|
+
allowHorizontal?: boolean;
|
|
9
|
+
containerRole?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function useRovingFocus(options?: UseRovingFocusOptions): {
|
|
9
12
|
activeId: string | null;
|
|
10
13
|
hasInteracted: boolean;
|
|
11
|
-
registerItem: (item:
|
|
14
|
+
registerItem: (item: RovingFocusItem) => void;
|
|
12
15
|
unregisterItem: (id: string) => void;
|
|
13
|
-
|
|
14
|
-
setActiveId: react.Dispatch<react.SetStateAction<string | null>>;
|
|
15
|
-
focusItem: (itemId: string) => void;
|
|
16
|
+
focusItem: (id: string) => void;
|
|
16
17
|
focusFirst: () => void;
|
|
17
|
-
|
|
18
|
+
focusLast: () => void;
|
|
19
|
+
getFirstEnabledId: () => string | null;
|
|
18
20
|
reset: () => void;
|
|
19
21
|
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
20
|
-
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (
|
|
21
|
-
role?: "radio" | "option";
|
|
22
|
-
allowHorizontal?: boolean;
|
|
23
|
-
}) => void;
|
|
22
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (id: string) => void, containerRef?: React.RefObject<HTMLElement>) => void;
|
|
24
23
|
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
25
24
|
};
|
|
25
|
+
type RovingFocusReturn = ReturnType<typeof useRovingFocus>;
|
|
26
26
|
|
|
27
|
-
export { useRovingFocus };
|
|
27
|
+
export { type RovingFocusItem, type RovingFocusReturn, type UseRovingFocusOptions, useRovingFocus };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface Item {
|
|
1
|
+
interface RovingFocusItem {
|
|
4
2
|
id: string;
|
|
5
3
|
ref: React.RefObject<HTMLElement>;
|
|
6
4
|
disabled?: boolean;
|
|
7
5
|
}
|
|
8
|
-
|
|
6
|
+
interface UseRovingFocusOptions {
|
|
7
|
+
wrap?: boolean;
|
|
8
|
+
allowHorizontal?: boolean;
|
|
9
|
+
containerRole?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function useRovingFocus(options?: UseRovingFocusOptions): {
|
|
9
12
|
activeId: string | null;
|
|
10
13
|
hasInteracted: boolean;
|
|
11
|
-
registerItem: (item:
|
|
14
|
+
registerItem: (item: RovingFocusItem) => void;
|
|
12
15
|
unregisterItem: (id: string) => void;
|
|
13
|
-
|
|
14
|
-
setActiveId: react.Dispatch<react.SetStateAction<string | null>>;
|
|
15
|
-
focusItem: (itemId: string) => void;
|
|
16
|
+
focusItem: (id: string) => void;
|
|
16
17
|
focusFirst: () => void;
|
|
17
|
-
|
|
18
|
+
focusLast: () => void;
|
|
19
|
+
getFirstEnabledId: () => string | null;
|
|
18
20
|
reset: () => void;
|
|
19
21
|
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
20
|
-
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (
|
|
21
|
-
role?: "radio" | "option";
|
|
22
|
-
allowHorizontal?: boolean;
|
|
23
|
-
}) => void;
|
|
22
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (id: string) => void, containerRef?: React.RefObject<HTMLElement>) => void;
|
|
24
23
|
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
25
24
|
};
|
|
25
|
+
type RovingFocusReturn = ReturnType<typeof useRovingFocus>;
|
|
26
26
|
|
|
27
|
-
export { useRovingFocus };
|
|
27
|
+
export { type RovingFocusItem, type RovingFocusReturn, type UseRovingFocusOptions, useRovingFocus };
|
package/dist/index.js
CHANGED
|
@@ -3,83 +3,119 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
function useRovingFocus() {
|
|
6
|
+
function useRovingFocus(options = {}) {
|
|
7
|
+
const { wrap = true, allowHorizontal = true, containerRole } = options;
|
|
7
8
|
const items = react.useRef([]);
|
|
8
|
-
const
|
|
9
|
-
const [
|
|
9
|
+
const activeIdRef = react.useRef(null);
|
|
10
|
+
const [activeId, _setActiveId] = react.useState(null);
|
|
11
|
+
const hasInteractedRef = react.useRef(false);
|
|
12
|
+
const [hasInteracted, _setHasInteracted] = react.useState(false);
|
|
13
|
+
const setActiveId = react.useCallback((id) => {
|
|
14
|
+
activeIdRef.current = id;
|
|
15
|
+
_setActiveId(id);
|
|
16
|
+
}, []);
|
|
17
|
+
const setHasInteracted = react.useCallback((value) => {
|
|
18
|
+
hasInteractedRef.current = value;
|
|
19
|
+
_setHasInteracted(value);
|
|
20
|
+
}, []);
|
|
10
21
|
const registerItem = react.useCallback(
|
|
11
22
|
(item) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
23
|
+
const idx = items.current.findIndex((i) => i.id === item.id);
|
|
24
|
+
if (idx === -1) {
|
|
14
25
|
items.current.push(item);
|
|
15
26
|
} else {
|
|
16
|
-
items.current[
|
|
27
|
+
items.current[idx] = item;
|
|
17
28
|
}
|
|
18
|
-
if (!
|
|
29
|
+
if (!activeIdRef.current && !item.disabled) {
|
|
19
30
|
setActiveId(item.id);
|
|
20
31
|
}
|
|
21
32
|
},
|
|
22
|
-
[
|
|
33
|
+
[setActiveId]
|
|
34
|
+
);
|
|
35
|
+
const unregisterItem = react.useCallback(
|
|
36
|
+
(id) => {
|
|
37
|
+
items.current = items.current.filter((i) => i.id !== id);
|
|
38
|
+
if (activeIdRef.current === id) {
|
|
39
|
+
const next = items.current.find((i) => !i.disabled);
|
|
40
|
+
setActiveId(next?.id ?? null);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[setActiveId]
|
|
23
44
|
);
|
|
24
|
-
const unregisterItem = react.useCallback((id) => {
|
|
25
|
-
items.current = items.current.filter((i) => i.id !== id);
|
|
26
|
-
}, []);
|
|
27
45
|
const move = react.useCallback(
|
|
28
46
|
(dir) => {
|
|
29
47
|
setHasInteracted(true);
|
|
30
48
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
31
49
|
if (!enabled.length) return;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
50
|
+
const idx = enabled.findIndex((i) => i.id === activeIdRef.current);
|
|
51
|
+
const nextIdx = wrap ? (idx + dir + enabled.length) % enabled.length : Math.min(Math.max(idx + dir, 0), enabled.length - 1);
|
|
52
|
+
const next = enabled[nextIdx];
|
|
34
53
|
setActiveId(next.id);
|
|
35
54
|
next.ref.current?.focus();
|
|
36
55
|
},
|
|
37
|
-
[
|
|
56
|
+
[wrap, setActiveId, setHasInteracted]
|
|
38
57
|
);
|
|
39
|
-
const focusItem = react.useCallback((itemId) => {
|
|
40
|
-
setHasInteracted(true);
|
|
41
|
-
setActiveId(itemId);
|
|
42
|
-
}, []);
|
|
43
58
|
const focusFirst = react.useCallback(() => {
|
|
59
|
+
const first = items.current.find((i) => !i.disabled);
|
|
60
|
+
if (!first) return;
|
|
61
|
+
setHasInteracted(true);
|
|
62
|
+
setActiveId(first.id);
|
|
63
|
+
first.ref.current?.focus();
|
|
64
|
+
}, [setActiveId, setHasInteracted]);
|
|
65
|
+
const focusLast = react.useCallback(() => {
|
|
44
66
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
const last = enabled[enabled.length - 1];
|
|
68
|
+
if (!last) return;
|
|
69
|
+
setHasInteracted(true);
|
|
70
|
+
setActiveId(last.id);
|
|
71
|
+
last.ref.current?.focus();
|
|
72
|
+
}, [setActiveId, setHasInteracted]);
|
|
73
|
+
const focusItem = react.useCallback(
|
|
74
|
+
(id) => {
|
|
47
75
|
setHasInteracted(true);
|
|
48
|
-
setActiveId(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const getFirstEnabledId = react.useCallback(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
setActiveId(id);
|
|
77
|
+
},
|
|
78
|
+
[setActiveId, setHasInteracted]
|
|
79
|
+
);
|
|
80
|
+
const getFirstEnabledId = react.useCallback(
|
|
81
|
+
() => items.current.find((i) => !i.disabled)?.id ?? null,
|
|
82
|
+
[]
|
|
83
|
+
);
|
|
56
84
|
const reset = react.useCallback(() => {
|
|
57
85
|
setHasInteracted(false);
|
|
58
|
-
}, []);
|
|
86
|
+
}, [setHasInteracted]);
|
|
59
87
|
const handleGroupKeyDown = react.useCallback(
|
|
60
88
|
(e) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
const isArrow = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(e.key);
|
|
90
|
+
if (!isArrow) return;
|
|
91
|
+
if (!e.currentTarget.contains(document.activeElement)) return;
|
|
92
|
+
if (e.currentTarget !== document.activeElement) return;
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
focusFirst();
|
|
95
|
+
},
|
|
96
|
+
[focusFirst]
|
|
97
|
+
);
|
|
98
|
+
const handleGroupBlur = react.useCallback(
|
|
99
|
+
(e) => {
|
|
100
|
+
const currentTarget = e.currentTarget;
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
if (!currentTarget.contains(document.activeElement)) reset();
|
|
103
|
+
}, 0);
|
|
67
104
|
},
|
|
68
|
-
[
|
|
105
|
+
[reset]
|
|
69
106
|
);
|
|
70
107
|
const handleItemKeyDown = react.useCallback(
|
|
71
|
-
(e, itemId, onSelect,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else
|
|
81
|
-
|
|
82
|
-
listbox?.focus();
|
|
108
|
+
(e, itemId, onSelect, containerRef) => {
|
|
109
|
+
if (e.key === "Tab") {
|
|
110
|
+
if (e.shiftKey) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
reset();
|
|
113
|
+
const container = containerRef?.current ?? (containerRole ? e.target.closest(
|
|
114
|
+
`[role="${containerRole}"]`
|
|
115
|
+
) : null);
|
|
116
|
+
container?.focus();
|
|
117
|
+
} else {
|
|
118
|
+
reset();
|
|
83
119
|
}
|
|
84
120
|
return;
|
|
85
121
|
}
|
|
@@ -93,33 +129,31 @@ function useRovingFocus() {
|
|
|
93
129
|
move(-1);
|
|
94
130
|
return;
|
|
95
131
|
}
|
|
96
|
-
if (e.key === "
|
|
132
|
+
if (e.key === "Home") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
focusFirst();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (e.key === "End") {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
focusLast();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
97
143
|
e.preventDefault();
|
|
98
144
|
onSelect?.(itemId);
|
|
99
145
|
}
|
|
100
146
|
},
|
|
101
|
-
[move, reset]
|
|
102
|
-
);
|
|
103
|
-
const handleGroupBlur = react.useCallback(
|
|
104
|
-
(e) => {
|
|
105
|
-
const currentTarget = e.currentTarget;
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
if (!currentTarget.contains(document.activeElement)) {
|
|
108
|
-
reset();
|
|
109
|
-
}
|
|
110
|
-
}, 0);
|
|
111
|
-
},
|
|
112
|
-
[reset]
|
|
147
|
+
[allowHorizontal, move, focusFirst, focusLast, reset, containerRole]
|
|
113
148
|
);
|
|
114
149
|
return {
|
|
115
150
|
activeId,
|
|
116
151
|
hasInteracted,
|
|
117
152
|
registerItem,
|
|
118
153
|
unregisterItem,
|
|
119
|
-
move,
|
|
120
|
-
setActiveId,
|
|
121
154
|
focusItem,
|
|
122
155
|
focusFirst,
|
|
156
|
+
focusLast,
|
|
123
157
|
getFirstEnabledId,
|
|
124
158
|
reset,
|
|
125
159
|
handleGroupKeyDown,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["useRef","useState","useCallback"],"mappings":";;;;;AAQO,SAAS,cAAA,GAAiB;AAC/B,EAAA,MAAM,KAAA,GAAQA,YAAA,CAAe,EAAE,CAAA;AAC/B,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIC,eAAwB,IAAI,CAAA;AAC5D,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAIA,eAAS,KAAK,CAAA;AAExD,EAAA,MAAM,YAAA,GAAeC,iBAAA;AAAA,IACnB,CAAC,IAAA,KAAe;AACd,MAAA,MAAM,aAAA,GAAgB,MAAM,OAAA,CAAQ,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,IAAA,CAAK,EAAE,CAAA;AAErE,MAAA,IAAI,kBAAkB,EAAA,EAAI;AACxB,QAAA,KAAA,CAAM,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,MACzB,CAAA,MAAO;AACL,QAAA,KAAA,CAAM,OAAA,CAAQ,aAAa,CAAA,GAAI,IAAA;AAAA,MACjC;AAEA,MAAA,IAAI,CAAC,QAAA,IAAY,CAAC,IAAA,CAAK,QAAA,EAAU;AAC/B,QAAA,WAAA,CAAY,KAAK,EAAE,CAAA;AAAA,MACrB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,QAAQ;AAAA,GACX;AAEA,EAAA,MAAM,cAAA,GAAiBA,iBAAA,CAAY,CAAC,EAAA,KAAe;AACjD,IAAA,KAAA,CAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,EAAE,CAAA;AAAA,EACzD,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,IAAA,GAAOA,iBAAA;AAAA,IACX,CAAC,GAAA,KAAgB;AACf,MAAA,gBAAA,CAAiB,IAAI,CAAA;AAErB,MAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,QAAQ,CAAA;AACvD,MAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AAErB,MAAA,MAAM,QAAQ,OAAA,CAAQ,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,QAAQ,CAAA;AACxD,MAAA,MAAM,OAAO,OAAA,CAAA,CAAS,KAAA,GAAQ,MAAM,OAAA,CAAQ,MAAA,IAAU,QAAQ,MAAM,CAAA;AAEpE,MAAA,WAAA,CAAY,KAAK,EAAE,CAAA;AACnB,MAAA,IAAA,CAAK,GAAA,CAAI,SAAS,KAAA,EAAM;AAAA,IAC1B,CAAA;AAAA,IACA,CAAC,QAAQ;AAAA,GACX;AAEA,EAAA,MAAM,SAAA,GAAYA,iBAAA,CAAY,CAAC,MAAA,KAAmB;AAChD,IAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,IAAA,WAAA,CAAY,MAAM,CAAA;AAAA,EACpB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAaA,kBAAY,MAAM;AACnC,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,QAAQ,CAAA;AACvD,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,MAAM,KAAA,GAAQ,QAAQ,CAAC,CAAA;AACvB,MAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,MAAA,WAAA,CAAY,MAAM,EAAE,CAAA;AACpB,MAAA,KAAA,CAAM,GAAA,CAAI,SAAS,KAAA,EAAM;AAAA,IAC3B;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,iBAAA,GAAoBA,kBAAY,MAAM;AAC1C,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,QAAQ,CAAA;AACvD,IAAA,OAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,EAAA,IAAM,IAAA;AAAA,EAC3B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,KAAA,GAAQA,kBAAY,MAAM;AAC9B,IAAA,gBAAA,CAAiB,KAAK,CAAA;AAAA,EACxB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,kBAAA,GAAqBA,iBAAA;AAAA,IACzB,CAAC,CAAA,KAA2B;AAC1B,MAAA,IAAI,CAAC,aAAA,EAAe;AAClB,QAAA,IACE,CAAA,CAAE,GAAA,KAAQ,WAAA,IACV,CAAA,CAAE,GAAA,KAAQ,YAAA,IACV,CAAA,CAAE,GAAA,KAAQ,SAAA,IACV,CAAA,CAAE,GAAA,KAAQ,WAAA,EACV;AACA,UAAA,CAAA,CAAE,cAAA,EAAe;AACjB,UAAA,UAAA,EAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,CAAC,eAAe,UAAU;AAAA,GAC5B;AAEA,EAAA,MAAM,iBAAA,GAAoBA,iBAAA;AAAA,IACxB,CACE,CAAA,EACA,MAAA,EACA,QAAA,EACA,OAAA,KAIG;AACH,MAAA,MAAM,IAAA,GAAO,SAAS,IAAA,IAAQ,OAAA;AAC9B,MAAA,MAAM,eAAA,GAAkB,SAAS,eAAA,IAAmB,IAAA;AAEpD,MAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,KAAA,IAAS,CAAA,CAAE,QAAA,EAAU;AACjC,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,KAAA,EAAM;AACN,QAAA,IAAI,SAAS,OAAA,EAAS;AACpB,UAAA,MAAM,QAAA,GAAY,CAAA,CAAE,MAAA,CAAuB,OAAA,CAAQ,qBAAqB,CAAA;AACxE,UAAA,QAAA,EAAU,KAAA,EAAM;AAAA,QAClB,CAAA,MAAA,IAAW,SAAS,QAAA,EAAU;AAC5B,UAAA,MAAM,OAAA,GAAW,CAAA,CAAE,MAAA,CAAuB,OAAA,CAAQ,kBAAkB,CAAA;AACpE,UAAA,OAAA,EAAS,KAAA,EAAM;AAAA,QACjB;AACA,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,EAAE,GAAA,KAAQ,WAAA,IAAgB,eAAA,IAAmB,CAAA,CAAE,QAAQ,YAAA,EAAe;AACxE,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,IAAA,CAAK,CAAC,CAAA;AACN,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,EAAE,GAAA,KAAQ,SAAA,IAAc,eAAA,IAAmB,CAAA,CAAE,QAAQ,WAAA,EAAc;AACrE,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,IAAA,CAAK,EAAE,CAAA;AACP,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,GAAA,IAAO,CAAA,CAAE,QAAQ,OAAA,EAAS;AACtC,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,QAAA,GAAW,MAAM,CAAA;AAAA,MACnB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAM,KAAK;AAAA,GACd;AAEA,EAAA,MAAM,eAAA,GAAkBA,iBAAA;AAAA,IACtB,CAAC,CAAA,KAAwB;AACvB,MAAA,MAAM,gBAAgB,CAAA,CAAE,aAAA;AACxB,MAAA,UAAA,CAAW,MAAM;AACf,QAAA,IAAI,CAAC,aAAA,CAAc,QAAA,CAAS,QAAA,CAAS,aAAa,CAAA,EAAG;AACnD,UAAA,KAAA,EAAM;AAAA,QACR;AAAA,MACF,GAAG,CAAC,CAAA;AAAA,IACN,CAAA;AAAA,IACA,CAAC,KAAK;AAAA,GACR;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,aAAA;AAAA,IACA,YAAA;AAAA,IACA,cAAA;AAAA,IACA,IAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,iBAAA;AAAA,IACA,KAAA;AAAA,IACA,kBAAA;AAAA,IACA,iBAAA;AAAA,IACA;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import { useCallback, useRef, useState } from 'react'\n\ninterface Item {\n id: string\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport function useRovingFocus() {\n const items = useRef<Item[]>([])\n const [activeId, setActiveId] = useState<string | null>(null)\n const [hasInteracted, setHasInteracted] = useState(false)\n\n const registerItem = useCallback(\n (item: Item) => {\n const existingIndex = items.current.findIndex((i) => i.id === item.id)\n\n if (existingIndex === -1) {\n items.current.push(item)\n } else {\n items.current[existingIndex] = item\n }\n\n if (!activeId && !item.disabled) {\n setActiveId(item.id)\n }\n },\n [activeId]\n )\n\n const unregisterItem = useCallback((id: string) => {\n items.current = items.current.filter((i) => i.id !== id)\n }, [])\n\n const move = useCallback(\n (dir: 1 | -1) => {\n setHasInteracted(true)\n\n const enabled = items.current.filter((i) => !i.disabled)\n if (!enabled.length) return\n\n const index = enabled.findIndex((i) => i.id === activeId)\n const next = enabled[(index + dir + enabled.length) % enabled.length]\n\n setActiveId(next.id)\n next.ref.current?.focus()\n },\n [activeId]\n )\n\n const focusItem = useCallback((itemId: string) => {\n setHasInteracted(true)\n setActiveId(itemId)\n }, [])\n\n const focusFirst = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n if (enabled.length > 0) {\n const first = enabled[0]\n setHasInteracted(true)\n setActiveId(first.id)\n first.ref.current?.focus()\n }\n }, [])\n\n const getFirstEnabledId = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n return enabled[0]?.id ?? null\n }, [])\n\n const reset = useCallback(() => {\n setHasInteracted(false)\n }, [])\n\n const handleGroupKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (!hasInteracted) {\n if (\n e.key === 'ArrowDown' ||\n e.key === 'ArrowRight' ||\n e.key === 'ArrowUp' ||\n e.key === 'ArrowLeft'\n ) {\n e.preventDefault()\n focusFirst()\n }\n }\n },\n [hasInteracted, focusFirst]\n )\n\n const handleItemKeyDown = useCallback(\n (\n e: React.KeyboardEvent,\n itemId: string,\n onSelect?: (value: string) => void,\n options?: {\n role?: 'radio' | 'option'\n allowHorizontal?: boolean\n }\n ) => {\n const role = options?.role ?? 'radio'\n const allowHorizontal = options?.allowHorizontal ?? true\n\n if (e.key === 'Tab' && e.shiftKey) {\n e.preventDefault()\n reset()\n if (role === 'radio') {\n const fieldset = (e.target as HTMLElement).closest('[role=\"radiogroup\"]') as HTMLElement\n fieldset?.focus()\n } else if (role === 'option') {\n const listbox = (e.target as HTMLElement).closest('[role=\"listbox\"]') as HTMLElement\n listbox?.focus()\n }\n return\n }\n\n if (e.key === 'ArrowDown' || (allowHorizontal && e.key === 'ArrowRight')) {\n e.preventDefault()\n move(1)\n return\n }\n\n if (e.key === 'ArrowUp' || (allowHorizontal && e.key === 'ArrowLeft')) {\n e.preventDefault()\n move(-1)\n return\n }\n\n if (e.key === ' ' || e.key === 'Enter') {\n e.preventDefault()\n onSelect?.(itemId)\n }\n },\n [move, reset]\n )\n\n const handleGroupBlur = useCallback(\n (e: React.FocusEvent) => {\n const currentTarget = e.currentTarget\n setTimeout(() => {\n if (!currentTarget.contains(document.activeElement)) {\n reset()\n }\n }, 0)\n },\n [reset]\n )\n\n return {\n activeId,\n hasInteracted,\n registerItem,\n unregisterItem,\n move,\n setActiveId,\n focusItem,\n focusFirst,\n getFirstEnabledId,\n reset,\n handleGroupKeyDown,\n handleItemKeyDown,\n handleGroupBlur\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["useRef","useState","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useRef, useState } from 'react'\n\nexport interface RovingFocusItem {\n id: string\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport interface UseRovingFocusOptions {\n wrap?: boolean\n allowHorizontal?: boolean\n containerRole?: string\n}\n\nexport function useRovingFocus(options: UseRovingFocusOptions = {}) {\n const { wrap = true, allowHorizontal = true, containerRole } = options\n\n const items = useRef<RovingFocusItem[]>([])\n const activeIdRef = useRef<string | null>(null)\n const [activeId, _setActiveId] = useState<string | null>(null)\n const hasInteractedRef = useRef(false)\n const [hasInteracted, _setHasInteracted] = useState(false)\n\n const setActiveId = useCallback((id: string | null) => {\n activeIdRef.current = id\n _setActiveId(id)\n }, [])\n\n const setHasInteracted = useCallback((value: boolean) => {\n hasInteractedRef.current = value\n _setHasInteracted(value)\n }, [])\n\n const registerItem = useCallback(\n (item: RovingFocusItem) => {\n const idx = items.current.findIndex((i) => i.id === item.id)\n if (idx === -1) {\n items.current.push(item)\n } else {\n items.current[idx] = item\n }\n if (!activeIdRef.current && !item.disabled) {\n setActiveId(item.id)\n }\n },\n [setActiveId]\n )\n\n const unregisterItem = useCallback(\n (id: string) => {\n items.current = items.current.filter((i) => i.id !== id)\n if (activeIdRef.current === id) {\n const next = items.current.find((i) => !i.disabled)\n setActiveId(next?.id ?? null)\n }\n },\n [setActiveId]\n )\n\n const move = useCallback(\n (dir: 1 | -1) => {\n setHasInteracted(true)\n const enabled = items.current.filter((i) => !i.disabled)\n if (!enabled.length) return\n const idx = enabled.findIndex((i) => i.id === activeIdRef.current)\n const nextIdx = wrap\n ? (idx + dir + enabled.length) % enabled.length\n : Math.min(Math.max(idx + dir, 0), enabled.length - 1)\n const next = enabled[nextIdx]\n setActiveId(next.id)\n next.ref.current?.focus()\n },\n [wrap, setActiveId, setHasInteracted]\n )\n\n const focusFirst = useCallback(() => {\n const first = items.current.find((i) => !i.disabled)\n if (!first) return\n setHasInteracted(true)\n setActiveId(first.id)\n first.ref.current?.focus()\n }, [setActiveId, setHasInteracted])\n\n const focusLast = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n const last = enabled[enabled.length - 1]\n if (!last) return\n setHasInteracted(true)\n setActiveId(last.id)\n last.ref.current?.focus()\n }, [setActiveId, setHasInteracted])\n\n const focusItem = useCallback(\n (id: string) => {\n setHasInteracted(true)\n setActiveId(id)\n },\n [setActiveId, setHasInteracted]\n )\n\n const getFirstEnabledId = useCallback(\n () => items.current.find((i) => !i.disabled)?.id ?? null,\n []\n )\n\n const reset = useCallback(() => {\n setHasInteracted(false)\n }, [setHasInteracted])\n\n const handleGroupKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n const isArrow = ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)\n if (!isArrow) return\n if (!e.currentTarget.contains(document.activeElement)) return\n if (e.currentTarget !== document.activeElement) return\n e.preventDefault()\n focusFirst()\n },\n [focusFirst]\n )\n\n const handleGroupBlur = useCallback(\n (e: React.FocusEvent) => {\n const currentTarget = e.currentTarget\n setTimeout(() => {\n if (!currentTarget.contains(document.activeElement)) reset()\n }, 0)\n },\n [reset]\n )\n\n const handleItemKeyDown = useCallback(\n (\n e: React.KeyboardEvent,\n itemId: string,\n onSelect?: (id: string) => void,\n containerRef?: React.RefObject<HTMLElement>\n ) => {\n if (e.key === 'Tab') {\n if (e.shiftKey) {\n e.preventDefault()\n reset()\n const container: HTMLElement | null =\n containerRef?.current ??\n (containerRole\n ? ((e.target as HTMLElement).closest(\n `[role=\"${containerRole}\"]`\n ) as HTMLElement | null)\n : null)\n container?.focus()\n } else {\n reset()\n }\n return\n }\n\n if (e.key === 'ArrowDown' || (allowHorizontal && e.key === 'ArrowRight')) {\n e.preventDefault()\n move(1)\n return\n }\n if (e.key === 'ArrowUp' || (allowHorizontal && e.key === 'ArrowLeft')) {\n e.preventDefault()\n move(-1)\n return\n }\n if (e.key === 'Home') {\n e.preventDefault()\n focusFirst()\n return\n }\n if (e.key === 'End') {\n e.preventDefault()\n focusLast()\n return\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onSelect?.(itemId)\n }\n },\n [allowHorizontal, move, focusFirst, focusLast, reset, containerRole]\n )\n\n return {\n activeId,\n hasInteracted,\n registerItem,\n unregisterItem,\n focusItem,\n focusFirst,\n focusLast,\n getFirstEnabledId,\n reset,\n handleGroupKeyDown,\n handleItemKeyDown,\n handleGroupBlur\n }\n}\n\nexport type RovingFocusReturn = ReturnType<typeof useRovingFocus>\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,83 +1,119 @@
|
|
|
1
1
|
import { useRef, useState, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
function useRovingFocus() {
|
|
4
|
+
function useRovingFocus(options = {}) {
|
|
5
|
+
const { wrap = true, allowHorizontal = true, containerRole } = options;
|
|
5
6
|
const items = useRef([]);
|
|
6
|
-
const
|
|
7
|
-
const [
|
|
7
|
+
const activeIdRef = useRef(null);
|
|
8
|
+
const [activeId, _setActiveId] = useState(null);
|
|
9
|
+
const hasInteractedRef = useRef(false);
|
|
10
|
+
const [hasInteracted, _setHasInteracted] = useState(false);
|
|
11
|
+
const setActiveId = useCallback((id) => {
|
|
12
|
+
activeIdRef.current = id;
|
|
13
|
+
_setActiveId(id);
|
|
14
|
+
}, []);
|
|
15
|
+
const setHasInteracted = useCallback((value) => {
|
|
16
|
+
hasInteractedRef.current = value;
|
|
17
|
+
_setHasInteracted(value);
|
|
18
|
+
}, []);
|
|
8
19
|
const registerItem = useCallback(
|
|
9
20
|
(item) => {
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
21
|
+
const idx = items.current.findIndex((i) => i.id === item.id);
|
|
22
|
+
if (idx === -1) {
|
|
12
23
|
items.current.push(item);
|
|
13
24
|
} else {
|
|
14
|
-
items.current[
|
|
25
|
+
items.current[idx] = item;
|
|
15
26
|
}
|
|
16
|
-
if (!
|
|
27
|
+
if (!activeIdRef.current && !item.disabled) {
|
|
17
28
|
setActiveId(item.id);
|
|
18
29
|
}
|
|
19
30
|
},
|
|
20
|
-
[
|
|
31
|
+
[setActiveId]
|
|
32
|
+
);
|
|
33
|
+
const unregisterItem = useCallback(
|
|
34
|
+
(id) => {
|
|
35
|
+
items.current = items.current.filter((i) => i.id !== id);
|
|
36
|
+
if (activeIdRef.current === id) {
|
|
37
|
+
const next = items.current.find((i) => !i.disabled);
|
|
38
|
+
setActiveId(next?.id ?? null);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[setActiveId]
|
|
21
42
|
);
|
|
22
|
-
const unregisterItem = useCallback((id) => {
|
|
23
|
-
items.current = items.current.filter((i) => i.id !== id);
|
|
24
|
-
}, []);
|
|
25
43
|
const move = useCallback(
|
|
26
44
|
(dir) => {
|
|
27
45
|
setHasInteracted(true);
|
|
28
46
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
29
47
|
if (!enabled.length) return;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
48
|
+
const idx = enabled.findIndex((i) => i.id === activeIdRef.current);
|
|
49
|
+
const nextIdx = wrap ? (idx + dir + enabled.length) % enabled.length : Math.min(Math.max(idx + dir, 0), enabled.length - 1);
|
|
50
|
+
const next = enabled[nextIdx];
|
|
32
51
|
setActiveId(next.id);
|
|
33
52
|
next.ref.current?.focus();
|
|
34
53
|
},
|
|
35
|
-
[
|
|
54
|
+
[wrap, setActiveId, setHasInteracted]
|
|
36
55
|
);
|
|
37
|
-
const focusItem = useCallback((itemId) => {
|
|
38
|
-
setHasInteracted(true);
|
|
39
|
-
setActiveId(itemId);
|
|
40
|
-
}, []);
|
|
41
56
|
const focusFirst = useCallback(() => {
|
|
57
|
+
const first = items.current.find((i) => !i.disabled);
|
|
58
|
+
if (!first) return;
|
|
59
|
+
setHasInteracted(true);
|
|
60
|
+
setActiveId(first.id);
|
|
61
|
+
first.ref.current?.focus();
|
|
62
|
+
}, [setActiveId, setHasInteracted]);
|
|
63
|
+
const focusLast = useCallback(() => {
|
|
42
64
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
const last = enabled[enabled.length - 1];
|
|
66
|
+
if (!last) return;
|
|
67
|
+
setHasInteracted(true);
|
|
68
|
+
setActiveId(last.id);
|
|
69
|
+
last.ref.current?.focus();
|
|
70
|
+
}, [setActiveId, setHasInteracted]);
|
|
71
|
+
const focusItem = useCallback(
|
|
72
|
+
(id) => {
|
|
45
73
|
setHasInteracted(true);
|
|
46
|
-
setActiveId(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const getFirstEnabledId = useCallback(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
setActiveId(id);
|
|
75
|
+
},
|
|
76
|
+
[setActiveId, setHasInteracted]
|
|
77
|
+
);
|
|
78
|
+
const getFirstEnabledId = useCallback(
|
|
79
|
+
() => items.current.find((i) => !i.disabled)?.id ?? null,
|
|
80
|
+
[]
|
|
81
|
+
);
|
|
54
82
|
const reset = useCallback(() => {
|
|
55
83
|
setHasInteracted(false);
|
|
56
|
-
}, []);
|
|
84
|
+
}, [setHasInteracted]);
|
|
57
85
|
const handleGroupKeyDown = useCallback(
|
|
58
86
|
(e) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
const isArrow = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(e.key);
|
|
88
|
+
if (!isArrow) return;
|
|
89
|
+
if (!e.currentTarget.contains(document.activeElement)) return;
|
|
90
|
+
if (e.currentTarget !== document.activeElement) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
focusFirst();
|
|
93
|
+
},
|
|
94
|
+
[focusFirst]
|
|
95
|
+
);
|
|
96
|
+
const handleGroupBlur = useCallback(
|
|
97
|
+
(e) => {
|
|
98
|
+
const currentTarget = e.currentTarget;
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
if (!currentTarget.contains(document.activeElement)) reset();
|
|
101
|
+
}, 0);
|
|
65
102
|
},
|
|
66
|
-
[
|
|
103
|
+
[reset]
|
|
67
104
|
);
|
|
68
105
|
const handleItemKeyDown = useCallback(
|
|
69
|
-
(e, itemId, onSelect,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else
|
|
79
|
-
|
|
80
|
-
listbox?.focus();
|
|
106
|
+
(e, itemId, onSelect, containerRef) => {
|
|
107
|
+
if (e.key === "Tab") {
|
|
108
|
+
if (e.shiftKey) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
reset();
|
|
111
|
+
const container = containerRef?.current ?? (containerRole ? e.target.closest(
|
|
112
|
+
`[role="${containerRole}"]`
|
|
113
|
+
) : null);
|
|
114
|
+
container?.focus();
|
|
115
|
+
} else {
|
|
116
|
+
reset();
|
|
81
117
|
}
|
|
82
118
|
return;
|
|
83
119
|
}
|
|
@@ -91,33 +127,31 @@ function useRovingFocus() {
|
|
|
91
127
|
move(-1);
|
|
92
128
|
return;
|
|
93
129
|
}
|
|
94
|
-
if (e.key === "
|
|
130
|
+
if (e.key === "Home") {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
focusFirst();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (e.key === "End") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
focusLast();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
95
141
|
e.preventDefault();
|
|
96
142
|
onSelect?.(itemId);
|
|
97
143
|
}
|
|
98
144
|
},
|
|
99
|
-
[move, reset]
|
|
100
|
-
);
|
|
101
|
-
const handleGroupBlur = useCallback(
|
|
102
|
-
(e) => {
|
|
103
|
-
const currentTarget = e.currentTarget;
|
|
104
|
-
setTimeout(() => {
|
|
105
|
-
if (!currentTarget.contains(document.activeElement)) {
|
|
106
|
-
reset();
|
|
107
|
-
}
|
|
108
|
-
}, 0);
|
|
109
|
-
},
|
|
110
|
-
[reset]
|
|
145
|
+
[allowHorizontal, move, focusFirst, focusLast, reset, containerRole]
|
|
111
146
|
);
|
|
112
147
|
return {
|
|
113
148
|
activeId,
|
|
114
149
|
hasInteracted,
|
|
115
150
|
registerItem,
|
|
116
151
|
unregisterItem,
|
|
117
|
-
move,
|
|
118
|
-
setActiveId,
|
|
119
152
|
focusItem,
|
|
120
153
|
focusFirst,
|
|
154
|
+
focusLast,
|
|
121
155
|
getFirstEnabledId,
|
|
122
156
|
reset,
|
|
123
157
|
handleGroupKeyDown,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useCallback, useRef, useState } from 'react'\n\ninterface Item {\n id: string\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport function useRovingFocus() {\n const items = useRef<Item[]>([])\n const [activeId, setActiveId] = useState<string | null>(null)\n const [hasInteracted, setHasInteracted] = useState(false)\n\n const registerItem = useCallback(\n (item: Item) => {\n const existingIndex = items.current.findIndex((i) => i.id === item.id)\n\n if (existingIndex === -1) {\n items.current.push(item)\n } else {\n items.current[existingIndex] = item\n }\n\n if (!activeId && !item.disabled) {\n setActiveId(item.id)\n }\n },\n [activeId]\n )\n\n const unregisterItem = useCallback((id: string) => {\n items.current = items.current.filter((i) => i.id !== id)\n }, [])\n\n const move = useCallback(\n (dir: 1 | -1) => {\n setHasInteracted(true)\n\n const enabled = items.current.filter((i) => !i.disabled)\n if (!enabled.length) return\n\n const index = enabled.findIndex((i) => i.id === activeId)\n const next = enabled[(index + dir + enabled.length) % enabled.length]\n\n setActiveId(next.id)\n next.ref.current?.focus()\n },\n [activeId]\n )\n\n const focusItem = useCallback((itemId: string) => {\n setHasInteracted(true)\n setActiveId(itemId)\n }, [])\n\n const focusFirst = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n if (enabled.length > 0) {\n const first = enabled[0]\n setHasInteracted(true)\n setActiveId(first.id)\n first.ref.current?.focus()\n }\n }, [])\n\n const getFirstEnabledId = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n return enabled[0]?.id ?? null\n }, [])\n\n const reset = useCallback(() => {\n setHasInteracted(false)\n }, [])\n\n const handleGroupKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (!hasInteracted) {\n if (\n e.key === 'ArrowDown' ||\n e.key === 'ArrowRight' ||\n e.key === 'ArrowUp' ||\n e.key === 'ArrowLeft'\n ) {\n e.preventDefault()\n focusFirst()\n }\n }\n },\n [hasInteracted, focusFirst]\n )\n\n const handleItemKeyDown = useCallback(\n (\n e: React.KeyboardEvent,\n itemId: string,\n onSelect?: (value: string) => void,\n options?: {\n role?: 'radio' | 'option'\n allowHorizontal?: boolean\n }\n ) => {\n const role = options?.role ?? 'radio'\n const allowHorizontal = options?.allowHorizontal ?? true\n\n if (e.key === 'Tab' && e.shiftKey) {\n e.preventDefault()\n reset()\n if (role === 'radio') {\n const fieldset = (e.target as HTMLElement).closest('[role=\"radiogroup\"]') as HTMLElement\n fieldset?.focus()\n } else if (role === 'option') {\n const listbox = (e.target as HTMLElement).closest('[role=\"listbox\"]') as HTMLElement\n listbox?.focus()\n }\n return\n }\n\n if (e.key === 'ArrowDown' || (allowHorizontal && e.key === 'ArrowRight')) {\n e.preventDefault()\n move(1)\n return\n }\n\n if (e.key === 'ArrowUp' || (allowHorizontal && e.key === 'ArrowLeft')) {\n e.preventDefault()\n move(-1)\n return\n }\n\n if (e.key === ' ' || e.key === 'Enter') {\n e.preventDefault()\n onSelect?.(itemId)\n }\n },\n [move, reset]\n )\n\n const handleGroupBlur = useCallback(\n (e: React.FocusEvent) => {\n const currentTarget = e.currentTarget\n setTimeout(() => {\n if (!currentTarget.contains(document.activeElement)) {\n reset()\n }\n }, 0)\n },\n [reset]\n )\n\n return {\n activeId,\n hasInteracted,\n registerItem,\n unregisterItem,\n move,\n setActiveId,\n focusItem,\n focusFirst,\n getFirstEnabledId,\n reset,\n handleGroupKeyDown,\n handleItemKeyDown,\n handleGroupBlur\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useCallback, useRef, useState } from 'react'\n\nexport interface RovingFocusItem {\n id: string\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport interface UseRovingFocusOptions {\n wrap?: boolean\n allowHorizontal?: boolean\n containerRole?: string\n}\n\nexport function useRovingFocus(options: UseRovingFocusOptions = {}) {\n const { wrap = true, allowHorizontal = true, containerRole } = options\n\n const items = useRef<RovingFocusItem[]>([])\n const activeIdRef = useRef<string | null>(null)\n const [activeId, _setActiveId] = useState<string | null>(null)\n const hasInteractedRef = useRef(false)\n const [hasInteracted, _setHasInteracted] = useState(false)\n\n const setActiveId = useCallback((id: string | null) => {\n activeIdRef.current = id\n _setActiveId(id)\n }, [])\n\n const setHasInteracted = useCallback((value: boolean) => {\n hasInteractedRef.current = value\n _setHasInteracted(value)\n }, [])\n\n const registerItem = useCallback(\n (item: RovingFocusItem) => {\n const idx = items.current.findIndex((i) => i.id === item.id)\n if (idx === -1) {\n items.current.push(item)\n } else {\n items.current[idx] = item\n }\n if (!activeIdRef.current && !item.disabled) {\n setActiveId(item.id)\n }\n },\n [setActiveId]\n )\n\n const unregisterItem = useCallback(\n (id: string) => {\n items.current = items.current.filter((i) => i.id !== id)\n if (activeIdRef.current === id) {\n const next = items.current.find((i) => !i.disabled)\n setActiveId(next?.id ?? null)\n }\n },\n [setActiveId]\n )\n\n const move = useCallback(\n (dir: 1 | -1) => {\n setHasInteracted(true)\n const enabled = items.current.filter((i) => !i.disabled)\n if (!enabled.length) return\n const idx = enabled.findIndex((i) => i.id === activeIdRef.current)\n const nextIdx = wrap\n ? (idx + dir + enabled.length) % enabled.length\n : Math.min(Math.max(idx + dir, 0), enabled.length - 1)\n const next = enabled[nextIdx]\n setActiveId(next.id)\n next.ref.current?.focus()\n },\n [wrap, setActiveId, setHasInteracted]\n )\n\n const focusFirst = useCallback(() => {\n const first = items.current.find((i) => !i.disabled)\n if (!first) return\n setHasInteracted(true)\n setActiveId(first.id)\n first.ref.current?.focus()\n }, [setActiveId, setHasInteracted])\n\n const focusLast = useCallback(() => {\n const enabled = items.current.filter((i) => !i.disabled)\n const last = enabled[enabled.length - 1]\n if (!last) return\n setHasInteracted(true)\n setActiveId(last.id)\n last.ref.current?.focus()\n }, [setActiveId, setHasInteracted])\n\n const focusItem = useCallback(\n (id: string) => {\n setHasInteracted(true)\n setActiveId(id)\n },\n [setActiveId, setHasInteracted]\n )\n\n const getFirstEnabledId = useCallback(\n () => items.current.find((i) => !i.disabled)?.id ?? null,\n []\n )\n\n const reset = useCallback(() => {\n setHasInteracted(false)\n }, [setHasInteracted])\n\n const handleGroupKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n const isArrow = ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)\n if (!isArrow) return\n if (!e.currentTarget.contains(document.activeElement)) return\n if (e.currentTarget !== document.activeElement) return\n e.preventDefault()\n focusFirst()\n },\n [focusFirst]\n )\n\n const handleGroupBlur = useCallback(\n (e: React.FocusEvent) => {\n const currentTarget = e.currentTarget\n setTimeout(() => {\n if (!currentTarget.contains(document.activeElement)) reset()\n }, 0)\n },\n [reset]\n )\n\n const handleItemKeyDown = useCallback(\n (\n e: React.KeyboardEvent,\n itemId: string,\n onSelect?: (id: string) => void,\n containerRef?: React.RefObject<HTMLElement>\n ) => {\n if (e.key === 'Tab') {\n if (e.shiftKey) {\n e.preventDefault()\n reset()\n const container: HTMLElement | null =\n containerRef?.current ??\n (containerRole\n ? ((e.target as HTMLElement).closest(\n `[role=\"${containerRole}\"]`\n ) as HTMLElement | null)\n : null)\n container?.focus()\n } else {\n reset()\n }\n return\n }\n\n if (e.key === 'ArrowDown' || (allowHorizontal && e.key === 'ArrowRight')) {\n e.preventDefault()\n move(1)\n return\n }\n if (e.key === 'ArrowUp' || (allowHorizontal && e.key === 'ArrowLeft')) {\n e.preventDefault()\n move(-1)\n return\n }\n if (e.key === 'Home') {\n e.preventDefault()\n focusFirst()\n return\n }\n if (e.key === 'End') {\n e.preventDefault()\n focusLast()\n return\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onSelect?.(itemId)\n }\n },\n [allowHorizontal, move, focusFirst, focusLast, reset, containerRole]\n )\n\n return {\n activeId,\n hasInteracted,\n registerItem,\n unregisterItem,\n focusItem,\n focusFirst,\n focusLast,\n getFirstEnabledId,\n reset,\n handleGroupKeyDown,\n handleItemKeyDown,\n handleGroupBlur\n }\n}\n\nexport type RovingFocusReturn = ReturnType<typeof useRovingFocus>\n"]}
|