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