@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* popover.tsx generated by @compiled/babel-plugin
|
|
1
|
+
/* popover.tsx generated by @compiled/babel-plugin v3.0.1 */
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
@@ -98,7 +98,9 @@ var POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree', '
|
|
|
98
98
|
* ### 📜 Browser dismiss is non-cancellable
|
|
99
99
|
*
|
|
100
100
|
* Per the popover spec, `beforetoggle` is cancellable on open transitions
|
|
101
|
-
* only, not on close.
|
|
101
|
+
* only, not on close. Consumers should update controlled intent by setting
|
|
102
|
+
* `isOpen` to `false` from `onClose`. If controlled intent remains open after
|
|
103
|
+
* native close, the Popover remains closed until the prop changes.
|
|
102
104
|
*
|
|
103
105
|
* - Spec: <https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute>
|
|
104
106
|
* - <https://github.com/whatwg/html/issues/8973>
|
|
@@ -137,44 +139,25 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
137
139
|
var combinedRef = (0, _mergeRefs.default)([ownRef, ref].filter(Boolean));
|
|
138
140
|
// `useId()` colons are invalid in CSS selectors and popover target attributes.
|
|
139
141
|
var popoverId = idProp !== null && idProp !== void 0 ? idProp : "popover-".concat(autoId.replace(/:/g, ''));
|
|
140
|
-
var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
|
|
141
|
-
isOpen: isOpen,
|
|
142
|
-
shouldAnimate: shouldAnimate,
|
|
143
|
-
elementRef: ownRef,
|
|
144
|
-
onEnterFinish: onEnterFinish,
|
|
145
|
-
onExitFinish: onExitFinish
|
|
146
|
-
}),
|
|
147
|
-
phase = _useAnimatedVisibilit.phase,
|
|
148
|
-
onToggle = _useAnimatedVisibilit.onToggle;
|
|
149
142
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// window (WCAG 2.4.3 guard). See notes/architecture/focus-restoration.md.
|
|
159
|
-
(0, _useFocusWrap.useFocusWrap)({
|
|
160
|
-
elementRef: ownRef,
|
|
161
|
-
role: role,
|
|
162
|
-
phase: phase
|
|
163
|
-
});
|
|
164
|
-
(0, _useInitialFocus.useInitialFocus)({
|
|
165
|
-
elementRef: ownRef,
|
|
166
|
-
phase: phase,
|
|
167
|
-
role: role
|
|
168
|
-
});
|
|
143
|
+
// Keep semantic callbacks current without rebinding native lifecycle
|
|
144
|
+
// listeners during the same commit that calls showPopover()/hidePopover().
|
|
145
|
+
var onCloseRef = (0, _react.useRef)(onClose);
|
|
146
|
+
var roleRef = (0, _react.useRef)(role);
|
|
147
|
+
(0, _react.useLayoutEffect)(function () {
|
|
148
|
+
onCloseRef.current = onClose;
|
|
149
|
+
roleRef.current = role;
|
|
150
|
+
}, [onClose, role]);
|
|
169
151
|
|
|
170
152
|
// Register with open layer observer so `closeLayers()` and open-count
|
|
171
153
|
// subscriptions work. Only popup-like roles register as `popup`; passive
|
|
172
154
|
// roles (tooltip, status, etc.) leave `type` undefined.
|
|
173
155
|
var handleObserverClose = (0, _react.useCallback)(function () {
|
|
174
|
-
|
|
156
|
+
var _onCloseRef$current;
|
|
157
|
+
(_onCloseRef$current = onCloseRef.current) === null || _onCloseRef$current === void 0 || _onCloseRef$current.call(onCloseRef, {
|
|
175
158
|
reason: 'programmatic'
|
|
176
159
|
});
|
|
177
|
-
}, [
|
|
160
|
+
}, []);
|
|
178
161
|
(0, _useNotifyOpenLayerObserver.useNotifyOpenLayerObserver)({
|
|
179
162
|
type: role && POPUP_ROLES.has(role) ? 'popup' : undefined,
|
|
180
163
|
isOpen: isOpen,
|
|
@@ -215,51 +198,75 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
215
198
|
var active = document.activeElement;
|
|
216
199
|
previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
|
|
217
200
|
}, []);
|
|
218
|
-
var
|
|
219
|
-
var
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
201
|
+
var restorePreviouslyFocusedElement = (0, _react.useCallback)(function () {
|
|
202
|
+
var previouslyFocused = previouslyFocusedElementRef.current;
|
|
203
|
+
previouslyFocusedElementRef.current = null;
|
|
204
|
+
if (previouslyFocused !== null && previouslyFocused.isConnected && (0, _shouldFocusIntoPopover.shouldFocusIntoPopover)({
|
|
205
|
+
role: roleRef.current
|
|
206
|
+
})) {
|
|
207
|
+
previouslyFocused.focus({
|
|
208
|
+
preventScroll: true
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}, []);
|
|
212
|
+
var handleToggleClosed = (0, _react.useCallback)(function (_event) {
|
|
213
|
+
var _onCloseRef$current2;
|
|
214
|
+
// Nested-popover focus restoration fallback (browser only
|
|
215
|
+
// restores outermost; Firefox skips nested entirely).
|
|
216
|
+
// Restore only when the role moved focus in on open AND the
|
|
217
|
+
// close was Escape/programmatic. For light dismiss, the
|
|
218
|
+
// click target keeps focus per HTML spec
|
|
219
|
+
// (`focusPreviousElement=false`).
|
|
220
|
+
var reason = closeReasonRef.current;
|
|
221
|
+
var isProgrammatic = programmaticCloseRef.current;
|
|
222
|
+
if (reason === 'escape' || isProgrammatic) {
|
|
223
|
+
restorePreviouslyFocusedElement();
|
|
224
|
+
} else {
|
|
225
|
+
previouslyFocusedElementRef.current = null;
|
|
223
226
|
}
|
|
224
|
-
if (event.newState === 'closed') {
|
|
225
|
-
if (element) {
|
|
226
|
-
// Nested-popover focus restoration fallback (browser only
|
|
227
|
-
// restores outermost; Firefox skips nested entirely).
|
|
228
|
-
// Restore only when the role moved focus in on open AND the
|
|
229
|
-
// close was Escape/programmatic. For light dismiss, the
|
|
230
|
-
// click target keeps focus per HTML spec
|
|
231
|
-
// (`focusPreviousElement=false`).
|
|
232
|
-
var previouslyFocused = previouslyFocusedElementRef.current;
|
|
233
|
-
var reason = closeReasonRef.current;
|
|
234
|
-
var isProgrammatic = programmaticCloseRef.current;
|
|
235
|
-
var shouldRestore = previouslyFocused !== null && previouslyFocused.isConnected && (0, _shouldFocusIntoPopover.shouldFocusIntoPopover)({
|
|
236
|
-
role: role
|
|
237
|
-
}) && (reason === 'escape' || isProgrammatic);
|
|
238
|
-
if (shouldRestore && previouslyFocused) {
|
|
239
|
-
previouslyFocused.focus({
|
|
240
|
-
preventScroll: true
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
previouslyFocusedElementRef.current = null;
|
|
244
|
-
}
|
|
245
227
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
var _reason = closeReasonRef.current;
|
|
254
|
-
closeReasonRef.current = 'light-dismiss';
|
|
255
|
-
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
256
|
-
onClose === null || onClose === void 0 || onClose({
|
|
257
|
-
reason: _reason
|
|
258
|
-
});
|
|
259
|
-
}
|
|
228
|
+
// Reset reason so a stale 'escape' from a race with Escape keydown
|
|
229
|
+
// does not corrupt the next browser-dismiss cycle.
|
|
230
|
+
closeReasonRef.current = 'light-dismiss';
|
|
231
|
+
|
|
232
|
+
// Programmatic close: consumer already knows.
|
|
233
|
+
if (isProgrammatic) {
|
|
234
|
+
return;
|
|
260
235
|
}
|
|
261
|
-
|
|
262
|
-
|
|
236
|
+
|
|
237
|
+
// Browser dismiss (Escape/click-outside).
|
|
238
|
+
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
239
|
+
(_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
|
|
240
|
+
reason: reason
|
|
241
|
+
});
|
|
242
|
+
}, [restorePreviouslyFocusedElement]);
|
|
243
|
+
var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
|
|
244
|
+
isOpen: isOpen,
|
|
245
|
+
shouldAnimate: shouldAnimate,
|
|
246
|
+
elementRef: ownRef,
|
|
247
|
+
onEnterFinish: onEnterFinish,
|
|
248
|
+
onExitFinish: onExitFinish
|
|
249
|
+
}),
|
|
250
|
+
phase = _useAnimatedVisibilit.phase,
|
|
251
|
+
isMounted = _useAnimatedVisibilit.isMounted,
|
|
252
|
+
onBeforeToggle = _useAnimatedVisibilit.onBeforeToggle,
|
|
253
|
+
onToggle = _useAnimatedVisibilit.onToggle;
|
|
254
|
+
|
|
255
|
+
// Focus management: initial focus on entry (role-dependent), Tab cycling
|
|
256
|
+
// for dialog roles. Restoration is native for outermost popovers; nested
|
|
257
|
+
// focus-capturing roles are handled above via the `beforetoggle` snapshot.
|
|
258
|
+
// Passing `phase` keeps `useFocusWrap` attached through the animated-exit
|
|
259
|
+
// window (WCAG 2.4.3 guard). See notes/architecture/focus-restoration.md.
|
|
260
|
+
(0, _useFocusWrap.useFocusWrap)({
|
|
261
|
+
elementRef: ownRef,
|
|
262
|
+
role: role,
|
|
263
|
+
phase: phase
|
|
264
|
+
});
|
|
265
|
+
(0, _useInitialFocus.useInitialFocus)({
|
|
266
|
+
elementRef: ownRef,
|
|
267
|
+
phase: phase,
|
|
268
|
+
role: role
|
|
269
|
+
});
|
|
263
270
|
|
|
264
271
|
// Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
|
|
265
272
|
// below, so listeners are attached when `showPopover()` synchronously
|
|
@@ -287,19 +294,28 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
287
294
|
});
|
|
288
295
|
var unbindToggle = (0, _bindEventListener.bind)(element, {
|
|
289
296
|
type: 'toggle',
|
|
290
|
-
listener:
|
|
297
|
+
listener: function listener(event) {
|
|
298
|
+
if (event.newState === 'closed') {
|
|
299
|
+
handleToggleClosed(event);
|
|
300
|
+
}
|
|
301
|
+
onToggle(event);
|
|
302
|
+
}
|
|
291
303
|
});
|
|
292
|
-
// Snapshot `document.activeElement` before `useInitialFocus` moves it
|
|
304
|
+
// Snapshot `document.activeElement` before `useInitialFocus` moves it,
|
|
305
|
+
// then advance the shared visibility lifecycle.
|
|
293
306
|
var unbindBeforeToggle = (0, _bindEventListener.bind)(element, {
|
|
294
307
|
type: 'beforetoggle',
|
|
295
|
-
listener:
|
|
308
|
+
listener: function listener(event) {
|
|
309
|
+
handleBeforeToggle(event);
|
|
310
|
+
onBeforeToggle(event);
|
|
311
|
+
}
|
|
296
312
|
});
|
|
297
313
|
return function () {
|
|
298
314
|
unbindEscape();
|
|
299
315
|
unbindToggle();
|
|
300
316
|
unbindBeforeToggle();
|
|
301
317
|
};
|
|
302
|
-
}, [
|
|
318
|
+
}, [handleBeforeToggle, handleToggleClosed, isMounted, onBeforeToggle, onToggle]);
|
|
303
319
|
|
|
304
320
|
// Show/hide based on isOpen. `showPopover`/`hidePopover` are no-ops when
|
|
305
321
|
// already in the target state. Try/catch guards `InvalidStateError` on
|
|
@@ -312,8 +328,8 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
312
328
|
if (isOpen) {
|
|
313
329
|
programmaticCloseRef.current = false;
|
|
314
330
|
// Clear stale 'escape' from a prior cycle: the ref outlives
|
|
315
|
-
// the host element, and
|
|
316
|
-
// `
|
|
331
|
+
// the host element, and interrupted close lifecycles may not reach
|
|
332
|
+
// `handleToggleClosed` to reset it.
|
|
317
333
|
closeReasonRef.current = 'light-dismiss';
|
|
318
334
|
try {
|
|
319
335
|
element.showPopover();
|
|
@@ -334,7 +350,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
334
350
|
// Unmount the host once exit completes so it does not leave an empty
|
|
335
351
|
// `role`/`popover` element in the accessibility tree. The element
|
|
336
352
|
// remounts on the next open; the `popoverId` is stable across opens.
|
|
337
|
-
if (!
|
|
353
|
+
if (!isMounted) {
|
|
338
354
|
return null;
|
|
339
355
|
}
|
|
340
356
|
return /*#__PURE__*/_react.default.createElement("div", {
|
|
@@ -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
|
import "./dialog-content.compiled.css";
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
4
|
import React, { forwardRef, useCallback, useId, useLayoutEffect, useRef } from 'react';
|
|
@@ -75,8 +75,17 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
|
|
|
75
75
|
const ownRef = useRef(null);
|
|
76
76
|
const combinedRef = mergeRefs([ownRef, ref]);
|
|
77
77
|
const closeReasonRef = useRef(null);
|
|
78
|
+
const dismissedByRef = useRef(dismissedBy);
|
|
79
|
+
const onCloseRef = useRef(onClose);
|
|
80
|
+
// Keep native event handlers stable while ensuring they read the latest committed props.
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
dismissedByRef.current = dismissedBy;
|
|
83
|
+
onCloseRef.current = onClose;
|
|
84
|
+
}, [dismissedBy, onClose]);
|
|
78
85
|
const {
|
|
79
86
|
phase,
|
|
87
|
+
isMounted,
|
|
88
|
+
onBeforeToggle,
|
|
80
89
|
onToggle
|
|
81
90
|
} = useAnimatedVisibility({
|
|
82
91
|
isOpen,
|
|
@@ -86,9 +95,48 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
|
|
|
86
95
|
onExitFinish
|
|
87
96
|
});
|
|
88
97
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
98
|
+
// Ordering is important: bind native lifecycle listeners before the later
|
|
99
|
+
// layout effect calls showModal() or close(), because `beforetoggle` fires
|
|
100
|
+
// synchronously during those calls.
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
if (!isMounted) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const dialog = ownRef.current;
|
|
106
|
+
if (!dialog) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
return bindAll(dialog, [{
|
|
110
|
+
type: 'beforetoggle',
|
|
111
|
+
listener: onBeforeToggle
|
|
112
|
+
}, {
|
|
113
|
+
// Browsers retarget clicks on ::backdrop to the <dialog> element.
|
|
114
|
+
// Close on backdrop click when outside-click dismissal is enabled.
|
|
115
|
+
type: 'click',
|
|
116
|
+
listener(event) {
|
|
117
|
+
if (event.target === event.currentTarget && dismissedByRef.current === 'escape-and-outside-click') {
|
|
118
|
+
closeReasonRef.current = 'overlay-click';
|
|
119
|
+
dialog.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, {
|
|
123
|
+
type: 'toggle',
|
|
124
|
+
listener(event) {
|
|
125
|
+
if (event.newState === 'closed') {
|
|
126
|
+
const reason = closeReasonRef.current;
|
|
127
|
+
closeReasonRef.current = null;
|
|
128
|
+
// A reason is only set for user-initiated closes. When React closes the
|
|
129
|
+
// dialog because isOpen changed, the consumer already knows.
|
|
130
|
+
if (reason) {
|
|
131
|
+
onCloseRef.current({
|
|
132
|
+
reason
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
onToggle(event);
|
|
137
|
+
}
|
|
138
|
+
}]);
|
|
139
|
+
}, [isMounted, onBeforeToggle, onToggle]);
|
|
92
140
|
|
|
93
141
|
// Native `<dialog>.showModal()` traps focus but wraps through `<body>` at
|
|
94
142
|
// the boundary (A → B → C → body → A). This hook intercepts Tab to wrap
|
|
@@ -136,7 +184,7 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
|
|
|
136
184
|
shouldIgnoreEscape
|
|
137
185
|
} = useSafariEscapeFix({
|
|
138
186
|
dialogRef: ownRef,
|
|
139
|
-
isVisible
|
|
187
|
+
isVisible: isMounted
|
|
140
188
|
});
|
|
141
189
|
|
|
142
190
|
// Handle native Escape (cancel event)
|
|
@@ -168,49 +216,11 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
|
|
|
168
216
|
closeReasonRef.current = 'escape';
|
|
169
217
|
}, [dismissedBy, shouldIgnoreEscape]);
|
|
170
218
|
|
|
171
|
-
// Handle backdrop interaction
|
|
172
|
-
// Attached via bind-event-listener rather than a React prop so we avoid
|
|
173
|
-
// a11y lint suppressions on the <dialog> element.
|
|
174
|
-
// The click listener provides light dismiss consistently across browsers.
|
|
175
|
-
// `isVisible` is in deps so the listener re-binds to the new <dialog>
|
|
176
|
-
// element after a host unmount / remount cycle.
|
|
177
|
-
useLayoutEffect(() => {
|
|
178
|
-
const dialog = ownRef.current;
|
|
179
|
-
if (!dialog) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
return bindAll(dialog, [{
|
|
183
|
-
type: 'click',
|
|
184
|
-
listener(event) {
|
|
185
|
-
if (event.target === event.currentTarget && dismissedBy === 'escape-and-outside-click') {
|
|
186
|
-
closeReasonRef.current = 'overlay-click';
|
|
187
|
-
// Calls native close because this is emulating the native light-dismiss behavior
|
|
188
|
-
dialog.close();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}, {
|
|
192
|
-
type: 'toggle',
|
|
193
|
-
listener(event) {
|
|
194
|
-
if (event.newState === 'closed') {
|
|
195
|
-
const reason = closeReasonRef.current;
|
|
196
|
-
closeReasonRef.current = null;
|
|
197
|
-
if (reason) {
|
|
198
|
-
onClose({
|
|
199
|
-
reason
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Visibility cleanup runs after close-reason handling.
|
|
205
|
-
onToggle(event);
|
|
206
|
-
}
|
|
207
|
-
}]);
|
|
208
|
-
}, [dismissedBy, onClose, onToggle, isVisible]);
|
|
209
|
-
|
|
210
219
|
// Unmount the `<dialog>` once exit completes so it does not leave an
|
|
211
|
-
// empty `role="dialog"` element in the accessibility tree.
|
|
212
|
-
//
|
|
213
|
-
|
|
220
|
+
// empty `role="dialog"` element in the accessibility tree. A later open
|
|
221
|
+
// remounts a fresh element, so the listener-binding and native-visibility
|
|
222
|
+
// effects run against that element.
|
|
223
|
+
if (!isMounted) {
|
|
214
224
|
return null;
|
|
215
225
|
}
|
|
216
226
|
const shouldAnimateBackdrop = !shouldHideBackdrop && shouldAnimate;
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
2
|
+
const focusableExclusions = ['[tabindex="-1"]', ':disabled', '[aria-disabled="true"]', '[aria-hidden="true"]'];
|
|
3
|
+
const exclusionSelector = focusableExclusions.map(selector => `:not(${selector})`).join('');
|
|
4
|
+
const focusableSelector = ['a[href]', 'area[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'button', 'iframe', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].map(selector => `${selector}${exclusionSelector}`).join(',');
|
|
5
|
+
function isVisible(element) {
|
|
6
|
+
// Visibility filtering must not break focus navigation when the API is unsupported.
|
|
7
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
// Exclude elements hidden by display, content-visibility or visibility because
|
|
12
|
+
// they cannot receive focus. Offscreen, occluded and opacity-zero elements can
|
|
13
|
+
// receive focus, so they intentionally remain included.
|
|
14
|
+
return element.checkVisibility({
|
|
15
|
+
checkVisibilityCSS: true
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
24
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
25
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
5
26
|
*
|
|
6
27
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
7
28
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -12,8 +33,24 @@ export function getFocusables({
|
|
|
12
33
|
container,
|
|
13
34
|
filter
|
|
14
35
|
}) {
|
|
15
|
-
|
|
16
|
-
element
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
const containerLayer = getClosestLayer({
|
|
37
|
+
element: container
|
|
38
|
+
});
|
|
39
|
+
return Array.from(container.querySelectorAll(focusableSelector)).filter(element => {
|
|
40
|
+
if (!(element instanceof HTMLElement)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (getClosestLayer({
|
|
44
|
+
element
|
|
45
|
+
}) !== containerLayer) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (element.closest('[inert]')) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!isVisible(element)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return !filter || filter(element, container);
|
|
55
|
+
});
|
|
19
56
|
}
|
|
@@ -2,37 +2,60 @@
|
|
|
2
2
|
* Returns the next focusable element in the given direction relative to the
|
|
3
3
|
* currently focused element within the container. Wraps around at both ends.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The currently focused element does not need to be tabbable. This prevents
|
|
6
|
+
* navigation from getting stuck on an element that was programmatically or
|
|
7
|
+
* pointer focused with `tabindex="-1"`. Negative-tab-index elements remain
|
|
8
|
+
* excluded as destinations.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` if there is no focused element within the container.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { getFocusables } from './get-focusables';
|
|
10
|
-
import { selectors } from './selectors';
|
|
11
14
|
export function getNextFocusable({
|
|
12
15
|
container,
|
|
13
16
|
direction,
|
|
14
17
|
filter
|
|
15
18
|
}) {
|
|
16
|
-
var
|
|
19
|
+
var _all3;
|
|
17
20
|
const all = getFocusables({
|
|
18
21
|
container,
|
|
19
22
|
filter
|
|
20
23
|
});
|
|
21
|
-
const
|
|
22
|
-
|
|
24
|
+
const first = all[0];
|
|
25
|
+
const last = all[all.length - 1];
|
|
26
|
+
|
|
27
|
+
// Checking the elements directly narrows types from `HTMLElement | undefined`.
|
|
28
|
+
if (!first || !last) {
|
|
23
29
|
return null;
|
|
24
30
|
}
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
31
|
+
const current = container.ownerDocument.activeElement;
|
|
32
|
+
if (!(current instanceof HTMLElement) || !container.contains(current)) {
|
|
27
33
|
return null;
|
|
28
34
|
}
|
|
29
|
-
const
|
|
35
|
+
const index = all.indexOf(current);
|
|
36
|
+
|
|
37
|
+
// The focused element can be absent from `all` when it is non-tabbable or
|
|
38
|
+
// excluded by the filter. Find the first focusable element that follows it in
|
|
39
|
+
// the DOM, which identifies the nearest focusable destination in either direction.
|
|
40
|
+
if (index === -1) {
|
|
41
|
+
var _all;
|
|
42
|
+
const followingIndex = all.findIndex(element => Boolean(current.compareDocumentPosition(element) & current.DOCUMENT_POSITION_FOLLOWING));
|
|
30
43
|
if (direction === 'forwards') {
|
|
31
|
-
|
|
44
|
+
var _all$followingIndex;
|
|
45
|
+
// If nothing follows the current element, then wrap to the start.
|
|
46
|
+
return (_all$followingIndex = all[followingIndex]) !== null && _all$followingIndex !== void 0 ? _all$followingIndex : first;
|
|
32
47
|
}
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
|
|
49
|
+
// The closest preceding element is immediately before the first following one.
|
|
50
|
+
// A missing adjacent entry means the backward sequence wraps to the end.
|
|
51
|
+
return (_all = all[followingIndex - 1]) !== null && _all !== void 0 ? _all : last;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The current element is tabbable, so use its adjacent list entry and wrap
|
|
55
|
+
// when it is already at the boundary.
|
|
56
|
+
if (direction === 'forwards') {
|
|
57
|
+
var _all2;
|
|
58
|
+
return (_all2 = all[index + 1]) !== null && _all2 !== void 0 ? _all2 : first;
|
|
59
|
+
}
|
|
60
|
+
return (_all3 = all[index - 1]) !== null && _all3 !== void 0 ? _all3 : last;
|
|
38
61
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isInSameLayer } from './is-in-same-layer';
|
|
1
|
+
import { getClosestLayer } from './get-closest-layer';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
6
|
-
* descendant of `container` (a `[popover]
|
|
7
|
-
*
|
|
5
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
6
|
+
* descendant of `container`).
|
|
8
7
|
*/
|
|
9
8
|
export function isNestedLayerFocused({
|
|
10
9
|
container
|
|
11
10
|
}) {
|
|
12
|
-
|
|
13
|
-
const focused = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
|
|
11
|
+
const focused = container.ownerDocument.activeElement;
|
|
14
12
|
if (!(focused instanceof HTMLElement)) {
|
|
15
13
|
return false;
|
|
16
14
|
}
|
|
@@ -21,8 +19,9 @@ export function isNestedLayerFocused({
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
// If not in the same layer, must be in a nested layer
|
|
24
|
-
return
|
|
25
|
-
element: focused
|
|
26
|
-
|
|
22
|
+
return getClosestLayer({
|
|
23
|
+
element: focused
|
|
24
|
+
}) !== getClosestLayer({
|
|
25
|
+
element: container
|
|
27
26
|
});
|
|
28
27
|
}
|