@atlaskit/top-layer 4.2.2 → 4.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 4.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`41f64e5df21c7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/41f64e5df21c7) -
8
+ `useWidthFromAnchor` now sizes against `anchor-size(self-inline)` rather than
9
+ `anchor-size(width)`, so an anchor-relative popover width is measured on the same axis as the
10
+ property it is set on. No change in a horizontal writing mode.
11
+
12
+ ## 4.2.3
13
+
14
+ ### Patch Changes
15
+
16
+ - [`3f3fc95b7af6b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/3f3fc95b7af6b) -
17
+ Fixed `offset.crossAxisShift` moving the popover the wrong distance or direction in some
18
+ placements, and restored the documented `useWidthFromAnchor({ mode: 'min-anchor' })` width
19
+ behaviour.
20
+
3
21
  ## 4.2.2
4
22
 
5
23
  ### Patch Changes
@@ -20,6 +20,19 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
20
20
  * position.
21
21
  */
22
22
 
23
+ /**
24
+ * A viewport-relative position, in pixels. Physical rather than logical
25
+ * because it is written straight to `top` / `left`.
26
+ */
27
+
28
+ /**
29
+ * The cross-axis shift with its length already resolved to pixels.
30
+ *
31
+ * Mirrors `TCrossAxisShiftOffset`, whose `value` is an unresolved CSS length
32
+ * string. Only this module needs the pixel form, because only this module does
33
+ * the arithmetic itself.
34
+ */
35
+
23
36
  /**
24
37
  * Computes the base {top, left} position based on the primary edge,
25
38
  * flipping to the opposite side when there is not enough space.
@@ -130,23 +143,26 @@ function applyAlignment(_ref2) {
130
143
  /**
131
144
  * Applies the cross-axis shift offset.
132
145
  *
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.
146
+ * `forwards` always moves the popover toward the cross-axis END and
147
+ * `backwards` toward the START, for every `align` value. See
148
+ * `notes/decisions/placement-offset.md`.
149
+ *
150
+ * This is a coordinate, so the direction is the sign and nothing else. There
151
+ * is deliberately no per-`align` sign inversion here: the CSS path inverts the
152
+ * sign only because a margin on the END side pushes a box the opposite way to
153
+ * a margin on the START side, which is a property of margins rather than of
154
+ * the shift. Applying that inversion to a coordinate moved `align: 'end'`
155
+ * popovers the wrong way.
138
156
  */
139
157
  function applyCrossAxisShift(_ref3) {
140
158
  var position = _ref3.position,
141
159
  axis = _ref3.axis,
142
- align = _ref3.align,
143
160
  crossAxisShift = _ref3.crossAxisShift;
144
161
  if (crossAxisShift.value === 0) {
145
162
  return position;
146
163
  }
147
164
  var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
148
- var sideSign = align === 'end' ? -1 : 1;
149
- var delta = crossAxisShift.value * directionSign * sideSign;
165
+ var delta = crossAxisShift.value * directionSign;
150
166
 
151
167
  // Cross-axis is the OPPOSITE of the placement axis.
152
168
  if (axis === 'block') {
@@ -202,7 +218,6 @@ function computeFallbackPosition(_ref4) {
202
218
  var shiftedPosition = applyCrossAxisShift({
203
219
  position: alignedPosition,
204
220
  axis: axis,
205
- align: align,
206
221
  crossAxisShift: crossAxisShift
207
222
  });
208
223
 
@@ -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.
@@ -12,10 +12,15 @@ var _resolveCssLength = require("./resolve-css-length");
12
12
  */
13
13
  var DEFAULT_GAP = "var(--ds-space-100, 8px)";
14
14
 
15
+ /**
16
+ * Which way along the cross axis a shift moves the popover. `'forwards'` is
17
+ * always toward the cross-axis END and `'backwards'` toward the START, for
18
+ * every `align` value. See `notes/decisions/placement-offset.md`.
19
+ */
20
+
15
21
  /**
16
22
  * Cross-axis shift. `value` is a CSS length string (consumer numbers are
17
- * normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
18
- * `'backwards'` (toward start).
23
+ * normalized to `${n}px`).
19
24
  */
20
25
 
21
26
  /**
@@ -4,6 +4,15 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.setStyle = setStyle;
7
+ /**
8
+ * A single inline style declaration.
9
+ *
10
+ * `TProperty` narrows the property name for callers that write a known, closed
11
+ * set of properties. Narrowing it lets a record keyed on those same names be
12
+ * checked for exhaustiveness at compile time, rather than silently resolving to
13
+ * `undefined` for a name that was never mapped.
14
+ */
15
+
7
16
  /**
8
17
  * Sets inline styles on an element and returns a cleanup function that
9
18
  * restores the prior inline values (so we do not stomp consumer styles).
@@ -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,40 @@ var supportsAnchorPositioning = (0, _once.default)(function () {
81
82
  return CSS.supports('anchor-name', '--a');
82
83
  });
83
84
 
85
+ /**
86
+ * Every logical margin property this module can write: one per axis, per side.
87
+ *
88
+ * Deriving the union from `TPlacementAxis` and `TPlacementEdge` rather than
89
+ * writing the four names out keeps it in step with the placement model, and
90
+ * makes `CROSS_AXIS_SHIFT_CUSTOM_PROPERTY` below exhaustive by construction.
91
+ */
92
+
93
+ /**
94
+ * The custom property mirroring each cross-axis shift margin.
95
+ *
96
+ * These are written for the planned named arrow `@position-try` rules. Nothing
97
+ * reads them yet, and the shift no longer depends on them: the antisymmetric
98
+ * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
99
+ * their own.
100
+ *
101
+ * Keyed on `TLogicalMarginProperty` and not on `string`, so a margin property
102
+ * that has no custom property (or a custom property whose margin no longer
103
+ * exists) is a compile error rather than an `undefined` lookup that reaches
104
+ * `setStyle` as `{ property: undefined, value }`.
105
+ */
106
+ var CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
107
+ 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
108
+ 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
109
+ 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
110
+ 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
111
+ };
112
+ var ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES = Object.values(CROSS_AXIS_SHIFT_CUSTOM_PROPERTY).map(function (property) {
113
+ return {
114
+ property: property,
115
+ value: '0px'
116
+ };
117
+ });
118
+
84
119
  /**
85
120
  * Returns the CSS margin declaration that creates a gap between the
86
121
  * popover and its trigger on the side facing the anchor.
@@ -93,62 +128,64 @@ function edgeMargin(_ref) {
93
128
  offset = _ref.offset;
94
129
  var axis = placement.axis,
95
130
  edge = placement.edge;
96
- var value = offset;
97
- if (axis === 'block' && edge === 'end') {
98
- return {
99
- property: 'margin-block-start',
100
- value: value
101
- };
102
- }
103
- if (axis === 'block' && edge === 'start') {
104
- return {
105
- property: 'margin-block-end',
106
- value: value
107
- };
108
- }
109
- if (axis === 'inline' && edge === 'end') {
110
- return {
111
- property: 'margin-inline-start',
112
- value: value
113
- };
114
- }
115
- if (axis === 'inline' && edge === 'start') {
116
- return {
117
- property: 'margin-inline-end',
118
- value: value
119
- };
120
- }
131
+ // The gap sits on the side FACING the anchor, which is always the side
132
+ // opposite the placement edge.
133
+ var anchorFacingEdge = edge === 'end' ? 'start' : 'end';
121
134
  return {
122
- property: 'margin-block-start',
123
- value: value
135
+ property: "margin-".concat(axis, "-").concat(anchorFacingEdge),
136
+ value: offset
124
137
  };
125
138
  }
126
139
 
127
140
  /**
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.
141
+ * **The cross-axis shift margins.**
142
+ *
143
+ * The shift is written antisymmetrically on BOTH cross-axis sides
144
+ * (`start: +value`, `end: -value`) rather than as a single margin on the side
145
+ * picked from `align`. Two separate behaviours require it:
146
+ *
147
+ * 1. **`align: 'center'` is centered with `anchor-center`, which centers the
148
+ * popover's MARGIN box on the anchor.** A single-sided margin widens the
149
+ * margin box, so it displaces the border box by only half its value. An
150
+ * antisymmetric pair leaves the margin box width unchanged (`+value` and
151
+ * `-value` sum to zero) while moving its center by the full value.
152
+ * 2. **`position-try-fallbacks` can slide the popover onto the opposite
153
+ * cross-axis side.** A `<position-area>` fallback keeps the base style's
154
+ * margins, and margin on the un-anchored side has no effect, so a
155
+ * single-sided margin is silently dropped once the browser slides across the
156
+ * cross axis. With both sides written, whichever side ends up anchored moves
157
+ * the popover the same physical direction.
158
+ *
159
+ * Leaving the margin box width unchanged also means the shift does not move
160
+ * the point at which `position-try-fallbacks` decides the popover overflows.
161
+ *
162
+ * A `<try-tactic>` fallback (`flip-block`, `flip-inline`) behaves differently to
163
+ * a `<position-area>` one: it SWAPS the start and end margins rather than
164
+ * keeping them, which is what preserves the gap through a flip. So a flip across
165
+ * the cross axis mirrors the shift along with the rest of the placement, rather
166
+ * than preserving its physical direction. Only the diagonal fallback does that,
167
+ * and only for `align: 'start' | 'end'`. See `placementToTryFallbacks`.
168
+ *
169
+ * A positive value always moves the popover toward the cross-axis END, so
170
+ * `direction: 'forwards'` means the same thing for every `align` value. See
171
+ * `notes/decisions/placement-offset.md`.
133
172
  */
134
- function crossAxisShiftMargin(_ref2) {
173
+ function crossAxisShiftMargins(_ref2) {
135
174
  var placement = _ref2.placement,
136
175
  crossAxisShiftCssValue = _ref2.crossAxisShiftCssValue,
137
176
  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
- };
177
+ var crossAxis = placement.axis === 'block' ? 'inline' : 'block';
178
+ // Wrap any string in calc() with the sign factor. CSS handles the math,
179
+ // so opaque values such as design tokens negate correctly.
180
+ var negated = "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
181
+ var isForwards = direction === 'forwards';
182
+ return [{
183
+ property: "margin-".concat(crossAxis, "-start"),
184
+ value: isForwards ? crossAxisShiftCssValue : negated
185
+ }, {
186
+ property: "margin-".concat(crossAxis, "-end"),
187
+ value: isForwards ? negated : crossAxisShiftCssValue
188
+ }];
152
189
  }
153
190
 
154
191
  /**
@@ -233,10 +270,9 @@ function useAnchorPosition(_ref3) {
233
270
  placement: stablePlacement,
234
271
  offset: gapCssValue
235
272
  });
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({
273
+ // Computed once; also mirrored into the
274
+ // `--ds-cross-axis-shift-margin-*` custom properties below.
275
+ var crossAxisShift = crossAxisShiftMargins({
240
276
  placement: stablePlacement,
241
277
  crossAxisShiftCssValue: crossAxisShiftCssValue,
242
278
  direction: stablePlacement.offset.crossAxisShift.direction
@@ -263,48 +299,14 @@ function useAnchorPosition(_ref3) {
263
299
  }, {
264
300
  property: 'inset',
265
301
  value: 'auto'
266
- }, {
267
- property: gap.property,
268
- 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
- }
302
+ }, gap].concat((0, _toConsumableArray2.default)(crossAxisShift), (0, _toConsumableArray2.default)(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), (0, _toConsumableArray2.default)(crossAxisShift.map(function (_ref4) {
303
+ var property = _ref4.property,
304
+ value = _ref4.value;
305
+ return {
306
+ property: CROSS_AXIS_SHIFT_CUSTOM_PROPERTY[property],
307
+ value: value
308
+ };
309
+ })));
308
310
 
309
311
  /**
310
312
  * **We are never cleaning up anchor names**
@@ -1,83 +1,95 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
7
  exports.useWidthFromAnchor = useWidthFromAnchor;
7
8
  var _react = require("react");
9
+ var _once = _interopRequireDefault(require("@atlaskit/ds-lib/once"));
8
10
  var _setStyle = require("./set-style");
11
+ /**
12
+ * Probes anchor SIZING, separately from `anchor-name` (see
13
+ * `supportsAnchorPositioning`): some browsers have positioning but not sizing.
14
+ * `once()` for SSR safety.
15
+ */
16
+ var supportsAnchorSize = (0, _once.default)(function () {
17
+ if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
18
+ return false;
19
+ }
20
+ return CSS.supports('min-inline-size', 'anchor-size(self-inline)');
21
+ });
22
+
23
+ /**
24
+ * The JS fallback's stand-in for `anchor-size(self-inline)`. `offsetWidth` / `offsetHeight` are
25
+ * physical, so the axis is picked from the popover's `writing-mode`. Anything not horizontal is
26
+ * treated as vertical, and an unknown or empty value falls back to `offsetWidth`.
27
+ */
28
+ function getFallbackAnchorInlineSize(_ref) {
29
+ var anchor = _ref.anchor,
30
+ popover = _ref.popover;
31
+ // `String()` because not every test environment implements `writing-mode`.
32
+ var writingMode = String(getComputedStyle(popover).writingMode);
33
+ var isVertical = writingMode.startsWith('vertical') || writingMode.startsWith('sideways');
34
+ return isVertical ? anchor.offsetHeight : anchor.offsetWidth;
35
+ }
36
+
9
37
  /**
10
38
  * Sets the width of a popover element relative to its anchor element.
11
39
  *
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)
40
+ * - `'match-anchor'` → `inline-size: anchor-size(self-inline)`, exact
41
+ * - `'min-anchor'` → `min-inline-size: anchor-size(self-inline)`, floor only
42
+ * - `'none'` → `min-inline-size: max-content`, content floor (default)
16
43
  *
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.
44
+ * `self-inline` is the inline axis of the element USING the function (the popover), never the
45
+ * anchor, so it matches the axis `inline-size` constrains. See
46
+ * `notes/decisions/width-from-anchor-floors.md`.
21
47
  */
22
- function useWidthFromAnchor(_ref) {
23
- var mode = _ref.mode,
24
- popoverRef = _ref.popoverRef,
25
- anchorRef = _ref.anchorRef,
26
- isOpen = _ref.isOpen;
48
+ function useWidthFromAnchor(_ref2) {
49
+ var mode = _ref2.mode,
50
+ popoverRef = _ref2.popoverRef,
51
+ anchorRef = _ref2.anchorRef,
52
+ isOpen = _ref2.isOpen;
27
53
  (0, _react.useLayoutEffect)(function () {
28
54
  var node = popoverRef.current;
29
55
  if (!node) {
30
56
  return;
31
57
  }
32
-
33
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
34
- // so the JS fallback fires correctly in browsers that support anchor
35
- // positioning but not anchor sizing.
36
- var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
37
58
  if (mode === 'match-anchor') {
38
- // Set an exact width matching the anchor.
39
- // `min-inline-size` is intentionally NOT set here - it would override the
40
- // explicit width constraint, allowing the popover to grow past the anchor's width.
41
- if (supportsAnchorSize) {
59
+ // No floor here: it would let the popover grow past the anchor.
60
+ if (supportsAnchorSize()) {
42
61
  return (0, _setStyle.setStyle)({
43
62
  element: node,
44
63
  styles: [{
45
- property: 'width',
46
- value: 'anchor-size(width)'
64
+ property: 'inline-size',
65
+ value: 'anchor-size(self-inline)'
47
66
  }]
48
67
  });
49
68
  }
50
-
51
- // JS fallback: one-off read of the anchor's width.
52
69
  var anchor = anchorRef.current;
53
70
  if (anchor) {
54
71
  return (0, _setStyle.setStyle)({
55
72
  element: node,
56
73
  styles: [{
57
- property: 'width',
58
- value: "".concat(anchor.offsetWidth, "px")
74
+ property: 'inline-size',
75
+ value: "".concat(getFallbackAnchorInlineSize({
76
+ anchor: anchor,
77
+ popover: node
78
+ }), "px")
59
79
  }]
60
80
  });
61
81
  }
62
82
  return;
63
83
  }
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
84
  if (mode === 'min-anchor') {
73
- // Set a minimum width matching the anchor. The popover can grow wider
74
- // if its content requires it.
75
- if (supportsAnchorSize) {
85
+ // One floor only. The content floor from mode 'none' deliberately does not also apply
86
+ // here, and cannot: it would need this same property. See the decision note.
87
+ if (supportsAnchorSize()) {
76
88
  return (0, _setStyle.setStyle)({
77
89
  element: node,
78
- styles: [minInlineSizeStyle, {
79
- property: 'min-width',
80
- value: 'anchor-size(width)'
90
+ styles: [{
91
+ property: 'min-inline-size',
92
+ value: 'anchor-size(self-inline)'
81
93
  }]
82
94
  });
83
95
  }
@@ -85,24 +97,28 @@ function useWidthFromAnchor(_ref) {
85
97
  if (_anchor) {
86
98
  return (0, _setStyle.setStyle)({
87
99
  element: node,
88
- styles: [minInlineSizeStyle, {
89
- property: 'min-width',
90
- value: "".concat(_anchor.offsetWidth, "px")
100
+ styles: [{
101
+ property: 'min-inline-size',
102
+ value: "".concat(getFallbackAnchorInlineSize({
103
+ anchor: _anchor,
104
+ popover: node
105
+ }), "px")
91
106
  }]
92
107
  });
93
108
  }
94
109
 
95
- // No anchor available in JS fallback - still apply min-inline-size.
96
- return (0, _setStyle.setStyle)({
97
- element: node,
98
- styles: [minInlineSizeStyle]
99
- });
110
+ // Nothing to measure, so no floor.
111
+ return;
100
112
  }
101
113
 
102
- // 'none' mode: no explicit width, but still apply `min-inline-size: max-content`.
114
+ // Makes a too-narrow span overflow and drive `position-try-fallbacks` instead of wrapping.
115
+ // `@atlaskit/popper` resets this exact property to `0` for its fit-to-viewport mode.
103
116
  return (0, _setStyle.setStyle)({
104
117
  element: node,
105
- styles: [minInlineSizeStyle]
118
+ styles: [{
119
+ property: 'min-inline-size',
120
+ value: 'max-content'
121
+ }]
106
122
  });
107
123
  }, [mode, popoverRef, anchorRef, isOpen]);
108
124
  }
@@ -10,6 +10,19 @@
10
10
  * position.
11
11
  */
12
12
 
13
+ /**
14
+ * A viewport-relative position, in pixels. Physical rather than logical
15
+ * because it is written straight to `top` / `left`.
16
+ */
17
+
18
+ /**
19
+ * The cross-axis shift with its length already resolved to pixels.
20
+ *
21
+ * Mirrors `TCrossAxisShiftOffset`, whose `value` is an unresolved CSS length
22
+ * string. Only this module needs the pixel form, because only this module does
23
+ * the arithmetic itself.
24
+ */
25
+
13
26
  /**
14
27
  * Computes the base {top, left} position based on the primary edge,
15
28
  * flipping to the opposite side when there is not enough space.
@@ -126,24 +139,27 @@ function applyAlignment({
126
139
  /**
127
140
  * Applies the cross-axis shift offset.
128
141
  *
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.
142
+ * `forwards` always moves the popover toward the cross-axis END and
143
+ * `backwards` toward the START, for every `align` value. See
144
+ * `notes/decisions/placement-offset.md`.
145
+ *
146
+ * This is a coordinate, so the direction is the sign and nothing else. There
147
+ * is deliberately no per-`align` sign inversion here: the CSS path inverts the
148
+ * sign only because a margin on the END side pushes a box the opposite way to
149
+ * a margin on the START side, which is a property of margins rather than of
150
+ * the shift. Applying that inversion to a coordinate moved `align: 'end'`
151
+ * popovers the wrong way.
134
152
  */
135
153
  function applyCrossAxisShift({
136
154
  position,
137
155
  axis,
138
- align,
139
156
  crossAxisShift
140
157
  }) {
141
158
  if (crossAxisShift.value === 0) {
142
159
  return position;
143
160
  }
144
161
  const directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
145
- const sideSign = align === 'end' ? -1 : 1;
146
- const delta = crossAxisShift.value * directionSign * sideSign;
162
+ const delta = crossAxisShift.value * directionSign;
147
163
 
148
164
  // Cross-axis is the OPPOSITE of the placement axis.
149
165
  if (axis === 'block') {
@@ -204,7 +220,6 @@ export function computeFallbackPosition({
204
220
  const shiftedPosition = applyCrossAxisShift({
205
221
  position: alignedPosition,
206
222
  axis,
207
- align,
208
223
  crossAxisShift
209
224
  });
210
225
 
@@ -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.
@@ -7,10 +7,15 @@ import { toCssLengthString } from './resolve-css-length';
7
7
  */
8
8
  const DEFAULT_GAP = "var(--ds-space-100, 8px)";
9
9
 
10
+ /**
11
+ * Which way along the cross axis a shift moves the popover. `'forwards'` is
12
+ * always toward the cross-axis END and `'backwards'` toward the START, for
13
+ * every `align` value. See `notes/decisions/placement-offset.md`.
14
+ */
15
+
10
16
  /**
11
17
  * Cross-axis shift. `value` is a CSS length string (consumer numbers are
12
- * normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
13
- * `'backwards'` (toward start).
18
+ * normalized to `${n}px`).
14
19
  */
15
20
 
16
21
  /**
@@ -1,3 +1,12 @@
1
+ /**
2
+ * A single inline style declaration.
3
+ *
4
+ * `TProperty` narrows the property name for callers that write a known, closed
5
+ * set of properties. Narrowing it lets a record keyed on those same names be
6
+ * checked for exhaustiveness at compile time, rather than silently resolving to
7
+ * `undefined` for a name that was never mapped.
8
+ */
9
+
1
10
  /**
2
11
  * Sets inline styles on an element and returns a cleanup function that
3
12
  * restores the prior inline values (so we do not stomp consumer styles).