@atlaskit/top-layer 0.10.0 → 0.12.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +3 -3
  3. package/dist/cjs/animations/presets.js +8 -1
  4. package/dist/cjs/internal/use-animated-visibility.js +84 -7
  5. package/dist/cjs/popover/popover.js +2 -0
  6. package/dist/cjs/popup/popup-content.js +53 -13
  7. package/dist/cjs/popup/popup-context.js +17 -0
  8. package/dist/cjs/popup/popup-trigger-function.js +7 -4
  9. package/dist/cjs/popup/popup-trigger.js +12 -7
  10. package/dist/cjs/popup/popup.js +5 -5
  11. package/dist/es2019/animations/presets.js +19 -2
  12. package/dist/es2019/internal/use-animated-visibility.js +84 -7
  13. package/dist/es2019/popover/popover.js +2 -0
  14. package/dist/es2019/popup/popup-content.js +53 -13
  15. package/dist/es2019/popup/popup-context.js +18 -0
  16. package/dist/es2019/popup/popup-trigger-function.js +7 -4
  17. package/dist/es2019/popup/popup-trigger.js +12 -7
  18. package/dist/es2019/popup/popup.js +3 -3
  19. package/dist/esm/animations/presets.js +8 -1
  20. package/dist/esm/internal/use-animated-visibility.js +84 -7
  21. package/dist/esm/popover/popover.js +2 -0
  22. package/dist/esm/popup/popup-content.js +53 -13
  23. package/dist/esm/popup/popup-context.js +18 -0
  24. package/dist/esm/popup/popup-trigger-function.js +7 -4
  25. package/dist/esm/popup/popup-trigger.js +12 -7
  26. package/dist/esm/popup/popup.js +5 -5
  27. package/dist/types/animations/types.d.ts +5 -1
  28. package/dist/types/internal/use-animated-visibility.d.ts +8 -2
  29. package/dist/types/popover/types.d.ts +8 -0
  30. package/dist/types/popup/popup-context.d.ts +24 -4
  31. package/dist/types/popup/types.d.ts +12 -0
  32. package/dist/types-ts4.5/animations/types.d.ts +5 -1
  33. package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +8 -2
  34. package/dist/types-ts4.5/popover/types.d.ts +8 -0
  35. package/dist/types-ts4.5/popup/popup-context.d.ts +24 -4
  36. package/dist/types-ts4.5/popup/types.d.ts +12 -0
  37. package/package.json +1 -1
@@ -3,6 +3,7 @@ import _extends from "@babel/runtime/helpers/extends";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
5
5
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
+ import { prefersReducedMotion } from '../internal/reduced-motion';
6
7
  import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
7
8
  import { useAnchorPosition } from '../internal/use-anchor-position';
8
9
  import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
@@ -16,7 +17,7 @@ import { useMaybePopupContext } from './popup-context';
16
17
  * use `Popover` + `useAnchorPosition` directly.
17
18
  */
18
19
  export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, ref) {
19
- var _ref2, _ref3, _ctx$popoverRef;
20
+ var _ref2, _ctx$popoverRef;
20
21
  var children = _ref.children,
21
22
  role = _ref.role,
22
23
  label = _ref.label,
@@ -32,7 +33,9 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
32
33
  onCloseProp = _ref.onClose,
33
34
  testIdProp = _ref.testId,
34
35
  forceFallbackPositioningProp = _ref.forceFallbackPositioning,
35
- idProp = _ref.id;
36
+ idProp = _ref.id,
37
+ onEnterFinish = _ref.onEnterFinish,
38
+ onExitFinish = _ref.onExitFinish;
36
39
  // When inside <Popup>, context provides defaults. Props always win.
37
40
  var ctx = useMaybePopupContext();
38
41
  var triggerRef = triggerRefProp !== null && triggerRefProp !== void 0 ? triggerRefProp : ctx === null || ctx === void 0 ? void 0 : ctx.triggerRef;
@@ -41,16 +44,47 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
41
44
  var testId = testIdProp !== null && testIdProp !== void 0 ? testIdProp : ctx === null || ctx === void 0 ? void 0 : ctx.testId;
42
45
  var forceFallbackPositioning = forceFallbackPositioningProp !== null && forceFallbackPositioningProp !== void 0 ? forceFallbackPositioningProp : ctx === null || ctx === void 0 ? void 0 : ctx.forceFallbackPositioning;
43
46
 
44
- // Resolve isOpen: prop wins, then context.
45
- // Inside the <Popup> compound, ctx.isOpen tracks the browser's toggle state.
46
- var isOpen = (_ref3 = isOpenProp !== null && isOpenProp !== void 0 ? isOpenProp : ctx === null || ctx === void 0 ? void 0 : ctx.isOpen) !== null && _ref3 !== void 0 ? _ref3 : false;
47
-
48
- // Combine internal state sync with consumer's onOpenChange callback.
49
- // The Popup compound needs setIsOpen to keep aria-expanded on the trigger
50
- // in sync, and the consumer may also want to respond to open/close events
51
- // (e.g. for focus management).
52
- var setIsOpen = ctx === null || ctx === void 0 ? void 0 : ctx.setIsOpen;
47
+ // isOpen: prop wins, otherwise derive from context popupState.
48
+ // 'animating-open' counts as open so <Popover> stays visible during entry animation.
49
+ var isOpen = isOpenProp !== null && isOpenProp !== void 0 ? isOpenProp : ctx !== null ? ctx.popupState === 'open' || ctx.popupState === 'animating-open' : false;
50
+ var setPopupState = ctx === null || ctx === void 0 ? void 0 : ctx.setPopupState;
53
51
  var consumerOnOpenChange = ctx === null || ctx === void 0 ? void 0 : ctx.onOpenChange;
52
+
53
+ // ── TPopupState ──
54
+ //
55
+ // popupState is driven by handleOpenChange (below), which fires on every browser
56
+ // toggle event (showPopover / hidePopover / light-dismiss).
57
+ //
58
+ // Exception — initial mount with isOpen=true (prop-controlled usage):
59
+ // handleOpenChange only fires from the Popover's toggle event listener, which
60
+ // is registered in a useEffect. On the first render, useEffect hasn't run yet,
61
+ // so handleOpenChange never fires. We sync popupState here instead.
62
+ //
63
+ // Transitions when animations are active:
64
+ // closed → animating-open → open (enter)
65
+ // open → animating-closed → closed (exit, via onExitFinish)
66
+ //
67
+ // Without animation: closed ↔ open directly.
68
+ var willAnimate = Boolean(animate) && !prefersReducedMotion();
69
+ var hasMountedRef = useRef(false);
70
+ useLayoutEffect(function () {
71
+ if (hasMountedRef.current) {
72
+ return;
73
+ }
74
+ hasMountedRef.current = true;
75
+ if (isOpen) {
76
+ setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-open' : 'open');
77
+ }
78
+ }, [isOpen, setPopupState, willAnimate]);
79
+ var handleEnterFinish = useCallback(function () {
80
+ setPopupState === null || setPopupState === void 0 || setPopupState('open');
81
+ onEnterFinish === null || onEnterFinish === void 0 || onEnterFinish();
82
+ }, [setPopupState, onEnterFinish]);
83
+ var handleExitFinish = useCallback(function () {
84
+ setPopupState === null || setPopupState === void 0 || setPopupState('closed');
85
+ onExitFinish === null || onExitFinish === void 0 || onExitFinish();
86
+ }, [setPopupState, onExitFinish]);
87
+
54
88
  // Note: onOpenChange composition is handled by handleOpenChange below,
55
89
  // which also adds nested-popover focus restoration.
56
90
 
@@ -82,7 +116,11 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
82
116
  // See: notes/architecture/focus-restoration.md
83
117
  var handleOpenChange = useCallback(function (args) {
84
118
  // Sync compound state
85
- setIsOpen === null || setIsOpen === void 0 || setIsOpen(args.isOpen);
119
+ if (args.isOpen) {
120
+ setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-open' : 'open');
121
+ } else {
122
+ setPopupState === null || setPopupState === void 0 || setPopupState(willAnimate ? 'animating-closed' : 'closed');
123
+ }
86
124
  consumerOnOpenChange === null || consumerOnOpenChange === void 0 || consumerOnOpenChange(args);
87
125
 
88
126
  // ── Nested popover focus restoration fallback ──
@@ -112,7 +150,7 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
112
150
  });
113
151
  }
114
152
  }
115
- }, [setIsOpen, consumerOnOpenChange, triggerRef, role]);
153
+ }, [consumerOnOpenChange, triggerRef, role, setPopupState, willAnimate]);
116
154
 
117
155
  // ── Anchor positioning ──
118
156
  // Compose useAnchorPosition with Popover. The hook positions the popover
@@ -151,6 +189,8 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
151
189
  labelledBy: labelledBy,
152
190
  onClose: onClose !== null && onClose !== void 0 ? onClose : undefined,
153
191
  onOpenChange: handleOpenChange,
192
+ onEnterFinish: handleEnterFinish,
193
+ onExitFinish: handleExitFinish,
154
194
  testId: testId,
155
195
  children: children,
156
196
  // Standalone consumers can pass an explicit `id` - used by their own
@@ -1,4 +1,22 @@
1
1
  import { createContext, useContext } from 'react';
2
+
3
+ /**
4
+ * Represents the lifecycle state of the popup.
5
+ *
6
+ * - `'closed'`: fully closed, not visible, no animation playing.
7
+ * - `'animating-open'`: entry animation is playing (popup is opening but not yet settled).
8
+ * - `'open'`: fully open, browser popover is showing, entry animation complete.
9
+ * - `'animating-closed'`: exit animation is playing (popup is closing but still visible).
10
+ *
11
+ * Used to derive `aria-expanded` on the trigger:
12
+ * - `aria-expanded` is `true` in `'animating-open'`, `'open'`, and `'animating-closed'`.
13
+ *
14
+ * This avoids screen readers announcing the popup as closed while it is
15
+ * still visually present on screen during animations, and prevents consumers
16
+ * that conditionally show/hide trigger UI based on `aria-expanded` from hiding
17
+ * the trigger (and thus the popup anchor) mid-animation.
18
+ */
19
+
2
20
  var PopupContext = /*#__PURE__*/createContext(null);
3
21
 
4
22
  /**
@@ -41,7 +41,7 @@ export function PopupTriggerFunction(_ref) {
41
41
  triggerRef = _usePopupContext.triggerRef,
42
42
  popoverRef = _usePopupContext.popoverRef,
43
43
  popoverId = _usePopupContext.popoverId,
44
- isOpen = _usePopupContext.isOpen,
44
+ popupState = _usePopupContext.popupState,
45
45
  ariaHasPopup = _usePopupContext.ariaHasPopup;
46
46
  var ref = useCallback(function (node) {
47
47
  triggerRef.current = node;
@@ -57,14 +57,17 @@ export function PopupTriggerFunction(_ref) {
57
57
  }, [popoverRef]);
58
58
  var ariaAttributes = useMemo(function () {
59
59
  return {
60
- 'aria-expanded': isOpen,
60
+ // aria-expanded stays true throughout entry and exit animations so screen readers
61
+ // don't announce the popup as closed while it's still visible, and consumers
62
+ // relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
63
+ 'aria-expanded': popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
61
64
  'aria-controls': popoverId,
62
65
  'aria-haspopup': ariaHasPopup
63
66
  };
64
- }, [isOpen, popoverId, ariaHasPopup]);
67
+ }, [popupState, popoverId, ariaHasPopup]);
65
68
  return children({
66
69
  ref: ref,
67
- isOpen: isOpen,
70
+ isOpen: popupState === 'open' || popupState === 'animating-open',
68
71
  popoverId: popoverId,
69
72
  toggle: toggle,
70
73
  ariaAttributes: ariaAttributes
@@ -18,14 +18,15 @@ export function PopupTrigger(_ref) {
18
18
  triggerRef = _usePopupContext.triggerRef,
19
19
  popoverRef = _usePopupContext.popoverRef,
20
20
  popoverId = _usePopupContext.popoverId,
21
- isOpen = _usePopupContext.isOpen,
21
+ popupState = _usePopupContext.popupState,
22
22
  ariaHasPopup = _usePopupContext.ariaHasPopup;
23
23
  var childOnClick = /*#__PURE__*/isValidElement(children) ? children.props.onClick : undefined;
24
24
 
25
- // Track latest isOpen via ref so the click handler keeps a stable
26
- // identity (cloneElement relies on it).
27
- var isOpenRef = React.useRef(isOpen);
28
- isOpenRef.current = isOpen;
25
+ // Use a ref to track the latest isOpen value so the click handler
26
+ // does not need isOpen in its dependency array (which would recreate
27
+ // the handler on every open/close, breaking cloneElement identity).
28
+ var isOpenRef = React.useRef(popupState === 'open' || popupState === 'animating-open');
29
+ isOpenRef.current = popupState === 'open' || popupState === 'animating-open';
29
30
  var handleClick = useCallback(function (event) {
30
31
  var popoverEl = popoverRef.current;
31
32
  if (popoverEl) {
@@ -45,8 +46,12 @@ export function PopupTrigger(_ref) {
45
46
  }, [popoverRef, childOnClick]);
46
47
  return /*#__PURE__*/React.createElement(Slot, {
47
48
  ref: triggerRef,
48
- onClick: handleClick,
49
- "aria-expanded": isOpen,
49
+ onClick: handleClick
50
+ // aria-expanded stays true throughout entry and exit animations so screen readers
51
+ // don't announce the popup as closed while it's still visible, and consumers
52
+ // relying on aria-expanded to show/hide trigger UI don't lose the anchor mid-animation.
53
+ ,
54
+ "aria-expanded": popupState === 'open' || popupState === 'animating-open' || popupState === 'animating-closed',
50
55
  "aria-controls": popoverId,
51
56
  "aria-haspopup": ariaHasPopup
52
57
  }, children);
@@ -35,10 +35,10 @@ export function PopupRoot(props) {
35
35
  var popoverId = "popover-".concat(sanitizedId);
36
36
  var triggerRef = useRef(null);
37
37
  var popoverRef = useRef(null);
38
- var _useState = useState(false),
38
+ var _useState = useState('closed'),
39
39
  _useState2 = _slicedToArray(_useState, 2),
40
- isOpen = _useState2[0],
41
- setIsOpen = _useState2[1];
40
+ popupState = _useState2[0],
41
+ setPopupState = _useState2[1];
42
42
  // Initial seed before `Popup.Content`'s layout effect resolves the
43
43
  // real role on first paint.
44
44
  var _useState3 = useState('dialog'),
@@ -57,8 +57,8 @@ export function PopupRoot(props) {
57
57
  onClose: onClose ? handleClose : null,
58
58
  triggerRef: triggerRef,
59
59
  popoverRef: popoverRef,
60
- isOpen: isOpen,
61
- setIsOpen: setIsOpen,
60
+ popupState: popupState,
61
+ setPopupState: setPopupState,
62
62
  onOpenChange: onOpenChange,
63
63
  mode: mode,
64
64
  ariaHasPopup: ariaHasPopup,
@@ -28,7 +28,11 @@ export type TAnimationPreset = {
28
28
  placement: TPlacementOptions;
29
29
  }) => Record<string, string>;
30
30
  /**
31
- * Exit duration in ms (for `transitionend` fallback timeout).
31
+ * Entry duration in ms (for `transitionend` fallback timeout on open).
32
+ */
33
+ enterDurationMs: number;
34
+ /**
35
+ * Exit duration in ms (for `transitionend` fallback timeout on close).
32
36
  */
33
37
  exitDurationMs: number;
34
38
  };
@@ -11,10 +11,16 @@ type TUseAnimatedVisibilityArgs = {
11
11
  */
12
12
  animate: TAnimationPreset | false | undefined;
13
13
  /**
14
- * Ref to the DOM element that plays the exit transition.
14
+ * Ref to the DOM element that plays the entry/exit transitions.
15
15
  * Used to listen for `transitionend`.
16
16
  */
17
17
  elementRef: RefObject<HTMLElement | null>;
18
+ /**
19
+ * Called after the entry animation completes (or immediately on open
20
+ * when there is no animation or reduced motion is active).
21
+ * This includes initial mount with `isOpen=true`.
22
+ */
23
+ onEnterFinish?: () => void;
18
24
  /**
19
25
  * Called after the exit animation completes (or immediately on close
20
26
  * when there is no animation or reduced motion is active).
@@ -70,5 +76,5 @@ type TUseAnimatedVisibilityResult = {
70
76
  * `isOpen` goes `false` → `showChildren` → `false` synchronously during
71
77
  * the same render → `onExitFinish` fires in a follow-up effect.
72
78
  */
73
- export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
79
+ export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onEnterFinish, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
74
80
  export {};
@@ -105,6 +105,14 @@ type TPopoverBaseProps = {
105
105
  * This does NOT control positioning. Use `useAnchorPosition` for that.
106
106
  */
107
107
  placement?: TPlacementOptions;
108
+ /**
109
+ * Called after the entry animation completes (or immediately on open when
110
+ * there is no animation or reduced motion is active).
111
+ *
112
+ * Use this for external lifecycle coordination — e.g. notifying a manager
113
+ * that the show sequence is finished.
114
+ */
115
+ onEnterFinish?: () => void;
108
116
  /**
109
117
  * Called after the exit animation completes (or immediately on close when
110
118
  * there is no animation or reduced motion is active).
@@ -2,6 +2,23 @@ import React, { type MutableRefObject, type RefObject } from 'react';
2
2
  import { type TAriaHasPopupValue } from '../internal/role-types';
3
3
  import { type TPopoverCloseReason } from '../popover/types';
4
4
  import { type TPlacementOptions } from './types';
5
+ /**
6
+ * Represents the lifecycle state of the popup.
7
+ *
8
+ * - `'closed'`: fully closed, not visible, no animation playing.
9
+ * - `'animating-open'`: entry animation is playing (popup is opening but not yet settled).
10
+ * - `'open'`: fully open, browser popover is showing, entry animation complete.
11
+ * - `'animating-closed'`: exit animation is playing (popup is closing but still visible).
12
+ *
13
+ * Used to derive `aria-expanded` on the trigger:
14
+ * - `aria-expanded` is `true` in `'animating-open'`, `'open'`, and `'animating-closed'`.
15
+ *
16
+ * This avoids screen readers announcing the popup as closed while it is
17
+ * still visually present on screen during animations, and prevents consumers
18
+ * that conditionally show/hide trigger UI based on `aria-expanded` from hiding
19
+ * the trigger (and thus the popup anchor) mid-animation.
20
+ */
21
+ export type TPopupState = 'closed' | 'animating-open' | 'open' | 'animating-closed';
5
22
  export type TPopupContextValue = {
6
23
  /**
7
24
  * Auto-generated ID for the popover content element (used for aria-controls).
@@ -27,13 +44,16 @@ export type TPopupContextValue = {
27
44
  */
28
45
  popoverRef: RefObject<HTMLDivElement | null>;
29
46
  /**
30
- * Whether the popover is currently open.
47
+ * The full lifecycle state of the popup.
48
+ *
49
+ * See `TPopupState` for the full state machine description.
31
50
  */
32
- isOpen: boolean;
51
+ popupState: TPopupState;
33
52
  /**
34
- * Set the open state (internal context sync).
53
+ * Updates the popup lifecycle state. Called by `PopupContent` as animations
54
+ * begin and complete.
35
55
  */
36
- setIsOpen: (open: boolean) => void;
56
+ setPopupState: (state: TPopupState) => void;
37
57
  /**
38
58
  * Consumer's onOpenChange callback, forwarded to the Popover.
39
59
  */
@@ -258,6 +258,18 @@ type TPopupContentBaseProps = {
258
258
  * ```
259
259
  */
260
260
  isOpen?: boolean;
261
+ /**
262
+ * Called after the entry animation completes (or immediately on open when
263
+ * there is no animation). Use this for lifecycle coordination that must
264
+ * happen after the popup has fully animated in.
265
+ */
266
+ onEnterFinish?: () => void;
267
+ /**
268
+ * Called after the exit animation completes (or immediately on close when
269
+ * there is no animation). Use this for lifecycle coordination that must
270
+ * happen after the popup has fully animated out.
271
+ */
272
+ onExitFinish?: () => void;
261
273
  };
262
274
  /**
263
275
  * Popup content props.
@@ -28,7 +28,11 @@ export type TAnimationPreset = {
28
28
  placement: TPlacementOptions;
29
29
  }) => Record<string, string>;
30
30
  /**
31
- * Exit duration in ms (for `transitionend` fallback timeout).
31
+ * Entry duration in ms (for `transitionend` fallback timeout on open).
32
+ */
33
+ enterDurationMs: number;
34
+ /**
35
+ * Exit duration in ms (for `transitionend` fallback timeout on close).
32
36
  */
33
37
  exitDurationMs: number;
34
38
  };
@@ -11,10 +11,16 @@ type TUseAnimatedVisibilityArgs = {
11
11
  */
12
12
  animate: TAnimationPreset | false | undefined;
13
13
  /**
14
- * Ref to the DOM element that plays the exit transition.
14
+ * Ref to the DOM element that plays the entry/exit transitions.
15
15
  * Used to listen for `transitionend`.
16
16
  */
17
17
  elementRef: RefObject<HTMLElement | null>;
18
+ /**
19
+ * Called after the entry animation completes (or immediately on open
20
+ * when there is no animation or reduced motion is active).
21
+ * This includes initial mount with `isOpen=true`.
22
+ */
23
+ onEnterFinish?: () => void;
18
24
  /**
19
25
  * Called after the exit animation completes (or immediately on close
20
26
  * when there is no animation or reduced motion is active).
@@ -70,5 +76,5 @@ type TUseAnimatedVisibilityResult = {
70
76
  * `isOpen` goes `false` → `showChildren` → `false` synchronously during
71
77
  * the same render → `onExitFinish` fires in a follow-up effect.
72
78
  */
73
- export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
79
+ export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onEnterFinish, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
74
80
  export {};
@@ -105,6 +105,14 @@ type TPopoverBaseProps = {
105
105
  * This does NOT control positioning. Use `useAnchorPosition` for that.
106
106
  */
107
107
  placement?: TPlacementOptions;
108
+ /**
109
+ * Called after the entry animation completes (or immediately on open when
110
+ * there is no animation or reduced motion is active).
111
+ *
112
+ * Use this for external lifecycle coordination — e.g. notifying a manager
113
+ * that the show sequence is finished.
114
+ */
115
+ onEnterFinish?: () => void;
108
116
  /**
109
117
  * Called after the exit animation completes (or immediately on close when
110
118
  * there is no animation or reduced motion is active).
@@ -2,6 +2,23 @@ import React, { type MutableRefObject, type RefObject } from 'react';
2
2
  import { type TAriaHasPopupValue } from '../internal/role-types';
3
3
  import { type TPopoverCloseReason } from '../popover/types';
4
4
  import { type TPlacementOptions } from './types';
5
+ /**
6
+ * Represents the lifecycle state of the popup.
7
+ *
8
+ * - `'closed'`: fully closed, not visible, no animation playing.
9
+ * - `'animating-open'`: entry animation is playing (popup is opening but not yet settled).
10
+ * - `'open'`: fully open, browser popover is showing, entry animation complete.
11
+ * - `'animating-closed'`: exit animation is playing (popup is closing but still visible).
12
+ *
13
+ * Used to derive `aria-expanded` on the trigger:
14
+ * - `aria-expanded` is `true` in `'animating-open'`, `'open'`, and `'animating-closed'`.
15
+ *
16
+ * This avoids screen readers announcing the popup as closed while it is
17
+ * still visually present on screen during animations, and prevents consumers
18
+ * that conditionally show/hide trigger UI based on `aria-expanded` from hiding
19
+ * the trigger (and thus the popup anchor) mid-animation.
20
+ */
21
+ export type TPopupState = 'closed' | 'animating-open' | 'open' | 'animating-closed';
5
22
  export type TPopupContextValue = {
6
23
  /**
7
24
  * Auto-generated ID for the popover content element (used for aria-controls).
@@ -27,13 +44,16 @@ export type TPopupContextValue = {
27
44
  */
28
45
  popoverRef: RefObject<HTMLDivElement | null>;
29
46
  /**
30
- * Whether the popover is currently open.
47
+ * The full lifecycle state of the popup.
48
+ *
49
+ * See `TPopupState` for the full state machine description.
31
50
  */
32
- isOpen: boolean;
51
+ popupState: TPopupState;
33
52
  /**
34
- * Set the open state (internal context sync).
53
+ * Updates the popup lifecycle state. Called by `PopupContent` as animations
54
+ * begin and complete.
35
55
  */
36
- setIsOpen: (open: boolean) => void;
56
+ setPopupState: (state: TPopupState) => void;
37
57
  /**
38
58
  * Consumer's onOpenChange callback, forwarded to the Popover.
39
59
  */
@@ -258,6 +258,18 @@ type TPopupContentBaseProps = {
258
258
  * ```
259
259
  */
260
260
  isOpen?: boolean;
261
+ /**
262
+ * Called after the entry animation completes (or immediately on open when
263
+ * there is no animation). Use this for lifecycle coordination that must
264
+ * happen after the popup has fully animated in.
265
+ */
266
+ onEnterFinish?: () => void;
267
+ /**
268
+ * Called after the exit animation completes (or immediately on close when
269
+ * there is no animation). Use this for lifecycle coordination that must
270
+ * happen after the popup has fully animated out.
271
+ */
272
+ onExitFinish?: () => void;
261
273
  };
262
274
  /**
263
275
  * Popup content props.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
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",