@negative-space/roving-focus 1.0.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 +23 -14
- package/dist/index.d.ts +23 -14
- package/dist/index.js +143 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +143 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type RovingFocusItem<Id extends string = string> = {
|
|
4
|
-
id: Id;
|
|
1
|
+
interface RovingFocusItem {
|
|
2
|
+
id: string;
|
|
5
3
|
ref: React.RefObject<HTMLElement>;
|
|
6
4
|
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface UseRovingFocusOptions {
|
|
7
|
+
wrap?: boolean;
|
|
8
|
+
allowHorizontal?: boolean;
|
|
9
|
+
containerRole?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function useRovingFocus(options?: UseRovingFocusOptions): {
|
|
12
|
+
activeId: string | null;
|
|
13
|
+
hasInteracted: boolean;
|
|
14
|
+
registerItem: (item: RovingFocusItem) => void;
|
|
15
|
+
unregisterItem: (id: string) => void;
|
|
16
|
+
focusItem: (id: string) => void;
|
|
17
|
+
focusFirst: () => void;
|
|
18
|
+
focusLast: () => void;
|
|
19
|
+
getFirstEnabledId: () => string | null;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
22
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (id: string) => void, containerRef?: React.RefObject<HTMLElement>) => void;
|
|
23
|
+
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
7
24
|
};
|
|
8
|
-
|
|
9
|
-
registerItem: (item: Item) => void;
|
|
10
|
-
unregisterItem: (id: Id) => void;
|
|
11
|
-
activeId: Id | null;
|
|
12
|
-
setActiveId: react.Dispatch<react.SetStateAction<Id | null>>;
|
|
13
|
-
onKeyDown: (event: React.KeyboardEvent) => void;
|
|
14
|
-
onFocus: (event: React.FocusEvent) => void;
|
|
15
|
-
onBlur: (event: React.FocusEvent) => void;
|
|
16
|
-
};
|
|
25
|
+
type RovingFocusReturn = ReturnType<typeof useRovingFocus>;
|
|
17
26
|
|
|
18
|
-
export { type RovingFocusItem, useRovingFocus };
|
|
27
|
+
export { type RovingFocusItem, type RovingFocusReturn, type UseRovingFocusOptions, useRovingFocus };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type RovingFocusItem<Id extends string = string> = {
|
|
4
|
-
id: Id;
|
|
1
|
+
interface RovingFocusItem {
|
|
2
|
+
id: string;
|
|
5
3
|
ref: React.RefObject<HTMLElement>;
|
|
6
4
|
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface UseRovingFocusOptions {
|
|
7
|
+
wrap?: boolean;
|
|
8
|
+
allowHorizontal?: boolean;
|
|
9
|
+
containerRole?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function useRovingFocus(options?: UseRovingFocusOptions): {
|
|
12
|
+
activeId: string | null;
|
|
13
|
+
hasInteracted: boolean;
|
|
14
|
+
registerItem: (item: RovingFocusItem) => void;
|
|
15
|
+
unregisterItem: (id: string) => void;
|
|
16
|
+
focusItem: (id: string) => void;
|
|
17
|
+
focusFirst: () => void;
|
|
18
|
+
focusLast: () => void;
|
|
19
|
+
getFirstEnabledId: () => string | null;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
handleGroupKeyDown: (e: React.KeyboardEvent) => void;
|
|
22
|
+
handleItemKeyDown: (e: React.KeyboardEvent, itemId: string, onSelect?: (id: string) => void, containerRef?: React.RefObject<HTMLElement>) => void;
|
|
23
|
+
handleGroupBlur: (e: React.FocusEvent) => void;
|
|
7
24
|
};
|
|
8
|
-
|
|
9
|
-
registerItem: (item: Item) => void;
|
|
10
|
-
unregisterItem: (id: Id) => void;
|
|
11
|
-
activeId: Id | null;
|
|
12
|
-
setActiveId: react.Dispatch<react.SetStateAction<Id | null>>;
|
|
13
|
-
onKeyDown: (event: React.KeyboardEvent) => void;
|
|
14
|
-
onFocus: (event: React.FocusEvent) => void;
|
|
15
|
-
onBlur: (event: React.FocusEvent) => void;
|
|
16
|
-
};
|
|
25
|
+
type RovingFocusReturn = ReturnType<typeof useRovingFocus>;
|
|
17
26
|
|
|
18
|
-
export { type RovingFocusItem, useRovingFocus };
|
|
27
|
+
export { type RovingFocusItem, type RovingFocusReturn, type UseRovingFocusOptions, useRovingFocus };
|
package/dist/index.js
CHANGED
|
@@ -3,64 +3,164 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
|
|
6
|
+
function useRovingFocus(options = {}) {
|
|
7
|
+
const { wrap = true, allowHorizontal = true, containerRole } = options;
|
|
7
8
|
const items = react.useRef([]);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
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);
|
|
12
16
|
}, []);
|
|
13
|
-
const
|
|
14
|
-
|
|
17
|
+
const setHasInteracted = react.useCallback((value) => {
|
|
18
|
+
hasInteractedRef.current = value;
|
|
19
|
+
_setHasInteracted(value);
|
|
15
20
|
}, []);
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const registerItem = react.useCallback(
|
|
22
|
+
(item) => {
|
|
23
|
+
const idx = items.current.findIndex((i) => i.id === item.id);
|
|
24
|
+
if (idx === -1) {
|
|
25
|
+
items.current.push(item);
|
|
26
|
+
} else {
|
|
27
|
+
items.current[idx] = item;
|
|
28
|
+
}
|
|
29
|
+
if (!activeIdRef.current && !item.disabled) {
|
|
30
|
+
setActiveId(item.id);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
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]
|
|
44
|
+
);
|
|
45
|
+
const move = react.useCallback(
|
|
46
|
+
(dir) => {
|
|
47
|
+
setHasInteracted(true);
|
|
23
48
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
24
49
|
if (!enabled.length) return;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
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];
|
|
53
|
+
setActiveId(next.id);
|
|
54
|
+
next.ref.current?.focus();
|
|
55
|
+
},
|
|
56
|
+
[wrap, setActiveId, setHasInteracted]
|
|
57
|
+
);
|
|
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(() => {
|
|
66
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
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) => {
|
|
75
|
+
setHasInteracted(true);
|
|
76
|
+
setActiveId(id);
|
|
28
77
|
},
|
|
29
|
-
[
|
|
78
|
+
[setActiveId, setHasInteracted]
|
|
79
|
+
);
|
|
80
|
+
const getFirstEnabledId = react.useCallback(
|
|
81
|
+
() => items.current.find((i) => !i.disabled)?.id ?? null,
|
|
82
|
+
[]
|
|
30
83
|
);
|
|
31
|
-
const
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
const reset = react.useCallback(() => {
|
|
85
|
+
setHasInteracted(false);
|
|
86
|
+
}, [setHasInteracted]);
|
|
87
|
+
const handleGroupKeyDown = react.useCallback(
|
|
88
|
+
(e) => {
|
|
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);
|
|
104
|
+
},
|
|
105
|
+
[reset]
|
|
106
|
+
);
|
|
107
|
+
const handleItemKeyDown = react.useCallback(
|
|
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();
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (e.key === "ArrowDown" || allowHorizontal && e.key === "ArrowRight") {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
move(1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (e.key === "ArrowUp" || allowHorizontal && e.key === "ArrowLeft") {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
move(-1);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (e.key === "Home") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
focusFirst();
|
|
135
|
+
return;
|
|
36
136
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
137
|
+
if (e.key === "End") {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
focusLast();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
onSelect?.(itemId);
|
|
40
145
|
}
|
|
41
146
|
},
|
|
42
|
-
[
|
|
147
|
+
[allowHorizontal, move, focusFirst, focusLast, reset, containerRole]
|
|
43
148
|
);
|
|
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
149
|
return {
|
|
150
|
+
activeId,
|
|
151
|
+
hasInteracted,
|
|
55
152
|
registerItem,
|
|
56
153
|
unregisterItem,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
154
|
+
focusItem,
|
|
155
|
+
focusFirst,
|
|
156
|
+
focusLast,
|
|
157
|
+
getFirstEnabledId,
|
|
158
|
+
reset,
|
|
159
|
+
handleGroupKeyDown,
|
|
160
|
+
handleItemKeyDown,
|
|
161
|
+
handleGroupBlur
|
|
62
162
|
};
|
|
63
|
-
}
|
|
163
|
+
}
|
|
64
164
|
|
|
65
165
|
exports.useRovingFocus = useRovingFocus;
|
|
66
166
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 type RovingFocusItem<Id extends string = string> = {\n id: Id\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport const useRovingFocus = <Item extends RovingFocusItem<Id>, Id extends string = string>() => {\n const items = useRef<Item[]>([])\n const [activeId, setActiveId] = useState<Id | null>(null)\n\n const registerItem = useCallback((item: Item) => {\n if (items.current.some((i) => i.id === item.id)) return\n items.current = [...items.current, item]\n }, [])\n\n const unregisterItem = useCallback((id: Id) => {\n items.current = items.current.filter((i) => i.id !== id)\n }, [])\n\n const focusItem = useCallback((item?: Item) => {\n if (!item || item.disabled) return\n setActiveId(item.id)\n item.ref.current?.focus()\n }, [])\n\n const moveFocus = useCallback(\n (offset: number) => {\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 nextIndex = index === -1 ? 0 : (index + offset + enabled.length) % enabled.length\n\n focusItem(enabled[nextIndex])\n },\n [activeId, focusItem]\n )\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n moveFocus(1)\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n moveFocus(-1)\n }\n },\n [moveFocus]\n )\n\n const onFocus = useCallback((event: React.FocusEvent) => {\n if (event.target === event.currentTarget) {\n setActiveId(null)\n }\n }, [])\n\n const onBlur = useCallback((event: React.FocusEvent) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setActiveId(null)\n }\n }, [])\n\n return {\n registerItem,\n unregisterItem,\n activeId,\n setActiveId,\n onKeyDown,\n onFocus,\n onBlur\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,64 +1,164 @@
|
|
|
1
1
|
import { useRef, useState, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
|
|
4
|
+
function useRovingFocus(options = {}) {
|
|
5
|
+
const { wrap = true, allowHorizontal = true, containerRole } = options;
|
|
5
6
|
const items = useRef([]);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
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);
|
|
10
14
|
}, []);
|
|
11
|
-
const
|
|
12
|
-
|
|
15
|
+
const setHasInteracted = useCallback((value) => {
|
|
16
|
+
hasInteractedRef.current = value;
|
|
17
|
+
_setHasInteracted(value);
|
|
13
18
|
}, []);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const registerItem = useCallback(
|
|
20
|
+
(item) => {
|
|
21
|
+
const idx = items.current.findIndex((i) => i.id === item.id);
|
|
22
|
+
if (idx === -1) {
|
|
23
|
+
items.current.push(item);
|
|
24
|
+
} else {
|
|
25
|
+
items.current[idx] = item;
|
|
26
|
+
}
|
|
27
|
+
if (!activeIdRef.current && !item.disabled) {
|
|
28
|
+
setActiveId(item.id);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
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]
|
|
42
|
+
);
|
|
43
|
+
const move = useCallback(
|
|
44
|
+
(dir) => {
|
|
45
|
+
setHasInteracted(true);
|
|
21
46
|
const enabled = items.current.filter((i) => !i.disabled);
|
|
22
47
|
if (!enabled.length) return;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
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];
|
|
51
|
+
setActiveId(next.id);
|
|
52
|
+
next.ref.current?.focus();
|
|
53
|
+
},
|
|
54
|
+
[wrap, setActiveId, setHasInteracted]
|
|
55
|
+
);
|
|
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(() => {
|
|
64
|
+
const enabled = items.current.filter((i) => !i.disabled);
|
|
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) => {
|
|
73
|
+
setHasInteracted(true);
|
|
74
|
+
setActiveId(id);
|
|
26
75
|
},
|
|
27
|
-
[
|
|
76
|
+
[setActiveId, setHasInteracted]
|
|
77
|
+
);
|
|
78
|
+
const getFirstEnabledId = useCallback(
|
|
79
|
+
() => items.current.find((i) => !i.disabled)?.id ?? null,
|
|
80
|
+
[]
|
|
28
81
|
);
|
|
29
|
-
const
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
const reset = useCallback(() => {
|
|
83
|
+
setHasInteracted(false);
|
|
84
|
+
}, [setHasInteracted]);
|
|
85
|
+
const handleGroupKeyDown = useCallback(
|
|
86
|
+
(e) => {
|
|
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);
|
|
102
|
+
},
|
|
103
|
+
[reset]
|
|
104
|
+
);
|
|
105
|
+
const handleItemKeyDown = useCallback(
|
|
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();
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (e.key === "ArrowDown" || allowHorizontal && e.key === "ArrowRight") {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
move(1);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (e.key === "ArrowUp" || allowHorizontal && e.key === "ArrowLeft") {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
move(-1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (e.key === "Home") {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
focusFirst();
|
|
133
|
+
return;
|
|
34
134
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
135
|
+
if (e.key === "End") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
focusLast();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
onSelect?.(itemId);
|
|
38
143
|
}
|
|
39
144
|
},
|
|
40
|
-
[
|
|
145
|
+
[allowHorizontal, move, focusFirst, focusLast, reset, containerRole]
|
|
41
146
|
);
|
|
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
147
|
return {
|
|
148
|
+
activeId,
|
|
149
|
+
hasInteracted,
|
|
53
150
|
registerItem,
|
|
54
151
|
unregisterItem,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
152
|
+
focusItem,
|
|
153
|
+
focusFirst,
|
|
154
|
+
focusLast,
|
|
155
|
+
getFirstEnabledId,
|
|
156
|
+
reset,
|
|
157
|
+
handleGroupKeyDown,
|
|
158
|
+
handleItemKeyDown,
|
|
159
|
+
handleGroupBlur
|
|
60
160
|
};
|
|
61
|
-
}
|
|
161
|
+
}
|
|
62
162
|
|
|
63
163
|
export { useRovingFocus };
|
|
64
164
|
//# sourceMappingURL=index.mjs.map
|
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\nexport type RovingFocusItem<Id extends string = string> = {\n id: Id\n ref: React.RefObject<HTMLElement>\n disabled?: boolean\n}\n\nexport const useRovingFocus = <Item extends RovingFocusItem<Id>, Id extends string = string>() => {\n const items = useRef<Item[]>([])\n const [activeId, setActiveId] = useState<Id | null>(null)\n\n const registerItem = useCallback((item: Item) => {\n if (items.current.some((i) => i.id === item.id)) return\n items.current = [...items.current, item]\n }, [])\n\n const unregisterItem = useCallback((id: Id) => {\n items.current = items.current.filter((i) => i.id !== id)\n }, [])\n\n const focusItem = useCallback((item?: Item) => {\n if (!item || item.disabled) return\n setActiveId(item.id)\n item.ref.current?.focus()\n }, [])\n\n const moveFocus = useCallback(\n (offset: number) => {\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 nextIndex = index === -1 ? 0 : (index + offset + enabled.length) % enabled.length\n\n focusItem(enabled[nextIndex])\n },\n [activeId, focusItem]\n )\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n moveFocus(1)\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n moveFocus(-1)\n }\n },\n [moveFocus]\n )\n\n const onFocus = useCallback((event: React.FocusEvent) => {\n if (event.target === event.currentTarget) {\n setActiveId(null)\n }\n }, [])\n\n const onBlur = useCallback((event: React.FocusEvent) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setActiveId(null)\n }\n }, [])\n\n return {\n registerItem,\n unregisterItem,\n activeId,\n setActiveId,\n onKeyDown,\n onFocus,\n onBlur\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"]}
|