@negative-space/roving-focus 1.0.0 → 1.1.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 +20 -11
- package/dist/index.d.ts +20 -11
- package/dist/index.js +105 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +105 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
id:
|
|
3
|
+
interface Item {
|
|
4
|
+
id: string;
|
|
5
5
|
ref: React.RefObject<HTMLElement>;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
declare
|
|
7
|
+
}
|
|
8
|
+
declare function useRovingFocus(): {
|
|
9
|
+
activeId: string | null;
|
|
10
|
+
hasInteracted: boolean;
|
|
9
11
|
registerItem: (item: Item) => void;
|
|
10
|
-
unregisterItem: (id:
|
|
11
|
-
|
|
12
|
-
setActiveId: react.Dispatch<react.SetStateAction<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
unregisterItem: (id: string) => void;
|
|
13
|
+
move: (dir: 1 | -1) => void;
|
|
14
|
+
setActiveId: react.Dispatch<react.SetStateAction<string | null>>;
|
|
15
|
+
focusItem: (itemId: string) => void;
|
|
16
|
+
focusFirst: () => void;
|
|
17
|
+
getFirstEnabledId: () => string;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
20
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (value: string) => void, options?: {
|
|
21
|
+
role?: "radio" | "option";
|
|
22
|
+
allowHorizontal?: boolean;
|
|
23
|
+
}) => void;
|
|
24
|
+
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
|
-
export {
|
|
27
|
+
export { useRovingFocus };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
id:
|
|
3
|
+
interface Item {
|
|
4
|
+
id: string;
|
|
5
5
|
ref: React.RefObject<HTMLElement>;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
declare
|
|
7
|
+
}
|
|
8
|
+
declare function useRovingFocus(): {
|
|
9
|
+
activeId: string | null;
|
|
10
|
+
hasInteracted: boolean;
|
|
9
11
|
registerItem: (item: Item) => void;
|
|
10
|
-
unregisterItem: (id:
|
|
11
|
-
|
|
12
|
-
setActiveId: react.Dispatch<react.SetStateAction<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
unregisterItem: (id: string) => void;
|
|
13
|
+
move: (dir: 1 | -1) => void;
|
|
14
|
+
setActiveId: react.Dispatch<react.SetStateAction<string | null>>;
|
|
15
|
+
focusItem: (itemId: string) => void;
|
|
16
|
+
focusFirst: () => void;
|
|
17
|
+
getFirstEnabledId: () => string;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
20
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (value: string) => void, options?: {
|
|
21
|
+
role?: "radio" | "option";
|
|
22
|
+
allowHorizontal?: boolean;
|
|
23
|
+
}) => void;
|
|
24
|
+
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
|
-
export {
|
|
27
|
+
export { useRovingFocus };
|
package/dist/index.js
CHANGED
|
@@ -3,64 +3,130 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
|
|
6
|
+
function useRovingFocus() {
|
|
7
7
|
const items = react.useRef([]);
|
|
8
8
|
const [activeId, setActiveId] = react.useState(null);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const [hasInteracted, setHasInteracted] = react.useState(false);
|
|
10
|
+
const registerItem = react.useCallback(
|
|
11
|
+
(item) => {
|
|
12
|
+
const existingIndex = items.current.findIndex((i) => i.id === item.id);
|
|
13
|
+
if (existingIndex === -1) {
|
|
14
|
+
items.current.push(item);
|
|
15
|
+
} else {
|
|
16
|
+
items.current[existingIndex] = item;
|
|
17
|
+
}
|
|
18
|
+
if (!activeId && !item.disabled) {
|
|
19
|
+
setActiveId(item.id);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
[activeId]
|
|
23
|
+
);
|
|
13
24
|
const unregisterItem = react.useCallback((id) => {
|
|
14
25
|
items.current = items.current.filter((i) => i.id !== id);
|
|
15
26
|
}, []);
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
item.ref.current?.focus();
|
|
20
|
-
}, []);
|
|
21
|
-
const moveFocus = react.useCallback(
|
|
22
|
-
(offset) => {
|
|
27
|
+
const move = react.useCallback(
|
|
28
|
+
(dir) => {
|
|
29
|
+
setHasInteracted(true);
|
|
23
30
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
24
31
|
if (!enabled.length) return;
|
|
25
32
|
const index = enabled.findIndex((i) => i.id === activeId);
|
|
26
|
-
const
|
|
27
|
-
|
|
33
|
+
const next = enabled[(index + dir + enabled.length) % enabled.length];
|
|
34
|
+
setActiveId(next.id);
|
|
35
|
+
next.ref.current?.focus();
|
|
36
|
+
},
|
|
37
|
+
[activeId]
|
|
38
|
+
);
|
|
39
|
+
const focusItem = react.useCallback((itemId) => {
|
|
40
|
+
setHasInteracted(true);
|
|
41
|
+
setActiveId(itemId);
|
|
42
|
+
}, []);
|
|
43
|
+
const focusFirst = react.useCallback(() => {
|
|
44
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
45
|
+
if (enabled.length > 0) {
|
|
46
|
+
const first = enabled[0];
|
|
47
|
+
setHasInteracted(true);
|
|
48
|
+
setActiveId(first.id);
|
|
49
|
+
first.ref.current?.focus();
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
const getFirstEnabledId = react.useCallback(() => {
|
|
53
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
54
|
+
return enabled[0]?.id ?? null;
|
|
55
|
+
}, []);
|
|
56
|
+
const reset = react.useCallback(() => {
|
|
57
|
+
setHasInteracted(false);
|
|
58
|
+
}, []);
|
|
59
|
+
const handleGroupKeyDown = react.useCallback(
|
|
60
|
+
(e) => {
|
|
61
|
+
if (!hasInteracted) {
|
|
62
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
focusFirst();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
28
67
|
},
|
|
29
|
-
[
|
|
68
|
+
[hasInteracted, focusFirst]
|
|
30
69
|
);
|
|
31
|
-
const
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
const handleItemKeyDown = react.useCallback(
|
|
71
|
+
(e, itemId, onSelect, options) => {
|
|
72
|
+
const role = options?.role ?? "radio";
|
|
73
|
+
const allowHorizontal = options?.allowHorizontal ?? true;
|
|
74
|
+
if (e.key === "Tab" && e.shiftKey) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
reset();
|
|
77
|
+
if (role === "radio") {
|
|
78
|
+
const fieldset = e.target.closest('[role="radiogroup"]');
|
|
79
|
+
fieldset?.focus();
|
|
80
|
+
} else if (role === "option") {
|
|
81
|
+
const listbox = e.target.closest('[role="listbox"]');
|
|
82
|
+
listbox?.focus();
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (e.key === "ArrowDown" || allowHorizontal && e.key === "ArrowRight") {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
move(1);
|
|
89
|
+
return;
|
|
36
90
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
91
|
+
if (e.key === "ArrowUp" || allowHorizontal && e.key === "ArrowLeft") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
move(-1);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (e.key === " " || e.key === "Enter") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
onSelect?.(itemId);
|
|
40
99
|
}
|
|
41
100
|
},
|
|
42
|
-
[
|
|
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]
|
|
43
113
|
);
|
|
44
|
-
const onFocus = react.useCallback((event) => {
|
|
45
|
-
if (event.target === event.currentTarget) {
|
|
46
|
-
setActiveId(null);
|
|
47
|
-
}
|
|
48
|
-
}, []);
|
|
49
|
-
const onBlur = react.useCallback((event) => {
|
|
50
|
-
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
51
|
-
setActiveId(null);
|
|
52
|
-
}
|
|
53
|
-
}, []);
|
|
54
114
|
return {
|
|
115
|
+
activeId,
|
|
116
|
+
hasInteracted,
|
|
55
117
|
registerItem,
|
|
56
118
|
unregisterItem,
|
|
57
|
-
|
|
119
|
+
move,
|
|
58
120
|
setActiveId,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
focusItem,
|
|
122
|
+
focusFirst,
|
|
123
|
+
getFirstEnabledId,
|
|
124
|
+
reset,
|
|
125
|
+
handleGroupKeyDown,
|
|
126
|
+
handleItemKeyDown,
|
|
127
|
+
handleGroupBlur
|
|
62
128
|
};
|
|
63
|
-
}
|
|
129
|
+
}
|
|
64
130
|
|
|
65
131
|
exports.useRovingFocus = useRovingFocus;
|
|
66
132
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["useRef","useState","useCallback"],"mappings":";;;;;AAQO,
|
|
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\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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,64 +1,130 @@
|
|
|
1
1
|
import { useRef, useState, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
|
|
4
|
+
function useRovingFocus() {
|
|
5
5
|
const items = useRef([]);
|
|
6
6
|
const [activeId, setActiveId] = useState(null);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
8
|
+
const registerItem = useCallback(
|
|
9
|
+
(item) => {
|
|
10
|
+
const existingIndex = items.current.findIndex((i) => i.id === item.id);
|
|
11
|
+
if (existingIndex === -1) {
|
|
12
|
+
items.current.push(item);
|
|
13
|
+
} else {
|
|
14
|
+
items.current[existingIndex] = item;
|
|
15
|
+
}
|
|
16
|
+
if (!activeId && !item.disabled) {
|
|
17
|
+
setActiveId(item.id);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
[activeId]
|
|
21
|
+
);
|
|
11
22
|
const unregisterItem = useCallback((id) => {
|
|
12
23
|
items.current = items.current.filter((i) => i.id !== id);
|
|
13
24
|
}, []);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
item.ref.current?.focus();
|
|
18
|
-
}, []);
|
|
19
|
-
const moveFocus = useCallback(
|
|
20
|
-
(offset) => {
|
|
25
|
+
const move = useCallback(
|
|
26
|
+
(dir) => {
|
|
27
|
+
setHasInteracted(true);
|
|
21
28
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
22
29
|
if (!enabled.length) return;
|
|
23
30
|
const index = enabled.findIndex((i) => i.id === activeId);
|
|
24
|
-
const
|
|
25
|
-
|
|
31
|
+
const next = enabled[(index + dir + enabled.length) % enabled.length];
|
|
32
|
+
setActiveId(next.id);
|
|
33
|
+
next.ref.current?.focus();
|
|
34
|
+
},
|
|
35
|
+
[activeId]
|
|
36
|
+
);
|
|
37
|
+
const focusItem = useCallback((itemId) => {
|
|
38
|
+
setHasInteracted(true);
|
|
39
|
+
setActiveId(itemId);
|
|
40
|
+
}, []);
|
|
41
|
+
const focusFirst = useCallback(() => {
|
|
42
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
43
|
+
if (enabled.length > 0) {
|
|
44
|
+
const first = enabled[0];
|
|
45
|
+
setHasInteracted(true);
|
|
46
|
+
setActiveId(first.id);
|
|
47
|
+
first.ref.current?.focus();
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
const getFirstEnabledId = useCallback(() => {
|
|
51
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
52
|
+
return enabled[0]?.id ?? null;
|
|
53
|
+
}, []);
|
|
54
|
+
const reset = useCallback(() => {
|
|
55
|
+
setHasInteracted(false);
|
|
56
|
+
}, []);
|
|
57
|
+
const handleGroupKeyDown = useCallback(
|
|
58
|
+
(e) => {
|
|
59
|
+
if (!hasInteracted) {
|
|
60
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
focusFirst();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
26
65
|
},
|
|
27
|
-
[
|
|
66
|
+
[hasInteracted, focusFirst]
|
|
28
67
|
);
|
|
29
|
-
const
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
68
|
+
const handleItemKeyDown = useCallback(
|
|
69
|
+
(e, itemId, onSelect, options) => {
|
|
70
|
+
const role = options?.role ?? "radio";
|
|
71
|
+
const allowHorizontal = options?.allowHorizontal ?? true;
|
|
72
|
+
if (e.key === "Tab" && e.shiftKey) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
reset();
|
|
75
|
+
if (role === "radio") {
|
|
76
|
+
const fieldset = e.target.closest('[role="radiogroup"]');
|
|
77
|
+
fieldset?.focus();
|
|
78
|
+
} else if (role === "option") {
|
|
79
|
+
const listbox = e.target.closest('[role="listbox"]');
|
|
80
|
+
listbox?.focus();
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (e.key === "ArrowDown" || allowHorizontal && e.key === "ArrowRight") {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
move(1);
|
|
87
|
+
return;
|
|
34
88
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
89
|
+
if (e.key === "ArrowUp" || allowHorizontal && e.key === "ArrowLeft") {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
move(-1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (e.key === " " || e.key === "Enter") {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
onSelect?.(itemId);
|
|
38
97
|
}
|
|
39
98
|
},
|
|
40
|
-
[
|
|
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]
|
|
41
111
|
);
|
|
42
|
-
const onFocus = useCallback((event) => {
|
|
43
|
-
if (event.target === event.currentTarget) {
|
|
44
|
-
setActiveId(null);
|
|
45
|
-
}
|
|
46
|
-
}, []);
|
|
47
|
-
const onBlur = useCallback((event) => {
|
|
48
|
-
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
49
|
-
setActiveId(null);
|
|
50
|
-
}
|
|
51
|
-
}, []);
|
|
52
112
|
return {
|
|
113
|
+
activeId,
|
|
114
|
+
hasInteracted,
|
|
53
115
|
registerItem,
|
|
54
116
|
unregisterItem,
|
|
55
|
-
|
|
117
|
+
move,
|
|
56
118
|
setActiveId,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
focusItem,
|
|
120
|
+
focusFirst,
|
|
121
|
+
getFirstEnabledId,
|
|
122
|
+
reset,
|
|
123
|
+
handleGroupKeyDown,
|
|
124
|
+
handleItemKeyDown,
|
|
125
|
+
handleGroupBlur
|
|
60
126
|
};
|
|
61
|
-
}
|
|
127
|
+
}
|
|
62
128
|
|
|
63
129
|
export { useRovingFocus };
|
|
64
130
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAQO,
|
|
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"]}
|