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