Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { contains } from './element'; 2 3interface RestoreInputSelection { 4 element: HTMLInputElement; 5 method: 'setSelectionRange'; 6 arguments: [number, number, 'forward' | 'backward' | 'none' | undefined]; 7} 8 9interface RestoreActiveNode { 10 element: HTMLElement; 11 method: 'focus'; 12} 13 14interface RestoreSelectionRange { 15 element: HTMLElement; 16 method: 'range'; 17 range: Range; 18} 19 20export type RestoreSelection = 21 | RestoreInputSelection 22 | RestoreActiveNode 23 | RestoreSelectionRange; 24 25const hasSelection = (node: HTMLElement): node is HTMLInputElement => 26 (node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA') && 27 typeof (node as HTMLInputElement).selectionStart === 'number' && 28 typeof (node as HTMLInputElement).selectionEnd === 'number'; 29 30/** Snapshots the current focus or selection target, optinally using a ref if it's passed. */ 31export const snapshotSelection = ( 32 node?: HTMLElement | null 33): RestoreSelection | null => { 34 const target = document.activeElement as HTMLElement | null; 35 const element = node && target && node !== target ? node : target; 36 if (!element || !target) { 37 return null; 38 } else if (hasSelection(element)) { 39 return { 40 element, 41 method: 'setSelectionRange', 42 arguments: [ 43 element.selectionStart!, 44 element.selectionEnd!, 45 element.selectionDirection || undefined, 46 ], 47 }; 48 } 49 50 const selection = window.getSelection && window.getSelection(); 51 if (selection && selection.rangeCount) { 52 const range = selection.getRangeAt(0); 53 if (range.startContainer && contains(target, range.startContainer)) { 54 return { element, method: 'range', range }; 55 } 56 } 57 58 return { element, method: 'focus' }; 59}; 60 61/** Restores a given snapshot of a selection, falling back to a simple focus. */ 62export const restoreSelection = (restore: RestoreSelection | null) => { 63 if (!restore || !restore.element.parentNode) { 64 return; 65 } else if (restore.method === 'setSelectionRange') { 66 restore.element.focus(); 67 restore.element.setSelectionRange(...restore.arguments); 68 } else if (restore.method === 'range') { 69 const selection = window.getSelection()!; 70 restore.element.focus(); 71 selection.removeAllRanges(); 72 selection.addRange(restore.range); 73 } else { 74 restore.element.focus(); 75 } 76};