UNPKG

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