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