@atlaskit/popup 1.22.1 → 1.23.0
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 +16 -0
- package/dist/cjs/popper-wrapper.js +11 -4
- package/dist/cjs/popup.js +2 -1
- package/dist/cjs/use-close-manager.js +104 -15
- package/dist/cjs/use-focus-manager.js +25 -10
- package/dist/cjs/utils/is-element-interactive.js +16 -0
- package/dist/cjs/utils/use-animation-frame.js +32 -0
- package/dist/es2019/popper-wrapper.js +11 -4
- package/dist/es2019/popup.js +2 -1
- package/dist/es2019/use-close-manager.js +109 -17
- package/dist/es2019/use-focus-manager.js +26 -10
- package/dist/es2019/utils/is-element-interactive.js +10 -0
- package/dist/es2019/utils/use-animation-frame.js +22 -0
- package/dist/esm/popper-wrapper.js +11 -4
- package/dist/esm/popup.js +2 -1
- package/dist/esm/use-close-manager.js +104 -15
- package/dist/esm/use-focus-manager.js +25 -10
- package/dist/esm/utils/is-element-interactive.js +10 -0
- package/dist/esm/utils/use-animation-frame.js +26 -0
- package/dist/types/types.d.ts +8 -2
- package/dist/types/use-close-manager.d.ts +1 -1
- package/dist/types/use-focus-manager.d.ts +1 -1
- package/dist/types/utils/is-element-interactive.d.ts +1 -0
- package/dist/types/utils/use-animation-frame.d.ts +5 -0
- package/dist/types-ts4.5/types.d.ts +8 -2
- package/dist/types-ts4.5/use-close-manager.d.ts +1 -1
- package/dist/types-ts4.5/use-focus-manager.d.ts +1 -1
- package/dist/types-ts4.5/utils/is-element-interactive.d.ts +1 -0
- package/dist/types-ts4.5/utils/use-animation-frame.d.ts +5 -0
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/popup
|
|
2
2
|
|
|
3
|
+
## 1.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#128022](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/128022)
|
|
8
|
+
[`1495b8f9c9253`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/1495b8f9c9253) -
|
|
9
|
+
[ux] We are testing new focus behavior in non-dialog popup instances behind a feature flag. With
|
|
10
|
+
that in place, all popup instances that don't have role="dialog" applied will have focus traps
|
|
11
|
+
disabled by default. If this fix is successful, it will be available in a later release.
|
|
12
|
+
|
|
13
|
+
## 1.22.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
|
|
3
19
|
## 1.22.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -105,13 +105,17 @@ function PopperWrapper(_ref) {
|
|
|
105
105
|
initialFocusRef = _useState4[0],
|
|
106
106
|
setInitialFocusRef = _useState4[1];
|
|
107
107
|
|
|
108
|
-
// We have cases
|
|
109
|
-
//
|
|
108
|
+
// We have cases where we need to close the Popup on Tab press.
|
|
109
|
+
// Example: DropdownMenu
|
|
110
110
|
var shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
|
|
111
|
+
var shouldDisableFocusTrap = role !== 'dialog';
|
|
111
112
|
(0, _useFocusManager.useFocusManager)({
|
|
112
113
|
initialFocusRef: initialFocusRef,
|
|
113
114
|
popupRef: popupRef,
|
|
114
|
-
shouldCloseOnTab: shouldCloseOnTab
|
|
115
|
+
shouldCloseOnTab: shouldCloseOnTab,
|
|
116
|
+
triggerRef: triggerRef,
|
|
117
|
+
autoFocus: autoFocus,
|
|
118
|
+
shouldDisableFocusTrap: shouldDisableFocusTrap
|
|
115
119
|
});
|
|
116
120
|
(0, _useCloseManager.useCloseManager)({
|
|
117
121
|
isOpen: isOpen,
|
|
@@ -119,7 +123,10 @@ function PopperWrapper(_ref) {
|
|
|
119
123
|
popupRef: popupRef,
|
|
120
124
|
triggerRef: triggerRef,
|
|
121
125
|
shouldUseCaptureOnOutsideClick: shouldUseCaptureOnOutsideClick,
|
|
122
|
-
shouldCloseOnTab: shouldCloseOnTab
|
|
126
|
+
shouldCloseOnTab: shouldCloseOnTab,
|
|
127
|
+
autoFocus: autoFocus,
|
|
128
|
+
shouldDisableFocusTrap: shouldDisableFocusTrap,
|
|
129
|
+
shouldRenderToParent: shouldRenderToParent
|
|
123
130
|
});
|
|
124
131
|
var _UNSAFE_useLayering = (0, _layering.UNSAFE_useLayering)(),
|
|
125
132
|
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");
|
|
@@ -102,7 +103,7 @@ var Popup = exports.Popup = /*#__PURE__*/(0, _react.memo)(function (_ref) {
|
|
|
102
103
|
ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
|
|
103
104
|
'aria-controls': isOpen ? id : undefined,
|
|
104
105
|
'aria-expanded': isOpen,
|
|
105
|
-
'aria-haspopup': true
|
|
106
|
+
'aria-haspopup': role === 'dialog' && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock') ? 'dialog' : true
|
|
106
107
|
});
|
|
107
108
|
}), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : (0, _react2.jsx)(_portal.default, {
|
|
108
109
|
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 (
|
|
48
|
-
|
|
49
|
-
|
|
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 (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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,14 +8,34 @@ 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
|
-
|
|
16
|
+
triggerRef = _ref.triggerRef,
|
|
17
|
+
autoFocus = _ref.autoFocus,
|
|
18
|
+
shouldCloseOnTab = _ref.shouldCloseOnTab,
|
|
19
|
+
shouldDisableFocusTrap = _ref.shouldDisableFocusTrap;
|
|
20
|
+
var _useAnimationFrame = (0, _useAnimationFrame2.useAnimationFrame)(),
|
|
21
|
+
requestFrame = _useAnimationFrame.requestFrame,
|
|
22
|
+
cancelAllFrames = _useAnimationFrame.cancelAllFrames;
|
|
15
23
|
(0, _react.useEffect)(function () {
|
|
16
24
|
if (!popupRef || shouldCloseOnTab) {
|
|
17
25
|
return _noop.default;
|
|
18
26
|
}
|
|
27
|
+
if (shouldDisableFocusTrap && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
|
|
28
|
+
// Plucking trigger & popup content container from the tab order so that
|
|
29
|
+
// when we Shift+Tab, the focus moves to the element before trigger
|
|
30
|
+
requestFrame(function () {
|
|
31
|
+
triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '-1');
|
|
32
|
+
if (popupRef && autoFocus) {
|
|
33
|
+
popupRef.setAttribute('tabindex', '-1');
|
|
34
|
+
}
|
|
35
|
+
(initialFocusRef || popupRef).focus();
|
|
36
|
+
});
|
|
37
|
+
return _noop.default;
|
|
38
|
+
}
|
|
19
39
|
var trapConfig = {
|
|
20
40
|
clickOutsideDeactivates: true,
|
|
21
41
|
escapeDeactivates: true,
|
|
@@ -24,19 +44,14 @@ var useFocusManager = exports.useFocusManager = function useFocusManager(_ref) {
|
|
|
24
44
|
returnFocusOnDeactivate: true
|
|
25
45
|
};
|
|
26
46
|
var focusTrap = (0, _focusTrap.default)(popupRef, trapConfig);
|
|
27
|
-
var frameId = null;
|
|
28
47
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
frameId = null;
|
|
48
|
+
// Wait for the popup to reposition itself before we focus
|
|
49
|
+
requestFrame(function () {
|
|
32
50
|
focusTrap.activate();
|
|
33
51
|
});
|
|
34
52
|
return function () {
|
|
35
|
-
|
|
36
|
-
cancelAnimationFrame(frameId);
|
|
37
|
-
frameId = null;
|
|
38
|
-
}
|
|
53
|
+
cancelAllFrames();
|
|
39
54
|
focusTrap.deactivate();
|
|
40
55
|
};
|
|
41
|
-
}, [popupRef, initialFocusRef, shouldCloseOnTab]);
|
|
56
|
+
}, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames]);
|
|
42
57
|
};
|
|
@@ -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
|
+
};
|
|
@@ -90,13 +90,17 @@ function PopperWrapper({
|
|
|
90
90
|
const [popupRef, setPopupRef] = useState(null);
|
|
91
91
|
const [initialFocusRef, setInitialFocusRef] = useState(null);
|
|
92
92
|
|
|
93
|
-
// We have cases
|
|
94
|
-
//
|
|
93
|
+
// We have cases where we need to close the Popup on Tab press.
|
|
94
|
+
// Example: DropdownMenu
|
|
95
95
|
const shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
|
|
96
|
+
const shouldDisableFocusTrap = role !== 'dialog';
|
|
96
97
|
useFocusManager({
|
|
97
98
|
initialFocusRef,
|
|
98
99
|
popupRef,
|
|
99
|
-
shouldCloseOnTab
|
|
100
|
+
shouldCloseOnTab,
|
|
101
|
+
triggerRef,
|
|
102
|
+
autoFocus,
|
|
103
|
+
shouldDisableFocusTrap
|
|
100
104
|
});
|
|
101
105
|
useCloseManager({
|
|
102
106
|
isOpen,
|
|
@@ -104,7 +108,10 @@ function PopperWrapper({
|
|
|
104
108
|
popupRef,
|
|
105
109
|
triggerRef,
|
|
106
110
|
shouldUseCaptureOnOutsideClick,
|
|
107
|
-
shouldCloseOnTab
|
|
111
|
+
shouldCloseOnTab,
|
|
112
|
+
autoFocus,
|
|
113
|
+
shouldDisableFocusTrap,
|
|
114
|
+
shouldRenderToParent
|
|
108
115
|
});
|
|
109
116
|
const {
|
|
110
117
|
currentLevel
|
package/dist/es2019/popup.js
CHANGED
|
@@ -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';
|
|
@@ -83,7 +84,7 @@ export const Popup = /*#__PURE__*/memo(({
|
|
|
83
84
|
ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
|
|
84
85
|
'aria-controls': isOpen ? id : undefined,
|
|
85
86
|
'aria-expanded': isOpen,
|
|
86
|
-
'aria-haspopup': true
|
|
87
|
+
'aria-haspopup': role === 'dialog' && fg('platform_dst_popup-disable-focuslock') ? 'dialog' : true
|
|
87
88
|
});
|
|
88
89
|
}), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : jsx(Portal, {
|
|
89
90
|
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 (
|
|
44
|
-
|
|
45
|
-
|
|
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 (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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,15 +1,36 @@
|
|
|
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
|
-
|
|
9
|
+
triggerRef,
|
|
10
|
+
autoFocus,
|
|
11
|
+
shouldCloseOnTab,
|
|
12
|
+
shouldDisableFocusTrap
|
|
8
13
|
}) => {
|
|
14
|
+
const {
|
|
15
|
+
requestFrame,
|
|
16
|
+
cancelAllFrames
|
|
17
|
+
} = useAnimationFrame();
|
|
9
18
|
useEffect(() => {
|
|
10
19
|
if (!popupRef || shouldCloseOnTab) {
|
|
11
20
|
return noop;
|
|
12
21
|
}
|
|
22
|
+
if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
|
|
23
|
+
// Plucking trigger & popup content container from the tab order so that
|
|
24
|
+
// when we Shift+Tab, the focus moves to the element before trigger
|
|
25
|
+
requestFrame(() => {
|
|
26
|
+
triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.setAttribute('tabindex', '-1');
|
|
27
|
+
if (popupRef && autoFocus) {
|
|
28
|
+
popupRef.setAttribute('tabindex', '-1');
|
|
29
|
+
}
|
|
30
|
+
(initialFocusRef || popupRef).focus();
|
|
31
|
+
});
|
|
32
|
+
return noop;
|
|
33
|
+
}
|
|
13
34
|
const trapConfig = {
|
|
14
35
|
clickOutsideDeactivates: true,
|
|
15
36
|
escapeDeactivates: true,
|
|
@@ -18,19 +39,14 @@ export const useFocusManager = ({
|
|
|
18
39
|
returnFocusOnDeactivate: true
|
|
19
40
|
};
|
|
20
41
|
const focusTrap = createFocusTrap(popupRef, trapConfig);
|
|
21
|
-
let frameId = null;
|
|
22
42
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
frameId = null;
|
|
43
|
+
// Wait for the popup to reposition itself before we focus
|
|
44
|
+
requestFrame(() => {
|
|
26
45
|
focusTrap.activate();
|
|
27
46
|
});
|
|
28
47
|
return () => {
|
|
29
|
-
|
|
30
|
-
cancelAnimationFrame(frameId);
|
|
31
|
-
frameId = null;
|
|
32
|
-
}
|
|
48
|
+
cancelAllFrames();
|
|
33
49
|
focusTrap.deactivate();
|
|
34
50
|
};
|
|
35
|
-
}, [popupRef, initialFocusRef, shouldCloseOnTab]);
|
|
51
|
+
}, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames]);
|
|
36
52
|
};
|
|
@@ -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
|
+
};
|
|
@@ -99,13 +99,17 @@ function PopperWrapper(_ref) {
|
|
|
99
99
|
initialFocusRef = _useState4[0],
|
|
100
100
|
setInitialFocusRef = _useState4[1];
|
|
101
101
|
|
|
102
|
-
// We have cases
|
|
103
|
-
//
|
|
102
|
+
// We have cases where we need to close the Popup on Tab press.
|
|
103
|
+
// Example: DropdownMenu
|
|
104
104
|
var shouldCloseOnTab = shouldRenderToParent && shouldDisableFocusLock;
|
|
105
|
+
var shouldDisableFocusTrap = role !== 'dialog';
|
|
105
106
|
useFocusManager({
|
|
106
107
|
initialFocusRef: initialFocusRef,
|
|
107
108
|
popupRef: popupRef,
|
|
108
|
-
shouldCloseOnTab: shouldCloseOnTab
|
|
109
|
+
shouldCloseOnTab: shouldCloseOnTab,
|
|
110
|
+
triggerRef: triggerRef,
|
|
111
|
+
autoFocus: autoFocus,
|
|
112
|
+
shouldDisableFocusTrap: shouldDisableFocusTrap
|
|
109
113
|
});
|
|
110
114
|
useCloseManager({
|
|
111
115
|
isOpen: isOpen,
|
|
@@ -113,7 +117,10 @@ function PopperWrapper(_ref) {
|
|
|
113
117
|
popupRef: popupRef,
|
|
114
118
|
triggerRef: triggerRef,
|
|
115
119
|
shouldUseCaptureOnOutsideClick: shouldUseCaptureOnOutsideClick,
|
|
116
|
-
shouldCloseOnTab: shouldCloseOnTab
|
|
120
|
+
shouldCloseOnTab: shouldCloseOnTab,
|
|
121
|
+
autoFocus: autoFocus,
|
|
122
|
+
shouldDisableFocusTrap: shouldDisableFocusTrap,
|
|
123
|
+
shouldRenderToParent: shouldRenderToParent
|
|
117
124
|
});
|
|
118
125
|
var _UNSAFE_useLayering = UNSAFE_useLayering(),
|
|
119
126
|
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';
|
|
@@ -94,7 +95,7 @@ export var Popup = /*#__PURE__*/memo(function (_ref) {
|
|
|
94
95
|
ref: getMergedTriggerRef(ref, setTriggerRef, isOpen),
|
|
95
96
|
'aria-controls': isOpen ? id : undefined,
|
|
96
97
|
'aria-expanded': isOpen,
|
|
97
|
-
'aria-haspopup': true
|
|
98
|
+
'aria-haspopup': role === 'dialog' && fg('platform_dst_popup-disable-focuslock') ? 'dialog' : true
|
|
98
99
|
});
|
|
99
100
|
}), isOpen && (shouldRenderToParent || shouldFitContainer ? renderPopperWrapper : jsx(Portal, {
|
|
100
101
|
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 (
|
|
40
|
-
|
|
41
|
-
|
|
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 (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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,14 +1,34 @@
|
|
|
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
|
-
|
|
9
|
+
triggerRef = _ref.triggerRef,
|
|
10
|
+
autoFocus = _ref.autoFocus,
|
|
11
|
+
shouldCloseOnTab = _ref.shouldCloseOnTab,
|
|
12
|
+
shouldDisableFocusTrap = _ref.shouldDisableFocusTrap;
|
|
13
|
+
var _useAnimationFrame = useAnimationFrame(),
|
|
14
|
+
requestFrame = _useAnimationFrame.requestFrame,
|
|
15
|
+
cancelAllFrames = _useAnimationFrame.cancelAllFrames;
|
|
8
16
|
useEffect(function () {
|
|
9
17
|
if (!popupRef || shouldCloseOnTab) {
|
|
10
18
|
return noop;
|
|
11
19
|
}
|
|
20
|
+
if (shouldDisableFocusTrap && fg('platform_dst_popup-disable-focuslock')) {
|
|
21
|
+
// Plucking trigger & popup content container from the tab order so that
|
|
22
|
+
// when we Shift+Tab, the focus moves to the element before trigger
|
|
23
|
+
requestFrame(function () {
|
|
24
|
+
triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '-1');
|
|
25
|
+
if (popupRef && autoFocus) {
|
|
26
|
+
popupRef.setAttribute('tabindex', '-1');
|
|
27
|
+
}
|
|
28
|
+
(initialFocusRef || popupRef).focus();
|
|
29
|
+
});
|
|
30
|
+
return noop;
|
|
31
|
+
}
|
|
12
32
|
var trapConfig = {
|
|
13
33
|
clickOutsideDeactivates: true,
|
|
14
34
|
escapeDeactivates: true,
|
|
@@ -17,19 +37,14 @@ export var useFocusManager = function useFocusManager(_ref) {
|
|
|
17
37
|
returnFocusOnDeactivate: true
|
|
18
38
|
};
|
|
19
39
|
var focusTrap = createFocusTrap(popupRef, trapConfig);
|
|
20
|
-
var frameId = null;
|
|
21
40
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
frameId = null;
|
|
41
|
+
// Wait for the popup to reposition itself before we focus
|
|
42
|
+
requestFrame(function () {
|
|
25
43
|
focusTrap.activate();
|
|
26
44
|
});
|
|
27
45
|
return function () {
|
|
28
|
-
|
|
29
|
-
cancelAnimationFrame(frameId);
|
|
30
|
-
frameId = null;
|
|
31
|
-
}
|
|
46
|
+
cancelAllFrames();
|
|
32
47
|
focusTrap.deactivate();
|
|
33
48
|
};
|
|
34
|
-
}, [popupRef, initialFocusRef, shouldCloseOnTab]);
|
|
49
|
+
}, [popupRef, triggerRef, autoFocus, initialFocusRef, shouldCloseOnTab, shouldDisableFocusTrap, requestFrame, cancelAllFrames]);
|
|
35
50
|
};
|
|
@@ -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
|
+
};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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,7 +164,7 @@ interface BaseProps {
|
|
|
164
164
|
*/
|
|
165
165
|
shouldFitContainer?: boolean;
|
|
166
166
|
/**
|
|
167
|
-
* This
|
|
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;
|
|
@@ -226,11 +226,17 @@ export type CloseManagerHook = Pick<PopupProps, 'isOpen' | 'onClose'> & {
|
|
|
226
226
|
triggerRef: TriggerRef;
|
|
227
227
|
shouldUseCaptureOnOutsideClick?: boolean;
|
|
228
228
|
shouldCloseOnTab?: boolean;
|
|
229
|
+
shouldDisableFocusTrap: boolean;
|
|
230
|
+
shouldRenderToParent?: boolean;
|
|
231
|
+
autoFocus: boolean;
|
|
229
232
|
};
|
|
230
233
|
export type FocusManagerHook = {
|
|
231
234
|
initialFocusRef: HTMLElement | null;
|
|
232
235
|
popupRef: PopupRef;
|
|
233
236
|
shouldCloseOnTab?: boolean;
|
|
237
|
+
triggerRef: TriggerRef;
|
|
238
|
+
autoFocus: boolean;
|
|
239
|
+
shouldDisableFocusTrap: boolean;
|
|
234
240
|
};
|
|
235
241
|
export type RepositionOnUpdateProps = PropsWithChildren<{
|
|
236
242
|
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, }: FocusManagerHook) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isInteractiveElement: (element: HTMLElement) => boolean;
|
|
@@ -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,7 +167,7 @@ interface BaseProps {
|
|
|
167
167
|
*/
|
|
168
168
|
shouldFitContainer?: boolean;
|
|
169
169
|
/**
|
|
170
|
-
* This
|
|
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;
|
|
@@ -229,11 +229,17 @@ export type CloseManagerHook = Pick<PopupProps, 'isOpen' | 'onClose'> & {
|
|
|
229
229
|
triggerRef: TriggerRef;
|
|
230
230
|
shouldUseCaptureOnOutsideClick?: boolean;
|
|
231
231
|
shouldCloseOnTab?: boolean;
|
|
232
|
+
shouldDisableFocusTrap: boolean;
|
|
233
|
+
shouldRenderToParent?: boolean;
|
|
234
|
+
autoFocus: boolean;
|
|
232
235
|
};
|
|
233
236
|
export type FocusManagerHook = {
|
|
234
237
|
initialFocusRef: HTMLElement | null;
|
|
235
238
|
popupRef: PopupRef;
|
|
236
239
|
shouldCloseOnTab?: boolean;
|
|
240
|
+
triggerRef: TriggerRef;
|
|
241
|
+
autoFocus: boolean;
|
|
242
|
+
shouldDisableFocusTrap: boolean;
|
|
237
243
|
};
|
|
238
244
|
export type RepositionOnUpdateProps = PropsWithChildren<{
|
|
239
245
|
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, }: FocusManagerHook) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isInteractiveElement: (element: HTMLElement) => boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/popup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "A popup displays brief content in an overlay.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@atlaskit/popper": "^6.2.0",
|
|
47
47
|
"@atlaskit/portal": "^4.9.0",
|
|
48
48
|
"@atlaskit/primitives": "^12.0.0",
|
|
49
|
-
"@atlaskit/theme": "^
|
|
49
|
+
"@atlaskit/theme": "^13.0.0",
|
|
50
50
|
"@atlaskit/tokens": "^1.58.0",
|
|
51
51
|
"@babel/runtime": "^7.0.0",
|
|
52
52
|
"@emotion/react": "^11.7.1",
|
|
@@ -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": "^
|
|
67
|
-
"@atlaskit/icon": "^22.
|
|
67
|
+
"@atlaskit/button": "^20.0.0",
|
|
68
|
+
"@atlaskit/icon": "^22.13.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/"
|