@atlaskit/top-layer 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -1
  3. package/dialog/create-close-event/package.json +17 -0
  4. package/dist/cjs/animations/presets.js +10 -5
  5. package/dist/cjs/arrow/index.js +131 -0
  6. package/dist/cjs/arrow/types.js +5 -0
  7. package/dist/cjs/dialog/dialog-content.js +11 -6
  8. package/dist/cjs/entry-points/dialog/create-close-event.js +12 -0
  9. package/dist/cjs/entry-points/placement-map.js +6 -5
  10. package/dist/cjs/entry-points/popover/create-close-event.js +12 -0
  11. package/dist/cjs/focus/focus.js +71 -19
  12. package/dist/cjs/index.js +3 -4
  13. package/dist/cjs/internal/resolve-css-length-to-pixels.js +61 -10
  14. package/dist/cjs/internal/role-types.js +40 -9
  15. package/dist/cjs/internal/set-style.js +23 -25
  16. package/dist/cjs/internal/use-anchor-position.js +107 -68
  17. package/dist/cjs/internal/use-focus-wrap.js +13 -2
  18. package/dist/cjs/internal/use-initial-focus.js +31 -10
  19. package/dist/cjs/placement-map/index.js +9 -111
  20. package/dist/cjs/placement-map/legacy-placements.js +113 -0
  21. package/dist/cjs/popover/popover.js +14 -12
  22. package/dist/cjs/popup/popup-content.js +19 -13
  23. package/dist/cjs/popup/popup-surface.js +6 -0
  24. package/dist/cjs/popup/popup-trigger-function.js +1 -1
  25. package/dist/cjs/popup/popup-trigger.js +17 -37
  26. package/dist/cjs/popup/popup.js +21 -10
  27. package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +26 -10
  28. package/dist/es2019/animations/presets.js +20 -5
  29. package/dist/es2019/arrow/index.js +269 -0
  30. package/dist/es2019/arrow/types.js +1 -0
  31. package/dist/es2019/dialog/dialog-content.js +11 -6
  32. package/dist/es2019/entry-points/create-close-event.js +7 -0
  33. package/dist/es2019/entry-points/dialog/create-close-event.js +1 -0
  34. package/dist/es2019/entry-points/placement-map.js +2 -1
  35. package/dist/es2019/entry-points/popover/create-close-event.js +1 -0
  36. package/dist/es2019/focus/focus.js +56 -7
  37. package/dist/es2019/index.js +3 -4
  38. package/dist/es2019/internal/resolve-css-length-to-pixels.js +60 -7
  39. package/dist/es2019/internal/role-types.js +40 -9
  40. package/dist/es2019/internal/set-style.js +19 -22
  41. package/dist/es2019/internal/use-anchor-position.js +95 -54
  42. package/dist/es2019/internal/use-focus-wrap.js +14 -3
  43. package/dist/es2019/internal/use-initial-focus.js +32 -11
  44. package/dist/es2019/placement-map/index.js +8 -109
  45. package/dist/es2019/placement-map/legacy-placements.js +107 -0
  46. package/dist/es2019/popover/popover.js +14 -11
  47. package/dist/es2019/popup/popup-content.js +20 -14
  48. package/dist/es2019/popup/popup-surface.js +6 -0
  49. package/dist/es2019/popup/popup-trigger-function.js +1 -1
  50. package/dist/es2019/popup/popup-trigger.js +17 -37
  51. package/dist/es2019/popup/popup.js +20 -9
  52. package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +26 -10
  53. package/dist/esm/animations/presets.js +9 -4
  54. package/dist/esm/arrow/index.js +124 -0
  55. package/dist/esm/arrow/types.js +1 -0
  56. package/dist/esm/dialog/dialog-content.js +11 -6
  57. package/dist/esm/entry-points/create-close-event.js +7 -0
  58. package/dist/esm/entry-points/dialog/create-close-event.js +1 -0
  59. package/dist/esm/entry-points/placement-map.js +2 -1
  60. package/dist/esm/entry-points/popover/create-close-event.js +1 -0
  61. package/dist/esm/focus/focus.js +70 -19
  62. package/dist/esm/index.js +3 -4
  63. package/dist/esm/internal/resolve-css-length-to-pixels.js +61 -10
  64. package/dist/esm/internal/role-types.js +39 -9
  65. package/dist/esm/internal/set-style.js +23 -25
  66. package/dist/esm/internal/use-anchor-position.js +108 -64
  67. package/dist/esm/internal/use-focus-wrap.js +14 -3
  68. package/dist/esm/internal/use-initial-focus.js +32 -11
  69. package/dist/esm/placement-map/index.js +8 -109
  70. package/dist/esm/placement-map/legacy-placements.js +107 -0
  71. package/dist/esm/popover/popover.js +14 -12
  72. package/dist/esm/popup/popup-content.js +20 -14
  73. package/dist/esm/popup/popup-surface.js +6 -0
  74. package/dist/esm/popup/popup-trigger-function.js +1 -1
  75. package/dist/esm/popup/popup-trigger.js +17 -37
  76. package/dist/esm/popup/popup.js +21 -10
  77. package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +26 -10
  78. package/dist/types/arrow/index.d.ts +24 -0
  79. package/dist/types/arrow/types.d.ts +29 -0
  80. package/dist/types/dialog/dialog-content.d.ts +5 -1
  81. package/dist/types/dialog/types.d.ts +50 -24
  82. package/dist/types/entry-points/create-close-event.d.ts +7 -0
  83. package/dist/types/entry-points/dialog/create-close-event.d.ts +2 -0
  84. package/dist/types/entry-points/placement-map.d.ts +2 -1
  85. package/dist/types/entry-points/popover/create-close-event.d.ts +2 -0
  86. package/dist/types/focus/focus.d.ts +8 -0
  87. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +4 -6
  88. package/dist/types/internal/role-types.d.ts +25 -4
  89. package/dist/types/internal/set-style.d.ts +2 -19
  90. package/dist/types/internal/use-anchor-position.d.ts +2 -3
  91. package/dist/types/internal/use-initial-focus.d.ts +5 -4
  92. package/dist/types/placement-map/index.d.ts +8 -21
  93. package/dist/types/placement-map/legacy-placements.d.ts +21 -0
  94. package/dist/types/popover/popover.d.ts +1 -1
  95. package/dist/types/popover/types.d.ts +17 -17
  96. package/dist/types/popup/popup-surface.d.ts +6 -0
  97. package/dist/types/popup/popup-trigger-function.d.ts +10 -6
  98. package/dist/types/popup/popup-trigger.d.ts +8 -17
  99. package/dist/types/popup/popup.d.ts +2 -2
  100. package/dist/types/popup/types.d.ts +68 -22
  101. package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  102. package/dist/types-ts4.5/arrow/index.d.ts +24 -0
  103. package/dist/types-ts4.5/arrow/types.d.ts +29 -0
  104. package/dist/types-ts4.5/dialog/dialog-content.d.ts +5 -1
  105. package/dist/types-ts4.5/dialog/types.d.ts +50 -24
  106. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +7 -0
  107. package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +2 -0
  108. package/dist/types-ts4.5/entry-points/placement-map.d.ts +2 -1
  109. package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +2 -0
  110. package/dist/types-ts4.5/focus/focus.d.ts +8 -0
  111. package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +4 -6
  112. package/dist/types-ts4.5/internal/role-types.d.ts +25 -4
  113. package/dist/types-ts4.5/internal/set-style.d.ts +2 -19
  114. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +2 -3
  115. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +5 -4
  116. package/dist/types-ts4.5/placement-map/index.d.ts +8 -21
  117. package/dist/types-ts4.5/placement-map/legacy-placements.d.ts +39 -0
  118. package/dist/types-ts4.5/popover/popover.d.ts +1 -1
  119. package/dist/types-ts4.5/popover/types.d.ts +17 -17
  120. package/dist/types-ts4.5/popup/popup-surface.d.ts +6 -0
  121. package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +10 -6
  122. package/dist/types-ts4.5/popup/popup-trigger.d.ts +8 -17
  123. package/dist/types-ts4.5/popup/popup.d.ts +2 -2
  124. package/dist/types-ts4.5/popup/types.d.ts +68 -22
  125. package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  126. package/package.json +1 -1
  127. package/popover/create-close-event/package.json +17 -0
@@ -4,9 +4,19 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.roleToAriaHasPopup = roleToAriaHasPopup;
7
+ exports.shouldFocusIntoPopover = shouldFocusIntoPopover;
7
8
  /**
8
9
  * Valid values for the `aria-haspopup` attribute.
9
- * Derived from the HTML spec — maps popover roles to what the trigger announces.
10
+ *
11
+ * Derived from the HTML spec - maps popover roles to what the trigger
12
+ * announces. `undefined` means the attribute is omitted entirely (used for
13
+ * non-popup roles like `tooltip`/`status`/`alert`/`note`/`log` where
14
+ * `aria-haspopup` would be misleading).
15
+ *
16
+ * `true` is intentionally NOT in the union - the runtime only ever produces
17
+ * the explicit string forms, so widening the type would invite consumers to
18
+ * pass `true` and get an `aria-haspopup="true"` serialisation that the
19
+ * runtime no longer emits.
10
20
  */
11
21
 
12
22
  /**
@@ -28,21 +38,27 @@ exports.roleToAriaHasPopup = roleToAriaHasPopup;
28
38
  /**
29
39
  * ARIA role props where a role is always required.
30
40
  *
31
- * Used by `TPopupContentProps` — popup content must always declare
41
+ * Used by `TPopupContentProps`. Popup content must always declare
32
42
  * its role so assistive technology can identify the layer.
33
43
  */
34
44
 
35
45
  /**
36
46
  * ARIA role props where a role is optional.
37
47
  *
38
- * Used by `TPopoverProps` — the low-level primitive allows omitting
48
+ * Used by `TPopoverProps`. The low-level primitive allows omitting
39
49
  * the role for cases where it is set externally or not needed.
40
50
  */
41
51
 
42
52
  /**
43
53
  * Maps a popover content role to the correct `aria-haspopup` value
44
54
  * for its trigger element.
55
+ *
56
+ * Returns `undefined` for roles that are not popups in the ARIA sense
57
+ * (`tooltip`, `note`, `status`, `alert`, `log`). Triggers for those roles
58
+ * should not announce a popup at all - emitting `aria-haspopup="dialog"`
59
+ * on a tooltip trigger would mislead assistive technology.
45
60
  */
61
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
46
62
  function roleToAriaHasPopup(_ref) {
47
63
  var role = _ref.role;
48
64
  if (role === 'menu') {
@@ -57,10 +73,25 @@ function roleToAriaHasPopup(_ref) {
57
73
  if (role === 'grid') {
58
74
  return 'grid';
59
75
  }
60
- // Default: return 'dialog' for all unmapped roles (tooltip, alertdialog,
61
- // note, status, alert, log, etc). This is a kinder API — rather than
62
- // throwing on unknown roles, we fall back to the most common popup type.
63
- // New roles that need specific aria-haspopup values should be added
64
- // as explicit cases above.
65
- return 'dialog';
76
+ if (role === 'dialog' || role === 'alertdialog') {
77
+ return 'dialog';
78
+ }
79
+ // Non-popup roles (tooltip, note, status, alert, log) and the
80
+ // no-role case: omit `aria-haspopup` entirely. React drops the
81
+ // attribute when the value is `undefined`.
82
+ return undefined;
83
+ }
84
+ var rolesThatMoveFocus = new Set(['dialog', 'alertdialog', 'menu', 'listbox', 'tree', 'grid']);
85
+
86
+ /**
87
+ * Returns `true` when the given popover role moves focus into the popover
88
+ * on open, and `false` otherwise (including when `role` is `undefined`).
89
+ */
90
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
91
+ function shouldFocusIntoPopover(_ref2) {
92
+ var role = _ref2.role;
93
+ if (!role) {
94
+ return false;
95
+ }
96
+ return rolesThatMoveFocus.has(role);
66
97
  }
@@ -5,38 +5,36 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.setStyle = setStyle;
7
7
  /**
8
- * Sets inline styles on an element and returns a cleanup function
9
- * that removes only the styles that were applied.
10
- *
11
- * Uses `style.setProperty()` / `style.removeProperty()` so that
12
- * hyphenated CSS property names (e.g. `position-area`, `anchor-name`)
13
- * work correctly. These newer properties do not have camelCase
14
- * mappings in `CSSStyleDeclaration` yet.
15
- *
16
- * @example
17
- * ```ts
18
- * const cleanup = setStyle({ el: popover, styles: [
19
- * { property: 'position-anchor', value: '--my-anchor' },
20
- * { property: 'position-area', value: 'block-end' },
21
- * { property: 'margin', value: '0' },
22
- * ] });
23
- *
24
- * // Later: removes all three properties
25
- * cleanup();
26
- * ```
8
+ * Sets inline styles on an element and returns a cleanup function that
9
+ * restores the prior inline values (so we do not stomp consumer styles).
27
10
  */
28
11
  function setStyle(_ref) {
29
12
  var el = _ref.el,
30
13
  styles = _ref.styles;
31
- styles.forEach(function (_ref2) {
32
- var property = _ref2.property,
33
- value = _ref2.value;
14
+ // Snapshot the prior inline value (NOT the computed style - we only want
15
+ // to restore values that the consumer/our previous run inlined). An empty
16
+ // string means "no inline value", in which case cleanup uses removeProperty.
17
+ var priorValues = styles.map(function (_ref2) {
18
+ var property = _ref2.property;
19
+ return {
20
+ property: property,
21
+ value: el.style.getPropertyValue(property)
22
+ };
23
+ });
24
+ styles.forEach(function (_ref3) {
25
+ var property = _ref3.property,
26
+ value = _ref3.value;
34
27
  el.style.setProperty(property, value);
35
28
  });
36
29
  return function cleanup() {
37
- styles.forEach(function (_ref3) {
38
- var property = _ref3.property;
39
- el.style.removeProperty(property);
30
+ priorValues.forEach(function (_ref4) {
31
+ var property = _ref4.property,
32
+ value = _ref4.value;
33
+ if (value === '') {
34
+ el.style.removeProperty(property);
35
+ return;
36
+ }
37
+ el.style.setProperty(property, value);
40
38
  });
41
39
  };
42
40
  }
@@ -4,15 +4,10 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- Object.defineProperty(exports, "getPlacement", {
8
- enumerable: true,
9
- get: function get() {
10
- return _resolvePlacement.getPlacement;
11
- }
12
- });
13
7
  exports.placementToPositionArea = placementToPositionArea;
14
8
  exports.placementToTryFallbacks = placementToTryFallbacks;
15
9
  exports.useAnchorPosition = useAnchorPosition;
10
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
16
11
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
17
12
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
18
13
  var _react = require("react");
@@ -39,6 +34,30 @@ var _setStyle = require("./set-style");
39
34
  * is never painted at the wrong location.
40
35
  */
41
36
 
37
+ // `getPlacement` is intentionally NOT re-exported here - import it directly
38
+ // from `./resolve-placement` so search-and-jump lands at the source of truth.
39
+
40
+ /**
41
+ * Module-scope no-op `ResizeObserver` used in non-DOM jest environments where
42
+ * `ResizeObserver` is `undefined`. Hoisted out of the effect to avoid
43
+ * allocating a new class declaration on every run.
44
+ */
45
+ var NoopResizeObserver = /*#__PURE__*/function () {
46
+ function NoopResizeObserver() {
47
+ (0, _classCallCheck2.default)(this, NoopResizeObserver);
48
+ }
49
+ return (0, _createClass2.default)(NoopResizeObserver, [{
50
+ key: "observe",
51
+ value: function observe() {}
52
+ }, {
53
+ key: "unobserve",
54
+ value: function unobserve() {}
55
+ }, {
56
+ key: "disconnect",
57
+ value: function disconnect() {}
58
+ }]);
59
+ }();
60
+
42
61
  /**
43
62
  * Resolves placement options to CSS `position-area` syntax.
44
63
  *
@@ -100,7 +119,7 @@ function placementToPositionArea(_ref) {
100
119
  * top-right), flipping only the primary axis (`flip-inline`) moves the popup
101
120
  * to the left but keeps the same cross-axis span direction (e.g. upward),
102
121
  * which is still off-screen. The combined keyword `flip-inline flip-block`
103
- * mirrors the popup diagonally — it flips BOTH the position-area AND both
122
+ * mirrors the popup diagonally. It flips BOTH the position-area AND both
104
123
  * inline and block margins simultaneously, placing the popup at the
105
124
  * diagonally opposite corner where there is always space.
106
125
  *
@@ -119,19 +138,36 @@ function placementToTryFallbacks(_ref2) {
119
138
  align = _getPlacement2.align;
120
139
  var crossAxis = axis === 'block' ? 'inline' : 'block';
121
140
  var flippedEdge = edge === 'start' ? 'end' : 'start';
122
- if (align !== 'center') {
123
- var _sameEdge = "".concat(axis, "-").concat(edge);
124
- var _oppositeEdge = "".concat(axis, "-").concat(flippedEdge);
125
- var _flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
126
- var flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
127
- // Try same-edge shifts first; two flip entries cover single-edge then corner overflow.
128
- // Legacy Popper fallbacks for e.g. bottom-start: [bottom, bottom-end, top-start, top, top-end, auto].
129
- return ["".concat(_sameEdge, " span-").concat(crossAxis, "-").concat(align === 'start' ? 'end' : 'start'), _sameEdge, "".concat(_sameEdge, " span-").concat(crossAxis, "-").concat(align === 'start' ? 'start' : 'end'), _flipKeyword, "".concat(_flipKeyword, " ").concat(flipCrossKeyword), "".concat(_oppositeEdge, " span-").concat(crossAxis, "-").concat(align === 'start' ? 'end' : 'start'), _oppositeEdge, "".concat(_oppositeEdge, " span-").concat(crossAxis, "-").concat(align === 'start' ? 'start' : 'end')].join(', ');
130
- }
131
141
  var sameEdge = "".concat(axis, "-").concat(edge);
132
142
  var oppositeEdge = "".concat(axis, "-").concat(flippedEdge);
133
143
  var flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
134
- return ["".concat(sameEdge, " span-").concat(crossAxis, "-end"), "".concat(sameEdge, " span-").concat(crossAxis, "-start"), flipKeyword, "".concat(oppositeEdge, " span-").concat(crossAxis, "-end"), "".concat(oppositeEdge, " span-").concat(crossAxis, "-start")].join(', ');
144
+ var flipCrossKeyword = axis === 'block' ? 'flip-inline' : 'flip-block';
145
+
146
+ // Single algorithm: produce the shift list against any base alignment.
147
+ // For `align: 'start'` the "near" shift expands toward `end`; for
148
+ // `align: 'end'` it expands toward `start`; for `align: 'center'`
149
+ // either order is symmetric (we use end-then-start to match the
150
+ // historical fallback list).
151
+ function shifts(baseAlign, edgeKey) {
152
+ if (baseAlign === 'center') {
153
+ return ["".concat(edgeKey, " span-").concat(crossAxis, "-end"), "".concat(edgeKey, " span-").concat(crossAxis, "-start")];
154
+ }
155
+ var near = baseAlign === 'start' ? 'end' : 'start';
156
+ var far = baseAlign === 'start' ? 'start' : 'end';
157
+ return ["".concat(edgeKey, " span-").concat(crossAxis, "-").concat(near), edgeKey, "".concat(edgeKey, " span-").concat(crossAxis, "-").concat(far)];
158
+ }
159
+ if (align !== 'center') {
160
+ // Aligned (start/end): try same-edge shifts, then single-axis flip,
161
+ // then diagonal flip for corner overflow, then opposite-edge shifts.
162
+ // Legacy Popper fallback ordering for e.g. bottom-start:
163
+ // [bottom, bottom-end, top-start, top, top-end, auto].
164
+ return [].concat((0, _toConsumableArray2.default)(shifts(align, sameEdge)), [flipKeyword, "".concat(flipKeyword, " ").concat(flipCrossKeyword)], (0, _toConsumableArray2.default)(shifts(align, oppositeEdge))).join(', ');
165
+ }
166
+
167
+ // Centered: same-edge shifts, single-axis flip, opposite-edge shifts.
168
+ // Diagonal flip is unnecessary because centered placements expand
169
+ // equally in both cross-axis directions.
170
+ return [].concat((0, _toConsumableArray2.default)(shifts('center', sameEdge)), [flipKeyword], (0, _toConsumableArray2.default)(shifts('center', oppositeEdge))).join(', ');
135
171
  }
136
172
 
137
173
  /**
@@ -149,7 +185,7 @@ function isPopoverOpen(popover) {
149
185
 
150
186
  /**
151
187
  * Detects whether the browser supports CSS Anchor Positioning.
152
- * Uses `once()` for lazy evaluation — safe for SSR and avoids hydration mismatches.
188
+ * Uses `once()` for lazy evaluation: safe for SSR and avoids hydration mismatches.
153
189
  */
154
190
  var supportsAnchorPositioning = (0, _once.default)(function () {
155
191
  if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
@@ -229,34 +265,51 @@ function crossAxisShiftMargin(_ref4) {
229
265
  }
230
266
 
231
267
  /**
232
- * Returns a referentially-stable, fully-resolved `TPlacement`: the same
233
- * reference is preserved across renders as long as the resolved shape
234
- * does not change.
268
+ * Returns a referentially-stable, fully-resolved `TPlacement`. `useMemo` is
269
+ * keyed on the primitive fields of the input so a fresh inline `placement`
270
+ * object on every render does not produce a fresh resolved object. Both
271
+ * `getPlacement` and the equality check are skipped when the inputs are
272
+ * unchanged.
235
273
  */
236
274
  function useStablePlacement(placement) {
275
+ // Resolve to the fully-defaulted primitives BEFORE the memo so the deps
276
+ // array sees the same key for `{}`, `undefined` fields, and explicit
277
+ // defaults. Otherwise `axis: undefined` and `axis: 'block'` would be
278
+ // treated as different inputs and re-run the downstream effect.
237
279
  var resolved = (0, _resolvePlacement.getPlacement)({
238
280
  placement: placement
239
281
  });
240
- var stableRef = (0, _react.useRef)(resolved);
241
- if (!isResolvedPlacementEqual(stableRef.current, resolved)) {
242
- stableRef.current = resolved;
243
- }
244
- return stableRef.current;
245
- }
246
-
247
- /**
248
- * Shallow-equals two fully-resolved placements. Avoids `JSON.stringify` so
249
- * the comparison cost stays proportional to the number of fields.
250
- */
251
- function isResolvedPlacementEqual(a, b) {
252
- return a.axis === b.axis && a.edge === b.edge && a.align === b.align && a.offset.gap === b.offset.gap && a.offset.crossAxisShift.value === b.offset.crossAxisShift.value && a.offset.crossAxisShift.direction === b.offset.crossAxisShift.direction;
282
+ var axis = resolved.axis;
283
+ var edge = resolved.edge;
284
+ var align = resolved.align;
285
+ var gap = resolved.offset.gap;
286
+ var shiftValue = resolved.offset.crossAxisShift.value;
287
+ var shiftDirection = resolved.offset.crossAxisShift.direction;
288
+ // Rebuild the placement object inside the memo from the resolved primitive
289
+ // deps so the factory does not close over the (potentially fresh) outer
290
+ // `resolved` reference, and `react-hooks/exhaustive-deps` can verify the
291
+ // deps array.
292
+ return (0, _react.useMemo)(function () {
293
+ return {
294
+ axis: axis,
295
+ edge: edge,
296
+ align: align,
297
+ offset: {
298
+ gap: gap,
299
+ crossAxisShift: {
300
+ value: shiftValue,
301
+ direction: shiftDirection
302
+ }
303
+ }
304
+ };
305
+ }, [axis, edge, align, gap, shiftValue, shiftDirection]);
253
306
  }
254
307
 
255
308
  /**
256
309
  * Hook that positions an element relative to an anchor element using
257
310
  * CSS Anchor Positioning (with a JS fallback for older browsers).
258
311
  *
259
- * This hook is the positioning primitive — it has no knowledge of popovers,
312
+ * This hook is the positioning primitive. It has no knowledge of popovers,
260
313
  * visibility, or animation. Compose it with `Popover` for anchor-positioned
261
314
  * top-layer content.
262
315
  *
@@ -301,6 +354,9 @@ function useAnchorPosition(_ref5) {
301
354
  placement: stablePlacement,
302
355
  offset: gapCssValue
303
356
  });
357
+ // Hoist crossAxisShift to a single computation; reused below to
358
+ // populate the active `--ds-cross-axis-shift-margin-*` custom
359
+ // property for the named arrow @position-try rules.
304
360
  var crossAxisShift = crossAxisShiftMargin({
305
361
  placement: stablePlacement,
306
362
  crossAxisShiftCssValue: crossAxisShiftCssValue,
@@ -360,11 +416,8 @@ function useAnchorPosition(_ref5) {
360
416
  property: '--ds-cross-axis-shift-margin-block-end',
361
417
  value: '0px'
362
418
  });
363
- var crossAxisShiftActive = crossAxisShiftMargin({
364
- placement: stablePlacement,
365
- crossAxisShiftCssValue: crossAxisShiftCssValue,
366
- direction: stablePlacement.offset.crossAxisShift.direction
367
- });
419
+ // `crossAxisShift` IS the active margin - reuse instead of recomputing.
420
+ var crossAxisShiftActive = crossAxisShift;
368
421
  // Map margin property to its corresponding custom property name.
369
422
  var customPropertyByMarginProperty = {
370
423
  'margin-inline-start': '--ds-cross-axis-shift-margin-start',
@@ -389,7 +442,7 @@ function useAnchorPosition(_ref5) {
389
442
  * - Sometimes the `useAnchorPosition` hook and `Popover`
390
443
  * are not in the same component.
391
444
  * - There can be times when, even with reference counting, we
392
- * could remove the anchor-name while it's still being used
445
+ * could remove the anchor-name while it is still being used
393
446
  * by something else - especially with async react updates
394
447
  * which can cause tearing
395
448
  *
@@ -420,7 +473,6 @@ function useAnchorPosition(_ref5) {
420
473
  }]
421
474
  });
422
475
  function update() {
423
- var _popover$parentElemen;
424
476
  if (!trigger || !popover) {
425
477
  return;
426
478
  }
@@ -431,19 +483,19 @@ function useAnchorPosition(_ref5) {
431
483
  };
432
484
 
433
485
  // Resolve consumer-supplied CSS length strings (tokens, calc, var,
434
- // px, rem, etc) to pixels. The probe is mounted next to the popover
435
- // so it inherits the same custom-property scope and font size.
436
- // `popover.parentElement` is guaranteed to exist for any rendered
437
- // popover (the popover itself is appended somewhere in the tree),
438
- // but fall back to the popover element if not for safety.
439
- var probeContainer = (_popover$parentElemen = popover.parentElement) !== null && _popover$parentElemen !== void 0 ? _popover$parentElemen : popover;
486
+ // px, rem, etc) to pixels. The probe is mounted INSIDE the popover
487
+ // itself so the resolved length matches the scope a token would
488
+ // resolve against from inside the popover content. (The popover
489
+ // lives in the top layer, not the trigger's DOM tree, so probing
490
+ // at `popover.parentElement` would resolve in a different scope
491
+ // than the consumer's token authoring expects.)
440
492
  var gapPx = (0, _resolveCssLengthToPixels.resolveCssLengthToPixels)({
441
493
  value: gapCssValue,
442
- container: probeContainer
494
+ container: popover
443
495
  });
444
496
  var crossAxisShiftPx = (0, _resolveCssLengthToPixels.resolveCssLengthToPixels)({
445
497
  value: crossAxisShiftCssValue,
446
- container: probeContainer
498
+ container: popover
447
499
  });
448
500
  var _computeFallbackPosit = (0, _anchorPositioningFallback.computeFallbackPosition)({
449
501
  triggerRect: triggerRect,
@@ -476,25 +528,12 @@ function useAnchorPosition(_ref5) {
476
528
  // scroll/resize is handled by the window listeners below.
477
529
  // `ResizeObserver` is missing in some non-DOM jest environments
478
530
  // (e.g. post-office's `node` environment). Fall back to a no-op
479
- // observer there — the scroll/resize listeners below still keep
531
+ // observer there. The scroll/resize listeners below still keep
480
532
  // the popover positioned in the rare case the consumer also
481
533
  // polyfilled `showPopover` but not `ResizeObserver`. Real
482
- // browsers always have it.
483
- var NoopResizeObserver = /*#__PURE__*/function () {
484
- function NoopResizeObserver() {
485
- (0, _classCallCheck2.default)(this, NoopResizeObserver);
486
- }
487
- return (0, _createClass2.default)(NoopResizeObserver, [{
488
- key: "observe",
489
- value: function observe() {}
490
- }, {
491
- key: "unobserve",
492
- value: function unobserve() {}
493
- }, {
494
- key: "disconnect",
495
- value: function disconnect() {}
496
- }]);
497
- }();
534
+ // browsers always have it. The `NoopResizeObserver` class is
535
+ // hoisted to module scope (see top of file) so we do not allocate
536
+ // a new constructor on every effect run.
498
537
  var ResizeObserverImpl = typeof ResizeObserver !== 'undefined' ? ResizeObserver : NoopResizeObserver;
499
538
  var resizeObserver = new ResizeObserverImpl(function () {
500
539
  if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
@@ -517,7 +556,7 @@ function useAnchorPosition(_ref5) {
517
556
  // the UA-default position, then re-observe for a fresh
518
557
  // measurement. We use `opacity: 0` rather than
519
558
  // `visibility: hidden` because Firefox skips visibility-hidden
520
- // elements during `<dialog>` initial-focus traversal — see
559
+ // elements during `<dialog>` initial-focus traversal. See
521
560
  // `form-in-popup.spec.tsx` on `desktop-firefox`.
522
561
  (0, _bindEventListener.bind)(popover, {
523
562
  type: 'toggle',
@@ -19,7 +19,7 @@ var _focus = require("../focus/focus");
19
19
  * not Tab. Tab/Shift+Tab should move focus OUT of a menu entirely.
20
20
  *
21
21
  * Menu keyboard behavior (arrow keys, Home/End, type-ahead, Enter/Space)
22
- * is context-dependent — it varies by orientation, nesting depth, item
22
+ * is context-dependent. It varies by orientation, nesting depth, item
23
23
  * type, and control pattern (menu button vs menubar). This makes it
24
24
  * impossible to implement generically here. The consumer component
25
25
  * (e.g. `dropdown-menu`) owns all menu keyboard navigation.
@@ -59,6 +59,17 @@ function useFocusWrap(_ref) {
59
59
  if (event.key !== 'Tab') {
60
60
  return;
61
61
  }
62
+
63
+ // If focus is already in a nested layer, then
64
+ // we continue to let that nested layer own focus
65
+ if ((0, _focus.isNestedLayerFocused)({
66
+ container: element
67
+ })) {
68
+ return;
69
+ }
70
+
71
+ // Prevent the browser giving focus to where it wants to
72
+ // (we will be handling it, including with wrapping)
62
73
  event.preventDefault();
63
74
  var direction = event.shiftKey ? 'backwards' : 'forwards';
64
75
 
@@ -74,7 +85,7 @@ function useFocusWrap(_ref) {
74
85
  }
75
86
 
76
87
  // Nothing is currently focused inside the container (or the
77
- // focused element isn't in the focusable list). Fall back to
88
+ // focused element is not in the focusable list). Fall back to
78
89
  // the first or last focusable element depending on direction.
79
90
  var fallback = direction === 'forwards' ? (0, _focus.getFirstFocusable)({
80
91
  container: element
@@ -24,8 +24,8 @@ var _focus = require("../focus/focus");
24
24
  * React 19+ reflects the JSX prop as the HTML attribute automatically.
25
25
  */
26
26
  function getInitialFocusTarget(_ref) {
27
- var container = _ref.container;
28
- var role = container.getAttribute('role');
27
+ var container = _ref.container,
28
+ role = _ref.role;
29
29
  if (role === 'dialog' || role === 'alertdialog') {
30
30
  // Prefer element with the native HTML autofocus attribute.
31
31
  var autofocusEl = container.querySelector('[autofocus]');
@@ -66,14 +66,15 @@ function getInitialFocusTarget(_ref) {
66
66
  /**
67
67
  * Moves focus into a popover element when it opens, based on its ARIA role.
68
68
  *
69
- * This implements WCAG 2.4.3 (Focus Order) initial focus placement:
69
+ * Follows the WAI-ARIA APG initial-focus patterns for each role:
70
70
  * - `dialog` / `alertdialog` → focus first focusable (or `[autofocus]`)
71
71
  * - `menu` → focus first menu item
72
72
  * - `listbox` → focus selected or first option
73
- * - `tooltip` / no role → no focus movement
73
+ * - `tooltip` / no role → no focus movement (focus must remain on trigger;
74
+ * moving focus into the tooltip would dismiss it)
74
75
  *
75
- * The role is read from the element's `role` attribute (not passed as a prop),
76
- * following the same pattern as `useFocusWrap`.
76
+ * Role is taken from the typed `role` prop - this is the consumer's contract
77
+ * and is not subject to drift from a directly-mutated DOM attribute.
77
78
  *
78
79
  * Always call this hook unconditionally. Focus is only moved when `isOpen`
79
80
  * transitions to `true` and the role requires it.
@@ -82,6 +83,12 @@ function useInitialFocus(_ref2) {
82
83
  var elementRef = _ref2.elementRef,
83
84
  isOpen = _ref2.isOpen,
84
85
  role = _ref2.role;
86
+ // Track the most recently scheduled RAF so a rapid `isOpen → false → true`
87
+ // transition can cancel the previous frame before its closure runs against
88
+ // a stale popover element. Without this, the RAF queued during the first
89
+ // open could fire after the second open had begun and focus the wrong
90
+ // instance.
91
+ var pendingRafRef = (0, _react.useRef)(null);
85
92
  (0, _react.useEffect)(function () {
86
93
  if (!isOpen) {
87
94
  return;
@@ -91,6 +98,11 @@ function useInitialFocus(_ref2) {
91
98
  return;
92
99
  }
93
100
 
101
+ // Cancel any RAF queued by the previous open cycle.
102
+ if (pendingRafRef.current !== null) {
103
+ cancelAnimationFrame(pendingRafRef.current);
104
+ }
105
+
94
106
  // Use requestAnimationFrame to ensure the popover is fully rendered
95
107
  // and visible in the top layer before attempting to move focus.
96
108
  // showPopover() is called in useLayoutEffect (which runs before this
@@ -98,15 +110,24 @@ function useInitialFocus(_ref2) {
98
110
  // element is guaranteed to be in the top layer.
99
111
  var rafId = requestAnimationFrame(function () {
100
112
  var _getInitialFocusTarge;
113
+ pendingRafRef.current = null;
114
+ // Verify the element we captured is still the live popover. In
115
+ // rapid-toggle scenarios the ref may already point to a different
116
+ // instance - skip rather than focus the wrong popover.
117
+ if (elementRef.current !== el) {
118
+ return;
119
+ }
101
120
  (_getInitialFocusTarge = getInitialFocusTarget({
102
- container: el
121
+ container: el,
122
+ role: role
103
123
  })) === null || _getInitialFocusTarge === void 0 || _getInitialFocusTarge.focus();
104
124
  });
125
+ pendingRafRef.current = rafId;
105
126
  return function () {
106
127
  cancelAnimationFrame(rafId);
128
+ if (pendingRafRef.current === rafId) {
129
+ pendingRafRef.current = null;
130
+ }
107
131
  };
108
- // We intentionally depend on `role` even though `getInitialFocusTarget`
109
- // reads from the DOM attribute. This ensures the effect re-runs if
110
- // the role prop changes while the popover is open.
111
132
  }, [elementRef, isOpen, role]);
112
133
  }