@atlaskit/top-layer 4.4.3 → 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 +9 -0
- package/dist/cjs/dialog/dialog-content.js +57 -47
- 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/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/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/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 +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @atlaskit/top-layer
|
|
2
2
|
|
|
3
|
+
## 4.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`610d564dbc5dd`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/610d564dbc5dd) -
|
|
8
|
+
Behind `platform-dst-top-layer`, harden the Popover and Dialog animation lifecycle state machine
|
|
9
|
+
and completion callbacks, restore focus before controlled Popover closure removes native
|
|
10
|
+
listeners, and preserve Modal Dialog open-complete callbacks across reopen cycles.
|
|
11
|
+
|
|
3
12
|
## 4.4.3
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* dialog-content.tsx generated by @compiled/babel-plugin
|
|
1
|
+
/* dialog-content.tsx generated by @compiled/babel-plugin v3.0.1 */
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
@@ -85,6 +85,13 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
85
85
|
var ownRef = (0, _react.useRef)(null);
|
|
86
86
|
var combinedRef = (0, _mergeRefs.default)([ownRef, ref]);
|
|
87
87
|
var closeReasonRef = (0, _react.useRef)(null);
|
|
88
|
+
var dismissedByRef = (0, _react.useRef)(dismissedBy);
|
|
89
|
+
var onCloseRef = (0, _react.useRef)(onClose);
|
|
90
|
+
// Keep native event handlers stable while ensuring they read the latest committed props.
|
|
91
|
+
(0, _react.useLayoutEffect)(function () {
|
|
92
|
+
dismissedByRef.current = dismissedBy;
|
|
93
|
+
onCloseRef.current = onClose;
|
|
94
|
+
}, [dismissedBy, onClose]);
|
|
88
95
|
var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
|
|
89
96
|
isOpen: isOpen,
|
|
90
97
|
shouldAnimate: shouldAnimate,
|
|
@@ -93,11 +100,52 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
93
100
|
onExitFinish: onExitFinish
|
|
94
101
|
}),
|
|
95
102
|
phase = _useAnimatedVisibilit.phase,
|
|
103
|
+
isMounted = _useAnimatedVisibilit.isMounted,
|
|
104
|
+
onBeforeToggle = _useAnimatedVisibilit.onBeforeToggle,
|
|
96
105
|
onToggle = _useAnimatedVisibilit.onToggle;
|
|
97
106
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
107
|
+
// Ordering is important: bind native lifecycle listeners before the later
|
|
108
|
+
// layout effect calls showModal() or close(), because `beforetoggle` fires
|
|
109
|
+
// synchronously during those calls.
|
|
110
|
+
(0, _react.useLayoutEffect)(function () {
|
|
111
|
+
if (!isMounted) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
var dialog = ownRef.current;
|
|
115
|
+
if (!dialog) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
return (0, _bindEventListener.bindAll)(dialog, [{
|
|
119
|
+
type: 'beforetoggle',
|
|
120
|
+
listener: onBeforeToggle
|
|
121
|
+
}, {
|
|
122
|
+
// Browsers retarget clicks on ::backdrop to the <dialog> element.
|
|
123
|
+
// Close on backdrop click when outside-click dismissal is enabled.
|
|
124
|
+
type: 'click',
|
|
125
|
+
listener: function listener(event) {
|
|
126
|
+
if (event.target === event.currentTarget && dismissedByRef.current === 'escape-and-outside-click') {
|
|
127
|
+
closeReasonRef.current = 'overlay-click';
|
|
128
|
+
dialog.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
type: 'toggle',
|
|
133
|
+
listener: function listener(event) {
|
|
134
|
+
if (event.newState === 'closed') {
|
|
135
|
+
var reason = closeReasonRef.current;
|
|
136
|
+
closeReasonRef.current = null;
|
|
137
|
+
// A reason is only set for user-initiated closes. When React closes the
|
|
138
|
+
// dialog because isOpen changed, the consumer already knows.
|
|
139
|
+
if (reason) {
|
|
140
|
+
onCloseRef.current({
|
|
141
|
+
reason: reason
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
onToggle(event);
|
|
146
|
+
}
|
|
147
|
+
}]);
|
|
148
|
+
}, [isMounted, onBeforeToggle, onToggle]);
|
|
101
149
|
|
|
102
150
|
// Native `<dialog>.showModal()` traps focus but wraps through `<body>` at
|
|
103
151
|
// the boundary (A → B → C → body → A). This hook intercepts Tab to wrap
|
|
@@ -143,7 +191,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
143
191
|
// See notes/decisions/safari-escape-nested-popover-in-dialog.md
|
|
144
192
|
var _useSafariEscapeFix = (0, _useSafariEscapeFix2.useSafariEscapeFix)({
|
|
145
193
|
dialogRef: ownRef,
|
|
146
|
-
isVisible:
|
|
194
|
+
isVisible: isMounted
|
|
147
195
|
}),
|
|
148
196
|
shouldIgnoreEscape = _useSafariEscapeFix.shouldIgnoreEscape;
|
|
149
197
|
|
|
@@ -176,49 +224,11 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
176
224
|
closeReasonRef.current = 'escape';
|
|
177
225
|
}, [dismissedBy, shouldIgnoreEscape]);
|
|
178
226
|
|
|
179
|
-
// Handle backdrop interaction
|
|
180
|
-
// Attached via bind-event-listener rather than a React prop so we avoid
|
|
181
|
-
// a11y lint suppressions on the <dialog> element.
|
|
182
|
-
// The click listener provides light dismiss consistently across browsers.
|
|
183
|
-
// `isVisible` is in deps so the listener re-binds to the new <dialog>
|
|
184
|
-
// element after a host unmount / remount cycle.
|
|
185
|
-
(0, _react.useLayoutEffect)(function () {
|
|
186
|
-
var dialog = ownRef.current;
|
|
187
|
-
if (!dialog) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
return (0, _bindEventListener.bindAll)(dialog, [{
|
|
191
|
-
type: 'click',
|
|
192
|
-
listener: function listener(event) {
|
|
193
|
-
if (event.target === event.currentTarget && dismissedBy === 'escape-and-outside-click') {
|
|
194
|
-
closeReasonRef.current = 'overlay-click';
|
|
195
|
-
// Calls native close because this is emulating the native light-dismiss behavior
|
|
196
|
-
dialog.close();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}, {
|
|
200
|
-
type: 'toggle',
|
|
201
|
-
listener: function listener(event) {
|
|
202
|
-
if (event.newState === 'closed') {
|
|
203
|
-
var reason = closeReasonRef.current;
|
|
204
|
-
closeReasonRef.current = null;
|
|
205
|
-
if (reason) {
|
|
206
|
-
onClose({
|
|
207
|
-
reason: reason
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Visibility cleanup runs after close-reason handling.
|
|
213
|
-
onToggle(event);
|
|
214
|
-
}
|
|
215
|
-
}]);
|
|
216
|
-
}, [dismissedBy, onClose, onToggle, isVisible]);
|
|
217
|
-
|
|
218
227
|
// Unmount the `<dialog>` once exit completes so it does not leave an
|
|
219
|
-
// empty `role="dialog"` element in the accessibility tree.
|
|
220
|
-
//
|
|
221
|
-
|
|
228
|
+
// empty `role="dialog"` element in the accessibility tree. A later open
|
|
229
|
+
// remounts a fresh element, so the listener-binding and native-visibility
|
|
230
|
+
// effects run against that element.
|
|
231
|
+
if (!isMounted) {
|
|
222
232
|
return null;
|
|
223
233
|
}
|
|
224
234
|
var shouldAnimateBackdrop = !shouldHideBackdrop && shouldAnimate;
|
|
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
});
|
|
7
7
|
exports.useAnimatedVisibility = useAnimatedVisibility;
|
|
8
8
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
10
|
var _react = require("react");
|
|
10
11
|
var _reducedMotion = require("./reduced-motion");
|
|
12
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
13
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
11
14
|
function runCancellableSettlement(_ref) {
|
|
12
15
|
var schedule = _ref.schedule,
|
|
13
16
|
onSettled = _ref.onSettled;
|
|
@@ -19,6 +22,7 @@ function runCancellableSettlement(_ref) {
|
|
|
19
22
|
onSettled();
|
|
20
23
|
});
|
|
21
24
|
return function () {
|
|
25
|
+
// Prevent an interrupted or unmounted lifecycle from firing a stale callback.
|
|
22
26
|
isCancelled = true;
|
|
23
27
|
};
|
|
24
28
|
}
|
|
@@ -41,6 +45,13 @@ function runOnAnimationsSettled(_ref2) {
|
|
|
41
45
|
}
|
|
42
46
|
});
|
|
43
47
|
}
|
|
48
|
+
function isNativeElementOpen(_ref3) {
|
|
49
|
+
var element = _ref3.element;
|
|
50
|
+
if (element instanceof HTMLDialogElement) {
|
|
51
|
+
return element.open;
|
|
52
|
+
}
|
|
53
|
+
return element.matches(':popover-open');
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
57
|
* Visibility lifecycle phase shared between `useAnimatedVisibility`,
|
|
@@ -53,11 +64,11 @@ function runOnAnimationsSettled(_ref2) {
|
|
|
53
64
|
* animation disabled the lifecycle goes directly `closed → open`.
|
|
54
65
|
* - `open`: host element is mounted and settled. No transition is
|
|
55
66
|
* playing.
|
|
56
|
-
* - `exiting`:
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* focus restoration can
|
|
67
|
+
* - `exiting`: a native close has begun but the host element remains
|
|
68
|
+
* mounted. Usually this follows `isOpen` changing to `false`, but it can
|
|
69
|
+
* also begin with a browser-initiated dismissal while controlled intent
|
|
70
|
+
* is still open. With animation disabled, this phase lasts until the
|
|
71
|
+
* browser's closed `toggle` fires so native focus restoration can finish.
|
|
61
72
|
*
|
|
62
73
|
* State machine:
|
|
63
74
|
* ```
|
|
@@ -67,140 +78,253 @@ function runOnAnimationsSettled(_ref2) {
|
|
|
67
78
|
* exiting → entering (reopen mid-exit, animated)
|
|
68
79
|
* exiting → open (reopen mid-handshake, non-animated)
|
|
69
80
|
* ```
|
|
70
|
-
*
|
|
71
|
-
* `phase !== 'closed'` is the canonical "host is currently in the DOM
|
|
72
|
-
* and on screen" predicate.
|
|
73
81
|
*/
|
|
74
82
|
|
|
83
|
+
function getInitialVisibilityState(_ref4) {
|
|
84
|
+
var isOpen = _ref4.isOpen,
|
|
85
|
+
willAnimate = _ref4.willAnimate;
|
|
86
|
+
if (!isOpen) {
|
|
87
|
+
return {
|
|
88
|
+
phase: 'closed',
|
|
89
|
+
controlledIntent: 'closed'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (willAnimate) {
|
|
93
|
+
return {
|
|
94
|
+
phase: 'entering',
|
|
95
|
+
controlledIntent: 'open'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
phase: 'open',
|
|
100
|
+
controlledIntent: 'open'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/**
|
|
76
|
-
*
|
|
105
|
+
* Applies one lifecycle event to the current visibility phase.
|
|
77
106
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
* Events that are stale for the current phase are ignored. This allows
|
|
108
|
+
* animation callbacks and native close events to settle safely after an
|
|
109
|
+
* interrupted lifecycle.
|
|
110
|
+
*/
|
|
111
|
+
function getNextVisibilityState(state, action) {
|
|
112
|
+
if (action.type === 'open-requested') {
|
|
113
|
+
var shouldEnter = state.phase === 'closed' || state.phase === 'exiting' && state.controlledIntent === 'closed';
|
|
114
|
+
if (shouldEnter && action.willAnimate) {
|
|
115
|
+
return {
|
|
116
|
+
phase: 'entering',
|
|
117
|
+
controlledIntent: 'open'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (shouldEnter) {
|
|
121
|
+
return {
|
|
122
|
+
phase: 'open',
|
|
123
|
+
controlledIntent: 'open'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
127
|
+
controlledIntent: 'open'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (action.type === 'close-requested') {
|
|
131
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
132
|
+
controlledIntent: 'closed'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (action.type === 'exit-started') {
|
|
136
|
+
if (state.phase !== 'open' && state.phase !== 'entering') {
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
140
|
+
phase: 'exiting'
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (action.type === 'entry-settled') {
|
|
144
|
+
if (state.phase !== 'entering') {
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
148
|
+
phase: 'open'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (action.type === 'exit-settled') {
|
|
152
|
+
if (state.phase !== 'exiting') {
|
|
153
|
+
return state;
|
|
154
|
+
}
|
|
155
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
156
|
+
phase: 'closed'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Coordinates controlled intent, native visibility, lifecycle phase, and host
|
|
164
|
+
* mounting around CSS exit transitions. The canonical contract is documented in
|
|
165
|
+
* `notes/architecture/animations.md#canonical-visibility-lifecycle-contract`.
|
|
88
166
|
*
|
|
89
|
-
*
|
|
167
|
+
* Used by both `Popover` and `Dialog` to share the same lifecycle logic. Those
|
|
168
|
+
* components own native event binding, show and hide commands, and
|
|
169
|
+
* component-specific event handling. This hook returns stable `onBeforeToggle`
|
|
170
|
+
* and `onToggle` callbacks that advance lifecycle phase and host mounting.
|
|
90
171
|
*
|
|
91
|
-
* The
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* while the CSS exit transition plays (or, with animation disabled,
|
|
95
|
-
* while we wait for the browser's closing `toggle` event to fire on the
|
|
96
|
-
* still-attached element):
|
|
172
|
+
* The host element is rendered while `phase !== 'closed'`. The `exiting`
|
|
173
|
+
* phase keeps it mounted until its CSS animation settles or, with animation
|
|
174
|
+
* disabled, until the browser reports that the native element has closed.
|
|
97
175
|
*
|
|
98
176
|
* ```
|
|
99
177
|
* isOpen: true ──────────────── false
|
|
100
178
|
* phase: open ──────────────── exiting ─── (exit settle) ─── closed
|
|
101
179
|
* ```
|
|
102
|
-
*
|
|
103
|
-
* Every effect dispatches on the single `phase` value instead of a
|
|
104
|
-
* cross-product of booleans.
|
|
105
180
|
*/
|
|
106
|
-
function useAnimatedVisibility(
|
|
107
|
-
var isOpen =
|
|
108
|
-
shouldAnimate =
|
|
109
|
-
elementRef =
|
|
110
|
-
onEnterFinish =
|
|
111
|
-
onExitFinish =
|
|
181
|
+
function useAnimatedVisibility(_ref5) {
|
|
182
|
+
var isOpen = _ref5.isOpen,
|
|
183
|
+
shouldAnimate = _ref5.shouldAnimate,
|
|
184
|
+
elementRef = _ref5.elementRef,
|
|
185
|
+
onEnterFinish = _ref5.onEnterFinish,
|
|
186
|
+
onExitFinish = _ref5.onExitFinish;
|
|
112
187
|
var willAnimate = shouldAnimate && !(0, _reducedMotion.prefersReducedMotion)();
|
|
113
|
-
var
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return willAnimate ? 'entering' : 'open';
|
|
123
|
-
}),
|
|
124
|
-
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
|
|
125
|
-
phase = _useState2[0],
|
|
126
|
-
setPhase = _useState2[1];
|
|
127
|
-
|
|
128
|
-
// Rearm only after controlled state and the native lifecycle have both
|
|
129
|
-
// reached closed. Resetting as soon as `isOpen` becomes false can erase a
|
|
130
|
-
// closing toggle that was handled in the same React batch.
|
|
131
|
-
if (!isOpen && phase === 'closed') {
|
|
132
|
-
isNativeClosedRef.current = false;
|
|
133
|
-
}
|
|
134
|
-
if (isOpen && phase === 'closed' && !isNativeClosedRef.current) {
|
|
135
|
-
setPhase(willAnimate ? 'entering' : 'open');
|
|
136
|
-
}
|
|
137
|
-
if (!isOpen && (phase === 'open' || phase === 'entering')) {
|
|
138
|
-
// Close intent. Move to `exiting` to gate the unmount on either
|
|
139
|
-
// the exit transition (animated) or the closing toggle event
|
|
140
|
-
// (non-animated). Includes interrupted entry (`entering → exiting`).
|
|
141
|
-
setPhase('exiting');
|
|
142
|
-
}
|
|
143
|
-
if (isOpen && phase === 'exiting' && !isNativeClosedRef.current) {
|
|
144
|
-
// Reopen mid-exit. With animation, jump back to `entering` and let
|
|
145
|
-
// the entry transition restart from current visual state. Without
|
|
146
|
-
// animation, jump straight to `open` (the handshake we were waiting
|
|
147
|
-
// for is no longer relevant; the consumer wants the host visible
|
|
148
|
-
// again synchronously).
|
|
149
|
-
setPhase(willAnimate ? 'entering' : 'open');
|
|
150
|
-
}
|
|
188
|
+
var _useReducer = (0, _react.useReducer)(getNextVisibilityState, {
|
|
189
|
+
isOpen: isOpen,
|
|
190
|
+
willAnimate: willAnimate
|
|
191
|
+
}, getInitialVisibilityState),
|
|
192
|
+
_useReducer2 = (0, _slicedToArray2.default)(_useReducer, 2),
|
|
193
|
+
visibilityState = _useReducer2[0],
|
|
194
|
+
dispatch = _useReducer2[1];
|
|
195
|
+
var phase = visibilityState.phase,
|
|
196
|
+
controlledIntent = visibilityState.controlledIntent;
|
|
151
197
|
|
|
152
|
-
// Keep stable refs to the latest callbacks so effects and
|
|
198
|
+
// Keep stable refs to the latest callbacks so effects and animation settlement closures
|
|
153
199
|
// always call the current callback without needing them in dependency arrays.
|
|
154
200
|
var onEnterFinishRef = (0, _react.useRef)(onEnterFinish);
|
|
155
|
-
onEnterFinishRef.current = onEnterFinish;
|
|
156
201
|
var onExitFinishRef = (0, _react.useRef)(onExitFinish);
|
|
157
|
-
|
|
202
|
+
var willAnimateRef = (0, _react.useRef)(willAnimate);
|
|
203
|
+
(0, _react.useLayoutEffect)(function () {
|
|
204
|
+
onEnterFinishRef.current = onEnterFinish;
|
|
205
|
+
onExitFinishRef.current = onExitFinish;
|
|
206
|
+
willAnimateRef.current = willAnimate;
|
|
207
|
+
}, [onEnterFinish, onExitFinish, willAnimate]);
|
|
158
208
|
|
|
159
|
-
//
|
|
209
|
+
// Synchronize controlled intent during render so opening mounts the host in
|
|
210
|
+
// the same commit. Keeping the processed intent in reducer state prevents a
|
|
211
|
+
// browser-driven exit from being mistaken for a new controlled open request.
|
|
212
|
+
if (isOpen && controlledIntent === 'closed') {
|
|
213
|
+
dispatch({
|
|
214
|
+
type: 'open-requested',
|
|
215
|
+
willAnimate: willAnimate
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (!isOpen && controlledIntent === 'open') {
|
|
219
|
+
dispatch({
|
|
220
|
+
type: 'close-requested'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Entry can commit `open` before its callback because the host stays mounted.
|
|
225
|
+
// This ensures consumers observe the settled DOM rather than entering styles.
|
|
226
|
+
// Treat the phase before the initial commit as closed so an initially open host
|
|
227
|
+
// notifies consumers.
|
|
228
|
+
var prevPhaseRef = (0, _react.useRef)('closed');
|
|
160
229
|
(0, _react.useEffect)(function () {
|
|
161
|
-
|
|
230
|
+
var prevPhase = prevPhaseRef.current;
|
|
231
|
+
prevPhaseRef.current = phase;
|
|
232
|
+
if (prevPhase === phase) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (phase === 'open') {
|
|
236
|
+
var _onEnterFinishRef$cur;
|
|
237
|
+
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
238
|
+
}
|
|
239
|
+
}, [phase]);
|
|
240
|
+
var isMounted = phase !== 'closed';
|
|
241
|
+
var cancelExitSettlementRef = (0, _react.useRef)(function () {});
|
|
242
|
+
|
|
243
|
+
// Exit must notify consumers before `closed` unmounts the host.
|
|
244
|
+
var settleExit = (0, _react.useCallback)(function () {
|
|
245
|
+
var _onExitFinishRef$curr;
|
|
246
|
+
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
|
|
247
|
+
dispatch({
|
|
248
|
+
type: 'exit-settled'
|
|
249
|
+
});
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
// Primitives bind these stable callbacks to their native lifecycle events.
|
|
253
|
+
// This keeps event ownership at the primitive while the hook owns state transitions.
|
|
254
|
+
var onBeforeToggle = (0, _react.useCallback)(function (event) {
|
|
255
|
+
// Any native transition supersedes a previous exit snapshot. This prevents
|
|
256
|
+
// a stale settlement from closing a host that is reopening or closing again.
|
|
257
|
+
cancelExitSettlementRef.current();
|
|
258
|
+
// Snapshot currentTarget because the DOM clears it after event dispatch.
|
|
259
|
+
var element = event.currentTarget;
|
|
260
|
+
if (element instanceof HTMLElement) {
|
|
261
|
+
if (event.newState === 'open') {
|
|
262
|
+
element.removeAttribute('inert');
|
|
263
|
+
element.removeAttribute('aria-hidden');
|
|
264
|
+
} else {
|
|
265
|
+
// We use a microtask to let native focus restoration happen before making the element inert.
|
|
266
|
+
// The browser restores focus after beforetoggle returns, during its close steps.
|
|
267
|
+
queueMicrotask(function () {
|
|
268
|
+
// A controlled close can be interrupted by isOpen changing back to true,
|
|
269
|
+
// which reopens the element before this microtask runs.
|
|
270
|
+
if (isNativeElementOpen({
|
|
271
|
+
element: element
|
|
272
|
+
})) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
element.setAttribute('inert', '');
|
|
276
|
+
// Tooling does not consistently treat inert content as hidden, even though the browser does.
|
|
277
|
+
element.setAttribute('aria-hidden', 'true');
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (event.newState === 'closed') {
|
|
282
|
+
dispatch({
|
|
283
|
+
type: 'exit-started'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}, []);
|
|
287
|
+
var onToggle = (0, _react.useCallback)(function (event) {
|
|
288
|
+
if (event.newState !== 'closed') {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!willAnimateRef.current) {
|
|
292
|
+
cancelExitSettlementRef.current = runCancellableSettlement({
|
|
293
|
+
schedule: queueMicrotask,
|
|
294
|
+
onSettled: settleExit
|
|
295
|
+
});
|
|
162
296
|
return;
|
|
163
297
|
}
|
|
164
298
|
var element = elementRef.current;
|
|
165
299
|
if (!element) {
|
|
166
300
|
return;
|
|
167
301
|
}
|
|
168
|
-
|
|
302
|
+
|
|
303
|
+
// The closed `toggle` is task-queued after synchronous `beforetoggle`.
|
|
304
|
+
// Snapshot here so the exiting phase styles have committed and the list
|
|
305
|
+
// represents the animations that actually govern exit settlement.
|
|
306
|
+
cancelExitSettlementRef.current = runOnAnimationsSettled({
|
|
169
307
|
element: element,
|
|
170
|
-
onSettled:
|
|
171
|
-
var _onEnterFinishRef$cur;
|
|
172
|
-
// Fire the consumer callback before transitioning phase so any
|
|
173
|
-
// state the consumer reads (refs, DOM nodes) is still attached
|
|
174
|
-
// in the same synchronous tick, regardless of React batching
|
|
175
|
-
// semantics.
|
|
176
|
-
(_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
|
|
177
|
-
setPhase(function (current) {
|
|
178
|
-
return current === 'entering' ? 'open' : current;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
308
|
+
onSettled: settleExit
|
|
181
309
|
});
|
|
182
|
-
}, [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
var prevPhase = prevPhaseForEntryRef.current;
|
|
190
|
-
prevPhaseForEntryRef.current = phase;
|
|
310
|
+
}, [elementRef, settleExit]);
|
|
311
|
+
(0, _react.useLayoutEffect)(function () {
|
|
312
|
+
return function () {
|
|
313
|
+
// Prevent a pending animation or microtask from notifying after unmount.
|
|
314
|
+
cancelExitSettlementRef.current();
|
|
315
|
+
};
|
|
316
|
+
}, []);
|
|
191
317
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
318
|
+
// Entry settlement only removes entering styles. Its callback runs after the
|
|
319
|
+
// resulting `open` commit in the effect above.
|
|
320
|
+
(0, _react.useEffect)(function settleEntry() {
|
|
321
|
+
if (phase !== 'entering') {
|
|
322
|
+
return;
|
|
197
323
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
(0, _react.useEffect)(function () {
|
|
203
|
-
if (phase !== 'exiting' || !willAnimate) {
|
|
324
|
+
if (!willAnimate) {
|
|
325
|
+
dispatch({
|
|
326
|
+
type: 'entry-settled'
|
|
327
|
+
});
|
|
204
328
|
return;
|
|
205
329
|
}
|
|
206
330
|
var element = elementRef.current;
|
|
@@ -210,67 +334,16 @@ function useAnimatedVisibility(_ref3) {
|
|
|
210
334
|
return runOnAnimationsSettled({
|
|
211
335
|
element: element,
|
|
212
336
|
onSettled: function onSettled() {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// `closed`. The phase change drives the host element unmount,
|
|
216
|
-
// and consumers expect to be able to read the element (or a
|
|
217
|
-
// forwarded ref to it) inside `onExitFinish`. Doing this in
|
|
218
|
-
// the safe order avoids relying on React's automatic batching
|
|
219
|
-
// to keep the element attached across the callback.
|
|
220
|
-
(_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
|
|
221
|
-
setPhase(function (current) {
|
|
222
|
-
return current === 'exiting' ? 'closed' : current;
|
|
337
|
+
dispatch({
|
|
338
|
+
type: 'entry-settled'
|
|
223
339
|
});
|
|
224
340
|
}
|
|
225
341
|
});
|
|
226
342
|
}, [phase, willAnimate, elementRef]);
|
|
227
|
-
|
|
228
|
-
// Native dismissal is authoritative. Its toggle can arrive while controlled
|
|
229
|
-
// state still says open, so remember that native state and do not
|
|
230
|
-
// reopen until `isOpen` has first moved to false.
|
|
231
|
-
(0, _react.useLayoutEffect)(function () {
|
|
232
|
-
if (phase === 'exiting' && !willAnimate && isNativeClosedRef.current) {
|
|
233
|
-
setPhase('closed');
|
|
234
|
-
}
|
|
235
|
-
}, [phase, willAnimate]);
|
|
236
|
-
var onToggle = (0, _react.useCallback)(function (event) {
|
|
237
|
-
if (event.newState === 'open') {
|
|
238
|
-
isNativeClosedRef.current = false;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (event.newState !== 'closed') {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
isNativeClosedRef.current = true;
|
|
245
|
-
setPhase(function (current) {
|
|
246
|
-
if (current === 'open' || current === 'entering') {
|
|
247
|
-
return 'exiting';
|
|
248
|
-
}
|
|
249
|
-
if (current === 'exiting' && !willAnimate) {
|
|
250
|
-
return 'closed';
|
|
251
|
-
}
|
|
252
|
-
return current;
|
|
253
|
-
});
|
|
254
|
-
}, [willAnimate]);
|
|
255
|
-
|
|
256
|
-
// Non-animated exit callback. There are no animations to wait for, so fire
|
|
257
|
-
// `onExitFinish` on the transition into `exiting` (the unmount itself
|
|
258
|
-
// is gated by the closing toggle handshake above).
|
|
259
|
-
var prevPhaseForExitRef = (0, _react.useRef)('closed');
|
|
260
|
-
(0, _react.useEffect)(function () {
|
|
261
|
-
var prevPhase = prevPhaseForExitRef.current;
|
|
262
|
-
prevPhaseForExitRef.current = phase;
|
|
263
|
-
|
|
264
|
-
// Only fire on the non-animated transition into `exiting`. The
|
|
265
|
-
// animated path's `exiting → closed` settle already fires
|
|
266
|
-
// `onExitFinish` from `runOnAnimationsSettled`.
|
|
267
|
-
if (prevPhase !== 'exiting' && phase === 'exiting' && !willAnimate) {
|
|
268
|
-
var _onExitFinishRef$curr2;
|
|
269
|
-
(_onExitFinishRef$curr2 = onExitFinishRef.current) === null || _onExitFinishRef$curr2 === void 0 || _onExitFinishRef$curr2.call(onExitFinishRef);
|
|
270
|
-
}
|
|
271
|
-
}, [phase, willAnimate]);
|
|
272
343
|
return {
|
|
273
344
|
phase: phase,
|
|
345
|
+
isMounted: isMounted,
|
|
346
|
+
onBeforeToggle: onBeforeToggle,
|
|
274
347
|
onToggle: onToggle
|
|
275
348
|
};
|
|
276
349
|
}
|