1 | import matches from 'dom-helpers/matches';
|
2 | import qsa from 'dom-helpers/querySelectorAll';
|
3 | import React, { useCallback, useRef, useEffect, useMemo } from 'react';
|
4 | import PropTypes from 'prop-types';
|
5 | import { useUncontrolledProp } from 'uncontrollable';
|
6 | import usePrevious from '@restart/hooks/usePrevious';
|
7 | import useCallbackRef from '@restart/hooks/useCallbackRef';
|
8 | import useForceUpdate from '@restart/hooks/useForceUpdate';
|
9 | import useEventCallback from '@restart/hooks/useEventCallback';
|
10 | import DropdownContext from './DropdownContext';
|
11 | import DropdownMenu from './DropdownMenu';
|
12 | import DropdownToggle from './DropdownToggle';
|
13 | var propTypes = {
|
14 | |
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | children: PropTypes.func.isRequired,
|
26 |
|
27 | |
28 |
|
29 |
|
30 | drop: PropTypes.oneOf(['up', 'left', 'right', 'down']),
|
31 |
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | focusFirstItemOnShow: PropTypes.oneOf([false, true, 'keyboard']),
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 | itemSelector: PropTypes.string,
|
48 |
|
49 | |
50 |
|
51 |
|
52 | alignEnd: PropTypes.bool,
|
53 |
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 | show: PropTypes.bool,
|
60 |
|
61 | |
62 |
|
63 |
|
64 | defaultShow: PropTypes.bool,
|
65 |
|
66 | |
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | onToggle: PropTypes.func
|
80 | };
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | function Dropdown(_ref) {
|
86 | var drop = _ref.drop,
|
87 | alignEnd = _ref.alignEnd,
|
88 | defaultShow = _ref.defaultShow,
|
89 | rawShow = _ref.show,
|
90 | rawOnToggle = _ref.onToggle,
|
91 | _ref$itemSelector = _ref.itemSelector,
|
92 | itemSelector = _ref$itemSelector === void 0 ? '* > *' : _ref$itemSelector,
|
93 | focusFirstItemOnShow = _ref.focusFirstItemOnShow,
|
94 | children = _ref.children;
|
95 | var forceUpdate = useForceUpdate();
|
96 |
|
97 | var _useUncontrolledProp = useUncontrolledProp(rawShow, defaultShow, rawOnToggle),
|
98 | show = _useUncontrolledProp[0],
|
99 | onToggle = _useUncontrolledProp[1];
|
100 |
|
101 | var _useCallbackRef = useCallbackRef(),
|
102 | toggleElement = _useCallbackRef[0],
|
103 | setToggle = _useCallbackRef[1];
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | var menuRef = useRef(null);
|
109 | var menuElement = menuRef.current;
|
110 | var setMenu = useCallback(function (ref) {
|
111 | menuRef.current = ref;
|
112 |
|
113 | forceUpdate();
|
114 | }, [forceUpdate]);
|
115 | var lastShow = usePrevious(show);
|
116 | var lastSourceEvent = useRef(null);
|
117 | var focusInDropdown = useRef(false);
|
118 | var toggle = useCallback(function (event) {
|
119 | onToggle(!show, event);
|
120 | }, [onToggle, show]);
|
121 | var context = useMemo(function () {
|
122 | return {
|
123 | toggle: toggle,
|
124 | drop: drop,
|
125 | show: show,
|
126 | alignEnd: alignEnd,
|
127 | menuElement: menuElement,
|
128 | toggleElement: toggleElement,
|
129 | setMenu: setMenu,
|
130 | setToggle: setToggle
|
131 | };
|
132 | }, [toggle, drop, show, alignEnd, menuElement, toggleElement, setMenu, setToggle]);
|
133 |
|
134 | if (menuElement && lastShow && !show) {
|
135 | focusInDropdown.current = menuElement.contains(document.activeElement);
|
136 | }
|
137 |
|
138 | var focusToggle = useEventCallback(function () {
|
139 | if (toggleElement && toggleElement.focus) {
|
140 | toggleElement.focus();
|
141 | }
|
142 | });
|
143 | var maybeFocusFirst = useEventCallback(function () {
|
144 | var type = lastSourceEvent.current;
|
145 | var focusType = focusFirstItemOnShow;
|
146 |
|
147 | if (focusType == null) {
|
148 | focusType = menuRef.current && matches(menuRef.current, '[role=menu]') ? 'keyboard' : false;
|
149 | }
|
150 |
|
151 | if (focusType === false || focusType === 'keyboard' && !/^key.+$/.test(type)) {
|
152 | return;
|
153 | }
|
154 |
|
155 | var first = qsa(menuRef.current, itemSelector)[0];
|
156 | if (first && first.focus) first.focus();
|
157 | });
|
158 | useEffect(function () {
|
159 | if (show) maybeFocusFirst();else if (focusInDropdown.current) {
|
160 | focusInDropdown.current = false;
|
161 | focusToggle();
|
162 | }
|
163 | }, [show, focusInDropdown, focusToggle, maybeFocusFirst]);
|
164 | useEffect(function () {
|
165 | lastSourceEvent.current = null;
|
166 | });
|
167 |
|
168 | var getNextFocusedChild = function getNextFocusedChild(current, offset) {
|
169 | if (!menuRef.current) return null;
|
170 | var items = qsa(menuRef.current, itemSelector);
|
171 | var index = items.indexOf(current) + offset;
|
172 | index = Math.max(0, Math.min(index, items.length));
|
173 | return items[index];
|
174 | };
|
175 |
|
176 | var handleKeyDown = function handleKeyDown(event) {
|
177 | var key = event.key;
|
178 | var target = event.target;
|
179 |
|
180 |
|
181 | var isInput = /input|textarea/i.test(target.tagName);
|
182 |
|
183 | if (isInput && (key === ' ' || key !== 'Escape' && menuRef.current && menuRef.current.contains(target))) {
|
184 | return;
|
185 | }
|
186 |
|
187 | lastSourceEvent.current = event.type;
|
188 |
|
189 | switch (key) {
|
190 | case 'ArrowUp':
|
191 | {
|
192 | var next = getNextFocusedChild(target, -1);
|
193 | if (next && next.focus) next.focus();
|
194 | event.preventDefault();
|
195 | return;
|
196 | }
|
197 |
|
198 | case 'ArrowDown':
|
199 | event.preventDefault();
|
200 |
|
201 | if (!show) {
|
202 | toggle(event);
|
203 | } else {
|
204 | var _next = getNextFocusedChild(target, 1);
|
205 |
|
206 | if (_next && _next.focus) _next.focus();
|
207 | }
|
208 |
|
209 | return;
|
210 |
|
211 | case 'Escape':
|
212 | case 'Tab':
|
213 | onToggle(false, event);
|
214 | break;
|
215 |
|
216 | default:
|
217 | }
|
218 | };
|
219 |
|
220 | return React.createElement(DropdownContext.Provider, {
|
221 | value: context
|
222 | }, children({
|
223 | props: {
|
224 | onKeyDown: handleKeyDown
|
225 | }
|
226 | }));
|
227 | }
|
228 |
|
229 | Dropdown.displayName = 'ReactOverlaysDropdown';
|
230 | Dropdown.propTypes = propTypes;
|
231 | Dropdown.Menu = DropdownMenu;
|
232 | Dropdown.Toggle = DropdownToggle;
|
233 | export default Dropdown; |
\ | No newline at end of file |