@atlaskit/top-layer 4.4.2 → 4.4.4
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/dialog/dialog-content.js +57 -47
- package/dist/cjs/focus/get-closest-layer.js +10 -0
- package/dist/cjs/focus/get-focusables.js +45 -10
- package/dist/cjs/focus/get-next-focusable.js +40 -15
- package/dist/cjs/focus/is-nested-layer-focused.js +8 -9
- package/dist/cjs/internal/use-animated-visibility.js +238 -165
- package/dist/cjs/popover/popover.js +97 -81
- package/dist/cjs/popover-surface/popover-surface.js +1 -1
- package/dist/es2019/dialog/dialog-content.js +57 -47
- package/dist/es2019/focus/get-closest-layer.js +5 -0
- package/dist/es2019/focus/get-focusables.js +43 -6
- package/dist/es2019/focus/get-next-focusable.js +38 -15
- package/dist/es2019/focus/is-nested-layer-focused.js +8 -9
- package/dist/es2019/internal/use-animated-visibility.js +235 -154
- package/dist/es2019/popover/popover.js +98 -82
- package/dist/es2019/popover-surface/popover-surface.js +1 -1
- package/dist/esm/dialog/dialog-content.js +57 -47
- package/dist/esm/focus/get-closest-layer.js +4 -0
- package/dist/esm/focus/get-focusables.js +45 -10
- package/dist/esm/focus/get-next-focusable.js +40 -15
- package/dist/esm/focus/is-nested-layer-focused.js +8 -9
- package/dist/esm/internal/use-animated-visibility.js +239 -166
- package/dist/esm/popover/popover.js +97 -81
- package/dist/esm/popover-surface/popover-surface.js +1 -1
- package/dist/types/dialog/types.d.ts +6 -4
- package/dist/types/focus/get-closest-layer.d.ts +3 -0
- package/dist/types/focus/get-focusables.d.ts +2 -0
- package/dist/types/focus/get-next-focusable.d.ts +6 -2
- package/dist/types/focus/is-nested-layer-focused.d.ts +2 -2
- package/dist/types/internal/use-animated-visibility.d.ts +37 -50
- package/dist/types/popover/popover.d.ts +3 -1
- package/dist/types/popover/types.d.ts +12 -9
- package/package.json +14 -9
- package/dist/cjs/focus/is-in-same-layer.js +0 -17
- package/dist/cjs/focus/selectors.js +0 -19
- package/dist/es2019/focus/is-in-same-layer.js +0 -12
- package/dist/es2019/focus/selectors.js +0 -11
- package/dist/esm/focus/is-in-same-layer.js +0 -11
- package/dist/esm/focus/selectors.js +0 -13
- package/dist/types/focus/is-in-same-layer.d.ts +0 -8
- package/dist/types/focus/selectors.d.ts +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/top-layer
|
|
2
2
|
|
|
3
|
+
## 4.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`610d564dbc5dd`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/610d564dbc5dd) -
|
|
8
|
+
Behind `platform-dst-top-layer`, harden the Popover and Dialog animation lifecycle state machine
|
|
9
|
+
and completion callbacks, restore focus before controlled Popover closure removes native
|
|
10
|
+
listeners, and preserve Modal Dialog open-complete callbacks across reopen cycles.
|
|
11
|
+
|
|
12
|
+
## 4.4.3
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [`25d19b9cc0200`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/25d19b9cc0200) -
|
|
17
|
+
Handle non-tabbable focus origins and skip hidden, disabled, or inert elements when wrapping focus
|
|
18
|
+
|
|
3
19
|
## 4.4.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* dialog-content.tsx generated by @compiled/babel-plugin
|
|
1
|
+
/* dialog-content.tsx generated by @compiled/babel-plugin v3.0.1 */
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
@@ -85,6 +85,13 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
85
85
|
var ownRef = (0, _react.useRef)(null);
|
|
86
86
|
var combinedRef = (0, _mergeRefs.default)([ownRef, ref]);
|
|
87
87
|
var closeReasonRef = (0, _react.useRef)(null);
|
|
88
|
+
var dismissedByRef = (0, _react.useRef)(dismissedBy);
|
|
89
|
+
var onCloseRef = (0, _react.useRef)(onClose);
|
|
90
|
+
// Keep native event handlers stable while ensuring they read the latest committed props.
|
|
91
|
+
(0, _react.useLayoutEffect)(function () {
|
|
92
|
+
dismissedByRef.current = dismissedBy;
|
|
93
|
+
onCloseRef.current = onClose;
|
|
94
|
+
}, [dismissedBy, onClose]);
|
|
88
95
|
var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
|
|
89
96
|
isOpen: isOpen,
|
|
90
97
|
shouldAnimate: shouldAnimate,
|
|
@@ -93,11 +100,52 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
93
100
|
onExitFinish: onExitFinish
|
|
94
101
|
}),
|
|
95
102
|
phase = _useAnimatedVisibilit.phase,
|
|
103
|
+
isMounted = _useAnimatedVisibilit.isMounted,
|
|
104
|
+
onBeforeToggle = _useAnimatedVisibilit.onBeforeToggle,
|
|
96
105
|
onToggle = _useAnimatedVisibilit.onToggle;
|
|
97
106
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
107
|
+
// Ordering is important: bind native lifecycle listeners before the later
|
|
108
|
+
// layout effect calls showModal() or close(), because `beforetoggle` fires
|
|
109
|
+
// synchronously during those calls.
|
|
110
|
+
(0, _react.useLayoutEffect)(function () {
|
|
111
|
+
if (!isMounted) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
var dialog = ownRef.current;
|
|
115
|
+
if (!dialog) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
return (0, _bindEventListener.bindAll)(dialog, [{
|
|
119
|
+
type: 'beforetoggle',
|
|
120
|
+
listener: onBeforeToggle
|
|
121
|
+
}, {
|
|
122
|
+
// Browsers retarget clicks on ::backdrop to the <dialog> element.
|
|
123
|
+
// Close on backdrop click when outside-click dismissal is enabled.
|
|
124
|
+
type: 'click',
|
|
125
|
+
listener: function listener(event) {
|
|
126
|
+
if (event.target === event.currentTarget && dismissedByRef.current === 'escape-and-outside-click') {
|
|
127
|
+
closeReasonRef.current = 'overlay-click';
|
|
128
|
+
dialog.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
type: 'toggle',
|
|
133
|
+
listener: function listener(event) {
|
|
134
|
+
if (event.newState === 'closed') {
|
|
135
|
+
var reason = closeReasonRef.current;
|
|
136
|
+
closeReasonRef.current = null;
|
|
137
|
+
// A reason is only set for user-initiated closes. When React closes the
|
|
138
|
+
// dialog because isOpen changed, the consumer already knows.
|
|
139
|
+
if (reason) {
|
|
140
|
+
onCloseRef.current({
|
|
141
|
+
reason: reason
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
onToggle(event);
|
|
146
|
+
}
|
|
147
|
+
}]);
|
|
148
|
+
}, [isMounted, onBeforeToggle, onToggle]);
|
|
101
149
|
|
|
102
150
|
// Native `<dialog>.showModal()` traps focus but wraps through `<body>` at
|
|
103
151
|
// the boundary (A → B → C → body → A). This hook intercepts Tab to wrap
|
|
@@ -143,7 +191,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
143
191
|
// See notes/decisions/safari-escape-nested-popover-in-dialog.md
|
|
144
192
|
var _useSafariEscapeFix = (0, _useSafariEscapeFix2.useSafariEscapeFix)({
|
|
145
193
|
dialogRef: ownRef,
|
|
146
|
-
isVisible:
|
|
194
|
+
isVisible: isMounted
|
|
147
195
|
}),
|
|
148
196
|
shouldIgnoreEscape = _useSafariEscapeFix.shouldIgnoreEscape;
|
|
149
197
|
|
|
@@ -176,49 +224,11 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
176
224
|
closeReasonRef.current = 'escape';
|
|
177
225
|
}, [dismissedBy, shouldIgnoreEscape]);
|
|
178
226
|
|
|
179
|
-
// Handle backdrop interaction
|
|
180
|
-
// Attached via bind-event-listener rather than a React prop so we avoid
|
|
181
|
-
// a11y lint suppressions on the <dialog> element.
|
|
182
|
-
// The click listener provides light dismiss consistently across browsers.
|
|
183
|
-
// `isVisible` is in deps so the listener re-binds to the new <dialog>
|
|
184
|
-
// element after a host unmount / remount cycle.
|
|
185
|
-
(0, _react.useLayoutEffect)(function () {
|
|
186
|
-
var dialog = ownRef.current;
|
|
187
|
-
if (!dialog) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
return (0, _bindEventListener.bindAll)(dialog, [{
|
|
191
|
-
type: 'click',
|
|
192
|
-
listener: function listener(event) {
|
|
193
|
-
if (event.target === event.currentTarget && dismissedBy === 'escape-and-outside-click') {
|
|
194
|
-
closeReasonRef.current = 'overlay-click';
|
|
195
|
-
// Calls native close because this is emulating the native light-dismiss behavior
|
|
196
|
-
dialog.close();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}, {
|
|
200
|
-
type: 'toggle',
|
|
201
|
-
listener: function listener(event) {
|
|
202
|
-
if (event.newState === 'closed') {
|
|
203
|
-
var reason = closeReasonRef.current;
|
|
204
|
-
closeReasonRef.current = null;
|
|
205
|
-
if (reason) {
|
|
206
|
-
onClose({
|
|
207
|
-
reason: reason
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Visibility cleanup runs after close-reason handling.
|
|
213
|
-
onToggle(event);
|
|
214
|
-
}
|
|
215
|
-
}]);
|
|
216
|
-
}, [dismissedBy, onClose, onToggle, isVisible]);
|
|
217
|
-
|
|
218
227
|
// Unmount the `<dialog>` once exit completes so it does not leave an
|
|
219
|
-
// empty `role="dialog"` element in the accessibility tree.
|
|
220
|
-
//
|
|
221
|
-
|
|
228
|
+
// empty `role="dialog"` element in the accessibility tree. A later open
|
|
229
|
+
// remounts a fresh element, so the listener-binding and native-visibility
|
|
230
|
+
// effects run against that element.
|
|
231
|
+
if (!isMounted) {
|
|
222
232
|
return null;
|
|
223
233
|
}
|
|
224
234
|
var shouldAnimateBackdrop = !shouldHideBackdrop && shouldAnimate;
|
|
@@ -4,10 +4,35 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.getFocusables = getFocusables;
|
|
7
|
-
var
|
|
8
|
-
var
|
|
7
|
+
var _getClosestLayer = require("./get-closest-layer");
|
|
8
|
+
var focusableExclusions = ['[tabindex="-1"]', ':disabled', '[aria-disabled="true"]', '[aria-hidden="true"]'];
|
|
9
|
+
var exclusionSelector = focusableExclusions.map(function (selector) {
|
|
10
|
+
return ":not(".concat(selector, ")");
|
|
11
|
+
}).join('');
|
|
12
|
+
var focusableSelector = ['a[href]', 'area[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'button', 'iframe', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].map(function (selector) {
|
|
13
|
+
return "".concat(selector).concat(exclusionSelector);
|
|
14
|
+
}).join(',');
|
|
15
|
+
function isVisible(element) {
|
|
16
|
+
// Visibility filtering must not break focus navigation when the API is unsupported.
|
|
17
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// Exclude elements hidden by display, content-visibility or visibility because
|
|
22
|
+
// they cannot receive focus. Offscreen, occluded and opacity-zero elements can
|
|
23
|
+
// receive focus, so they intentionally remain included.
|
|
24
|
+
return element.checkVisibility({
|
|
25
|
+
checkVisibilityCSS: true
|
|
26
|
+
});
|
|
27
|
+
} catch (_unused) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
/**
|
|
10
33
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
34
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
35
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
11
36
|
*
|
|
12
37
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
13
38
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -17,14 +42,24 @@ var _selectors = require("./selectors");
|
|
|
17
42
|
function getFocusables(_ref) {
|
|
18
43
|
var container = _ref.container,
|
|
19
44
|
filter = _ref.filter;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
45
|
+
var containerLayer = (0, _getClosestLayer.getClosestLayer)({
|
|
46
|
+
element: container
|
|
47
|
+
});
|
|
48
|
+
return Array.from(container.querySelectorAll(focusableSelector)).filter(function (element) {
|
|
49
|
+
if (!(element instanceof HTMLElement)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if ((0, _getClosestLayer.getClosestLayer)({
|
|
53
|
+
element: element
|
|
54
|
+
}) !== containerLayer) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (element.closest('[inert]')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!isVisible(element)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
28
63
|
return !filter || filter(element, container);
|
|
29
64
|
});
|
|
30
65
|
}
|
|
@@ -5,17 +5,20 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getNextFocusable = getNextFocusable;
|
|
7
7
|
var _getFocusables = require("./get-focusables");
|
|
8
|
-
var _selectors = require("./selectors");
|
|
9
8
|
/**
|
|
10
9
|
* Returns the next focusable element in the given direction relative to the
|
|
11
10
|
* currently focused element within the container. Wraps around at both ends.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
13
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
14
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
15
|
+
* excluded as destinations.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` if there is no focused element within the container.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
function getNextFocusable(_ref) {
|
|
18
|
-
var
|
|
21
|
+
var _all3;
|
|
19
22
|
var container = _ref.container,
|
|
20
23
|
direction = _ref.direction,
|
|
21
24
|
filter = _ref.filter;
|
|
@@ -23,21 +26,43 @@ function getNextFocusable(_ref) {
|
|
|
23
26
|
container: container,
|
|
24
27
|
filter: filter
|
|
25
28
|
});
|
|
26
|
-
var
|
|
27
|
-
|
|
29
|
+
var first = all[0];
|
|
30
|
+
var last = all[all.length - 1];
|
|
31
|
+
|
|
32
|
+
// Checking the elements directly narrows types from `HTMLElement | undefined`.
|
|
33
|
+
if (!first || !last) {
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
30
|
-
var
|
|
31
|
-
if (
|
|
36
|
+
var current = container.ownerDocument.activeElement;
|
|
37
|
+
if (!(current instanceof HTMLElement) || !container.contains(current)) {
|
|
32
38
|
return null;
|
|
33
39
|
}
|
|
34
|
-
var
|
|
40
|
+
var index = all.indexOf(current);
|
|
41
|
+
|
|
42
|
+
// The focused element can be absent from `all` when it is non-tabbable or
|
|
43
|
+
// excluded by the filter. Find the first focusable element that follows it in
|
|
44
|
+
// the DOM, which identifies the nearest focusable destination in either direction.
|
|
45
|
+
if (index === -1) {
|
|
46
|
+
var _all;
|
|
47
|
+
var followingIndex = all.findIndex(function (element) {
|
|
48
|
+
return Boolean(current.compareDocumentPosition(element) & current.DOCUMENT_POSITION_FOLLOWING);
|
|
49
|
+
});
|
|
35
50
|
if (direction === 'forwards') {
|
|
36
|
-
|
|
51
|
+
var _all$followingIndex;
|
|
52
|
+
// If nothing follows the current element, then wrap to the start.
|
|
53
|
+
return (_all$followingIndex = all[followingIndex]) !== null && _all$followingIndex !== void 0 ? _all$followingIndex : first;
|
|
37
54
|
}
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
|
|
56
|
+
// The closest preceding element is immediately before the first following one.
|
|
57
|
+
// A missing adjacent entry means the backward sequence wraps to the end.
|
|
58
|
+
return (_all = all[followingIndex - 1]) !== null && _all !== void 0 ? _all : last;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The current element is tabbable, so use its adjacent list entry and wrap
|
|
62
|
+
// when it is already at the boundary.
|
|
63
|
+
if (direction === 'forwards') {
|
|
64
|
+
var _all2;
|
|
65
|
+
return (_all2 = all[index + 1]) !== null && _all2 !== void 0 ? _all2 : first;
|
|
66
|
+
}
|
|
67
|
+
return (_all3 = all[index - 1]) !== null && _all3 !== void 0 ? _all3 : last;
|
|
43
68
|
}
|
|
@@ -4,17 +4,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.isNestedLayerFocused = isNestedLayerFocused;
|
|
7
|
-
var
|
|
8
|
-
var _isInSameLayer = require("./is-in-same-layer");
|
|
7
|
+
var _getClosestLayer = require("./get-closest-layer");
|
|
9
8
|
/**
|
|
10
9
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
11
|
-
* descendant of `container` (a `[popover]
|
|
12
|
-
*
|
|
10
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
11
|
+
* descendant of `container`).
|
|
13
12
|
*/
|
|
14
13
|
function isNestedLayerFocused(_ref) {
|
|
15
|
-
var _getDocument;
|
|
16
14
|
var container = _ref.container;
|
|
17
|
-
var focused =
|
|
15
|
+
var focused = container.ownerDocument.activeElement;
|
|
18
16
|
if (!(focused instanceof HTMLElement)) {
|
|
19
17
|
return false;
|
|
20
18
|
}
|
|
@@ -25,8 +23,9 @@ function isNestedLayerFocused(_ref) {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
// If not in the same layer, must be in a nested layer
|
|
28
|
-
return
|
|
29
|
-
element: focused
|
|
30
|
-
|
|
26
|
+
return (0, _getClosestLayer.getClosestLayer)({
|
|
27
|
+
element: focused
|
|
28
|
+
}) !== (0, _getClosestLayer.getClosestLayer)({
|
|
29
|
+
element: container
|
|
31
30
|
});
|
|
32
31
|
}
|