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

Configure Feed

Select the types of activity you want to include in your feed.

Respect shadow DOM root in root node / activeElement

+81 -57
+17 -11
src/useDialogFocus.ts
··· 1 1 import { snapshotSelection, restoreSelection } from './utils/selection'; 2 - import { getFocusTargets, getNextFocusTarget, focus } from './utils/focus'; 2 + import { 3 + getFocusTargets, 4 + getNextFocusTarget, 5 + getActive, 6 + focus, 7 + } from './utils/focus'; 3 8 import { click } from './utils/click'; 4 9 import { useLayoutEffect } from './utils/react'; 5 - import { contains, isInputElement } from './utils/element'; 10 + import { contains, getRoot, isInputElement } from './utils/element'; 6 11 import { makePriorityHook } from './usePriority'; 7 12 import { Ref } from './types'; 8 13 ··· 25 30 const { current: element } = ref; 26 31 if (!element || disabled) return; 27 32 33 + const root = getRoot(element); 28 34 let selection = snapshotSelection(ownerRef && ownerRef.current); 29 35 let willReceiveFocus = false; 30 36 let focusMovesForward = true; ··· 77 83 return; 78 84 } 79 85 80 - const active = document.activeElement as HTMLElement; 86 + const active = getActive(); 81 87 const owner = 82 88 (ownerRef && ownerRef.current) || (selection && selection.element); 83 89 const focusTargets = getFocusTargets(element); ··· 99 105 ) { 100 106 // Implement forward movement in focus targets 101 107 event.preventDefault(); 102 - const focusIndex = focusTargets.indexOf(active); 108 + const focusIndex = focusTargets.indexOf(active!); 103 109 const nextIndex = 104 110 focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0; 105 111 willReceiveFocus = true; ··· 110 116 ) { 111 117 // Implement backward movement in focus targets 112 118 event.preventDefault(); 113 - const focusIndex = focusTargets.indexOf(active); 119 + const focusIndex = focusTargets.indexOf(active!); 114 120 const nextIndex = 115 121 focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1; 116 122 willReceiveFocus = true; ··· 149 155 } 150 156 } else if ( 151 157 (event.code === 'Enter' || event.code === 'Space') && 152 - focusTargets.indexOf(active) > -1 && 158 + focusTargets.indexOf(active!) > -1 && 153 159 !isInputElement(active) 154 160 ) { 155 161 // Implement virtual click / activation for list items ··· 170 176 } 171 177 172 178 element.addEventListener('mousedown', onClick, true); 173 - document.body.addEventListener('focusin', onFocus); 174 - document.addEventListener('keydown', onKey); 179 + root.addEventListener('focusin', onFocus); 180 + root.addEventListener('keydown', onKey); 175 181 176 182 return () => { 177 183 element.removeEventListener('mousedown', onClick); 178 - document.body.removeEventListener('focusin', onFocus); 179 - document.removeEventListener('keydown', onKey); 184 + root.removeEventListener('focusin', onFocus); 185 + root.removeEventListener('keydown', onKey); 180 186 181 - const active = document.activeElement as HTMLElement; 187 + const active = getActive(); 182 188 if (!active || contains(element, active)) { 183 189 restoreSelection(selection); 184 190 }
+12 -11
src/useDismissable.ts
··· 1 1 import { useRef } from 'react'; 2 2 import { useLayoutEffect } from './utils/react'; 3 - import { contains } from './utils/element'; 3 + import { contains, getRoot } from './utils/element'; 4 4 import { makePriorityHook } from './usePriority'; 5 5 import { Ref } from './types'; 6 6 ··· 29 29 const { current: element } = ref; 30 30 if (!element || disabled) return; 31 31 32 + const root = getRoot(element); 32 33 let willLoseFocus = false; 33 34 34 35 function onFocusOut(event: FocusEvent) { ··· 82 83 } 83 84 84 85 if (focusLoss) { 85 - document.body.addEventListener('focusout', onFocusOut, true); 86 - document.body.addEventListener('focusin', onFocusIn, true); 86 + root.addEventListener('focusout', onFocusOut, true); 87 + root.addEventListener('focusin', onFocusIn, true); 87 88 } 88 89 89 - document.addEventListener('click', onClick, true); 90 - document.addEventListener('touchstart', onClick, true); 91 - document.addEventListener('keydown', onKey, true); 90 + root.addEventListener('click', onClick, true); 91 + root.addEventListener('touchstart', onClick, true); 92 + root.addEventListener('keydown', onKey, true); 92 93 93 94 return () => { 94 95 if (focusLoss) { 95 - document.body.removeEventListener('focusout', onFocusOut, true); 96 - document.body.removeEventListener('focusin', onFocusIn, true); 96 + root.removeEventListener('focusout', onFocusOut, true); 97 + root.removeEventListener('focusin', onFocusIn, true); 97 98 } 98 99 99 - document.removeEventListener('click', onClick, true); 100 - document.removeEventListener('touchstart', onClick, true); 101 - document.removeEventListener('keydown', onKey, true); 100 + root.removeEventListener('click', onClick, true); 101 + root.removeEventListener('touchstart', onClick, true); 102 + root.removeEventListener('keydown', onKey, true); 102 103 }; 103 104 }, [ref.current, hasPriority, disabled, focusLoss]); 104 105 }
+13 -11
src/useMenuFocus.ts
··· 3 3 snapshotSelection, 4 4 restoreSelection, 5 5 } from './utils/selection'; 6 - import { getFocusTargets, focus } from './utils/focus'; 6 + 7 + import { getActive, getFocusTargets, focus } from './utils/focus'; 7 8 import { click } from './utils/click'; 8 9 import { useLayoutEffect } from './utils/react'; 9 - import { contains, isInputElement } from './utils/element'; 10 + import { contains, getRoot, isInputElement } from './utils/element'; 10 11 import { Ref } from './types'; 11 12 12 13 export interface MenuFocusOptions { ··· 25 26 const { current: element } = ref; 26 27 if (!element || disabled) return; 27 28 29 + const root = getRoot(element); 28 30 let selection: RestoreSelection | null = null; 29 31 30 32 function onFocus(event: FocusEvent) { ··· 57 59 58 60 const owner = 59 61 (ownerRef && ownerRef.current) || (selection && selection.element); 60 - const active = document.activeElement as HTMLElement; 62 + const active = getActive(); 61 63 const focusTargets = getFocusTargets(element); 62 64 if ( 63 65 !focusTargets.length || ··· 73 75 ) { 74 76 // Implement forward movement in focus targets 75 77 event.preventDefault(); 76 - const focusIndex = focusTargets.indexOf(active); 78 + const focusIndex = focusTargets.indexOf(active!); 77 79 const nextIndex = 78 80 focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0; 79 81 focus(focusTargets[nextIndex]); ··· 83 85 ) { 84 86 // Implement backward movement in focus targets 85 87 event.preventDefault(); 86 - const focusIndex = focusTargets.indexOf(active); 88 + const focusIndex = focusTargets.indexOf(active!); 87 89 const nextIndex = 88 90 focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1; 89 91 focus(focusTargets[nextIndex]); ··· 115 117 restoreSelection(selection); 116 118 } else if ( 117 119 (event.code === 'Enter' || event.code === 'Space') && 118 - focusTargets.indexOf(active) > -1 && 120 + focusTargets.indexOf(active!) > -1 && 119 121 !isInputElement(active) 120 122 ) { 121 123 // Implement virtual click / activation for list items ··· 132 134 } 133 135 } 134 136 135 - document.body.addEventListener('focusin', onFocus); 136 - document.addEventListener('keydown', onKey); 137 + root.addEventListener('focusin', onFocus); 138 + root.addEventListener('keydown', onKey); 137 139 138 140 return () => { 139 - document.body.removeEventListener('focusin', onFocus); 140 - document.removeEventListener('keydown', onKey); 141 + root.removeEventListener('focusin', onFocus); 142 + root.removeEventListener('keydown', onKey); 141 143 142 - const active = document.activeElement as HTMLElement; 144 + const active = getActive(); 143 145 if (!active || contains(element, active)) { 144 146 restoreSelection(selection); 145 147 }
+13 -14
src/useModalFocus.ts
··· 4 4 restoreSelection, 5 5 } from './utils/selection'; 6 6 7 - import { getAutofocusTarget, getFocusTargets } from './utils/focus'; 8 - 7 + import { getActive, getAutofocusTarget, getFocusTargets } from './utils/focus'; 9 8 import { useLayoutEffect } from './utils/react'; 10 - import { contains } from './utils/element'; 9 + import { contains, getRoot } from './utils/element'; 11 10 import { makePriorityHook } from './usePriority'; 12 11 import { Ref } from './types'; 13 12 ··· 25 24 const hasPriority = usePriority(ref, disabled); 26 25 27 26 useLayoutEffect(() => { 28 - if (disabled) return; 27 + const { current: element } = ref; 28 + if (!element || disabled) return; 29 29 30 + const root = getRoot(element); 31 + const active = getActive(); 30 32 let selection: RestoreSelection | null = null; 31 - if ( 32 - !document.activeElement || 33 - !contains(ref.current, document.activeElement) 34 - ) { 33 + if (!active || !contains(element, active)) { 35 34 const newTarget = ref.current ? getAutofocusTarget(ref.current) : null; 36 35 selection = snapshotSelection(); 37 36 if (newTarget) newTarget.focus(); ··· 55 54 if (!hasPriority.current || !element || event.defaultPrevented) return; 56 55 57 56 if (event.code === 'Tab') { 58 - const activeElement = document.activeElement as HTMLElement; 57 + const activeElement = getActive()!; 59 58 const targets = getFocusTargets(element); 60 59 const index = targets.indexOf(activeElement); 61 60 if (event.shiftKey && index === 0) { ··· 68 67 } 69 68 } 70 69 71 - document.body.addEventListener('focusout', onBlur); 72 - document.addEventListener('keydown', onKeyDown); 70 + root.addEventListener('focusout', onBlur); 71 + root.addEventListener('keydown', onKeyDown); 73 72 74 73 return () => { 75 - document.body.removeEventListener('focusout', onBlur); 76 - document.removeEventListener('keydown', onKeyDown); 74 + root.removeEventListener('focusout', onBlur); 75 + root.removeEventListener('keydown', onKeyDown); 77 76 restoreSelection(selection); 78 77 }; 79 - }, [ref, hasPriority, disabled]); 78 + }, [ref.current, hasPriority, disabled]); 80 79 }
+2 -2
src/useOptionFocus.ts
··· 1 - import { isFocusTarget } from './utils/focus'; 1 + import { isFocusTarget, getActive } from './utils/focus'; 2 2 import { useLayoutEffect } from './utils/react'; 3 3 import { click } from './utils/click'; 4 4 import { contains, isInputElement } from './utils/element'; ··· 26 26 function onKey(event: KeyboardEvent) { 27 27 if (!element || event.defaultPrevented || event.isComposing) return; 28 28 29 - const active = document.activeElement as HTMLElement; 29 + const active = getActive(); 30 30 if (!isFocusTarget(element) || !contains(active, element)) { 31 31 // Do nothing if the current item is not a target or not focused 32 32 return;
+5 -3
src/utils/click.ts
··· 1 - import { clickableSelectors, focus } from './focus'; 1 + import { clickableSelectors, focus, getActive } from './focus'; 2 2 import { contains } from './element'; 3 3 4 - export const click = (node: Element) => { 5 - const activeElement = document.activeElement; 4 + export const click = (node: Element | null) => { 5 + if (!node) return; 6 + 7 + const activeElement = getActive(); 6 8 if (!activeElement || contains(node, activeElement)) { 7 9 let target: Element | null = node; 8 10 if (node.tagName === 'LABEL') {
+6 -2
src/utils/element.ts
··· 12 12 node.matches(excludeSelector) && node.getClientRects().length > 0; 13 13 14 14 /** Returns whether an element accepts text input. */ 15 - export const isInputElement = (node: Element): boolean => 16 - node.matches(inputSelectors); 15 + export const isInputElement = (node: Element | null): boolean => 16 + !!node && node.matches(inputSelectors); 17 17 18 18 export const contains = ( 19 19 owner: Element | EventTarget | null, ··· 24 24 owner && 25 25 (owner === node || (owner as Element).contains(node as Element)) 26 26 ); 27 + 28 + /** Returns the root element of the input element */ 29 + export const getRoot = (node: Element): HTMLElement => 30 + (node.getRootNode() || document.body) as HTMLElement;
+11 -2
src/utils/focus.ts
··· 82 82 export const focus = (node: Element | null) => { 83 83 if (node) { 84 84 (node as HTMLElement).focus(); 85 - } else if (document.activeElement) { 86 - (document.activeElement as HTMLElement).blur(); 85 + } else { 86 + const active = getActive(); 87 + if (active) active.blur(); 87 88 } 88 89 }; 90 + 91 + /** Returns the currently active element, even if it’s contained in a shadow root. */ 92 + export const getActive = (): HTMLElement | null => { 93 + let element = document.activeElement; 94 + while (element && element.shadowRoot) 95 + element = element.shadowRoot.activeElement; 96 + return element as HTMLElement | null; 97 + };
+2 -1
src/utils/selection.ts
··· 1 1 import { contains } from './element'; 2 + import { getActive } from './focus'; 2 3 3 4 export interface RestoreSelection { 4 5 element: HTMLElement; ··· 13 14 export const snapshotSelection = ( 14 15 node?: HTMLElement | null 15 16 ): RestoreSelection | null => { 16 - const target = document.activeElement as HTMLElement | null; 17 + const target = getActive(); 17 18 const element = node && target && node !== target ? node : target; 18 19 if (!element || !target) { 19 20 return null;