Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { contains } from './element';
2import { getActive } from './focus';
3
4export interface RestoreSelection {
5 element: HTMLElement;
6 restore(): void;
7}
8
9const hasSelection = (node: HTMLElement): node is HTMLInputElement =>
10 typeof (node as HTMLInputElement).selectionStart === 'number' &&
11 typeof (node as HTMLInputElement).selectionEnd === 'number';
12
13/** Snapshots the current focus or selection target, optinally using a ref if it's passed. */
14export const snapshotSelection = (
15 node?: HTMLElement | null
16): RestoreSelection | null => {
17 const target = getActive();
18 const element = node && target && node !== target ? node : target;
19 if (!element || !target) {
20 return null;
21 } else if (hasSelection(element)) {
22 const { selectionStart, selectionEnd, selectionDirection } = element;
23 return {
24 element,
25 restore() {
26 element.focus();
27 element.setSelectionRange(
28 selectionStart,
29 selectionEnd,
30 selectionDirection || undefined
31 );
32 },
33 };
34 }
35
36 let range: Range | undefined;
37
38 const selection = window.getSelection();
39 if (selection && selection.rangeCount) {
40 const _range = selection.getRangeAt(0);
41 if (_range.startContainer && contains(target, _range.startContainer)) {
42 range = _range;
43 }
44 }
45
46 return {
47 element,
48 restore() {
49 element.focus();
50 const selection = window.getSelection();
51 if (range && selection) {
52 selection.removeAllRanges();
53 selection.addRange(range);
54 }
55 },
56 };
57};
58
59/** Restores a given snapshot of a selection, falling back to a simple focus. */
60export const restoreSelection = (selection: RestoreSelection | null) => {
61 if (selection && selection.element.parentNode) {
62 selection.restore();
63 }
64};