@atlaskit/popup 1.22.2 → 1.23.1

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/popper-wrapper.js +14 -4
  3. package/dist/cjs/popup.js +5 -1
  4. package/dist/cjs/use-close-manager.js +104 -15
  5. package/dist/cjs/use-focus-manager.js +27 -11
  6. package/dist/cjs/utils/is-element-interactive.js +16 -0
  7. package/dist/cjs/utils/use-animation-frame.js +32 -0
  8. package/dist/es2019/popper-wrapper.js +13 -4
  9. package/dist/es2019/popup.js +4 -1
  10. package/dist/es2019/use-close-manager.js +109 -17
  11. package/dist/es2019/use-focus-manager.js +28 -11
  12. package/dist/es2019/utils/is-element-interactive.js +10 -0
  13. package/dist/es2019/utils/use-animation-frame.js +22 -0
  14. package/dist/esm/popper-wrapper.js +14 -4
  15. package/dist/esm/popup.js +5 -1
  16. package/dist/esm/use-close-manager.js +104 -15
  17. package/dist/esm/use-focus-manager.js +27 -11
  18. package/dist/esm/utils/is-element-interactive.js +10 -0
  19. package/dist/esm/utils/use-animation-frame.js +26 -0
  20. package/dist/types/popper-wrapper.d.ts +1 -1
  21. package/dist/types/types.d.ts +14 -2
  22. package/dist/types/use-close-manager.d.ts +1 -1
  23. package/dist/types/use-focus-manager.d.ts +1 -1
  24. package/dist/types/utils/is-element-interactive.d.ts +1 -0
  25. package/dist/types/utils/use-animation-frame.d.ts +5 -0
  26. package/dist/types-ts4.5/popper-wrapper.d.ts +1 -1
  27. package/dist/types-ts4.5/types.d.ts +14 -2
  28. package/dist/types-ts4.5/use-close-manager.d.ts +1 -1
  29. package/dist/types-ts4.5/use-focus-manager.d.ts +1 -1
  30. package/dist/types-ts4.5/utils/is-element-interactive.d.ts +1 -0
  31. package/dist/types-ts4.5/utils/use-animation-frame.d.ts +5 -0
  32. package/package.json +9 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/popup
2
2
 
3
+ ## 1.23.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#133686](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/133686)
8
+ [`462353527b0db`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/462353527b0db) -
9
+ Expose shouldReturnFocus on Popup component to allow consumers to prevent trigger refocusing on
10
+ popup close
11
+
12
+ ## 1.23.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#128022](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/128022)
17
+ [`1495b8f9c9253`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/1495b8f9c9253) -
18
+ [ux] We are testing new focus behavior in non-dialog popup instances behind a feature flag. With
19
+ that in place, all popup instances that don't have role="dialog" applied will have focus traps
20
+ disabled by default. If this fix is successful, it will be available in a later release.
21
+
3
22
  ## 1.22.2
4
23
 
5
24
  ### Patch Changes
@@ -91,6 +91,8 @@ function PopperWrapper(_ref) {
91
91
  shouldRenderToParent = _ref.shouldRenderToParent,
92
92
  shouldFitContainer = _ref.shouldFitContainer,
93
93
  shouldDisableFocusLock = _ref.shouldDisableFocusLock,
94
+ _ref$shouldReturnFocu = _ref.shouldReturnFocus,
95
+ shouldReturnFocus = _ref$shouldReturnFocu === void 0 ? true : _ref$shouldReturnFocu,
94
96
  strategy = _ref.strategy,
95
97
  role = _ref.role,
96
98
  label = _ref.label,
@@ -105,13 +107,18 @@ function PopperWrapper(_ref) {
105
107
  initialFocusRef = _useState4[0],
106
108
  setInitialFocusRef = _useState4[1];
107
109
 
108
- // We have cases when we need to prohibit focus locking
109
- // e.g. in DropdownMenu
110
+ // We have cases where we need to close the Popup on Tab press.
111
+ // Example: DropdownMenu
110
112
  var shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
113
+ var shouldDisableFocusTrap = role !== 'dialog';
111
114
  (0, _useFocusManager.useFocusManager)({
112
115
  initialFocusRef: initialFocusRef,
113
116
  popupRef: popupRef,
114
- shouldCloseOnTab: shouldCloseOnTab
117
+ shouldCloseOnTab: shouldCloseOnTab,
118
+ triggerRef: triggerRef,
119
+ autoFocus: autoFocus,
120
+ shouldDisableFocusTrap: shouldDisableFocusTrap,
121
+ shouldReturnFocus: shouldReturnFocus
115
122
  });
116
123
  (0, _useCloseManager.useCloseManager)({
117
124
  isOpen: isOpen,
@@ -119,7 +126,10 @@ function PopperWrapper(_ref) {
119
126
  popupRef: popupRef,
120
127
  triggerRef: triggerRef,
121
128
  shouldUseCaptureOnOutsideClick: shouldUseCaptureOnOutsideClick,
122
- shouldCloseOnTab: shouldCloseOnTab
129
+ shouldCloseOnTab: shouldCloseOnTab,
130
+ autoFocus: autoFocus,
131
+ shouldDisableFocusTrap: shouldDisableFocusTrap,
132
+ shouldRenderToParent: shouldRenderToParent
123
133
  });
124
134
  var _UNSAFE_useLayering = (0, _layering.UNSAFE_useLayering)(),
125
135
  currentLevel = _UNSAFE_useLayering.currentLevel;
package/dist/cjs/popup.js CHANGED
@@ -10,6 +10,7 @@ var _react = require("react");
10
10
  var _react2 = require("@emotion/react");
11
11
  var _reactUid = require("react-uid");
12
12
  var _layering = require("@atlaskit/layering");
13
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
14
  var _popper = require("@atlaskit/popper");
14
15
  var _portal = _interopRequireDefault(require("@atlaskit/portal"));
15
16
  var _primitives = require("@atlaskit/primitives");
@@ -57,6 +58,8 @@ var Popup = exports.Popup = /*#__PURE__*/(0, _react.memo)(function (_ref) {
57
58
  shouldFitContainer = _ref$shouldFitContain === void 0 ? false : _ref$shouldFitContain,
58
59
  _ref$shouldDisableFoc = _ref.shouldDisableFocusLock,
59
60
  shouldDisableFocusLock = _ref$shouldDisableFoc === void 0 ? false : _ref$shouldDisableFoc,
61
+ _ref$shouldReturnFocu = _ref.shouldReturnFocus,
62
+ shouldReturnFocus = _ref$shouldReturnFocu === void 0 ? true : _ref$shouldReturnFocu,
60
63
  strategy = _ref.strategy,
61
64
  role = _ref.role,
62
65
  label = _ref.label,
@@ -89,6 +92,7 @@ var Popup = exports.Popup = /*#__PURE__*/(0, _react.memo)(function (_ref) {
89
92
  shouldRenderToParent: shouldRenderToParent || shouldFitContainer,
90
93
  shouldFitContainer: shouldFitContainer,
91
94
  shouldDisableFocusLock: shouldDisableFocusLock,
95
+ shouldReturnFocus: shouldReturnFocus,
92
96
  triggerRef: triggerRef,
93
97
  strategy: shouldFitContainer ? 'absolute' : strategy,
94
98
  role: role,
@@ -102,7 +106,7 @@ var Popup = exports.Popup = /*#__PURE__*/(0, _react.memo)(function (_ref) {
102
106
  ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
103
107
  'aria-controls': isOpen ? id : undefined,
104
108
  'aria-expanded': isOpen,
105
- 'aria-haspopup': true
109
+ 'aria-haspopup': role === 'dialog' && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock') ? 'dialog' : true
106
110
  });
107
111
  }), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : (0, _react2.jsx)(_portal.default, {
108
112
  zIndex: zIndex
@@ -9,6 +9,9 @@ var _react = require("react");
9
9
  var _bindEventListener = require("bind-event-listener");
10
10
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
11
11
  var _layering = require("@atlaskit/layering");
12
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
+ var _isElementInteractive = require("./utils/is-element-interactive");
14
+ var _useAnimationFrame2 = require("./utils/use-animation-frame");
12
15
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
13
16
 
14
17
  var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
@@ -16,11 +19,17 @@ var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
16
19
  onClose = _ref.onClose,
17
20
  popupRef = _ref.popupRef,
18
21
  triggerRef = _ref.triggerRef,
22
+ autoFocus = _ref.autoFocus,
23
+ shouldDisableFocusTrap = _ref.shouldDisableFocusTrap,
19
24
  capture = _ref.shouldUseCaptureOnOutsideClick,
20
- shouldCloseOnTab = _ref.shouldCloseOnTab;
25
+ shouldCloseOnTab = _ref.shouldCloseOnTab,
26
+ shouldRenderToParent = _ref.shouldRenderToParent;
21
27
  var _UNSAFE_useLayering = (0, _layering.UNSAFE_useLayering)(),
22
28
  isLayerDisabled = _UNSAFE_useLayering.isLayerDisabled,
23
29
  currentLevel = _UNSAFE_useLayering.currentLevel;
30
+ var _useAnimationFrame = (0, _useAnimationFrame2.useAnimationFrame)(),
31
+ requestFrame = _useAnimationFrame.requestFrame,
32
+ cancelAllFrames = _useAnimationFrame.cancelAllFrames;
24
33
  (0, _react.useEffect)(function () {
25
34
  if (!isOpen || !popupRef) {
26
35
  return _noop.default;
@@ -29,6 +38,13 @@ var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
29
38
  if (onClose) {
30
39
  onClose(event);
31
40
  }
41
+ if (shouldDisableFocusTrap && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
42
+ // Restoring the normal focus order for trigger.
43
+ triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '0');
44
+ if (popupRef && autoFocus) {
45
+ popupRef.setAttribute('tabindex', '0');
46
+ }
47
+ }
32
48
  };
33
49
 
34
50
  // This check is required for cases where components like
@@ -44,23 +60,98 @@ var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
44
60
  if (!doesDomNodeExist) {
45
61
  return;
46
62
  }
47
- if (isLayerDisabled()) {
48
- //if it is a disabled layer, we need to disable its click listener.
49
- return;
63
+ if ((0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
64
+ var _document$activeEleme;
65
+ if (isLayerDisabled() && (_document$activeEleme = document.activeElement) !== null && _document$activeEleme !== void 0 && _document$activeEleme.closest('[aria-modal]')) {
66
+ //if it is a disabled layer, we need to disable its click listener.
67
+ return;
68
+ }
69
+ } else {
70
+ if (isLayerDisabled()) {
71
+ //if it is a disabled layer, we need to disable its click listener.
72
+ return;
73
+ }
50
74
  }
51
75
  var isClickOnPopup = popupRef && popupRef.contains(target);
52
76
  var isClickOnTrigger = triggerRef && triggerRef.contains(target);
53
77
  if (!isClickOnPopup && !isClickOnTrigger) {
54
78
  closePopup(event);
79
+ // If there was an outside click on a non-interactive element, the focus should be on the trigger.
80
+ if (document.activeElement && !(0, _isElementInteractive.isInteractiveElement)(document.activeElement) && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
81
+ triggerRef === null || triggerRef === void 0 || triggerRef.focus();
82
+ }
55
83
  }
56
84
  };
57
85
  var onKeyDown = function onKeyDown(event) {
58
- if (isLayerDisabled()) {
59
- return;
60
- }
61
- var key = event.key;
62
- if (key === 'Escape' || key === 'Esc' || shouldCloseOnTab && key === 'Tab') {
63
- closePopup(event);
86
+ if ((0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
87
+ var key = event.key,
88
+ shiftKey = event.shiftKey;
89
+ if (shiftKey && key === 'Tab' && !shouldRenderToParent) {
90
+ if (isLayerDisabled()) {
91
+ return;
92
+ }
93
+ // We need to move the focus to the popup trigger when the popup is displayed in React.Portal.
94
+ requestFrame(function () {
95
+ var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
96
+ if (isPopupFocusOut) {
97
+ closePopup(event);
98
+ if (currentLevel === 1) {
99
+ triggerRef === null || triggerRef === void 0 || triggerRef.focus();
100
+ }
101
+ }
102
+ });
103
+ return;
104
+ }
105
+ if (key === 'Tab') {
106
+ var _document$activeEleme2;
107
+ // We have cases where we need to close the Popup on Tab press.
108
+ // Example: DropdownMenu
109
+ if (shouldCloseOnTab) {
110
+ if (isLayerDisabled()) {
111
+ return;
112
+ }
113
+ closePopup(event);
114
+ return;
115
+ }
116
+ if (isLayerDisabled() && (_document$activeEleme2 = document.activeElement) !== null && _document$activeEleme2 !== void 0 && _document$activeEleme2.closest('[aria-modal]')) {
117
+ return;
118
+ }
119
+ if (shouldDisableFocusTrap) {
120
+ if (shouldRenderToParent) {
121
+ // We need to move the focus to the previous interactive element before popup trigger
122
+ requestFrame(function () {
123
+ var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
124
+ if (isPopupFocusOut) {
125
+ closePopup(event);
126
+ }
127
+ });
128
+ } else {
129
+ requestFrame(function () {
130
+ if (!document.hasFocus()) {
131
+ closePopup(event);
132
+ }
133
+ });
134
+ }
135
+ return;
136
+ }
137
+ }
138
+ if (isLayerDisabled()) {
139
+ return;
140
+ }
141
+ if (key === 'Escape' || key === 'Esc') {
142
+ if (triggerRef && autoFocus) {
143
+ triggerRef.focus();
144
+ }
145
+ closePopup(event);
146
+ }
147
+ } else {
148
+ if (isLayerDisabled()) {
149
+ return;
150
+ }
151
+ var _key = event.key;
152
+ if (_key === 'Escape' || _key === 'Esc' || shouldCloseOnTab && _key === 'Tab') {
153
+ closePopup(event);
154
+ }
64
155
  }
65
156
  };
66
157
  var unbind = (0, _bindEventListener.bindAll)(window, [{
@@ -82,15 +173,13 @@ var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
82
173
  if (isLayerDisabled() || !(document.activeElement instanceof HTMLIFrameElement)) {
83
174
  return;
84
175
  }
85
- var wrapper = document.activeElement.closest('[data-ds--level]');
86
- if (!wrapper || currentLevel > Number(wrapper.getAttribute('data-ds--level'))) {
87
- closePopup(e);
88
- }
176
+ closePopup(e);
89
177
  }
90
178
  });
91
179
  return function () {
180
+ cancelAllFrames();
92
181
  unbind();
93
182
  unbindBlur();
94
183
  };
95
- }, [isOpen, onClose, popupRef, triggerRef, capture, isLayerDisabled, shouldCloseOnTab, currentLevel]);
184
+ }, [isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, capture, isLayerDisabled, shouldCloseOnTab, currentLevel, shouldRenderToParent, requestFrame, cancelAllFrames]);
96
185
  };
@@ -8,35 +8,51 @@ exports.useFocusManager = void 0;
8
8
  var _react = require("react");
9
9
  var _focusTrap = _interopRequireDefault(require("focus-trap"));
10
10
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
11
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
12
+ var _useAnimationFrame2 = require("./utils/use-animation-frame");
11
13
  var useFocusManager = exports.useFocusManager = function useFocusManager(_ref) {
12
14
  var initialFocusRef = _ref.initialFocusRef,
13
15
  popupRef = _ref.popupRef,
14
- shouldCloseOnTab = _ref.shouldCloseOnTab;
16
+ triggerRef = _ref.triggerRef,
17
+ autoFocus = _ref.autoFocus,
18
+ shouldCloseOnTab = _ref.shouldCloseOnTab,
19
+ shouldDisableFocusTrap = _ref.shouldDisableFocusTrap,
20
+ shouldReturnFocus = _ref.shouldReturnFocus;
21
+ var _useAnimationFrame = (0, _useAnimationFrame2.useAnimationFrame)(),
22
+ requestFrame = _useAnimationFrame.requestFrame,
23
+ cancelAllFrames = _useAnimationFrame.cancelAllFrames;
15
24
  (0, _react.useEffect)(function () {
16
25
  if (!popupRef || shouldCloseOnTab) {
17
26
  return _noop.default;
18
27
  }
28
+ if (shouldDisableFocusTrap && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
29
+ // Plucking trigger & popup content container from the tab order so that
30
+ // when we Shift+Tab, the focus moves to the element before trigger
31
+ requestFrame(function () {
32
+ triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '-1');
33
+ if (popupRef && autoFocus) {
34
+ popupRef.setAttribute('tabindex', '-1');
35
+ }
36
+ (initialFocusRef || popupRef).focus();
37
+ });
38
+ return _noop.default;
39
+ }
19
40
  var trapConfig = {
20
41
  clickOutsideDeactivates: true,
21
42
  escapeDeactivates: true,
22
43
  initialFocus: initialFocusRef || popupRef,
23
44
  fallbackFocus: popupRef,
24
- returnFocusOnDeactivate: true
45
+ returnFocusOnDeactivate: shouldReturnFocus
25
46
  };
26
47
  var focusTrap = (0, _focusTrap.default)(popupRef, trapConfig);
27
- var frameId = null;
28
48
 
29
- // wait for the popup to reposition itself before we focus
30
- frameId = requestAnimationFrame(function () {
31
- frameId = null;
49
+ // Wait for the popup to reposition itself before we focus
50
+ requestFrame(function () {
32
51
  focusTrap.activate();
33
52
  });
34
53
  return function () {
35
- if (frameId != null) {
36
- cancelAnimationFrame(frameId);
37
- frameId = null;
38
- }
54
+ cancelAllFrames();
39
55
  focusTrap.deactivate();
40
56
  };
41
- }, [popupRef, initialFocusRef, shouldCloseOnTab]);
57
+ }, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames, shouldReturnFocus]);
42
58
  };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isInteractiveElement = void 0;
7
+ var interactiveTags = ['button', 'a', 'input', 'select', 'textarea'];
8
+ var isInteractiveElement = exports.isInteractiveElement = function isInteractiveElement(element) {
9
+ if (interactiveTags.includes(element.tagName.toLowerCase())) {
10
+ return true;
11
+ }
12
+ if (element.getAttribute('tabindex') !== null || element.hasAttribute('contenteditable')) {
13
+ return true;
14
+ }
15
+ return false;
16
+ };
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useAnimationFrame = void 0;
7
+ var _react = require("react");
8
+ var useAnimationFrame = exports.useAnimationFrame = function useAnimationFrame() {
9
+ var animationsRef = (0, _react.useRef)([]);
10
+ var requestFrame = (0, _react.useCallback)(function (callback) {
11
+ var id = requestAnimationFrame(callback);
12
+ animationsRef.current.push(id);
13
+ return id;
14
+ }, []);
15
+ var cancelFrame = (0, _react.useCallback)(function (id) {
16
+ cancelAnimationFrame(id);
17
+ animationsRef.current = animationsRef.current.filter(function (frameId) {
18
+ return frameId !== id;
19
+ });
20
+ }, []);
21
+ var cancelAllFrames = (0, _react.useCallback)(function () {
22
+ animationsRef.current.forEach(function (id) {
23
+ return cancelAnimationFrame(id);
24
+ });
25
+ animationsRef.current = [];
26
+ }, []);
27
+ return {
28
+ requestFrame: requestFrame,
29
+ cancelFrame: cancelFrame,
30
+ cancelAllFrames: cancelAllFrames
31
+ };
32
+ };
@@ -81,6 +81,7 @@ function PopperWrapper({
81
81
  shouldRenderToParent,
82
82
  shouldFitContainer,
83
83
  shouldDisableFocusLock,
84
+ shouldReturnFocus = true,
84
85
  strategy,
85
86
  role,
86
87
  label,
@@ -90,13 +91,18 @@ function PopperWrapper({
90
91
  const [popupRef, setPopupRef] = useState(null);
91
92
  const [initialFocusRef, setInitialFocusRef] = useState(null);
92
93
 
93
- // We have cases when we need to prohibit focus locking
94
- // e.g. in DropdownMenu
94
+ // We have cases where we need to close the Popup on Tab press.
95
+ // Example: DropdownMenu
95
96
  const shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
97
+ const shouldDisableFocusTrap = role !== 'dialog';
96
98
  useFocusManager({
97
99
  initialFocusRef,
98
100
  popupRef,
99
- shouldCloseOnTab
101
+ shouldCloseOnTab,
102
+ triggerRef,
103
+ autoFocus,
104
+ shouldDisableFocusTrap,
105
+ shouldReturnFocus
100
106
  });
101
107
  useCloseManager({
102
108
  isOpen,
@@ -104,7 +110,10 @@ function PopperWrapper({
104
110
  popupRef,
105
111
  triggerRef,
106
112
  shouldUseCaptureOnOutsideClick,
107
- shouldCloseOnTab
113
+ shouldCloseOnTab,
114
+ autoFocus,
115
+ shouldDisableFocusTrap,
116
+ shouldRenderToParent
108
117
  });
109
118
  const {
110
119
  currentLevel
@@ -9,6 +9,7 @@ import { memo, useState } from 'react';
9
9
  import { jsx } from '@emotion/react';
10
10
  import { useUID } from 'react-uid';
11
11
  import { UNSAFE_LAYERING } from '@atlaskit/layering';
12
+ import { fg } from '@atlaskit/platform-feature-flags';
12
13
  import { Manager, Reference } from '@atlaskit/popper';
13
14
  import Portal from '@atlaskit/portal';
14
15
  import { Box, xcss } from '@atlaskit/primitives';
@@ -39,6 +40,7 @@ export const Popup = /*#__PURE__*/memo(({
39
40
  shouldRenderToParent = false,
40
41
  shouldFitContainer = false,
41
42
  shouldDisableFocusLock = false,
43
+ shouldReturnFocus = true,
42
44
  strategy,
43
45
  role,
44
46
  label,
@@ -69,6 +71,7 @@ export const Popup = /*#__PURE__*/memo(({
69
71
  shouldRenderToParent: shouldRenderToParent || shouldFitContainer,
70
72
  shouldFitContainer: shouldFitContainer,
71
73
  shouldDisableFocusLock: shouldDisableFocusLock,
74
+ shouldReturnFocus: shouldReturnFocus,
72
75
  triggerRef: triggerRef,
73
76
  strategy: shouldFitContainer ? 'absolute' : strategy,
74
77
  role: role,
@@ -83,7 +86,7 @@ export const Popup = /*#__PURE__*/memo(({
83
86
  ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
84
87
  'aria-controls': isOpen ? id : undefined,
85
88
  'aria-expanded': isOpen,
86
- 'aria-haspopup': true
89
+ 'aria-haspopup': role === 'dialog' && fg('platform_dst_popup-disable-focuslock') ? 'dialog' : true
87
90
  });
88
91
  }), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : jsx(Portal, {
89
92
  zIndex: zIndex
@@ -3,18 +3,28 @@ import { useEffect } from 'react';
3
3
  import { bind, bindAll } from 'bind-event-listener';
4
4
  import noop from '@atlaskit/ds-lib/noop';
5
5
  import { UNSAFE_useLayering } from '@atlaskit/layering';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
7
+ import { isInteractiveElement } from './utils/is-element-interactive';
8
+ import { useAnimationFrame } from './utils/use-animation-frame';
6
9
  export const useCloseManager = ({
7
10
  isOpen,
8
11
  onClose,
9
12
  popupRef,
10
13
  triggerRef,
14
+ autoFocus,
15
+ shouldDisableFocusTrap,
11
16
  shouldUseCaptureOnOutsideClick: capture,
12
- shouldCloseOnTab
17
+ shouldCloseOnTab,
18
+ shouldRenderToParent
13
19
  }) => {
14
20
  const {
15
21
  isLayerDisabled,
16
22
  currentLevel
17
23
  } = UNSAFE_useLayering();
24
+ const {
25
+ requestFrame,
26
+ cancelAllFrames
27
+ } = useAnimationFrame();
18
28
  useEffect(() => {
19
29
  if (!isOpen || !popupRef) {
20
30
  return noop;
@@ -23,6 +33,13 @@ export const useCloseManager = ({
23
33
  if (onClose) {
24
34
  onClose(event);
25
35
  }
36
+ if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
37
+ // Restoring the normal focus order for trigger.
38
+ triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.setAttribute('tabindex', '0');
39
+ if (popupRef && autoFocus) {
40
+ popupRef.setAttribute('tabindex', '0');
41
+ }
42
+ }
26
43
  };
27
44
 
28
45
  // This check is required for cases where components like
@@ -40,25 +57,102 @@ export const useCloseManager = ({
40
57
  if (!doesDomNodeExist) {
41
58
  return;
42
59
  }
43
- if (isLayerDisabled()) {
44
- //if it is a disabled layer, we need to disable its click listener.
45
- return;
60
+ if (fg('platform_dst_popup-disable-focuslock')) {
61
+ var _document$activeEleme;
62
+ if (isLayerDisabled() && (_document$activeEleme = document.activeElement) !== null && _document$activeEleme !== void 0 && _document$activeEleme.closest('[aria-modal]')) {
63
+ //if it is a disabled layer, we need to disable its click listener.
64
+ return;
65
+ }
66
+ } else {
67
+ if (isLayerDisabled()) {
68
+ //if it is a disabled layer, we need to disable its click listener.
69
+ return;
70
+ }
46
71
  }
47
72
  const isClickOnPopup = popupRef && popupRef.contains(target);
48
73
  const isClickOnTrigger = triggerRef && triggerRef.contains(target);
49
74
  if (!isClickOnPopup && !isClickOnTrigger) {
50
75
  closePopup(event);
76
+ // If there was an outside click on a non-interactive element, the focus should be on the trigger.
77
+ if (document.activeElement && !isInteractiveElement(document.activeElement) && fg('platform_dst_popup-disable-focuslock')) {
78
+ triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.focus();
79
+ }
51
80
  }
52
81
  };
53
82
  const onKeyDown = event => {
54
- if (isLayerDisabled()) {
55
- return;
56
- }
57
- const {
58
- key
59
- } = event;
60
- if (key === 'Escape' || key === 'Esc' || shouldCloseOnTab && key === 'Tab') {
61
- closePopup(event);
83
+ if (fg('platform_dst_popup-disable-focuslock')) {
84
+ const {
85
+ key,
86
+ shiftKey
87
+ } = event;
88
+ if (shiftKey && key === 'Tab' && !shouldRenderToParent) {
89
+ if (isLayerDisabled()) {
90
+ return;
91
+ }
92
+ // We need to move the focus to the popup trigger when the popup is displayed in React.Portal.
93
+ requestFrame(() => {
94
+ const isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
95
+ if (isPopupFocusOut) {
96
+ closePopup(event);
97
+ if (currentLevel === 1) {
98
+ triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.focus();
99
+ }
100
+ }
101
+ });
102
+ return;
103
+ }
104
+ if (key === 'Tab') {
105
+ var _document$activeEleme2;
106
+ // We have cases where we need to close the Popup on Tab press.
107
+ // Example: DropdownMenu
108
+ if (shouldCloseOnTab) {
109
+ if (isLayerDisabled()) {
110
+ return;
111
+ }
112
+ closePopup(event);
113
+ return;
114
+ }
115
+ if (isLayerDisabled() && (_document$activeEleme2 = document.activeElement) !== null && _document$activeEleme2 !== void 0 && _document$activeEleme2.closest('[aria-modal]')) {
116
+ return;
117
+ }
118
+ if (shouldDisableFocusTrap) {
119
+ if (shouldRenderToParent) {
120
+ // We need to move the focus to the previous interactive element before popup trigger
121
+ requestFrame(() => {
122
+ const isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
123
+ if (isPopupFocusOut) {
124
+ closePopup(event);
125
+ }
126
+ });
127
+ } else {
128
+ requestFrame(() => {
129
+ if (!document.hasFocus()) {
130
+ closePopup(event);
131
+ }
132
+ });
133
+ }
134
+ return;
135
+ }
136
+ }
137
+ if (isLayerDisabled()) {
138
+ return;
139
+ }
140
+ if (key === 'Escape' || key === 'Esc') {
141
+ if (triggerRef && autoFocus) {
142
+ triggerRef.focus();
143
+ }
144
+ closePopup(event);
145
+ }
146
+ } else {
147
+ if (isLayerDisabled()) {
148
+ return;
149
+ }
150
+ const {
151
+ key
152
+ } = event;
153
+ if (key === 'Escape' || key === 'Esc' || shouldCloseOnTab && key === 'Tab') {
154
+ closePopup(event);
155
+ }
62
156
  }
63
157
  };
64
158
  const unbind = bindAll(window, [{
@@ -80,15 +174,13 @@ export const useCloseManager = ({
80
174
  if (isLayerDisabled() || !(document.activeElement instanceof HTMLIFrameElement)) {
81
175
  return;
82
176
  }
83
- const wrapper = document.activeElement.closest('[data-ds--level]');
84
- if (!wrapper || currentLevel > Number(wrapper.getAttribute('data-ds--level'))) {
85
- closePopup(e);
86
- }
177
+ closePopup(e);
87
178
  }
88
179
  });
89
180
  return () => {
181
+ cancelAllFrames();
90
182
  unbind();
91
183
  unbindBlur();
92
184
  };
93
- }, [isOpen, onClose, popupRef, triggerRef, capture, isLayerDisabled, shouldCloseOnTab, currentLevel]);
185
+ }, [isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, capture, isLayerDisabled, shouldCloseOnTab, currentLevel, shouldRenderToParent, requestFrame, cancelAllFrames]);
94
186
  };
@@ -1,36 +1,53 @@
1
1
  import { useEffect } from 'react';
2
2
  import createFocusTrap from 'focus-trap';
3
3
  import noop from '@atlaskit/ds-lib/noop';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
5
+ import { useAnimationFrame } from './utils/use-animation-frame';
4
6
  export const useFocusManager = ({
5
7
  initialFocusRef,
6
8
  popupRef,
7
- shouldCloseOnTab
9
+ triggerRef,
10
+ autoFocus,
11
+ shouldCloseOnTab,
12
+ shouldDisableFocusTrap,
13
+ shouldReturnFocus
8
14
  }) => {
15
+ const {
16
+ requestFrame,
17
+ cancelAllFrames
18
+ } = useAnimationFrame();
9
19
  useEffect(() => {
10
20
  if (!popupRef || shouldCloseOnTab) {
11
21
  return noop;
12
22
  }
23
+ if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
24
+ // Plucking trigger & popup content container from the tab order so that
25
+ // when we Shift+Tab, the focus moves to the element before trigger
26
+ requestFrame(() => {
27
+ triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.setAttribute('tabindex', '-1');
28
+ if (popupRef && autoFocus) {
29
+ popupRef.setAttribute('tabindex', '-1');
30
+ }
31
+ (initialFocusRef || popupRef).focus();
32
+ });
33
+ return noop;
34
+ }
13
35
  const trapConfig = {
14
36
  clickOutsideDeactivates: true,
15
37
  escapeDeactivates: true,
16
38
  initialFocus: initialFocusRef || popupRef,
17
39
  fallbackFocus: popupRef,
18
- returnFocusOnDeactivate: true
40
+ returnFocusOnDeactivate: shouldReturnFocus
19
41
  };
20
42
  const focusTrap = createFocusTrap(popupRef, trapConfig);
21
- let frameId = null;
22
43
 
23
- // wait for the popup to reposition itself before we focus
24
- frameId = requestAnimationFrame(() => {
25
- frameId = null;
44
+ // Wait for the popup to reposition itself before we focus
45
+ requestFrame(() => {
26
46
  focusTrap.activate();
27
47
  });
28
48
  return () => {
29
- if (frameId != null) {
30
- cancelAnimationFrame(frameId);
31
- frameId = null;
32
- }
49
+ cancelAllFrames();
33
50
  focusTrap.deactivate();
34
51
  };
35
- }, [popupRef, initialFocusRef, shouldCloseOnTab]);
52
+ }, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames, shouldReturnFocus]);
36
53
  };
@@ -0,0 +1,10 @@
1
+ const interactiveTags = ['button', 'a', 'input', 'select', 'textarea'];
2
+ export const isInteractiveElement = element => {
3
+ if (interactiveTags.includes(element.tagName.toLowerCase())) {
4
+ return true;
5
+ }
6
+ if (element.getAttribute('tabindex') !== null || element.hasAttribute('contenteditable')) {
7
+ return true;
8
+ }
9
+ return false;
10
+ };
@@ -0,0 +1,22 @@
1
+ import { useCallback, useRef } from 'react';
2
+ export const useAnimationFrame = () => {
3
+ const animationsRef = useRef([]);
4
+ const requestFrame = useCallback(callback => {
5
+ const id = requestAnimationFrame(callback);
6
+ animationsRef.current.push(id);
7
+ return id;
8
+ }, []);
9
+ const cancelFrame = useCallback(id => {
10
+ cancelAnimationFrame(id);
11
+ animationsRef.current = animationsRef.current.filter(frameId => frameId !== id);
12
+ }, []);
13
+ const cancelAllFrames = useCallback(() => {
14
+ animationsRef.current.forEach(id => cancelAnimationFrame(id));
15
+ animationsRef.current = [];
16
+ }, []);
17
+ return {
18
+ requestFrame,
19
+ cancelFrame,
20
+ cancelAllFrames
21
+ };
22
+ };
@@ -85,6 +85,8 @@ function PopperWrapper(_ref) {
85
85
  shouldRenderToParent = _ref.shouldRenderToParent,
86
86
  shouldFitContainer = _ref.shouldFitContainer,
87
87
  shouldDisableFocusLock = _ref.shouldDisableFocusLock,
88
+ _ref$shouldReturnFocu = _ref.shouldReturnFocus,
89
+ shouldReturnFocus = _ref$shouldReturnFocu === void 0 ? true : _ref$shouldReturnFocu,
88
90
  strategy = _ref.strategy,
89
91
  role = _ref.role,
90
92
  label = _ref.label,
@@ -99,13 +101,18 @@ function PopperWrapper(_ref) {
99
101
  initialFocusRef = _useState4[0],
100
102
  setInitialFocusRef = _useState4[1];
101
103
 
102
- // We have cases when we need to prohibit focus locking
103
- // e.g. in DropdownMenu
104
+ // We have cases where we need to close the Popup on Tab press.
105
+ // Example: DropdownMenu
104
106
  var shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
107
+ var shouldDisableFocusTrap = role !== 'dialog';
105
108
  useFocusManager({
106
109
  initialFocusRef: initialFocusRef,
107
110
  popupRef: popupRef,
108
- shouldCloseOnTab: shouldCloseOnTab
111
+ shouldCloseOnTab: shouldCloseOnTab,
112
+ triggerRef: triggerRef,
113
+ autoFocus: autoFocus,
114
+ shouldDisableFocusTrap: shouldDisableFocusTrap,
115
+ shouldReturnFocus: shouldReturnFocus
109
116
  });
110
117
  useCloseManager({
111
118
  isOpen: isOpen,
@@ -113,7 +120,10 @@ function PopperWrapper(_ref) {
113
120
  popupRef: popupRef,
114
121
  triggerRef: triggerRef,
115
122
  shouldUseCaptureOnOutsideClick: shouldUseCaptureOnOutsideClick,
116
- shouldCloseOnTab: shouldCloseOnTab
123
+ shouldCloseOnTab: shouldCloseOnTab,
124
+ autoFocus: autoFocus,
125
+ shouldDisableFocusTrap: shouldDisableFocusTrap,
126
+ shouldRenderToParent: shouldRenderToParent
117
127
  });
118
128
  var _UNSAFE_useLayering = UNSAFE_useLayering(),
119
129
  currentLevel = _UNSAFE_useLayering.currentLevel;
package/dist/esm/popup.js CHANGED
@@ -10,6 +10,7 @@ import { memo, useState } from 'react';
10
10
  import { jsx } from '@emotion/react';
11
11
  import { useUID } from 'react-uid';
12
12
  import { UNSAFE_LAYERING } from '@atlaskit/layering';
13
+ import { fg } from '@atlaskit/platform-feature-flags';
13
14
  import { Manager, Reference } from '@atlaskit/popper';
14
15
  import Portal from '@atlaskit/portal';
15
16
  import { Box, xcss } from '@atlaskit/primitives';
@@ -49,6 +50,8 @@ export var Popup = /*#__PURE__*/memo(function (_ref) {
49
50
  shouldFitContainer = _ref$shouldFitContain === void 0 ? false : _ref$shouldFitContain,
50
51
  _ref$shouldDisableFoc = _ref.shouldDisableFocusLock,
51
52
  shouldDisableFocusLock = _ref$shouldDisableFoc === void 0 ? false : _ref$shouldDisableFoc,
53
+ _ref$shouldReturnFocu = _ref.shouldReturnFocus,
54
+ shouldReturnFocus = _ref$shouldReturnFocu === void 0 ? true : _ref$shouldReturnFocu,
52
55
  strategy = _ref.strategy,
53
56
  role = _ref.role,
54
57
  label = _ref.label,
@@ -81,6 +84,7 @@ export var Popup = /*#__PURE__*/memo(function (_ref) {
81
84
  shouldRenderToParent: shouldRenderToParent || shouldFitContainer,
82
85
  shouldFitContainer: shouldFitContainer,
83
86
  shouldDisableFocusLock: shouldDisableFocusLock,
87
+ shouldReturnFocus: shouldReturnFocus,
84
88
  triggerRef: triggerRef,
85
89
  strategy: shouldFitContainer ? 'absolute' : strategy,
86
90
  role: role,
@@ -94,7 +98,7 @@ export var Popup = /*#__PURE__*/memo(function (_ref) {
94
98
  ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
95
99
  'aria-controls': isOpen ? id : undefined,
96
100
  'aria-expanded': isOpen,
97
- 'aria-haspopup': true
101
+ 'aria-haspopup': role === 'dialog' && fg('platform_dst_popup-disable-focuslock') ? 'dialog' : true
98
102
  });
99
103
  }), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : jsx(Portal, {
100
104
  zIndex: zIndex
@@ -3,16 +3,25 @@ import { useEffect } from 'react';
3
3
  import { bind, bindAll } from 'bind-event-listener';
4
4
  import noop from '@atlaskit/ds-lib/noop';
5
5
  import { UNSAFE_useLayering } from '@atlaskit/layering';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
7
+ import { isInteractiveElement } from './utils/is-element-interactive';
8
+ import { useAnimationFrame } from './utils/use-animation-frame';
6
9
  export var useCloseManager = function useCloseManager(_ref) {
7
10
  var isOpen = _ref.isOpen,
8
11
  onClose = _ref.onClose,
9
12
  popupRef = _ref.popupRef,
10
13
  triggerRef = _ref.triggerRef,
14
+ autoFocus = _ref.autoFocus,
15
+ shouldDisableFocusTrap = _ref.shouldDisableFocusTrap,
11
16
  capture = _ref.shouldUseCaptureOnOutsideClick,
12
- shouldCloseOnTab = _ref.shouldCloseOnTab;
17
+ shouldCloseOnTab = _ref.shouldCloseOnTab,
18
+ shouldRenderToParent = _ref.shouldRenderToParent;
13
19
  var _UNSAFE_useLayering = UNSAFE_useLayering(),
14
20
  isLayerDisabled = _UNSAFE_useLayering.isLayerDisabled,
15
21
  currentLevel = _UNSAFE_useLayering.currentLevel;
22
+ var _useAnimationFrame = useAnimationFrame(),
23
+ requestFrame = _useAnimationFrame.requestFrame,
24
+ cancelAllFrames = _useAnimationFrame.cancelAllFrames;
16
25
  useEffect(function () {
17
26
  if (!isOpen || !popupRef) {
18
27
  return noop;
@@ -21,6 +30,13 @@ export var useCloseManager = function useCloseManager(_ref) {
21
30
  if (onClose) {
22
31
  onClose(event);
23
32
  }
33
+ if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
34
+ // Restoring the normal focus order for trigger.
35
+ triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '0');
36
+ if (popupRef && autoFocus) {
37
+ popupRef.setAttribute('tabindex', '0');
38
+ }
39
+ }
24
40
  };
25
41
 
26
42
  // This check is required for cases where components like
@@ -36,23 +52,98 @@ export var useCloseManager = function useCloseManager(_ref) {
36
52
  if (!doesDomNodeExist) {
37
53
  return;
38
54
  }
39
- if (isLayerDisabled()) {
40
- //if it is a disabled layer, we need to disable its click listener.
41
- return;
55
+ if (fg('platform_dst_popup-disable-focuslock')) {
56
+ var _document$activeEleme;
57
+ if (isLayerDisabled() && (_document$activeEleme = document.activeElement) !== null && _document$activeEleme !== void 0 && _document$activeEleme.closest('[aria-modal]')) {
58
+ //if it is a disabled layer, we need to disable its click listener.
59
+ return;
60
+ }
61
+ } else {
62
+ if (isLayerDisabled()) {
63
+ //if it is a disabled layer, we need to disable its click listener.
64
+ return;
65
+ }
42
66
  }
43
67
  var isClickOnPopup = popupRef && popupRef.contains(target);
44
68
  var isClickOnTrigger = triggerRef && triggerRef.contains(target);
45
69
  if (!isClickOnPopup && !isClickOnTrigger) {
46
70
  closePopup(event);
71
+ // If there was an outside click on a non-interactive element, the focus should be on the trigger.
72
+ if (document.activeElement && !isInteractiveElement(document.activeElement) && fg('platform_dst_popup-disable-focuslock')) {
73
+ triggerRef === null || triggerRef === void 0 || triggerRef.focus();
74
+ }
47
75
  }
48
76
  };
49
77
  var onKeyDown = function onKeyDown(event) {
50
- if (isLayerDisabled()) {
51
- return;
52
- }
53
- var key = event.key;
54
- if (key === 'Escape' || key === 'Esc' || shouldCloseOnTab && key === 'Tab') {
55
- closePopup(event);
78
+ if (fg('platform_dst_popup-disable-focuslock')) {
79
+ var key = event.key,
80
+ shiftKey = event.shiftKey;
81
+ if (shiftKey && key === 'Tab' && !shouldRenderToParent) {
82
+ if (isLayerDisabled()) {
83
+ return;
84
+ }
85
+ // We need to move the focus to the popup trigger when the popup is displayed in React.Portal.
86
+ requestFrame(function () {
87
+ var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
88
+ if (isPopupFocusOut) {
89
+ closePopup(event);
90
+ if (currentLevel === 1) {
91
+ triggerRef === null || triggerRef === void 0 || triggerRef.focus();
92
+ }
93
+ }
94
+ });
95
+ return;
96
+ }
97
+ if (key === 'Tab') {
98
+ var _document$activeEleme2;
99
+ // We have cases where we need to close the Popup on Tab press.
100
+ // Example: DropdownMenu
101
+ if (shouldCloseOnTab) {
102
+ if (isLayerDisabled()) {
103
+ return;
104
+ }
105
+ closePopup(event);
106
+ return;
107
+ }
108
+ if (isLayerDisabled() && (_document$activeEleme2 = document.activeElement) !== null && _document$activeEleme2 !== void 0 && _document$activeEleme2.closest('[aria-modal]')) {
109
+ return;
110
+ }
111
+ if (shouldDisableFocusTrap) {
112
+ if (shouldRenderToParent) {
113
+ // We need to move the focus to the previous interactive element before popup trigger
114
+ requestFrame(function () {
115
+ var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
116
+ if (isPopupFocusOut) {
117
+ closePopup(event);
118
+ }
119
+ });
120
+ } else {
121
+ requestFrame(function () {
122
+ if (!document.hasFocus()) {
123
+ closePopup(event);
124
+ }
125
+ });
126
+ }
127
+ return;
128
+ }
129
+ }
130
+ if (isLayerDisabled()) {
131
+ return;
132
+ }
133
+ if (key === 'Escape' || key === 'Esc') {
134
+ if (triggerRef && autoFocus) {
135
+ triggerRef.focus();
136
+ }
137
+ closePopup(event);
138
+ }
139
+ } else {
140
+ if (isLayerDisabled()) {
141
+ return;
142
+ }
143
+ var _key = event.key;
144
+ if (_key === 'Escape' || _key === 'Esc' || shouldCloseOnTab && _key === 'Tab') {
145
+ closePopup(event);
146
+ }
56
147
  }
57
148
  };
58
149
  var unbind = bindAll(window, [{
@@ -74,15 +165,13 @@ export var useCloseManager = function useCloseManager(_ref) {
74
165
  if (isLayerDisabled() || !(document.activeElement instanceof HTMLIFrameElement)) {
75
166
  return;
76
167
  }
77
- var wrapper = document.activeElement.closest('[data-ds--level]');
78
- if (!wrapper || currentLevel > Number(wrapper.getAttribute('data-ds--level'))) {
79
- closePopup(e);
80
- }
168
+ closePopup(e);
81
169
  }
82
170
  });
83
171
  return function () {
172
+ cancelAllFrames();
84
173
  unbind();
85
174
  unbindBlur();
86
175
  };
87
- }, [isOpen, onClose, popupRef, triggerRef, capture, isLayerDisabled, shouldCloseOnTab, currentLevel]);
176
+ }, [isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, capture, isLayerDisabled, shouldCloseOnTab, currentLevel, shouldRenderToParent, requestFrame, cancelAllFrames]);
88
177
  };
@@ -1,35 +1,51 @@
1
1
  import { useEffect } from 'react';
2
2
  import createFocusTrap from 'focus-trap';
3
3
  import noop from '@atlaskit/ds-lib/noop';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
5
+ import { useAnimationFrame } from './utils/use-animation-frame';
4
6
  export var useFocusManager = function useFocusManager(_ref) {
5
7
  var initialFocusRef = _ref.initialFocusRef,
6
8
  popupRef = _ref.popupRef,
7
- shouldCloseOnTab = _ref.shouldCloseOnTab;
9
+ triggerRef = _ref.triggerRef,
10
+ autoFocus = _ref.autoFocus,
11
+ shouldCloseOnTab = _ref.shouldCloseOnTab,
12
+ shouldDisableFocusTrap = _ref.shouldDisableFocusTrap,
13
+ shouldReturnFocus = _ref.shouldReturnFocus;
14
+ var _useAnimationFrame = useAnimationFrame(),
15
+ requestFrame = _useAnimationFrame.requestFrame,
16
+ cancelAllFrames = _useAnimationFrame.cancelAllFrames;
8
17
  useEffect(function () {
9
18
  if (!popupRef || shouldCloseOnTab) {
10
19
  return noop;
11
20
  }
21
+ if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
22
+ // Plucking trigger & popup content container from the tab order so that
23
+ // when we Shift+Tab, the focus moves to the element before trigger
24
+ requestFrame(function () {
25
+ triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '-1');
26
+ if (popupRef && autoFocus) {
27
+ popupRef.setAttribute('tabindex', '-1');
28
+ }
29
+ (initialFocusRef || popupRef).focus();
30
+ });
31
+ return noop;
32
+ }
12
33
  var trapConfig = {
13
34
  clickOutsideDeactivates: true,
14
35
  escapeDeactivates: true,
15
36
  initialFocus: initialFocusRef || popupRef,
16
37
  fallbackFocus: popupRef,
17
- returnFocusOnDeactivate: true
38
+ returnFocusOnDeactivate: shouldReturnFocus
18
39
  };
19
40
  var focusTrap = createFocusTrap(popupRef, trapConfig);
20
- var frameId = null;
21
41
 
22
- // wait for the popup to reposition itself before we focus
23
- frameId = requestAnimationFrame(function () {
24
- frameId = null;
42
+ // Wait for the popup to reposition itself before we focus
43
+ requestFrame(function () {
25
44
  focusTrap.activate();
26
45
  });
27
46
  return function () {
28
- if (frameId != null) {
29
- cancelAnimationFrame(frameId);
30
- frameId = null;
31
- }
47
+ cancelAllFrames();
32
48
  focusTrap.deactivate();
33
49
  };
34
- }, [popupRef, initialFocusRef, shouldCloseOnTab]);
50
+ }, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames, shouldReturnFocus]);
35
51
  };
@@ -0,0 +1,10 @@
1
+ var interactiveTags = ['button', 'a', 'input', 'select', 'textarea'];
2
+ export var isInteractiveElement = function isInteractiveElement(element) {
3
+ if (interactiveTags.includes(element.tagName.toLowerCase())) {
4
+ return true;
5
+ }
6
+ if (element.getAttribute('tabindex') !== null || element.hasAttribute('contenteditable')) {
7
+ return true;
8
+ }
9
+ return false;
10
+ };
@@ -0,0 +1,26 @@
1
+ import { useCallback, useRef } from 'react';
2
+ export var useAnimationFrame = function useAnimationFrame() {
3
+ var animationsRef = useRef([]);
4
+ var requestFrame = useCallback(function (callback) {
5
+ var id = requestAnimationFrame(callback);
6
+ animationsRef.current.push(id);
7
+ return id;
8
+ }, []);
9
+ var cancelFrame = useCallback(function (id) {
10
+ cancelAnimationFrame(id);
11
+ animationsRef.current = animationsRef.current.filter(function (frameId) {
12
+ return frameId !== id;
13
+ });
14
+ }, []);
15
+ var cancelAllFrames = useCallback(function () {
16
+ animationsRef.current.forEach(function (id) {
17
+ return cancelAnimationFrame(id);
18
+ });
19
+ animationsRef.current = [];
20
+ }, []);
21
+ return {
22
+ requestFrame: requestFrame,
23
+ cancelFrame: cancelFrame,
24
+ cancelAllFrames: cancelAllFrames
25
+ };
26
+ };
@@ -1,4 +1,4 @@
1
1
  import { jsx } from '@emotion/react';
2
2
  import { type PopperWrapperProps } from './types';
3
- declare function PopperWrapper({ isOpen, id, offset, testId, content, fallbackPlacements, onClose, boundary, rootBoundary, shouldFlip, placement, popupComponent: PopupContainer, autoFocus, triggerRef, shouldUseCaptureOnOutsideClick, shouldRenderToParent, shouldFitContainer, shouldDisableFocusLock, strategy, role, label, titleId, modifiers, }: PopperWrapperProps): jsx.JSX.Element;
3
+ declare function PopperWrapper({ isOpen, id, offset, testId, content, fallbackPlacements, onClose, boundary, rootBoundary, shouldFlip, placement, popupComponent: PopupContainer, autoFocus, triggerRef, shouldUseCaptureOnOutsideClick, shouldRenderToParent, shouldFitContainer, shouldDisableFocusLock, shouldReturnFocus, strategy, role, label, titleId, modifiers, }: PopperWrapperProps): jsx.JSX.Element;
4
4
  export default PopperWrapper;
@@ -4,7 +4,7 @@ export interface TriggerProps {
4
4
  ref: Ref<any>;
5
5
  'aria-controls'?: string;
6
6
  'aria-expanded': boolean;
7
- 'aria-haspopup': boolean;
7
+ 'aria-haspopup': boolean | 'dialog';
8
8
  }
9
9
  export type PopupRef = HTMLDivElement | null;
10
10
  export type TriggerRef = HTMLElement | HTMLButtonElement | null;
@@ -164,10 +164,15 @@ interface BaseProps {
164
164
  */
165
165
  shouldFitContainer?: boolean;
166
166
  /**
167
- * This allows the popup disable focus lock. It will only work when `shouldRenderToParent` is `true`.
167
+ * This makes the popup close on Tab key press. It will only work when `shouldRenderToParent` is `true`.
168
168
  * The default is `false`.
169
169
  */
170
170
  shouldDisableFocusLock?: boolean;
171
+ /**
172
+ * This determines whether the popup trigger will be focused when the popup content closes.
173
+ * The default is `true`.
174
+ */
175
+ shouldReturnFocus?: boolean;
171
176
  /**
172
177
  * This controls the positioning strategy to use. Can vary between `absolute` and `fixed`.
173
178
  * The default is `fixed`.
@@ -226,11 +231,18 @@ export type CloseManagerHook = Pick<PopupProps, 'isOpen' | 'onClose'> & {
226
231
  triggerRef: TriggerRef;
227
232
  shouldUseCaptureOnOutsideClick?: boolean;
228
233
  shouldCloseOnTab?: boolean;
234
+ shouldDisableFocusTrap: boolean;
235
+ shouldRenderToParent?: boolean;
236
+ autoFocus: boolean;
229
237
  };
230
238
  export type FocusManagerHook = {
231
239
  initialFocusRef: HTMLElement | null;
232
240
  popupRef: PopupRef;
233
241
  shouldCloseOnTab?: boolean;
242
+ triggerRef: TriggerRef;
243
+ autoFocus: boolean;
244
+ shouldDisableFocusTrap: boolean;
245
+ shouldReturnFocus: boolean;
234
246
  };
235
247
  export type RepositionOnUpdateProps = PropsWithChildren<{
236
248
  update: PopperChildrenProps['update'];
@@ -1,2 +1,2 @@
1
1
  import { type CloseManagerHook } from './types';
2
- export declare const useCloseManager: ({ isOpen, onClose, popupRef, triggerRef, shouldUseCaptureOnOutsideClick: capture, shouldCloseOnTab, }: CloseManagerHook) => void;
2
+ export declare const useCloseManager: ({ isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, shouldUseCaptureOnOutsideClick: capture, shouldCloseOnTab, shouldRenderToParent, }: CloseManagerHook) => void;
@@ -1,2 +1,2 @@
1
1
  import { type FocusManagerHook } from './types';
2
- export declare const useFocusManager: ({ initialFocusRef, popupRef, shouldCloseOnTab, }: FocusManagerHook) => void;
2
+ export declare const useFocusManager: ({ initialFocusRef, popupRef, triggerRef, autoFocus, shouldCloseOnTab, shouldDisableFocusTrap, shouldReturnFocus, }: FocusManagerHook) => void;
@@ -0,0 +1 @@
1
+ export declare const isInteractiveElement: (element: HTMLElement) => boolean;
@@ -0,0 +1,5 @@
1
+ export declare const useAnimationFrame: () => {
2
+ requestFrame: (callback: () => void) => number;
3
+ cancelFrame: (id: number) => void;
4
+ cancelAllFrames: () => void;
5
+ };
@@ -1,4 +1,4 @@
1
1
  import { jsx } from '@emotion/react';
2
2
  import { type PopperWrapperProps } from './types';
3
- declare function PopperWrapper({ isOpen, id, offset, testId, content, fallbackPlacements, onClose, boundary, rootBoundary, shouldFlip, placement, popupComponent: PopupContainer, autoFocus, triggerRef, shouldUseCaptureOnOutsideClick, shouldRenderToParent, shouldFitContainer, shouldDisableFocusLock, strategy, role, label, titleId, modifiers, }: PopperWrapperProps): jsx.JSX.Element;
3
+ declare function PopperWrapper({ isOpen, id, offset, testId, content, fallbackPlacements, onClose, boundary, rootBoundary, shouldFlip, placement, popupComponent: PopupContainer, autoFocus, triggerRef, shouldUseCaptureOnOutsideClick, shouldRenderToParent, shouldFitContainer, shouldDisableFocusLock, shouldReturnFocus, strategy, role, label, titleId, modifiers, }: PopperWrapperProps): jsx.JSX.Element;
4
4
  export default PopperWrapper;
@@ -4,7 +4,7 @@ export interface TriggerProps {
4
4
  ref: Ref<any>;
5
5
  'aria-controls'?: string;
6
6
  'aria-expanded': boolean;
7
- 'aria-haspopup': boolean;
7
+ 'aria-haspopup': boolean | 'dialog';
8
8
  }
9
9
  export type PopupRef = HTMLDivElement | null;
10
10
  export type TriggerRef = HTMLElement | HTMLButtonElement | null;
@@ -167,10 +167,15 @@ interface BaseProps {
167
167
  */
168
168
  shouldFitContainer?: boolean;
169
169
  /**
170
- * This allows the popup disable focus lock. It will only work when `shouldRenderToParent` is `true`.
170
+ * This makes the popup close on Tab key press. It will only work when `shouldRenderToParent` is `true`.
171
171
  * The default is `false`.
172
172
  */
173
173
  shouldDisableFocusLock?: boolean;
174
+ /**
175
+ * This determines whether the popup trigger will be focused when the popup content closes.
176
+ * The default is `true`.
177
+ */
178
+ shouldReturnFocus?: boolean;
174
179
  /**
175
180
  * This controls the positioning strategy to use. Can vary between `absolute` and `fixed`.
176
181
  * The default is `fixed`.
@@ -229,11 +234,18 @@ export type CloseManagerHook = Pick<PopupProps, 'isOpen' | 'onClose'> & {
229
234
  triggerRef: TriggerRef;
230
235
  shouldUseCaptureOnOutsideClick?: boolean;
231
236
  shouldCloseOnTab?: boolean;
237
+ shouldDisableFocusTrap: boolean;
238
+ shouldRenderToParent?: boolean;
239
+ autoFocus: boolean;
232
240
  };
233
241
  export type FocusManagerHook = {
234
242
  initialFocusRef: HTMLElement | null;
235
243
  popupRef: PopupRef;
236
244
  shouldCloseOnTab?: boolean;
245
+ triggerRef: TriggerRef;
246
+ autoFocus: boolean;
247
+ shouldDisableFocusTrap: boolean;
248
+ shouldReturnFocus: boolean;
237
249
  };
238
250
  export type RepositionOnUpdateProps = PropsWithChildren<{
239
251
  update: PopperChildrenProps['update'];
@@ -1,2 +1,2 @@
1
1
  import { type CloseManagerHook } from './types';
2
- export declare const useCloseManager: ({ isOpen, onClose, popupRef, triggerRef, shouldUseCaptureOnOutsideClick: capture, shouldCloseOnTab, }: CloseManagerHook) => void;
2
+ export declare const useCloseManager: ({ isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, shouldUseCaptureOnOutsideClick: capture, shouldCloseOnTab, shouldRenderToParent, }: CloseManagerHook) => void;
@@ -1,2 +1,2 @@
1
1
  import { type FocusManagerHook } from './types';
2
- export declare const useFocusManager: ({ initialFocusRef, popupRef, shouldCloseOnTab, }: FocusManagerHook) => void;
2
+ export declare const useFocusManager: ({ initialFocusRef, popupRef, triggerRef, autoFocus, shouldCloseOnTab, shouldDisableFocusTrap, shouldReturnFocus, }: FocusManagerHook) => void;
@@ -0,0 +1 @@
1
+ export declare const isInteractiveElement: (element: HTMLElement) => boolean;
@@ -0,0 +1,5 @@
1
+ export declare const useAnimationFrame: () => {
2
+ requestFrame: (callback: () => void) => number;
3
+ cancelFrame: (id: number) => void;
4
+ cancelAllFrames: () => void;
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/popup",
3
- "version": "1.22.2",
3
+ "version": "1.23.1",
4
4
  "description": "A popup displays brief content in an overlay.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -40,14 +40,14 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@atlaskit/ds-lib": "^2.4.0",
43
+ "@atlaskit/ds-lib": "^2.5.0",
44
44
  "@atlaskit/layering": "^0.4.0",
45
45
  "@atlaskit/platform-feature-flags": "^0.3.0",
46
46
  "@atlaskit/popper": "^6.2.0",
47
47
  "@atlaskit/portal": "^4.9.0",
48
48
  "@atlaskit/primitives": "^12.0.0",
49
49
  "@atlaskit/theme": "^13.0.0",
50
- "@atlaskit/tokens": "^1.58.0",
50
+ "@atlaskit/tokens": "^1.59.0",
51
51
  "@babel/runtime": "^7.0.0",
52
52
  "@emotion/react": "^11.7.1",
53
53
  "bind-event-listener": "^3.0.0",
@@ -62,9 +62,10 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@af/accessibility-testing": "*",
65
+ "@af/integration-testing": "*",
65
66
  "@af/visual-regression": "*",
66
- "@atlaskit/button": "^19.2.0",
67
- "@atlaskit/icon": "^22.12.0",
67
+ "@atlaskit/button": "^20.1.0",
68
+ "@atlaskit/icon": "^22.15.0",
68
69
  "@atlaskit/ssr": "*",
69
70
  "@atlaskit/textfield": "^6.5.0",
70
71
  "@atlaskit/toggle": "^13.3.0",
@@ -109,6 +110,9 @@
109
110
  "platform-feature-flags": {
110
111
  "platform.design-system-team.iframe_gojiv": {
111
112
  "type": "boolean"
113
+ },
114
+ "platform_dst_popup-disable-focuslock": {
115
+ "type": "boolean"
112
116
  }
113
117
  },
114
118
  "homepage": "https://atlassian.design/components/popup/"