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