@atlaskit/top-layer 0.8.0 → 0.10.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 (130) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +5 -1
  3. package/dialog/create-close-event/package.json +17 -0
  4. package/dist/cjs/animations/presets.js +11 -4
  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 +23 -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 +73 -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 +109 -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 -110
  20. package/dist/cjs/placement-map/legacy-placements.js +113 -0
  21. package/dist/cjs/popover/popover.js +44 -11
  22. package/dist/cjs/popup/popup-content.js +19 -13
  23. package/dist/cjs/popup/popup-context.js +2 -0
  24. package/dist/cjs/popup/popup-surface.js +6 -0
  25. package/dist/cjs/popup/popup-trigger-function.js +1 -1
  26. package/dist/cjs/popup/popup-trigger.js +17 -37
  27. package/dist/cjs/popup/popup.js +21 -10
  28. package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +27 -10
  29. package/dist/es2019/animations/presets.js +10 -3
  30. package/dist/es2019/arrow/index.js +269 -0
  31. package/dist/es2019/arrow/types.js +1 -0
  32. package/dist/es2019/dialog/dialog-content.js +23 -6
  33. package/dist/es2019/entry-points/create-close-event.js +7 -0
  34. package/dist/es2019/entry-points/dialog/create-close-event.js +1 -0
  35. package/dist/es2019/entry-points/placement-map.js +2 -1
  36. package/dist/es2019/entry-points/popover/create-close-event.js +1 -0
  37. package/dist/es2019/focus/focus.js +58 -7
  38. package/dist/es2019/index.js +3 -4
  39. package/dist/es2019/internal/resolve-css-length-to-pixels.js +60 -7
  40. package/dist/es2019/internal/role-types.js +40 -9
  41. package/dist/es2019/internal/set-style.js +19 -22
  42. package/dist/es2019/internal/use-anchor-position.js +97 -54
  43. package/dist/es2019/internal/use-focus-wrap.js +14 -3
  44. package/dist/es2019/internal/use-initial-focus.js +32 -11
  45. package/dist/es2019/placement-map/index.js +8 -108
  46. package/dist/es2019/placement-map/legacy-placements.js +107 -0
  47. package/dist/es2019/popover/popover.js +44 -10
  48. package/dist/es2019/popup/popup-content.js +20 -14
  49. package/dist/es2019/popup/popup-context.js +2 -0
  50. package/dist/es2019/popup/popup-surface.js +6 -0
  51. package/dist/es2019/popup/popup-trigger-function.js +1 -1
  52. package/dist/es2019/popup/popup-trigger.js +17 -37
  53. package/dist/es2019/popup/popup.js +20 -9
  54. package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +27 -10
  55. package/dist/esm/animations/presets.js +10 -3
  56. package/dist/esm/arrow/index.js +124 -0
  57. package/dist/esm/arrow/types.js +1 -0
  58. package/dist/esm/dialog/dialog-content.js +23 -6
  59. package/dist/esm/entry-points/create-close-event.js +7 -0
  60. package/dist/esm/entry-points/dialog/create-close-event.js +1 -0
  61. package/dist/esm/entry-points/placement-map.js +2 -1
  62. package/dist/esm/entry-points/popover/create-close-event.js +1 -0
  63. package/dist/esm/focus/focus.js +72 -19
  64. package/dist/esm/index.js +3 -4
  65. package/dist/esm/internal/resolve-css-length-to-pixels.js +61 -10
  66. package/dist/esm/internal/role-types.js +39 -9
  67. package/dist/esm/internal/set-style.js +23 -25
  68. package/dist/esm/internal/use-anchor-position.js +110 -64
  69. package/dist/esm/internal/use-focus-wrap.js +14 -3
  70. package/dist/esm/internal/use-initial-focus.js +32 -11
  71. package/dist/esm/placement-map/index.js +8 -108
  72. package/dist/esm/placement-map/legacy-placements.js +107 -0
  73. package/dist/esm/popover/popover.js +44 -11
  74. package/dist/esm/popup/popup-content.js +20 -14
  75. package/dist/esm/popup/popup-context.js +2 -0
  76. package/dist/esm/popup/popup-surface.js +6 -0
  77. package/dist/esm/popup/popup-trigger-function.js +1 -1
  78. package/dist/esm/popup/popup-trigger.js +17 -37
  79. package/dist/esm/popup/popup.js +21 -10
  80. package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +27 -10
  81. package/dist/types/arrow/index.d.ts +24 -0
  82. package/dist/types/arrow/types.d.ts +29 -0
  83. package/dist/types/dialog/dialog-content.d.ts +5 -1
  84. package/dist/types/dialog/types.d.ts +50 -24
  85. package/dist/types/entry-points/create-close-event.d.ts +7 -0
  86. package/dist/types/entry-points/dialog/create-close-event.d.ts +2 -0
  87. package/dist/types/entry-points/placement-map.d.ts +2 -1
  88. package/dist/types/entry-points/popover/create-close-event.d.ts +2 -0
  89. package/dist/types/focus/focus.d.ts +8 -0
  90. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +4 -6
  91. package/dist/types/internal/role-types.d.ts +25 -4
  92. package/dist/types/internal/set-style.d.ts +2 -19
  93. package/dist/types/internal/use-anchor-position.d.ts +2 -3
  94. package/dist/types/internal/use-initial-focus.d.ts +5 -4
  95. package/dist/types/placement-map/index.d.ts +8 -21
  96. package/dist/types/placement-map/legacy-placements.d.ts +21 -0
  97. package/dist/types/popover/popover.d.ts +1 -1
  98. package/dist/types/popover/types.d.ts +20 -19
  99. package/dist/types/popup/popup-surface.d.ts +6 -0
  100. package/dist/types/popup/popup-trigger-function.d.ts +10 -6
  101. package/dist/types/popup/popup-trigger.d.ts +8 -17
  102. package/dist/types/popup/popup.d.ts +2 -2
  103. package/dist/types/popup/types.d.ts +68 -22
  104. package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  105. package/dist/types-ts4.5/arrow/index.d.ts +24 -0
  106. package/dist/types-ts4.5/arrow/types.d.ts +29 -0
  107. package/dist/types-ts4.5/dialog/dialog-content.d.ts +5 -1
  108. package/dist/types-ts4.5/dialog/types.d.ts +50 -24
  109. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +7 -0
  110. package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +2 -0
  111. package/dist/types-ts4.5/entry-points/placement-map.d.ts +2 -1
  112. package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +2 -0
  113. package/dist/types-ts4.5/focus/focus.d.ts +8 -0
  114. package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +4 -6
  115. package/dist/types-ts4.5/internal/role-types.d.ts +25 -4
  116. package/dist/types-ts4.5/internal/set-style.d.ts +2 -19
  117. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +2 -3
  118. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +5 -4
  119. package/dist/types-ts4.5/placement-map/index.d.ts +8 -21
  120. package/dist/types-ts4.5/placement-map/legacy-placements.d.ts +39 -0
  121. package/dist/types-ts4.5/popover/popover.d.ts +1 -1
  122. package/dist/types-ts4.5/popover/types.d.ts +20 -19
  123. package/dist/types-ts4.5/popup/popup-surface.d.ts +6 -0
  124. package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +10 -6
  125. package/dist/types-ts4.5/popup/popup-trigger.d.ts +8 -17
  126. package/dist/types-ts4.5/popup/popup.d.ts +2 -2
  127. package/dist/types-ts4.5/popup/types.d.ts +68 -22
  128. package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  129. package/package.json +3 -1
  130. package/popover/create-close-event/package.json +17 -0
@@ -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.
@@ -55,6 +55,17 @@ export function useFocusWrap({
55
55
  if (event.key !== 'Tab') {
56
56
  return;
57
57
  }
58
+
59
+ // If focus is already in a nested layer, then
60
+ // we continue to let that nested layer own focus
61
+ if (isNestedLayerFocused({
62
+ container: element
63
+ })) {
64
+ return;
65
+ }
66
+
67
+ // Prevent the browser giving focus to where it wants to
68
+ // (we will be handling it, including with wrapping)
58
69
  event.preventDefault();
59
70
  const direction = event.shiftKey ? 'backwards' : 'forwards';
60
71
 
@@ -70,7 +81,7 @@ export function useFocusWrap({
70
81
  }
71
82
 
72
83
  // Nothing is currently focused inside the container (or the
73
- // focused element isn't in the focusable list). Fall back to
84
+ // focused element is not in the focusable list). Fall back to
74
85
  // the first or last focusable element depending on direction.
75
86
  const fallback = direction === 'forwards' ? getFirstFocusable({
76
87
  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,9 +19,9 @@ import { getFirstFocusable } from '../focus/focus';
19
19
  * React 19+ reflects the JSX prop as the HTML attribute automatically.
20
20
  */
21
21
  function getInitialFocusTarget({
22
- container
22
+ container,
23
+ role
23
24
  }) {
24
- const role = container.getAttribute('role');
25
25
  if (role === 'dialog' || role === 'alertdialog') {
26
26
  // Prefer element with the native HTML autofocus attribute.
27
27
  const autofocusEl = container.querySelector('[autofocus]');
@@ -62,14 +62,15 @@ function getInitialFocusTarget({
62
62
  /**
63
63
  * Moves focus into a popover element when it opens, based on its ARIA role.
64
64
  *
65
- * This implements WCAG 2.4.3 (Focus Order) initial focus placement:
65
+ * Follows the WAI-ARIA APG initial-focus patterns for each role:
66
66
  * - `dialog` / `alertdialog` → focus first focusable (or `[autofocus]`)
67
67
  * - `menu` → focus first menu item
68
68
  * - `listbox` → focus selected or first option
69
- * - `tooltip` / no role → no focus movement
69
+ * - `tooltip` / no role → no focus movement (focus must remain on trigger;
70
+ * moving focus into the tooltip would dismiss it)
70
71
  *
71
- * The role is read from the element's `role` attribute (not passed as a prop),
72
- * following the same pattern as `useFocusWrap`.
72
+ * Role is taken from the typed `role` prop - this is the consumer's contract
73
+ * and is not subject to drift from a directly-mutated DOM attribute.
73
74
  *
74
75
  * Always call this hook unconditionally. Focus is only moved when `isOpen`
75
76
  * transitions to `true` and the role requires it.
@@ -79,6 +80,12 @@ export function useInitialFocus({
79
80
  isOpen,
80
81
  role
81
82
  }) {
83
+ // Track the most recently scheduled RAF so a rapid `isOpen → false → true`
84
+ // transition can cancel the previous frame before its closure runs against
85
+ // a stale popover element. Without this, the RAF queued during the first
86
+ // open could fire after the second open had begun and focus the wrong
87
+ // instance.
88
+ const pendingRafRef = useRef(null);
82
89
  useEffect(() => {
83
90
  if (!isOpen) {
84
91
  return;
@@ -88,6 +95,11 @@ export function useInitialFocus({
88
95
  return;
89
96
  }
90
97
 
98
+ // Cancel any RAF queued by the previous open cycle.
99
+ if (pendingRafRef.current !== null) {
100
+ cancelAnimationFrame(pendingRafRef.current);
101
+ }
102
+
91
103
  // Use requestAnimationFrame to ensure the popover is fully rendered
92
104
  // and visible in the top layer before attempting to move focus.
93
105
  // showPopover() is called in useLayoutEffect (which runs before this
@@ -95,15 +107,24 @@ export function useInitialFocus({
95
107
  // element is guaranteed to be in the top layer.
96
108
  const rafId = requestAnimationFrame(() => {
97
109
  var _getInitialFocusTarge;
110
+ pendingRafRef.current = null;
111
+ // Verify the element we captured is still the live popover. In
112
+ // rapid-toggle scenarios the ref may already point to a different
113
+ // instance - skip rather than focus the wrong popover.
114
+ if (elementRef.current !== el) {
115
+ return;
116
+ }
98
117
  (_getInitialFocusTarge = getInitialFocusTarget({
99
- container: el
118
+ container: el,
119
+ role
100
120
  })) === null || _getInitialFocusTarge === void 0 ? void 0 : _getInitialFocusTarge.focus();
101
121
  });
122
+ pendingRafRef.current = rafId;
102
123
  return () => {
103
124
  cancelAnimationFrame(rafId);
125
+ if (pendingRafRef.current === rafId) {
126
+ pendingRafRef.current = null;
127
+ }
104
128
  };
105
- // We intentionally depend on `role` even though `getInitialFocusTarget`
106
- // reads from the DOM attribute. This ensures the effect re-runs if
107
- // the role prop changes while the popover is open.
108
129
  }, [elementRef, isOpen, role]);
109
130
  }
@@ -1,7 +1,4 @@
1
- /**
2
- * Legacy Popper.js placement values (used by `@atlaskit/popper`, `@atlaskit/popup`,
3
- * `@atlaskit/tooltip`, etc.).
4
- */
1
+ import { placementMapping } from './legacy-placements';
5
2
 
6
3
  /**
7
4
  * Maps legacy Popper.js placement values to the new object-based
@@ -13,9 +10,11 @@
13
10
  * | ------------------- | --------------------------------------------------- |
14
11
  * | `top` | `{ axis: 'block', edge: 'start', align: 'center' }` |
15
12
  * | `top-start` | `{ axis: 'block', edge: 'start', align: 'start' }` |
13
+ * | `top-center` | `{ axis: 'block', edge: 'start', align: 'center' }` |
16
14
  * | `top-end` | `{ axis: 'block', edge: 'start', align: 'end' }` |
17
15
  * | `bottom` | `{ axis: 'block', edge: 'end', align: 'center' }` |
18
16
  * | `bottom-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
17
+ * | `bottom-center` | `{ axis: 'block', edge: 'end', align: 'center' }` |
19
18
  * | `bottom-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
20
19
  * | `right` | `{ axis: 'inline', edge: 'end', align: 'center' }` |
21
20
  * | `right-start` | `{ axis: 'inline', edge: 'end', align: 'start' }` |
@@ -27,7 +26,10 @@
27
26
  * | `auto-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
28
27
  * | `auto-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
29
28
  *
30
- * The `position-try-fallbacks` CSS property handles flipping automatically.
29
+ * `auto*` is mapped to `block-end` (a reduction in expressiveness vs Popper's
30
+ * runtime auto-flip). CSS `position-try-fallbacks` provides flipping at the
31
+ * edge - but the initial placement is fixed. If migrating away from Popper's
32
+ * dynamic auto, prefer an explicit placement.
31
33
  *
32
34
  * @example
33
35
  * ```ts
@@ -39,103 +41,6 @@
39
41
  * // newPlacement === { axis: 'block', edge: 'end', align: 'start' }
40
42
  * ```
41
43
  */
42
- const placementMapping = {
43
- top: {
44
- axis: 'block',
45
- edge: 'start',
46
- align: 'center'
47
- },
48
- 'top-start': {
49
- axis: 'block',
50
- edge: 'start',
51
- align: 'start'
52
- },
53
- 'top-center': {
54
- axis: 'block',
55
- edge: 'start',
56
- align: 'center'
57
- },
58
- 'top-end': {
59
- axis: 'block',
60
- edge: 'start',
61
- align: 'end'
62
- },
63
- bottom: {
64
- axis: 'block',
65
- edge: 'end',
66
- align: 'center'
67
- },
68
- 'bottom-start': {
69
- axis: 'block',
70
- edge: 'end',
71
- align: 'start'
72
- },
73
- 'bottom-center': {
74
- axis: 'block',
75
- edge: 'end',
76
- align: 'center'
77
- },
78
- 'bottom-end': {
79
- axis: 'block',
80
- edge: 'end',
81
- align: 'end'
82
- },
83
- right: {
84
- axis: 'inline',
85
- edge: 'end',
86
- align: 'center'
87
- },
88
- 'right-start': {
89
- axis: 'inline',
90
- edge: 'end',
91
- align: 'start'
92
- },
93
- 'right-end': {
94
- axis: 'inline',
95
- edge: 'end',
96
- align: 'end'
97
- },
98
- left: {
99
- axis: 'inline',
100
- edge: 'start',
101
- align: 'center'
102
- },
103
- 'left-start': {
104
- axis: 'inline',
105
- edge: 'start',
106
- align: 'start'
107
- },
108
- 'left-end': {
109
- axis: 'inline',
110
- edge: 'start',
111
- align: 'end'
112
- },
113
- auto: {
114
- axis: 'block',
115
- edge: 'end',
116
- align: 'center'
117
- },
118
- 'auto-start': {
119
- axis: 'block',
120
- edge: 'end',
121
- align: 'start'
122
- },
123
- 'auto-end': {
124
- axis: 'block',
125
- edge: 'end',
126
- align: 'end'
127
- }
128
- };
129
-
130
- /**
131
- * Convert a legacy Popper.js placement to the new `position-area`-based Placement.
132
- * Optional `offset` is the legacy Popper `[along, away]` tuple (also known as
133
- * `[skidding, distance]`); negative `along` becomes `direction: 'backwards'`.
134
- *
135
- * The tuple keeps the historical popper names so call sites that already pass
136
- * `[along, away]` migrate without thinking; the new internal field names
137
- * (`gap` / `crossAxisShift`) are produced inside this adapter.
138
- */
139
44
  export function fromLegacyPlacement({
140
45
  legacy,
141
46
  offset
@@ -156,9 +61,4 @@ export function fromLegacyPlacement({
156
61
  }
157
62
  }
158
63
  };
159
- }
160
-
161
- /**
162
- * The full mapping record, exposed for consumers that need to iterate or introspect.
163
- */
164
- export { placementMapping };
64
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Legacy Popper.js placement values (used by `@atlaskit/popper`,
3
+ * `@atlaskit/popup`, `@atlaskit/tooltip`, etc.). Re-exported from
4
+ * `@atlaskit/top-layer/placement-map` for migration consumers that need
5
+ * to enumerate every placement (e.g. for a settings dropdown). New code
6
+ * should use the object-based `TPlacementOptions` directly.
7
+ */
8
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
9
+ export const LEGACY_PLACEMENTS = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-center', 'top-end', 'bottom', 'bottom-start', 'bottom-center', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
10
+ /**
11
+ * The full mapping record. Internal - use `fromLegacyPlacement` from the
12
+ * public entry-point so the offset-tuple translation is applied consistently.
13
+ *
14
+ * Note: `auto`, `auto-start`, `auto-end` map to `block-end` placements. This
15
+ * is a reduction in expressiveness vs Popper.js (which picked the best
16
+ * placement at runtime). CSS `position-try-fallbacks` flips at the edge but
17
+ * the initial placement is fixed. Migration consumers who relied on Popper's
18
+ * dynamic auto-flip should pick an explicit placement instead.
19
+ */
20
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
21
+ export const placementMapping = {
22
+ top: {
23
+ axis: 'block',
24
+ edge: 'start',
25
+ align: 'center'
26
+ },
27
+ 'top-start': {
28
+ axis: 'block',
29
+ edge: 'start',
30
+ align: 'start'
31
+ },
32
+ 'top-center': {
33
+ axis: 'block',
34
+ edge: 'start',
35
+ align: 'center'
36
+ },
37
+ 'top-end': {
38
+ axis: 'block',
39
+ edge: 'start',
40
+ align: 'end'
41
+ },
42
+ bottom: {
43
+ axis: 'block',
44
+ edge: 'end',
45
+ align: 'center'
46
+ },
47
+ 'bottom-start': {
48
+ axis: 'block',
49
+ edge: 'end',
50
+ align: 'start'
51
+ },
52
+ 'bottom-center': {
53
+ axis: 'block',
54
+ edge: 'end',
55
+ align: 'center'
56
+ },
57
+ 'bottom-end': {
58
+ axis: 'block',
59
+ edge: 'end',
60
+ align: 'end'
61
+ },
62
+ right: {
63
+ axis: 'inline',
64
+ edge: 'end',
65
+ align: 'center'
66
+ },
67
+ 'right-start': {
68
+ axis: 'inline',
69
+ edge: 'end',
70
+ align: 'start'
71
+ },
72
+ 'right-end': {
73
+ axis: 'inline',
74
+ edge: 'end',
75
+ align: 'end'
76
+ },
77
+ left: {
78
+ axis: 'inline',
79
+ edge: 'start',
80
+ align: 'center'
81
+ },
82
+ 'left-start': {
83
+ axis: 'inline',
84
+ edge: 'start',
85
+ align: 'start'
86
+ },
87
+ 'left-end': {
88
+ axis: 'inline',
89
+ edge: 'start',
90
+ align: 'end'
91
+ },
92
+ auto: {
93
+ axis: 'block',
94
+ edge: 'end',
95
+ align: 'center'
96
+ },
97
+ 'auto-start': {
98
+ axis: 'block',
99
+ edge: 'end',
100
+ align: 'start'
101
+ },
102
+ 'auto-end': {
103
+ axis: 'block',
104
+ edge: 'end',
105
+ align: 'end'
106
+ }
107
+ };
@@ -5,8 +5,8 @@ import { ax, ix } from "@compiled/react/runtime";
5
5
  import React, { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef } from 'react';
6
6
  import { bind } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
- import noop from '@atlaskit/ds-lib/noop';
9
8
  import once from '@atlaskit/ds-lib/once';
9
+ import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
10
10
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
11
11
  import { useFocusWrap } from '../internal/use-focus-wrap';
12
12
  import { useInitialFocus } from '../internal/use-initial-focus';
@@ -29,6 +29,18 @@ const supportsPopoverHint = once(() => {
29
29
  const styles = {
30
30
  root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
31
31
  };
32
+ /**
33
+ * ARIA roles on the Popover primitive that register with the open layer observer
34
+ * as LayerType 'popup'. These are interactive overlays that should be tracked
35
+ * and dismissed when closeLayers() is called (e.g. when the side nav resizes).
36
+ *
37
+ * Note on open layer observer types not included here:
38
+ * - 'modal' only ever comes from the Dialog primitive, not Popover.
39
+ * - Other layer types (e.g. 'tooltip') do not yet exist in LayerType because
40
+ * there is no current use case for distinguishing them in the observer.
41
+ */
42
+ const POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree', 'grid']);
43
+
32
44
  /**
33
45
  * Unopinionated top-layer primitive.
34
46
  *
@@ -43,13 +55,13 @@ const styles = {
43
55
  *
44
56
  * For `mode="auto"`, the browser can dismiss the popover via light dismiss
45
57
  * (Escape, click outside). When that happens, `onClose` is called and the
46
- * consumer must set `isOpen` to `false`. The DOM owns the dismiss — React
58
+ * consumer must set `isOpen` to `false`. The DOM owns the dismiss; React
47
59
  * must follow.
48
60
  */
49
61
  export const Popover = /*#__PURE__*/forwardRef(function Popover({
50
62
  children,
51
63
  mode: modeProp = 'auto',
52
- onClose = noop,
64
+ onClose,
53
65
  onOpenChange,
54
66
  onExitFinish,
55
67
  animate,
@@ -66,7 +78,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
66
78
  const autoId = useId();
67
79
  const ownRef = useRef(null);
68
80
  const combinedRef = mergeRefs([ownRef, ref].filter(Boolean));
69
- // Regex is intentional — `replaceAll` is not available in our supported
81
+ // Regex is intentional: `replaceAll` is not available in our supported
70
82
  // browser matrix. In React 18, `useId()` returns IDs containing colons
71
83
  // (e.g. `:r1:`) which are invalid in CSS selectors and `popover` target
72
84
  // attributes. React 19.2.0 replaced colons with underscores, making this
@@ -85,7 +97,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
85
97
  // ── Focus management ──
86
98
  // Initial focus: on open, moves focus into the popover (role-dependent).
87
99
  // Focus wrap: Tab/Shift+Tab cycle within the popover for dialog roles.
88
- // Focus restoration: handled natively by the Popover API — the browser
100
+ // Focus restoration: handled natively by the Popover API. The browser
89
101
  // tracks `previouslyFocusedElement` on show and restores it on hide.
90
102
  // No custom restoration hook is needed. See: notes/architecture/focus-restoration.md
91
103
  useFocusWrap({
@@ -98,6 +110,24 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
98
110
  role
99
111
  });
100
112
 
113
+ // ── Open layer observer registration ──
114
+ // Notifies the open layer observer so app-coordination features
115
+ // (closeLayers(), open-count subscriptions) work with top-layer popovers.
116
+ //
117
+ // Only set type: 'popup' for roles that are interactive overlay popups.
118
+ // Passive/informational roles (tooltip, note, status, alert, log) and
119
+ // no-role cases do not map to LayerType 'popup', so type is left undefined.
120
+ const handleObserverClose = useCallback(() => {
121
+ onClose === null || onClose === void 0 ? void 0 : onClose({
122
+ reason: 'programmatic'
123
+ });
124
+ }, [onClose]);
125
+ useNotifyOpenLayerObserver({
126
+ type: role && POPUP_ROLES.has(role) ? 'popup' : undefined,
127
+ isOpen,
128
+ onClose: handleObserverClose
129
+ });
130
+
101
131
  // Hint mode fallback: "hint" downgrades to "auto" if unsupported.
102
132
  //
103
133
  // Decision (2026-03-17 audit): We keep "auto" as the fallback (not "manual")
@@ -110,8 +140,8 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
110
140
  const mode = modeProp === 'hint' && !supportsPopoverHint() ? 'auto' : modeProp;
111
141
 
112
142
  // Distinguishes programmatic hidePopover() from browser-initiated dismiss.
113
- // When we call hidePopover() ourselves the beforetoggle event fires synchronously —
114
- // this ref prevents the toggle handler from calling onClose redundantly.
143
+ // When we call hidePopover() ourselves the beforetoggle event fires synchronously,
144
+ // so this ref prevents the toggle handler from calling onClose redundantly.
115
145
  const programmaticCloseRef = useRef(false);
116
146
 
117
147
  // Escape key tracking: set in a capture-phase keydown listener (fires before
@@ -144,7 +174,11 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
144
174
  // Browser dismiss (Escape or click-outside). Notify consumer.
145
175
  const reason = closeReasonRef.current;
146
176
  closeReasonRef.current = 'light-dismiss'; // reset for next dismiss
147
- onClose({
177
+ // `onClose` is required for `auto`/`hint` and forbidden for
178
+ // `manual`, but `TPopoverForwardedProps` is a flat type used
179
+ // internally by `PopupContent` - the optional check guards
180
+ // the manual-mode forwarded path where the field is undefined.
181
+ onClose === null || onClose === void 0 ? void 0 : onClose({
148
182
  reason
149
183
  });
150
184
  }
@@ -179,7 +213,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
179
213
 
180
214
  // Placement-dependent animation CSS vars (e.g. slide direction).
181
215
  // Separate from show/hide so that preset reference changes
182
- // (e.g. inline `animate={slideAndFade()}`) don't re-trigger visibility.
216
+ // (e.g. inline `animate={slideAndFade()}`) do not re-trigger visibility.
183
217
  useLayoutEffect(() => {
184
218
  const el = ownRef.current;
185
219
  if (!el || !(preset !== null && preset !== void 0 && preset.getProperties) || !placement) {
@@ -198,7 +232,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
198
232
  // in the target state (e.g. browser dismissed via light-dismiss before our
199
233
  // effect ran).
200
234
  // Defensive try/catch: these APIs throw InvalidStateError if the element
201
- // is disconnected or missing the popover attribute. While we don't expect
235
+ // is disconnected or missing the popover attribute. While we do not expect
202
236
  // that to happen in normal usage, it can occur in edge cases (e.g. React
203
237
  // concurrent mode, StrictMode double-effects, or unmount races).
204
238
  useLayoutEffect(() => {
@@ -3,7 +3,7 @@ import _extends from "@babel/runtime/helpers/extends";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
5
5
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
- import { roleToAriaHasPopup } from '../internal/role-types';
6
+ import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
7
7
  import { useAnchorPosition } from '../internal/use-anchor-position';
8
8
  import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
9
9
  import { Popover } from '../popover/popover';
@@ -29,7 +29,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
29
29
  placement: placementProp,
30
30
  onClose: onCloseProp,
31
31
  testId: testIdProp,
32
- forceFallbackPositioning: forceFallbackPositioningProp
32
+ forceFallbackPositioning: forceFallbackPositioningProp,
33
+ id: idProp
33
34
  }, ref) {
34
35
  var _ref, _ref2, _ctx$popoverRef;
35
36
  // When inside <Popup>, context provides defaults. Props always win.
@@ -74,7 +75,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
74
75
  // does not restore focus when they close.
75
76
  //
76
77
  // To handle this, we listen for the close transition via `onOpenChange` and
77
- // restore focus to the trigger if the browser didn't. This runs after the
78
+ // restore focus to the trigger if the browser did not. This runs after the
78
79
  // browser's native restoration (which happens synchronously during hidePopover),
79
80
  // so it only takes effect when the browser skipped restoration (nested case).
80
81
  //
@@ -91,7 +92,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
91
92
  // focus when they close.
92
93
  //
93
94
  // We detect this case by checking if focus is still inside the
94
- // closing popover element. If it is, the browser didn't restore,
95
+ // closing popover element. If it is, the browser did not restore,
95
96
  // and we need to move focus to the trigger ourselves.
96
97
  //
97
98
  // This does NOT fire for:
@@ -100,13 +101,15 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
100
101
  if (!args.isOpen) {
101
102
  const trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
102
103
  const focusStillInPopover = trigger && args.element.contains(document.activeElement);
103
- if (focusStillInPopover) {
104
- const shouldRestore = role === 'dialog' || role === 'alertdialog' || role === 'menu' || role === 'listbox';
105
- if (shouldRestore) {
106
- trigger.focus({
107
- preventScroll: true
108
- });
109
- }
104
+ // Single source of truth for "this role moves focus on open
105
+ // and therefore needs to be restored on close" - see
106
+ // `shouldFocusIntoPopover` in `internal/role-types.tsx`.
107
+ if (focusStillInPopover && shouldFocusIntoPopover({
108
+ role
109
+ })) {
110
+ trigger.focus({
111
+ preventScroll: true
112
+ });
110
113
  }
111
114
  }
112
115
  }, [setIsOpen, consumerOnOpenChange, triggerRef, role]);
@@ -136,8 +139,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
136
139
  const combinedRef = mergeRefs([popoverRef, ref]);
137
140
 
138
141
  // TPopupContentProps already validates the discriminated union (role + label,
139
- // mode + onClose) at its API boundary. Popover accepts TPopoverInternalProps
140
- // internally so individual prop forwarding doesn't need to re-prove the union.
142
+ // mode + onClose) at its API boundary. Popover accepts TPopoverForwardedProps
143
+ // internally so individual prop forwarding does not need to re-prove the union.
141
144
  const popoverProps = {
142
145
  isOpen,
143
146
  animate,
@@ -150,7 +153,10 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
150
153
  onOpenChange: handleOpenChange,
151
154
  testId,
152
155
  children,
153
- id: ctx === null || ctx === void 0 ? void 0 : ctx.popoverId,
156
+ // Standalone consumers can pass an explicit `id` - used by their own
157
+ // trigger to wire `aria-controls`. Inside the `<Popup>` compound, the
158
+ // id flows from context (`ctx.popoverId`).
159
+ id: idProp !== null && idProp !== void 0 ? idProp : ctx === null || ctx === void 0 ? void 0 : ctx.popoverId,
154
160
  xcss
155
161
  };
156
162
  return /*#__PURE__*/React.createElement(Popover, _extends({
@@ -15,6 +15,7 @@ export const PopupProvider = PopupContext.Provider;
15
15
  *
16
16
  * Throws if called outside of a `<Popup>` compound component.
17
17
  */
18
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
18
19
  export function usePopupContext() {
19
20
  const ctx = useContext(PopupContext);
20
21
  if (ctx === null) {
@@ -29,6 +30,7 @@ export function usePopupContext() {
29
30
  * Used by `PopupContent` to support standalone usage (e.g. tooltip)
30
31
  * where values are passed as props instead of coming from a compound component.
31
32
  */
33
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
32
34
  export function useMaybePopupContext() {
33
35
  return useContext(PopupContext);
34
36
  }
@@ -14,6 +14,12 @@ const styles = {
14
14
  * should use `Popup.Content` without wrapping in `PopupSurface`.
15
15
  *
16
16
  * Exposed as `Popup.Surface` on the compound component.
17
+ *
18
+ * **Presentational primitive - `children`-only by design.** This component
19
+ * intentionally exposes no `style`, `className`, `xcss`, `id`, or `ref`
20
+ * props. If you need any of those, render your own wrapper element inside
21
+ * `Popup.Content` instead - `PopupSurface` is the no-config "give me the
22
+ * default ADS overlay look" surface, nothing more.
17
23
  */
18
24
  export function PopupSurface({
19
25
  children
@@ -8,7 +8,7 @@ import { usePopupContext } from './popup-context';
8
8
  /**
9
9
  * Render-prop alternative to `Popup.Trigger`.
10
10
  *
11
- * Use when you need full control over the trigger element — for example,
11
+ * Use when you need full control over the trigger element. For example,
12
12
  * to compose your own trigger with custom event handlers, wrap a third-party
13
13
  * component, or build a trigger from multiple elements.
14
14
  *