@atlaskit/dropdown-menu 11.11.4 → 11.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @atlaskit/dropdown-menu
2
2
 
3
+ ## 11.11.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [`9e84eaacfbe`](https://bitbucket.org/atlassian/atlassian-frontend/commits/9e84eaacfbe) - Nested dropdown menus are now accessible using arrow key navigation.
8
+
3
9
  ## 11.11.4
4
10
 
5
11
  ### Patch Changes
@@ -14,6 +14,7 @@ var _react = _interopRequireWildcard(require("react"));
14
14
  var _bindEventListener = require("bind-event-listener");
15
15
  var _standardButton = _interopRequireDefault(require("@atlaskit/button/standard-button"));
16
16
  var _keycodes = require("@atlaskit/ds-lib/keycodes");
17
+ var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
17
18
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
18
19
  var _useControlled = _interopRequireDefault(require("@atlaskit/ds-lib/use-controlled"));
19
20
  var _useFocusEvent = _interopRequireDefault(require("@atlaskit/ds-lib/use-focus-event"));
@@ -21,9 +22,11 @@ var _chevronDown = _interopRequireDefault(require("@atlaskit/icon/glyph/chevron-
21
22
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
22
23
  var _popup = _interopRequireDefault(require("@atlaskit/popup"));
23
24
  var _constants = require("@atlaskit/theme/constants");
25
+ var _context = require("./internal/components/context");
24
26
  var _focusManager = _interopRequireDefault(require("./internal/components/focus-manager"));
25
27
  var _menuWrapper = _interopRequireDefault(require("./internal/components/menu-wrapper"));
26
28
  var _selectionStore = _interopRequireDefault(require("./internal/context/selection-store"));
29
+ var _useRegisterItemWithFocusManager = _interopRequireDefault(require("./internal/hooks/use-register-item-with-focus-manager"));
27
30
  var _useGeneratedId = _interopRequireWildcard(require("./internal/utils/use-generated-id"));
28
31
  var _excluded = ["ref"]; // eslint-disable-next-line @atlaskit/design-system/no-deprecated-imports
29
32
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
@@ -59,7 +62,6 @@ var getFallbackPlacements = function getFallbackPlacements(placement) {
59
62
  return ["".concat(mainAxis, "-start"), "".concat(mainAxis, "-end"), "".concat(opposites[mainAxis]), "".concat(opposites[mainAxis], "-start"), "".concat(opposites[mainAxis], "-end"), "auto"];
60
63
  }
61
64
  };
62
- var NestedContext = /*#__PURE__*/(0, _react.createContext)(false);
63
65
 
64
66
  /**
65
67
  * __Dropdown menu__
@@ -97,7 +99,7 @@ var DropdownMenu = function DropdownMenu(props) {
97
99
  _useControlledState2 = (0, _slicedToArray2.default)(_useControlledState, 2),
98
100
  isLocalOpen = _useControlledState2[0],
99
101
  setLocalIsOpen = _useControlledState2[1];
100
- var isNested = (0, _react.useContext)(NestedContext);
102
+ var nestedLevel = (0, _react.useContext)(_context.NestedLevelContext);
101
103
  var _useState = (0, _react.useState)(false),
102
104
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
103
105
  isTriggeredUsingKeyboard = _useState2[0],
@@ -171,6 +173,8 @@ var DropdownMenu = function DropdownMenu(props) {
171
173
  });
172
174
  }, [isFocused, isLocalOpen, handleTriggerClicked]);
173
175
  var id = (0, _useGeneratedId.default)();
176
+ var isNested = nestedLevel > 0;
177
+ var itemRef = (0, _useRegisterItemWithFocusManager.default)();
174
178
  return /*#__PURE__*/_react.default.createElement(_selectionStore.default, null, /*#__PURE__*/_react.default.createElement(_popup.default, {
175
179
  id: isLocalOpen ? id : undefined,
176
180
  shouldFlip: shouldFlip,
@@ -188,14 +192,14 @@ var DropdownMenu = function DropdownMenu(props) {
188
192
  var ref = triggerProps.ref,
189
193
  providedProps = (0, _objectWithoutProperties2.default)(triggerProps, _excluded);
190
194
  return _trigger(_objectSpread(_objectSpread(_objectSpread({}, providedProps), bindFocus), {}, {
191
- triggerRef: ref,
195
+ triggerRef: isNested ? (0, _mergeRefs.default)([ref, itemRef]) : ref,
192
196
  isSelected: isLocalOpen,
193
197
  onClick: handleTriggerClicked,
194
198
  testId: testId && "".concat(testId, "--trigger")
195
199
  }));
196
200
  }
197
201
  return /*#__PURE__*/_react.default.createElement(_standardButton.default, (0, _extends2.default)({}, bindFocus, {
198
- ref: triggerProps.ref,
202
+ ref: isNested ? (0, _mergeRefs.default)([triggerProps.ref, itemRef]) : triggerProps.ref,
199
203
  "aria-controls": triggerProps['aria-controls'],
200
204
  "aria-expanded": triggerProps['aria-expanded'],
201
205
  "aria-haspopup": triggerProps['aria-haspopup'],
@@ -211,7 +215,7 @@ var DropdownMenu = function DropdownMenu(props) {
211
215
  content: function content(_ref) {
212
216
  var setInitialFocusRef = _ref.setInitialFocusRef,
213
217
  update = _ref.update;
214
- return /*#__PURE__*/_react.default.createElement(_focusManager.default, null, /*#__PURE__*/_react.default.createElement(_menuWrapper.default, {
218
+ var content = /*#__PURE__*/_react.default.createElement(_focusManager.default, null, /*#__PURE__*/_react.default.createElement(_menuWrapper.default, {
215
219
  spacing: spacing,
216
220
  maxHeight: MAX_HEIGHT,
217
221
  maxWidth: 800,
@@ -220,9 +224,10 @@ var DropdownMenu = function DropdownMenu(props) {
220
224
  isLoading: isLoading,
221
225
  statusLabel: statusLabel,
222
226
  setInitialFocusRef: isTriggeredUsingKeyboard || autoFocus ? setInitialFocusRef : undefined
223
- }, isNested ? children : /*#__PURE__*/_react.default.createElement(NestedContext.Provider, {
224
- value: true
227
+ }, /*#__PURE__*/_react.default.createElement(_context.NestedLevelContext.Provider, {
228
+ value: nestedLevel + 1
225
229
  }, children)));
230
+ return isNested ? content : /*#__PURE__*/_react.default.createElement(_context.TrackLevelProvider, null, content);
226
231
  }
227
232
  }));
228
233
  };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.TrackMaxLevelContext = exports.TrackLevelProvider = exports.NestedLevelContext = void 0;
8
+ var _react = _interopRequireWildcard(require("react"));
9
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
10
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
11
+ /**
12
+ *
13
+ * @internal
14
+ * Context which maintains the current level of dropdown menu if it is nested
15
+ * Default is 0
16
+ *
17
+ */
18
+ var NestedLevelContext = /*#__PURE__*/(0, _react.createContext)(0);
19
+
20
+ /**
21
+ *
22
+ * @internal
23
+ * Context which maintains the maximun level of dropdown menu if it is nested
24
+ * Default is 0
25
+ *
26
+ */
27
+ exports.NestedLevelContext = NestedLevelContext;
28
+ var TrackMaxLevelContext = /*#__PURE__*/(0, _react.createContext)({
29
+ maxLevelRef: {
30
+ current: 0
31
+ },
32
+ setMaxLevel: function setMaxLevel() {}
33
+ });
34
+
35
+ /**
36
+ *
37
+ * @internal
38
+ * Context provider which maintains the maximun level of dropdown menu if it is nested
39
+ *
40
+ */
41
+ exports.TrackMaxLevelContext = TrackMaxLevelContext;
42
+ var TrackLevelProvider = function TrackLevelProvider(_ref) {
43
+ var children = _ref.children;
44
+ var maxLevelRef = (0, _react.useRef)(0);
45
+ var value = (0, _react.useMemo)(function () {
46
+ return {
47
+ maxLevelRef: maxLevelRef,
48
+ setMaxLevel: function setMaxLevel(level) {
49
+ var isMin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
50
+ maxLevelRef.current = isMin ? Math.min(maxLevelRef.current, level) : level;
51
+ }
52
+ };
53
+ }, [maxLevelRef]);
54
+ return /*#__PURE__*/_react.default.createElement(TrackMaxLevelContext.Provider, {
55
+ value: value
56
+ }, children);
57
+ };
58
+ exports.TrackLevelProvider = TrackLevelProvider;
@@ -10,6 +10,7 @@ var _react = _interopRequireWildcard(require("react"));
10
10
  var _bindEventListener = require("bind-event-listener");
11
11
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
12
12
  var _handleFocus = _interopRequireDefault(require("../utils/handle-focus"));
13
+ var _context = require("./context");
13
14
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
14
15
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
15
16
  /**
@@ -37,13 +38,24 @@ var FocusManager = function FocusManager(_ref) {
37
38
  menuItemRefs.current.push(ref);
38
39
  }
39
40
  }, []);
40
-
41
+ var nestedLevel = (0, _react.useContext)(_context.NestedLevelContext);
42
+ var _useContext = (0, _react.useContext)(_context.TrackMaxLevelContext),
43
+ maxLevelRef = _useContext.maxLevelRef,
44
+ setMaxLevel = _useContext.setMaxLevel;
41
45
  // Intentionally rebinding on each render
42
46
  (0, _react.useEffect)(function () {
43
- return (0, _bindEventListener.bind)(window, {
47
+ var prevLevel = nestedLevel - 1;
48
+ setMaxLevel(nestedLevel);
49
+ var unbind = (0, _bindEventListener.bind)(window, {
44
50
  type: 'keydown',
45
- listener: (0, _handleFocus.default)(menuItemRefs.current)
51
+ listener: (0, _handleFocus.default)(menuItemRefs.current, nestedLevel, maxLevelRef)
46
52
  });
53
+ return function () {
54
+ // Always get the minimun level when multiple levels of menu are closed
55
+ // If the stored level is smaller, we won't update it
56
+ setMaxLevel(prevLevel, true);
57
+ unbind();
58
+ };
47
59
  });
48
60
  var contextValue = {
49
61
  menuItemRefs: menuItemRefs.current,
@@ -18,7 +18,7 @@ var actionMap = (_actionMap = {}, (0, _defineProperty2.default)(_actionMap, _key
18
18
  */
19
19
  var getNextFocusableElement = function getNextFocusableElement(refs, currentFocusedIdx) {
20
20
  while (currentFocusedIdx + 1 < refs.length) {
21
- var isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
21
+ var isDisabled = refs[currentFocusedIdx + 1].hasAttribute('disabled');
22
22
  if (!isDisabled) {
23
23
  return refs[currentFocusedIdx + 1];
24
24
  }
@@ -34,19 +34,24 @@ var getNextFocusableElement = function getNextFocusableElement(refs, currentFocu
34
34
  */
35
35
  var getPrevFocusableElement = function getPrevFocusableElement(refs, currentFocusedIdx) {
36
36
  while (currentFocusedIdx > 0) {
37
- var isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
37
+ var isDisabled = refs[currentFocusedIdx - 1].hasAttribute('disabled');
38
38
  if (!isDisabled) {
39
39
  return refs[currentFocusedIdx - 1];
40
40
  }
41
41
  currentFocusedIdx--;
42
42
  }
43
43
  };
44
- function handleFocus(refs) {
44
+ function handleFocus(refs, nestedLevel, maxLevelRef) {
45
45
  return function (e) {
46
46
  var currentFocusedIdx = refs.findIndex(function (el) {
47
47
  var _document$activeEleme;
48
48
  return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
49
49
  });
50
+ if (nestedLevel < maxLevelRef.current) {
51
+ // if it is a nested dropdown and the level of the given dropdown is not the current level,
52
+ // we don't need to have focus on it
53
+ return;
54
+ }
50
55
  var action = actionMap[e.key];
51
56
  switch (action) {
52
57
  case 'next':
@@ -54,7 +59,7 @@ function handleFocus(refs) {
54
59
  e.preventDefault();
55
60
  if (currentFocusedIdx < refs.length - 1) {
56
61
  var _nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
57
- _nextFocusableElement && _nextFocusableElement.focus();
62
+ _nextFocusableElement === null || _nextFocusableElement === void 0 ? void 0 : _nextFocusableElement.focus();
58
63
  }
59
64
  break;
60
65
  case 'prev':
@@ -62,20 +67,20 @@ function handleFocus(refs) {
62
67
  e.preventDefault();
63
68
  if (currentFocusedIdx > 0) {
64
69
  var _prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
65
- _prevFocusableElement && _prevFocusableElement.focus();
70
+ _prevFocusableElement === null || _prevFocusableElement === void 0 ? void 0 : _prevFocusableElement.focus();
66
71
  }
67
72
  break;
68
73
  case 'first':
69
74
  e.preventDefault();
70
75
  // Search for first non-disabled element if first element is disabled
71
76
  var nextFocusableElement = getNextFocusableElement(refs, -1);
72
- nextFocusableElement && nextFocusableElement.focus();
77
+ nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
73
78
  break;
74
79
  case 'last':
75
80
  e.preventDefault();
76
81
  // Search for last non-disabled element if last element is disabled
77
82
  var prevFocusableElement = getPrevFocusableElement(refs, refs.length);
78
- prevFocusableElement && prevFocusableElement.focus();
83
+ prevFocusableElement === null || prevFocusableElement === void 0 ? void 0 : prevFocusableElement.focus();
79
84
  break;
80
85
  default:
81
86
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/dropdown-menu",
3
- "version": "11.11.4",
3
+ "version": "11.11.5",
4
4
  "sideEffects": false
5
5
  }
@@ -1,8 +1,9 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
- import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
2
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
3
3
  import { bind } from 'bind-event-listener';
4
4
  import Button from '@atlaskit/button/standard-button';
5
5
  import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
6
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
7
  import noop from '@atlaskit/ds-lib/noop';
7
8
  import useControlledState from '@atlaskit/ds-lib/use-controlled';
8
9
  import useFocus from '@atlaskit/ds-lib/use-focus-event';
@@ -11,9 +12,11 @@ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
11
12
  import Popup from '@atlaskit/popup';
12
13
  // eslint-disable-next-line @atlaskit/design-system/no-deprecated-imports
13
14
  import { gridSize as gridSizeFn, layers } from '@atlaskit/theme/constants';
15
+ import { NestedLevelContext, TrackLevelProvider } from './internal/components/context';
14
16
  import FocusManager from './internal/components/focus-manager';
15
17
  import MenuWrapper from './internal/components/menu-wrapper';
16
18
  import SelectionStore from './internal/context/selection-store';
19
+ import useRegisterItemWithFocusManager from './internal/hooks/use-register-item-with-focus-manager';
17
20
  import useGeneratedId, { PREFIX } from './internal/utils/use-generated-id';
18
21
  const gridSize = gridSizeFn();
19
22
  const MAX_HEIGHT = `calc(100vh - ${gridSize * 2}px)`;
@@ -44,7 +47,6 @@ const getFallbackPlacements = placement => {
44
47
  return [`${mainAxis}-start`, `${mainAxis}-end`, `${opposites[mainAxis]}`, `${opposites[mainAxis]}-start`, `${opposites[mainAxis]}-end`, `auto`];
45
48
  }
46
49
  };
47
- const NestedContext = /*#__PURE__*/createContext(false);
48
50
 
49
51
  /**
50
52
  * __Dropdown menu__
@@ -72,7 +74,7 @@ const DropdownMenu = props => {
72
74
  zIndex = layers.modal()
73
75
  } = props;
74
76
  const [isLocalOpen, setLocalIsOpen] = useControlledState(isOpen, () => defaultOpen);
75
- const isNested = useContext(NestedContext);
77
+ const nestedLevel = useContext(NestedLevelContext);
76
78
  const [isTriggeredUsingKeyboard, setTriggeredUsingKeyboard] = useState(false);
77
79
  const fallbackPlacements = useMemo(() => getFallbackPlacements(placement), [placement]);
78
80
  const handleTriggerClicked = useCallback(
@@ -144,6 +146,8 @@ const DropdownMenu = props => {
144
146
  });
145
147
  }, [isFocused, isLocalOpen, handleTriggerClicked]);
146
148
  const id = useGeneratedId();
149
+ const isNested = nestedLevel > 0;
150
+ const itemRef = useRegisterItemWithFocusManager();
147
151
  return /*#__PURE__*/React.createElement(SelectionStore, null, /*#__PURE__*/React.createElement(Popup, {
148
152
  id: isLocalOpen ? id : undefined,
149
153
  shouldFlip: shouldFlip,
@@ -165,14 +169,14 @@ const DropdownMenu = props => {
165
169
  return trigger({
166
170
  ...providedProps,
167
171
  ...bindFocus,
168
- triggerRef: ref,
172
+ triggerRef: isNested ? mergeRefs([ref, itemRef]) : ref,
169
173
  isSelected: isLocalOpen,
170
174
  onClick: handleTriggerClicked,
171
175
  testId: testId && `${testId}--trigger`
172
176
  });
173
177
  }
174
178
  return /*#__PURE__*/React.createElement(Button, _extends({}, bindFocus, {
175
- ref: triggerProps.ref,
179
+ ref: isNested ? mergeRefs([triggerProps.ref, itemRef]) : triggerProps.ref,
176
180
  "aria-controls": triggerProps['aria-controls'],
177
181
  "aria-expanded": triggerProps['aria-expanded'],
178
182
  "aria-haspopup": triggerProps['aria-haspopup'],
@@ -188,18 +192,21 @@ const DropdownMenu = props => {
188
192
  content: ({
189
193
  setInitialFocusRef,
190
194
  update
191
- }) => /*#__PURE__*/React.createElement(FocusManager, null, /*#__PURE__*/React.createElement(MenuWrapper, {
192
- spacing: spacing,
193
- maxHeight: MAX_HEIGHT,
194
- maxWidth: 800,
195
- onClose: handleOnClose,
196
- onUpdate: update,
197
- isLoading: isLoading,
198
- statusLabel: statusLabel,
199
- setInitialFocusRef: isTriggeredUsingKeyboard || autoFocus ? setInitialFocusRef : undefined
200
- }, isNested ? children : /*#__PURE__*/React.createElement(NestedContext.Provider, {
201
- value: true
202
- }, children)))
195
+ }) => {
196
+ const content = /*#__PURE__*/React.createElement(FocusManager, null, /*#__PURE__*/React.createElement(MenuWrapper, {
197
+ spacing: spacing,
198
+ maxHeight: MAX_HEIGHT,
199
+ maxWidth: 800,
200
+ onClose: handleOnClose,
201
+ onUpdate: update,
202
+ isLoading: isLoading,
203
+ statusLabel: statusLabel,
204
+ setInitialFocusRef: isTriggeredUsingKeyboard || autoFocus ? setInitialFocusRef : undefined
205
+ }, /*#__PURE__*/React.createElement(NestedLevelContext.Provider, {
206
+ value: nestedLevel + 1
207
+ }, children)));
208
+ return isNested ? content : /*#__PURE__*/React.createElement(TrackLevelProvider, null, content);
209
+ }
203
210
  }));
204
211
  };
205
212
  export default DropdownMenu;
@@ -0,0 +1,45 @@
1
+ import React, { createContext, useMemo, useRef } from 'react';
2
+
3
+ /**
4
+ *
5
+ * @internal
6
+ * Context which maintains the current level of dropdown menu if it is nested
7
+ * Default is 0
8
+ *
9
+ */
10
+ export const NestedLevelContext = /*#__PURE__*/createContext(0);
11
+
12
+ /**
13
+ *
14
+ * @internal
15
+ * Context which maintains the maximun level of dropdown menu if it is nested
16
+ * Default is 0
17
+ *
18
+ */
19
+ export const TrackMaxLevelContext = /*#__PURE__*/createContext({
20
+ maxLevelRef: {
21
+ current: 0
22
+ },
23
+ setMaxLevel: () => {}
24
+ });
25
+
26
+ /**
27
+ *
28
+ * @internal
29
+ * Context provider which maintains the maximun level of dropdown menu if it is nested
30
+ *
31
+ */
32
+ export const TrackLevelProvider = ({
33
+ children
34
+ }) => {
35
+ const maxLevelRef = useRef(0);
36
+ const value = useMemo(() => ({
37
+ maxLevelRef,
38
+ setMaxLevel: (level, isMin = false) => {
39
+ maxLevelRef.current = isMin ? Math.min(maxLevelRef.current, level) : level;
40
+ }
41
+ }), [maxLevelRef]);
42
+ return /*#__PURE__*/React.createElement(TrackMaxLevelContext.Provider, {
43
+ value: value
44
+ }, children);
45
+ };
@@ -1,7 +1,8 @@
1
- import React, { createContext, useCallback, useEffect, useRef } from 'react';
1
+ import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
3
  import __noop from '@atlaskit/ds-lib/noop';
4
4
  import handleFocus from '../utils/handle-focus';
5
+ import { NestedLevelContext, TrackMaxLevelContext } from './context';
5
6
 
6
7
  /**
7
8
  *
@@ -28,13 +29,25 @@ const FocusManager = ({
28
29
  menuItemRefs.current.push(ref);
29
30
  }
30
31
  }, []);
31
-
32
+ const nestedLevel = useContext(NestedLevelContext);
33
+ const {
34
+ maxLevelRef,
35
+ setMaxLevel
36
+ } = useContext(TrackMaxLevelContext);
32
37
  // Intentionally rebinding on each render
33
38
  useEffect(() => {
34
- return bind(window, {
39
+ const prevLevel = nestedLevel - 1;
40
+ setMaxLevel(nestedLevel);
41
+ const unbind = bind(window, {
35
42
  type: 'keydown',
36
- listener: handleFocus(menuItemRefs.current)
43
+ listener: handleFocus(menuItemRefs.current, nestedLevel, maxLevelRef)
37
44
  });
45
+ return () => {
46
+ // Always get the minimun level when multiple levels of menu are closed
47
+ // If the stored level is smaller, we won't update it
48
+ setMaxLevel(prevLevel, true);
49
+ unbind();
50
+ };
38
51
  });
39
52
  const contextValue = {
40
53
  menuItemRefs: menuItemRefs.current,
@@ -14,7 +14,7 @@ const actionMap = {
14
14
  */
15
15
  const getNextFocusableElement = (refs, currentFocusedIdx) => {
16
16
  while (currentFocusedIdx + 1 < refs.length) {
17
- const isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
17
+ const isDisabled = refs[currentFocusedIdx + 1].hasAttribute('disabled');
18
18
  if (!isDisabled) {
19
19
  return refs[currentFocusedIdx + 1];
20
20
  }
@@ -30,19 +30,24 @@ const getNextFocusableElement = (refs, currentFocusedIdx) => {
30
30
  */
31
31
  const getPrevFocusableElement = (refs, currentFocusedIdx) => {
32
32
  while (currentFocusedIdx > 0) {
33
- const isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
33
+ const isDisabled = refs[currentFocusedIdx - 1].hasAttribute('disabled');
34
34
  if (!isDisabled) {
35
35
  return refs[currentFocusedIdx - 1];
36
36
  }
37
37
  currentFocusedIdx--;
38
38
  }
39
39
  };
40
- export default function handleFocus(refs) {
40
+ export default function handleFocus(refs, nestedLevel, maxLevelRef) {
41
41
  return e => {
42
42
  const currentFocusedIdx = refs.findIndex(el => {
43
43
  var _document$activeEleme;
44
44
  return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
45
45
  });
46
+ if (nestedLevel < maxLevelRef.current) {
47
+ // if it is a nested dropdown and the level of the given dropdown is not the current level,
48
+ // we don't need to have focus on it
49
+ return;
50
+ }
46
51
  const action = actionMap[e.key];
47
52
  switch (action) {
48
53
  case 'next':
@@ -50,7 +55,7 @@ export default function handleFocus(refs) {
50
55
  e.preventDefault();
51
56
  if (currentFocusedIdx < refs.length - 1) {
52
57
  const nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
53
- nextFocusableElement && nextFocusableElement.focus();
58
+ nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
54
59
  }
55
60
  break;
56
61
  case 'prev':
@@ -58,20 +63,20 @@ export default function handleFocus(refs) {
58
63
  e.preventDefault();
59
64
  if (currentFocusedIdx > 0) {
60
65
  const prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
61
- prevFocusableElement && prevFocusableElement.focus();
66
+ prevFocusableElement === null || prevFocusableElement === void 0 ? void 0 : prevFocusableElement.focus();
62
67
  }
63
68
  break;
64
69
  case 'first':
65
70
  e.preventDefault();
66
71
  // Search for first non-disabled element if first element is disabled
67
72
  const nextFocusableElement = getNextFocusableElement(refs, -1);
68
- nextFocusableElement && nextFocusableElement.focus();
73
+ nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
69
74
  break;
70
75
  case 'last':
71
76
  e.preventDefault();
72
77
  // Search for last non-disabled element if last element is disabled
73
78
  const prevFocusableElement = getPrevFocusableElement(refs, refs.length);
74
- prevFocusableElement && prevFocusableElement.focus();
79
+ prevFocusableElement === null || prevFocusableElement === void 0 ? void 0 : prevFocusableElement.focus();
75
80
  break;
76
81
  default:
77
82
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/dropdown-menu",
3
- "version": "11.11.4",
3
+ "version": "11.11.5",
4
4
  "sideEffects": false
5
5
  }
@@ -5,10 +5,11 @@ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
5
5
  var _excluded = ["ref"];
6
6
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
7
7
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
8
- import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
9
9
  import { bind } from 'bind-event-listener';
10
10
  import Button from '@atlaskit/button/standard-button';
11
11
  import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
12
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
12
13
  import noop from '@atlaskit/ds-lib/noop';
13
14
  import useControlledState from '@atlaskit/ds-lib/use-controlled';
14
15
  import useFocus from '@atlaskit/ds-lib/use-focus-event';
@@ -17,9 +18,11 @@ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
17
18
  import Popup from '@atlaskit/popup';
18
19
  // eslint-disable-next-line @atlaskit/design-system/no-deprecated-imports
19
20
  import { gridSize as gridSizeFn, layers } from '@atlaskit/theme/constants';
21
+ import { NestedLevelContext, TrackLevelProvider } from './internal/components/context';
20
22
  import FocusManager from './internal/components/focus-manager';
21
23
  import MenuWrapper from './internal/components/menu-wrapper';
22
24
  import SelectionStore from './internal/context/selection-store';
25
+ import useRegisterItemWithFocusManager from './internal/hooks/use-register-item-with-focus-manager';
23
26
  import useGeneratedId, { PREFIX } from './internal/utils/use-generated-id';
24
27
  var gridSize = gridSizeFn();
25
28
  var MAX_HEIGHT = "calc(100vh - ".concat(gridSize * 2, "px)");
@@ -50,7 +53,6 @@ var getFallbackPlacements = function getFallbackPlacements(placement) {
50
53
  return ["".concat(mainAxis, "-start"), "".concat(mainAxis, "-end"), "".concat(opposites[mainAxis]), "".concat(opposites[mainAxis], "-start"), "".concat(opposites[mainAxis], "-end"), "auto"];
51
54
  }
52
55
  };
53
- var NestedContext = /*#__PURE__*/createContext(false);
54
56
 
55
57
  /**
56
58
  * __Dropdown menu__
@@ -88,7 +90,7 @@ var DropdownMenu = function DropdownMenu(props) {
88
90
  _useControlledState2 = _slicedToArray(_useControlledState, 2),
89
91
  isLocalOpen = _useControlledState2[0],
90
92
  setLocalIsOpen = _useControlledState2[1];
91
- var isNested = useContext(NestedContext);
93
+ var nestedLevel = useContext(NestedLevelContext);
92
94
  var _useState = useState(false),
93
95
  _useState2 = _slicedToArray(_useState, 2),
94
96
  isTriggeredUsingKeyboard = _useState2[0],
@@ -162,6 +164,8 @@ var DropdownMenu = function DropdownMenu(props) {
162
164
  });
163
165
  }, [isFocused, isLocalOpen, handleTriggerClicked]);
164
166
  var id = useGeneratedId();
167
+ var isNested = nestedLevel > 0;
168
+ var itemRef = useRegisterItemWithFocusManager();
165
169
  return /*#__PURE__*/React.createElement(SelectionStore, null, /*#__PURE__*/React.createElement(Popup, {
166
170
  id: isLocalOpen ? id : undefined,
167
171
  shouldFlip: shouldFlip,
@@ -179,14 +183,14 @@ var DropdownMenu = function DropdownMenu(props) {
179
183
  var ref = triggerProps.ref,
180
184
  providedProps = _objectWithoutProperties(triggerProps, _excluded);
181
185
  return _trigger(_objectSpread(_objectSpread(_objectSpread({}, providedProps), bindFocus), {}, {
182
- triggerRef: ref,
186
+ triggerRef: isNested ? mergeRefs([ref, itemRef]) : ref,
183
187
  isSelected: isLocalOpen,
184
188
  onClick: handleTriggerClicked,
185
189
  testId: testId && "".concat(testId, "--trigger")
186
190
  }));
187
191
  }
188
192
  return /*#__PURE__*/React.createElement(Button, _extends({}, bindFocus, {
189
- ref: triggerProps.ref,
193
+ ref: isNested ? mergeRefs([triggerProps.ref, itemRef]) : triggerProps.ref,
190
194
  "aria-controls": triggerProps['aria-controls'],
191
195
  "aria-expanded": triggerProps['aria-expanded'],
192
196
  "aria-haspopup": triggerProps['aria-haspopup'],
@@ -202,7 +206,7 @@ var DropdownMenu = function DropdownMenu(props) {
202
206
  content: function content(_ref) {
203
207
  var setInitialFocusRef = _ref.setInitialFocusRef,
204
208
  update = _ref.update;
205
- return /*#__PURE__*/React.createElement(FocusManager, null, /*#__PURE__*/React.createElement(MenuWrapper, {
209
+ var content = /*#__PURE__*/React.createElement(FocusManager, null, /*#__PURE__*/React.createElement(MenuWrapper, {
206
210
  spacing: spacing,
207
211
  maxHeight: MAX_HEIGHT,
208
212
  maxWidth: 800,
@@ -211,9 +215,10 @@ var DropdownMenu = function DropdownMenu(props) {
211
215
  isLoading: isLoading,
212
216
  statusLabel: statusLabel,
213
217
  setInitialFocusRef: isTriggeredUsingKeyboard || autoFocus ? setInitialFocusRef : undefined
214
- }, isNested ? children : /*#__PURE__*/React.createElement(NestedContext.Provider, {
215
- value: true
218
+ }, /*#__PURE__*/React.createElement(NestedLevelContext.Provider, {
219
+ value: nestedLevel + 1
216
220
  }, children)));
221
+ return isNested ? content : /*#__PURE__*/React.createElement(TrackLevelProvider, null, content);
217
222
  }
218
223
  }));
219
224
  };
@@ -0,0 +1,47 @@
1
+ import React, { createContext, useMemo, useRef } from 'react';
2
+
3
+ /**
4
+ *
5
+ * @internal
6
+ * Context which maintains the current level of dropdown menu if it is nested
7
+ * Default is 0
8
+ *
9
+ */
10
+ export var NestedLevelContext = /*#__PURE__*/createContext(0);
11
+
12
+ /**
13
+ *
14
+ * @internal
15
+ * Context which maintains the maximun level of dropdown menu if it is nested
16
+ * Default is 0
17
+ *
18
+ */
19
+ export var TrackMaxLevelContext = /*#__PURE__*/createContext({
20
+ maxLevelRef: {
21
+ current: 0
22
+ },
23
+ setMaxLevel: function setMaxLevel() {}
24
+ });
25
+
26
+ /**
27
+ *
28
+ * @internal
29
+ * Context provider which maintains the maximun level of dropdown menu if it is nested
30
+ *
31
+ */
32
+ export var TrackLevelProvider = function TrackLevelProvider(_ref) {
33
+ var children = _ref.children;
34
+ var maxLevelRef = useRef(0);
35
+ var value = useMemo(function () {
36
+ return {
37
+ maxLevelRef: maxLevelRef,
38
+ setMaxLevel: function setMaxLevel(level) {
39
+ var isMin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
40
+ maxLevelRef.current = isMin ? Math.min(maxLevelRef.current, level) : level;
41
+ }
42
+ };
43
+ }, [maxLevelRef]);
44
+ return /*#__PURE__*/React.createElement(TrackMaxLevelContext.Provider, {
45
+ value: value
46
+ }, children);
47
+ };
@@ -1,7 +1,8 @@
1
- import React, { createContext, useCallback, useEffect, useRef } from 'react';
1
+ import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
3
  import __noop from '@atlaskit/ds-lib/noop';
4
4
  import handleFocus from '../utils/handle-focus';
5
+ import { NestedLevelContext, TrackMaxLevelContext } from './context';
5
6
 
6
7
  /**
7
8
  *
@@ -27,13 +28,24 @@ var FocusManager = function FocusManager(_ref) {
27
28
  menuItemRefs.current.push(ref);
28
29
  }
29
30
  }, []);
30
-
31
+ var nestedLevel = useContext(NestedLevelContext);
32
+ var _useContext = useContext(TrackMaxLevelContext),
33
+ maxLevelRef = _useContext.maxLevelRef,
34
+ setMaxLevel = _useContext.setMaxLevel;
31
35
  // Intentionally rebinding on each render
32
36
  useEffect(function () {
33
- return bind(window, {
37
+ var prevLevel = nestedLevel - 1;
38
+ setMaxLevel(nestedLevel);
39
+ var unbind = bind(window, {
34
40
  type: 'keydown',
35
- listener: handleFocus(menuItemRefs.current)
41
+ listener: handleFocus(menuItemRefs.current, nestedLevel, maxLevelRef)
36
42
  });
43
+ return function () {
44
+ // Always get the minimun level when multiple levels of menu are closed
45
+ // If the stored level is smaller, we won't update it
46
+ setMaxLevel(prevLevel, true);
47
+ unbind();
48
+ };
37
49
  });
38
50
  var contextValue = {
39
51
  menuItemRefs: menuItemRefs.current,
@@ -11,7 +11,7 @@ var actionMap = (_actionMap = {}, _defineProperty(_actionMap, KEY_DOWN, 'next'),
11
11
  */
12
12
  var getNextFocusableElement = function getNextFocusableElement(refs, currentFocusedIdx) {
13
13
  while (currentFocusedIdx + 1 < refs.length) {
14
- var isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
14
+ var isDisabled = refs[currentFocusedIdx + 1].hasAttribute('disabled');
15
15
  if (!isDisabled) {
16
16
  return refs[currentFocusedIdx + 1];
17
17
  }
@@ -27,19 +27,24 @@ var getNextFocusableElement = function getNextFocusableElement(refs, currentFocu
27
27
  */
28
28
  var getPrevFocusableElement = function getPrevFocusableElement(refs, currentFocusedIdx) {
29
29
  while (currentFocusedIdx > 0) {
30
- var isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
30
+ var isDisabled = refs[currentFocusedIdx - 1].hasAttribute('disabled');
31
31
  if (!isDisabled) {
32
32
  return refs[currentFocusedIdx - 1];
33
33
  }
34
34
  currentFocusedIdx--;
35
35
  }
36
36
  };
37
- export default function handleFocus(refs) {
37
+ export default function handleFocus(refs, nestedLevel, maxLevelRef) {
38
38
  return function (e) {
39
39
  var currentFocusedIdx = refs.findIndex(function (el) {
40
40
  var _document$activeEleme;
41
41
  return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
42
42
  });
43
+ if (nestedLevel < maxLevelRef.current) {
44
+ // if it is a nested dropdown and the level of the given dropdown is not the current level,
45
+ // we don't need to have focus on it
46
+ return;
47
+ }
43
48
  var action = actionMap[e.key];
44
49
  switch (action) {
45
50
  case 'next':
@@ -47,7 +52,7 @@ export default function handleFocus(refs) {
47
52
  e.preventDefault();
48
53
  if (currentFocusedIdx < refs.length - 1) {
49
54
  var _nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
50
- _nextFocusableElement && _nextFocusableElement.focus();
55
+ _nextFocusableElement === null || _nextFocusableElement === void 0 ? void 0 : _nextFocusableElement.focus();
51
56
  }
52
57
  break;
53
58
  case 'prev':
@@ -55,20 +60,20 @@ export default function handleFocus(refs) {
55
60
  e.preventDefault();
56
61
  if (currentFocusedIdx > 0) {
57
62
  var _prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
58
- _prevFocusableElement && _prevFocusableElement.focus();
63
+ _prevFocusableElement === null || _prevFocusableElement === void 0 ? void 0 : _prevFocusableElement.focus();
59
64
  }
60
65
  break;
61
66
  case 'first':
62
67
  e.preventDefault();
63
68
  // Search for first non-disabled element if first element is disabled
64
69
  var nextFocusableElement = getNextFocusableElement(refs, -1);
65
- nextFocusableElement && nextFocusableElement.focus();
70
+ nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
66
71
  break;
67
72
  case 'last':
68
73
  e.preventDefault();
69
74
  // Search for last non-disabled element if last element is disabled
70
75
  var prevFocusableElement = getPrevFocusableElement(refs, refs.length);
71
- prevFocusableElement && prevFocusableElement.focus();
76
+ prevFocusableElement === null || prevFocusableElement === void 0 ? void 0 : prevFocusableElement.focus();
72
77
  break;
73
78
  default:
74
79
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/dropdown-menu",
3
- "version": "11.11.4",
3
+ "version": "11.11.5",
4
4
  "sideEffects": false
5
5
  }
@@ -0,0 +1,27 @@
1
+ import React, { FC, MutableRefObject } from 'react';
2
+ /**
3
+ *
4
+ * @internal
5
+ * Context which maintains the current level of dropdown menu if it is nested
6
+ * Default is 0
7
+ *
8
+ */
9
+ export declare const NestedLevelContext: React.Context<number>;
10
+ /**
11
+ *
12
+ * @internal
13
+ * Context which maintains the maximun level of dropdown menu if it is nested
14
+ * Default is 0
15
+ *
16
+ */
17
+ export declare const TrackMaxLevelContext: React.Context<{
18
+ maxLevelRef: MutableRefObject<number>;
19
+ setMaxLevel: (level: number, isMin?: boolean) => void;
20
+ }>;
21
+ /**
22
+ *
23
+ * @internal
24
+ * Context provider which maintains the maximun level of dropdown menu if it is nested
25
+ *
26
+ */
27
+ export declare const TrackLevelProvider: FC;
@@ -1,2 +1,3 @@
1
+ import { MutableRefObject } from 'react';
1
2
  import { FocusableElement } from '../../types';
2
- export default function handleFocus(refs: Array<FocusableElement>): (e: KeyboardEvent) => void;
3
+ export default function handleFocus(refs: Array<FocusableElement>, nestedLevel: Number, maxLevelRef: MutableRefObject<number>): (e: KeyboardEvent) => void;
@@ -0,0 +1,27 @@
1
+ import React, { FC, MutableRefObject } from 'react';
2
+ /**
3
+ *
4
+ * @internal
5
+ * Context which maintains the current level of dropdown menu if it is nested
6
+ * Default is 0
7
+ *
8
+ */
9
+ export declare const NestedLevelContext: React.Context<number>;
10
+ /**
11
+ *
12
+ * @internal
13
+ * Context which maintains the maximun level of dropdown menu if it is nested
14
+ * Default is 0
15
+ *
16
+ */
17
+ export declare const TrackMaxLevelContext: React.Context<{
18
+ maxLevelRef: MutableRefObject<number>;
19
+ setMaxLevel: (level: number, isMin?: boolean) => void;
20
+ }>;
21
+ /**
22
+ *
23
+ * @internal
24
+ * Context provider which maintains the maximun level of dropdown menu if it is nested
25
+ *
26
+ */
27
+ export declare const TrackLevelProvider: FC;
@@ -1,2 +1,3 @@
1
+ import { MutableRefObject } from 'react';
1
2
  import { FocusableElement } from '../../types';
2
- export default function handleFocus(refs: Array<FocusableElement>): (e: KeyboardEvent) => void;
3
+ export default function handleFocus(refs: Array<FocusableElement>, nestedLevel: Number, maxLevelRef: MutableRefObject<number>): (e: KeyboardEvent) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/dropdown-menu",
3
- "version": "11.11.4",
3
+ "version": "11.11.5",
4
4
  "description": "A dropdown menu displays a list of actions or options to a user.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"