@atlaskit/top-layer 4.2.2 → 4.2.3

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,14 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 4.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`3f3fc95b7af6b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/3f3fc95b7af6b) -
8
+ Fixed `offset.crossAxisShift` moving the popover the wrong distance or direction in some
9
+ placements, and restored the documented `useWidthFromAnchor({ mode: 'min-anchor' })` width
10
+ behaviour.
11
+
3
12
  ## 4.2.2
4
13
 
5
14
  ### Patch Changes
@@ -130,23 +130,26 @@ function applyAlignment(_ref2) {
130
130
  /**
131
131
  * Applies the cross-axis shift offset.
132
132
  *
133
- * The sign mirrors the CSS path (see `crossAxisShiftMargin` in
134
- * `use-anchor-position.tsx`):
135
- * - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
136
- * - `align: 'end'` + `forwards` → push toward start (negative)
137
- * - `backwards` flips the sign in both cases.
133
+ * `forwards` always moves the popover toward the cross-axis END and
134
+ * `backwards` toward the START, for every `align` value. See
135
+ * `notes/decisions/placement-offset.md`.
136
+ *
137
+ * This is a coordinate, so the direction is the sign and nothing else. There
138
+ * is deliberately no per-`align` sign inversion here: the CSS path inverts the
139
+ * sign only because a margin on the END side pushes a box the opposite way to
140
+ * a margin on the START side, which is a property of margins rather than of
141
+ * the shift. Applying that inversion to a coordinate moved `align: 'end'`
142
+ * popovers the wrong way.
138
143
  */
139
144
  function applyCrossAxisShift(_ref3) {
140
145
  var position = _ref3.position,
141
146
  axis = _ref3.axis,
142
- align = _ref3.align,
143
147
  crossAxisShift = _ref3.crossAxisShift;
144
148
  if (crossAxisShift.value === 0) {
145
149
  return position;
146
150
  }
147
151
  var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
148
- var sideSign = align === 'end' ? -1 : 1;
149
- var delta = crossAxisShift.value * directionSign * sideSign;
152
+ var delta = crossAxisShift.value * directionSign;
150
153
 
151
154
  // Cross-axis is the OPPOSITE of the placement axis.
152
155
  if (axis === 'block') {
@@ -202,7 +205,6 @@ function computeFallbackPosition(_ref4) {
202
205
  var shiftedPosition = applyCrossAxisShift({
203
206
  position: alignedPosition,
204
207
  axis: axis,
205
- align: align,
206
208
  crossAxisShift: crossAxisShift
207
209
  });
208
210
 
@@ -50,8 +50,9 @@ var roleToAriaHasPopup = {
50
50
  * `menu → "menu"`, `tooltip → undefined`). React omits the attribute when
51
51
  * the value is `undefined`.
52
52
  * - `aria-expanded` - reflects the current open state.
53
- * - `aria-controls` - references the popover element by its id while the
54
- * popover is open, and is omitted while the popover is closed and unmounted.
53
+ * - `aria-controls` - references the popover element by its id, even
54
+ * while the popover is closed. The relationship is stable; `aria-expanded`
55
+ * carries the current visibility state.
55
56
  *
56
57
  * **This function is for click/keyboard-activated popovers only.** For
57
58
  * hover-driven tooltips, the trigger should use `aria-describedby` instead.
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.useAnchorPosition = useAnchorPosition;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
8
9
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
10
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
11
  var _react = require("react");
@@ -81,6 +82,31 @@ var supportsAnchorPositioning = (0, _once.default)(function () {
81
82
  return CSS.supports('anchor-name', '--a');
82
83
  });
83
84
 
85
+ /**
86
+ * A single inline style declaration, as consumed by `setStyle`.
87
+ */
88
+
89
+ /**
90
+ * The custom property mirroring each cross-axis shift margin.
91
+ *
92
+ * These are written for the planned named arrow `@position-try` rules. Nothing
93
+ * reads them yet, and the shift no longer depends on them: the antisymmetric
94
+ * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
95
+ * their own.
96
+ */
97
+ var CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
98
+ 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
99
+ 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
100
+ 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
101
+ 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
102
+ };
103
+ var ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES = Object.values(CROSS_AXIS_SHIFT_CUSTOM_PROPERTY).map(function (property) {
104
+ return {
105
+ property: property,
106
+ value: '0px'
107
+ };
108
+ });
109
+
84
110
  /**
85
111
  * Returns the CSS margin declaration that creates a gap between the
86
112
  * popover and its trigger on the side facing the anchor.
@@ -125,30 +151,54 @@ function edgeMargin(_ref) {
125
151
  }
126
152
 
127
153
  /**
128
- * Cross-axis shift margin. Margin side matches the popover's anchored
129
- * cross-axis edge: `align: 'start'` uses the START margin, `align: 'end'`
130
- * uses the END margin with the sign inverted (so `forwards` is always
131
- * positive), `align: 'center'` uses START. Margin on the un-anchored side
132
- * has no effect under CSS Anchor Positioning.
154
+ * **The cross-axis shift margins.**
155
+ *
156
+ * The shift is written antisymmetrically on BOTH cross-axis sides
157
+ * (`start: +value`, `end: -value`) rather than as a single margin on the side
158
+ * picked from `align`. Two separate behaviours require it:
159
+ *
160
+ * 1. **`align: 'center'` is centered with `anchor-center`, which centers the
161
+ * popover's MARGIN box on the anchor.** A single-sided margin widens the
162
+ * margin box, so it displaces the border box by only half its value. An
163
+ * antisymmetric pair leaves the margin box width unchanged (`+value` and
164
+ * `-value` sum to zero) while moving its center by the full value.
165
+ * 2. **`position-try-fallbacks` can slide the popover onto the opposite
166
+ * cross-axis side.** A `<position-area>` fallback keeps the base style's
167
+ * margins, and margin on the un-anchored side has no effect, so a
168
+ * single-sided margin is silently dropped once the browser slides across the
169
+ * cross axis. With both sides written, whichever side ends up anchored moves
170
+ * the popover the same physical direction.
171
+ *
172
+ * Leaving the margin box width unchanged also means the shift does not move
173
+ * the point at which `position-try-fallbacks` decides the popover overflows.
174
+ *
175
+ * A `<try-tactic>` fallback (`flip-block`, `flip-inline`) behaves differently to
176
+ * a `<position-area>` one: it SWAPS the start and end margins rather than
177
+ * keeping them, which is what preserves the gap through a flip. So a flip across
178
+ * the cross axis mirrors the shift along with the rest of the placement, rather
179
+ * than preserving its physical direction. Only the diagonal fallback does that,
180
+ * and only for `align: 'start' | 'end'`. See `placementToTryFallbacks`.
181
+ *
182
+ * A positive value always moves the popover toward the cross-axis END, so
183
+ * `direction: 'forwards'` means the same thing for every `align` value. See
184
+ * `notes/decisions/placement-offset.md`.
133
185
  */
134
- function crossAxisShiftMargin(_ref2) {
186
+ function crossAxisShiftMargins(_ref2) {
135
187
  var placement = _ref2.placement,
136
188
  crossAxisShiftCssValue = _ref2.crossAxisShiftCssValue,
137
189
  direction = _ref2.direction;
138
- var axis = placement.axis,
139
- align = placement.align;
140
- var directionSign = direction === 'forwards' ? 1 : -1;
141
- var crossAxis = axis === 'block' ? 'inline' : 'block';
142
- var useEndSide = align === 'end';
143
- var side = useEndSide ? 'end' : 'start';
144
- var sideSign = useEndSide ? -1 : 1;
145
- var finalSign = directionSign * sideSign;
146
- // Wrap any string in calc() with the sign factor. CSS handles the math.
147
- var value = finalSign === 1 ? crossAxisShiftCssValue : "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
148
- return {
149
- property: "margin-".concat(crossAxis, "-").concat(side),
150
- value: value
151
- };
190
+ var crossAxis = placement.axis === 'block' ? 'inline' : 'block';
191
+ // Wrap any string in calc() with the sign factor. CSS handles the math,
192
+ // so opaque values such as design tokens negate correctly.
193
+ var negated = "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
194
+ var isForwards = direction === 'forwards';
195
+ return [{
196
+ property: "margin-".concat(crossAxis, "-start"),
197
+ value: isForwards ? crossAxisShiftCssValue : negated
198
+ }, {
199
+ property: "margin-".concat(crossAxis, "-end"),
200
+ value: isForwards ? negated : crossAxisShiftCssValue
201
+ }];
152
202
  }
153
203
 
154
204
  /**
@@ -233,10 +283,9 @@ function useAnchorPosition(_ref3) {
233
283
  placement: stablePlacement,
234
284
  offset: gapCssValue
235
285
  });
236
- // Computed once; reused below to populate the active
237
- // `--ds-cross-axis-shift-margin-*` custom property for the
238
- // named arrow @position-try rules.
239
- var crossAxisShift = crossAxisShiftMargin({
286
+ // Computed once; also mirrored into the
287
+ // `--ds-cross-axis-shift-margin-*` custom properties below.
288
+ var crossAxisShift = crossAxisShiftMargins({
240
289
  placement: stablePlacement,
241
290
  crossAxisShiftCssValue: crossAxisShiftCssValue,
242
291
  direction: stablePlacement.offset.crossAxisShift.direction
@@ -266,45 +315,14 @@ function useAnchorPosition(_ref3) {
266
315
  }, {
267
316
  property: gap.property,
268
317
  value: gap.value
269
- }, {
270
- property: crossAxisShift.property,
271
- value: crossAxisShift.value
272
- }];
273
-
274
- // Expose the active cross-axis shift via custom properties so each
275
- // named arrow @position-try rule can re-apply it after a flip.
276
- // One of the four (cross-axis, side) properties is non-zero at a time.
277
- popoverStyles.push({
278
- property: '--ds-cross-axis-shift-margin-start',
279
- value: '0px'
280
- });
281
- popoverStyles.push({
282
- property: '--ds-cross-axis-shift-margin-end',
283
- value: '0px'
284
- });
285
- popoverStyles.push({
286
- property: '--ds-cross-axis-shift-margin-block-start',
287
- value: '0px'
288
- });
289
- popoverStyles.push({
290
- property: '--ds-cross-axis-shift-margin-block-end',
291
- value: '0px'
292
- });
293
- // `crossAxisShift` IS the active margin; reuse instead of recomputing.
294
- var crossAxisShiftActive = crossAxisShift;
295
- var customPropertyByMarginProperty = {
296
- 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
297
- 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
298
- 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
299
- 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
300
- };
301
- var crossAxisShiftCustomProperty = customPropertyByMarginProperty[crossAxisShiftActive.property];
302
- if (crossAxisShiftCustomProperty) {
303
- popoverStyles.push({
304
- property: crossAxisShiftCustomProperty,
305
- value: crossAxisShiftActive.value
306
- });
307
- }
318
+ }].concat((0, _toConsumableArray2.default)(crossAxisShift), (0, _toConsumableArray2.default)(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), (0, _toConsumableArray2.default)(crossAxisShift.map(function (_ref4) {
319
+ var property = _ref4.property,
320
+ value = _ref4.value;
321
+ return {
322
+ property: CROSS_AXIS_SHIFT_CUSTOM_PROPERTY[property],
323
+ value: value
324
+ };
325
+ })));
308
326
 
309
327
  /**
310
328
  * **We are never cleaning up anchor names**
@@ -10,9 +10,12 @@ var _setStyle = require("./set-style");
10
10
  * Sets the width of a popover element relative to its anchor element.
11
11
  *
12
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)
13
+ * - `'min-anchor'` → `min-inline-size: anchor-size(width)` - popover is at least as wide as the
14
+ * anchor, and can grow wider if its content requires it. The anchor width is the ONLY floor: see
15
+ * `notes/decisions/width-from-anchor-floors.md` for why this mode deliberately does not also floor
16
+ * the popover at its own content width.
17
+ * - `'none'` → `min-inline-size: max-content` - no anchor-relative width, but floored at the
18
+ * popover's own content width (default behaviour)
16
19
  *
17
20
  * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
18
21
  * one-off measurement of `anchor.offsetWidth`.
@@ -61,22 +64,24 @@ function useWidthFromAnchor(_ref) {
61
64
  }
62
65
  return;
63
66
  }
64
-
65
- // For 'min-anchor' and 'none', apply `min-inline-size: max-content` to ensure
66
- // correct position-try-fallbacks behaviour (popover margin box overflows the
67
- // viewport when its span region is too narrow, rather than wrapping content).
68
- var minInlineSizeStyle = {
69
- property: 'min-inline-size',
70
- value: 'max-content'
71
- };
72
67
  if (mode === 'min-anchor') {
73
- // Set a minimum width matching the anchor. The popover can grow wider
74
- // if its content requires it.
68
+ // One declaration, one floor: the anchor width, as a lower bound and not an
69
+ // exact size, which is what separates 'min-anchor' from 'match-anchor'.
70
+ //
71
+ // This mode deliberately does NOT also floor the popover at its own content
72
+ // width. Mode 'none' does, to make a too-narrow span overflow and drive
73
+ // `position-try-fallbacks`, but that floor can only be expressed on
74
+ // `min-inline-size` or on `width`, and both collide with the anchor floor here:
75
+ // `min-inline-size` IS `min-width` in a horizontal writing mode, and `width` is
76
+ // an exact size, so it would cap the popover and override a consumer width.
77
+ // The anchor floor already guarantees a usable width for this mode, so the
78
+ // popover is allowed to wrap down to it. See
79
+ // `notes/decisions/width-from-anchor-floors.md`.
75
80
  if (supportsAnchorSize) {
76
81
  return (0, _setStyle.setStyle)({
77
82
  element: node,
78
- styles: [minInlineSizeStyle, {
79
- property: 'min-width',
83
+ styles: [{
84
+ property: 'min-inline-size',
80
85
  value: 'anchor-size(width)'
81
86
  }]
82
87
  });
@@ -85,24 +90,26 @@ function useWidthFromAnchor(_ref) {
85
90
  if (_anchor) {
86
91
  return (0, _setStyle.setStyle)({
87
92
  element: node,
88
- styles: [minInlineSizeStyle, {
89
- property: 'min-width',
93
+ styles: [{
94
+ property: 'min-inline-size',
90
95
  value: "".concat(_anchor.offsetWidth, "px")
91
96
  }]
92
97
  });
93
98
  }
94
99
 
95
- // No anchor available in JS fallback - still apply min-inline-size.
96
- return (0, _setStyle.setStyle)({
97
- element: node,
98
- styles: [minInlineSizeStyle]
99
- });
100
+ // No anchor to measure in the JS fallback, so there is no floor to apply.
101
+ return;
100
102
  }
101
103
 
102
- // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
104
+ // 'none' has no anchor floor to collide with, so it can carry the content floor.
105
+ // `@atlaskit/popper` depends on the property: its fit-to-viewport mode neutralises
106
+ // this floor with `min-inline-size: 0`.
103
107
  return (0, _setStyle.setStyle)({
104
108
  element: node,
105
- styles: [minInlineSizeStyle]
109
+ styles: [{
110
+ property: 'min-inline-size',
111
+ value: 'max-content'
112
+ }]
106
113
  });
107
114
  }, [mode, popoverRef, anchorRef, isOpen]);
108
115
  }
@@ -126,24 +126,27 @@ function applyAlignment({
126
126
  /**
127
127
  * Applies the cross-axis shift offset.
128
128
  *
129
- * The sign mirrors the CSS path (see `crossAxisShiftMargin` in
130
- * `use-anchor-position.tsx`):
131
- * - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
132
- * - `align: 'end'` + `forwards` → push toward start (negative)
133
- * - `backwards` flips the sign in both cases.
129
+ * `forwards` always moves the popover toward the cross-axis END and
130
+ * `backwards` toward the START, for every `align` value. See
131
+ * `notes/decisions/placement-offset.md`.
132
+ *
133
+ * This is a coordinate, so the direction is the sign and nothing else. There
134
+ * is deliberately no per-`align` sign inversion here: the CSS path inverts the
135
+ * sign only because a margin on the END side pushes a box the opposite way to
136
+ * a margin on the START side, which is a property of margins rather than of
137
+ * the shift. Applying that inversion to a coordinate moved `align: 'end'`
138
+ * popovers the wrong way.
134
139
  */
135
140
  function applyCrossAxisShift({
136
141
  position,
137
142
  axis,
138
- align,
139
143
  crossAxisShift
140
144
  }) {
141
145
  if (crossAxisShift.value === 0) {
142
146
  return position;
143
147
  }
144
148
  const directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
145
- const sideSign = align === 'end' ? -1 : 1;
146
- const delta = crossAxisShift.value * directionSign * sideSign;
149
+ const delta = crossAxisShift.value * directionSign;
147
150
 
148
151
  // Cross-axis is the OPPOSITE of the placement axis.
149
152
  if (axis === 'block') {
@@ -204,7 +207,6 @@ export function computeFallbackPosition({
204
207
  const shiftedPosition = applyCrossAxisShift({
205
208
  position: alignedPosition,
206
209
  axis,
207
- align,
208
210
  crossAxisShift
209
211
  });
210
212
 
@@ -44,8 +44,9 @@ const roleToAriaHasPopup = {
44
44
  * `menu → "menu"`, `tooltip → undefined`). React omits the attribute when
45
45
  * the value is `undefined`.
46
46
  * - `aria-expanded` - reflects the current open state.
47
- * - `aria-controls` - references the popover element by its id while the
48
- * popover is open, and is omitted while the popover is closed and unmounted.
47
+ * - `aria-controls` - references the popover element by its id, even
48
+ * while the popover is closed. The relationship is stable; `aria-expanded`
49
+ * carries the current visibility state.
49
50
  *
50
51
  * **This function is for click/keyboard-activated popovers only.** For
51
52
  * hover-driven tooltips, the trigger should use `aria-describedby` instead.
@@ -62,6 +62,29 @@ const supportsAnchorPositioning = once(() => {
62
62
  return CSS.supports('anchor-name', '--a');
63
63
  });
64
64
 
65
+ /**
66
+ * A single inline style declaration, as consumed by `setStyle`.
67
+ */
68
+
69
+ /**
70
+ * The custom property mirroring each cross-axis shift margin.
71
+ *
72
+ * These are written for the planned named arrow `@position-try` rules. Nothing
73
+ * reads them yet, and the shift no longer depends on them: the antisymmetric
74
+ * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
75
+ * their own.
76
+ */
77
+ const CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
78
+ 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
79
+ 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
80
+ 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
81
+ 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
82
+ };
83
+ const ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES = Object.values(CROSS_AXIS_SHIFT_CUSTOM_PROPERTY).map(property => ({
84
+ property,
85
+ value: '0px'
86
+ }));
87
+
65
88
  /**
66
89
  * Returns the CSS margin declaration that creates a gap between the
67
90
  * popover and its trigger on the side facing the anchor.
@@ -109,33 +132,55 @@ function edgeMargin({
109
132
  }
110
133
 
111
134
  /**
112
- * Cross-axis shift margin. Margin side matches the popover's anchored
113
- * cross-axis edge: `align: 'start'` uses the START margin, `align: 'end'`
114
- * uses the END margin with the sign inverted (so `forwards` is always
115
- * positive), `align: 'center'` uses START. Margin on the un-anchored side
116
- * has no effect under CSS Anchor Positioning.
135
+ * **The cross-axis shift margins.**
136
+ *
137
+ * The shift is written antisymmetrically on BOTH cross-axis sides
138
+ * (`start: +value`, `end: -value`) rather than as a single margin on the side
139
+ * picked from `align`. Two separate behaviours require it:
140
+ *
141
+ * 1. **`align: 'center'` is centered with `anchor-center`, which centers the
142
+ * popover's MARGIN box on the anchor.** A single-sided margin widens the
143
+ * margin box, so it displaces the border box by only half its value. An
144
+ * antisymmetric pair leaves the margin box width unchanged (`+value` and
145
+ * `-value` sum to zero) while moving its center by the full value.
146
+ * 2. **`position-try-fallbacks` can slide the popover onto the opposite
147
+ * cross-axis side.** A `<position-area>` fallback keeps the base style's
148
+ * margins, and margin on the un-anchored side has no effect, so a
149
+ * single-sided margin is silently dropped once the browser slides across the
150
+ * cross axis. With both sides written, whichever side ends up anchored moves
151
+ * the popover the same physical direction.
152
+ *
153
+ * Leaving the margin box width unchanged also means the shift does not move
154
+ * the point at which `position-try-fallbacks` decides the popover overflows.
155
+ *
156
+ * A `<try-tactic>` fallback (`flip-block`, `flip-inline`) behaves differently to
157
+ * a `<position-area>` one: it SWAPS the start and end margins rather than
158
+ * keeping them, which is what preserves the gap through a flip. So a flip across
159
+ * the cross axis mirrors the shift along with the rest of the placement, rather
160
+ * than preserving its physical direction. Only the diagonal fallback does that,
161
+ * and only for `align: 'start' | 'end'`. See `placementToTryFallbacks`.
162
+ *
163
+ * A positive value always moves the popover toward the cross-axis END, so
164
+ * `direction: 'forwards'` means the same thing for every `align` value. See
165
+ * `notes/decisions/placement-offset.md`.
117
166
  */
118
- function crossAxisShiftMargin({
167
+ function crossAxisShiftMargins({
119
168
  placement,
120
169
  crossAxisShiftCssValue,
121
170
  direction
122
171
  }) {
123
- const {
124
- axis,
125
- align
126
- } = placement;
127
- const directionSign = direction === 'forwards' ? 1 : -1;
128
- const crossAxis = axis === 'block' ? 'inline' : 'block';
129
- const useEndSide = align === 'end';
130
- const side = useEndSide ? 'end' : 'start';
131
- const sideSign = useEndSide ? -1 : 1;
132
- const finalSign = directionSign * sideSign;
133
- // Wrap any string in calc() with the sign factor. CSS handles the math.
134
- const value = finalSign === 1 ? crossAxisShiftCssValue : `calc(-1 * ${crossAxisShiftCssValue})`;
135
- return {
136
- property: `margin-${crossAxis}-${side}`,
137
- value
138
- };
172
+ const crossAxis = placement.axis === 'block' ? 'inline' : 'block';
173
+ // Wrap any string in calc() with the sign factor. CSS handles the math,
174
+ // so opaque values such as design tokens negate correctly.
175
+ const negated = `calc(-1 * ${crossAxisShiftCssValue})`;
176
+ const isForwards = direction === 'forwards';
177
+ return [{
178
+ property: `margin-${crossAxis}-start`,
179
+ value: isForwards ? crossAxisShiftCssValue : negated
180
+ }, {
181
+ property: `margin-${crossAxis}-end`,
182
+ value: isForwards ? negated : crossAxisShiftCssValue
183
+ }];
139
184
  }
140
185
 
141
186
  /**
@@ -216,10 +261,9 @@ export function useAnchorPosition({
216
261
  placement: stablePlacement,
217
262
  offset: gapCssValue
218
263
  });
219
- // Computed once; reused below to populate the active
220
- // `--ds-cross-axis-shift-margin-*` custom property for the
221
- // named arrow @position-try rules.
222
- const crossAxisShift = crossAxisShiftMargin({
264
+ // Computed once; also mirrored into the
265
+ // `--ds-cross-axis-shift-margin-*` custom properties below.
266
+ const crossAxisShift = crossAxisShiftMargins({
223
267
  placement: stablePlacement,
224
268
  crossAxisShiftCssValue,
225
269
  direction: stablePlacement.offset.crossAxisShift.direction
@@ -249,45 +293,16 @@ export function useAnchorPosition({
249
293
  }, {
250
294
  property: gap.property,
251
295
  value: gap.value
252
- }, {
253
- property: crossAxisShift.property,
254
- value: crossAxisShift.value
255
- }];
256
-
257
- // Expose the active cross-axis shift via custom properties so each
258
- // named arrow @position-try rule can re-apply it after a flip.
259
- // One of the four (cross-axis, side) properties is non-zero at a time.
260
- popoverStyles.push({
261
- property: '--ds-cross-axis-shift-margin-start',
262
- value: '0px'
263
- });
264
- popoverStyles.push({
265
- property: '--ds-cross-axis-shift-margin-end',
266
- value: '0px'
267
- });
268
- popoverStyles.push({
269
- property: '--ds-cross-axis-shift-margin-block-start',
270
- value: '0px'
271
- });
272
- popoverStyles.push({
273
- property: '--ds-cross-axis-shift-margin-block-end',
274
- value: '0px'
275
- });
276
- // `crossAxisShift` IS the active margin; reuse instead of recomputing.
277
- const crossAxisShiftActive = crossAxisShift;
278
- const customPropertyByMarginProperty = {
279
- 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
280
- 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
281
- 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
282
- 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
283
- };
284
- const crossAxisShiftCustomProperty = customPropertyByMarginProperty[crossAxisShiftActive.property];
285
- if (crossAxisShiftCustomProperty) {
286
- popoverStyles.push({
287
- property: crossAxisShiftCustomProperty,
288
- value: crossAxisShiftActive.value
289
- });
290
- }
296
+ }, ...crossAxisShift,
297
+ // Zero every side first so the two sides that are NOT on the
298
+ // active cross axis do not keep a value from a previous placement.
299
+ ...ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES, ...crossAxisShift.map(({
300
+ property,
301
+ value
302
+ }) => ({
303
+ property: CROSS_AXIS_SHIFT_CUSTOM_PROPERTY[property],
304
+ value
305
+ }))];
291
306
 
292
307
  /**
293
308
  * **We are never cleaning up anchor names**
@@ -5,9 +5,12 @@ import { setStyle } from './set-style';
5
5
  * Sets the width of a popover element relative to its anchor element.
6
6
  *
7
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)
8
+ * - `'min-anchor'` → `min-inline-size: anchor-size(width)` - popover is at least as wide as the
9
+ * anchor, and can grow wider if its content requires it. The anchor width is the ONLY floor: see
10
+ * `notes/decisions/width-from-anchor-floors.md` for why this mode deliberately does not also floor
11
+ * the popover at its own content width.
12
+ * - `'none'` → `min-inline-size: max-content` - no anchor-relative width, but floored at the
13
+ * popover's own content width (default behaviour)
11
14
  *
12
15
  * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
13
16
  * one-off measurement of `anchor.offsetWidth`.
@@ -57,22 +60,24 @@ export function useWidthFromAnchor({
57
60
  }
58
61
  return;
59
62
  }
60
-
61
- // For 'min-anchor' and 'none', apply `min-inline-size: max-content` to ensure
62
- // correct position-try-fallbacks behaviour (popover margin box overflows the
63
- // viewport when its span region is too narrow, rather than wrapping content).
64
- const minInlineSizeStyle = {
65
- property: 'min-inline-size',
66
- value: 'max-content'
67
- };
68
63
  if (mode === 'min-anchor') {
69
- // Set a minimum width matching the anchor. The popover can grow wider
70
- // if its content requires it.
64
+ // One declaration, one floor: the anchor width, as a lower bound and not an
65
+ // exact size, which is what separates 'min-anchor' from 'match-anchor'.
66
+ //
67
+ // This mode deliberately does NOT also floor the popover at its own content
68
+ // width. Mode 'none' does, to make a too-narrow span overflow and drive
69
+ // `position-try-fallbacks`, but that floor can only be expressed on
70
+ // `min-inline-size` or on `width`, and both collide with the anchor floor here:
71
+ // `min-inline-size` IS `min-width` in a horizontal writing mode, and `width` is
72
+ // an exact size, so it would cap the popover and override a consumer width.
73
+ // The anchor floor already guarantees a usable width for this mode, so the
74
+ // popover is allowed to wrap down to it. See
75
+ // `notes/decisions/width-from-anchor-floors.md`.
71
76
  if (supportsAnchorSize) {
72
77
  return setStyle({
73
78
  element: node,
74
- styles: [minInlineSizeStyle, {
75
- property: 'min-width',
79
+ styles: [{
80
+ property: 'min-inline-size',
76
81
  value: 'anchor-size(width)'
77
82
  }]
78
83
  });
@@ -81,24 +86,26 @@ export function useWidthFromAnchor({
81
86
  if (anchor) {
82
87
  return setStyle({
83
88
  element: node,
84
- styles: [minInlineSizeStyle, {
85
- property: 'min-width',
89
+ styles: [{
90
+ property: 'min-inline-size',
86
91
  value: `${anchor.offsetWidth}px`
87
92
  }]
88
93
  });
89
94
  }
90
95
 
91
- // No anchor available in JS fallback - still apply min-inline-size.
92
- return setStyle({
93
- element: node,
94
- styles: [minInlineSizeStyle]
95
- });
96
+ // No anchor to measure in the JS fallback, so there is no floor to apply.
97
+ return;
96
98
  }
97
99
 
98
- // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
100
+ // 'none' has no anchor floor to collide with, so it can carry the content floor.
101
+ // `@atlaskit/popper` depends on the property: its fit-to-viewport mode neutralises
102
+ // this floor with `min-inline-size: 0`.
99
103
  return setStyle({
100
104
  element: node,
101
- styles: [minInlineSizeStyle]
105
+ styles: [{
106
+ property: 'min-inline-size',
107
+ value: 'max-content'
108
+ }]
102
109
  });
103
110
  }, [mode, popoverRef, anchorRef, isOpen]);
104
111
  }
@@ -123,23 +123,26 @@ function applyAlignment(_ref2) {
123
123
  /**
124
124
  * Applies the cross-axis shift offset.
125
125
  *
126
- * The sign mirrors the CSS path (see `crossAxisShiftMargin` in
127
- * `use-anchor-position.tsx`):
128
- * - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
129
- * - `align: 'end'` + `forwards` → push toward start (negative)
130
- * - `backwards` flips the sign in both cases.
126
+ * `forwards` always moves the popover toward the cross-axis END and
127
+ * `backwards` toward the START, for every `align` value. See
128
+ * `notes/decisions/placement-offset.md`.
129
+ *
130
+ * This is a coordinate, so the direction is the sign and nothing else. There
131
+ * is deliberately no per-`align` sign inversion here: the CSS path inverts the
132
+ * sign only because a margin on the END side pushes a box the opposite way to
133
+ * a margin on the START side, which is a property of margins rather than of
134
+ * the shift. Applying that inversion to a coordinate moved `align: 'end'`
135
+ * popovers the wrong way.
131
136
  */
132
137
  function applyCrossAxisShift(_ref3) {
133
138
  var position = _ref3.position,
134
139
  axis = _ref3.axis,
135
- align = _ref3.align,
136
140
  crossAxisShift = _ref3.crossAxisShift;
137
141
  if (crossAxisShift.value === 0) {
138
142
  return position;
139
143
  }
140
144
  var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
141
- var sideSign = align === 'end' ? -1 : 1;
142
- var delta = crossAxisShift.value * directionSign * sideSign;
145
+ var delta = crossAxisShift.value * directionSign;
143
146
 
144
147
  // Cross-axis is the OPPOSITE of the placement axis.
145
148
  if (axis === 'block') {
@@ -195,7 +198,6 @@ export function computeFallbackPosition(_ref4) {
195
198
  var shiftedPosition = applyCrossAxisShift({
196
199
  position: alignedPosition,
197
200
  axis: axis,
198
- align: align,
199
201
  crossAxisShift: crossAxisShift
200
202
  });
201
203
 
@@ -44,8 +44,9 @@ var roleToAriaHasPopup = {
44
44
  * `menu → "menu"`, `tooltip → undefined`). React omits the attribute when
45
45
  * the value is `undefined`.
46
46
  * - `aria-expanded` - reflects the current open state.
47
- * - `aria-controls` - references the popover element by its id while the
48
- * popover is open, and is omitted while the popover is closed and unmounted.
47
+ * - `aria-controls` - references the popover element by its id, even
48
+ * while the popover is closed. The relationship is stable; `aria-expanded`
49
+ * carries the current visibility state.
49
50
  *
50
51
  * **This function is for click/keyboard-activated popovers only.** For
51
52
  * hover-driven tooltips, the trigger should use `aria-describedby` instead.
@@ -1,3 +1,4 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
3
  import _createClass from "@babel/runtime/helpers/createClass";
3
4
  /**
@@ -74,6 +75,31 @@ var supportsAnchorPositioning = once(function () {
74
75
  return CSS.supports('anchor-name', '--a');
75
76
  });
76
77
 
78
+ /**
79
+ * A single inline style declaration, as consumed by `setStyle`.
80
+ */
81
+
82
+ /**
83
+ * The custom property mirroring each cross-axis shift margin.
84
+ *
85
+ * These are written for the planned named arrow `@position-try` rules. Nothing
86
+ * reads them yet, and the shift no longer depends on them: the antisymmetric
87
+ * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
88
+ * their own.
89
+ */
90
+ var CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
91
+ 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
92
+ 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
93
+ 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
94
+ 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
95
+ };
96
+ var ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES = Object.values(CROSS_AXIS_SHIFT_CUSTOM_PROPERTY).map(function (property) {
97
+ return {
98
+ property: property,
99
+ value: '0px'
100
+ };
101
+ });
102
+
77
103
  /**
78
104
  * Returns the CSS margin declaration that creates a gap between the
79
105
  * popover and its trigger on the side facing the anchor.
@@ -118,30 +144,54 @@ function edgeMargin(_ref) {
118
144
  }
119
145
 
120
146
  /**
121
- * Cross-axis shift margin. Margin side matches the popover's anchored
122
- * cross-axis edge: `align: 'start'` uses the START margin, `align: 'end'`
123
- * uses the END margin with the sign inverted (so `forwards` is always
124
- * positive), `align: 'center'` uses START. Margin on the un-anchored side
125
- * has no effect under CSS Anchor Positioning.
147
+ * **The cross-axis shift margins.**
148
+ *
149
+ * The shift is written antisymmetrically on BOTH cross-axis sides
150
+ * (`start: +value`, `end: -value`) rather than as a single margin on the side
151
+ * picked from `align`. Two separate behaviours require it:
152
+ *
153
+ * 1. **`align: 'center'` is centered with `anchor-center`, which centers the
154
+ * popover's MARGIN box on the anchor.** A single-sided margin widens the
155
+ * margin box, so it displaces the border box by only half its value. An
156
+ * antisymmetric pair leaves the margin box width unchanged (`+value` and
157
+ * `-value` sum to zero) while moving its center by the full value.
158
+ * 2. **`position-try-fallbacks` can slide the popover onto the opposite
159
+ * cross-axis side.** A `<position-area>` fallback keeps the base style's
160
+ * margins, and margin on the un-anchored side has no effect, so a
161
+ * single-sided margin is silently dropped once the browser slides across the
162
+ * cross axis. With both sides written, whichever side ends up anchored moves
163
+ * the popover the same physical direction.
164
+ *
165
+ * Leaving the margin box width unchanged also means the shift does not move
166
+ * the point at which `position-try-fallbacks` decides the popover overflows.
167
+ *
168
+ * A `<try-tactic>` fallback (`flip-block`, `flip-inline`) behaves differently to
169
+ * a `<position-area>` one: it SWAPS the start and end margins rather than
170
+ * keeping them, which is what preserves the gap through a flip. So a flip across
171
+ * the cross axis mirrors the shift along with the rest of the placement, rather
172
+ * than preserving its physical direction. Only the diagonal fallback does that,
173
+ * and only for `align: 'start' | 'end'`. See `placementToTryFallbacks`.
174
+ *
175
+ * A positive value always moves the popover toward the cross-axis END, so
176
+ * `direction: 'forwards'` means the same thing for every `align` value. See
177
+ * `notes/decisions/placement-offset.md`.
126
178
  */
127
- function crossAxisShiftMargin(_ref2) {
179
+ function crossAxisShiftMargins(_ref2) {
128
180
  var placement = _ref2.placement,
129
181
  crossAxisShiftCssValue = _ref2.crossAxisShiftCssValue,
130
182
  direction = _ref2.direction;
131
- var axis = placement.axis,
132
- align = placement.align;
133
- var directionSign = direction === 'forwards' ? 1 : -1;
134
- var crossAxis = axis === 'block' ? 'inline' : 'block';
135
- var useEndSide = align === 'end';
136
- var side = useEndSide ? 'end' : 'start';
137
- var sideSign = useEndSide ? -1 : 1;
138
- var finalSign = directionSign * sideSign;
139
- // Wrap any string in calc() with the sign factor. CSS handles the math.
140
- var value = finalSign === 1 ? crossAxisShiftCssValue : "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
141
- return {
142
- property: "margin-".concat(crossAxis, "-").concat(side),
143
- value: value
144
- };
183
+ var crossAxis = placement.axis === 'block' ? 'inline' : 'block';
184
+ // Wrap any string in calc() with the sign factor. CSS handles the math,
185
+ // so opaque values such as design tokens negate correctly.
186
+ var negated = "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
187
+ var isForwards = direction === 'forwards';
188
+ return [{
189
+ property: "margin-".concat(crossAxis, "-start"),
190
+ value: isForwards ? crossAxisShiftCssValue : negated
191
+ }, {
192
+ property: "margin-".concat(crossAxis, "-end"),
193
+ value: isForwards ? negated : crossAxisShiftCssValue
194
+ }];
145
195
  }
146
196
 
147
197
  /**
@@ -226,10 +276,9 @@ export function useAnchorPosition(_ref3) {
226
276
  placement: stablePlacement,
227
277
  offset: gapCssValue
228
278
  });
229
- // Computed once; reused below to populate the active
230
- // `--ds-cross-axis-shift-margin-*` custom property for the
231
- // named arrow @position-try rules.
232
- var crossAxisShift = crossAxisShiftMargin({
279
+ // Computed once; also mirrored into the
280
+ // `--ds-cross-axis-shift-margin-*` custom properties below.
281
+ var crossAxisShift = crossAxisShiftMargins({
233
282
  placement: stablePlacement,
234
283
  crossAxisShiftCssValue: crossAxisShiftCssValue,
235
284
  direction: stablePlacement.offset.crossAxisShift.direction
@@ -259,45 +308,14 @@ export function useAnchorPosition(_ref3) {
259
308
  }, {
260
309
  property: gap.property,
261
310
  value: gap.value
262
- }, {
263
- property: crossAxisShift.property,
264
- value: crossAxisShift.value
265
- }];
266
-
267
- // Expose the active cross-axis shift via custom properties so each
268
- // named arrow @position-try rule can re-apply it after a flip.
269
- // One of the four (cross-axis, side) properties is non-zero at a time.
270
- popoverStyles.push({
271
- property: '--ds-cross-axis-shift-margin-start',
272
- value: '0px'
273
- });
274
- popoverStyles.push({
275
- property: '--ds-cross-axis-shift-margin-end',
276
- value: '0px'
277
- });
278
- popoverStyles.push({
279
- property: '--ds-cross-axis-shift-margin-block-start',
280
- value: '0px'
281
- });
282
- popoverStyles.push({
283
- property: '--ds-cross-axis-shift-margin-block-end',
284
- value: '0px'
285
- });
286
- // `crossAxisShift` IS the active margin; reuse instead of recomputing.
287
- var crossAxisShiftActive = crossAxisShift;
288
- var customPropertyByMarginProperty = {
289
- 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
290
- 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
291
- 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
292
- 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
293
- };
294
- var crossAxisShiftCustomProperty = customPropertyByMarginProperty[crossAxisShiftActive.property];
295
- if (crossAxisShiftCustomProperty) {
296
- popoverStyles.push({
297
- property: crossAxisShiftCustomProperty,
298
- value: crossAxisShiftActive.value
299
- });
300
- }
311
+ }].concat(_toConsumableArray(crossAxisShift), _toConsumableArray(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), _toConsumableArray(crossAxisShift.map(function (_ref4) {
312
+ var property = _ref4.property,
313
+ value = _ref4.value;
314
+ return {
315
+ property: CROSS_AXIS_SHIFT_CUSTOM_PROPERTY[property],
316
+ value: value
317
+ };
318
+ })));
301
319
 
302
320
  /**
303
321
  * **We are never cleaning up anchor names**
@@ -5,9 +5,12 @@ import { setStyle } from './set-style';
5
5
  * Sets the width of a popover element relative to its anchor element.
6
6
  *
7
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)
8
+ * - `'min-anchor'` → `min-inline-size: anchor-size(width)` - popover is at least as wide as the
9
+ * anchor, and can grow wider if its content requires it. The anchor width is the ONLY floor: see
10
+ * `notes/decisions/width-from-anchor-floors.md` for why this mode deliberately does not also floor
11
+ * the popover at its own content width.
12
+ * - `'none'` → `min-inline-size: max-content` - no anchor-relative width, but floored at the
13
+ * popover's own content width (default behaviour)
11
14
  *
12
15
  * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
13
16
  * one-off measurement of `anchor.offsetWidth`.
@@ -56,22 +59,24 @@ export function useWidthFromAnchor(_ref) {
56
59
  }
57
60
  return;
58
61
  }
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
- var minInlineSizeStyle = {
64
- property: 'min-inline-size',
65
- value: 'max-content'
66
- };
67
62
  if (mode === 'min-anchor') {
68
- // Set a minimum width matching the anchor. The popover can grow wider
69
- // if its content requires it.
63
+ // One declaration, one floor: the anchor width, as a lower bound and not an
64
+ // exact size, which is what separates 'min-anchor' from 'match-anchor'.
65
+ //
66
+ // This mode deliberately does NOT also floor the popover at its own content
67
+ // width. Mode 'none' does, to make a too-narrow span overflow and drive
68
+ // `position-try-fallbacks`, but that floor can only be expressed on
69
+ // `min-inline-size` or on `width`, and both collide with the anchor floor here:
70
+ // `min-inline-size` IS `min-width` in a horizontal writing mode, and `width` is
71
+ // an exact size, so it would cap the popover and override a consumer width.
72
+ // The anchor floor already guarantees a usable width for this mode, so the
73
+ // popover is allowed to wrap down to it. See
74
+ // `notes/decisions/width-from-anchor-floors.md`.
70
75
  if (supportsAnchorSize) {
71
76
  return setStyle({
72
77
  element: node,
73
- styles: [minInlineSizeStyle, {
74
- property: 'min-width',
78
+ styles: [{
79
+ property: 'min-inline-size',
75
80
  value: 'anchor-size(width)'
76
81
  }]
77
82
  });
@@ -80,24 +85,26 @@ export function useWidthFromAnchor(_ref) {
80
85
  if (_anchor) {
81
86
  return setStyle({
82
87
  element: node,
83
- styles: [minInlineSizeStyle, {
84
- property: 'min-width',
88
+ styles: [{
89
+ property: 'min-inline-size',
85
90
  value: "".concat(_anchor.offsetWidth, "px")
86
91
  }]
87
92
  });
88
93
  }
89
94
 
90
- // No anchor available in JS fallback - still apply min-inline-size.
91
- return setStyle({
92
- element: node,
93
- styles: [minInlineSizeStyle]
94
- });
95
+ // No anchor to measure in the JS fallback, so there is no floor to apply.
96
+ return;
95
97
  }
96
98
 
97
- // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
99
+ // 'none' has no anchor floor to collide with, so it can carry the content floor.
100
+ // `@atlaskit/popper` depends on the property: its fit-to-viewport mode neutralises
101
+ // this floor with `min-inline-size: 0`.
98
102
  return setStyle({
99
103
  element: node,
100
- styles: [minInlineSizeStyle]
104
+ styles: [{
105
+ property: 'min-inline-size',
106
+ value: 'max-content'
107
+ }]
101
108
  });
102
109
  }, [mode, popoverRef, anchorRef, isOpen]);
103
110
  }
@@ -65,8 +65,9 @@ type TAriaForTrigger = {
65
65
  * `menu → "menu"`, `tooltip → undefined`). React omits the attribute when
66
66
  * the value is `undefined`.
67
67
  * - `aria-expanded` - reflects the current open state.
68
- * - `aria-controls` - references the popover element by its id while the
69
- * popover is open, and is omitted while the popover is closed and unmounted.
68
+ * - `aria-controls` - references the popover element by its id, even
69
+ * while the popover is closed. The relationship is stable; `aria-expanded`
70
+ * carries the current visibility state.
70
71
  *
71
72
  * **This function is for click/keyboard-activated popovers only.** For
72
73
  * hover-driven tooltips, the trigger should use `aria-describedby` instead.
@@ -4,9 +4,12 @@ import type { TWidthFromAnchorMode } from '../popover/types';
4
4
  * Sets the width of a popover element relative to its anchor element.
5
5
  *
6
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)
7
+ * - `'min-anchor'` → `min-inline-size: anchor-size(width)` - popover is at least as wide as the
8
+ * anchor, and can grow wider if its content requires it. The anchor width is the ONLY floor: see
9
+ * `notes/decisions/width-from-anchor-floors.md` for why this mode deliberately does not also floor
10
+ * the popover at its own content width.
11
+ * - `'none'` → `min-inline-size: max-content` - no anchor-relative width, but floored at the
12
+ * popover's own content width (default behaviour)
10
13
  *
11
14
  * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
12
15
  * one-off measurement of `anchor.offsetWidth`.
@@ -170,7 +170,8 @@ export type TPopoverProps = TPopoverBaseProps & TAriaRoleRequired & ({
170
170
  * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
171
171
  * is not supported.
172
172
  * - `'min-anchor'`: popover is at least as wide as the anchor, but
173
- * can grow wider if content requires it. Uses `min-width: anchor-size(width)`.
174
- * Falls back to a one-off measurement of the anchor's width.
173
+ * can grow wider if content requires it. Uses
174
+ * `min-inline-size: anchor-size(width)`. Falls back to a one-off
175
+ * measurement of the anchor's width.
175
176
  */
176
177
  export type TWidthFromAnchorMode = 'none' | 'match-anchor' | 'min-anchor';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
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",