@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/dialog/dialog-content.js +57 -47
  3. package/dist/cjs/focus/get-closest-layer.js +10 -0
  4. package/dist/cjs/focus/get-focusables.js +45 -10
  5. package/dist/cjs/focus/get-next-focusable.js +40 -15
  6. package/dist/cjs/focus/is-nested-layer-focused.js +8 -9
  7. package/dist/cjs/internal/use-animated-visibility.js +238 -165
  8. package/dist/cjs/popover/popover.js +97 -81
  9. package/dist/cjs/popover-surface/popover-surface.js +1 -1
  10. package/dist/es2019/dialog/dialog-content.js +57 -47
  11. package/dist/es2019/focus/get-closest-layer.js +5 -0
  12. package/dist/es2019/focus/get-focusables.js +43 -6
  13. package/dist/es2019/focus/get-next-focusable.js +38 -15
  14. package/dist/es2019/focus/is-nested-layer-focused.js +8 -9
  15. package/dist/es2019/internal/use-animated-visibility.js +235 -154
  16. package/dist/es2019/popover/popover.js +98 -82
  17. package/dist/es2019/popover-surface/popover-surface.js +1 -1
  18. package/dist/esm/dialog/dialog-content.js +57 -47
  19. package/dist/esm/focus/get-closest-layer.js +4 -0
  20. package/dist/esm/focus/get-focusables.js +45 -10
  21. package/dist/esm/focus/get-next-focusable.js +40 -15
  22. package/dist/esm/focus/is-nested-layer-focused.js +8 -9
  23. package/dist/esm/internal/use-animated-visibility.js +239 -166
  24. package/dist/esm/popover/popover.js +97 -81
  25. package/dist/esm/popover-surface/popover-surface.js +1 -1
  26. package/dist/types/dialog/types.d.ts +6 -4
  27. package/dist/types/focus/get-closest-layer.d.ts +3 -0
  28. package/dist/types/focus/get-focusables.d.ts +2 -0
  29. package/dist/types/focus/get-next-focusable.d.ts +6 -2
  30. package/dist/types/focus/is-nested-layer-focused.d.ts +2 -2
  31. package/dist/types/internal/use-animated-visibility.d.ts +37 -50
  32. package/dist/types/popover/popover.d.ts +3 -1
  33. package/dist/types/popover/types.d.ts +12 -9
  34. package/package.json +14 -9
  35. package/dist/cjs/focus/is-in-same-layer.js +0 -17
  36. package/dist/cjs/focus/selectors.js +0 -19
  37. package/dist/es2019/focus/is-in-same-layer.js +0 -12
  38. package/dist/es2019/focus/selectors.js +0 -11
  39. package/dist/esm/focus/is-in-same-layer.js +0 -11
  40. package/dist/esm/focus/selectors.js +0 -13
  41. package/dist/types/focus/is-in-same-layer.d.ts +0 -8
  42. package/dist/types/focus/selectors.d.ts +0 -4
@@ -1,4 +1,4 @@
1
- /* popover.tsx generated by @compiled/babel-plugin v2.0.0 */
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
- // True while the host element is mounted (any phase except `closed`).
142
- // Used as a dep so listener-rebind effects re-attach after a remount.
143
- var isVisible = phase !== 'closed';
144
-
145
- // Focus management: initial focus on entry (role-dependent), Tab cycling
146
- // for dialog roles. Restoration is native for outermost popovers; nested
147
- // focus-capturing roles are handled below via the `beforetoggle` snapshot.
148
- // Passing `phase` keeps `useFocusWrap` attached through the animated-exit
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
- onClose === null || onClose === void 0 || onClose({
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
- }, [onClose]);
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 handleToggle = useCallback(function (event) {
210
- var element = ownRef.current;
211
- if (event.newState === 'open') {
212
- onToggle(event);
213
- return;
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
- // Programmatic close: consumer already knows.
238
- if (programmaticCloseRef.current) {
239
- // Reset reason so a stale 'escape' from a race with Escape keydown
240
- // does not corrupt the next browser-dismiss cycle.
241
- closeReasonRef.current = 'light-dismiss';
242
- } else {
243
- // Browser dismiss (Escape/click-outside).
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
- onToggle(event);
253
- }, [onClose, onToggle, role]);
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: handleToggle
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: handleBeforeToggle
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
- }, [handleToggle, handleBeforeToggle, isVisible]);
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 the no-animation path unmounts before
307
- // `handleToggle` can reset it.
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 (!isVisible) {
344
+ if (!isMounted) {
329
345
  return null;
330
346
  }
331
347
  return /*#__PURE__*/React.createElement("div", {
@@ -1,4 +1,4 @@
1
- /* popover-surface.tsx generated by @compiled/babel-plugin v2.0.0 */
1
+ /* popover-surface.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  import "./popover-surface.compiled.css";
3
3
  import * as React from 'react';
4
4
  import { ax, ix } from "@compiled/react/runtime";
@@ -25,12 +25,13 @@ export type TDialogDismissedBy = 'none' | 'escape' | 'escape-and-outside-click';
25
25
  type TDialogBaseProps = {
26
26
  children: ReactNode;
27
27
  /**
28
- * Whether the dialog is open.
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` before the dialog becomes logically closed.
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 exit animation completes (or immediately on close when
123
- * there is no animation or reduced motion is active).
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.
@@ -0,0 +1,3 @@
1
+ export declare function getClosestLayer({ element }: {
2
+ element: HTMLElement;
3
+ }): Element | null;
@@ -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
- * Returns `null` if there is no focused element within the container or if
6
- * the focused element is not in the list of focusable elements.
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]`, `<dialog>`, `[role="dialog"]` or
4
- * `[role="alertdialog"]` that is itself a descendant of `container`).
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
- * Whether the element is logically open.
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 transitions.
9
- * Pass `false` or `undefined` to disable animation.
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 transitions.
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 entry animation completes (or immediately on open
19
- * when there is no animation or reduced motion is active).
20
- * This includes initial mount with `isOpen=true`.
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 exit animation completes (or immediately on close
25
- * when there is no animation or reduced motion is active).
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. See `TPhase` for the full lifecycle.
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
- * Notifies the visibility lifecycle after the primitive has finished its
42
- * own native toggle handling. Closing toggles make native dismissal
43
- * authoritative while preserving primitive-specific handler ordering.
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`: `isOpen` has flipped to `false` but the host element
59
- * is still mounted. Emitted while the CSS exit transition is playing
60
- * (animation enabled) OR while we wait for the browser's
61
- * `toggle`/`close` event to fire on the still-attached element so
62
- * focus restoration can run (animation disabled).
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
- * Manages the children mount/unmount lifecycle around CSS exit transitions.
79
- *
80
- * Used by both `Popover` and `Dialog` to share the same animation lifecycle
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
- * ## Problem
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
- * We want to delay the unmount of children until a CSS exit transition has
88
- * finished playing. If we unmount children the moment `isOpen` becomes
89
- * `false`, the exit animation is never visible - the content just disappears.
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
- * Whether the popover is open.
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` before the popover becomes logically closed.
71
- * Otherwise hides instantly.
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 will be out of sync (the popover will be hidden despite `isOpen={true}`).
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 exit animation completes (or immediately on close when
114
- * there is no animation or reduced motion is active).
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 - without it, browser
150
- * dismissals leave the consumer's `isOpen` stuck at `true` while
151
- * the DOM is hidden, producing a stale-open state.
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.2",
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.0.0",
39
+ "@atlaskit/css": "^1.1.0",
40
40
  "@atlaskit/ds-lib": "^8.0.0",
41
- "@atlaskit/layering": "^5.1.0",
41
+ "@atlaskit/layering": "^5.2.0",
42
42
  "@atlaskit/react-compiler-gating": "^0.2.0",
43
- "@atlaskit/tokens": "^16.8.0",
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.2.0",
57
- "@atlaskit/heading": "^7.1.0",
58
- "@atlaskit/lozenge": "^15.3.0",
59
- "@atlaskit/primitives": "^22.4.0",
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.1.0",
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
- })();