@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 @@ var 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,44 +130,25 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
|
|
|
128
130
|
var combinedRef = mergeRefs([ownRef, ref].filter(Boolean));
|
|
129
131
|
// `useId()` colons are invalid in CSS selectors and popover target attributes.
|
|
130
132
|
var popoverId = idProp !== null && idProp !== void 0 ? idProp : "popover-".concat(autoId.replace(/:/g, ''));
|
|
131
|
-
var _useAnimatedVisibilit = useAnimatedVisibility({
|
|
132
|
-
isOpen: isOpen,
|
|
133
|
-
shouldAnimate: shouldAnimate,
|
|
134
|
-
elementRef: ownRef,
|
|
135
|
-
onEnterFinish: onEnterFinish,
|
|
136
|
-
onExitFinish: onExitFinish
|
|
137
|
-
}),
|
|
138
|
-
phase = _useAnimatedVisibilit.phase,
|
|
139
|
-
onToggle = _useAnimatedVisibilit.onToggle;
|
|
140
133
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
var
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// window (WCAG 2.4.3 guard). See notes/architecture/focus-restoration.md.
|
|
150
|
-
useFocusWrap({
|
|
151
|
-
elementRef: ownRef,
|
|
152
|
-
role: role,
|
|
153
|
-
phase: phase
|
|
154
|
-
});
|
|
155
|
-
useInitialFocus({
|
|
156
|
-
elementRef: ownRef,
|
|
157
|
-
phase: phase,
|
|
158
|
-
role: role
|
|
159
|
-
});
|
|
134
|
+
// Keep semantic callbacks current without rebinding native lifecycle
|
|
135
|
+
// listeners during the same commit that calls showPopover()/hidePopover().
|
|
136
|
+
var onCloseRef = useRef(onClose);
|
|
137
|
+
var roleRef = useRef(role);
|
|
138
|
+
useLayoutEffect(function () {
|
|
139
|
+
onCloseRef.current = onClose;
|
|
140
|
+
roleRef.current = role;
|
|
141
|
+
}, [onClose, role]);
|
|
160
142
|
|
|
161
143
|
// Register with open layer observer so `closeLayers()` and open-count
|
|
162
144
|
// subscriptions work. Only popup-like roles register as `popup`; passive
|
|
163
145
|
// roles (tooltip, status, etc.) leave `type` undefined.
|
|
164
146
|
var handleObserverClose = useCallback(function () {
|
|
165
|
-
|
|
147
|
+
var _onCloseRef$current;
|
|
148
|
+
(_onCloseRef$current = onCloseRef.current) === null || _onCloseRef$current === void 0 || _onCloseRef$current.call(onCloseRef, {
|
|
166
149
|
reason: 'programmatic'
|
|
167
150
|
});
|
|
168
|
-
}, [
|
|
151
|
+
}, []);
|
|
169
152
|
useNotifyOpenLayerObserver({
|
|
170
153
|
type: role && POPUP_ROLES.has(role) ? 'popup' : undefined,
|
|
171
154
|
isOpen: isOpen,
|
|
@@ -206,51 +189,75 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
|
|
|
206
189
|
var active = document.activeElement;
|
|
207
190
|
previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
|
|
208
191
|
}, []);
|
|
209
|
-
var
|
|
210
|
-
var
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
192
|
+
var restorePreviouslyFocusedElement = useCallback(function () {
|
|
193
|
+
var previouslyFocused = previouslyFocusedElementRef.current;
|
|
194
|
+
previouslyFocusedElementRef.current = null;
|
|
195
|
+
if (previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
|
|
196
|
+
role: roleRef.current
|
|
197
|
+
})) {
|
|
198
|
+
previouslyFocused.focus({
|
|
199
|
+
preventScroll: true
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
var handleToggleClosed = useCallback(function (_event) {
|
|
204
|
+
var _onCloseRef$current2;
|
|
205
|
+
// Nested-popover focus restoration fallback (browser only
|
|
206
|
+
// restores outermost; Firefox skips nested entirely).
|
|
207
|
+
// Restore only when the role moved focus in on open AND the
|
|
208
|
+
// close was Escape/programmatic. For light dismiss, the
|
|
209
|
+
// click target keeps focus per HTML spec
|
|
210
|
+
// (`focusPreviousElement=false`).
|
|
211
|
+
var reason = closeReasonRef.current;
|
|
212
|
+
var isProgrammatic = programmaticCloseRef.current;
|
|
213
|
+
if (reason === 'escape' || isProgrammatic) {
|
|
214
|
+
restorePreviouslyFocusedElement();
|
|
215
|
+
} else {
|
|
216
|
+
previouslyFocusedElementRef.current = null;
|
|
214
217
|
}
|
|
215
|
-
if (event.newState === 'closed') {
|
|
216
|
-
if (element) {
|
|
217
|
-
// Nested-popover focus restoration fallback (browser only
|
|
218
|
-
// restores outermost; Firefox skips nested entirely).
|
|
219
|
-
// Restore only when the role moved focus in on open AND the
|
|
220
|
-
// close was Escape/programmatic. For light dismiss, the
|
|
221
|
-
// click target keeps focus per HTML spec
|
|
222
|
-
// (`focusPreviousElement=false`).
|
|
223
|
-
var previouslyFocused = previouslyFocusedElementRef.current;
|
|
224
|
-
var reason = closeReasonRef.current;
|
|
225
|
-
var isProgrammatic = programmaticCloseRef.current;
|
|
226
|
-
var shouldRestore = previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
|
|
227
|
-
role: role
|
|
228
|
-
}) && (reason === 'escape' || isProgrammatic);
|
|
229
|
-
if (shouldRestore && previouslyFocused) {
|
|
230
|
-
previouslyFocused.focus({
|
|
231
|
-
preventScroll: true
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
previouslyFocusedElementRef.current = null;
|
|
235
|
-
}
|
|
236
218
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
var _reason = closeReasonRef.current;
|
|
245
|
-
closeReasonRef.current = 'light-dismiss';
|
|
246
|
-
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
247
|
-
onClose === null || onClose === void 0 || onClose({
|
|
248
|
-
reason: _reason
|
|
249
|
-
});
|
|
250
|
-
}
|
|
219
|
+
// Reset reason so a stale 'escape' from a race with Escape keydown
|
|
220
|
+
// does not corrupt the next browser-dismiss cycle.
|
|
221
|
+
closeReasonRef.current = 'light-dismiss';
|
|
222
|
+
|
|
223
|
+
// Programmatic close: consumer already knows.
|
|
224
|
+
if (isProgrammatic) {
|
|
225
|
+
return;
|
|
251
226
|
}
|
|
252
|
-
|
|
253
|
-
|
|
227
|
+
|
|
228
|
+
// Browser dismiss (Escape/click-outside).
|
|
229
|
+
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
230
|
+
(_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
|
|
231
|
+
reason: reason
|
|
232
|
+
});
|
|
233
|
+
}, [restorePreviouslyFocusedElement]);
|
|
234
|
+
var _useAnimatedVisibilit = useAnimatedVisibility({
|
|
235
|
+
isOpen: isOpen,
|
|
236
|
+
shouldAnimate: shouldAnimate,
|
|
237
|
+
elementRef: ownRef,
|
|
238
|
+
onEnterFinish: onEnterFinish,
|
|
239
|
+
onExitFinish: onExitFinish
|
|
240
|
+
}),
|
|
241
|
+
phase = _useAnimatedVisibilit.phase,
|
|
242
|
+
isMounted = _useAnimatedVisibilit.isMounted,
|
|
243
|
+
onBeforeToggle = _useAnimatedVisibilit.onBeforeToggle,
|
|
244
|
+
onToggle = _useAnimatedVisibilit.onToggle;
|
|
245
|
+
|
|
246
|
+
// Focus management: initial focus on entry (role-dependent), Tab cycling
|
|
247
|
+
// for dialog roles. Restoration is native for outermost popovers; nested
|
|
248
|
+
// focus-capturing roles are handled above via the `beforetoggle` snapshot.
|
|
249
|
+
// Passing `phase` keeps `useFocusWrap` attached through the animated-exit
|
|
250
|
+
// window (WCAG 2.4.3 guard). See notes/architecture/focus-restoration.md.
|
|
251
|
+
useFocusWrap({
|
|
252
|
+
elementRef: ownRef,
|
|
253
|
+
role: role,
|
|
254
|
+
phase: phase
|
|
255
|
+
});
|
|
256
|
+
useInitialFocus({
|
|
257
|
+
elementRef: ownRef,
|
|
258
|
+
phase: phase,
|
|
259
|
+
role: role
|
|
260
|
+
});
|
|
254
261
|
|
|
255
262
|
// Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
|
|
256
263
|
// below, so listeners are attached when `showPopover()` synchronously
|
|
@@ -278,19 +285,28 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
|
|
|
278
285
|
});
|
|
279
286
|
var unbindToggle = bind(element, {
|
|
280
287
|
type: 'toggle',
|
|
281
|
-
listener:
|
|
288
|
+
listener: function listener(event) {
|
|
289
|
+
if (event.newState === 'closed') {
|
|
290
|
+
handleToggleClosed(event);
|
|
291
|
+
}
|
|
292
|
+
onToggle(event);
|
|
293
|
+
}
|
|
282
294
|
});
|
|
283
|
-
// Snapshot `document.activeElement` before `useInitialFocus` moves it
|
|
295
|
+
// Snapshot `document.activeElement` before `useInitialFocus` moves it,
|
|
296
|
+
// then advance the shared visibility lifecycle.
|
|
284
297
|
var unbindBeforeToggle = bind(element, {
|
|
285
298
|
type: 'beforetoggle',
|
|
286
|
-
listener:
|
|
299
|
+
listener: function listener(event) {
|
|
300
|
+
handleBeforeToggle(event);
|
|
301
|
+
onBeforeToggle(event);
|
|
302
|
+
}
|
|
287
303
|
});
|
|
288
304
|
return function () {
|
|
289
305
|
unbindEscape();
|
|
290
306
|
unbindToggle();
|
|
291
307
|
unbindBeforeToggle();
|
|
292
308
|
};
|
|
293
|
-
}, [
|
|
309
|
+
}, [handleBeforeToggle, handleToggleClosed, isMounted, onBeforeToggle, onToggle]);
|
|
294
310
|
|
|
295
311
|
// Show/hide based on isOpen. `showPopover`/`hidePopover` are no-ops when
|
|
296
312
|
// already in the target state. Try/catch guards `InvalidStateError` on
|
|
@@ -303,8 +319,8 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
|
|
|
303
319
|
if (isOpen) {
|
|
304
320
|
programmaticCloseRef.current = false;
|
|
305
321
|
// Clear stale 'escape' from a prior cycle: the ref outlives
|
|
306
|
-
// the host element, and
|
|
307
|
-
// `
|
|
322
|
+
// the host element, and interrupted close lifecycles may not reach
|
|
323
|
+
// `handleToggleClosed` to reset it.
|
|
308
324
|
closeReasonRef.current = 'light-dismiss';
|
|
309
325
|
try {
|
|
310
326
|
element.showPopover();
|
|
@@ -325,7 +341,7 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
|
|
|
325
341
|
// Unmount the host once exit completes so it does not leave an empty
|
|
326
342
|
// `role`/`popover` element in the accessibility tree. The element
|
|
327
343
|
// remounts on the next open; the `popoverId` is stable across opens.
|
|
328
|
-
if (!
|
|
344
|
+
if (!isMounted) {
|
|
329
345
|
return null;
|
|
330
346
|
}
|
|
331
347
|
return /*#__PURE__*/React.createElement("div", {
|
|
@@ -25,12 +25,13 @@ export type TDialogDismissedBy = 'none' | 'escape' | 'escape-and-outside-click';
|
|
|
25
25
|
type TDialogBaseProps = {
|
|
26
26
|
children: ReactNode;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Controlled visibility intent for the dialog. Native visibility and lifecycle
|
|
29
|
+
* phase can temporarily differ from this value while a close settles.
|
|
29
30
|
*
|
|
30
31
|
* - `true`: calls `showModal()`. When `shouldAnimate` is `true`, the entry
|
|
31
32
|
* animation plays via `@starting-style`.
|
|
32
33
|
* - `false`: calls `close()`. When `shouldAnimate` is `true`, the exit
|
|
33
|
-
* animation plays via `allow-discrete`
|
|
34
|
+
* animation plays via `allow-discrete` while the lifecycle phase is `exiting`.
|
|
34
35
|
*
|
|
35
36
|
* **Lifecycle observable to consumers:**
|
|
36
37
|
*
|
|
@@ -119,8 +120,9 @@ type TDialogBaseProps = {
|
|
|
119
120
|
*/
|
|
120
121
|
onEnterFinish?: () => void;
|
|
121
122
|
/**
|
|
122
|
-
* Called after the
|
|
123
|
-
*
|
|
123
|
+
* Called after the native closed `toggle` and any exit animations settle.
|
|
124
|
+
* With no animation or reduced motion, this still waits for the browser's
|
|
125
|
+
* task-queued `toggle` so focus restoration finishes against a mounted host.
|
|
124
126
|
*
|
|
125
127
|
* Use this for external lifecycle coordination, e.g. firing an
|
|
126
128
|
* `onCloseComplete` callback or notifying ExitingPersistence.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TFocusableFilter } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Returns all focusable HTMLElements within the container, optionally filtered.
|
|
4
|
+
* Elements with `tabindex="-1"` are excluded because they can be focused
|
|
5
|
+
* programmatically but are not sequential focus navigation destinations.
|
|
4
6
|
*
|
|
5
7
|
* Focusables that belong to a nested top-layer scope (a `[popover]` or
|
|
6
8
|
* `<dialog>` descendant of the container) are excluded - those elements
|
|
@@ -2,8 +2,12 @@
|
|
|
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
|
import type { TFocusableFilter } from './types';
|
|
9
13
|
export declare function getNextFocusable({ container, direction, filter, }: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns `true` when `document.activeElement` is inside a nested top-layer
|
|
3
|
-
* descendant of `container` (a `[popover]
|
|
4
|
-
*
|
|
3
|
+
* descendant of `container` (a `[popover]` or `<dialog>` that is itself a
|
|
4
|
+
* descendant of `container`).
|
|
5
5
|
*/
|
|
6
6
|
export declare function isNestedLayerFocused({ container }: {
|
|
7
7
|
container: HTMLElement;
|
|
@@ -1,46 +1,48 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
2
|
type TUseAnimatedVisibilityArgs = {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Controlled visibility intent from the consumer. This can temporarily differ
|
|
5
|
+
* from native visibility during browser-initiated dismissal and close settlement.
|
|
5
6
|
*/
|
|
6
7
|
isOpen: boolean;
|
|
7
8
|
/**
|
|
8
|
-
* Animation config for entry/exit
|
|
9
|
-
* Pass `false`
|
|
9
|
+
* Animation config for entry/exit animations.
|
|
10
|
+
* Pass `false` to disable animation.
|
|
10
11
|
*/
|
|
11
12
|
shouldAnimate: boolean;
|
|
12
13
|
/**
|
|
13
|
-
* Ref to the DOM element that plays the entry/exit
|
|
14
|
-
* Used to inspect active animations.
|
|
14
|
+
* Ref to the DOM element that plays the entry/exit animations.
|
|
15
|
+
* Used to inspect its active animations.
|
|
15
16
|
*/
|
|
16
17
|
elementRef: RefObject<HTMLElement | null>;
|
|
17
18
|
/**
|
|
18
|
-
* Called after the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Called after entry settles and the `open` phase has committed, so the
|
|
20
|
+
* callback observes the host without its entering styles. This includes
|
|
21
|
+
* initial mount with `isOpen=true`.
|
|
21
22
|
*/
|
|
22
23
|
onEnterFinish?: () => void;
|
|
23
24
|
/**
|
|
24
|
-
* Called after the
|
|
25
|
-
*
|
|
25
|
+
* Called after the native closed `toggle` and any exit animations settle.
|
|
26
|
+
* With animation disabled, this still waits for the task-queued `toggle`.
|
|
27
|
+
* The callback runs immediately before the host transitions to `closed`,
|
|
28
|
+
* so the element remains mounted while the callback runs.
|
|
26
29
|
*/
|
|
27
30
|
onExitFinish?: () => void;
|
|
28
31
|
};
|
|
29
32
|
type TUseAnimatedVisibilityResult = {
|
|
30
33
|
/**
|
|
31
|
-
* Current visibility phase.
|
|
32
|
-
*
|
|
33
|
-
* `phase !== 'closed'` means the host element is mounted and on
|
|
34
|
-
* screen, including during animated entry, animated exit, and the
|
|
35
|
-
* non-animated close handshake. Use this when you need "the user
|
|
36
|
-
* can still see / interact with the element" semantics (e.g. focus
|
|
37
|
-
* trapping, listeners that must outlive the exit).
|
|
34
|
+
* Current visibility phase. The host element is mounted whenever
|
|
35
|
+
* `phase !== 'closed'`.
|
|
38
36
|
*/
|
|
39
37
|
phase: TPhase;
|
|
38
|
+
isMounted: boolean;
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
* Bind to the primitive's native `beforetoggle` event.
|
|
41
|
+
*/
|
|
42
|
+
onBeforeToggle: (event: ToggleEvent) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Bind to the primitive's native `toggle` event after any primitive-specific
|
|
45
|
+
* close handling that must complete before exit settlement.
|
|
44
46
|
*/
|
|
45
47
|
onToggle: (event: ToggleEvent) => void;
|
|
46
48
|
};
|
|
@@ -55,11 +57,11 @@ type TUseAnimatedVisibilityResult = {
|
|
|
55
57
|
* animation disabled the lifecycle goes directly `closed → open`.
|
|
56
58
|
* - `open`: host element is mounted and settled. No transition is
|
|
57
59
|
* playing.
|
|
58
|
-
* - `exiting`:
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* focus restoration can
|
|
60
|
+
* - `exiting`: a native close has begun but the host element remains
|
|
61
|
+
* mounted. Usually this follows `isOpen` changing to `false`, but it can
|
|
62
|
+
* also begin with a browser-initiated dismissal while controlled intent
|
|
63
|
+
* is still open. With animation disabled, this phase lasts until the
|
|
64
|
+
* browser's closed `toggle` fires so native focus restoration can finish.
|
|
63
65
|
*
|
|
64
66
|
* State machine:
|
|
65
67
|
* ```
|
|
@@ -69,41 +71,26 @@ type TUseAnimatedVisibilityResult = {
|
|
|
69
71
|
* exiting → entering (reopen mid-exit, animated)
|
|
70
72
|
* exiting → open (reopen mid-handshake, non-animated)
|
|
71
73
|
* ```
|
|
72
|
-
*
|
|
73
|
-
* `phase !== 'closed'` is the canonical "host is currently in the DOM
|
|
74
|
-
* and on screen" predicate.
|
|
75
74
|
*/
|
|
76
75
|
export type TPhase = 'closed' | 'entering' | 'open' | 'exiting';
|
|
77
76
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* logic. Those components own their own show/hide mechanisms (showPopover /
|
|
82
|
-
* hidePopover vs showModal / close) and event handling - this hook only
|
|
83
|
-
* manages the relationship between `isOpen` and when children are rendered.
|
|
77
|
+
* Coordinates controlled intent, native visibility, lifecycle phase, and host
|
|
78
|
+
* mounting around CSS exit transitions. The canonical contract is documented in
|
|
79
|
+
* `notes/architecture/animations.md#canonical-visibility-lifecycle-contract`.
|
|
84
80
|
*
|
|
85
|
-
*
|
|
81
|
+
* Used by both `Popover` and `Dialog` to share the same lifecycle logic. Those
|
|
82
|
+
* components own native event binding, show and hide commands, and
|
|
83
|
+
* component-specific event handling. This hook returns stable `onBeforeToggle`
|
|
84
|
+
* and `onToggle` callbacks that advance lifecycle phase and host mounting.
|
|
86
85
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* ## How it works
|
|
92
|
-
*
|
|
93
|
-
* The lifecycle is modeled as a `phase` discriminated union (`closed`,
|
|
94
|
-
* `entering`, `open`, `exiting`). The host element is rendered while
|
|
95
|
-
* `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
|
|
96
|
-
* while the CSS exit transition plays (or, with animation disabled,
|
|
97
|
-
* while we wait for the browser's closing `toggle` event to fire on the
|
|
98
|
-
* still-attached element):
|
|
86
|
+
* The host element is rendered while `phase !== 'closed'`. The `exiting`
|
|
87
|
+
* phase keeps it mounted until its CSS animation settles or, with animation
|
|
88
|
+
* disabled, until the browser reports that the native element has closed.
|
|
99
89
|
*
|
|
100
90
|
* ```
|
|
101
91
|
* isOpen: true ──────────────── false
|
|
102
92
|
* phase: open ──────────────── exiting ─── (exit settle) ─── closed
|
|
103
93
|
* ```
|
|
104
|
-
*
|
|
105
|
-
* Every effect dispatches on the single `phase` value instead of a
|
|
106
|
-
* cross-product of booleans.
|
|
107
94
|
*/
|
|
108
95
|
export declare function useAnimatedVisibility({ isOpen, shouldAnimate, elementRef, onEnterFinish, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
|
|
109
96
|
export {};
|
|
@@ -16,7 +16,9 @@ import { type TPopoverForwardedProps } from './types';
|
|
|
16
16
|
* ### 📜 Browser dismiss is non-cancellable
|
|
17
17
|
*
|
|
18
18
|
* Per the popover spec, `beforetoggle` is cancellable on open transitions
|
|
19
|
-
* only, not on close.
|
|
19
|
+
* only, not on close. Consumers should update controlled intent by setting
|
|
20
|
+
* `isOpen` to `false` from `onClose`. If controlled intent remains open after
|
|
21
|
+
* native close, the Popover remains closed until the prop changes.
|
|
20
22
|
*
|
|
21
23
|
* - Spec: <https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute>
|
|
22
24
|
* - <https://github.com/whatwg/html/issues/8973>
|
|
@@ -62,13 +62,15 @@ type TPopoverBaseProps = {
|
|
|
62
62
|
*/
|
|
63
63
|
id?: string;
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* Controlled visibility intent for the popover. Native visibility and lifecycle
|
|
66
|
+
* phase can temporarily differ from this value while a close settles.
|
|
66
67
|
*
|
|
67
68
|
* - **`true`:** show the popover (calls `showPopover()`). When `shouldAnimate`
|
|
68
69
|
* is `true`, the entry animation plays via `@starting-style`.
|
|
69
70
|
* - **`false`:** hide the popover. When `shouldAnimate` is `true`, the exit
|
|
70
|
-
* animation plays via `allow-discrete`
|
|
71
|
-
* Otherwise hides
|
|
71
|
+
* animation plays via `allow-discrete` while the lifecycle phase is `exiting`.
|
|
72
|
+
* Otherwise it hides visually without animation, while lifecycle settlement still
|
|
73
|
+
* waits for the native closed `toggle`.
|
|
72
74
|
*
|
|
73
75
|
* The consumer does not conditionally render the `Popover` - visibility is driven
|
|
74
76
|
* by this prop.
|
|
@@ -88,7 +90,7 @@ type TPopoverBaseProps = {
|
|
|
88
90
|
* via light dismiss (Escape, click outside) independently of this prop. When that
|
|
89
91
|
* happens, `onClose` is called and the consumer should respond by setting `isOpen`
|
|
90
92
|
* to `false`. If `isOpen` remains `true` after a browser dismiss, the DOM and
|
|
91
|
-
* React state
|
|
93
|
+
* React state remain out of sync until the consumer changes the prop.
|
|
92
94
|
*/
|
|
93
95
|
isOpen: boolean;
|
|
94
96
|
/**
|
|
@@ -110,8 +112,9 @@ type TPopoverBaseProps = {
|
|
|
110
112
|
*/
|
|
111
113
|
onEnterFinish?: () => void;
|
|
112
114
|
/**
|
|
113
|
-
* Called after the
|
|
114
|
-
*
|
|
115
|
+
* Called after the native closed `toggle` and any exit animations settle.
|
|
116
|
+
* With no animation or reduced motion, this still waits for the browser's
|
|
117
|
+
* task-queued `toggle` so focus restoration finishes against a mounted host.
|
|
115
118
|
*
|
|
116
119
|
* Use this for external lifecycle coordination, e.g. notifying a manager
|
|
117
120
|
* that the hide sequence is finished, or firing an `onCloseComplete` callback.
|
|
@@ -146,9 +149,9 @@ export type TPopoverProps = TPopoverBaseProps & TAriaRoleRequired & ({
|
|
|
146
149
|
* Called when the popover is dismissed via light dismiss
|
|
147
150
|
* (Escape, click outside).
|
|
148
151
|
*
|
|
149
|
-
* Required for `auto` and `hint` modes
|
|
150
|
-
*
|
|
151
|
-
* the
|
|
152
|
+
* Required for `auto` and `hint` modes so the consumer can update
|
|
153
|
+
* controlled intent after native dismissal. If `isOpen` remains `true`,
|
|
154
|
+
* the Popover remains natively closed until the prop changes.
|
|
152
155
|
*
|
|
153
156
|
* The `reason` field indicates how the dismiss occurred:
|
|
154
157
|
* - `'escape'`: the user pressed the Escape key.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/top-layer",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.4",
|
|
4
4
|
"description": "Low-level layering primitives based on popover and dialog",
|
|
5
5
|
"homepage": "https://atlassian.design/components/top-layer",
|
|
6
6
|
"repository": "https://bitbucket.org/atlassian/atlassian-frontend-mirror",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@atlaskit/browser-apis": "^1.2.0",
|
|
39
|
-
"@atlaskit/css": "^1.
|
|
39
|
+
"@atlaskit/css": "^1.1.0",
|
|
40
40
|
"@atlaskit/ds-lib": "^8.0.0",
|
|
41
|
-
"@atlaskit/layering": "^5.
|
|
41
|
+
"@atlaskit/layering": "^5.2.0",
|
|
42
42
|
"@atlaskit/react-compiler-gating": "^0.2.0",
|
|
43
|
-
"@atlaskit/tokens": "^16.
|
|
43
|
+
"@atlaskit/tokens": "^16.11.0",
|
|
44
44
|
"@babel/runtime": "^7.0.0",
|
|
45
45
|
"@compiled/react": "^1.0.2",
|
|
46
46
|
"bind-event-listener": "^3.0.0",
|
|
@@ -53,14 +53,19 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@af/integration-testing": "workspace:^",
|
|
55
55
|
"@af/visual-regression": "workspace:^",
|
|
56
|
-
"@atlaskit/button": "^25.
|
|
57
|
-
"@atlaskit/
|
|
58
|
-
"@atlaskit/
|
|
59
|
-
"@atlaskit/
|
|
56
|
+
"@atlaskit/button": "^25.3.0",
|
|
57
|
+
"@atlaskit/checkbox": "^19.2.0",
|
|
58
|
+
"@atlaskit/heading": "^7.2.0",
|
|
59
|
+
"@atlaskit/lozenge": "^15.4.0",
|
|
60
|
+
"@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
|
|
61
|
+
"@atlaskit/primitives": "^22.5.0",
|
|
62
|
+
"@atlaskit/select": "^22.11.0",
|
|
60
63
|
"@atlaskit/ssr": "workspace:^",
|
|
61
|
-
"@atlaskit/textfield": "^10.
|
|
64
|
+
"@atlaskit/textfield": "^10.2.0",
|
|
65
|
+
"@atlaskit/toggle": "^17.3.0",
|
|
62
66
|
"@atlassian/ssr-tests": "workspace:^",
|
|
63
67
|
"@atlassian/testing-library": "^0.11.0",
|
|
68
|
+
"@atlassian/workbench": "workspace:*",
|
|
64
69
|
"@testing-library/jest-dom": "^6.4.5",
|
|
65
70
|
"@types/raf-schd": "^4.0.1",
|
|
66
71
|
"react": "^19.0.0",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.isInSameLayer = isInSameLayer;
|
|
7
|
-
var topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
11
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
12
|
-
*/
|
|
13
|
-
function isInSameLayer(_ref) {
|
|
14
|
-
var element = _ref.element,
|
|
15
|
-
container = _ref.container;
|
|
16
|
-
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
17
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.selectors = void 0;
|
|
7
|
-
var selectors = exports.selectors = function () {
|
|
8
|
-
// Common exclusion filter applied to every selector.
|
|
9
|
-
var not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
10
|
-
var notDisabled = ":not([disabled])".concat(not);
|
|
11
|
-
var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled)];
|
|
12
|
-
return {
|
|
13
|
-
focusable: focusable.join(','),
|
|
14
|
-
// Build the `:focus` selector
|
|
15
|
-
focused: focusable.map(function (selector) {
|
|
16
|
-
return "".concat(selector, ":focus");
|
|
17
|
-
}).join(',')
|
|
18
|
-
};
|
|
19
|
-
}();
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns true when `element` and `container` resolve to the same nearest
|
|
5
|
-
* top-layer host (or both resolve to `null`, meaning the document layer).
|
|
6
|
-
*/
|
|
7
|
-
export function isInSameLayer({
|
|
8
|
-
element,
|
|
9
|
-
container
|
|
10
|
-
}) {
|
|
11
|
-
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
12
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const selectors = (() => {
|
|
2
|
-
// Common exclusion filter applied to every selector.
|
|
3
|
-
const not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
4
|
-
const notDisabled = `:not([disabled])${not}`;
|
|
5
|
-
const focusable = [`a[href]${not}`, `area[href]${not}`, `input${notDisabled}:not([type="hidden"])`, `select${notDisabled}`, `textarea${notDisabled}`, `button${notDisabled}`, `iframe${not}`, `audio[controls]${not}`, `video[controls]${not}`, `[contenteditable]:not([contenteditable="false"])${not}`, `[tabindex]${notDisabled}`];
|
|
6
|
-
return {
|
|
7
|
-
focusable: focusable.join(','),
|
|
8
|
-
// Build the `:focus` selector
|
|
9
|
-
focused: focusable.map(selector => `${selector}:focus`).join(',')
|
|
10
|
-
};
|
|
11
|
-
})();
|