Skip to content

Commit 7b64207

Browse files
committed
feat: support useFocusLock
1 parent 385ff69 commit 7b64207

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

docs/demo/focus.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ title: Focus Utils
44

55
# Focus Utils Demo
66

7-
Demonstrates the usage of focus-related utility functions, including `limitTabRange`, `getFocusNodeList`, and `triggerFocus`.
7+
Demonstrates the usage of focus-related utility functions.
88

99
<code src="../examples/focus.tsx"></code>

docs/examples/focus.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
import React, { useRef } from 'react';
22
import {} from '../../src';
3+
import { lockFocus } from '../../src/Dom/focus';
4+
import './focus.css';
35

46
export default function FocusDemo() {
57
const containerRef = useRef<HTMLDivElement>(null);
8+
const [locking, setLocking] = React.useState(false);
9+
10+
React.useEffect(() => {
11+
if (locking) {
12+
return lockFocus(containerRef.current!);
13+
}
14+
}, [locking]);
615

716
return (
8-
<div style={{ padding: 32 }}>
17+
<div style={{ padding: 32 }} className="focus-demo">
918
<h2>Focus Utils Demo</h2>
1019

1120
{/* External buttons */}
12-
<button>External Button 1</button>
21+
<button onClick={() => setLocking(!locking)}>
22+
Lock ({String(locking)})
23+
</button>
1324

1425
{/* Middle container - Tab key cycling is limited within this area */}
1526
<div
1627
ref={containerRef}
28+
tabIndex={0}
1729
style={{
18-
border: '2px solid #1890ff',
30+
border: '2px solid green',
1931
padding: 24,
2032
margin: 16,
2133
borderRadius: 8,

src/Dom/focus.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect } from 'react';
12
import isVisible from './isVisible';
23

34
type DisabledElement =
@@ -99,12 +100,50 @@ export function triggerFocus(
99100
// ======================================================
100101
// == Lock Focus ==
101102
// ======================================================
103+
let lastFocusElement: HTMLElement | null = null;
102104
let focusElements: HTMLElement[] = [];
103105

104-
function onWindowFocus(e: FocusEvent) {
105-
const lastElement = focusElements[focusElements.length - 1];
106+
function getLastElement() {
107+
return focusElements[focusElements.length - 1];
108+
}
109+
110+
function hasFocus(element: HTMLElement) {
111+
const { activeElement } = document;
112+
return element === activeElement || element.contains(activeElement);
113+
}
114+
115+
function syncFocus() {
116+
const lastElement = getLastElement();
117+
const { activeElement } = document;
118+
119+
if (lastElement && !hasFocus(lastElement)) {
120+
const focusableList = getFocusNodeList(lastElement);
121+
122+
const matchElement = focusableList.includes(lastFocusElement as HTMLElement)
123+
? lastFocusElement
124+
: focusableList[0];
125+
126+
matchElement?.focus();
127+
} else {
128+
lastFocusElement = activeElement as HTMLElement;
129+
}
130+
}
106131

107-
console.log('lock focus', e.target, lastElement);
132+
function onWindowKeyDown(e: KeyboardEvent) {
133+
if (e.key === 'Tab') {
134+
const { activeElement } = document;
135+
const lastElement = getLastElement();
136+
const focusableList = getFocusNodeList(lastElement);
137+
const last = focusableList[focusableList.length - 1];
138+
139+
if (e.shiftKey && activeElement === focusableList[0]) {
140+
// Tab backward on first focusable element
141+
lastFocusElement = last;
142+
} else if (!e.shiftKey && activeElement === last) {
143+
// Tab forward on last focusable element
144+
lastFocusElement = focusableList[0];
145+
}
146+
}
108147
}
109148

110149
/**
@@ -117,12 +156,24 @@ export function lockFocus(element: HTMLElement): VoidFunction {
117156
focusElements.push(element);
118157

119158
// Just add event since it will de-duplicate
120-
window.addEventListener('focusin', onWindowFocus, true);
159+
window.addEventListener('focusin', syncFocus);
160+
window.addEventListener('keydown', onWindowKeyDown, true);
161+
syncFocus();
121162

122163
return () => {
164+
lastFocusElement = null;
123165
focusElements = focusElements.filter(ele => ele !== element);
124166
if (focusElements.length === 0) {
125-
window.removeEventListener('focusin', onWindowFocus, true);
167+
window.removeEventListener('focusin', syncFocus);
168+
window.removeEventListener('keydown', onWindowKeyDown, true);
126169
}
127170
};
128171
}
172+
173+
export function useFocusLock(element: HTMLElement | null) {
174+
useEffect(() => {
175+
if (element) {
176+
return lockFocus(element);
177+
}
178+
}, [element]);
179+
}

0 commit comments

Comments
 (0)