Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { contains } from './element';
2
3interface RestoreInputSelection {
4 element: HTMLElement;
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 isInputElement = (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 (isInputElement(target)) {
39 return {
40 element,
41 method: 'setSelectionRange',
42 arguments: [
43 target.selectionStart!,
44 target.selectionEnd!,
45 target.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 (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 const target = restore && restore.element;
64 if (!restore || !target || !target.parentNode) {
65 return;
66 } else if (restore.method === 'setSelectionRange' && isInputElement(target)) {
67 target.focus();
68 target.setSelectionRange(...restore.arguments);
69 } else if (restore.method === 'range') {
70 const selection = window.getSelection()!;
71 target.focus();
72 selection.removeAllRanges();
73 selection.addRange(restore.range);
74 } else {
75 target.focus();
76 }
77};