@atlaskit/top-layer 4.4.3 → 4.4.5

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.
@@ -1,5 +1,8 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ 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; }
4
+ 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) { _defineProperty(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; }
5
+ import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
3
6
  import { prefersReducedMotion } from './reduced-motion';
4
7
  function runCancellableSettlement(_ref) {
5
8
  var schedule = _ref.schedule,
@@ -12,6 +15,7 @@ function runCancellableSettlement(_ref) {
12
15
  onSettled();
13
16
  });
14
17
  return function () {
18
+ // Prevent an interrupted or unmounted lifecycle from firing a stale callback.
15
19
  isCancelled = true;
16
20
  };
17
21
  }
@@ -34,6 +38,19 @@ function runOnAnimationsSettled(_ref2) {
34
38
  }
35
39
  });
36
40
  }
41
+ function isNativeElementOpen(_ref3) {
42
+ var element = _ref3.element;
43
+ if (element instanceof HTMLDialogElement) {
44
+ return element.open;
45
+ }
46
+
47
+ // Some DOM implementations throw for unsupported pseudo-classes.
48
+ try {
49
+ return element.matches(':popover-open');
50
+ } catch (_unused) {
51
+ return false;
52
+ }
53
+ }
37
54
 
38
55
  /**
39
56
  * Visibility lifecycle phase shared between `useAnimatedVisibility`,
@@ -46,11 +63,11 @@ function runOnAnimationsSettled(_ref2) {
46
63
  * animation disabled the lifecycle goes directly `closed → open`.
47
64
  * - `open`: host element is mounted and settled. No transition is
48
65
  * playing.
49
- * - `exiting`: `isOpen` has flipped to `false` but the host element
50
- * is still mounted. Emitted while the CSS exit transition is playing
51
- * (animation enabled) OR while we wait for the browser's
52
- * `toggle`/`close` event to fire on the still-attached element so
53
- * focus restoration can run (animation disabled).
66
+ * - `exiting`: a native close has begun but the host element remains
67
+ * mounted. Usually this follows `isOpen` changing to `false`, but it can
68
+ * also begin with a browser-initiated dismissal while controlled intent
69
+ * is still open. With animation disabled, this phase lasts until the
70
+ * browser's closed `toggle` fires so native focus restoration can finish.
54
71
  *
55
72
  * State machine:
56
73
  * ```
@@ -60,210 +77,273 @@ function runOnAnimationsSettled(_ref2) {
60
77
  * exiting → entering (reopen mid-exit, animated)
61
78
  * exiting → open (reopen mid-handshake, non-animated)
62
79
  * ```
63
- *
64
- * `phase !== 'closed'` is the canonical "host is currently in the DOM
65
- * and on screen" predicate.
66
80
  */
67
81
 
82
+ function getInitialVisibilityState(_ref4) {
83
+ var isOpen = _ref4.isOpen,
84
+ willAnimate = _ref4.willAnimate;
85
+ if (!isOpen) {
86
+ return {
87
+ phase: 'closed',
88
+ controlledIntent: 'closed'
89
+ };
90
+ }
91
+ if (willAnimate) {
92
+ return {
93
+ phase: 'entering',
94
+ controlledIntent: 'open'
95
+ };
96
+ }
97
+ return {
98
+ phase: 'open',
99
+ controlledIntent: 'open'
100
+ };
101
+ }
102
+
68
103
  /**
69
- * Manages the children mount/unmount lifecycle around CSS exit transitions.
70
- *
71
- * Used by both `Popover` and `Dialog` to share the same animation lifecycle
72
- * logic. Those components own their own show/hide mechanisms (showPopover /
73
- * hidePopover vs showModal / close) and event handling - this hook only
74
- * manages the relationship between `isOpen` and when children are rendered.
104
+ * Applies one lifecycle event to the current visibility phase.
75
105
  *
76
- * ## Problem
77
- *
78
- * We want to delay the unmount of children until a CSS exit transition has
79
- * finished playing. If we unmount children the moment `isOpen` becomes
80
- * `false`, the exit animation is never visible - the content just disappears.
106
+ * Events that are stale for the current phase are ignored. This allows
107
+ * animation callbacks and native close events to settle safely after an
108
+ * interrupted lifecycle.
109
+ */
110
+ function getNextVisibilityState(state, action) {
111
+ if (action.type === 'open-requested') {
112
+ var shouldEnter = state.phase === 'closed' || state.phase === 'exiting' && state.controlledIntent === 'closed';
113
+ if (shouldEnter && action.willAnimate) {
114
+ return {
115
+ phase: 'entering',
116
+ controlledIntent: 'open'
117
+ };
118
+ }
119
+ if (shouldEnter) {
120
+ return {
121
+ phase: 'open',
122
+ controlledIntent: 'open'
123
+ };
124
+ }
125
+ return _objectSpread(_objectSpread({}, state), {}, {
126
+ controlledIntent: 'open'
127
+ });
128
+ }
129
+ if (action.type === 'close-requested') {
130
+ return _objectSpread(_objectSpread({}, state), {}, {
131
+ controlledIntent: 'closed'
132
+ });
133
+ }
134
+ if (action.type === 'exit-started') {
135
+ if (state.phase !== 'open' && state.phase !== 'entering') {
136
+ return state;
137
+ }
138
+ return _objectSpread(_objectSpread({}, state), {}, {
139
+ phase: 'exiting'
140
+ });
141
+ }
142
+ if (action.type === 'entry-settled') {
143
+ if (state.phase !== 'entering') {
144
+ return state;
145
+ }
146
+ return _objectSpread(_objectSpread({}, state), {}, {
147
+ phase: 'open'
148
+ });
149
+ }
150
+ if (action.type === 'exit-settled') {
151
+ if (state.phase !== 'exiting') {
152
+ return state;
153
+ }
154
+ return _objectSpread(_objectSpread({}, state), {}, {
155
+ phase: 'closed'
156
+ });
157
+ }
158
+ return state;
159
+ }
160
+
161
+ /**
162
+ * Coordinates controlled intent, native visibility, lifecycle phase, and host
163
+ * mounting around CSS exit transitions. The canonical contract is documented in
164
+ * `notes/architecture/animations.md#canonical-visibility-lifecycle-contract`.
81
165
  *
82
- * ## How it works
166
+ * Used by both `Popover` and `Dialog` to share the same lifecycle logic. Those
167
+ * components own native event binding, show and hide commands, and
168
+ * component-specific event handling. This hook returns stable `onBeforeToggle`
169
+ * and `onToggle` callbacks that advance lifecycle phase and host mounting.
83
170
  *
84
- * The lifecycle is modeled as a `phase` discriminated union (`closed`,
85
- * `entering`, `open`, `exiting`). The host element is rendered while
86
- * `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
87
- * while the CSS exit transition plays (or, with animation disabled,
88
- * while we wait for the browser's closing `toggle` event to fire on the
89
- * still-attached element):
171
+ * The host element is rendered while `phase !== 'closed'`. The `exiting`
172
+ * phase keeps it mounted until its CSS animation settles or, with animation
173
+ * disabled, until the browser reports that the native element has closed.
90
174
  *
91
175
  * ```
92
176
  * isOpen: true ──────────────── false
93
177
  * phase: open ──────────────── exiting ─── (exit settle) ─── closed
94
178
  * ```
95
- *
96
- * Every effect dispatches on the single `phase` value instead of a
97
- * cross-product of booleans.
98
179
  */
99
- export function useAnimatedVisibility(_ref3) {
100
- var isOpen = _ref3.isOpen,
101
- shouldAnimate = _ref3.shouldAnimate,
102
- elementRef = _ref3.elementRef,
103
- onEnterFinish = _ref3.onEnterFinish,
104
- onExitFinish = _ref3.onExitFinish;
180
+ export function useAnimatedVisibility(_ref5) {
181
+ var isOpen = _ref5.isOpen,
182
+ shouldAnimate = _ref5.shouldAnimate,
183
+ elementRef = _ref5.elementRef,
184
+ onEnterFinish = _ref5.onEnterFinish,
185
+ onExitFinish = _ref5.onExitFinish;
105
186
  var willAnimate = shouldAnimate && !prefersReducedMotion();
106
- var isNativeClosedRef = useRef(false);
107
-
108
- // Promote `closed → entering`/`closed → open` synchronously during
109
- // render so the host element mounts in the same commit as the open
110
- // intent. All other transitions are driven by phase-specific effects.
111
- var _useState = useState(function () {
112
- if (!isOpen) {
113
- return 'closed';
114
- }
115
- return willAnimate ? 'entering' : 'open';
116
- }),
117
- _useState2 = _slicedToArray(_useState, 2),
118
- phase = _useState2[0],
119
- setPhase = _useState2[1];
187
+ var _useReducer = useReducer(getNextVisibilityState, {
188
+ isOpen: isOpen,
189
+ willAnimate: willAnimate
190
+ }, getInitialVisibilityState),
191
+ _useReducer2 = _slicedToArray(_useReducer, 2),
192
+ visibilityState = _useReducer2[0],
193
+ dispatch = _useReducer2[1];
194
+ var phase = visibilityState.phase,
195
+ controlledIntent = visibilityState.controlledIntent;
120
196
 
121
- // Rearm only after controlled state and the native lifecycle have both
122
- // reached closed. Resetting as soon as `isOpen` becomes false can erase a
123
- // closing toggle that was handled in the same React batch.
124
- if (!isOpen && phase === 'closed') {
125
- isNativeClosedRef.current = false;
126
- }
127
- if (isOpen && phase === 'closed' && !isNativeClosedRef.current) {
128
- setPhase(willAnimate ? 'entering' : 'open');
129
- }
130
- if (!isOpen && (phase === 'open' || phase === 'entering')) {
131
- // Close intent. Move to `exiting` to gate the unmount on either
132
- // the exit transition (animated) or the closing toggle event
133
- // (non-animated). Includes interrupted entry (`entering → exiting`).
134
- setPhase('exiting');
135
- }
136
- if (isOpen && phase === 'exiting' && !isNativeClosedRef.current) {
137
- // Reopen mid-exit. With animation, jump back to `entering` and let
138
- // the entry transition restart from current visual state. Without
139
- // animation, jump straight to `open` (the handshake we were waiting
140
- // for is no longer relevant; the consumer wants the host visible
141
- // again synchronously).
142
- setPhase(willAnimate ? 'entering' : 'open');
143
- }
144
-
145
- // Keep stable refs to the latest callbacks so effects and promise callbacks
197
+ // Keep stable refs to the latest callbacks so effects and animation settlement closures
146
198
  // always call the current callback without needing them in dependency arrays.
147
199
  var onEnterFinishRef = useRef(onEnterFinish);
148
- onEnterFinishRef.current = onEnterFinish;
149
200
  var onExitFinishRef = useRef(onExitFinish);
150
- onExitFinishRef.current = onExitFinish;
201
+ var willAnimateRef = useRef(willAnimate);
202
+ useLayoutEffect(function () {
203
+ onEnterFinishRef.current = onEnterFinish;
204
+ onExitFinishRef.current = onExitFinish;
205
+ willAnimateRef.current = willAnimate;
206
+ }, [onEnterFinish, onExitFinish, willAnimate]);
207
+
208
+ // Synchronize controlled intent during render so opening mounts the host in
209
+ // the same commit. Keeping the processed intent in reducer state prevents a
210
+ // browser-driven exit from being mistaken for a new controlled open request.
211
+ if (isOpen && controlledIntent === 'closed') {
212
+ dispatch({
213
+ type: 'open-requested',
214
+ willAnimate: willAnimate
215
+ });
216
+ }
217
+ if (!isOpen && controlledIntent === 'open') {
218
+ dispatch({
219
+ type: 'close-requested'
220
+ });
221
+ }
151
222
 
152
- // Animated entry: wait for all active animations and move to `open`.
223
+ // Entry can commit `open` before its callback because the host stays mounted.
224
+ // This ensures consumers observe the settled DOM rather than entering styles.
225
+ // Treat the phase before the initial commit as closed so an initially open host
226
+ // notifies consumers.
227
+ var prevPhaseRef = useRef('closed');
153
228
  useEffect(function () {
154
- if (phase !== 'entering') {
229
+ var prevPhase = prevPhaseRef.current;
230
+ prevPhaseRef.current = phase;
231
+ if (prevPhase === phase) {
155
232
  return;
156
233
  }
157
- var element = elementRef.current;
158
- if (!element) {
159
- return;
234
+ if (phase === 'open') {
235
+ var _onEnterFinishRef$cur;
236
+ (_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
160
237
  }
161
- return runOnAnimationsSettled({
162
- element: element,
163
- onSettled: function onSettled() {
164
- var _onEnterFinishRef$cur;
165
- // Fire the consumer callback before transitioning phase so any
166
- // state the consumer reads (refs, DOM nodes) is still attached
167
- // in the same synchronous tick, regardless of React batching
168
- // semantics.
169
- (_onEnterFinishRef$cur = onEnterFinishRef.current) === null || _onEnterFinishRef$cur === void 0 || _onEnterFinishRef$cur.call(onEnterFinishRef);
170
- setPhase(function (current) {
171
- return current === 'entering' ? 'open' : current;
172
- });
173
- }
174
- });
175
- }, [phase, elementRef]);
238
+ }, [phase]);
239
+ var isMounted = phase !== 'closed';
240
+ var cancelExitSettlementRef = useRef(function () {});
176
241
 
177
- // Non-animated entry callback. The lifecycle goes `closed → open`
178
- // directly with no `entering` phase; fire `onEnterFinish` on that jump.
179
- // A previous-phase ref avoids double-firing on the animated path.
180
- var prevPhaseForEntryRef = useRef('closed');
181
- useEffect(function () {
182
- var prevPhase = prevPhaseForEntryRef.current;
183
- prevPhaseForEntryRef.current = phase;
242
+ // Exit must notify consumers before `closed` unmounts the host.
243
+ var settleExit = useCallback(function () {
244
+ var _onExitFinishRef$curr;
245
+ (_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
246
+ dispatch({
247
+ type: 'exit-settled'
248
+ });
249
+ }, []);
184
250
 
185
- // Animated entry settle goes through `entering` and already fired
186
- // onEnterFinish above; only handle the non-animated direct jumps.
187
- if (prevPhase !== 'open' && phase === 'open' && prevPhase !== 'entering') {
188
- var _onEnterFinishRef$cur2;
189
- (_onEnterFinishRef$cur2 = onEnterFinishRef.current) === null || _onEnterFinishRef$cur2 === void 0 || _onEnterFinishRef$cur2.call(onEnterFinishRef);
251
+ // Primitives bind these stable callbacks to their native lifecycle events.
252
+ // This keeps event ownership at the primitive while the hook owns state transitions.
253
+ var onBeforeToggle = useCallback(function (event) {
254
+ // Any native transition supersedes a previous exit snapshot. This prevents
255
+ // a stale settlement from closing a host that is reopening or closing again.
256
+ cancelExitSettlementRef.current();
257
+ // Snapshot currentTarget because the DOM clears it after event dispatch.
258
+ var element = event.currentTarget;
259
+ if (event.newState === 'open' && element instanceof HTMLElement) {
260
+ element.removeAttribute('inert');
261
+ element.removeAttribute('aria-hidden');
190
262
  }
191
- }, [phase]);
263
+ if (event.newState === 'closed') {
264
+ if (element instanceof HTMLElement) {
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
+ }
192
275
 
193
- // Animated exit: wait for all active animations, fire `onExitFinish`,
194
- // then move to `closed` to unmount the host.
195
- useEffect(function () {
196
- if (phase !== 'exiting' || !willAnimate) {
276
+ // Exit animations keep closed content rendered, so make it non-interactive and hide it from assistive technology.
277
+ element.setAttribute('inert', '');
278
+ // Tooling does not consistently treat inert content as hidden, even though the browser does.
279
+ element.setAttribute('aria-hidden', 'true');
280
+ });
281
+ }
282
+ dispatch({
283
+ type: 'exit-started'
284
+ });
285
+ }
286
+ }, []);
287
+ var onToggle = 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
+ });
197
296
  return;
198
297
  }
199
298
  var element = elementRef.current;
200
299
  if (!element) {
201
300
  return;
202
301
  }
203
- return runOnAnimationsSettled({
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({
204
307
  element: element,
205
- onSettled: function onSettled() {
206
- var _onExitFinishRef$curr;
207
- // Fire the consumer callback before transitioning phase to
208
- // `closed`. The phase change drives the host element unmount,
209
- // and consumers expect to be able to read the element (or a
210
- // forwarded ref to it) inside `onExitFinish`. Doing this in
211
- // the safe order avoids relying on React's automatic batching
212
- // to keep the element attached across the callback.
213
- (_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 || _onExitFinishRef$curr.call(onExitFinishRef);
214
- setPhase(function (current) {
215
- return current === 'exiting' ? 'closed' : current;
216
- });
217
- }
308
+ onSettled: settleExit
218
309
  });
219
- }, [phase, willAnimate, elementRef]);
220
-
221
- // Native dismissal is authoritative. Its toggle can arrive while controlled
222
- // state still says open, so remember that native state and do not
223
- // reopen until `isOpen` has first moved to false.
310
+ }, [elementRef, settleExit]);
224
311
  useLayoutEffect(function () {
225
- if (phase === 'exiting' && !willAnimate && isNativeClosedRef.current) {
226
- setPhase('closed');
312
+ return function () {
313
+ // Prevent a pending animation or microtask from notifying after unmount.
314
+ cancelExitSettlementRef.current();
315
+ };
316
+ }, []);
317
+
318
+ // Entry settlement only removes entering styles. Its callback runs after the
319
+ // resulting `open` commit in the effect above.
320
+ useEffect(function settleEntry() {
321
+ if (phase !== 'entering') {
322
+ return;
227
323
  }
228
- }, [phase, willAnimate]);
229
- var onToggle = useCallback(function (event) {
230
- if (event.newState === 'open') {
231
- isNativeClosedRef.current = false;
324
+ if (!willAnimate) {
325
+ dispatch({
326
+ type: 'entry-settled'
327
+ });
232
328
  return;
233
329
  }
234
- if (event.newState !== 'closed') {
330
+ var element = elementRef.current;
331
+ if (!element) {
235
332
  return;
236
333
  }
237
- isNativeClosedRef.current = true;
238
- setPhase(function (current) {
239
- if (current === 'open' || current === 'entering') {
240
- return 'exiting';
241
- }
242
- if (current === 'exiting' && !willAnimate) {
243
- return 'closed';
334
+ return runOnAnimationsSettled({
335
+ element: element,
336
+ onSettled: function onSettled() {
337
+ dispatch({
338
+ type: 'entry-settled'
339
+ });
244
340
  }
245
- return current;
246
341
  });
247
- }, [willAnimate]);
248
-
249
- // Non-animated exit callback. There are no animations to wait for, so fire
250
- // `onExitFinish` on the transition into `exiting` (the unmount itself
251
- // is gated by the closing toggle handshake above).
252
- var prevPhaseForExitRef = useRef('closed');
253
- useEffect(function () {
254
- var prevPhase = prevPhaseForExitRef.current;
255
- prevPhaseForExitRef.current = phase;
256
-
257
- // Only fire on the non-animated transition into `exiting`. The
258
- // animated path's `exiting → closed` settle already fires
259
- // `onExitFinish` from `runOnAnimationsSettled`.
260
- if (prevPhase !== 'exiting' && phase === 'exiting' && !willAnimate) {
261
- var _onExitFinishRef$curr2;
262
- (_onExitFinishRef$curr2 = onExitFinishRef.current) === null || _onExitFinishRef$curr2 === void 0 || _onExitFinishRef$curr2.call(onExitFinishRef);
263
- }
264
- }, [phase, willAnimate]);
342
+ }, [phase, willAnimate, elementRef]);
265
343
  return {
266
344
  phase: phase,
345
+ isMounted: isMounted,
346
+ onBeforeToggle: onBeforeToggle,
267
347
  onToggle: onToggle
268
348
  };
269
349
  }