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};