|
| 1 | +import { useEffect } from 'react'; |
1 | 2 | import isVisible from './isVisible'; |
2 | 3 |
|
3 | 4 | type DisabledElement = |
@@ -56,48 +57,6 @@ export function getFocusNodeList(node: HTMLElement, includePositive = false) { |
56 | 57 | return res; |
57 | 58 | } |
58 | 59 |
|
59 | | -let lastFocusElement = null; |
60 | | - |
61 | | -/** @deprecated Do not use since this may failed when used in async */ |
62 | | -export function saveLastFocusNode() { |
63 | | - lastFocusElement = document.activeElement; |
64 | | -} |
65 | | - |
66 | | -/** @deprecated Do not use since this may failed when used in async */ |
67 | | -export function clearLastFocusNode() { |
68 | | - lastFocusElement = null; |
69 | | -} |
70 | | - |
71 | | -/** @deprecated Do not use since this may failed when used in async */ |
72 | | -export function backLastFocusNode() { |
73 | | - if (lastFocusElement) { |
74 | | - try { |
75 | | - // 元素可能已经被移动了 |
76 | | - lastFocusElement.focus(); |
77 | | - |
78 | | - /* eslint-disable no-empty */ |
79 | | - } catch (e) { |
80 | | - // empty |
81 | | - } |
82 | | - /* eslint-enable no-empty */ |
83 | | - } |
84 | | -} |
85 | | - |
86 | | -export function limitTabRange(node: HTMLElement, e: KeyboardEvent) { |
87 | | - if (e.keyCode === 9) { |
88 | | - const tabNodeList = getFocusNodeList(node); |
89 | | - const lastTabNode = tabNodeList[e.shiftKey ? 0 : tabNodeList.length - 1]; |
90 | | - const leavingTab = |
91 | | - lastTabNode === document.activeElement || node === document.activeElement; |
92 | | - |
93 | | - if (leavingTab) { |
94 | | - const target = tabNodeList[e.shiftKey ? tabNodeList.length - 1 : 0]; |
95 | | - target.focus(); |
96 | | - e.preventDefault(); |
97 | | - } |
98 | | - } |
99 | | -} |
100 | | - |
101 | 60 | export interface InputFocusOptions extends FocusOptions { |
102 | 61 | cursor?: 'start' | 'end' | 'all'; |
103 | 62 | } |
@@ -137,3 +96,98 @@ export function triggerFocus( |
137 | 96 | } |
138 | 97 | } |
139 | 98 | } |
| 99 | + |
| 100 | +// ====================================================== |
| 101 | +// == Lock Focus == |
| 102 | +// ====================================================== |
| 103 | +let lastFocusElement: HTMLElement | null = null; |
| 104 | +let focusElements: HTMLElement[] = []; |
| 105 | + |
| 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 | +} |
| 131 | + |
| 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 | + } |
| 147 | +} |
| 148 | + |
| 149 | +/** |
| 150 | + * Lock focus in the element. |
| 151 | + * It will force back to the first focusable element when focus leaves the element. |
| 152 | + */ |
| 153 | +export function lockFocus(element: HTMLElement): VoidFunction { |
| 154 | + if (element) { |
| 155 | + // Refresh focus elements |
| 156 | + focusElements = focusElements.filter(ele => ele !== element); |
| 157 | + focusElements.push(element); |
| 158 | + |
| 159 | + // Just add event since it will de-duplicate |
| 160 | + window.addEventListener('focusin', syncFocus); |
| 161 | + window.addEventListener('keydown', onWindowKeyDown, true); |
| 162 | + syncFocus(); |
| 163 | + } |
| 164 | + |
| 165 | + // Always return unregister function |
| 166 | + return () => { |
| 167 | + lastFocusElement = null; |
| 168 | + focusElements = focusElements.filter(ele => ele !== element); |
| 169 | + if (focusElements.length === 0) { |
| 170 | + window.removeEventListener('focusin', syncFocus); |
| 171 | + window.removeEventListener('keydown', onWindowKeyDown, true); |
| 172 | + } |
| 173 | + }; |
| 174 | +} |
| 175 | + |
| 176 | +/** |
| 177 | + * Lock focus within an element. |
| 178 | + * When locked, focus will be restricted to focusable elements within the specified element. |
| 179 | + * If multiple elements are locked, only the last locked element will be effective. |
| 180 | + */ |
| 181 | +export function useLockFocus( |
| 182 | + lock: boolean, |
| 183 | + getElement: () => HTMLElement | null, |
| 184 | +) { |
| 185 | + useEffect(() => { |
| 186 | + if (lock) { |
| 187 | + const element = getElement(); |
| 188 | + if (element) { |
| 189 | + return lockFocus(element); |
| 190 | + } |
| 191 | + } |
| 192 | + }, [lock]); |
| 193 | +} |
0 commit comments