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