Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

249 Zeilen
7.7 KiB

  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 { useUncontrolled } 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. * A render prop that returns the root dropdown element. The `props`
  16. * argument should spread through to an element containing _both_ the
  17. * menu and toggle in order to handle keyboard events for focus management.
  18. *
  19. * @type {Function ({
  20. * props: {
  21. * onKeyDown: (SyntheticEvent) => void,
  22. * },
  23. * }) => React.Element}
  24. */
  25. children: PropTypes.func.isRequired,
  26. /**
  27. * Determines the direction and location of the Menu in relation to it's Toggle.
  28. */
  29. drop: PropTypes.oneOf(['up', 'left', 'right', 'down']),
  30. /**
  31. * Controls the focus behavior for when the Dropdown is opened. Set to
  32. * `true` to always focus the first menu item, `keyboard` to focus only when
  33. * navigating via the keyboard, or `false` to disable completely
  34. *
  35. * The Default behavior is `false` **unless** the Menu has a `role="menu"`
  36. * where it will default to `keyboard` to match the recommended [ARIA Authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/#menubutton).
  37. */
  38. focusFirstItemOnShow: PropTypes.oneOf([false, true, 'keyboard']),
  39. /**
  40. * A css slector string that will return __focusable__ menu items.
  41. * Selectors should be relative to the menu component:
  42. * e.g. ` > li:not('.disabled')`
  43. */
  44. itemSelector: PropTypes.string.isRequired,
  45. /**
  46. * Align the menu to the 'end' side of the placement side of the Dropdown toggle. The default placement is `top-start` or `bottom-start`.
  47. */
  48. alignEnd: PropTypes.bool,
  49. /**
  50. * Whether or not the Dropdown is visible.
  51. *
  52. * @controllable onToggle
  53. */
  54. show: PropTypes.bool,
  55. /**
  56. * Sets the initial show position of the Dropdown.
  57. */
  58. defaultShow: PropTypes.bool,
  59. /**
  60. * A callback fired when the Dropdown wishes to change visibility. Called with the requested
  61. * `show` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`.
  62. *
  63. * ```js
  64. * function(
  65. * isOpen: boolean,
  66. * event: SyntheticEvent,
  67. * ): void
  68. * ```
  69. *
  70. * @controllable show
  71. */
  72. onToggle: PropTypes.func
  73. };
  74. var defaultProps = {
  75. itemSelector: '* > *'
  76. };
  77. /**
  78. * `Dropdown` is set of structural components for building, accessible dropdown menus with close-on-click,
  79. * keyboard navigation, and correct focus handling. As with all the react-overlay's
  80. * components its BYOS (bring your own styles). Dropdown is primarily
  81. * built from three base components, you should compose to build your Dropdowns.
  82. *
  83. * - `Dropdown`, which wraps the menu and toggle, and handles keyboard navigation
  84. * - `Dropdown.Toggle` generally a button that triggers the menu opening
  85. * - `Dropdown.Menu` The overlaid, menu, positioned to the toggle with PopperJs
  86. */
  87. function Dropdown(_ref) {
  88. var drop = _ref.drop,
  89. alignEnd = _ref.alignEnd,
  90. defaultShow = _ref.defaultShow,
  91. rawShow = _ref.show,
  92. rawOnToggle = _ref.onToggle,
  93. itemSelector = _ref.itemSelector,
  94. focusFirstItemOnShow = _ref.focusFirstItemOnShow,
  95. children = _ref.children;
  96. var forceUpdate = useForceUpdate();
  97. var _useUncontrolled = useUncontrolled({
  98. defaultShow: defaultShow,
  99. show: rawShow,
  100. onToggle: rawOnToggle
  101. }, {
  102. show: 'onToggle'
  103. }),
  104. show = _useUncontrolled.show,
  105. onToggle = _useUncontrolled.onToggle;
  106. var _useCallbackRef = useCallbackRef(),
  107. toggleElement = _useCallbackRef[0],
  108. setToggle = _useCallbackRef[1]; // We use normal refs instead of useCallbackRef in order to populate the
  109. // the value as quickly as possible, otherwise the effect to focus the element
  110. // may run before the state value is set
  111. var menuRef = useRef();
  112. var menuElement = menuRef.current;
  113. var setMenu = useCallback(function (ref) {
  114. menuRef.current = ref; // ensure that a menu set triggers an update for consumers
  115. forceUpdate();
  116. }, [forceUpdate]);
  117. var lastShow = usePrevious(show);
  118. var lastSourceEvent = useRef(null);
  119. var focusInDropdown = useRef(false);
  120. var toggle = useCallback(function (event) {
  121. onToggle(!show, event);
  122. }, [onToggle, show]);
  123. var context = useMemo(function () {
  124. return {
  125. toggle: toggle,
  126. drop: drop,
  127. show: show,
  128. alignEnd: alignEnd,
  129. menuElement: menuElement,
  130. toggleElement: toggleElement,
  131. setMenu: setMenu,
  132. setToggle: setToggle
  133. };
  134. }, [toggle, drop, show, alignEnd, menuElement, toggleElement, setMenu, setToggle]);
  135. if (menuElement && lastShow && !show) {
  136. focusInDropdown.current = menuElement.contains(document.activeElement);
  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. if (focusType == null) {
  147. focusType = menuRef.current && matches(menuRef.current, '[role=menu]') ? 'keyboard' : false;
  148. }
  149. if (focusType === false || focusType === 'keyboard' && !/^key.+$/.test(type)) {
  150. return;
  151. }
  152. var first = qsa(menuRef.current, itemSelector)[0];
  153. if (first && first.focus) first.focus();
  154. });
  155. useEffect(function () {
  156. if (show) maybeFocusFirst();else if (focusInDropdown.current) {
  157. focusInDropdown.current = false;
  158. focusToggle();
  159. } // only `show` should be changing
  160. }, [show, focusInDropdown, focusToggle, maybeFocusFirst]);
  161. useEffect(function () {
  162. lastSourceEvent.current = null;
  163. });
  164. var getNextFocusedChild = function getNextFocusedChild(current, offset) {
  165. if (!menuRef.current) return null;
  166. var items = qsa(menuRef.current, itemSelector);
  167. var index = items.indexOf(current) + offset;
  168. index = Math.max(0, Math.min(index, items.length));
  169. return items[index];
  170. };
  171. var handleKeyDown = function handleKeyDown(event) {
  172. var key = event.key,
  173. target = event.target; // Second only to https://github.com/twbs/bootstrap/blob/8cfbf6933b8a0146ac3fbc369f19e520bd1ebdac/js/src/dropdown.js#L400
  174. // in inscrutability
  175. var isInput = /input|textarea/i.test(target.tagName);
  176. if (isInput && (key === ' ' || key !== 'Escape' && menuRef.current && menuRef.current.contains(target))) {
  177. return;
  178. }
  179. lastSourceEvent.current = event.type;
  180. switch (key) {
  181. case 'ArrowUp':
  182. {
  183. var next = getNextFocusedChild(target, -1);
  184. if (next && next.focus) next.focus();
  185. event.preventDefault();
  186. return;
  187. }
  188. case 'ArrowDown':
  189. event.preventDefault();
  190. if (!show) {
  191. toggle(event);
  192. } else {
  193. var _next = getNextFocusedChild(target, 1);
  194. if (_next && _next.focus) _next.focus();
  195. }
  196. return;
  197. case 'Escape':
  198. case 'Tab':
  199. onToggle(false, event);
  200. break;
  201. default:
  202. }
  203. };
  204. return React.createElement(DropdownContext.Provider, {
  205. value: context
  206. }, children({
  207. props: {
  208. onKeyDown: handleKeyDown
  209. }
  210. }));
  211. }
  212. Dropdown.displayName = 'ReactOverlaysDropdown';
  213. Dropdown.propTypes = propTypes;
  214. Dropdown.defaultProps = defaultProps;
  215. Dropdown.Menu = DropdownMenu;
  216. Dropdown.Toggle = DropdownToggle;
  217. export default Dropdown;