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