-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
use-focus-marshal.js
149 lines (126 loc) · 3.87 KB
/
use-focus-marshal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// @flow
import { useRef } from 'react';
import { useMemo, useCallback } from 'use-memo-one';
import type { DraggableId, ContextId } from '../../types';
import type { FocusMarshal, Unregister } from './focus-marshal-types';
import { dragHandle as dragHandleAttr } from '../data-attributes';
import { warning } from '../../dev-warning';
import useLayoutEffect from '../use-isomorphic-layout-effect';
import { find, toArray } from '../../native-with-fallback';
import isHtmlElement from '../is-type-of-element/is-html-element';
type Entry = {|
id: DraggableId,
focus: () => void,
|};
type EntryMap = {
[id: DraggableId]: Entry,
};
function getDragHandle(
contextId: ContextId,
draggableId: DraggableId,
): ?HTMLElement {
// find the drag handle
const selector: string = `[${dragHandleAttr.contextId}="${contextId}"]`;
const possible: Element[] = toArray(document.querySelectorAll(selector));
if (!possible.length) {
warning(`Unable to find any drag handles in the context "${contextId}"`);
return null;
}
const handle: ?Element = find(possible, (el: Element): boolean => {
return el.getAttribute(dragHandleAttr.draggableId) === draggableId;
});
if (!handle) {
warning(
`Unable to find drag handle with id "${draggableId}" as no handle with a matching id was found`,
);
return null;
}
if (!isHtmlElement(handle)) {
warning('drag handle needs to be a HTMLElement');
return null;
}
return handle;
}
export default function useFocusMarshal(contextId: ContextId): FocusMarshal {
const entriesRef = useRef<EntryMap>({});
const recordRef = useRef<?DraggableId>(null);
const restoreFocusFrameRef = useRef<?AnimationFrameID>(null);
const register = useCallback(function register(
id: DraggableId,
focus: () => void,
): Unregister {
const entry: Entry = { id, focus };
entriesRef.current[id] = entry;
return function unregister() {
const entries: EntryMap = entriesRef.current;
const current: Entry = entries[id];
// entry might have been overrided by another registration
if (current !== entry) {
delete entries[id];
}
};
},
[]);
const tryGiveFocus = useCallback(
function tryGiveFocus(tryGiveFocusTo: DraggableId) {
const handle: ?HTMLElement = getDragHandle(contextId, tryGiveFocusTo);
if (handle && handle !== document.activeElement) {
handle.focus();
}
},
[contextId],
);
const tryShiftRecord = useCallback(function tryShiftRecord(
previous: DraggableId,
redirectTo: DraggableId,
) {
if (recordRef.current === previous) {
recordRef.current = redirectTo;
}
},
[]);
const tryRestoreFocusRecorded = useCallback(
function tryRestoreFocusRecorded() {
restoreFocusFrameRef.current = requestAnimationFrame(() => {
restoreFocusFrameRef.current = null;
const record: ?DraggableId = recordRef.current;
if (record) {
tryGiveFocus(record);
}
});
},
[tryGiveFocus],
);
function tryRecordFocus(id: DraggableId) {
// clear any existing record
recordRef.current = null;
const focused: ?Element = document.activeElement;
// no item focused so it cannot be our item
if (!focused) {
return;
}
// focused element is not a drag handle or does not have the right id
if (focused.getAttribute(dragHandleAttr.draggableId) !== id) {
return;
}
recordRef.current = id;
}
useLayoutEffect(() => {
return function clearFrameOnUnmount() {
const frameId: ?AnimationFrameID = restoreFocusFrameRef.current;
if (frameId) {
cancelAnimationFrame(frameId);
}
};
}, []);
const marshal: FocusMarshal = useMemo(
() => ({
register,
tryRecordFocus,
tryRestoreFocusRecorded,
tryShiftRecord,
}),
[register, tryRestoreFocusRecorded, tryShiftRecord],
);
return marshal;
}