@atlaskit/top-layer 4.2.3 → 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,14 @@
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
+
3
12
  ## 4.2.3
4
13
 
5
14
  ### 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.
@@ -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).
@@ -83,7 +83,11 @@ var supportsAnchorPositioning = (0, _once.default)(function () {
83
83
  });
84
84
 
85
85
  /**
86
- * A single inline style declaration, as consumed by `setStyle`.
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.
87
91
  */
88
92
 
89
93
  /**
@@ -93,6 +97,11 @@ var supportsAnchorPositioning = (0, _once.default)(function () {
93
97
  * reads them yet, and the shift no longer depends on them: the antisymmetric
94
98
  * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
95
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 }`.
96
105
  */
97
106
  var CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
98
107
  'margin-inline-start': '--ds-cross-axis-shift-margin-start',
@@ -119,34 +128,12 @@ function edgeMargin(_ref) {
119
128
  offset = _ref.offset;
120
129
  var axis = placement.axis,
121
130
  edge = placement.edge;
122
- var value = offset;
123
- if (axis === 'block' && edge === 'end') {
124
- return {
125
- property: 'margin-block-start',
126
- value: value
127
- };
128
- }
129
- if (axis === 'block' && edge === 'start') {
130
- return {
131
- property: 'margin-block-end',
132
- value: value
133
- };
134
- }
135
- if (axis === 'inline' && edge === 'end') {
136
- return {
137
- property: 'margin-inline-start',
138
- value: value
139
- };
140
- }
141
- if (axis === 'inline' && edge === 'start') {
142
- return {
143
- property: 'margin-inline-end',
144
- value: value
145
- };
146
- }
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';
147
134
  return {
148
- property: 'margin-block-start',
149
- value: value
135
+ property: "margin-".concat(axis, "-").concat(anchorFacingEdge),
136
+ value: offset
150
137
  };
151
138
  }
152
139
 
@@ -312,10 +299,7 @@ function useAnchorPosition(_ref3) {
312
299
  }, {
313
300
  property: 'inset',
314
301
  value: 'auto'
315
- }, {
316
- property: gap.property,
317
- value: gap.value
318
- }].concat((0, _toConsumableArray2.default)(crossAxisShift), (0, _toConsumableArray2.default)(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), (0, _toConsumableArray2.default)(crossAxisShift.map(function (_ref4) {
302
+ }, gap].concat((0, _toConsumableArray2.default)(crossAxisShift), (0, _toConsumableArray2.default)(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), (0, _toConsumableArray2.default)(crossAxisShift.map(function (_ref4) {
319
303
  var property = _ref4.property,
320
304
  value = _ref4.value;
321
305
  return {
@@ -1,88 +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-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)
19
- *
20
- * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
21
- * one-off measurement of `anchor.offsetWidth`.
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)
22
43
  *
23
- * 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`.
24
47
  */
25
- function useWidthFromAnchor(_ref) {
26
- var mode = _ref.mode,
27
- popoverRef = _ref.popoverRef,
28
- anchorRef = _ref.anchorRef,
29
- isOpen = _ref.isOpen;
48
+ function useWidthFromAnchor(_ref2) {
49
+ var mode = _ref2.mode,
50
+ popoverRef = _ref2.popoverRef,
51
+ anchorRef = _ref2.anchorRef,
52
+ isOpen = _ref2.isOpen;
30
53
  (0, _react.useLayoutEffect)(function () {
31
54
  var node = popoverRef.current;
32
55
  if (!node) {
33
56
  return;
34
57
  }
35
-
36
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
37
- // so the JS fallback fires correctly in browsers that support anchor
38
- // positioning but not anchor sizing.
39
- var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
40
58
  if (mode === 'match-anchor') {
41
- // Set an exact width matching the anchor.
42
- // `min-inline-size` is intentionally NOT set here - it would override the
43
- // explicit width constraint, allowing the popover to grow past the anchor's width.
44
- if (supportsAnchorSize) {
59
+ // No floor here: it would let the popover grow past the anchor.
60
+ if (supportsAnchorSize()) {
45
61
  return (0, _setStyle.setStyle)({
46
62
  element: node,
47
63
  styles: [{
48
- property: 'width',
49
- value: 'anchor-size(width)'
64
+ property: 'inline-size',
65
+ value: 'anchor-size(self-inline)'
50
66
  }]
51
67
  });
52
68
  }
53
-
54
- // JS fallback: one-off read of the anchor's width.
55
69
  var anchor = anchorRef.current;
56
70
  if (anchor) {
57
71
  return (0, _setStyle.setStyle)({
58
72
  element: node,
59
73
  styles: [{
60
- property: 'width',
61
- value: "".concat(anchor.offsetWidth, "px")
74
+ property: 'inline-size',
75
+ value: "".concat(getFallbackAnchorInlineSize({
76
+ anchor: anchor,
77
+ popover: node
78
+ }), "px")
62
79
  }]
63
80
  });
64
81
  }
65
82
  return;
66
83
  }
67
84
  if (mode === 'min-anchor') {
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`.
80
- 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()) {
81
88
  return (0, _setStyle.setStyle)({
82
89
  element: node,
83
90
  styles: [{
84
91
  property: 'min-inline-size',
85
- value: 'anchor-size(width)'
92
+ value: 'anchor-size(self-inline)'
86
93
  }]
87
94
  });
88
95
  }
@@ -92,18 +99,20 @@ function useWidthFromAnchor(_ref) {
92
99
  element: node,
93
100
  styles: [{
94
101
  property: 'min-inline-size',
95
- value: "".concat(_anchor.offsetWidth, "px")
102
+ value: "".concat(getFallbackAnchorInlineSize({
103
+ anchor: _anchor,
104
+ popover: node
105
+ }), "px")
96
106
  }]
97
107
  });
98
108
  }
99
109
 
100
- // No anchor to measure in the JS fallback, so there is no floor to apply.
110
+ // Nothing to measure, so no floor.
101
111
  return;
102
112
  }
103
113
 
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`.
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.
107
116
  return (0, _setStyle.setStyle)({
108
117
  element: node,
109
118
  styles: [{
@@ -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.
@@ -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).
@@ -63,7 +63,11 @@ const supportsAnchorPositioning = once(() => {
63
63
  });
64
64
 
65
65
  /**
66
- * A single inline style declaration, as consumed by `setStyle`.
66
+ * Every logical margin property this module can write: one per axis, per side.
67
+ *
68
+ * Deriving the union from `TPlacementAxis` and `TPlacementEdge` rather than
69
+ * writing the four names out keeps it in step with the placement model, and
70
+ * makes `CROSS_AXIS_SHIFT_CUSTOM_PROPERTY` below exhaustive by construction.
67
71
  */
68
72
 
69
73
  /**
@@ -73,6 +77,11 @@ const supportsAnchorPositioning = once(() => {
73
77
  * reads them yet, and the shift no longer depends on them: the antisymmetric
74
78
  * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
75
79
  * their own.
80
+ *
81
+ * Keyed on `TLogicalMarginProperty` and not on `string`, so a margin property
82
+ * that has no custom property (or a custom property whose margin no longer
83
+ * exists) is a compile error rather than an `undefined` lookup that reaches
84
+ * `setStyle` as `{ property: undefined, value }`.
76
85
  */
77
86
  const CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
78
87
  'margin-inline-start': '--ds-cross-axis-shift-margin-start',
@@ -100,34 +109,12 @@ function edgeMargin({
100
109
  axis,
101
110
  edge
102
111
  } = placement;
103
- const value = offset;
104
- if (axis === 'block' && edge === 'end') {
105
- return {
106
- property: 'margin-block-start',
107
- value
108
- };
109
- }
110
- if (axis === 'block' && edge === 'start') {
111
- return {
112
- property: 'margin-block-end',
113
- value
114
- };
115
- }
116
- if (axis === 'inline' && edge === 'end') {
117
- return {
118
- property: 'margin-inline-start',
119
- value
120
- };
121
- }
122
- if (axis === 'inline' && edge === 'start') {
123
- return {
124
- property: 'margin-inline-end',
125
- value
126
- };
127
- }
112
+ // The gap sits on the side FACING the anchor, which is always the side
113
+ // opposite the placement edge.
114
+ const anchorFacingEdge = edge === 'end' ? 'start' : 'end';
128
115
  return {
129
- property: 'margin-block-start',
130
- value
116
+ property: `margin-${axis}-${anchorFacingEdge}`,
117
+ value: offset
131
118
  };
132
119
  }
133
120
 
@@ -290,10 +277,7 @@ export function useAnchorPosition({
290
277
  }, {
291
278
  property: 'inset',
292
279
  value: 'auto'
293
- }, {
294
- property: gap.property,
295
- value: gap.value
296
- }, ...crossAxisShift,
280
+ }, gap, ...crossAxisShift,
297
281
  // Zero every side first so the two sides that are NOT on the
298
282
  // active cross axis do not keep a value from a previous placement.
299
283
  ...ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES, ...crossAxisShift.map(({
@@ -1,21 +1,44 @@
1
1
  import { useLayoutEffect } from 'react';
2
+ import once from '@atlaskit/ds-lib/once';
2
3
  import { setStyle } from './set-style';
3
4
 
5
+ /**
6
+ * Probes anchor SIZING, separately from `anchor-name` (see
7
+ * `supportsAnchorPositioning`): some browsers have positioning but not sizing.
8
+ * `once()` for SSR safety.
9
+ */
10
+ const supportsAnchorSize = once(() => {
11
+ if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
12
+ return false;
13
+ }
14
+ return CSS.supports('min-inline-size', 'anchor-size(self-inline)');
15
+ });
16
+
17
+ /**
18
+ * The JS fallback's stand-in for `anchor-size(self-inline)`. `offsetWidth` / `offsetHeight` are
19
+ * physical, so the axis is picked from the popover's `writing-mode`. Anything not horizontal is
20
+ * treated as vertical, and an unknown or empty value falls back to `offsetWidth`.
21
+ */
22
+ function getFallbackAnchorInlineSize({
23
+ anchor,
24
+ popover
25
+ }) {
26
+ // `String()` because not every test environment implements `writing-mode`.
27
+ const writingMode = String(getComputedStyle(popover).writingMode);
28
+ const isVertical = writingMode.startsWith('vertical') || writingMode.startsWith('sideways');
29
+ return isVertical ? anchor.offsetHeight : anchor.offsetWidth;
30
+ }
31
+
4
32
  /**
5
33
  * Sets the width of a popover element relative to its anchor element.
6
34
  *
7
- * - `'match-anchor'` → `width: anchor-size(width)` - popover matches anchor width exactly.
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)
14
- *
15
- * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
16
- * one-off measurement of `anchor.offsetWidth`.
35
+ * - `'match-anchor'` → `inline-size: anchor-size(self-inline)`, exact
36
+ * - `'min-anchor'` → `min-inline-size: anchor-size(self-inline)`, floor only
37
+ * - `'none'` → `min-inline-size: max-content`, content floor (default)
17
38
  *
18
- * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
39
+ * `self-inline` is the inline axis of the element USING the function (the popover), never the
40
+ * anchor, so it matches the axis `inline-size` constrains. See
41
+ * `notes/decisions/width-from-anchor-floors.md`.
19
42
  */
20
43
  export function useWidthFromAnchor({
21
44
  mode,
@@ -28,57 +51,41 @@ export function useWidthFromAnchor({
28
51
  if (!node) {
29
52
  return;
30
53
  }
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
- const supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
36
54
  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) {
55
+ // No floor here: it would let the popover grow past the anchor.
56
+ if (supportsAnchorSize()) {
41
57
  return setStyle({
42
58
  element: node,
43
59
  styles: [{
44
- property: 'width',
45
- value: 'anchor-size(width)'
60
+ property: 'inline-size',
61
+ value: 'anchor-size(self-inline)'
46
62
  }]
47
63
  });
48
64
  }
49
-
50
- // JS fallback: one-off read of the anchor's width.
51
65
  const anchor = anchorRef.current;
52
66
  if (anchor) {
53
67
  return setStyle({
54
68
  element: node,
55
69
  styles: [{
56
- property: 'width',
57
- value: `${anchor.offsetWidth}px`
70
+ property: 'inline-size',
71
+ value: `${getFallbackAnchorInlineSize({
72
+ anchor,
73
+ popover: node
74
+ })}px`
58
75
  }]
59
76
  });
60
77
  }
61
78
  return;
62
79
  }
63
80
  if (mode === 'min-anchor') {
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`.
76
- if (supportsAnchorSize) {
81
+ // One floor only. The content floor from mode 'none' deliberately does not also apply
82
+ // here, and cannot: it would need this same property. See the decision note.
83
+ if (supportsAnchorSize()) {
77
84
  return setStyle({
78
85
  element: node,
79
86
  styles: [{
80
87
  property: 'min-inline-size',
81
- value: 'anchor-size(width)'
88
+ value: 'anchor-size(self-inline)'
82
89
  }]
83
90
  });
84
91
  }
@@ -88,18 +95,20 @@ export function useWidthFromAnchor({
88
95
  element: node,
89
96
  styles: [{
90
97
  property: 'min-inline-size',
91
- value: `${anchor.offsetWidth}px`
98
+ value: `${getFallbackAnchorInlineSize({
99
+ anchor,
100
+ popover: node
101
+ })}px`
92
102
  }]
93
103
  });
94
104
  }
95
105
 
96
- // No anchor to measure in the JS fallback, so there is no floor to apply.
106
+ // Nothing to measure, so no floor.
97
107
  return;
98
108
  }
99
109
 
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`.
110
+ // Makes a too-narrow span overflow and drive `position-try-fallbacks` instead of wrapping.
111
+ // `@atlaskit/popper` resets this exact property to `0` for its fit-to-viewport mode.
103
112
  return setStyle({
104
113
  element: node,
105
114
  styles: [{
@@ -13,6 +13,19 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
13
13
  * position.
14
14
  */
15
15
 
16
+ /**
17
+ * A viewport-relative position, in pixels. Physical rather than logical
18
+ * because it is written straight to `top` / `left`.
19
+ */
20
+
21
+ /**
22
+ * The cross-axis shift with its length already resolved to pixels.
23
+ *
24
+ * Mirrors `TCrossAxisShiftOffset`, whose `value` is an unresolved CSS length
25
+ * string. Only this module needs the pixel form, because only this module does
26
+ * the arithmetic itself.
27
+ */
28
+
16
29
  /**
17
30
  * Computes the base {top, left} position based on the primary edge,
18
31
  * flipping to the opposite side when there is not enough space.
@@ -7,10 +7,15 @@ import { toCssLengthString } from './resolve-css-length';
7
7
  */
8
8
  var 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).
@@ -76,7 +76,11 @@ var supportsAnchorPositioning = once(function () {
76
76
  });
77
77
 
78
78
  /**
79
- * A single inline style declaration, as consumed by `setStyle`.
79
+ * Every logical margin property this module can write: one per axis, per side.
80
+ *
81
+ * Deriving the union from `TPlacementAxis` and `TPlacementEdge` rather than
82
+ * writing the four names out keeps it in step with the placement model, and
83
+ * makes `CROSS_AXIS_SHIFT_CUSTOM_PROPERTY` below exhaustive by construction.
80
84
  */
81
85
 
82
86
  /**
@@ -86,6 +90,11 @@ var supportsAnchorPositioning = once(function () {
86
90
  * reads them yet, and the shift no longer depends on them: the antisymmetric
87
91
  * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
88
92
  * their own.
93
+ *
94
+ * Keyed on `TLogicalMarginProperty` and not on `string`, so a margin property
95
+ * that has no custom property (or a custom property whose margin no longer
96
+ * exists) is a compile error rather than an `undefined` lookup that reaches
97
+ * `setStyle` as `{ property: undefined, value }`.
89
98
  */
90
99
  var CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
91
100
  'margin-inline-start': '--ds-cross-axis-shift-margin-start',
@@ -112,34 +121,12 @@ function edgeMargin(_ref) {
112
121
  offset = _ref.offset;
113
122
  var axis = placement.axis,
114
123
  edge = placement.edge;
115
- var value = offset;
116
- if (axis === 'block' && edge === 'end') {
117
- return {
118
- property: 'margin-block-start',
119
- value: value
120
- };
121
- }
122
- if (axis === 'block' && edge === 'start') {
123
- return {
124
- property: 'margin-block-end',
125
- value: value
126
- };
127
- }
128
- if (axis === 'inline' && edge === 'end') {
129
- return {
130
- property: 'margin-inline-start',
131
- value: value
132
- };
133
- }
134
- if (axis === 'inline' && edge === 'start') {
135
- return {
136
- property: 'margin-inline-end',
137
- value: value
138
- };
139
- }
124
+ // The gap sits on the side FACING the anchor, which is always the side
125
+ // opposite the placement edge.
126
+ var anchorFacingEdge = edge === 'end' ? 'start' : 'end';
140
127
  return {
141
- property: 'margin-block-start',
142
- value: value
128
+ property: "margin-".concat(axis, "-").concat(anchorFacingEdge),
129
+ value: offset
143
130
  };
144
131
  }
145
132
 
@@ -305,10 +292,7 @@ export function useAnchorPosition(_ref3) {
305
292
  }, {
306
293
  property: 'inset',
307
294
  value: 'auto'
308
- }, {
309
- property: gap.property,
310
- value: gap.value
311
- }].concat(_toConsumableArray(crossAxisShift), _toConsumableArray(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), _toConsumableArray(crossAxisShift.map(function (_ref4) {
295
+ }, gap].concat(_toConsumableArray(crossAxisShift), _toConsumableArray(ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES), _toConsumableArray(crossAxisShift.map(function (_ref4) {
312
296
  var property = _ref4.property,
313
297
  value = _ref4.value;
314
298
  return {
@@ -1,83 +1,89 @@
1
1
  import { useLayoutEffect } from 'react';
2
+ import once from '@atlaskit/ds-lib/once';
2
3
  import { setStyle } from './set-style';
3
4
 
5
+ /**
6
+ * Probes anchor SIZING, separately from `anchor-name` (see
7
+ * `supportsAnchorPositioning`): some browsers have positioning but not sizing.
8
+ * `once()` for SSR safety.
9
+ */
10
+ var supportsAnchorSize = once(function () {
11
+ if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
12
+ return false;
13
+ }
14
+ return CSS.supports('min-inline-size', 'anchor-size(self-inline)');
15
+ });
16
+
17
+ /**
18
+ * The JS fallback's stand-in for `anchor-size(self-inline)`. `offsetWidth` / `offsetHeight` are
19
+ * physical, so the axis is picked from the popover's `writing-mode`. Anything not horizontal is
20
+ * treated as vertical, and an unknown or empty value falls back to `offsetWidth`.
21
+ */
22
+ function getFallbackAnchorInlineSize(_ref) {
23
+ var anchor = _ref.anchor,
24
+ popover = _ref.popover;
25
+ // `String()` because not every test environment implements `writing-mode`.
26
+ var writingMode = String(getComputedStyle(popover).writingMode);
27
+ var isVertical = writingMode.startsWith('vertical') || writingMode.startsWith('sideways');
28
+ return isVertical ? anchor.offsetHeight : anchor.offsetWidth;
29
+ }
30
+
4
31
  /**
5
32
  * Sets the width of a popover element relative to its anchor element.
6
33
  *
7
- * - `'match-anchor'` → `width: anchor-size(width)` - popover matches anchor width exactly.
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)
14
- *
15
- * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
16
- * one-off measurement of `anchor.offsetWidth`.
34
+ * - `'match-anchor'` → `inline-size: anchor-size(self-inline)`, exact
35
+ * - `'min-anchor'` → `min-inline-size: anchor-size(self-inline)`, floor only
36
+ * - `'none'` → `min-inline-size: max-content`, content floor (default)
17
37
  *
18
- * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
38
+ * `self-inline` is the inline axis of the element USING the function (the popover), never the
39
+ * anchor, so it matches the axis `inline-size` constrains. See
40
+ * `notes/decisions/width-from-anchor-floors.md`.
19
41
  */
20
- export function useWidthFromAnchor(_ref) {
21
- var mode = _ref.mode,
22
- popoverRef = _ref.popoverRef,
23
- anchorRef = _ref.anchorRef,
24
- isOpen = _ref.isOpen;
42
+ export function useWidthFromAnchor(_ref2) {
43
+ var mode = _ref2.mode,
44
+ popoverRef = _ref2.popoverRef,
45
+ anchorRef = _ref2.anchorRef,
46
+ isOpen = _ref2.isOpen;
25
47
  useLayoutEffect(function () {
26
48
  var node = popoverRef.current;
27
49
  if (!node) {
28
50
  return;
29
51
  }
30
-
31
- // Feature-detect `anchor-size()` specifically (not just `anchor-name`)
32
- // so the JS fallback fires correctly in browsers that support anchor
33
- // positioning but not anchor sizing.
34
- var supportsAnchorSize = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('width', 'anchor-size(width)');
35
52
  if (mode === 'match-anchor') {
36
- // Set an exact width matching the anchor.
37
- // `min-inline-size` is intentionally NOT set here - it would override the
38
- // explicit width constraint, allowing the popover to grow past the anchor's width.
39
- if (supportsAnchorSize) {
53
+ // No floor here: it would let the popover grow past the anchor.
54
+ if (supportsAnchorSize()) {
40
55
  return setStyle({
41
56
  element: node,
42
57
  styles: [{
43
- property: 'width',
44
- value: 'anchor-size(width)'
58
+ property: 'inline-size',
59
+ value: 'anchor-size(self-inline)'
45
60
  }]
46
61
  });
47
62
  }
48
-
49
- // JS fallback: one-off read of the anchor's width.
50
63
  var anchor = anchorRef.current;
51
64
  if (anchor) {
52
65
  return setStyle({
53
66
  element: node,
54
67
  styles: [{
55
- property: 'width',
56
- value: "".concat(anchor.offsetWidth, "px")
68
+ property: 'inline-size',
69
+ value: "".concat(getFallbackAnchorInlineSize({
70
+ anchor: anchor,
71
+ popover: node
72
+ }), "px")
57
73
  }]
58
74
  });
59
75
  }
60
76
  return;
61
77
  }
62
78
  if (mode === 'min-anchor') {
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`.
75
- if (supportsAnchorSize) {
79
+ // One floor only. The content floor from mode 'none' deliberately does not also apply
80
+ // here, and cannot: it would need this same property. See the decision note.
81
+ if (supportsAnchorSize()) {
76
82
  return setStyle({
77
83
  element: node,
78
84
  styles: [{
79
85
  property: 'min-inline-size',
80
- value: 'anchor-size(width)'
86
+ value: 'anchor-size(self-inline)'
81
87
  }]
82
88
  });
83
89
  }
@@ -87,18 +93,20 @@ export function useWidthFromAnchor(_ref) {
87
93
  element: node,
88
94
  styles: [{
89
95
  property: 'min-inline-size',
90
- value: "".concat(_anchor.offsetWidth, "px")
96
+ value: "".concat(getFallbackAnchorInlineSize({
97
+ anchor: _anchor,
98
+ popover: node
99
+ }), "px")
91
100
  }]
92
101
  });
93
102
  }
94
103
 
95
- // No anchor to measure in the JS fallback, so there is no floor to apply.
104
+ // Nothing to measure, so no floor.
96
105
  return;
97
106
  }
98
107
 
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`.
108
+ // Makes a too-narrow span overflow and drive `position-try-fallbacks` instead of wrapping.
109
+ // `@atlaskit/popper` resets this exact property to `0` for its fit-to-viewport mode.
102
110
  return setStyle({
103
111
  element: node,
104
112
  styles: [{
@@ -1,4 +1,34 @@
1
- import { type TPlacement } from './resolve-placement';
1
+ import { type TCrossAxisShiftDirection, type TPlacement } from './resolve-placement';
2
+ /**
3
+ * JS fallback positioning math. Used only when the browser does not
4
+ * support CSS Anchor Positioning (less than 6% of users).
5
+ *
6
+ * Consumer-supplied `offset.gap` and `offset.crossAxisShift` are
7
+ * honored: the caller (`useAnchorPosition`) resolves CSS length strings
8
+ * (tokens, calc, var, etc) to pixels via a DOM probe before passing them
9
+ * in. The caller also keeps the popover hidden (`opacity: 0`) until the
10
+ * first measurement completes, so the popover never paints at the wrong
11
+ * position.
12
+ */
13
+ /**
14
+ * A viewport-relative position, in pixels. Physical rather than logical
15
+ * because it is written straight to `top` / `left`.
16
+ */
17
+ type TPosition = {
18
+ top: number;
19
+ left: number;
20
+ };
21
+ /**
22
+ * The cross-axis shift with its length already resolved to pixels.
23
+ *
24
+ * Mirrors `TCrossAxisShiftOffset`, whose `value` is an unresolved CSS length
25
+ * string. Only this module needs the pixel form, because only this module does
26
+ * the arithmetic itself.
27
+ */
28
+ type TResolvedCrossAxisShift = {
29
+ value: number;
30
+ direction: TCrossAxisShiftDirection;
31
+ };
2
32
  /**
3
33
  * Computes popover position from trigger rect and viewport, flipping to
4
34
  * the side with more space when needed. Re-runs on scroll/resize.
@@ -16,11 +46,6 @@ export declare function computeFallbackPosition({ triggerRect, popoverEl, placem
16
46
  height: number;
17
47
  };
18
48
  gap: number;
19
- crossAxisShift: {
20
- value: number;
21
- direction: 'forwards' | 'backwards';
22
- };
23
- }): {
24
- top: number;
25
- left: number;
26
- };
49
+ crossAxisShift: TResolvedCrossAxisShift;
50
+ }): TPosition;
51
+ export {};
@@ -1,14 +1,20 @@
1
+ /**
2
+ * Which way along the cross axis a shift moves the popover. `'forwards'` is
3
+ * always toward the cross-axis END and `'backwards'` toward the START, for
4
+ * every `align` value. See `notes/decisions/placement-offset.md`.
5
+ */
6
+ export type TCrossAxisShiftDirection = 'forwards' | 'backwards';
1
7
  /**
2
8
  * Cross-axis shift. `value` is a CSS length string (consumer numbers are
3
- * normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
4
- * `'backwards'` (toward start).
9
+ * normalized to `${n}px`).
5
10
  */
6
11
  export type TCrossAxisShiftOffset = {
7
12
  value: string;
8
- direction: 'forwards' | 'backwards';
13
+ direction: TCrossAxisShiftDirection;
9
14
  };
10
15
  export type TPlacementAxis = 'block' | 'inline';
11
16
  export type TPlacementEdge = 'start' | 'end';
17
+ export type TPlacementAlign = 'start' | 'center' | 'end';
12
18
  /**
13
19
  * Fully-resolved placement (internal). Use `resolvePlacement` for partial input.
14
20
  * `offset.gap` and `offset.crossAxisShift.value` are always CSS
@@ -17,13 +23,10 @@ export type TPlacementEdge = 'start' | 'end';
17
23
  export type TPlacement = {
18
24
  axis: TPlacementAxis;
19
25
  edge: TPlacementEdge;
20
- align: 'start' | 'center' | 'end';
26
+ align: TPlacementAlign;
21
27
  offset: {
22
28
  gap: string;
23
- crossAxisShift: {
24
- value: string;
25
- direction: 'forwards' | 'backwards';
26
- };
29
+ crossAxisShift: TCrossAxisShiftOffset;
27
30
  };
28
31
  };
29
32
  /**
@@ -36,14 +39,14 @@ export type TPlacement = {
36
39
  * - `crossAxisShift` - nudge along the trigger edge (perpendicular to the placement axis)
37
40
  */
38
41
  export type TPlacementOptions = {
39
- axis?: 'block' | 'inline';
40
- edge?: 'start' | 'end';
41
- align?: 'start' | 'center' | 'end';
42
+ axis?: TPlacementAxis;
43
+ edge?: TPlacementEdge;
44
+ align?: TPlacementAlign;
42
45
  offset?: {
43
46
  gap?: number | string;
44
47
  crossAxisShift?: {
45
48
  value?: number | string;
46
- direction?: 'forwards' | 'backwards';
49
+ direction?: TCrossAxisShiftDirection;
47
50
  };
48
51
  };
49
52
  };
@@ -1,11 +1,20 @@
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
+ export type TStyleDeclaration<TProperty extends string = string> = {
10
+ property: TProperty;
11
+ value: string;
12
+ };
1
13
  /**
2
14
  * Sets inline styles on an element and returns a cleanup function that
3
15
  * restores the prior inline values (so we do not stomp consumer styles).
4
16
  */
5
17
  export declare function setStyle({ element, styles, }: {
6
18
  element: HTMLElement;
7
- styles: Array<{
8
- property: string;
9
- value: string;
10
- }>;
19
+ styles: TStyleDeclaration[];
11
20
  }): () => void;
@@ -3,18 +3,13 @@ import type { TWidthFromAnchorMode } from '../popover/types';
3
3
  /**
4
4
  * Sets the width of a popover element relative to its anchor element.
5
5
  *
6
- * - `'match-anchor'` → `width: anchor-size(width)` - popover matches anchor width exactly.
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)
6
+ * - `'match-anchor'` → `inline-size: anchor-size(self-inline)`, exact
7
+ * - `'min-anchor'` → `min-inline-size: anchor-size(self-inline)`, floor only
8
+ * - `'none'` → `min-inline-size: max-content`, content floor (default)
13
9
  *
14
- * Uses CSS `anchor-size(width)` when the browser supports it, falling back to a
15
- * one-off measurement of `anchor.offsetWidth`.
16
- *
17
- * Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
10
+ * `self-inline` is the inline axis of the element USING the function (the popover), never the
11
+ * anchor, so it matches the axis `inline-size` constrains. See
12
+ * `notes/decisions/width-from-anchor-floors.md`.
18
13
  */
19
14
  export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, isOpen, }: {
20
15
  mode: TWidthFromAnchorMode;
@@ -166,12 +166,12 @@ export type TPopoverProps = TPopoverBaseProps & TAriaRoleRequired & ({
166
166
  *
167
167
  * - `'none'` (default): popover sizes to its content, ignoring the anchor width.
168
168
  * - `'match-anchor'`: popover matches the anchor element's width exactly
169
- * via CSS `anchor-size(width)`. Falls back to a one-off measurement
170
- * of `anchorRef.current.offsetWidth` when CSS Anchor Positioning
171
- * is not supported.
169
+ * via CSS `inline-size: anchor-size(self-inline)`. Falls back to a
170
+ * one-off measurement of `anchorRef.current.offsetWidth` when CSS
171
+ * Anchor Positioning is not supported.
172
172
  * - `'min-anchor'`: popover is at least as wide as the anchor, but
173
173
  * can grow wider if content requires it. Uses
174
- * `min-inline-size: anchor-size(width)`. Falls back to a one-off
174
+ * `min-inline-size: anchor-size(self-inline)`. Falls back to a one-off
175
175
  * measurement of the anchor's width.
176
176
  */
177
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.3",
3
+ "version": "4.2.4",
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",