UNPKG

9.64 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
4
5var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
6
7exports.__esModule = true;
8exports["default"] = void 0;
9
10var _matches = _interopRequireDefault(require("dom-helpers/matches"));
11
12var _querySelectorAll = _interopRequireDefault(require("dom-helpers/querySelectorAll"));
13
14var _addEventListener = _interopRequireDefault(require("dom-helpers/addEventListener"));
15
16var _react = _interopRequireWildcard(require("react"));
17
18var _propTypes = _interopRequireDefault(require("prop-types"));
19
20var _uncontrollable = require("uncontrollable");
21
22var _usePrevious = _interopRequireDefault(require("@restart/hooks/usePrevious"));
23
24var _useForceUpdate = _interopRequireDefault(require("@restart/hooks/useForceUpdate"));
25
26var _useGlobalListener = _interopRequireDefault(require("@restart/hooks/useGlobalListener"));
27
28var _useEventCallback = _interopRequireDefault(require("@restart/hooks/useEventCallback"));
29
30var _DropdownContext = _interopRequireDefault(require("./DropdownContext"));
31
32var _DropdownMenu = _interopRequireDefault(require("./DropdownMenu"));
33
34var _DropdownToggle = _interopRequireDefault(require("./DropdownToggle"));
35
36var propTypes = {
37 /**
38 * A render prop that returns the root dropdown element. The `props`
39 * argument should spread through to an element containing _both_ the
40 * menu and toggle in order to handle keyboard events for focus management.
41 *
42 * @type {Function ({
43 * props: {
44 * onKeyDown: (SyntheticEvent) => void,
45 * },
46 * }) => React.Element}
47 */
48 children: _propTypes["default"].node,
49
50 /**
51 * Determines the direction and location of the Menu in relation to it's Toggle.
52 */
53 drop: _propTypes["default"].oneOf(['up', 'left', 'right', 'down']),
54
55 /**
56 * Controls the focus behavior for when the Dropdown is opened. Set to
57 * `true` to always focus the first menu item, `keyboard` to focus only when
58 * navigating via the keyboard, or `false` to disable completely
59 *
60 * The Default behavior is `false` **unless** the Menu has a `role="menu"`
61 * where it will default to `keyboard` to match the recommended [ARIA Authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/#menubutton).
62 */
63 focusFirstItemOnShow: _propTypes["default"].oneOf([false, true, 'keyboard']),
64
65 /**
66 * A css slector string that will return __focusable__ menu items.
67 * Selectors should be relative to the menu component:
68 * e.g. ` > li:not('.disabled')`
69 */
70 itemSelector: _propTypes["default"].string,
71
72 /**
73 * Align the menu to the 'end' side of the placement side of the Dropdown toggle. The default placement is `top-start` or `bottom-start`.
74 */
75 alignEnd: _propTypes["default"].bool,
76
77 /**
78 * Whether or not the Dropdown is visible.
79 *
80 * @controllable onToggle
81 */
82 show: _propTypes["default"].bool,
83
84 /**
85 * Sets the initial show position of the Dropdown.
86 */
87 defaultShow: _propTypes["default"].bool,
88
89 /**
90 * A callback fired when the Dropdown wishes to change visibility. Called with the requested
91 * `show` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`.
92 *
93 * ```ts static
94 * function(
95 * isOpen: boolean,
96 * event: SyntheticEvent,
97 * ): void
98 * ```
99 *
100 * @controllable show
101 */
102 onToggle: _propTypes["default"].func
103};
104
105function useRefWithUpdate() {
106 var forceUpdate = (0, _useForceUpdate["default"])();
107 var ref = (0, _react.useRef)(null);
108 var attachRef = (0, _react.useCallback)(function (element) {
109 ref.current = element; // ensure that a menu set triggers an update for consumers
110
111 forceUpdate();
112 }, [forceUpdate]);
113 return [ref, attachRef];
114}
115/**
116 * @displayName Dropdown
117 * @public
118 */
119
120
121function Dropdown(_ref) {
122 var drop = _ref.drop,
123 alignEnd = _ref.alignEnd,
124 defaultShow = _ref.defaultShow,
125 rawShow = _ref.show,
126 rawOnToggle = _ref.onToggle,
127 _ref$itemSelector = _ref.itemSelector,
128 itemSelector = _ref$itemSelector === void 0 ? '* > *' : _ref$itemSelector,
129 focusFirstItemOnShow = _ref.focusFirstItemOnShow,
130 children = _ref.children;
131
132 var _useUncontrolledProp = (0, _uncontrollable.useUncontrolledProp)(rawShow, defaultShow, rawOnToggle),
133 show = _useUncontrolledProp[0],
134 onToggle = _useUncontrolledProp[1]; // We use normal refs instead of useCallbackRef in order to populate the
135 // the value as quickly as possible, otherwise the effect to focus the element
136 // may run before the state value is set
137
138
139 var _useRefWithUpdate = useRefWithUpdate(),
140 menuRef = _useRefWithUpdate[0],
141 setMenu = _useRefWithUpdate[1];
142
143 var menuElement = menuRef.current;
144
145 var _useRefWithUpdate2 = useRefWithUpdate(),
146 toggleRef = _useRefWithUpdate2[0],
147 setToggle = _useRefWithUpdate2[1];
148
149 var toggleElement = toggleRef.current;
150 var lastShow = (0, _usePrevious["default"])(show);
151 var lastSourceEvent = (0, _react.useRef)(null);
152 var focusInDropdown = (0, _react.useRef)(false);
153 var toggle = (0, _react.useCallback)(function (nextShow, event) {
154 onToggle(nextShow, event);
155 }, [onToggle]);
156 var context = (0, _react.useMemo)(function () {
157 return {
158 toggle: toggle,
159 drop: drop,
160 show: show,
161 alignEnd: alignEnd,
162 menuElement: menuElement,
163 toggleElement: toggleElement,
164 setMenu: setMenu,
165 setToggle: setToggle
166 };
167 }, [toggle, drop, show, alignEnd, menuElement, toggleElement, setMenu, setToggle]);
168
169 if (menuElement && lastShow && !show) {
170 focusInDropdown.current = menuElement.contains(document.activeElement);
171 }
172
173 var focusToggle = (0, _useEventCallback["default"])(function () {
174 if (toggleElement && toggleElement.focus) {
175 toggleElement.focus();
176 }
177 });
178 var maybeFocusFirst = (0, _useEventCallback["default"])(function () {
179 var type = lastSourceEvent.current;
180 var focusType = focusFirstItemOnShow;
181
182 if (focusType == null) {
183 focusType = menuRef.current && (0, _matches["default"])(menuRef.current, '[role=menu]') ? 'keyboard' : false;
184 }
185
186 if (focusType === false || focusType === 'keyboard' && !/^key.+$/.test(type)) {
187 return;
188 }
189
190 var first = (0, _querySelectorAll["default"])(menuRef.current, itemSelector)[0];
191 if (first && first.focus) first.focus();
192 });
193 (0, _react.useEffect)(function () {
194 if (show) maybeFocusFirst();else if (focusInDropdown.current) {
195 focusInDropdown.current = false;
196 focusToggle();
197 } // only `show` should be changing
198 }, [show, focusInDropdown, focusToggle, maybeFocusFirst]);
199 (0, _react.useEffect)(function () {
200 lastSourceEvent.current = null;
201 });
202
203 var getNextFocusedChild = function getNextFocusedChild(current, offset) {
204 if (!menuRef.current) return null;
205 var items = (0, _querySelectorAll["default"])(menuRef.current, itemSelector);
206 var index = items.indexOf(current) + offset;
207 index = Math.max(0, Math.min(index, items.length));
208 return items[index];
209 };
210
211 (0, _useGlobalListener["default"])('keydown', function (event) {
212 var _menuRef$current, _toggleRef$current;
213
214 var key = event.key;
215 var target = event.target;
216 var fromMenu = (_menuRef$current = menuRef.current) == null ? void 0 : _menuRef$current.contains(target);
217 var fromToggle = (_toggleRef$current = toggleRef.current) == null ? void 0 : _toggleRef$current.contains(target); // Second only to https://github.com/twbs/bootstrap/blob/8cfbf6933b8a0146ac3fbc369f19e520bd1ebdac/js/src/dropdown.js#L400
218 // in inscrutability
219
220 var isInput = /input|textarea/i.test(target.tagName);
221
222 if (isInput && (key === ' ' || key !== 'Escape' && fromMenu)) {
223 return;
224 }
225
226 if (!fromMenu && !fromToggle) {
227 return;
228 }
229
230 if (!menuRef.current && key === 'Tab') {
231 return;
232 }
233
234 lastSourceEvent.current = event.type;
235
236 switch (key) {
237 case 'ArrowUp':
238 {
239 var next = getNextFocusedChild(target, -1);
240 if (next && next.focus) next.focus();
241 event.preventDefault();
242 return;
243 }
244
245 case 'ArrowDown':
246 event.preventDefault();
247
248 if (!show) {
249 onToggle(true, event);
250 } else {
251 var _next = getNextFocusedChild(target, 1);
252
253 if (_next && _next.focus) _next.focus();
254 }
255
256 return;
257
258 case 'Tab':
259 // on keydown the target is the element being tabbed FROM, we need that
260 // to know if this event is relevant to this dropdown (e.g. in this menu).
261 // On `keyup` the target is the element being tagged TO which we use to check
262 // if focus has left the menu
263 (0, _addEventListener["default"])(document, 'keyup', function (e) {
264 var _menuRef$current2;
265
266 if (e.key === 'Tab' && !e.target || !((_menuRef$current2 = menuRef.current) != null && _menuRef$current2.contains(e.target))) {
267 onToggle(false, event);
268 }
269 }, {
270 once: true
271 });
272 break;
273
274 case 'Escape':
275 event.preventDefault();
276 event.stopPropagation();
277 onToggle(false, event);
278 break;
279
280 default:
281 }
282 });
283 return /*#__PURE__*/_react["default"].createElement(_DropdownContext["default"].Provider, {
284 value: context
285 }, children);
286}
287
288Dropdown.displayName = 'ReactOverlaysDropdown';
289Dropdown.propTypes = propTypes;
290Dropdown.Menu = _DropdownMenu["default"];
291Dropdown.Toggle = _DropdownToggle["default"];
292var _default = Dropdown;
293exports["default"] = _default;
294module.exports = exports.default;
\No newline at end of file