@atlaskit/popup 4.19.1 → 4.20.1

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @atlaskit/popup
2
2
 
3
+ ## 4.20.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 4.20.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`43e486948865a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/43e486948865a) -
14
+ When `platform-dst-top-layer` is enabled, the compositional `PopupContent` now renders using
15
+ `Popover` directly (instead of `Popup.Content`) so that `useAnchorPosition` and
16
+ `useWidthFromAnchor` hooks can be applied with the correct anchor ref from
17
+ `TriggerRefObjectContext`.
18
+
19
+ This fixes the popup appearing in the wrong position and having incorrect width/layout relative to
20
+ its trigger.
21
+
22
+ The `xcss` prop is also now correctly forwarded to the underlying container element.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies
27
+
3
28
  ## 4.19.1
4
29
 
5
30
  ### Patch Changes
@@ -10,15 +10,19 @@ exports.PopupContentTopLayer = PopupContentTopLayer;
10
10
  require("./popup-content-top-layer.compiled.css");
11
11
  var _react = _interopRequireWildcard(require("react"));
12
12
  var React = _react;
13
- var _runtime = require("@compiled/react/runtime");
14
13
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
15
14
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
15
+ var _runtime = require("@compiled/react/runtime");
16
16
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
17
17
  var _animations = require("@atlaskit/top-layer/animations");
18
18
  var _createCloseEvent = require("@atlaskit/top-layer/create-close-event");
19
19
  var _placementMap = require("@atlaskit/top-layer/placement-map");
20
- var _popup = require("@atlaskit/top-layer/popup");
20
+ var _popover = require("@atlaskit/top-layer/popover");
21
+ var _popupSurface = require("@atlaskit/top-layer/popup-surface");
22
+ var _useAnchorPosition = require("@atlaskit/top-layer/use-anchor-position");
23
+ var _useWidthFromAnchor = require("@atlaskit/top-layer/use-width-from-anchor");
21
24
  var _topLayerBridge = require("../internal/top-layer-bridge");
25
+ var _triggerRefObjectContext = require("./trigger-ref-object-context");
22
26
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
23
27
  var overflowAutoStyles = null;
24
28
  var animation = (0, _animations.slideAndFade)();
@@ -63,7 +67,6 @@ function PopupContentTopLayer(_ref) {
63
67
  _shouldFlip = _ref.shouldFlip,
64
68
  _appearance = _ref.appearance,
65
69
  _shouldDisableGpuAcceleration = _ref.shouldDisableGpuAcceleration;
66
- var popupContainerRef = (0, _react.useRef)(null);
67
70
  var _useState = (0, _react.useState)(null),
68
71
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
69
72
  setInitialFocusRef = _useState2[1];
@@ -105,31 +108,39 @@ function PopupContentTopLayer(_ref) {
105
108
  label: label,
106
109
  titleId: titleId
107
110
  });
111
+ var popoverRef = (0, _react.useRef)(null);
112
+ var anchorRef = (0, _react.useContext)(_triggerRefObjectContext.TriggerRefObjectContext);
113
+ (0, _useAnchorPosition.useAnchorPosition)({
114
+ anchorRef: anchorRef,
115
+ popoverRef: popoverRef,
116
+ placement: topLayerPlacement
117
+ });
118
+ (0, _useWidthFromAnchor.useWidthFromAnchor)({
119
+ mode: shouldFitContainer ? 'match-anchor' : 'none',
120
+ popoverRef: popoverRef,
121
+ anchorRef: anchorRef
122
+ });
108
123
 
109
124
  // Narrow to ForwardRefExoticComponent so JSX accepts the ref prop.
110
125
  // All popupComponent implementations use forwardRef per the PopupComponentProps contract.
111
126
  var Container = PopupContainer;
112
- if (!isOpen) {
113
- return null;
114
- }
115
- return /*#__PURE__*/React.createElement(_popup.Popup, {
116
- placement: topLayerPlacement,
117
- onClose: handleOnClose,
118
- testId: testId
119
- }, /*#__PURE__*/React.createElement(_popup.Popup.Content, (0, _extends2.default)({}, roleProps, {
127
+ return /*#__PURE__*/React.createElement(_popover.Popover, (0, _extends2.default)({
128
+ ref: popoverRef
129
+ }, roleProps, {
120
130
  isOpen: isOpen,
121
131
  animate: animation,
122
- testId: testId && "".concat(testId, "--content"),
123
- width: shouldFitContainer ? 'trigger' : 'content'
132
+ placement: topLayerPlacement,
133
+ onClose: handleOnClose,
134
+ testId: testId && "".concat(testId, "--content")
124
135
  }), Container ? /*#__PURE__*/React.createElement(Container, {
125
- ref: popupContainerRef,
126
136
  style: EMPTY_STYLE,
127
137
  id: providedId,
128
138
  "data-placement": placement,
129
139
  "data-testid": testId,
130
140
  tabIndex: autoFocus ? -1 : undefined,
131
141
  xcss: xcss
132
- }, children(contentProps)) : /*#__PURE__*/React.createElement(_popup.Popup.Surface, null, /*#__PURE__*/React.createElement("div", {
133
- className: (0, _runtime.ax)([shouldFitViewport && "_1reo1wug _18m91wug"])
134
- }, children(contentProps)))));
142
+ }, children(contentProps)) : /*#__PURE__*/React.createElement(_popupSurface.PopupSurface, null, /*#__PURE__*/React.createElement("div", {
143
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
144
+ className: (0, _runtime.ax)([shouldFitViewport && "_1reo1wug _18m91wug", (0, _runtime.ax)([xcss])])
145
+ }, children(contentProps))));
135
146
  }
@@ -114,8 +114,7 @@ var PopupContent = exports.PopupContent = function PopupContent(_ref) {
114
114
  shouldFlip: shouldFlip,
115
115
  shouldDisableGpuAcceleration: shouldDisableGpuAcceleration,
116
116
  isOpen: isOpen,
117
- id: id,
118
- triggerRef: triggerRef
117
+ id: id
119
118
  }, children);
120
119
  }
121
120
  if (!isOpen) {
@@ -13,6 +13,7 @@ var _idContext = require("./id-context");
13
13
  var _isOpenContext = require("./is-open-context");
14
14
  var _roleContext = require("./role-context");
15
15
  var _setTriggerRefContext = require("./set-trigger-ref-context");
16
+ var _triggerRefObjectContext = require("./trigger-ref-object-context");
16
17
  var _useEnsureIsInsidePopup = require("./use-ensure-is-inside-popup");
17
18
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
18
19
  /**
@@ -27,16 +28,37 @@ var PopupTrigger = exports.PopupTrigger = function PopupTrigger(_ref) {
27
28
  (0, _useEnsureIsInsidePopup.useEnsureIsInsidePopup)();
28
29
  var id = (0, _react.useContext)(_idContext.IdContext);
29
30
  var setTriggerRef = (0, _react.useContext)(_setTriggerRefContext.SetTriggerRefContext);
31
+ var triggerRefObject = (0, _react.useContext)(_triggerRefObjectContext.TriggerRefObjectContext);
30
32
  var isOpen = (0, _react.useContext)(_isOpenContext.IsOpenContext);
31
33
  var getMergedTriggerRef = (0, _useGetMemoizedMergedTriggerRefNew.useGetMemoizedMergedTriggerRefNew)();
32
34
  var role = (0, _react.useContext)(_roleContext.RoleContext);
35
+
36
+ // Stable ref callback — avoids creating a new function instance on every render,
37
+ // which would cause React to detach and reattach the ref unnecessarily.
38
+ // triggerRefObject is a MutableRefObject (from useRef) so its identity never changes,
39
+ // meaning this callback is effectively created once per mount.
40
+ var triggerRef = (0, _react.useCallback)(function (node) {
41
+ triggerRefObject.current = node;
42
+ }, [triggerRefObject]);
43
+ var ariaHasPopup = role === 'dialog' && (0, _platformFeatureFlags.fg)('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true;
44
+
45
+ // When the top-layer flag is on, bypass Popper's <Reference> entirely.
46
+ // We only need to set triggerRefObject.current — no Popper ref merging needed.
47
+ if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
48
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, children({
49
+ ref: triggerRef,
50
+ 'aria-controls': id,
51
+ 'aria-expanded': isOpen,
52
+ 'aria-haspopup': ariaHasPopup
53
+ }));
54
+ }
33
55
  return /*#__PURE__*/_react.default.createElement(_popper.Reference, null, function (_ref2) {
34
56
  var ref = _ref2.ref;
35
57
  return children({
36
58
  ref: getMergedTriggerRef(ref, setTriggerRef),
37
59
  'aria-controls': id,
38
60
  'aria-expanded': isOpen,
39
- 'aria-haspopup': role === 'dialog' && (0, _platformFeatureFlags.fg)('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true
61
+ 'aria-haspopup': ariaHasPopup
40
62
  });
41
63
  });
42
64
  };
@@ -9,6 +9,7 @@ exports.Popup = void 0;
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
10
  var _react = _interopRequireWildcard(require("react"));
11
11
  var _useId = require("@atlaskit/ds-lib/use-id");
12
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
12
13
  var _popper = require("@atlaskit/popper");
13
14
  var _idContext = require("./id-context");
14
15
  var _isInsidePopupContext = require("./is-inside-popup-context");
@@ -16,6 +17,7 @@ var _isOpenContext = require("./is-open-context");
16
17
  var _roleContext = require("./role-context");
17
18
  var _setTriggerRefContext = require("./set-trigger-ref-context");
18
19
  var _triggerRefContext = require("./trigger-ref-context");
20
+ var _triggerRefObjectContext = require("./trigger-ref-object-context");
19
21
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
20
22
  /**
21
23
  * __Popup__
@@ -46,6 +48,15 @@ var Popup = exports.Popup = function Popup(_ref) {
46
48
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
47
49
  triggerRef = _useState2[0],
48
50
  setTriggerRef = _useState2[1];
51
+
52
+ // When the top-layer flag is on, we maintain a stable RefObject for the
53
+ // trigger element, provided through TriggerRefObjectContext. Unlike the
54
+ // state-based triggerRef (which causes a re-render when the trigger mounts),
55
+ // this ref has a stable identity so useAnchorPosition's useLayoutEffect dep
56
+ // array never changes — the effect runs once on mount when the trigger element
57
+ // is already populated. PopupTrigger sets this ref directly instead of calling
58
+ // the state setter, avoiding an unnecessary re-render.
59
+ var triggerRefObject = (0, _react.useRef)(null);
49
60
  var generatedId = (0, _useId.useId)();
50
61
  var id = providedId || generatedId;
51
62
  return /*#__PURE__*/_react.default.createElement(_roleContext.RoleContext.Provider, {
@@ -60,5 +71,10 @@ var Popup = exports.Popup = function Popup(_ref) {
60
71
  value: setTriggerRef
61
72
  }, /*#__PURE__*/_react.default.createElement(_isOpenContext.IsOpenContext.Provider, {
62
73
  value: isOpen
63
- }, /*#__PURE__*/_react.default.createElement(_popper.Manager, null, children)))))));
74
+ }, (0, _platformFeatureFlags.fg)('platform-dst-top-layer') ?
75
+ /*#__PURE__*/
76
+ // Skip Popper's <Manager> when Top Layer is being used.
77
+ _react.default.createElement(_triggerRefObjectContext.TriggerRefObjectContext.Provider, {
78
+ value: triggerRefObject
79
+ }, children) : /*#__PURE__*/_react.default.createElement(_popper.Manager, null, children)))))));
64
80
  };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TriggerRefObjectContext = void 0;
7
+ var _react = require("react");
8
+ /* eslint-disable @repo/internal/react/require-jsdoc */
9
+
10
+ /**
11
+ * Provides the trigger element as a stable `MutableRefObject<HTMLElement | null>`
12
+ * for the top-layer path. Unlike `TriggerRefContext` (which provides
13
+ * `HTMLElement | null` as React state and causes a re-render when the trigger
14
+ * mounts), this context provides a ref object whose identity is stable across
15
+ * renders — allowing `useAnchorPosition` to receive a stable `anchorRef`
16
+ * without needing to create a new ref wrapper on every `triggerRef` state change.
17
+ */
18
+ var TriggerRefObjectContext = exports.TriggerRefObjectContext = /*#__PURE__*/(0, _react.createContext)({
19
+ current: null
20
+ });
@@ -170,7 +170,7 @@ var PopupTopLayer = exports.PopupTopLayer = /*#__PURE__*/(0, _react.memo)(functi
170
170
  isOpen: effectiveIsOpen,
171
171
  animate: animation,
172
172
  testId: testId && "".concat(testId, "--content"),
173
- width: shouldFitContainer ? 'trigger' : 'content'
173
+ widthFromAnchor: shouldFitContainer ? 'match-anchor' : 'none'
174
174
  }), Container ? /*#__PURE__*/React.createElement(Container, {
175
175
  ref: popupContainerRef,
176
176
  style: EMPTY_STYLE,
@@ -2,14 +2,18 @@
2
2
  import _extends from "@babel/runtime/helpers/extends";
3
3
  import "./popup-content-top-layer.compiled.css";
4
4
  import * as React from 'react';
5
- import { ax, ix } from "@compiled/react/runtime";
6
- import { useCallback, useMemo, useRef, useState } from 'react';
5
+ import { useCallback, useContext, useMemo, useRef, useState } from 'react';
6
+ import { ax, ix } from '@compiled/react/runtime';
7
7
  import noop from '@atlaskit/ds-lib/noop';
8
8
  import { slideAndFade } from '@atlaskit/top-layer/animations';
9
9
  import { createPopoverCloseEvent } from '@atlaskit/top-layer/create-close-event';
10
10
  import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
11
- import { Popup } from '@atlaskit/top-layer/popup';
11
+ import { Popover } from '@atlaskit/top-layer/popover';
12
+ import { PopupSurface } from '@atlaskit/top-layer/popup-surface';
13
+ import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
14
+ import { useWidthFromAnchor } from '@atlaskit/top-layer/use-width-from-anchor';
12
15
  import { useRoleProps } from '../internal/top-layer-bridge';
16
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
13
17
  const overflowAutoStyles = null;
14
18
  const animation = slideAndFade();
15
19
 
@@ -73,7 +77,6 @@ export function PopupContentTopLayer({
73
77
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
74
78
  shouldDisableGpuAcceleration: _shouldDisableGpuAcceleration
75
79
  }) {
76
- const popupContainerRef = useRef(null);
77
80
  const [, setInitialFocusRef] = useState(null);
78
81
 
79
82
  // ── Placement conversion ──
@@ -110,31 +113,39 @@ export function PopupContentTopLayer({
110
113
  label,
111
114
  titleId
112
115
  });
116
+ const popoverRef = useRef(null);
117
+ const anchorRef = useContext(TriggerRefObjectContext);
118
+ useAnchorPosition({
119
+ anchorRef,
120
+ popoverRef,
121
+ placement: topLayerPlacement
122
+ });
123
+ useWidthFromAnchor({
124
+ mode: shouldFitContainer ? 'match-anchor' : 'none',
125
+ popoverRef,
126
+ anchorRef
127
+ });
113
128
 
114
129
  // Narrow to ForwardRefExoticComponent so JSX accepts the ref prop.
115
130
  // All popupComponent implementations use forwardRef per the PopupComponentProps contract.
116
131
  const Container = PopupContainer;
117
- if (!isOpen) {
118
- return null;
119
- }
120
- return /*#__PURE__*/React.createElement(Popup, {
121
- placement: topLayerPlacement,
122
- onClose: handleOnClose,
123
- testId: testId
124
- }, /*#__PURE__*/React.createElement(Popup.Content, _extends({}, roleProps, {
132
+ return /*#__PURE__*/React.createElement(Popover, _extends({
133
+ ref: popoverRef
134
+ }, roleProps, {
125
135
  isOpen: isOpen,
126
136
  animate: animation,
127
- testId: testId && `${testId}--content`,
128
- width: shouldFitContainer ? 'trigger' : 'content'
137
+ placement: topLayerPlacement,
138
+ onClose: handleOnClose,
139
+ testId: testId && `${testId}--content`
129
140
  }), Container ? /*#__PURE__*/React.createElement(Container, {
130
- ref: popupContainerRef,
131
141
  style: EMPTY_STYLE,
132
142
  id: providedId,
133
143
  "data-placement": placement,
134
144
  "data-testid": testId,
135
145
  tabIndex: autoFocus ? -1 : undefined,
136
146
  xcss: xcss
137
- }, children(contentProps)) : /*#__PURE__*/React.createElement(Popup.Surface, null, /*#__PURE__*/React.createElement("div", {
138
- className: ax([shouldFitViewport && "_1reo1wug _18m91wug"])
139
- }, children(contentProps)))));
147
+ }, children(contentProps)) : /*#__PURE__*/React.createElement(PopupSurface, null, /*#__PURE__*/React.createElement("div", {
148
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
149
+ className: ax([shouldFitViewport && "_1reo1wug _18m91wug", ax([xcss])])
150
+ }, children(contentProps))));
140
151
  }
@@ -98,8 +98,7 @@ export const PopupContent = ({
98
98
  shouldFlip: shouldFlip,
99
99
  shouldDisableGpuAcceleration: shouldDisableGpuAcceleration,
100
100
  isOpen: isOpen,
101
- id: id,
102
- triggerRef: triggerRef
101
+ id: id
103
102
  }, children);
104
103
  }
105
104
  if (!isOpen) {
@@ -1,4 +1,4 @@
1
- import React, { useContext } from 'react';
1
+ import React, { useCallback, useContext } from 'react';
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { Reference } from '@atlaskit/popper';
4
4
  import { useGetMemoizedMergedTriggerRefNew } from '../use-get-memoized-merged-trigger-ref-new';
@@ -6,6 +6,7 @@ import { IdContext } from './id-context';
6
6
  import { IsOpenContext } from './is-open-context';
7
7
  import { RoleContext } from './role-context';
8
8
  import { SetTriggerRefContext } from './set-trigger-ref-context';
9
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
9
10
  import { useEnsureIsInsidePopup } from './use-ensure-is-inside-popup';
10
11
  /**
11
12
  * __Popup trigger__
@@ -20,15 +21,36 @@ export const PopupTrigger = ({
20
21
  useEnsureIsInsidePopup();
21
22
  const id = useContext(IdContext);
22
23
  const setTriggerRef = useContext(SetTriggerRefContext);
24
+ const triggerRefObject = useContext(TriggerRefObjectContext);
23
25
  const isOpen = useContext(IsOpenContext);
24
26
  const getMergedTriggerRef = useGetMemoizedMergedTriggerRefNew();
25
27
  const role = useContext(RoleContext);
28
+
29
+ // Stable ref callback — avoids creating a new function instance on every render,
30
+ // which would cause React to detach and reattach the ref unnecessarily.
31
+ // triggerRefObject is a MutableRefObject (from useRef) so its identity never changes,
32
+ // meaning this callback is effectively created once per mount.
33
+ const triggerRef = useCallback(node => {
34
+ triggerRefObject.current = node;
35
+ }, [triggerRefObject]);
36
+ const ariaHasPopup = role === 'dialog' && fg('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true;
37
+
38
+ // When the top-layer flag is on, bypass Popper's <Reference> entirely.
39
+ // We only need to set triggerRefObject.current — no Popper ref merging needed.
40
+ if (fg('platform-dst-top-layer')) {
41
+ return /*#__PURE__*/React.createElement(React.Fragment, null, children({
42
+ ref: triggerRef,
43
+ 'aria-controls': id,
44
+ 'aria-expanded': isOpen,
45
+ 'aria-haspopup': ariaHasPopup
46
+ }));
47
+ }
26
48
  return /*#__PURE__*/React.createElement(Reference, null, ({
27
49
  ref
28
50
  }) => children({
29
51
  ref: getMergedTriggerRef(ref, setTriggerRef),
30
52
  'aria-controls': id,
31
53
  'aria-expanded': isOpen,
32
- 'aria-haspopup': role === 'dialog' && fg('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true
54
+ 'aria-haspopup': ariaHasPopup
33
55
  }));
34
56
  };
@@ -1,5 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React, { useRef, useState } from 'react';
2
2
  import { useId } from '@atlaskit/ds-lib/use-id';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
3
4
  import { Manager } from '@atlaskit/popper';
4
5
  import { IdContext } from './id-context';
5
6
  import { EnsureIsInsidePopupContext } from './is-inside-popup-context';
@@ -7,6 +8,7 @@ import { IsOpenContext } from './is-open-context';
7
8
  import { RoleContext } from './role-context';
8
9
  import { SetTriggerRefContext } from './set-trigger-ref-context';
9
10
  import { TriggerRefContext } from './trigger-ref-context';
11
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
10
12
  /**
11
13
  * __Popup__
12
14
  *
@@ -33,6 +35,15 @@ export const Popup = ({
33
35
  role
34
36
  }) => {
35
37
  const [triggerRef, setTriggerRef] = useState(null);
38
+
39
+ // When the top-layer flag is on, we maintain a stable RefObject for the
40
+ // trigger element, provided through TriggerRefObjectContext. Unlike the
41
+ // state-based triggerRef (which causes a re-render when the trigger mounts),
42
+ // this ref has a stable identity so useAnchorPosition's useLayoutEffect dep
43
+ // array never changes — the effect runs once on mount when the trigger element
44
+ // is already populated. PopupTrigger sets this ref directly instead of calling
45
+ // the state setter, avoiding an unnecessary re-render.
46
+ const triggerRefObject = useRef(null);
36
47
  const generatedId = useId();
37
48
  const id = providedId || generatedId;
38
49
  return /*#__PURE__*/React.createElement(RoleContext.Provider, {
@@ -47,5 +58,10 @@ export const Popup = ({
47
58
  value: setTriggerRef
48
59
  }, /*#__PURE__*/React.createElement(IsOpenContext.Provider, {
49
60
  value: isOpen
50
- }, /*#__PURE__*/React.createElement(Manager, null, children)))))));
61
+ }, fg('platform-dst-top-layer') ?
62
+ /*#__PURE__*/
63
+ // Skip Popper's <Manager> when Top Layer is being used.
64
+ React.createElement(TriggerRefObjectContext.Provider, {
65
+ value: triggerRefObject
66
+ }, children) : /*#__PURE__*/React.createElement(Manager, null, children)))))));
51
67
  };
@@ -0,0 +1,14 @@
1
+ /* eslint-disable @repo/internal/react/require-jsdoc */
2
+ import { createContext } from 'react';
3
+
4
+ /**
5
+ * Provides the trigger element as a stable `MutableRefObject<HTMLElement | null>`
6
+ * for the top-layer path. Unlike `TriggerRefContext` (which provides
7
+ * `HTMLElement | null` as React state and causes a re-render when the trigger
8
+ * mounts), this context provides a ref object whose identity is stable across
9
+ * renders — allowing `useAnchorPosition` to receive a stable `anchorRef`
10
+ * without needing to create a new ref wrapper on every `triggerRef` state change.
11
+ */
12
+ export const TriggerRefObjectContext = /*#__PURE__*/createContext({
13
+ current: null
14
+ });
@@ -178,7 +178,7 @@ export const PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer({
178
178
  isOpen: effectiveIsOpen,
179
179
  animate: animation,
180
180
  testId: testId && `${testId}--content`,
181
- width: shouldFitContainer ? 'trigger' : 'content'
181
+ widthFromAnchor: shouldFitContainer ? 'match-anchor' : 'none'
182
182
  }), Container ? /*#__PURE__*/React.createElement(Container, {
183
183
  ref: popupContainerRef,
184
184
  style: EMPTY_STYLE,
@@ -3,14 +3,18 @@ import _extends from "@babel/runtime/helpers/extends";
3
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
4
4
  import "./popup-content-top-layer.compiled.css";
5
5
  import * as React from 'react';
6
- import { ax, ix } from "@compiled/react/runtime";
7
- import { useCallback, useMemo, useRef, useState } from 'react';
6
+ import { useCallback, useContext, useMemo, useRef, useState } from 'react';
7
+ import { ax, ix } from '@compiled/react/runtime';
8
8
  import noop from '@atlaskit/ds-lib/noop';
9
9
  import { slideAndFade } from '@atlaskit/top-layer/animations';
10
10
  import { createPopoverCloseEvent } from '@atlaskit/top-layer/create-close-event';
11
11
  import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
12
- import { Popup } from '@atlaskit/top-layer/popup';
12
+ import { Popover } from '@atlaskit/top-layer/popover';
13
+ import { PopupSurface } from '@atlaskit/top-layer/popup-surface';
14
+ import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
15
+ import { useWidthFromAnchor } from '@atlaskit/top-layer/use-width-from-anchor';
13
16
  import { useRoleProps } from '../internal/top-layer-bridge';
17
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
14
18
  var overflowAutoStyles = null;
15
19
  var animation = slideAndFade();
16
20
 
@@ -54,7 +58,6 @@ export function PopupContentTopLayer(_ref) {
54
58
  _shouldFlip = _ref.shouldFlip,
55
59
  _appearance = _ref.appearance,
56
60
  _shouldDisableGpuAcceleration = _ref.shouldDisableGpuAcceleration;
57
- var popupContainerRef = useRef(null);
58
61
  var _useState = useState(null),
59
62
  _useState2 = _slicedToArray(_useState, 2),
60
63
  setInitialFocusRef = _useState2[1];
@@ -96,31 +99,39 @@ export function PopupContentTopLayer(_ref) {
96
99
  label: label,
97
100
  titleId: titleId
98
101
  });
102
+ var popoverRef = useRef(null);
103
+ var anchorRef = useContext(TriggerRefObjectContext);
104
+ useAnchorPosition({
105
+ anchorRef: anchorRef,
106
+ popoverRef: popoverRef,
107
+ placement: topLayerPlacement
108
+ });
109
+ useWidthFromAnchor({
110
+ mode: shouldFitContainer ? 'match-anchor' : 'none',
111
+ popoverRef: popoverRef,
112
+ anchorRef: anchorRef
113
+ });
99
114
 
100
115
  // Narrow to ForwardRefExoticComponent so JSX accepts the ref prop.
101
116
  // All popupComponent implementations use forwardRef per the PopupComponentProps contract.
102
117
  var Container = PopupContainer;
103
- if (!isOpen) {
104
- return null;
105
- }
106
- return /*#__PURE__*/React.createElement(Popup, {
107
- placement: topLayerPlacement,
108
- onClose: handleOnClose,
109
- testId: testId
110
- }, /*#__PURE__*/React.createElement(Popup.Content, _extends({}, roleProps, {
118
+ return /*#__PURE__*/React.createElement(Popover, _extends({
119
+ ref: popoverRef
120
+ }, roleProps, {
111
121
  isOpen: isOpen,
112
122
  animate: animation,
113
- testId: testId && "".concat(testId, "--content"),
114
- width: shouldFitContainer ? 'trigger' : 'content'
123
+ placement: topLayerPlacement,
124
+ onClose: handleOnClose,
125
+ testId: testId && "".concat(testId, "--content")
115
126
  }), Container ? /*#__PURE__*/React.createElement(Container, {
116
- ref: popupContainerRef,
117
127
  style: EMPTY_STYLE,
118
128
  id: providedId,
119
129
  "data-placement": placement,
120
130
  "data-testid": testId,
121
131
  tabIndex: autoFocus ? -1 : undefined,
122
132
  xcss: xcss
123
- }, children(contentProps)) : /*#__PURE__*/React.createElement(Popup.Surface, null, /*#__PURE__*/React.createElement("div", {
124
- className: ax([shouldFitViewport && "_1reo1wug _18m91wug"])
125
- }, children(contentProps)))));
133
+ }, children(contentProps)) : /*#__PURE__*/React.createElement(PopupSurface, null, /*#__PURE__*/React.createElement("div", {
134
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
135
+ className: ax([shouldFitViewport && "_1reo1wug _18m91wug", ax([xcss])])
136
+ }, children(contentProps))));
126
137
  }
@@ -105,8 +105,7 @@ export var PopupContent = function PopupContent(_ref) {
105
105
  shouldFlip: shouldFlip,
106
106
  shouldDisableGpuAcceleration: shouldDisableGpuAcceleration,
107
107
  isOpen: isOpen,
108
- id: id,
109
- triggerRef: triggerRef
108
+ id: id
110
109
  }, children);
111
110
  }
112
111
  if (!isOpen) {
@@ -1,4 +1,4 @@
1
- import React, { useContext } from 'react';
1
+ import React, { useCallback, useContext } from 'react';
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { Reference } from '@atlaskit/popper';
4
4
  import { useGetMemoizedMergedTriggerRefNew } from '../use-get-memoized-merged-trigger-ref-new';
@@ -6,6 +6,7 @@ import { IdContext } from './id-context';
6
6
  import { IsOpenContext } from './is-open-context';
7
7
  import { RoleContext } from './role-context';
8
8
  import { SetTriggerRefContext } from './set-trigger-ref-context';
9
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
9
10
  import { useEnsureIsInsidePopup } from './use-ensure-is-inside-popup';
10
11
  /**
11
12
  * __Popup trigger__
@@ -19,16 +20,37 @@ export var PopupTrigger = function PopupTrigger(_ref) {
19
20
  useEnsureIsInsidePopup();
20
21
  var id = useContext(IdContext);
21
22
  var setTriggerRef = useContext(SetTriggerRefContext);
23
+ var triggerRefObject = useContext(TriggerRefObjectContext);
22
24
  var isOpen = useContext(IsOpenContext);
23
25
  var getMergedTriggerRef = useGetMemoizedMergedTriggerRefNew();
24
26
  var role = useContext(RoleContext);
27
+
28
+ // Stable ref callback — avoids creating a new function instance on every render,
29
+ // which would cause React to detach and reattach the ref unnecessarily.
30
+ // triggerRefObject is a MutableRefObject (from useRef) so its identity never changes,
31
+ // meaning this callback is effectively created once per mount.
32
+ var triggerRef = useCallback(function (node) {
33
+ triggerRefObject.current = node;
34
+ }, [triggerRefObject]);
35
+ var ariaHasPopup = role === 'dialog' && fg('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true;
36
+
37
+ // When the top-layer flag is on, bypass Popper's <Reference> entirely.
38
+ // We only need to set triggerRefObject.current — no Popper ref merging needed.
39
+ if (fg('platform-dst-top-layer')) {
40
+ return /*#__PURE__*/React.createElement(React.Fragment, null, children({
41
+ ref: triggerRef,
42
+ 'aria-controls': id,
43
+ 'aria-expanded': isOpen,
44
+ 'aria-haspopup': ariaHasPopup
45
+ }));
46
+ }
25
47
  return /*#__PURE__*/React.createElement(Reference, null, function (_ref2) {
26
48
  var ref = _ref2.ref;
27
49
  return children({
28
50
  ref: getMergedTriggerRef(ref, setTriggerRef),
29
51
  'aria-controls': id,
30
52
  'aria-expanded': isOpen,
31
- 'aria-haspopup': role === 'dialog' && fg('platform_dst_nav4_flyout_menu_slots_close_button') ? 'dialog' : true
53
+ 'aria-haspopup': ariaHasPopup
32
54
  });
33
55
  });
34
56
  };
@@ -1,6 +1,7 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
- import React, { useState } from 'react';
2
+ import React, { useRef, useState } from 'react';
3
3
  import { useId } from '@atlaskit/ds-lib/use-id';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { Manager } from '@atlaskit/popper';
5
6
  import { IdContext } from './id-context';
6
7
  import { EnsureIsInsidePopupContext } from './is-inside-popup-context';
@@ -8,6 +9,7 @@ import { IsOpenContext } from './is-open-context';
8
9
  import { RoleContext } from './role-context';
9
10
  import { SetTriggerRefContext } from './set-trigger-ref-context';
10
11
  import { TriggerRefContext } from './trigger-ref-context';
12
+ import { TriggerRefObjectContext } from './trigger-ref-object-context';
11
13
  /**
12
14
  * __Popup__
13
15
  *
@@ -37,6 +39,15 @@ export var Popup = function Popup(_ref) {
37
39
  _useState2 = _slicedToArray(_useState, 2),
38
40
  triggerRef = _useState2[0],
39
41
  setTriggerRef = _useState2[1];
42
+
43
+ // When the top-layer flag is on, we maintain a stable RefObject for the
44
+ // trigger element, provided through TriggerRefObjectContext. Unlike the
45
+ // state-based triggerRef (which causes a re-render when the trigger mounts),
46
+ // this ref has a stable identity so useAnchorPosition's useLayoutEffect dep
47
+ // array never changes — the effect runs once on mount when the trigger element
48
+ // is already populated. PopupTrigger sets this ref directly instead of calling
49
+ // the state setter, avoiding an unnecessary re-render.
50
+ var triggerRefObject = useRef(null);
40
51
  var generatedId = useId();
41
52
  var id = providedId || generatedId;
42
53
  return /*#__PURE__*/React.createElement(RoleContext.Provider, {
@@ -51,5 +62,10 @@ export var Popup = function Popup(_ref) {
51
62
  value: setTriggerRef
52
63
  }, /*#__PURE__*/React.createElement(IsOpenContext.Provider, {
53
64
  value: isOpen
54
- }, /*#__PURE__*/React.createElement(Manager, null, children)))))));
65
+ }, fg('platform-dst-top-layer') ?
66
+ /*#__PURE__*/
67
+ // Skip Popper's <Manager> when Top Layer is being used.
68
+ React.createElement(TriggerRefObjectContext.Provider, {
69
+ value: triggerRefObject
70
+ }, children) : /*#__PURE__*/React.createElement(Manager, null, children)))))));
55
71
  };
@@ -0,0 +1,14 @@
1
+ /* eslint-disable @repo/internal/react/require-jsdoc */
2
+ import { createContext } from 'react';
3
+
4
+ /**
5
+ * Provides the trigger element as a stable `MutableRefObject<HTMLElement | null>`
6
+ * for the top-layer path. Unlike `TriggerRefContext` (which provides
7
+ * `HTMLElement | null` as React state and causes a re-render when the trigger
8
+ * mounts), this context provides a ref object whose identity is stable across
9
+ * renders — allowing `useAnchorPosition` to receive a stable `anchorRef`
10
+ * without needing to create a new ref wrapper on every `triggerRef` state change.
11
+ */
12
+ export var TriggerRefObjectContext = /*#__PURE__*/createContext({
13
+ current: null
14
+ });
@@ -161,7 +161,7 @@ export var PopupTopLayer = /*#__PURE__*/memo(function PopupTopLayer(_ref) {
161
161
  isOpen: effectiveIsOpen,
162
162
  animate: animation,
163
163
  testId: testId && "".concat(testId, "--content"),
164
- width: shouldFitContainer ? 'trigger' : 'content'
164
+ widthFromAnchor: shouldFitContainer ? 'match-anchor' : 'none'
165
165
  }), Container ? /*#__PURE__*/React.createElement(Container, {
166
166
  ref: popupContainerRef,
167
167
  style: EMPTY_STYLE,
@@ -17,5 +17,4 @@ export declare function PopupContentTopLayer({ xcss, children, offset: offsetPro
17
17
  shouldDisableGpuAcceleration?: boolean;
18
18
  isOpen: boolean;
19
19
  id: string | undefined;
20
- triggerRef: HTMLElement | null;
21
20
  }): ReactNode;
@@ -0,0 +1,10 @@
1
+ import { type Context, type MutableRefObject } from 'react';
2
+ /**
3
+ * Provides the trigger element as a stable `MutableRefObject<HTMLElement | null>`
4
+ * for the top-layer path. Unlike `TriggerRefContext` (which provides
5
+ * `HTMLElement | null` as React state and causes a re-render when the trigger
6
+ * mounts), this context provides a ref object whose identity is stable across
7
+ * renders — allowing `useAnchorPosition` to receive a stable `anchorRef`
8
+ * without needing to create a new ref wrapper on every `triggerRef` state change.
9
+ */
10
+ export declare const TriggerRefObjectContext: Context<MutableRefObject<HTMLElement | null>>;
@@ -17,5 +17,4 @@ export declare function PopupContentTopLayer({ xcss, children, offset: offsetPro
17
17
  shouldDisableGpuAcceleration?: boolean;
18
18
  isOpen: boolean;
19
19
  id: string | undefined;
20
- triggerRef: HTMLElement | null;
21
20
  }): ReactNode;
@@ -0,0 +1,10 @@
1
+ import { type Context, type MutableRefObject } from 'react';
2
+ /**
3
+ * Provides the trigger element as a stable `MutableRefObject<HTMLElement | null>`
4
+ * for the top-layer path. Unlike `TriggerRefContext` (which provides
5
+ * `HTMLElement | null` as React state and causes a re-render when the trigger
6
+ * mounts), this context provides a ref object whose identity is stable across
7
+ * renders — allowing `useAnchorPosition` to receive a stable `anchorRef`
8
+ * without needing to create a new ref wrapper on every `triggerRef` state change.
9
+ */
10
+ export declare const TriggerRefObjectContext: Context<MutableRefObject<HTMLElement | null>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/popup",
3
- "version": "4.19.1",
3
+ "version": "4.20.1",
4
4
  "description": "A popup displays brief content in an overlay.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -48,7 +48,7 @@
48
48
  "@atlaskit/portal": "^5.5.0",
49
49
  "@atlaskit/primitives": "^19.0.0",
50
50
  "@atlaskit/tokens": "^13.0.0",
51
- "@atlaskit/top-layer": "^0.6.0",
51
+ "@atlaskit/top-layer": "^0.8.0",
52
52
  "@babel/runtime": "^7.0.0",
53
53
  "@compiled/react": "^0.20.0",
54
54
  "bind-event-listener": "^3.0.0",
@@ -69,9 +69,9 @@
69
69
  "@atlaskit/docs": "^11.8.0",
70
70
  "@atlaskit/form": "^15.5.0",
71
71
  "@atlaskit/heading": "^5.4.0",
72
- "@atlaskit/icon": "^34.5.0",
72
+ "@atlaskit/icon": "^35.0.0",
73
73
  "@atlaskit/link": "^3.4.0",
74
- "@atlaskit/modal-dialog": "^15.0.0",
74
+ "@atlaskit/modal-dialog": "^15.1.0",
75
75
  "@atlaskit/section-message": "^8.12.0",
76
76
  "@atlaskit/textfield": "^8.3.0",
77
77
  "@atlaskit/toggle": "^15.6.0",
@@ -79,7 +79,7 @@
79
79
  "@atlassian/react-compiler-gating": "workspace:^",
80
80
  "@atlassian/ssr-tests": "workspace:^",
81
81
  "@atlassian/structured-docs-types": "workspace:^",
82
- "@atlassian/testing-library": "^0.5.0",
82
+ "@atlassian/testing-library": "^0.6.0",
83
83
  "@testing-library/react": "^16.3.0",
84
84
  "@testing-library/user-event": "^14.4.3",
85
85
  "ast-types": "^0.13.3",