Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Update ref.current access in hooks and fix useForwardedRef

+13 -12
src/useDialogFocus.ts
··· 25 25 const hasPriority = usePriority(ref, disabled); 26 26 27 27 useLayoutEffect(() => { 28 - if (!ref.current || disabled) return; 28 + const { current: element } = ref; 29 + if (!element || disabled) return; 29 30 30 31 let selection = snapshotSelection(ownerRef && ownerRef.current); 31 32 let willReceiveFocus = false; 32 33 let focusMovesForward = true; 33 34 34 35 function onClick(event: MouseEvent) { 35 - if (!ref.current || event.defaultPrevented) return; 36 + if (!element || event.defaultPrevented) return; 36 37 37 38 const target = event.target as HTMLElement | null; 38 - if (target && getFocusTargets(ref.current).indexOf(target) > -1) { 39 + if (target && getFocusTargets(element).indexOf(target) > -1) { 39 40 selection = null; 40 41 willReceiveFocus = true; 41 42 } 42 43 } 43 44 44 45 function onFocus(event: FocusEvent) { 45 - if (!ref.current || event.defaultPrevented) return; 46 + if (!element || event.defaultPrevented) return; 46 47 47 48 const active = document.activeElement as HTMLElement; 48 49 const owner = ··· 65 66 !contains(ref.current, relatedTarget) 66 67 ) { 67 68 // Get the next focus target of the container 68 - const focusTarget = getNextFocusTarget(ref.current, !focusMovesForward); 69 + const focusTarget = getNextFocusTarget(element, !focusMovesForward); 69 70 if (focusTarget) { 70 71 focusMovesForward = true; 71 72 event.preventDefault(); ··· 75 76 } 76 77 77 78 function onKey(event: KeyboardEvent) { 78 - if (!ref.current || event.defaultPrevented || event.isComposing) return; 79 + if (!element || event.defaultPrevented || event.isComposing) return; 79 80 80 81 // Mark whether focus is moving forward for the `onFocus` handler 81 82 if (event.code === 'Tab') { ··· 87 88 const active = document.activeElement as HTMLElement; 88 89 const owner = 89 90 (ownerRef && ownerRef.current) || (selection && selection.element); 90 - const focusTargets = getFocusTargets(ref.current); 91 + const focusTargets = getFocusTargets(element); 91 92 92 93 if ( 93 94 !focusTargets.length || ··· 97 98 return; 98 99 } else if (event.code === 'Tab') { 99 100 // Skip over the listbox via the parent if we press tab 100 - const currentTarget = contains(owner, active) ? owner! : ref.current; 101 + const currentTarget = contains(owner, active) ? owner! : element; 101 102 const focusTarget = getNextFocusTarget(currentTarget, event.shiftKey); 102 103 if (focusTarget) { 103 104 event.preventDefault(); ··· 152 153 event.code === 'Enter' 153 154 ) { 154 155 // Move focus to first target when Enter is pressed 155 - const newTarget = getFirstFocusTarget(ref.current); 156 + const newTarget = getFirstFocusTarget(element); 156 157 if (newTarget) { 157 158 willReceiveFocus = true; 158 159 newTarget.focus(); ··· 169 170 } 170 171 } 171 172 172 - ref.current.addEventListener('mousedown', onClick, true); 173 + element.addEventListener('mousedown', onClick, true); 173 174 document.body.addEventListener('focusin', onFocus); 174 175 document.addEventListener('keydown', onKey); 175 176 176 177 return () => { 177 - ref.current!.removeEventListener('mousedown', onClick); 178 + element.removeEventListener('mousedown', onClick); 178 179 document.body.removeEventListener('focusin', onFocus); 179 180 document.removeEventListener('keydown', onKey); 180 181 }; 181 - }, [ref.current!, disabled, hasPriority]); 182 + }, [ref.current, disabled, hasPriority]); 182 183 }
+7 -9
src/useDismissable.ts
··· 26 26 }, [onDismiss]); 27 27 28 28 useLayoutEffect(() => { 29 - if (!ref.current || disabled) return; 29 + const { current: element } = ref; 30 + if (!element || disabled) return; 30 31 31 32 function onFocusOut(event: FocusEvent) { 32 33 if (event.defaultPrevented) return; 33 34 34 35 const { target, relatedTarget } = event; 35 - if ( 36 - contains(ref.current, target) && 37 - !contains(ref.current, relatedTarget) 38 - ) { 36 + if (contains(element, target) && !contains(element, relatedTarget)) { 39 37 onDismissRef.current(); 40 38 } 41 39 } ··· 45 43 // The current dialog can be dismissed by pressing escape if it either has focus 46 44 // or it has priority 47 45 const active = document.activeElement; 48 - if (hasPriority || (active && contains(ref.current, active))) { 46 + if (hasPriority || (active && contains(element, active))) { 49 47 event.preventDefault(); 50 48 onDismissRef.current(); 51 49 } ··· 54 52 55 53 function onClick(event: MouseEvent | TouchEvent) { 56 54 const { target } = event; 57 - if (contains(ref.current, target) || event.defaultPrevented) { 55 + if (contains(element, target) || event.defaultPrevented) { 58 56 return; 59 57 } 60 58 61 59 // The current dialog can be dismissed by pressing outside of it if it either has 62 60 // focus or it has priority 63 61 const active = document.activeElement; 64 - if (hasPriority || (active && contains(ref.current, active))) { 62 + if (hasPriority || (active && contains(element, active))) { 65 63 event.preventDefault(); 66 64 onDismissRef.current(); 67 65 } ··· 80 78 document.removeEventListener('touchstart', onClick); 81 79 document.removeEventListener('keydown', onKey); 82 80 }; 83 - }, [ref.current!, hasPriority, disabled, focusLoss]); 81 + }, [ref.current, hasPriority, disabled, focusLoss]); 84 82 }
+5 -7
src/useForwardedRef.ts
··· 9 9 export function useForwardedRef<T extends HTMLElement>( 10 10 forwarded: ForwardedRef<T> 11 11 ): Ref<T> { 12 - const ref: RefWithState<T> = useRef<T>(null); 13 - if (ref._forwarded !== forwarded) { 14 - ref._forwarded = forwarded; 15 - Object.defineProperty(ref, 'current', { 12 + const ref = useRef<RefWithState<T> | null>(null); 13 + if (!ref.current || ref.current._forwarded !== forwarded) { 14 + ref.current = Object.defineProperty({ _forwarded: forwarded }, 'current', { 16 15 enumerable: true, 17 16 configurable: true, 18 17 get() { ··· 26 25 this._forwarded.current = value; 27 26 } 28 27 }, 29 - }); 28 + }) as RefWithState<T>; 30 29 } 31 - 32 - return ref; 30 + return ref.current; 33 31 }
+12 -11
src/useMenuFocus.ts
··· 21 21 const disabled = !!(options && options.disabled); 22 22 23 23 useLayoutEffect(() => { 24 - if (!ref.current || disabled) return; 24 + const { current: element } = ref; 25 + if (!element || disabled) return; 25 26 26 27 let selection: RestoreSelection | null = null; 27 28 28 29 function onFocus(event: FocusEvent) { 29 - if (!ref.current || event.defaultPrevented) return; 30 + if (!element || event.defaultPrevented) return; 30 31 31 32 const owner = 32 33 (ownerRef && ownerRef.current) || (selection && selection.element); ··· 35 36 // When owner is explicitly passed we can make a snapshot early 36 37 selection = snapshotSelection(owner); 37 38 } else if ( 38 - contains(ref.current, target) && 39 - !contains(ref.current, relatedTarget) && 39 + contains(element, target) && 40 + !contains(element, relatedTarget) && 40 41 (!ownerRef || contains(relatedTarget, ownerRef.current)) 41 42 ) { 42 43 // Check whether focus is about to move into the container and snapshot last focus 43 44 selection = snapshotSelection(owner); 44 45 } else if ( 45 - contains(ref.current, relatedTarget) && 46 - !contains(ref.current, target) 46 + contains(element, relatedTarget) && 47 + !contains(element, target) 47 48 ) { 48 49 // Reset focus if it's lost and has left the menu 49 50 selection = null; ··· 51 52 } 52 53 53 54 function onKey(event: KeyboardEvent) { 54 - if (!ref.current || event.defaultPrevented || event.isComposing) return; 55 + if (!element || event.defaultPrevented || event.isComposing) return; 55 56 56 57 const owner = 57 58 (ownerRef && ownerRef.current) || (selection && selection.element); 58 59 const active = document.activeElement as HTMLElement; 59 - const focusTargets = getFocusTargets(ref.current); 60 + const focusTargets = getFocusTargets(element); 60 61 if ( 61 62 !focusTargets.length || 62 - (!contains(ref.current, active) && !contains(owner, active)) 63 + (!contains(element, active) && !contains(owner, active)) 63 64 ) { 64 65 // Do nothing if container doesn't contain focus or not targets are available 65 66 return; ··· 100 101 event.code === 'Enter' 101 102 ) { 102 103 // Move focus to first target when enter is pressed 103 - const newTarget = getFirstFocusTarget(ref.current); 104 + const newTarget = getFirstFocusTarget(element); 104 105 if (newTarget) newTarget.focus(); 105 106 } else if ( 106 107 owner && ··· 129 130 document.body.removeEventListener('focusin', onFocus); 130 131 document.removeEventListener('keydown', onKey); 131 132 }; 132 - }, [ref.current!, disabled]); 133 + }, [ref.current, disabled]); 133 134 }
+12 -16
src/useModalFocus.ts
··· 29 29 const hasPriority = usePriority(ref, disabled); 30 30 31 31 useLayoutEffect(() => { 32 - if (!ref.current || !hasPriority || disabled) return; 32 + const { current: element } = ref; 33 + if (!element || !hasPriority || disabled) return; 33 34 34 35 let selection: RestoreSelection | null = null; 35 - if ( 36 - !document.activeElement || 37 - !contains(ref.current, document.activeElement) 38 - ) { 39 - const newTarget = getAutofocusTarget(ref.current); 40 - selection = snapshotSelection(ref.current); 36 + if (!document.activeElement || !contains(element, document.activeElement)) { 37 + const newTarget = getAutofocusTarget(element); 38 + selection = snapshotSelection(element); 41 39 newTarget.focus(); 42 40 } 43 41 44 42 function onBlur(event: FocusEvent) { 45 - const parent = ref.current; 46 - if (!parent || event.defaultPrevented) return; 43 + if (!element || event.defaultPrevented) return; 47 44 48 45 if ( 49 - contains(parent, event.target) && 50 - !contains(parent, event.relatedTarget) 46 + contains(element, event.target) && 47 + !contains(element, event.relatedTarget) 51 48 ) { 52 - const target = getFirstFocusTarget(parent); 49 + const target = getFirstFocusTarget(element); 53 50 if (target) target.focus(); 54 51 } 55 52 } 56 53 57 54 function onKeyDown(event: KeyboardEvent) { 58 - const parent = ref.current; 59 - if (!parent || event.defaultPrevented) return; 55 + if (!element || event.defaultPrevented) return; 60 56 61 57 if (event.code === 'Tab') { 62 58 const activeElement = document.activeElement as HTMLElement; 63 - const targets = getFocusTargets(parent); 59 + const targets = getFocusTargets(element); 64 60 const index = targets.indexOf(activeElement); 65 61 if (event.shiftKey && index === 0) { 66 62 event.preventDefault(); ··· 80 76 document.body.removeEventListener('focusout', onBlur); 81 77 document.removeEventListener('keydown', onKeyDown); 82 78 }; 83 - }, [ref.current!, hasPriority, disabled]); 79 + }, [ref.current, hasPriority, disabled]); 84 80 }
+5 -6
src/usePriority.ts
··· 31 31 }); 32 32 33 33 useLayoutEffect(() => { 34 - if (!ref.current || isDisabled) return; 35 - 36 - const { current } = ref; 34 + const { current: element } = ref; 35 + if (!element || isDisabled) return; 37 36 38 37 function onChange() { 39 38 setHasPriority(() => priorityStack[0] === ref.current); 40 39 } 41 40 42 - priorityStack.push(current); 41 + priorityStack.push(element); 43 42 priorityStack.sort(sortByHierarchy); 44 43 listeners.add(onChange); 45 44 listeners.forEach(fn => fn()); 46 45 47 46 return () => { 48 - const index = priorityStack.indexOf(current); 47 + const index = priorityStack.indexOf(element); 49 48 priorityStack.splice(index, 1); 50 49 listeners.delete(onChange); 51 50 listeners.forEach(fn => fn()); 52 51 }; 53 - }, [ref.current!, isDisabled]); 52 + }, [ref.current, isDisabled]); 54 53 55 54 return hasPriority; 56 55 };