|
| 1 | +import { useEffect } from 'react'; |
1 | 2 | import isVisible from './isVisible'; |
2 | 3 |
|
3 | 4 | type DisabledElement = |
@@ -56,44 +57,137 @@ export function getFocusNodeList(node: HTMLElement, includePositive = false) { |
56 | 57 | return res; |
57 | 58 | } |
58 | 59 |
|
59 | | -let lastFocusElement = null; |
| 60 | +export interface InputFocusOptions extends FocusOptions { |
| 61 | + cursor?: 'start' | 'end' | 'all'; |
| 62 | +} |
| 63 | + |
| 64 | +// Used for `rc-input` `rc-textarea` `rc-input-number` |
| 65 | +/** |
| 66 | + * Focus element and set cursor position for input/textarea elements. |
| 67 | + */ |
| 68 | +export function triggerFocus( |
| 69 | + element?: HTMLElement, |
| 70 | + option?: InputFocusOptions, |
| 71 | +) { |
| 72 | + if (!element) return; |
| 73 | + |
| 74 | + element.focus(option); |
| 75 | + |
| 76 | + // Selection content |
| 77 | + const { cursor } = option || {}; |
| 78 | + if ( |
| 79 | + cursor && |
| 80 | + (element instanceof HTMLInputElement || |
| 81 | + element instanceof HTMLTextAreaElement) |
| 82 | + ) { |
| 83 | + const len = element.value.length; |
| 84 | + |
| 85 | + switch (cursor) { |
| 86 | + case 'start': |
| 87 | + element.setSelectionRange(0, 0); |
| 88 | + break; |
| 89 | + |
| 90 | + case 'end': |
| 91 | + element.setSelectionRange(len, len); |
| 92 | + break; |
60 | 93 |
|
61 | | -/** @deprecated Do not use since this may failed when used in async */ |
62 | | -export function saveLastFocusNode() { |
63 | | - lastFocusElement = document.activeElement; |
| 94 | + default: |
| 95 | + element.setSelectionRange(0, len); |
| 96 | + } |
| 97 | + } |
| 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]; |
64 | 108 | } |
65 | 109 |
|
66 | | -/** @deprecated Do not use since this may failed when used in async */ |
67 | | -export function clearLastFocusNode() { |
68 | | - lastFocusElement = null; |
| 110 | +function hasFocus(element: HTMLElement) { |
| 111 | + const { activeElement } = document; |
| 112 | + return element === activeElement || element.contains(activeElement); |
69 | 113 | } |
70 | 114 |
|
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(); |
| 115 | +function syncFocus() { |
| 116 | + const lastElement = getLastElement(); |
| 117 | + const { activeElement } = document; |
77 | 118 |
|
78 | | - /* eslint-disable no-empty */ |
79 | | - } catch (e) { |
80 | | - // empty |
81 | | - } |
82 | | - /* eslint-enable no-empty */ |
| 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; |
83 | 129 | } |
84 | 130 | } |
85 | 131 |
|
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(); |
| 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]; |
97 | 145 | } |
98 | 146 | } |
99 | 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