@atlaskit/top-layer 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/dist/cjs/dialog/create-close-event.js +1 -2
- package/dist/cjs/dialog/dialog-content.js +33 -4
- package/dist/cjs/dialog-scroll-lock/index.js +17 -7
- package/dist/cjs/entry-points/dialog.js +6 -0
- package/dist/cjs/entry-points/popover.js +6 -0
- package/dist/cjs/entry-points/use-popover-id.js +3 -1
- package/dist/cjs/internal/get-aria-for-trigger.js +8 -3
- package/dist/cjs/internal/use-anchor-position-at-point.js +4 -2
- package/dist/cjs/internal/use-anchor-position.js +3 -2
- package/dist/cjs/internal/use-animated-visibility.js +233 -155
- package/dist/cjs/internal/use-focus-wrap.js +5 -3
- package/dist/cjs/internal/use-initial-focus.js +196 -50
- package/dist/cjs/internal/use-width-from-anchor.js +5 -4
- package/dist/cjs/popover/create-close-event.js +1 -2
- package/dist/cjs/popover/popover.js +58 -9
- package/dist/es2019/dialog/create-close-event.js +1 -2
- package/dist/es2019/dialog/dialog-content.js +33 -4
- package/dist/es2019/dialog-scroll-lock/index.js +18 -8
- package/dist/es2019/entry-points/dialog.js +1 -1
- package/dist/es2019/entry-points/popover.js +1 -1
- package/dist/es2019/entry-points/use-popover-id.js +3 -1
- package/dist/es2019/internal/get-aria-for-trigger.js +8 -3
- package/dist/es2019/internal/use-anchor-position-at-point.js +4 -2
- package/dist/es2019/internal/use-anchor-position.js +3 -2
- package/dist/es2019/internal/use-animated-visibility.js +222 -149
- package/dist/es2019/internal/use-focus-wrap.js +5 -4
- package/dist/es2019/internal/use-initial-focus.js +189 -45
- package/dist/es2019/internal/use-width-from-anchor.js +5 -4
- package/dist/es2019/popover/create-close-event.js +1 -2
- package/dist/es2019/popover/popover.js +59 -10
- package/dist/esm/dialog/create-close-event.js +1 -2
- package/dist/esm/dialog/dialog-content.js +33 -4
- package/dist/esm/dialog-scroll-lock/index.js +17 -8
- package/dist/esm/entry-points/dialog.js +1 -1
- package/dist/esm/entry-points/popover.js +1 -1
- package/dist/esm/entry-points/use-popover-id.js +3 -1
- package/dist/esm/internal/get-aria-for-trigger.js +8 -3
- package/dist/esm/internal/use-anchor-position-at-point.js +4 -2
- package/dist/esm/internal/use-anchor-position.js +3 -2
- package/dist/esm/internal/use-animated-visibility.js +235 -157
- package/dist/esm/internal/use-focus-wrap.js +5 -4
- package/dist/esm/internal/use-initial-focus.js +196 -51
- package/dist/esm/internal/use-width-from-anchor.js +5 -4
- package/dist/esm/popover/create-close-event.js +1 -2
- package/dist/esm/popover/popover.js +59 -10
- package/dist/types/animations/types.d.ts +0 -8
- package/dist/types/dialog/create-close-event.d.ts +1 -2
- package/dist/types/dialog/types.d.ts +9 -1
- package/dist/types/dialog-scroll-lock/index.d.ts +22 -6
- package/dist/types/entry-points/animations.d.ts +1 -1
- package/dist/types/entry-points/dialog.d.ts +1 -1
- package/dist/types/entry-points/popover.d.ts +1 -1
- package/dist/types/internal/get-aria-for-trigger.d.ts +8 -1
- package/dist/types/internal/use-anchor-position-at-point.d.ts +6 -1
- package/dist/types/internal/use-anchor-position.d.ts +14 -1
- package/dist/types/internal/use-animated-visibility.d.ts +48 -19
- package/dist/types/internal/use-focus-wrap.d.ts +6 -1
- package/dist/types/internal/use-initial-focus.d.ts +55 -4
- package/dist/types/internal/use-width-from-anchor.d.ts +3 -2
- package/dist/types/popover/create-close-event.d.ts +1 -2
- package/dist/types/popover/types.d.ts +13 -2
- package/dist/types-ts4.5/animations/types.d.ts +0 -8
- package/dist/types-ts4.5/dialog/create-close-event.d.ts +1 -2
- package/dist/types-ts4.5/dialog/types.d.ts +9 -1
- package/dist/types-ts4.5/dialog-scroll-lock/index.d.ts +22 -6
- package/dist/types-ts4.5/entry-points/animations.d.ts +1 -1
- package/dist/types-ts4.5/entry-points/dialog.d.ts +1 -1
- package/dist/types-ts4.5/entry-points/popover.d.ts +1 -1
- package/dist/types-ts4.5/internal/get-aria-for-trigger.d.ts +8 -1
- package/dist/types-ts4.5/internal/use-anchor-position-at-point.d.ts +6 -1
- package/dist/types-ts4.5/internal/use-anchor-position.d.ts +14 -1
- package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +48 -19
- package/dist/types-ts4.5/internal/use-focus-wrap.d.ts +6 -1
- package/dist/types-ts4.5/internal/use-initial-focus.d.ts +55 -4
- package/dist/types-ts4.5/internal/use-width-from-anchor.d.ts +3 -2
- package/dist/types-ts4.5/popover/create-close-event.d.ts +1 -2
- package/dist/types-ts4.5/popover/types.d.ts +13 -2
- package/package.json +1 -1
- package/create-close-event/package.json +0 -17
- package/dialog/create-close-event/package.json +0 -17
- package/dist/cjs/entry-points/create-close-event.js +0 -19
- package/dist/cjs/entry-points/dialog/create-close-event.js +0 -12
- package/dist/cjs/entry-points/popover/create-close-event.js +0 -12
- package/dist/es2019/entry-points/create-close-event.js +0 -9
- package/dist/es2019/entry-points/dialog/create-close-event.js +0 -1
- package/dist/es2019/entry-points/popover/create-close-event.js +0 -1
- package/dist/esm/entry-points/create-close-event.js +0 -9
- package/dist/esm/entry-points/dialog/create-close-event.js +0 -1
- package/dist/esm/entry-points/popover/create-close-event.js +0 -1
- package/dist/types/entry-points/create-close-event.d.ts +0 -11
- package/dist/types/entry-points/dialog/create-close-event.d.ts +0 -2
- package/dist/types/entry-points/popover/create-close-event.d.ts +0 -2
- package/dist/types-ts4.5/entry-points/create-close-event.d.ts +0 -11
- package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +0 -2
- package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +0 -2
- package/popover/create-close-event/package.json +0 -17
|
@@ -70,7 +70,8 @@ export function useAnchorPositionAtPoint(_ref) {
|
|
|
70
70
|
placement = _ref.placement,
|
|
71
71
|
getPoint = _ref.getPoint,
|
|
72
72
|
_ref$isEnabled = _ref.isEnabled,
|
|
73
|
-
isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled
|
|
73
|
+
isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled,
|
|
74
|
+
isOpen = _ref.isOpen;
|
|
74
75
|
var syntheticAnchorRef = useRef(null);
|
|
75
76
|
|
|
76
77
|
// `getPoint` is kept in a ref so the layout effect can call the
|
|
@@ -117,6 +118,7 @@ export function useAnchorPositionAtPoint(_ref) {
|
|
|
117
118
|
anchorRef: syntheticAnchorRef,
|
|
118
119
|
popoverRef: popoverRef,
|
|
119
120
|
placement: placement,
|
|
120
|
-
isEnabled: isEnabled
|
|
121
|
+
isEnabled: isEnabled,
|
|
122
|
+
isOpen: isOpen
|
|
121
123
|
});
|
|
122
124
|
}
|
|
@@ -214,7 +214,8 @@ export function useAnchorPosition(_ref3) {
|
|
|
214
214
|
_ref3$forceFallbackPo = _ref3.forceFallbackPositioning,
|
|
215
215
|
forceFallbackPositioning = _ref3$forceFallbackPo === void 0 ? false : _ref3$forceFallbackPo,
|
|
216
216
|
_ref3$isEnabled = _ref3.isEnabled,
|
|
217
|
-
isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled
|
|
217
|
+
isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled,
|
|
218
|
+
isOpen = _ref3.isOpen;
|
|
218
219
|
var id = useId();
|
|
219
220
|
// Stabilize `placement` object so the same object literal
|
|
220
221
|
// value returns an object with the same reference
|
|
@@ -474,5 +475,5 @@ export function useAnchorPosition(_ref3) {
|
|
|
474
475
|
popover.style.removeProperty('opacity');
|
|
475
476
|
});
|
|
476
477
|
return undoPositioning;
|
|
477
|
-
}, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id]);
|
|
478
|
+
}, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id, isOpen]);
|
|
478
479
|
}
|
|
@@ -1,8 +1,71 @@
|
|
|
1
1
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { bind } from 'bind-event-listener';
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { bind, bindAll } from 'bind-event-listener';
|
|
4
4
|
import { prefersReducedMotion } from './reduced-motion';
|
|
5
5
|
import { usePresetStyles } from './use-preset-styles';
|
|
6
|
+
/**
|
|
7
|
+
* Fires `onSettled` once when `transitionend` fires on `element`, or after
|
|
8
|
+
* `safetyNetMs` if the event is swallowed (mid-transition unmount, CSS
|
|
9
|
+
* mismatch, browser quirks). Returns a cleanup that cancels both paths.
|
|
10
|
+
*/
|
|
11
|
+
function runOnTransitionEndOrTimeout(_ref) {
|
|
12
|
+
var element = _ref.element,
|
|
13
|
+
safetyNetMs = _ref.safetyNetMs,
|
|
14
|
+
onSettled = _ref.onSettled;
|
|
15
|
+
var isSettled = false;
|
|
16
|
+
function teardown() {
|
|
17
|
+
isSettled = true;
|
|
18
|
+
clearTimeout(safetyNetId);
|
|
19
|
+
unbind();
|
|
20
|
+
}
|
|
21
|
+
function settle() {
|
|
22
|
+
if (isSettled) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
teardown();
|
|
26
|
+
onSettled();
|
|
27
|
+
}
|
|
28
|
+
var unbind = bind(element, {
|
|
29
|
+
type: 'transitionend',
|
|
30
|
+
listener: settle,
|
|
31
|
+
options: {
|
|
32
|
+
once: true
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
var safetyNetId = setTimeout(settle, safetyNetMs);
|
|
36
|
+
return teardown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Visibility lifecycle phase shared between `useAnimatedVisibility`,
|
|
41
|
+
* `useFocusWrap`, `useInitialFocus`, and the `Popover` / `Dialog`
|
|
42
|
+
* components.
|
|
43
|
+
*
|
|
44
|
+
* - `closed`: host element is unmounted. No DOM presence.
|
|
45
|
+
* - `entering`: host element is mounted and the CSS entry transition
|
|
46
|
+
* is playing. **Only emitted when animation is enabled.** With
|
|
47
|
+
* animation disabled the lifecycle goes directly `closed → open`.
|
|
48
|
+
* - `open`: host element is mounted and settled. No transition is
|
|
49
|
+
* playing.
|
|
50
|
+
* - `exiting`: `isOpen` has flipped to `false` but the host element
|
|
51
|
+
* is still mounted. Emitted while the CSS exit transition is playing
|
|
52
|
+
* (animation enabled) OR while we wait for the browser's
|
|
53
|
+
* `toggle`/`close` event to fire on the still-attached element so
|
|
54
|
+
* focus restoration can run (animation disabled).
|
|
55
|
+
*
|
|
56
|
+
* State machine:
|
|
57
|
+
* ```
|
|
58
|
+
* animation ON: closed → entering → open → exiting → closed
|
|
59
|
+
* animation OFF: closed → open → exiting → closed
|
|
60
|
+
* interrupts: entering → exiting (close mid-entry)
|
|
61
|
+
* exiting → entering (reopen mid-exit, animated)
|
|
62
|
+
* exiting → open (reopen mid-handshake, non-animated)
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* `phase !== 'closed'` is the canonical "host is currently in the DOM
|
|
66
|
+
* and on screen" predicate.
|
|
67
|
+
*/
|
|
68
|
+
|
|
6
69
|
/**
|
|
7
70
|
* Manages the children mount/unmount lifecycle around CSS exit transitions.
|
|
8
71
|
*
|
|
@@ -19,33 +82,27 @@ import { usePresetStyles } from './use-preset-styles';
|
|
|
19
82
|
*
|
|
20
83
|
* ## How it works
|
|
21
84
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
85
|
+
* The lifecycle is modeled as a `phase` discriminated union (`closed`,
|
|
86
|
+
* `entering`, `open`, `exiting`). The host element is rendered while
|
|
87
|
+
* `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
|
|
88
|
+
* while the CSS exit transition plays (or, with animation disabled,
|
|
89
|
+
* while we wait for the browser `toggle` / `close` event to fire on the
|
|
90
|
+
* still-attached element):
|
|
25
91
|
*
|
|
26
92
|
* ```
|
|
27
|
-
* isOpen:
|
|
28
|
-
*
|
|
93
|
+
* isOpen: true ──────────────── false
|
|
94
|
+
* phase: open ──────────────── exiting ─── (exit settle) ─── closed
|
|
29
95
|
* ```
|
|
30
96
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 1. **Animated close** (`willAnimate === true`):
|
|
34
|
-
* `isOpen` goes `false` → `showChildren` stays `true` → the CSS exit
|
|
35
|
-
* transition plays → `transitionend` fires → `showChildren` → `false`
|
|
36
|
-
* → `onExitFinish` fires. A timeout fallback ensures we still unmount
|
|
37
|
-
* if `transitionend` never fires (e.g. browser quirks, zero-duration).
|
|
38
|
-
*
|
|
39
|
-
* 2. **Non-animated close** (`willAnimate === false`):
|
|
40
|
-
* `isOpen` goes `false` → `showChildren` → `false` synchronously during
|
|
41
|
-
* the same render → `onExitFinish` fires in a follow-up effect.
|
|
97
|
+
* Every effect dispatches on the single `phase` value instead of a
|
|
98
|
+
* cross-product of booleans.
|
|
42
99
|
*/
|
|
43
|
-
export function useAnimatedVisibility(
|
|
44
|
-
var isOpen =
|
|
45
|
-
animate =
|
|
46
|
-
elementRef =
|
|
47
|
-
onEnterFinish =
|
|
48
|
-
onExitFinish =
|
|
100
|
+
export function useAnimatedVisibility(_ref2) {
|
|
101
|
+
var isOpen = _ref2.isOpen,
|
|
102
|
+
animate = _ref2.animate,
|
|
103
|
+
elementRef = _ref2.elementRef,
|
|
104
|
+
onEnterFinish = _ref2.onEnterFinish,
|
|
105
|
+
onExitFinish = _ref2.onExitFinish;
|
|
49
106
|
// Resolve the animation preset and inject its CSS (idempotent).
|
|
50
107
|
// Returns `null` when `animate` is falsy.
|
|
51
108
|
var preset = usePresetStyles({
|
|
@@ -56,29 +113,37 @@ export function useAnimatedVisibility(_ref) {
|
|
|
56
113
|
// or the user has enabled the "prefers reduced motion" OS setting.
|
|
57
114
|
var willAnimate = Boolean(animate) && !prefersReducedMotion();
|
|
58
115
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// true here and the effect below will set it to false
|
|
72
|
-
// after `transitionend`.
|
|
73
|
-
var _useState = useState(isOpen),
|
|
116
|
+
// `phase` is the single source of truth for the visibility lifecycle.
|
|
117
|
+
// We promote `closed → entering` (animated) or `closed → open`
|
|
118
|
+
// (non-animated) synchronously during render so the host element mounts
|
|
119
|
+
// in the same commit as the open intent (React supports a single
|
|
120
|
+
// setState during render as a synchronous re-render before paint).
|
|
121
|
+
// All other transitions are driven by phase-specific effects below.
|
|
122
|
+
var _useState = useState(function () {
|
|
123
|
+
if (!isOpen) {
|
|
124
|
+
return 'closed';
|
|
125
|
+
}
|
|
126
|
+
return willAnimate ? 'entering' : 'open';
|
|
127
|
+
}),
|
|
74
128
|
_useState2 = _slicedToArray(_useState, 2),
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (isOpen &&
|
|
78
|
-
|
|
129
|
+
phase = _useState2[0],
|
|
130
|
+
setPhase = _useState2[1];
|
|
131
|
+
if (isOpen && phase === 'closed') {
|
|
132
|
+
setPhase(willAnimate ? 'entering' : 'open');
|
|
79
133
|
}
|
|
80
|
-
if (!isOpen &&
|
|
81
|
-
|
|
134
|
+
if (!isOpen && (phase === 'open' || phase === 'entering')) {
|
|
135
|
+
// Close intent. Move to `exiting` to gate the unmount on either
|
|
136
|
+
// the exit transition (animated) or the toggle/close event
|
|
137
|
+
// (non-animated). Includes interrupted entry (`entering → exiting`).
|
|
138
|
+
setPhase('exiting');
|
|
139
|
+
}
|
|
140
|
+
if (isOpen && phase === 'exiting') {
|
|
141
|
+
// Reopen mid-exit. With animation, jump back to `entering` and let
|
|
142
|
+
// the entry transition restart from current visual state. Without
|
|
143
|
+
// animation, jump straight to `open` (the handshake we were waiting
|
|
144
|
+
// for is no longer relevant; the consumer wants the host visible
|
|
145
|
+
// again synchronously).
|
|
146
|
+
setPhase(willAnimate ? 'entering' : 'open');
|
|
82
147
|
}
|
|
83
148
|
|
|
84
149
|
// Keep stable refs to the latest callbacks so effects and timeout closures
|
|
@@ -88,152 +153,165 @@ export function useAnimatedVisibility(_ref) {
|
|
|
88
153
|
var onExitFinishRef = useRef(onExitFinish);
|
|
89
154
|
onExitFinishRef.current = onExitFinish;
|
|
90
155
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// A fallback timeout (enterDurationMs + 50ms buffer) ensures it always
|
|
97
|
-
// fires even if `transitionend` is never emitted (e.g. zero-duration,
|
|
98
|
-
// browser quirk, or reduced motion).
|
|
156
|
+
// `entering` (animation enabled):
|
|
157
|
+
// Wait for the entry transition to settle, then move to `open` and fire
|
|
158
|
+
// `onEnterFinish`. A fallback timeout (enterDurationMs + 50ms buffer)
|
|
159
|
+
// guarantees the settle path runs even if `transitionend` is swallowed
|
|
160
|
+
// (zero-duration, browser quirk, mid-transition unmount).
|
|
99
161
|
useEffect(function () {
|
|
100
162
|
var _preset$enterDuration;
|
|
101
|
-
if (
|
|
163
|
+
if (phase !== 'entering') {
|
|
102
164
|
return;
|
|
103
165
|
}
|
|
104
166
|
var element = elementRef.current;
|
|
105
167
|
if (!element) {
|
|
106
168
|
return;
|
|
107
169
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
clearTimeout(fallbackId);
|
|
122
|
-
unbind();
|
|
123
|
-
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
124
|
-
}
|
|
125
|
-
var fallbackId = setTimeout(handleTransitionEnd, ((_preset$enterDuration = preset === null || preset === void 0 ? void 0 : preset.enterDurationMs) !== null && _preset$enterDuration !== void 0 ? _preset$enterDuration : 0) + 50);
|
|
126
|
-
var unbind = bind(element, {
|
|
127
|
-
type: 'transitionend',
|
|
128
|
-
listener: handleTransitionEnd,
|
|
129
|
-
options: {
|
|
130
|
-
once: true
|
|
170
|
+
return runOnTransitionEndOrTimeout({
|
|
171
|
+
element: element,
|
|
172
|
+
safetyNetMs: ((_preset$enterDuration = preset === null || preset === void 0 ? void 0 : preset.enterDurationMs) !== null && _preset$enterDuration !== void 0 ? _preset$enterDuration : 0) + 50,
|
|
173
|
+
onSettled: function onSettled() {
|
|
174
|
+
var _onEnterFinishRef$cur;
|
|
175
|
+
// Fire the consumer callback before transitioning phase so any
|
|
176
|
+
// state the consumer reads (refs, DOM nodes) is still attached
|
|
177
|
+
// in the same synchronous tick, regardless of React batching
|
|
178
|
+
// semantics.
|
|
179
|
+
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
180
|
+
setPhase(function (current) {
|
|
181
|
+
return current === 'entering' ? 'open' : current;
|
|
182
|
+
});
|
|
131
183
|
}
|
|
132
184
|
});
|
|
133
|
-
|
|
134
|
-
hasFired = true;
|
|
135
|
-
clearTimeout(fallbackId);
|
|
136
|
-
unbind();
|
|
137
|
-
};
|
|
138
|
-
}, [isOpen, showChildren, willAnimate, preset === null || preset === void 0 ? void 0 : preset.enterDurationMs, elementRef]);
|
|
185
|
+
}, [phase, preset === null || preset === void 0 ? void 0 : preset.enterDurationMs, elementRef]);
|
|
139
186
|
|
|
140
|
-
// Non-animated entry callback
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
187
|
+
// Non-animated entry callback:
|
|
188
|
+
// When animation is disabled, the lifecycle goes `closed → open`
|
|
189
|
+
// directly with no `entering` phase. Fire `onEnterFinish` on the
|
|
190
|
+
// transition into `open` from the non-animated path. We detect this
|
|
191
|
+
// via a previous-phase ref to avoid firing on the animated path's
|
|
192
|
+
// `entering → open` transition (which has its own callback above).
|
|
193
|
+
var prevPhaseForEntryRef = useRef('closed');
|
|
145
194
|
useEffect(function () {
|
|
146
|
-
var
|
|
147
|
-
|
|
148
|
-
|
|
195
|
+
var prevPhase = prevPhaseForEntryRef.current;
|
|
196
|
+
prevPhaseForEntryRef.current = phase;
|
|
197
|
+
|
|
198
|
+
// Animated entry settle goes through `entering` and already fired
|
|
199
|
+
// onEnterFinish above; only handle the non-animated direct jumps.
|
|
200
|
+
if (prevPhase !== 'open' && phase === 'open' && prevPhase !== 'entering') {
|
|
149
201
|
var _onEnterFinishRef$cur2;
|
|
150
202
|
(_onEnterFinishRef$cur2 = onEnterFinishRef.current) === null || _onEnterFinishRef$cur2 === void 0 || _onEnterFinishRef$cur2.call(onEnterFinishRef);
|
|
151
203
|
}
|
|
152
|
-
}, [
|
|
204
|
+
}, [phase]);
|
|
153
205
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// Listens for `transitionend` on the element to know when the CSS exit
|
|
162
|
-
// transition has finished, then unmounts children and fires onExitFinish.
|
|
163
|
-
//
|
|
164
|
-
// Fallback timeout: `transitionend` can fail to fire in edge cases
|
|
165
|
-
// (e.g. element removed from top layer by browser, zero-duration
|
|
166
|
-
// transition, interrupted animation). The timeout is set to the
|
|
167
|
-
// preset's exit duration + 50ms buffer to guarantee we always unmount.
|
|
206
|
+
// `exiting` (animation enabled):
|
|
207
|
+
// Wait for the exit transition to settle, then unmount the host element
|
|
208
|
+
// (set phase back to `closed`) and fire `onExitFinish`. The fallback
|
|
209
|
+
// timeout (exitDurationMs + 50ms buffer) guarantees we always unmount
|
|
210
|
+
// even if `transitionend` is never emitted (element removed from the
|
|
211
|
+
// top layer, zero-duration transition, interrupted animation).
|
|
168
212
|
useEffect(function () {
|
|
169
213
|
var _preset$exitDurationM;
|
|
170
|
-
|
|
171
|
-
if (isOpen || !showChildren || !willAnimate) {
|
|
214
|
+
if (phase !== 'exiting' || !willAnimate) {
|
|
172
215
|
return;
|
|
173
216
|
}
|
|
174
217
|
var element = elementRef.current;
|
|
175
218
|
if (!element) {
|
|
176
219
|
return;
|
|
177
220
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
setShowChildren(false);
|
|
194
|
-
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Fallback: unmount even if transitionend never fires
|
|
198
|
-
var fallbackId = setTimeout(handleTransitionEnd, ((_preset$exitDurationM = preset === null || preset === void 0 ? void 0 : preset.exitDurationMs) !== null && _preset$exitDurationM !== void 0 ? _preset$exitDurationM : 0) + 50);
|
|
199
|
-
|
|
200
|
-
// Primary: listen for the CSS transition to complete
|
|
201
|
-
var unbind = bind(element, {
|
|
202
|
-
type: 'transitionend',
|
|
203
|
-
listener: handleTransitionEnd,
|
|
204
|
-
options: {
|
|
205
|
-
once: true
|
|
221
|
+
return runOnTransitionEndOrTimeout({
|
|
222
|
+
element: element,
|
|
223
|
+
safetyNetMs: ((_preset$exitDurationM = preset === null || preset === void 0 ? void 0 : preset.exitDurationMs) !== null && _preset$exitDurationM !== void 0 ? _preset$exitDurationM : 0) + 50,
|
|
224
|
+
onSettled: function onSettled() {
|
|
225
|
+
var _onExitFinishRef$curr;
|
|
226
|
+
// Fire the consumer callback before transitioning phase to
|
|
227
|
+
// `closed`. The phase change drives the host element unmount,
|
|
228
|
+
// and consumers expect to be able to read the element (or a
|
|
229
|
+
// forwarded ref to it) inside `onExitFinish`. Doing this in
|
|
230
|
+
// the safe order avoids relying on React's automatic batching
|
|
231
|
+
// to keep the element attached across the callback.
|
|
232
|
+
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
|
|
233
|
+
setPhase(function (current) {
|
|
234
|
+
return current === 'exiting' ? 'closed' : current;
|
|
235
|
+
});
|
|
206
236
|
}
|
|
207
237
|
});
|
|
238
|
+
}, [phase, willAnimate, preset === null || preset === void 0 ? void 0 : preset.exitDurationMs, elementRef]);
|
|
208
239
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
240
|
+
// `exiting` (animation disabled):
|
|
241
|
+
// Wait for the browser's `toggle` (closed) event (for `<div popover>`)
|
|
242
|
+
// or `close` event (for `<dialog>`) to fire before unmounting. This
|
|
243
|
+
// guarantees:
|
|
244
|
+
// - the consumer's toggle/close listener (close-reason capture,
|
|
245
|
+
// nested focus restoration, `onClose` notification) runs against
|
|
246
|
+
// the still-attached element,
|
|
247
|
+
// - the browser's native focus restoration triggered by
|
|
248
|
+
// `hidePopover()` / `<dialog>.close()` has fired,
|
|
249
|
+
// before React unmounts the node. A naive `setTimeout(0)` race
|
|
250
|
+
// occasionally beats the `toggle` task in Chromium, causing focus
|
|
251
|
+
// restoration to drop (the toggle event arrives at a detached element).
|
|
218
252
|
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
253
|
+
// `useLayoutEffect` (not `useEffect`) so the listener is bound in the
|
|
254
|
+
// same commit as the consumer's show/hide layout effect that calls
|
|
255
|
+
// `hidePopover()` / `dialog.close()`. The browser queues the resulting
|
|
256
|
+
// `toggle`/`close` event as a task, which cannot run until the JS
|
|
257
|
+
// frame (including all effect flushes) completes, so the listener is
|
|
258
|
+
// always attached before the event fires. Promotion to layout timing
|
|
259
|
+
// removes any theoretical gap and future-proofs the ordering against
|
|
260
|
+
// effect reordering. Safe under SSR: `phase` starts at `'closed'`, so
|
|
261
|
+
// the body short-circuits and no DOM access happens on the server.
|
|
222
262
|
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
263
|
+
// `onExitFinish` is NOT called here. It is decoupled from the unmount
|
|
264
|
+
// handshake and fired by the dedicated non-animated exit-callback
|
|
265
|
+
// effect below, so consumers observe a synchronous "exit finished"
|
|
266
|
+
// signal that matches `isOpen` flipping to `false`.
|
|
267
|
+
useLayoutEffect(function () {
|
|
268
|
+
if (phase !== 'exiting' || willAnimate) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
var element = elementRef.current;
|
|
272
|
+
if (!element) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
function unmount() {
|
|
276
|
+
setPhase(function (current) {
|
|
277
|
+
return current === 'exiting' ? 'closed' : current;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return bindAll(element, [{
|
|
281
|
+
type: 'toggle',
|
|
282
|
+
listener: function listener(event) {
|
|
283
|
+
if (event.newState === 'closed') {
|
|
284
|
+
unmount();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}, {
|
|
288
|
+
type: 'close',
|
|
289
|
+
listener: unmount
|
|
290
|
+
}]);
|
|
291
|
+
}, [phase, willAnimate, elementRef]);
|
|
292
|
+
|
|
293
|
+
// Non-animated exit callback:
|
|
294
|
+
// When animation is disabled there is no `transitionend` to wait for,
|
|
295
|
+
// so fire `onExitFinish` on the transition into `exiting`. The actual
|
|
296
|
+
// host-element unmount is gated on the browser `toggle`/`close` event
|
|
297
|
+
// by the effect above; `onExitFinish` is observably decoupled from
|
|
298
|
+
// that handshake so consumers see the callback fire at the same time
|
|
299
|
+
// `isOpen` flipped to `false`.
|
|
300
|
+
var prevPhaseForExitRef = useRef('closed');
|
|
227
301
|
useEffect(function () {
|
|
228
|
-
var
|
|
229
|
-
|
|
230
|
-
|
|
302
|
+
var prevPhase = prevPhaseForExitRef.current;
|
|
303
|
+
prevPhaseForExitRef.current = phase;
|
|
304
|
+
|
|
305
|
+
// Only fire on the non-animated transition into `exiting`. The
|
|
306
|
+
// animated path's `exiting → closed` settle already fires
|
|
307
|
+
// `onExitFinish` from `runOnTransitionEndOrTimeout`.
|
|
308
|
+
if (prevPhase !== 'exiting' && phase === 'exiting' && !willAnimate) {
|
|
231
309
|
var _onExitFinishRef$curr2;
|
|
232
310
|
(_onExitFinishRef$curr2 = onExitFinishRef.current) === null || _onExitFinishRef$curr2 === void 0 || _onExitFinishRef$curr2.call(onExitFinishRef);
|
|
233
311
|
}
|
|
234
|
-
}, [
|
|
312
|
+
}, [phase, willAnimate]);
|
|
235
313
|
return {
|
|
236
|
-
|
|
314
|
+
phase: phase,
|
|
237
315
|
preset: preset
|
|
238
316
|
};
|
|
239
317
|
}
|
|
@@ -4,7 +4,6 @@ import { getFirstFocusable } from '../focus/get-first-focusable';
|
|
|
4
4
|
import { getLastFocusable } from '../focus/get-last-focusable';
|
|
5
5
|
import { getNextFocusable } from '../focus/get-next-focusable';
|
|
6
6
|
import { isNestedLayerFocused } from '../focus/is-nested-layer-focused';
|
|
7
|
-
|
|
8
7
|
/**
|
|
9
8
|
* Roles that require focus wrapping per WAI-ARIA APG.
|
|
10
9
|
*
|
|
@@ -45,10 +44,12 @@ function roleRequiresFocusWrap(role) {
|
|
|
45
44
|
*/
|
|
46
45
|
export function useFocusWrap(_ref) {
|
|
47
46
|
var elementRef = _ref.elementRef,
|
|
48
|
-
role = _ref.role
|
|
47
|
+
role = _ref.role,
|
|
48
|
+
phase = _ref.phase;
|
|
49
|
+
var isVisible = phase !== 'closed';
|
|
49
50
|
useEffect(function () {
|
|
50
51
|
var element = elementRef.current;
|
|
51
|
-
if (!element || !roleRequiresFocusWrap(role)) {
|
|
52
|
+
if (!element || !roleRequiresFocusWrap(role) || !isVisible) {
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
var unbind = bind(element, {
|
|
@@ -99,5 +100,5 @@ export function useFocusWrap(_ref) {
|
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
102
|
return unbind;
|
|
102
|
-
}, [elementRef, role]);
|
|
103
|
+
}, [elementRef, role, isVisible]);
|
|
103
104
|
}
|