@atlaskit/top-layer 0.6.3 → 0.7.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 (30) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/entry-points/use-width-from-anchor.js +12 -0
  3. package/dist/cjs/internal/use-anchor-position.js +0 -6
  4. package/dist/cjs/internal/use-width-from-anchor.js +107 -0
  5. package/dist/cjs/popover/popover.js +3 -1
  6. package/dist/cjs/popup/popup-content.js +8 -35
  7. package/dist/es2019/entry-points/use-width-from-anchor.js +1 -0
  8. package/dist/es2019/internal/use-anchor-position.js +0 -6
  9. package/dist/es2019/internal/use-width-from-anchor.js +103 -0
  10. package/dist/es2019/popover/popover.js +3 -1
  11. package/dist/es2019/popup/popup-content.js +7 -34
  12. package/dist/esm/entry-points/use-width-from-anchor.js +1 -0
  13. package/dist/esm/internal/use-anchor-position.js +0 -6
  14. package/dist/esm/internal/use-width-from-anchor.js +102 -0
  15. package/dist/esm/popover/popover.js +3 -1
  16. package/dist/esm/popup/popup-content.js +8 -35
  17. package/dist/types/entry-points/use-width-from-anchor.d.ts +1 -0
  18. package/dist/types/internal/ensure-preset-styles.d.ts +1 -1
  19. package/dist/types/internal/use-width-from-anchor.d.ts +20 -0
  20. package/dist/types/popover/popover.d.ts +3 -1
  21. package/dist/types/popover/types.d.ts +13 -0
  22. package/dist/types/popup/types.d.ts +8 -8
  23. package/dist/types-ts4.5/entry-points/use-width-from-anchor.d.ts +1 -0
  24. package/dist/types-ts4.5/internal/ensure-preset-styles.d.ts +1 -1
  25. package/dist/types-ts4.5/internal/use-width-from-anchor.d.ts +20 -0
  26. package/dist/types-ts4.5/popover/popover.d.ts +3 -1
  27. package/dist/types-ts4.5/popover/types.d.ts +13 -0
  28. package/dist/types-ts4.5/popup/types.d.ts +8 -8
  29. package/package.json +2 -2
  30. package/use-width-from-anchor/package.json +17 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`43e486948865a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/43e486948865a) -
8
+ Added a new `useWidthFromAnchor` hook exported from `@atlaskit/top-layer/use-width-from-anchor`,
9
+ for setting the width of a popover element relative to its anchor trigger. Supports three modes:
10
+ `'none'` (no width constraint), `'match-anchor'` (popover matches anchor width exactly), and
11
+ `'min-anchor'` (popover is at least as wide as the anchor).
12
+
13
+ Renamed the `width` prop on `Popup.Content` to `widthFromAnchor`, and updated its type from a
14
+ string enum `'content' | 'trigger' | 'min-trigger'` to `'none' | 'match-anchor' | 'min-anchor'` to
15
+ match the new hook.
16
+
3
17
  ## 0.6.3
4
18
 
5
19
  ### Patch Changes
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "useWidthFromAnchor", {
7
+ enumerable: true,
8
+ get: function get() {
9
+ return _useWidthFromAnchor.useWidthFromAnchor;
10
+ }
11
+ });
12
+ var _useWidthFromAnchor = require("../internal/use-width-from-anchor");
@@ -330,12 +330,6 @@ function useAnchorPosition(_ref5) {
330
330
  property: 'inset',
331
331
  value: 'auto'
332
332
  },
333
- // Ensures the popup's margin box overflows the viewport when its span region is too
334
- // narrow, triggering position-try-fallbacks correctly rather than wrapping content.
335
- {
336
- property: 'min-inline-size',
337
- value: 'max-content'
338
- },
339
333
  // Main-axis gap between the popover and its trigger
340
334
  {
341
335
  property: gap.property,
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useWidthFromAnchor = useWidthFromAnchor;
7
+ var _react = require("react");
8
+ var _setStyle = require("./set-style");
9
+ /**
10
+ * Sets the width of a popover element relative to its anchor element.
11
+ *
12
+ * - `'match-anchor'` → `width: anchor-size(width)` — popover matches anchor width exactly.
13
+ * - `'min-anchor'` → `min-width: anchor-size(width)` — popover is at least as wide as anchor,
14
+ * but can grow wider.
15
+ * - `'none'` → no explicit width is set (default behaviour)
16
+ *
17
+ * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
18
+ * one-off measurement of `anchor.offsetWidth`.
19
+ *
20
+ * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
21
+ */
22
+ function useWidthFromAnchor(_ref) {
23
+ var mode = _ref.mode,
24
+ popoverRef = _ref.popoverRef,
25
+ anchorRef = _ref.anchorRef;
26
+ (0, _react.useLayoutEffect)(function () {
27
+ var node = popoverRef.current;
28
+ if (!node) {
29
+ return;
30
+ }
31
+
32
+ // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
33
+ // so the JS fallback fires correctly in browsers that support anchor
34
+ // positioning but not anchor sizing.
35
+ var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
36
+ if (mode === 'match-anchor') {
37
+ // Set an exact width matching the anchor.
38
+ // `min-inline-size` is intentionally NOT set here — it would override the
39
+ // explicit width constraint, allowing the popover to grow past the anchor's width.
40
+ if (supportsAnchorSize) {
41
+ return (0, _setStyle.setStyle)({
42
+ el: node,
43
+ styles: [{
44
+ property: 'width',
45
+ value: 'anchor-size(width)'
46
+ }]
47
+ });
48
+ }
49
+
50
+ // JS fallback: one-off read of the anchor's width.
51
+ var anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
52
+ if (anchor) {
53
+ return (0, _setStyle.setStyle)({
54
+ el: node,
55
+ styles: [{
56
+ property: 'width',
57
+ value: "".concat(anchor.offsetWidth, "px")
58
+ }]
59
+ });
60
+ }
61
+ return;
62
+ }
63
+
64
+ // For 'min-anchor' and 'none', apply `min-inline-size: max-content` to ensure
65
+ // correct position-try-fallbacks behaviour (popover margin box overflows the
66
+ // viewport when its span region is too narrow, rather than wrapping content).
67
+ var minInlineSizeStyle = {
68
+ property: 'min-inline-size',
69
+ value: 'max-content'
70
+ };
71
+ if (mode === 'min-anchor') {
72
+ // Set a minimum width matching the anchor. The popover can grow wider
73
+ // if its content requires it.
74
+ if (supportsAnchorSize) {
75
+ return (0, _setStyle.setStyle)({
76
+ el: node,
77
+ styles: [minInlineSizeStyle, {
78
+ property: 'min-width',
79
+ value: 'anchor-size(width)'
80
+ }]
81
+ });
82
+ }
83
+ var _anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
84
+ if (_anchor) {
85
+ return (0, _setStyle.setStyle)({
86
+ el: node,
87
+ styles: [minInlineSizeStyle, {
88
+ property: 'min-width',
89
+ value: "".concat(_anchor.offsetWidth, "px")
90
+ }]
91
+ });
92
+ }
93
+
94
+ // No anchor available in JS fallback — still apply min-inline-size.
95
+ return (0, _setStyle.setStyle)({
96
+ el: node,
97
+ styles: [minInlineSizeStyle]
98
+ });
99
+ }
100
+
101
+ // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
102
+ return (0, _setStyle.setStyle)({
103
+ el: node,
104
+ styles: [minInlineSizeStyle]
105
+ });
106
+ }, [mode, popoverRef, anchorRef]);
107
+ }
@@ -44,7 +44,9 @@ var styles = {
44
44
  * Unopinionated top-layer primitive.
45
45
  *
46
46
  * Manages only visibility (isOpen) and animation (animate). Has no knowledge
47
- * of positioning — compose with `useAnchorPosition` for anchor positioning.
47
+ * of positioning — compose with `useAnchorPosition` for anchor positioning
48
+ * and optionally `useWidthFromAnchor` for setting the width of a popover element
49
+ * relative to its anchor trigger.
48
50
  *
49
51
  * Visibility is driven by the `isOpen` prop:
50
52
  * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
@@ -14,6 +14,7 @@ var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
14
14
  var _roleTypes = require("../internal/role-types");
15
15
  var _useAnchorPosition = require("../internal/use-anchor-position");
16
16
  var _usePresetStyles = require("../internal/use-preset-styles");
17
+ var _useWidthFromAnchor = require("../internal/use-width-from-anchor");
17
18
  var _popover = require("../popover/popover");
18
19
  var _popupContext = require("./popup-context");
19
20
  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); }
@@ -33,8 +34,8 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
33
34
  animate = _ref.animate,
34
35
  arrow = _ref.arrow,
35
36
  mode = _ref.mode,
36
- _ref$width = _ref.width,
37
- width = _ref$width === void 0 ? 'content' : _ref$width,
37
+ _ref$widthFromAnchor = _ref.widthFromAnchor,
38
+ widthFromAnchor = _ref$widthFromAnchor === void 0 ? 'none' : _ref$widthFromAnchor,
38
39
  xcss = _ref.xcss,
39
40
  isOpenProp = _ref.isOpen,
40
41
  triggerRefProp = _ref.triggerRef,
@@ -140,39 +141,11 @@ var PopupContent = exports.PopupContent = /*#__PURE__*/(0, _react.forwardRef)(fu
140
141
  forceFallbackPositioning: forceFallbackPositioning,
141
142
  arrow: arrowPreset !== null && arrowPreset !== void 0 ? arrowPreset : undefined
142
143
  });
143
-
144
- // ── Width: match trigger ──
145
- // When width="trigger" or width="min-trigger", size the popover relative
146
- // to the trigger element. Uses CSS `anchor-size(width)` when supported,
147
- // falling back to a one-off measurement of the trigger's offsetWidth.
148
- (0, _react.useLayoutEffect)(function () {
149
- if (width === 'content') {
150
- return;
151
- }
152
- var node = popoverRef.current;
153
- if (!node) {
154
- return;
155
- }
156
- var cssProperty = width === 'trigger' ? 'width' : 'min-width';
157
- var anchorValue = 'anchor-size(width)';
158
-
159
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
160
- // so the JS fallback fires correctly in browsers that support anchor
161
- // positioning but not anchor sizing.
162
- var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
163
- if (supportsAnchorSize) {
164
- node.style.setProperty(cssProperty, anchorValue);
165
- } else {
166
- // JS fallback: one-off read of the trigger's width.
167
- var trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
168
- if (trigger) {
169
- node.style.setProperty(cssProperty, "".concat(trigger.offsetWidth, "px"));
170
- }
171
- }
172
- return function () {
173
- node.style.removeProperty(cssProperty);
174
- };
175
- }, [width, popoverRef, triggerRef]);
144
+ (0, _useWidthFromAnchor.useWidthFromAnchor)({
145
+ mode: widthFromAnchor,
146
+ popoverRef: popoverRef,
147
+ anchorRef: triggerRef
148
+ });
176
149
 
177
150
  // Merge: the popoverRef (shared with trigger + positioning) and the forwarded ref.
178
151
  var combinedRef = (0, _mergeRefs.default)([popoverRef, ref]);
@@ -0,0 +1 @@
1
+ export { useWidthFromAnchor } from '../internal/use-width-from-anchor';
@@ -323,12 +323,6 @@ export function useAnchorPosition({
323
323
  property: 'inset',
324
324
  value: 'auto'
325
325
  },
326
- // Ensures the popup's margin box overflows the viewport when its span region is too
327
- // narrow, triggering position-try-fallbacks correctly rather than wrapping content.
328
- {
329
- property: 'min-inline-size',
330
- value: 'max-content'
331
- },
332
326
  // Main-axis gap between the popover and its trigger
333
327
  {
334
328
  property: gap.property,
@@ -0,0 +1,103 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import { setStyle } from './set-style';
3
+
4
+ /**
5
+ * Sets the width of a popover element relative to its anchor element.
6
+ *
7
+ * - `'match-anchor'` → `width: anchor-size(width)` — popover matches anchor width exactly.
8
+ * - `'min-anchor'` → `min-width: anchor-size(width)` — popover is at least as wide as anchor,
9
+ * but can grow wider.
10
+ * - `'none'` → no explicit width is set (default behaviour)
11
+ *
12
+ * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
13
+ * one-off measurement of `anchor.offsetWidth`.
14
+ *
15
+ * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
16
+ */
17
+ export function useWidthFromAnchor({
18
+ mode,
19
+ popoverRef,
20
+ anchorRef
21
+ }) {
22
+ useLayoutEffect(() => {
23
+ const node = popoverRef.current;
24
+ if (!node) {
25
+ return;
26
+ }
27
+
28
+ // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
29
+ // so the JS fallback fires correctly in browsers that support anchor
30
+ // positioning but not anchor sizing.
31
+ const supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
32
+ if (mode === 'match-anchor') {
33
+ // Set an exact width matching the anchor.
34
+ // `min-inline-size` is intentionally NOT set here — it would override the
35
+ // explicit width constraint, allowing the popover to grow past the anchor's width.
36
+ if (supportsAnchorSize) {
37
+ return setStyle({
38
+ el: node,
39
+ styles: [{
40
+ property: 'width',
41
+ value: 'anchor-size(width)'
42
+ }]
43
+ });
44
+ }
45
+
46
+ // JS fallback: one-off read of the anchor's width.
47
+ const anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
48
+ if (anchor) {
49
+ return setStyle({
50
+ el: node,
51
+ styles: [{
52
+ property: 'width',
53
+ value: `${anchor.offsetWidth}px`
54
+ }]
55
+ });
56
+ }
57
+ return;
58
+ }
59
+
60
+ // For 'min-anchor' and 'none', apply `min-inline-size: max-content` to ensure
61
+ // correct position-try-fallbacks behaviour (popover margin box overflows the
62
+ // viewport when its span region is too narrow, rather than wrapping content).
63
+ const minInlineSizeStyle = {
64
+ property: 'min-inline-size',
65
+ value: 'max-content'
66
+ };
67
+ if (mode === 'min-anchor') {
68
+ // Set a minimum width matching the anchor. The popover can grow wider
69
+ // if its content requires it.
70
+ if (supportsAnchorSize) {
71
+ return setStyle({
72
+ el: node,
73
+ styles: [minInlineSizeStyle, {
74
+ property: 'min-width',
75
+ value: 'anchor-size(width)'
76
+ }]
77
+ });
78
+ }
79
+ const anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
80
+ if (anchor) {
81
+ return setStyle({
82
+ el: node,
83
+ styles: [minInlineSizeStyle, {
84
+ property: 'min-width',
85
+ value: `${anchor.offsetWidth}px`
86
+ }]
87
+ });
88
+ }
89
+
90
+ // No anchor available in JS fallback — still apply min-inline-size.
91
+ return setStyle({
92
+ el: node,
93
+ styles: [minInlineSizeStyle]
94
+ });
95
+ }
96
+
97
+ // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
98
+ return setStyle({
99
+ el: node,
100
+ styles: [minInlineSizeStyle]
101
+ });
102
+ }, [mode, popoverRef, anchorRef]);
103
+ }
@@ -33,7 +33,9 @@ const styles = {
33
33
  * Unopinionated top-layer primitive.
34
34
  *
35
35
  * Manages only visibility (isOpen) and animation (animate). Has no knowledge
36
- * of positioning — compose with `useAnchorPosition` for anchor positioning.
36
+ * of positioning — compose with `useAnchorPosition` for anchor positioning
37
+ * and optionally `useWidthFromAnchor` for setting the width of a popover element
38
+ * relative to its anchor trigger.
37
39
  *
38
40
  * Visibility is driven by the `isOpen` prop:
39
41
  * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
@@ -6,6 +6,7 @@ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
6
  import { roleToAriaHasPopup } from '../internal/role-types';
7
7
  import { useAnchorPosition } from '../internal/use-anchor-position';
8
8
  import { usePresetStyles } from '../internal/use-preset-styles';
9
+ import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
9
10
  import { Popover } from '../popover/popover';
10
11
  import { useMaybePopupContext } from './popup-context';
11
12
  /**
@@ -23,7 +24,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
23
24
  animate,
24
25
  arrow,
25
26
  mode,
26
- width = 'content',
27
+ widthFromAnchor = 'none',
27
28
  xcss,
28
29
  isOpen: isOpenProp,
29
30
  triggerRef: triggerRefProp,
@@ -131,39 +132,11 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
131
132
  forceFallbackPositioning,
132
133
  arrow: arrowPreset !== null && arrowPreset !== void 0 ? arrowPreset : undefined
133
134
  });
134
-
135
- // ── Width: match trigger ──
136
- // When width="trigger" or width="min-trigger", size the popover relative
137
- // to the trigger element. Uses CSS `anchor-size(width)` when supported,
138
- // falling back to a one-off measurement of the trigger's offsetWidth.
139
- useLayoutEffect(() => {
140
- if (width === 'content') {
141
- return;
142
- }
143
- const node = popoverRef.current;
144
- if (!node) {
145
- return;
146
- }
147
- const cssProperty = width === 'trigger' ? 'width' : 'min-width';
148
- const anchorValue = 'anchor-size(width)';
149
-
150
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
151
- // so the JS fallback fires correctly in browsers that support anchor
152
- // positioning but not anchor sizing.
153
- const supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
154
- if (supportsAnchorSize) {
155
- node.style.setProperty(cssProperty, anchorValue);
156
- } else {
157
- // JS fallback: one-off read of the trigger's width.
158
- const trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
159
- if (trigger) {
160
- node.style.setProperty(cssProperty, `${trigger.offsetWidth}px`);
161
- }
162
- }
163
- return () => {
164
- node.style.removeProperty(cssProperty);
165
- };
166
- }, [width, popoverRef, triggerRef]);
135
+ useWidthFromAnchor({
136
+ mode: widthFromAnchor,
137
+ popoverRef,
138
+ anchorRef: triggerRef
139
+ });
167
140
 
168
141
  // Merge: the popoverRef (shared with trigger + positioning) and the forwarded ref.
169
142
  const combinedRef = mergeRefs([popoverRef, ref]);
@@ -0,0 +1 @@
1
+ export { useWidthFromAnchor } from '../internal/use-width-from-anchor';
@@ -316,12 +316,6 @@ export function useAnchorPosition(_ref5) {
316
316
  property: 'inset',
317
317
  value: 'auto'
318
318
  },
319
- // Ensures the popup's margin box overflows the viewport when its span region is too
320
- // narrow, triggering position-try-fallbacks correctly rather than wrapping content.
321
- {
322
- property: 'min-inline-size',
323
- value: 'max-content'
324
- },
325
319
  // Main-axis gap between the popover and its trigger
326
320
  {
327
321
  property: gap.property,
@@ -0,0 +1,102 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import { setStyle } from './set-style';
3
+
4
+ /**
5
+ * Sets the width of a popover element relative to its anchor element.
6
+ *
7
+ * - `'match-anchor'` → `width: anchor-size(width)` — popover matches anchor width exactly.
8
+ * - `'min-anchor'` → `min-width: anchor-size(width)` — popover is at least as wide as anchor,
9
+ * but can grow wider.
10
+ * - `'none'` → no explicit width is set (default behaviour)
11
+ *
12
+ * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
13
+ * one-off measurement of `anchor.offsetWidth`.
14
+ *
15
+ * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
16
+ */
17
+ export function useWidthFromAnchor(_ref) {
18
+ var mode = _ref.mode,
19
+ popoverRef = _ref.popoverRef,
20
+ anchorRef = _ref.anchorRef;
21
+ useLayoutEffect(function () {
22
+ var node = popoverRef.current;
23
+ if (!node) {
24
+ return;
25
+ }
26
+
27
+ // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
28
+ // so the JS fallback fires correctly in browsers that support anchor
29
+ // positioning but not anchor sizing.
30
+ var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
31
+ if (mode === 'match-anchor') {
32
+ // Set an exact width matching the anchor.
33
+ // `min-inline-size` is intentionally NOT set here — it would override the
34
+ // explicit width constraint, allowing the popover to grow past the anchor's width.
35
+ if (supportsAnchorSize) {
36
+ return setStyle({
37
+ el: node,
38
+ styles: [{
39
+ property: 'width',
40
+ value: 'anchor-size(width)'
41
+ }]
42
+ });
43
+ }
44
+
45
+ // JS fallback: one-off read of the anchor's width.
46
+ var anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
47
+ if (anchor) {
48
+ return setStyle({
49
+ el: node,
50
+ styles: [{
51
+ property: 'width',
52
+ value: "".concat(anchor.offsetWidth, "px")
53
+ }]
54
+ });
55
+ }
56
+ return;
57
+ }
58
+
59
+ // For 'min-anchor' and 'none', apply `min-inline-size: max-content` to ensure
60
+ // correct position-try-fallbacks behaviour (popover margin box overflows the
61
+ // viewport when its span region is too narrow, rather than wrapping content).
62
+ var minInlineSizeStyle = {
63
+ property: 'min-inline-size',
64
+ value: 'max-content'
65
+ };
66
+ if (mode === 'min-anchor') {
67
+ // Set a minimum width matching the anchor. The popover can grow wider
68
+ // if its content requires it.
69
+ if (supportsAnchorSize) {
70
+ return setStyle({
71
+ el: node,
72
+ styles: [minInlineSizeStyle, {
73
+ property: 'min-width',
74
+ value: 'anchor-size(width)'
75
+ }]
76
+ });
77
+ }
78
+ var _anchor = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
79
+ if (_anchor) {
80
+ return setStyle({
81
+ el: node,
82
+ styles: [minInlineSizeStyle, {
83
+ property: 'min-width',
84
+ value: "".concat(_anchor.offsetWidth, "px")
85
+ }]
86
+ });
87
+ }
88
+
89
+ // No anchor available in JS fallback — still apply min-inline-size.
90
+ return setStyle({
91
+ el: node,
92
+ styles: [minInlineSizeStyle]
93
+ });
94
+ }
95
+
96
+ // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
97
+ return setStyle({
98
+ el: node,
99
+ styles: [minInlineSizeStyle]
100
+ });
101
+ }, [mode, popoverRef, anchorRef]);
102
+ }
@@ -35,7 +35,9 @@ var styles = {
35
35
  * Unopinionated top-layer primitive.
36
36
  *
37
37
  * Manages only visibility (isOpen) and animation (animate). Has no knowledge
38
- * of positioning — compose with `useAnchorPosition` for anchor positioning.
38
+ * of positioning — compose with `useAnchorPosition` for anchor positioning
39
+ * and optionally `useWidthFromAnchor` for setting the width of a popover element
40
+ * relative to its anchor trigger.
39
41
  *
40
42
  * Visibility is driven by the `isOpen` prop:
41
43
  * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
@@ -6,6 +6,7 @@ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
6
  import { roleToAriaHasPopup } from '../internal/role-types';
7
7
  import { useAnchorPosition } from '../internal/use-anchor-position';
8
8
  import { usePresetStyles } from '../internal/use-preset-styles';
9
+ import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
9
10
  import { Popover } from '../popover/popover';
10
11
  import { useMaybePopupContext } from './popup-context';
11
12
  /**
@@ -24,8 +25,8 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
24
25
  animate = _ref.animate,
25
26
  arrow = _ref.arrow,
26
27
  mode = _ref.mode,
27
- _ref$width = _ref.width,
28
- width = _ref$width === void 0 ? 'content' : _ref$width,
28
+ _ref$widthFromAnchor = _ref.widthFromAnchor,
29
+ widthFromAnchor = _ref$widthFromAnchor === void 0 ? 'none' : _ref$widthFromAnchor,
29
30
  xcss = _ref.xcss,
30
31
  isOpenProp = _ref.isOpen,
31
32
  triggerRefProp = _ref.triggerRef,
@@ -131,39 +132,11 @@ export var PopupContent = /*#__PURE__*/forwardRef(function PopupContent(_ref, re
131
132
  forceFallbackPositioning: forceFallbackPositioning,
132
133
  arrow: arrowPreset !== null && arrowPreset !== void 0 ? arrowPreset : undefined
133
134
  });
134
-
135
- // ── Width: match trigger ──
136
- // When width="trigger" or width="min-trigger", size the popover relative
137
- // to the trigger element. Uses CSS `anchor-size(width)` when supported,
138
- // falling back to a one-off measurement of the trigger's offsetWidth.
139
- useLayoutEffect(function () {
140
- if (width === 'content') {
141
- return;
142
- }
143
- var node = popoverRef.current;
144
- if (!node) {
145
- return;
146
- }
147
- var cssProperty = width === 'trigger' ? 'width' : 'min-width';
148
- var anchorValue = 'anchor-size(width)';
149
-
150
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
151
- // so the JS fallback fires correctly in browsers that support anchor
152
- // positioning but not anchor sizing.
153
- var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
154
- if (supportsAnchorSize) {
155
- node.style.setProperty(cssProperty, anchorValue);
156
- } else {
157
- // JS fallback: one-off read of the trigger's width.
158
- var trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
159
- if (trigger) {
160
- node.style.setProperty(cssProperty, "".concat(trigger.offsetWidth, "px"));
161
- }
162
- }
163
- return function () {
164
- node.style.removeProperty(cssProperty);
165
- };
166
- }, [width, popoverRef, triggerRef]);
135
+ useWidthFromAnchor({
136
+ mode: widthFromAnchor,
137
+ popoverRef: popoverRef,
138
+ anchorRef: triggerRef
139
+ });
167
140
 
168
141
  // Merge: the popoverRef (shared with trigger + positioning) and the forwarded ref.
169
142
  var combinedRef = mergeRefs([popoverRef, ref]);
@@ -0,0 +1 @@
1
+ export { useWidthFromAnchor } from '../internal/use-width-from-anchor';
@@ -1,4 +1,4 @@
1
- export declare function ensurePresetStyles({ preset, }: {
1
+ export declare function ensurePresetStyles({ preset }: {
2
2
  preset: {
3
3
  css: string;
4
4
  name: string;
@@ -0,0 +1,20 @@
1
+ import { type RefObject } from 'react';
2
+ import type { TWidthFromAnchorMode } from '../popover/types';
3
+ /**
4
+ * Sets the width of a popover element relative to its anchor element.
5
+ *
6
+ * - `'match-anchor'` → `width: anchor-size(width)` — popover matches anchor width exactly.
7
+ * - `'min-anchor'` → `min-width: anchor-size(width)` — popover is at least as wide as anchor,
8
+ * but can grow wider.
9
+ * - `'none'` → no explicit width is set (default behaviour)
10
+ *
11
+ * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
12
+ * one-off measurement of `anchor.offsetWidth`.
13
+ *
14
+ * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
15
+ */
16
+ export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, }: {
17
+ mode: TWidthFromAnchorMode;
18
+ popoverRef: RefObject<HTMLElement | null>;
19
+ anchorRef: RefObject<HTMLElement | null> | undefined;
20
+ }): void;
@@ -8,7 +8,9 @@ import { type TPopoverForwardedProps } from './types';
8
8
  * Unopinionated top-layer primitive.
9
9
  *
10
10
  * Manages only visibility (isOpen) and animation (animate). Has no knowledge
11
- * of positioning — compose with `useAnchorPosition` for anchor positioning.
11
+ * of positioning — compose with `useAnchorPosition` for anchor positioning
12
+ * and optionally `useWidthFromAnchor` for setting the width of a popover element
13
+ * relative to its anchor trigger.
12
14
  *
13
15
  * Visibility is driven by the `isOpen` prop:
14
16
  * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
@@ -157,4 +157,17 @@ export type TPopoverProps = TPopoverBaseProps & TAriaRoleOptional & ({
157
157
  mode: 'manual';
158
158
  onClose?: never;
159
159
  });
160
+ /**
161
+ * The width mode of the popover relative to its anchor element.
162
+ *
163
+ * - `'none'` (default): popover sizes to its content, ignoring the anchor width.
164
+ * - `'match-anchor'`: popover matches the anchor element's width exactly
165
+ * via CSS `anchor-size(width)`. Falls back to a one-off measurement
166
+ * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
167
+ * is not supported.
168
+ * - `'min-anchor'`: popover is at least as wide as the anchor, but
169
+ * can grow wider if content requires it. Uses `min-width: anchor-size(width)`.
170
+ * Falls back to a one-off measurement of the anchor's width.
171
+ */
172
+ export type TWidthFromAnchorMode = 'none' | 'match-anchor' | 'min-anchor';
160
173
  export {};
@@ -4,7 +4,7 @@ import { type TAnimationPreset } from '../animations/types';
4
4
  import { type TArrowPreset } from '../arrow/types';
5
5
  import { type TPlacementOptions } from '../internal/resolve-placement';
6
6
  import { type TRoleRequiringAccessibleName, type TRoleWithImplicitName } from '../internal/role-types';
7
- import { type TPopoverCloseReason } from '../popover/types';
7
+ import { type TPopoverCloseReason, type TWidthFromAnchorMode } from '../popover/types';
8
8
  /**
9
9
  * Describes where to position a popover relative to its trigger. All fields
10
10
  * are optional. The default `{}` means "below the trigger, centered, with a
@@ -115,18 +115,18 @@ type TPopupContentBaseProps = {
115
115
  */
116
116
  ref?: Ref<HTMLDivElement>;
117
117
  /**
118
- * Controls the width of the popover.
118
+ * Controls the width of the popover relative to its anchor element.
119
119
  *
120
- * - `'content'` (default): popover sizes to its content.
121
- * - `'trigger'`: popover matches the trigger element's width exactly
120
+ * - `'none'` (default): popover sizes to its content, ignoring the anchor width.
121
+ * - `'match-anchor'`: popover matches the anchor element's width exactly
122
122
  * via CSS `anchor-size(width)`. Falls back to a one-off measurement
123
- * of `triggerRef.current.offsetWidth` when CSS Anchor Positioning
123
+ * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
124
124
  * is not supported.
125
- * - `'min-trigger'`: popover is at least as wide as the trigger, but
125
+ * - `'min-anchor'`: popover is at least as wide as the anchor, but
126
126
  * can grow wider if content requires it. Uses `min-width: anchor-size(width)`.
127
- * Falls back to a one-off measurement of the trigger's width.
127
+ * Falls back to a one-off measurement of the anchor's width.
128
128
  */
129
- width?: 'content' | 'trigger' | 'min-trigger';
129
+ widthFromAnchor?: TWidthFromAnchorMode;
130
130
  /**
131
131
  * Applies additional CSS styles to the popover root element using
132
132
  * design-system-safe xcss.
@@ -0,0 +1 @@
1
+ export { useWidthFromAnchor } from '../internal/use-width-from-anchor';
@@ -1,4 +1,4 @@
1
- export declare function ensurePresetStyles({ preset, }: {
1
+ export declare function ensurePresetStyles({ preset }: {
2
2
  preset: {
3
3
  css: string;
4
4
  name: string;
@@ -0,0 +1,20 @@
1
+ import { type RefObject } from 'react';
2
+ import type { TWidthFromAnchorMode } from '../popover/types';
3
+ /**
4
+ * Sets the width of a popover element relative to its anchor element.
5
+ *
6
+ * - `'match-anchor'` → `width: anchor-size(width)` — popover matches anchor width exactly.
7
+ * - `'min-anchor'` → `min-width: anchor-size(width)` — popover is at least as wide as anchor,
8
+ * but can grow wider.
9
+ * - `'none'` → no explicit width is set (default behaviour)
10
+ *
11
+ * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
12
+ * one-off measurement of `anchor.offsetWidth`.
13
+ *
14
+ * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
15
+ */
16
+ export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, }: {
17
+ mode: TWidthFromAnchorMode;
18
+ popoverRef: RefObject<HTMLElement | null>;
19
+ anchorRef: RefObject<HTMLElement | null> | undefined;
20
+ }): void;
@@ -8,7 +8,9 @@ import { type TPopoverForwardedProps } from './types';
8
8
  * Unopinionated top-layer primitive.
9
9
  *
10
10
  * Manages only visibility (isOpen) and animation (animate). Has no knowledge
11
- * of positioning — compose with `useAnchorPosition` for anchor positioning.
11
+ * of positioning — compose with `useAnchorPosition` for anchor positioning
12
+ * and optionally `useWidthFromAnchor` for setting the width of a popover element
13
+ * relative to its anchor trigger.
12
14
  *
13
15
  * Visibility is driven by the `isOpen` prop:
14
16
  * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
@@ -157,4 +157,17 @@ export type TPopoverProps = TPopoverBaseProps & TAriaRoleOptional & ({
157
157
  mode: 'manual';
158
158
  onClose?: never;
159
159
  });
160
+ /**
161
+ * The width mode of the popover relative to its anchor element.
162
+ *
163
+ * - `'none'` (default): popover sizes to its content, ignoring the anchor width.
164
+ * - `'match-anchor'`: popover matches the anchor element's width exactly
165
+ * via CSS `anchor-size(width)`. Falls back to a one-off measurement
166
+ * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
167
+ * is not supported.
168
+ * - `'min-anchor'`: popover is at least as wide as the anchor, but
169
+ * can grow wider if content requires it. Uses `min-width: anchor-size(width)`.
170
+ * Falls back to a one-off measurement of the anchor's width.
171
+ */
172
+ export type TWidthFromAnchorMode = 'none' | 'match-anchor' | 'min-anchor';
160
173
  export {};
@@ -4,7 +4,7 @@ import { type TAnimationPreset } from '../animations/types';
4
4
  import { type TArrowPreset } from '../arrow/types';
5
5
  import { type TPlacementOptions } from '../internal/resolve-placement';
6
6
  import { type TRoleRequiringAccessibleName, type TRoleWithImplicitName } from '../internal/role-types';
7
- import { type TPopoverCloseReason } from '../popover/types';
7
+ import { type TPopoverCloseReason, type TWidthFromAnchorMode } from '../popover/types';
8
8
  /**
9
9
  * Describes where to position a popover relative to its trigger. All fields
10
10
  * are optional. The default `{}` means "below the trigger, centered, with a
@@ -115,18 +115,18 @@ type TPopupContentBaseProps = {
115
115
  */
116
116
  ref?: Ref<HTMLDivElement>;
117
117
  /**
118
- * Controls the width of the popover.
118
+ * Controls the width of the popover relative to its anchor element.
119
119
  *
120
- * - `'content'` (default): popover sizes to its content.
121
- * - `'trigger'`: popover matches the trigger element's width exactly
120
+ * - `'none'` (default): popover sizes to its content, ignoring the anchor width.
121
+ * - `'match-anchor'`: popover matches the anchor element's width exactly
122
122
  * via CSS `anchor-size(width)`. Falls back to a one-off measurement
123
- * of `triggerRef.current.offsetWidth` when CSS Anchor Positioning
123
+ * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
124
124
  * is not supported.
125
- * - `'min-trigger'`: popover is at least as wide as the trigger, but
125
+ * - `'min-anchor'`: popover is at least as wide as the anchor, but
126
126
  * can grow wider if content requires it. Uses `min-width: anchor-size(width)`.
127
- * Falls back to a one-off measurement of the trigger's width.
127
+ * Falls back to a one-off measurement of the anchor's width.
128
128
  */
129
- width?: 'content' | 'trigger' | 'min-trigger';
129
+ widthFromAnchor?: TWidthFromAnchorMode;
130
130
  /**
131
131
  * Applies additional CSS styles to the popover root element using
132
132
  * design-system-safe xcss.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "0.6.3",
3
+ "version": "0.7.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",
@@ -59,7 +59,7 @@
59
59
  "@atlaskit/textfield": "^8.3.0",
60
60
  "@atlassian/react-compiler-gating": "workspace:^",
61
61
  "@atlassian/ssr-tests": "workspace:^",
62
- "@atlassian/testing-library": "^0.5.0",
62
+ "@atlassian/testing-library": "^0.6.0",
63
63
  "@testing-library/jest-dom": "^6.4.5",
64
64
  "@testing-library/react": "^16.3.0",
65
65
  "@types/raf-schd": "^4.0.1",
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@atlaskit/top-layer/use-width-from-anchor",
3
+ "main": "../dist/cjs/entry-points/use-width-from-anchor.js",
4
+ "module": "../dist/esm/entry-points/use-width-from-anchor.js",
5
+ "module:es2019": "../dist/es2019/entry-points/use-width-from-anchor.js",
6
+ "sideEffects": [
7
+ "*.compiled.css"
8
+ ],
9
+ "types": "../dist/types/entry-points/use-width-from-anchor.d.ts",
10
+ "typesVersions": {
11
+ ">=4.5 <5.9": {
12
+ "*": [
13
+ "../dist/types-ts4.5/entry-points/use-width-from-anchor.d.ts"
14
+ ]
15
+ }
16
+ }
17
+ }