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 = RestoreInputSelection | RestoreActiveNode | RestoreSelectionRange; 19 20const isInputElement = (node: HTMLElement): node is HTMLInputElement => ( 21 (node.nodeName === 'input' || node.nodeName === 'textarea') && 22 typeof (node as HTMLInputElement).selectionStart === 'number' && 23 typeof (node as HTMLInputElement).selectionEnd === 'number' 24); 25 26/** Snapshots the current focus or selection target, optinally using a ref if it's passed. */ 27export const snapshotSelection = (node?: HTMLElement | null): RestoreSelection | null => { 28 const target = document.activeElement as HTMLElement | null; 29 const element = node && target && node !== target ? node : target; 30 if (!element || !target) { 31 return null; 32 } else if (isInputElement(target)) { 33 return { 34 element, 35 method: 'setSelectionRange', 36 arguments: [target.selectionStart!, target.selectionEnd!, target.selectionDirection || undefined], 37 }; 38 } 39 40 const selection = window.getSelection && window.getSelection(); 41 if (selection && selection.rangeCount) { 42 const range = selection.getRangeAt(0); 43 return { element, method: 'range', range }; 44 } 45 46 return { element, method: 'focus' }; 47}; 48 49/** Restores a given snapshot of a selection, falling back to a simple focus. */ 50export const restoreSelection = (restore: RestoreSelection | null) => { 51 const target = restore && restore.element; 52 if (!restore || !target || !target.parentNode) { 53 return; 54 } else if (restore.method === 'setSelectionRange' && isInputElement(target)) { 55 target.setSelectionRange(...restore.arguments); 56 } else if (restore.method === 'range') { 57 const selection = window.getSelection()!; 58 selection.removeAllRanges(); 59 selection.addRange(restore.range); 60 } else { 61 target.focus(); 62 } 63};