@radix-solid-js/focus-scope 0.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.cjs +208 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +206 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var solidJs = require('solid-js');
|
|
4
|
+
var composeRefs = require('@radix-solid-js/compose-refs');
|
|
5
|
+
var primitiveComponent = require('@radix-solid-js/primitive-component');
|
|
6
|
+
|
|
7
|
+
// src/focus-scope.tsx
|
|
8
|
+
var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
|
|
9
|
+
var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
|
|
10
|
+
var EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
11
|
+
function FocusScope(inProps) {
|
|
12
|
+
const [local, rest] = solidJs.splitProps(inProps, [
|
|
13
|
+
"loop",
|
|
14
|
+
"trapped",
|
|
15
|
+
"onMountAutoFocus",
|
|
16
|
+
"onUnmountAutoFocus",
|
|
17
|
+
"ref"
|
|
18
|
+
]);
|
|
19
|
+
const loop = () => local.loop ?? false;
|
|
20
|
+
const trapped = () => local.trapped ?? false;
|
|
21
|
+
const [container, setContainer] = solidJs.createSignal(null);
|
|
22
|
+
let lastFocusedElementRef = null;
|
|
23
|
+
const focusScope = {
|
|
24
|
+
paused: false,
|
|
25
|
+
pause() {
|
|
26
|
+
this.paused = true;
|
|
27
|
+
},
|
|
28
|
+
resume() {
|
|
29
|
+
this.paused = false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
solidJs.createEffect(() => {
|
|
33
|
+
if (!trapped()) return;
|
|
34
|
+
const el = container();
|
|
35
|
+
if (!el) return;
|
|
36
|
+
function handleFocusIn(event) {
|
|
37
|
+
if (focusScope.paused || !el) return;
|
|
38
|
+
const target = event.target;
|
|
39
|
+
if (el.contains(target)) {
|
|
40
|
+
lastFocusedElementRef = target;
|
|
41
|
+
} else {
|
|
42
|
+
focus(lastFocusedElementRef, { select: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function handleFocusOut(event) {
|
|
46
|
+
if (focusScope.paused || !el) return;
|
|
47
|
+
const relatedTarget = event.relatedTarget;
|
|
48
|
+
if (relatedTarget === null) return;
|
|
49
|
+
if (!el.contains(relatedTarget)) {
|
|
50
|
+
focus(lastFocusedElementRef, { select: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function handleMutations(mutations) {
|
|
54
|
+
const focusedElement = document.activeElement;
|
|
55
|
+
if (focusedElement !== document.body) return;
|
|
56
|
+
for (const mutation of mutations) {
|
|
57
|
+
if (mutation.removedNodes.length > 0) focus(el);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
document.addEventListener("focusin", handleFocusIn);
|
|
61
|
+
document.addEventListener("focusout", handleFocusOut);
|
|
62
|
+
const mutationObserver = new MutationObserver(handleMutations);
|
|
63
|
+
if (el) mutationObserver.observe(el, { childList: true, subtree: true });
|
|
64
|
+
solidJs.onCleanup(() => {
|
|
65
|
+
document.removeEventListener("focusin", handleFocusIn);
|
|
66
|
+
document.removeEventListener("focusout", handleFocusOut);
|
|
67
|
+
mutationObserver.disconnect();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
solidJs.createEffect(() => {
|
|
71
|
+
const el = container();
|
|
72
|
+
if (!el) return;
|
|
73
|
+
focusScopesStack.add(focusScope);
|
|
74
|
+
const previouslyFocusedElement = document.activeElement;
|
|
75
|
+
const hasFocusedCandidate = el.contains(previouslyFocusedElement);
|
|
76
|
+
if (!hasFocusedCandidate) {
|
|
77
|
+
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
|
|
78
|
+
el.addEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus || (() => {
|
|
79
|
+
}));
|
|
80
|
+
el.dispatchEvent(mountEvent);
|
|
81
|
+
if (!mountEvent.defaultPrevented) {
|
|
82
|
+
focusFirst(removeLinks(getTabbableCandidates(el)), { select: true });
|
|
83
|
+
if (document.activeElement === previouslyFocusedElement) {
|
|
84
|
+
focus(el);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
solidJs.onCleanup(() => {
|
|
89
|
+
el.removeEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus || (() => {
|
|
90
|
+
}));
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);
|
|
93
|
+
const handler = local.onUnmountAutoFocus || (() => {
|
|
94
|
+
});
|
|
95
|
+
el.addEventListener(AUTOFOCUS_ON_UNMOUNT, handler);
|
|
96
|
+
el.dispatchEvent(unmountEvent);
|
|
97
|
+
if (!unmountEvent.defaultPrevented) {
|
|
98
|
+
focus(previouslyFocusedElement ?? document.body, { select: true });
|
|
99
|
+
}
|
|
100
|
+
el.removeEventListener(AUTOFOCUS_ON_UNMOUNT, handler);
|
|
101
|
+
focusScopesStack.remove(focusScope);
|
|
102
|
+
}, 0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
const handleKeyDown = (event) => {
|
|
106
|
+
if (!loop() && !trapped()) return;
|
|
107
|
+
if (focusScope.paused) return;
|
|
108
|
+
const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
109
|
+
const focusedElement = document.activeElement;
|
|
110
|
+
if (isTabKey && focusedElement) {
|
|
111
|
+
const el = event.currentTarget;
|
|
112
|
+
const [first, last] = getTabbableEdges(el);
|
|
113
|
+
const hasTabbableElementsInside = first && last;
|
|
114
|
+
if (!hasTabbableElementsInside) {
|
|
115
|
+
if (focusedElement === el) event.preventDefault();
|
|
116
|
+
} else {
|
|
117
|
+
if (!event.shiftKey && focusedElement === last) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
if (loop()) focus(first, { select: true });
|
|
120
|
+
} else if (event.shiftKey && focusedElement === first) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
if (loop()) focus(last, { select: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return /* @__PURE__ */ React.createElement(
|
|
128
|
+
primitiveComponent.Primitive.div,
|
|
129
|
+
{
|
|
130
|
+
tabIndex: -1,
|
|
131
|
+
...rest,
|
|
132
|
+
ref: composeRefs.mergeRefs(local.ref, (node) => setContainer(node)),
|
|
133
|
+
onKeyDown: handleKeyDown
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function focusFirst(candidates, { select = false } = {}) {
|
|
138
|
+
const previouslyFocusedElement = document.activeElement;
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
focus(candidate, { select });
|
|
141
|
+
if (document.activeElement !== previouslyFocusedElement) return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function getTabbableEdges(container) {
|
|
145
|
+
const candidates = getTabbableCandidates(container);
|
|
146
|
+
const first = findVisible(candidates, container);
|
|
147
|
+
const last = findVisible(candidates.reverse(), container);
|
|
148
|
+
return [first, last];
|
|
149
|
+
}
|
|
150
|
+
function getTabbableCandidates(container) {
|
|
151
|
+
const nodes = [];
|
|
152
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
153
|
+
acceptNode: (node) => {
|
|
154
|
+
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
|
|
155
|
+
if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
|
|
156
|
+
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
160
|
+
return nodes;
|
|
161
|
+
}
|
|
162
|
+
function findVisible(elements, container) {
|
|
163
|
+
for (const element of elements) {
|
|
164
|
+
if (!isHidden(element, { upTo: container })) return element;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function isHidden(node, { upTo }) {
|
|
168
|
+
if (getComputedStyle(node).visibility === "hidden") return true;
|
|
169
|
+
while (node) {
|
|
170
|
+
if (upTo !== void 0 && node === upTo) return false;
|
|
171
|
+
if (getComputedStyle(node).display === "none") return true;
|
|
172
|
+
node = node.parentElement;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function isSelectableInput(element) {
|
|
177
|
+
return element instanceof HTMLInputElement && "select" in element;
|
|
178
|
+
}
|
|
179
|
+
function focus(element, { select = false } = {}) {
|
|
180
|
+
if (element && element.focus) {
|
|
181
|
+
const previouslyFocusedElement = document.activeElement;
|
|
182
|
+
element.focus({ preventScroll: true });
|
|
183
|
+
if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
var focusScopesStack = createFocusScopesStack();
|
|
187
|
+
function createFocusScopesStack() {
|
|
188
|
+
let stack = [];
|
|
189
|
+
return {
|
|
190
|
+
add(focusScope) {
|
|
191
|
+
const activeFocusScope = stack[0];
|
|
192
|
+
if (focusScope !== activeFocusScope) activeFocusScope?.pause();
|
|
193
|
+
stack = stack.filter((s) => s !== focusScope);
|
|
194
|
+
stack.unshift(focusScope);
|
|
195
|
+
},
|
|
196
|
+
remove(focusScope) {
|
|
197
|
+
stack = stack.filter((s) => s !== focusScope);
|
|
198
|
+
stack[0]?.resume();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function removeLinks(items) {
|
|
203
|
+
return items.filter((item) => item.tagName !== "A");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
exports.FocusScope = FocusScope;
|
|
207
|
+
//# sourceMappingURL=index.cjs.map
|
|
208
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/focus-scope.tsx"],"names":["splitProps","createSignal","createEffect","onCleanup","Primitive","mergeRefs"],"mappings":"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","file":"index.cjs","sourcesContent":["import { type JSX, createSignal, createEffect, onCleanup, splitProps } from 'solid-js';\nimport { mergeRefs } from '@radix-solid-js/compose-refs';\nimport { Primitive } from '@radix-solid-js/primitive-component';\n\nconst AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';\nconst AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype FocusableTarget = HTMLElement | { focus(): void };\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope\n * -----------------------------------------------------------------------------------------------*/\n\ninterface FocusScopeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** When true, tabbing from last item will focus first and vice versa */\n loop?: boolean;\n /** When true, focus cannot escape the scope */\n trapped?: boolean;\n /** Event handler called when auto-focusing on mount */\n onMountAutoFocus?: (event: Event) => void;\n /** Event handler called when auto-focusing on unmount */\n onUnmountAutoFocus?: (event: Event) => void;\n ref?: (el: HTMLDivElement) => void;\n}\n\nfunction FocusScope(inProps: FocusScopeProps) {\n const [local, rest] = splitProps(inProps, [\n 'loop', 'trapped', 'onMountAutoFocus', 'onUnmountAutoFocus', 'ref',\n ]);\n\n const loop = () => local.loop ?? false;\n const trapped = () => local.trapped ?? false;\n\n const [container, setContainer] = createSignal<HTMLElement | null>(null);\n let lastFocusedElementRef: HTMLElement | null = null;\n\n const focusScope = {\n paused: false,\n pause() { this.paused = true; },\n resume() { this.paused = false; },\n };\n\n // Focus trapping\n createEffect(() => {\n if (!trapped()) return;\n const el = container();\n if (!el) return;\n\n function handleFocusIn(event: FocusEvent) {\n if (focusScope.paused || !el) return;\n const target = event.target as HTMLElement | null;\n if (el.contains(target)) {\n lastFocusedElementRef = target;\n } else {\n focus(lastFocusedElementRef, { select: true });\n }\n }\n\n function handleFocusOut(event: FocusEvent) {\n if (focusScope.paused || !el) return;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n if (relatedTarget === null) return;\n if (!el.contains(relatedTarget)) {\n focus(lastFocusedElementRef, { select: true });\n }\n }\n\n function handleMutations(mutations: MutationRecord[]) {\n const focusedElement = document.activeElement as HTMLElement | null;\n if (focusedElement !== document.body) return;\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) focus(el);\n }\n }\n\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n const mutationObserver = new MutationObserver(handleMutations);\n if (el) mutationObserver.observe(el, { childList: true, subtree: true });\n\n onCleanup(() => {\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n mutationObserver.disconnect();\n });\n });\n\n // Auto focus on mount/unmount\n createEffect(() => {\n const el = container();\n if (!el) return;\n\n focusScopesStack.add(focusScope);\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = el.contains(previouslyFocusedElement);\n\n if (!hasFocusedCandidate) {\n const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);\n el.addEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus as EventListener || (() => {}));\n el.dispatchEvent(mountEvent);\n if (!mountEvent.defaultPrevented) {\n focusFirst(removeLinks(getTabbableCandidates(el)), { select: true });\n if (document.activeElement === previouslyFocusedElement) {\n focus(el);\n }\n }\n }\n\n onCleanup(() => {\n el.removeEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus as EventListener || (() => {}));\n setTimeout(() => {\n const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);\n const handler = local.onUnmountAutoFocus as EventListener || (() => {});\n el.addEventListener(AUTOFOCUS_ON_UNMOUNT, handler);\n el.dispatchEvent(unmountEvent);\n if (!unmountEvent.defaultPrevented) {\n focus(previouslyFocusedElement ?? document.body, { select: true });\n }\n el.removeEventListener(AUTOFOCUS_ON_UNMOUNT, handler);\n focusScopesStack.remove(focusScope);\n }, 0);\n });\n });\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!loop() && !trapped()) return;\n if (focusScope.paused) return;\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const el = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(el);\n const hasTabbableElementsInside = first && last;\n\n if (!hasTabbableElementsInside) {\n if (focusedElement === el) event.preventDefault();\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n if (loop()) focus(first, { select: true });\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n if (loop()) focus(last, { select: true });\n }\n }\n }\n };\n\n return (\n <Primitive.div\n tabIndex={-1}\n {...rest}\n ref={mergeRefs(local.ref as any, (node: HTMLElement) => setContainer(node))}\n onKeyDown={handleKeyDown}\n />\n );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\nfunction focusFirst(candidates: HTMLElement[], { select = false } = {}) {\n const previouslyFocusedElement = document.activeElement;\n for (const candidate of candidates) {\n focus(candidate, { select });\n if (document.activeElement !== previouslyFocusedElement) return;\n }\n}\n\nfunction getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates, container);\n const last = findVisible(candidates.reverse(), container);\n return [first, last] as const;\n}\n\nfunction getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: any) => {\n const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';\n if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;\n return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n },\n });\n while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement);\n return nodes;\n}\n\nfunction findVisible(elements: HTMLElement[], container: HTMLElement) {\n for (const element of elements) {\n if (!isHidden(element, { upTo: container })) return element;\n }\n}\n\nfunction isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {\n if (getComputedStyle(node).visibility === 'hidden') return true;\n while (node) {\n if (upTo !== undefined && node === upTo) return false;\n if (getComputedStyle(node).display === 'none') return true;\n node = node.parentElement as HTMLElement;\n }\n return false;\n}\n\nfunction isSelectableInput(element: any): element is FocusableTarget & { select: () => void } {\n return element instanceof HTMLInputElement && 'select' in element;\n}\n\nfunction focus(element?: FocusableTarget | null, { select = false } = {}) {\n if (element && element.focus) {\n const previouslyFocusedElement = document.activeElement;\n element.focus({ preventScroll: true });\n if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();\n }\n}\n\ntype FocusScopeAPI = { paused: boolean; pause(): void; resume(): void };\nconst focusScopesStack = createFocusScopesStack();\n\nfunction createFocusScopesStack() {\n let stack: FocusScopeAPI[] = [];\n return {\n add(focusScope: FocusScopeAPI) {\n const activeFocusScope = stack[0];\n if (focusScope !== activeFocusScope) activeFocusScope?.pause();\n stack = stack.filter((s) => s !== focusScope);\n stack.unshift(focusScope);\n },\n remove(focusScope: FocusScopeAPI) {\n stack = stack.filter((s) => s !== focusScope);\n stack[0]?.resume();\n },\n };\n}\n\nfunction removeLinks(items: HTMLElement[]) {\n return items.filter((item) => item.tagName !== 'A');\n}\n\nexport { FocusScope };\nexport type { FocusScopeProps };\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
interface FocusScopeProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** When true, tabbing from last item will focus first and vice versa */
|
|
5
|
+
loop?: boolean;
|
|
6
|
+
/** When true, focus cannot escape the scope */
|
|
7
|
+
trapped?: boolean;
|
|
8
|
+
/** Event handler called when auto-focusing on mount */
|
|
9
|
+
onMountAutoFocus?: (event: Event) => void;
|
|
10
|
+
/** Event handler called when auto-focusing on unmount */
|
|
11
|
+
onUnmountAutoFocus?: (event: Event) => void;
|
|
12
|
+
ref?: (el: HTMLDivElement) => void;
|
|
13
|
+
}
|
|
14
|
+
declare function FocusScope(inProps: FocusScopeProps): JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { FocusScope, type FocusScopeProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
interface FocusScopeProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** When true, tabbing from last item will focus first and vice versa */
|
|
5
|
+
loop?: boolean;
|
|
6
|
+
/** When true, focus cannot escape the scope */
|
|
7
|
+
trapped?: boolean;
|
|
8
|
+
/** Event handler called when auto-focusing on mount */
|
|
9
|
+
onMountAutoFocus?: (event: Event) => void;
|
|
10
|
+
/** Event handler called when auto-focusing on unmount */
|
|
11
|
+
onUnmountAutoFocus?: (event: Event) => void;
|
|
12
|
+
ref?: (el: HTMLDivElement) => void;
|
|
13
|
+
}
|
|
14
|
+
declare function FocusScope(inProps: FocusScopeProps): JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { FocusScope, type FocusScopeProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { splitProps, createSignal, createEffect, onCleanup } from 'solid-js';
|
|
2
|
+
import { mergeRefs } from '@radix-solid-js/compose-refs';
|
|
3
|
+
import { Primitive } from '@radix-solid-js/primitive-component';
|
|
4
|
+
|
|
5
|
+
// src/focus-scope.tsx
|
|
6
|
+
var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
|
|
7
|
+
var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
|
|
8
|
+
var EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
9
|
+
function FocusScope(inProps) {
|
|
10
|
+
const [local, rest] = splitProps(inProps, [
|
|
11
|
+
"loop",
|
|
12
|
+
"trapped",
|
|
13
|
+
"onMountAutoFocus",
|
|
14
|
+
"onUnmountAutoFocus",
|
|
15
|
+
"ref"
|
|
16
|
+
]);
|
|
17
|
+
const loop = () => local.loop ?? false;
|
|
18
|
+
const trapped = () => local.trapped ?? false;
|
|
19
|
+
const [container, setContainer] = createSignal(null);
|
|
20
|
+
let lastFocusedElementRef = null;
|
|
21
|
+
const focusScope = {
|
|
22
|
+
paused: false,
|
|
23
|
+
pause() {
|
|
24
|
+
this.paused = true;
|
|
25
|
+
},
|
|
26
|
+
resume() {
|
|
27
|
+
this.paused = false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
createEffect(() => {
|
|
31
|
+
if (!trapped()) return;
|
|
32
|
+
const el = container();
|
|
33
|
+
if (!el) return;
|
|
34
|
+
function handleFocusIn(event) {
|
|
35
|
+
if (focusScope.paused || !el) return;
|
|
36
|
+
const target = event.target;
|
|
37
|
+
if (el.contains(target)) {
|
|
38
|
+
lastFocusedElementRef = target;
|
|
39
|
+
} else {
|
|
40
|
+
focus(lastFocusedElementRef, { select: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function handleFocusOut(event) {
|
|
44
|
+
if (focusScope.paused || !el) return;
|
|
45
|
+
const relatedTarget = event.relatedTarget;
|
|
46
|
+
if (relatedTarget === null) return;
|
|
47
|
+
if (!el.contains(relatedTarget)) {
|
|
48
|
+
focus(lastFocusedElementRef, { select: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function handleMutations(mutations) {
|
|
52
|
+
const focusedElement = document.activeElement;
|
|
53
|
+
if (focusedElement !== document.body) return;
|
|
54
|
+
for (const mutation of mutations) {
|
|
55
|
+
if (mutation.removedNodes.length > 0) focus(el);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
document.addEventListener("focusin", handleFocusIn);
|
|
59
|
+
document.addEventListener("focusout", handleFocusOut);
|
|
60
|
+
const mutationObserver = new MutationObserver(handleMutations);
|
|
61
|
+
if (el) mutationObserver.observe(el, { childList: true, subtree: true });
|
|
62
|
+
onCleanup(() => {
|
|
63
|
+
document.removeEventListener("focusin", handleFocusIn);
|
|
64
|
+
document.removeEventListener("focusout", handleFocusOut);
|
|
65
|
+
mutationObserver.disconnect();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
createEffect(() => {
|
|
69
|
+
const el = container();
|
|
70
|
+
if (!el) return;
|
|
71
|
+
focusScopesStack.add(focusScope);
|
|
72
|
+
const previouslyFocusedElement = document.activeElement;
|
|
73
|
+
const hasFocusedCandidate = el.contains(previouslyFocusedElement);
|
|
74
|
+
if (!hasFocusedCandidate) {
|
|
75
|
+
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
|
|
76
|
+
el.addEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus || (() => {
|
|
77
|
+
}));
|
|
78
|
+
el.dispatchEvent(mountEvent);
|
|
79
|
+
if (!mountEvent.defaultPrevented) {
|
|
80
|
+
focusFirst(removeLinks(getTabbableCandidates(el)), { select: true });
|
|
81
|
+
if (document.activeElement === previouslyFocusedElement) {
|
|
82
|
+
focus(el);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
onCleanup(() => {
|
|
87
|
+
el.removeEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus || (() => {
|
|
88
|
+
}));
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);
|
|
91
|
+
const handler = local.onUnmountAutoFocus || (() => {
|
|
92
|
+
});
|
|
93
|
+
el.addEventListener(AUTOFOCUS_ON_UNMOUNT, handler);
|
|
94
|
+
el.dispatchEvent(unmountEvent);
|
|
95
|
+
if (!unmountEvent.defaultPrevented) {
|
|
96
|
+
focus(previouslyFocusedElement ?? document.body, { select: true });
|
|
97
|
+
}
|
|
98
|
+
el.removeEventListener(AUTOFOCUS_ON_UNMOUNT, handler);
|
|
99
|
+
focusScopesStack.remove(focusScope);
|
|
100
|
+
}, 0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
const handleKeyDown = (event) => {
|
|
104
|
+
if (!loop() && !trapped()) return;
|
|
105
|
+
if (focusScope.paused) return;
|
|
106
|
+
const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
107
|
+
const focusedElement = document.activeElement;
|
|
108
|
+
if (isTabKey && focusedElement) {
|
|
109
|
+
const el = event.currentTarget;
|
|
110
|
+
const [first, last] = getTabbableEdges(el);
|
|
111
|
+
const hasTabbableElementsInside = first && last;
|
|
112
|
+
if (!hasTabbableElementsInside) {
|
|
113
|
+
if (focusedElement === el) event.preventDefault();
|
|
114
|
+
} else {
|
|
115
|
+
if (!event.shiftKey && focusedElement === last) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
if (loop()) focus(first, { select: true });
|
|
118
|
+
} else if (event.shiftKey && focusedElement === first) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
if (loop()) focus(last, { select: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return /* @__PURE__ */ React.createElement(
|
|
126
|
+
Primitive.div,
|
|
127
|
+
{
|
|
128
|
+
tabIndex: -1,
|
|
129
|
+
...rest,
|
|
130
|
+
ref: mergeRefs(local.ref, (node) => setContainer(node)),
|
|
131
|
+
onKeyDown: handleKeyDown
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function focusFirst(candidates, { select = false } = {}) {
|
|
136
|
+
const previouslyFocusedElement = document.activeElement;
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
focus(candidate, { select });
|
|
139
|
+
if (document.activeElement !== previouslyFocusedElement) return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function getTabbableEdges(container) {
|
|
143
|
+
const candidates = getTabbableCandidates(container);
|
|
144
|
+
const first = findVisible(candidates, container);
|
|
145
|
+
const last = findVisible(candidates.reverse(), container);
|
|
146
|
+
return [first, last];
|
|
147
|
+
}
|
|
148
|
+
function getTabbableCandidates(container) {
|
|
149
|
+
const nodes = [];
|
|
150
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
151
|
+
acceptNode: (node) => {
|
|
152
|
+
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
|
|
153
|
+
if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
|
|
154
|
+
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
158
|
+
return nodes;
|
|
159
|
+
}
|
|
160
|
+
function findVisible(elements, container) {
|
|
161
|
+
for (const element of elements) {
|
|
162
|
+
if (!isHidden(element, { upTo: container })) return element;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isHidden(node, { upTo }) {
|
|
166
|
+
if (getComputedStyle(node).visibility === "hidden") return true;
|
|
167
|
+
while (node) {
|
|
168
|
+
if (upTo !== void 0 && node === upTo) return false;
|
|
169
|
+
if (getComputedStyle(node).display === "none") return true;
|
|
170
|
+
node = node.parentElement;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function isSelectableInput(element) {
|
|
175
|
+
return element instanceof HTMLInputElement && "select" in element;
|
|
176
|
+
}
|
|
177
|
+
function focus(element, { select = false } = {}) {
|
|
178
|
+
if (element && element.focus) {
|
|
179
|
+
const previouslyFocusedElement = document.activeElement;
|
|
180
|
+
element.focus({ preventScroll: true });
|
|
181
|
+
if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var focusScopesStack = createFocusScopesStack();
|
|
185
|
+
function createFocusScopesStack() {
|
|
186
|
+
let stack = [];
|
|
187
|
+
return {
|
|
188
|
+
add(focusScope) {
|
|
189
|
+
const activeFocusScope = stack[0];
|
|
190
|
+
if (focusScope !== activeFocusScope) activeFocusScope?.pause();
|
|
191
|
+
stack = stack.filter((s) => s !== focusScope);
|
|
192
|
+
stack.unshift(focusScope);
|
|
193
|
+
},
|
|
194
|
+
remove(focusScope) {
|
|
195
|
+
stack = stack.filter((s) => s !== focusScope);
|
|
196
|
+
stack[0]?.resume();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function removeLinks(items) {
|
|
201
|
+
return items.filter((item) => item.tagName !== "A");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { FocusScope };
|
|
205
|
+
//# sourceMappingURL=index.js.map
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/focus-scope.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { type JSX, createSignal, createEffect, onCleanup, splitProps } from 'solid-js';\nimport { mergeRefs } from '@radix-solid-js/compose-refs';\nimport { Primitive } from '@radix-solid-js/primitive-component';\n\nconst AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';\nconst AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype FocusableTarget = HTMLElement | { focus(): void };\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope\n * -----------------------------------------------------------------------------------------------*/\n\ninterface FocusScopeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** When true, tabbing from last item will focus first and vice versa */\n loop?: boolean;\n /** When true, focus cannot escape the scope */\n trapped?: boolean;\n /** Event handler called when auto-focusing on mount */\n onMountAutoFocus?: (event: Event) => void;\n /** Event handler called when auto-focusing on unmount */\n onUnmountAutoFocus?: (event: Event) => void;\n ref?: (el: HTMLDivElement) => void;\n}\n\nfunction FocusScope(inProps: FocusScopeProps) {\n const [local, rest] = splitProps(inProps, [\n 'loop', 'trapped', 'onMountAutoFocus', 'onUnmountAutoFocus', 'ref',\n ]);\n\n const loop = () => local.loop ?? false;\n const trapped = () => local.trapped ?? false;\n\n const [container, setContainer] = createSignal<HTMLElement | null>(null);\n let lastFocusedElementRef: HTMLElement | null = null;\n\n const focusScope = {\n paused: false,\n pause() { this.paused = true; },\n resume() { this.paused = false; },\n };\n\n // Focus trapping\n createEffect(() => {\n if (!trapped()) return;\n const el = container();\n if (!el) return;\n\n function handleFocusIn(event: FocusEvent) {\n if (focusScope.paused || !el) return;\n const target = event.target as HTMLElement | null;\n if (el.contains(target)) {\n lastFocusedElementRef = target;\n } else {\n focus(lastFocusedElementRef, { select: true });\n }\n }\n\n function handleFocusOut(event: FocusEvent) {\n if (focusScope.paused || !el) return;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n if (relatedTarget === null) return;\n if (!el.contains(relatedTarget)) {\n focus(lastFocusedElementRef, { select: true });\n }\n }\n\n function handleMutations(mutations: MutationRecord[]) {\n const focusedElement = document.activeElement as HTMLElement | null;\n if (focusedElement !== document.body) return;\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) focus(el);\n }\n }\n\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n const mutationObserver = new MutationObserver(handleMutations);\n if (el) mutationObserver.observe(el, { childList: true, subtree: true });\n\n onCleanup(() => {\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n mutationObserver.disconnect();\n });\n });\n\n // Auto focus on mount/unmount\n createEffect(() => {\n const el = container();\n if (!el) return;\n\n focusScopesStack.add(focusScope);\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = el.contains(previouslyFocusedElement);\n\n if (!hasFocusedCandidate) {\n const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);\n el.addEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus as EventListener || (() => {}));\n el.dispatchEvent(mountEvent);\n if (!mountEvent.defaultPrevented) {\n focusFirst(removeLinks(getTabbableCandidates(el)), { select: true });\n if (document.activeElement === previouslyFocusedElement) {\n focus(el);\n }\n }\n }\n\n onCleanup(() => {\n el.removeEventListener(AUTOFOCUS_ON_MOUNT, local.onMountAutoFocus as EventListener || (() => {}));\n setTimeout(() => {\n const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);\n const handler = local.onUnmountAutoFocus as EventListener || (() => {});\n el.addEventListener(AUTOFOCUS_ON_UNMOUNT, handler);\n el.dispatchEvent(unmountEvent);\n if (!unmountEvent.defaultPrevented) {\n focus(previouslyFocusedElement ?? document.body, { select: true });\n }\n el.removeEventListener(AUTOFOCUS_ON_UNMOUNT, handler);\n focusScopesStack.remove(focusScope);\n }, 0);\n });\n });\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!loop() && !trapped()) return;\n if (focusScope.paused) return;\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const el = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(el);\n const hasTabbableElementsInside = first && last;\n\n if (!hasTabbableElementsInside) {\n if (focusedElement === el) event.preventDefault();\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n if (loop()) focus(first, { select: true });\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n if (loop()) focus(last, { select: true });\n }\n }\n }\n };\n\n return (\n <Primitive.div\n tabIndex={-1}\n {...rest}\n ref={mergeRefs(local.ref as any, (node: HTMLElement) => setContainer(node))}\n onKeyDown={handleKeyDown}\n />\n );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\nfunction focusFirst(candidates: HTMLElement[], { select = false } = {}) {\n const previouslyFocusedElement = document.activeElement;\n for (const candidate of candidates) {\n focus(candidate, { select });\n if (document.activeElement !== previouslyFocusedElement) return;\n }\n}\n\nfunction getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates, container);\n const last = findVisible(candidates.reverse(), container);\n return [first, last] as const;\n}\n\nfunction getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: any) => {\n const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';\n if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;\n return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n },\n });\n while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement);\n return nodes;\n}\n\nfunction findVisible(elements: HTMLElement[], container: HTMLElement) {\n for (const element of elements) {\n if (!isHidden(element, { upTo: container })) return element;\n }\n}\n\nfunction isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {\n if (getComputedStyle(node).visibility === 'hidden') return true;\n while (node) {\n if (upTo !== undefined && node === upTo) return false;\n if (getComputedStyle(node).display === 'none') return true;\n node = node.parentElement as HTMLElement;\n }\n return false;\n}\n\nfunction isSelectableInput(element: any): element is FocusableTarget & { select: () => void } {\n return element instanceof HTMLInputElement && 'select' in element;\n}\n\nfunction focus(element?: FocusableTarget | null, { select = false } = {}) {\n if (element && element.focus) {\n const previouslyFocusedElement = document.activeElement;\n element.focus({ preventScroll: true });\n if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();\n }\n}\n\ntype FocusScopeAPI = { paused: boolean; pause(): void; resume(): void };\nconst focusScopesStack = createFocusScopesStack();\n\nfunction createFocusScopesStack() {\n let stack: FocusScopeAPI[] = [];\n return {\n add(focusScope: FocusScopeAPI) {\n const activeFocusScope = stack[0];\n if (focusScope !== activeFocusScope) activeFocusScope?.pause();\n stack = stack.filter((s) => s !== focusScope);\n stack.unshift(focusScope);\n },\n remove(focusScope: FocusScopeAPI) {\n stack = stack.filter((s) => s !== focusScope);\n stack[0]?.resume();\n },\n };\n}\n\nfunction removeLinks(items: HTMLElement[]) {\n return items.filter((item) => item.tagName !== 'A');\n}\n\nexport { FocusScope };\nexport type { FocusScopeProps };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radix-solid-js/focus-scope",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"solid-js": "^1.8.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@radix-solid-js/compose-refs": "workspace:*",
|
|
35
|
+
"@radix-solid-js/primitive-component": "workspace:*"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@repo/tsconfig": "workspace:*",
|
|
39
|
+
"tsup": "^8.3.6",
|
|
40
|
+
"typescript": "^5.7.3",
|
|
41
|
+
"solid-js": "^1.9.3"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/ljho01/shadcn-solid-js.git",
|
|
49
|
+
"directory": "packages/solid/focus-scope"
|
|
50
|
+
}
|
|
51
|
+
}
|