@atlaskit/top-layer 0.9.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 (127) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -1
  3. package/dialog/create-close-event/package.json +17 -0
  4. package/dist/cjs/animations/presets.js +7 -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 +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 +6 -3
  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 +6 -3
  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
@@ -14,16 +14,16 @@ var menuScope = '[role="menu"]';
14
14
 
15
15
  /**
16
16
  * Returns true if the element belongs to the current menu level of the
17
- * container — i.e. it is NOT inside a nested `role="menu"` sub-container.
17
+ * container, i.e. it is NOT inside a nested `role="menu"` sub-container.
18
18
  *
19
19
  * Uses `element.closest('[role="menu"]')` to determine the nearest menu
20
20
  * scope. If the nearest scope is the container's own `role="menu"` element,
21
- * the element belongs to this level. If it's a different (nested) menu,
21
+ * the element belongs to this level. If it is a different (nested) menu,
22
22
  * the element belongs to a child level and should be excluded.
23
23
  *
24
24
  * Supports two container patterns:
25
25
  *
26
- * **1. Container has `role="menu"` directly** (preferred — used by avatar-group):
26
+ * **1. Container has `role="menu"` directly** (preferred, used by avatar-group):
27
27
  * ```
28
28
  * <div ref={containerRef} role="menu">
29
29
  * <button role="menuitem" /> ← isAtCurrentMenuLevel → true
@@ -35,7 +35,7 @@ var menuScope = '[role="menu"]';
35
35
  * Uses the simple check: `element.closest('[role="menu"]') === container`
36
36
  *
37
37
  * **2. Container is a wrapper without `role="menu"`** (used by dropdown-menu,
38
- * where `MenuGroup` owns `role="menu"` but doesn't accept a ref):
38
+ * where `MenuGroup` owns `role="menu"` but does not accept a ref):
39
39
  * ```
40
40
  * <div ref={containerRef}> ← wrapper (no role="menu")
41
41
  * <MenuGroup role="menu">
@@ -62,7 +62,7 @@ var menuScope = '[role="menu"]';
62
62
  function isAtCurrentMenuLevel(element, container) {
63
63
  var closestMenu = element.closest(menuScope);
64
64
 
65
- // Container itself has role="menu" — element belongs to this level
65
+ // Container itself has role="menu". Element belongs to this level
66
66
  // if its closest menu is the container.
67
67
  if (container.getAttribute('role') === 'menu') {
68
68
  return closestMenu === container;
@@ -71,18 +71,18 @@ function isAtCurrentMenuLevel(element, container) {
71
71
  // Container is a wrapper without role="menu" (e.g. dropdown-menu
72
72
  // wraps a MenuGroup). The element belongs to the top level if its
73
73
  // closest role="menu" ancestor is a direct child of the container,
74
- // i.e. there's no nested role="menu" between them.
74
+ // i.e. there is no nested role="menu" between them.
75
75
  if (closestMenu === null) {
76
76
  return true;
77
77
  }
78
78
 
79
- // Walk up from the closest menu to see if it's "owned" by this
79
+ // Walk up from the closest menu to see if it is "owned" by this
80
80
  // container without passing through another role="menu" first.
81
81
  var parent = closestMenu.parentElement;
82
82
  while (parent && parent !== container) {
83
83
  if (parent.getAttribute('role') === 'menu') {
84
- // There's a parent menu between the closest menu and the
85
- // container — the element is in a nested sub-menu.
84
+ // There is a parent menu between the closest menu and the
85
+ // container, so the element is in a nested sub-menu.
86
86
  return false;
87
87
  }
88
88
  parent = parent.parentElement;
@@ -91,6 +91,22 @@ function isAtCurrentMenuLevel(element, container) {
91
91
  // The closest menu is directly inside the container (no nesting).
92
92
  return parent === container;
93
93
  }
94
+
95
+ /**
96
+ * Args for `useArrowNavigation`.
97
+ *
98
+ * Keyboard contract:
99
+ * - `ArrowDown` / `ArrowUp` move within the current menu level.
100
+ * - `Home` / `End` jump to first / last.
101
+ * - `ArrowRight` / `ArrowLeft` opens / closes nested submenus.
102
+ * - `Tab` / `Shift+Tab` dismiss the menu (matching the ARIA APG menu pattern,
103
+ * plus Radix UI / Headless UI / React Aria / Floating UI conventions).
104
+ * Symmetric in both directions: there is no special-case for the last
105
+ * item that lets Tab leave the menu without dismissing it.
106
+ * - `Page Up` / `Page Down`: NOT handled - extend via your own listener if
107
+ * your menu needs page-style scrolling.
108
+ */
109
+
94
110
  /**
95
111
  * Provides arrow key navigation for menu containers.
96
112
  *
@@ -222,7 +238,7 @@ function useArrowNavigation(_ref) {
222
238
  if (onNestedClose) {
223
239
  var _container$parentElem;
224
240
  // Only close if this container is inside another role="menu"
225
- // (i.e. it's a submenu, not the top-level menu).
241
+ // (i.e. it is a submenu, not the top-level menu).
226
242
  // Walk up from the parent to avoid matching the container itself.
227
243
  var isNested = ((_container$parentElem = container.parentElement) === null || _container$parentElem === void 0 ? void 0 : _container$parentElem.closest(menuScope)) !== null;
228
244
  if (isNested) {
@@ -1,4 +1,4 @@
1
- import { getPlacement } from '../internal/use-anchor-position';
1
+ import { getPlacement } from '../internal/resolve-placement';
2
2
 
3
3
  // ── slideAndFade ──
4
4
 
@@ -297,8 +297,10 @@ const DIALOG_SLIDE_UP_AND_FADE_CSS = `
297
297
  export function dialogSlideUpAndFade(options) {
298
298
  var _options$distance2;
299
299
  const distance = (_options$distance2 = options === null || options === void 0 ? void 0 : options.distance) !== null && _options$distance2 !== void 0 ? _options$distance2 : 12;
300
+
301
+ // Bare preset name; `Dialog` prefixes it via `data-ds-dialog-{name}`.
300
302
  return {
301
- name: distance === 12 ? 'dialog-slide-up-and-fade' : `dialog-slide-up-and-fade-${distance}`,
303
+ name: distance === 12 ? 'slide-up-and-fade' : `slide-up-and-fade-${distance}`,
302
304
  css: distance === 12 ? DIALOG_SLIDE_UP_AND_FADE_CSS : DIALOG_SLIDE_UP_AND_FADE_CSS.replace(/12px/g, `${distance}px`),
303
305
  exitDurationMs: 175
304
306
  };
@@ -371,8 +373,9 @@ const DIALOG_FADE_CSS = `
371
373
  */
372
374
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
373
375
  export function dialogFade() {
376
+ // Bare preset name; `Dialog` prefixes it via `data-ds-dialog-{name}`.
374
377
  return {
375
- name: 'dialog-fade',
378
+ name: 'fade',
376
379
  css: DIALOG_FADE_CSS,
377
380
  exitDurationMs: 175
378
381
  };
@@ -0,0 +1,269 @@
1
+ import once from '@atlaskit/ds-lib/once';
2
+ import { getPlacement } from '../internal/resolve-placement';
3
+ import { placementToPositionArea } from '../internal/use-anchor-position';
4
+ /**
5
+ * CSS for popover arrows using the `clip-path: inset() margin-box` technique.
6
+ *
7
+ * How it works:
8
+ * 1. Four arrow shapes exist simultaneously as `::before` (top/bottom) and
9
+ * `::after` (left/right) hexagonal pseudo-elements.
10
+ * 2. `clip-path: inset(0) margin-box` clips the popover to its margin-box.
11
+ * 3. Margin on the anchor-facing side pushes the popover away, creating space
12
+ * where one arrow "escapes" the clip boundary and becomes visible.
13
+ * 4. `@position-try` rules change both `position-area` and `margin` when
14
+ * flipping, ensuring the correct arrow is always shown.
15
+ *
16
+ * Constraints:
17
+ * - `box-shadow` must be `none` (interferes with `clip-path: inset() margin-box`)
18
+ * - `::before` and `::after` are consumed - consumers cannot use them
19
+ * - Arrow pseudo-elements use `background: inherit` from the popover element
20
+ *
21
+ * @see https://goulet.dev/posts/tooltip-with-popover-and-anchor-positioning/
22
+ */
23
+ const ARROW_CSS = `
24
+ [data-ds-popover-arrow] {
25
+ clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;
26
+ box-shadow: none;
27
+ }
28
+
29
+ /* Top and Bottom arrows (vertical hexagon) */
30
+ [data-ds-popover-arrow]::before {
31
+ content: "";
32
+ position: absolute;
33
+ z-index: -1;
34
+ background: inherit;
35
+ left: 50%;
36
+ transform: translateX(-50%);
37
+ width: calc(var(--ds-arrow-size, 8px) * 2);
38
+ height: calc(100% + var(--ds-arrow-size, 8px) * 2);
39
+ top: calc(var(--ds-arrow-size, 8px) * -1);
40
+ clip-path: polygon(
41
+ 0 var(--ds-arrow-size, 8px),
42
+ 50% 0,
43
+ 100% var(--ds-arrow-size, 8px),
44
+ 100% calc(100% - var(--ds-arrow-size, 8px)),
45
+ 50% 100%,
46
+ 0 calc(100% - var(--ds-arrow-size, 8px))
47
+ );
48
+ }
49
+
50
+ /* Left and Right arrows (horizontal hexagon) */
51
+ [data-ds-popover-arrow]::after {
52
+ content: "";
53
+ position: absolute;
54
+ z-index: -1;
55
+ background: inherit;
56
+ top: 50%;
57
+ transform: translateY(-50%);
58
+ height: calc(var(--ds-arrow-size, 8px) * 2);
59
+ width: calc(100% + var(--ds-arrow-size, 8px) * 2);
60
+ left: calc(var(--ds-arrow-size, 8px) * -1);
61
+ clip-path: polygon(
62
+ var(--ds-arrow-size, 8px) 0,
63
+ calc(100% - var(--ds-arrow-size, 8px)) 0,
64
+ 100% 50%,
65
+ calc(100% - var(--ds-arrow-size, 8px)) 100%,
66
+ var(--ds-arrow-size, 8px) 100%,
67
+ 0 50%
68
+ );
69
+ }
70
+
71
+ /* ── @position-try rules ──
72
+ * One rule per possible position-area value.
73
+ * Each sets the position-area and the correct margin direction
74
+ * so the arrow on the anchor-facing side is revealed.
75
+ *
76
+ * Margin direction per edge:
77
+ * block-start (above anchor) → margin-block-end (bottom, facing anchor)
78
+ * block-end (below anchor) → margin-block-start (top, facing anchor)
79
+ * inline-start (left of anchor) → margin-inline-end (right, facing anchor)
80
+ * inline-end (right of anchor) → margin-inline-start (left, facing anchor)
81
+ */
82
+
83
+ /* Each rule sets two margins: the anchor-facing margin for the gap
84
+ (via --ds-arrow-size) and one cross-axis margin for the shift
85
+ (via one of --ds-cross-axis-shift-margin-start, --ds-cross-axis-shift-margin-end,
86
+ --ds-cross-axis-shift-margin-block-start, or --ds-cross-axis-shift-margin-block-end). The
87
+ side matches the popover's anchored cross-axis edge per align. Only
88
+ one of the four properties is non-zero at a time; useAnchorPosition
89
+ picks it. See notes/architecture/positioning.md for details. */
90
+ /* Single-axis centered (align: center) */
91
+ @position-try --ds-arrow-block-start {
92
+ position-area: block-start;
93
+ margin: 0;
94
+ margin-block-end: var(--ds-arrow-size, 8px);
95
+ margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);
96
+ }
97
+ @position-try --ds-arrow-block-end {
98
+ position-area: block-end;
99
+ margin: 0;
100
+ margin-block-start: var(--ds-arrow-size, 8px);
101
+ margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);
102
+ }
103
+ @position-try --ds-arrow-inline-start {
104
+ position-area: inline-start;
105
+ margin: 0;
106
+ margin-inline-end: var(--ds-arrow-size, 8px);
107
+ margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);
108
+ }
109
+ @position-try --ds-arrow-inline-end {
110
+ position-area: inline-end;
111
+ margin: 0;
112
+ margin-inline-start: var(--ds-arrow-size, 8px);
113
+ margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);
114
+ }
115
+
116
+ /* Block edge + inline span (align: start uses span-inline-end; align: end uses span-inline-start) */
117
+ @position-try --ds-arrow-block-start-span-inline-start {
118
+ position-area: block-start span-inline-start;
119
+ margin: 0;
120
+ margin-block-end: var(--ds-arrow-size, 8px);
121
+ margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);
122
+ }
123
+ @position-try --ds-arrow-block-start-span-inline-end {
124
+ position-area: block-start span-inline-end;
125
+ margin: 0;
126
+ margin-block-end: var(--ds-arrow-size, 8px);
127
+ margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);
128
+ }
129
+ @position-try --ds-arrow-block-end-span-inline-start {
130
+ position-area: block-end span-inline-start;
131
+ margin: 0;
132
+ margin-block-start: var(--ds-arrow-size, 8px);
133
+ margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);
134
+ }
135
+ @position-try --ds-arrow-block-end-span-inline-end {
136
+ position-area: block-end span-inline-end;
137
+ margin: 0;
138
+ margin-block-start: var(--ds-arrow-size, 8px);
139
+ margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);
140
+ }
141
+
142
+ /* Inline edge + block span (align: start uses span-block-end; align: end uses span-block-start) */
143
+ @position-try --ds-arrow-inline-start-span-block-start {
144
+ position-area: inline-start span-block-start;
145
+ margin: 0;
146
+ margin-inline-end: var(--ds-arrow-size, 8px);
147
+ margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);
148
+ }
149
+ @position-try --ds-arrow-inline-start-span-block-end {
150
+ position-area: inline-start span-block-end;
151
+ margin: 0;
152
+ margin-inline-end: var(--ds-arrow-size, 8px);
153
+ margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);
154
+ }
155
+ @position-try --ds-arrow-inline-end-span-block-start {
156
+ position-area: inline-end span-block-start;
157
+ margin: 0;
158
+ margin-inline-start: var(--ds-arrow-size, 8px);
159
+ margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);
160
+ }
161
+ @position-try --ds-arrow-inline-end-span-block-end {
162
+ position-area: inline-end span-block-end;
163
+ margin: 0;
164
+ margin-inline-start: var(--ds-arrow-size, 8px);
165
+ margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);
166
+ }
167
+ `;
168
+
169
+ // ── Placement utilities ──
170
+
171
+ /**
172
+ * Converts a `position-area` value to a CSS rule name by replacing spaces with hyphens
173
+ * and prepending `--ds-arrow-`.
174
+ *
175
+ * @example positionAreaToRuleName('block-end span-inline-start') → '--ds-arrow-block-end-span-inline-start'
176
+ */
177
+ function positionAreaToRuleName({
178
+ positionArea
179
+ }) {
180
+ return `--ds-arrow-${positionArea.replace(/ /g, '-')}`;
181
+ }
182
+
183
+ /**
184
+ * Returns `position-try-fallbacks` using named `@position-try` rules
185
+ * that change both `position-area` and `margin` when flipping.
186
+ *
187
+ * This is required for the arrow technique because built-in keywords
188
+ * (`flip-block`, `flip-inline`) only flip `position-area` - they do
189
+ * not update the margin direction needed to reveal the correct arrow.
190
+ */
191
+ function getTryFallbacks({
192
+ placement
193
+ }) {
194
+ const {
195
+ axis,
196
+ edge,
197
+ align
198
+ } = getPlacement({
199
+ placement
200
+ });
201
+ if (align === 'center') {
202
+ // Single-axis: only flip the edge
203
+ const flippedEdge = edge === 'start' ? 'end' : 'start';
204
+ return positionAreaToRuleName({
205
+ positionArea: placementToPositionArea({
206
+ placement: {
207
+ axis,
208
+ edge: flippedEdge
209
+ }
210
+ })
211
+ });
212
+ }
213
+
214
+ // Compound placement: try flipping edge, alignment, and both
215
+ const flippedEdge = edge === 'start' ? 'end' : 'start';
216
+ const flippedAlign = align === 'start' ? 'end' : 'start';
217
+ return [positionAreaToRuleName({
218
+ positionArea: placementToPositionArea({
219
+ placement: {
220
+ axis,
221
+ edge: flippedEdge,
222
+ align
223
+ }
224
+ })
225
+ }), positionAreaToRuleName({
226
+ positionArea: placementToPositionArea({
227
+ placement: {
228
+ axis,
229
+ edge,
230
+ align: flippedAlign
231
+ }
232
+ })
233
+ }), positionAreaToRuleName({
234
+ positionArea: placementToPositionArea({
235
+ placement: {
236
+ axis,
237
+ edge: flippedEdge,
238
+ align: flippedAlign
239
+ }
240
+ })
241
+ })].join(', ');
242
+ }
243
+
244
+ /**
245
+ * Creates an arrow preset for popover content.
246
+ *
247
+ * The returned object is passed to `PopoverContent`'s `arrow` prop.
248
+ * It carries the CSS string and fallback logic so that consumers who
249
+ * do not use arrows pay no bundle cost.
250
+ *
251
+ * The arrows inherit their background from the popover element.
252
+ * When `arrow` is active, `PopoverContent` does not reset `background`
253
+ * to `transparent`, allowing the UA default (`canvas`) or a consumer-set
254
+ * background to show through.
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * import { arrow } from '@atlaskit/top-layer/arrow';
259
+ *
260
+ * const myArrow = arrow();
261
+ *
262
+ * <Popup.Content arrow={myArrow} />
263
+ * ```
264
+ */
265
+ export const arrow = once(() => ({
266
+ name: 'arrow',
267
+ css: ARROW_CSS,
268
+ getTryFallbacks
269
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -15,7 +15,7 @@ const styles = {
15
15
  };
16
16
 
17
17
  /**
18
- * Low-level `<dialog>` primitive. No visual opinions — no width, height,
18
+ * Low-level `<dialog>` primitive. No visual opinions - no width, height,
19
19
  * background, border-radius, or layout. Consumers provide their own styling.
20
20
  *
21
21
  * Visibility is controlled declaratively via `isOpen`:
@@ -26,6 +26,10 @@ const styles = {
26
26
  *
27
27
  * Close flow: we never call `dialog.close()` from event handlers. We always call
28
28
  * `onClose`; the consumer decides whether to set `isOpen={false}`.
29
+ *
30
+ * Accessibility: render at least one focusable element (typically a close
31
+ * button) when `isOpen` becomes `true`. Tab is always trapped inside the
32
+ * dialog, so keyboard users need somewhere to land.
29
33
  */
30
34
  export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
31
35
  children,
@@ -133,11 +137,12 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
133
137
  return /*#__PURE__*/React.createElement("dialog", _extends({
134
138
  ref: combinedRef,
135
139
  id: dialogId
136
- // Modern browsers infer modal semantics from `.showModal()` but
137
- // some assistive tech still keys off `aria-modal="true"`. Setting
138
- // it explicitly is recommended by WAI-ARIA APG dialog pattern.
140
+ // `aria-modal` is intentionally NOT set: native `<dialog>.showModal()`
141
+ // already conveys modal semantics to assistive tech, and double-
142
+ // declaring it forecloses non-modal use cases (consumers calling
143
+ // `.show()` would still appear modal). Modern AT (NVDA / JAWS /
144
+ // VoiceOver) infer modality from the platform accessibility API.
139
145
  ,
140
- "aria-modal": "true",
141
146
  "aria-label": label,
142
147
  "aria-labelledby": label ? undefined : labelledBy,
143
148
  // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
@@ -145,7 +150,7 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
145
150
  onCancel: handleCancel,
146
151
  "data-testid": testId
147
152
  }, preset ? {
148
- [`data-ds-${preset.name}`]: ''
153
+ [`data-ds-dialog-${preset.name}`]: ''
149
154
  } : undefined, {
150
155
  className: ax([styles.dialog])
151
156
  }), shouldHideBackdrop &&
@@ -1,2 +1,9 @@
1
+ /**
2
+ * @deprecated Combined entry-point retained for backwards-compatibility.
3
+ * Prefer the disjoint subpaths so that bundle analysis attributes the cost
4
+ * to the right consumer:
5
+ * - `@atlaskit/top-layer/dialog/create-close-event` for `createCloseEvent`
6
+ * - `@atlaskit/top-layer/popover/create-close-event` for `createPopoverCloseEvent`
7
+ */
1
8
  export { createCloseEvent } from '../dialog';
2
9
  export { createPopoverCloseEvent } from '../popover/create-close-event';
@@ -0,0 +1 @@
1
+ export { createCloseEvent } from '../../dialog/create-close-event';
@@ -1 +1,2 @@
1
- export { fromLegacyPlacement, placementMapping } from '../placement-map';
1
+ export { fromLegacyPlacement } from '../placement-map';
2
+ export { LEGACY_PLACEMENTS } from '../placement-map/legacy-placements';
@@ -0,0 +1 @@
1
+ export { createPopoverCloseEvent } from '../../popover/create-close-event';
@@ -10,6 +10,7 @@
10
10
  * restrict which focusable elements are included.
11
11
  */
12
12
 
13
+ import { getDocument } from '@atlaskit/browser-apis';
13
14
  const selectors = (() => {
14
15
  // Common exclusion filter applied to every selector.
15
16
  const not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
@@ -17,9 +18,54 @@ const selectors = (() => {
17
18
  const focusable = [`a[href]${not}`, `area[href]${not}`, `input${notDisabled}:not([type="hidden"])`, `select${notDisabled}`, `textarea${notDisabled}`, `button${notDisabled}`, `iframe${not}`, `audio[controls]${not}`, `video[controls]${not}`, `[contenteditable]:not([contenteditable="false"])${not}`, `[tabindex]${notDisabled}${not}`];
18
19
  return {
19
20
  focusable: focusable.join(','),
20
- focused: focusable.join(':focus,') + ':focus'
21
+ // Build the `:focus` selector
22
+ focused: focusable.map(selector => `${selector}:focus`).join(',')
21
23
  };
22
24
  })();
25
+ const topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
26
+
27
+ /**
28
+ * Returns true when `element` and `container` resolve to the same nearest
29
+ * top-layer host (or both resolve to `null`, meaning the document layer).
30
+ */
31
+ function isInSameLayer({
32
+ element,
33
+ container
34
+ }) {
35
+ const elementLayer = element.closest(topLayerSelector);
36
+ const containerLayer = container.closest(topLayerSelector);
37
+ // Both elements are in the "document" layer
38
+ if (!elementLayer && !containerLayer) {
39
+ return true;
40
+ }
41
+ return elementLayer === containerLayer;
42
+ }
43
+
44
+ /**
45
+ * Returns `true` when `document.activeElement` is inside a nested top-layer
46
+ * descendant of `container` (a `[popover]`, `<dialog>`, `[role="dialog"]` or
47
+ * `[role="alertdialog"]` that is itself a descendant of `container`).
48
+ */
49
+ export function isNestedLayerFocused({
50
+ container
51
+ }) {
52
+ var _getDocument;
53
+ const focused = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
54
+ if (!(focused instanceof HTMLElement)) {
55
+ return false;
56
+ }
57
+
58
+ // Cannot be in a nested layer if it is not in the container
59
+ if (!container.contains(focused)) {
60
+ return false;
61
+ }
62
+
63
+ // If not in the same layer, must be in a nested layer
64
+ return !isInSameLayer({
65
+ element: focused,
66
+ container
67
+ });
68
+ }
23
69
 
24
70
  /**
25
71
  * An optional callback to restrict which focusable elements are included.
@@ -29,22 +75,25 @@ const selectors = (() => {
29
75
  /**
30
76
  * Returns all focusable HTMLElements within the container, optionally filtered.
31
77
  *
32
- * Note: this searches the full subtree and does not stop at nested
33
- * popover/dialog boundaries. In practice this is fine because nested
34
- * popovers are not rendered until they are opened, so their content
35
- * is not in the DOM when the parent's initial focus runs. If that
36
- * assumption changes, this would need to be scoped to the current layer.
78
+ * Focusables that belong to a nested top-layer scope (a `[popover]` or
79
+ * `<dialog>` descendant of the container) are excluded - those elements
80
+ * are owned by the inner layer's focus management. The container itself
81
+ * is allowed to be a popover/dialog; only nested ones are filtered.
37
82
  */
38
83
  function getFocusables({
39
84
  container,
40
85
  filter
41
86
  }) {
42
- return Array.from(container.querySelectorAll(selectors.focusable)).filter(element => element instanceof HTMLElement).filter(element => !filter || filter(element, container));
87
+ return Array.from(container.querySelectorAll(selectors.focusable)).filter(element => element instanceof HTMLElement).filter(element => isInSameLayer({
88
+ element,
89
+ container
90
+ })).filter(element => !filter || filter(element, container));
43
91
  }
44
92
 
45
93
  /**
46
94
  * Returns the first focusable element within the container.
47
95
  */
96
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
48
97
  export function getFirstFocusable({
49
98
  container,
50
99
  filter
@@ -1,7 +1,6 @@
1
1
  // This file exists for module resolution purposes.
2
2
  // Not exporting anything from the root entry point.
3
- // Please use individual entry points for minimum bundle size:
4
3
  //
5
- // import { Popup } from '@atlaskit/top-layer/popup';
6
- // import { Dialog } from '@atlaskit/top-layer/dialog';
7
- // import { Popover } from '@atlaskit/top-layer/popover';
4
+ // Use individual entry points for minimum bundle size - the full list of
5
+ // available subpaths is the `"exports"` field in this package's
6
+ // `package.json`. The README documents the most common ones.
@@ -1,3 +1,34 @@
1
+ const elementCaches = new WeakMap();
2
+ function canCacheResolvedLength(value) {
3
+ // Custom-property values can be retargeted by inline overrides on the
4
+ // consumer's wrapper, so we cannot prove a cached pixel value is still valid.
5
+ return !value.includes('var(');
6
+ }
7
+ function getCachedResolvedLength({
8
+ element,
9
+ value
10
+ }) {
11
+ var _cacheForElement$get;
12
+ if (!canCacheResolvedLength(value)) {
13
+ return null;
14
+ }
15
+ const cacheForElement = elementCaches.get(element);
16
+ return (_cacheForElement$get = cacheForElement === null || cacheForElement === void 0 ? void 0 : cacheForElement.get(value)) !== null && _cacheForElement$get !== void 0 ? _cacheForElement$get : null;
17
+ }
18
+ function cacheResolvedLength({
19
+ element,
20
+ value,
21
+ result
22
+ }) {
23
+ var _elementCaches$get;
24
+ if (!canCacheResolvedLength(value)) {
25
+ return;
26
+ }
27
+ const cacheForElement = (_elementCaches$get = elementCaches.get(element)) !== null && _elementCaches$get !== void 0 ? _elementCaches$get : new Map();
28
+ cacheForElement.set(value, result);
29
+ elementCaches.set(element, cacheForElement);
30
+ }
31
+
1
32
  /**
2
33
  * Resolves a CSS length value to a pixel number using a hidden DOM probe.
3
34
  *
@@ -8,13 +39,11 @@
8
39
  * the browser do the math.
9
40
  *
10
41
  * The probe is appended to `container` so it inherits the same containing
11
- * block, font size, and custom-property scope as the popover. We use `width`
12
- * because it accepts any CSS length, including negative `calc()` results
13
- * (negative widths are clamped to 0 — see note below).
42
+ * block, font size, and custom-property scope as the popover.
14
43
  *
15
- * Cost: one synchronous reflow per call. The fallback only resolves on
16
- * placement/offset changes, not on every scroll/resize update, so this is
17
- * not a hot path.
44
+ * Cost: one synchronous reflow on first cacheable resolution per
45
+ * (container, value) pair, then a `WeakMap` lookup. Resolutions involving
46
+ * `var(` are intentionally not cached.
18
47
  */
19
48
  export function resolveCssLengthToPixels({
20
49
  value,
@@ -24,12 +53,26 @@ export function resolveCssLengthToPixels({
24
53
  return value;
25
54
  }
26
55
 
56
+ // Fast path: unitless zero is universally zero pixels - accept `'0'` and
57
+ // `'-0'` without a probe. (Consumers occasionally pass `'0'` instead of
58
+ // the number `0`.)
59
+ if (value === '0' || value === '-0') {
60
+ return 0;
61
+ }
62
+
27
63
  // Fast path: bare `${n}px` strings come from the consumer normalisation
28
64
  // in `getPlacement`. Avoid a reflow when we can.
29
65
  const pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
30
66
  if (pxMatch) {
31
67
  return Number(pxMatch[1]);
32
68
  }
69
+ const cached = getCachedResolvedLength({
70
+ element: container,
71
+ value
72
+ });
73
+ if (cached !== null) {
74
+ return cached;
75
+ }
33
76
  const probe = container.ownerDocument.createElement('div');
34
77
  probe.style.position = 'absolute';
35
78
  probe.style.visibility = 'hidden';
@@ -41,8 +84,18 @@ export function resolveCssLengthToPixels({
41
84
  // preserved. `getComputedStyle` returns the resolved pixel value for
42
85
  // margin-* properties even when the source was a token / calc / var.
43
86
  probe.style.marginLeft = value;
87
+ // Hide the probe from assistive tech and event targeting; defensive
88
+ // against `getElementsByTagName('div')` walks in consumer code.
89
+ probe.setAttribute('aria-hidden', 'true');
90
+ probe.setAttribute('inert', '');
44
91
  container.appendChild(probe);
45
92
  const resolved = parseFloat(getComputedStyle(probe).marginLeft);
46
93
  container.removeChild(probe);
47
- return Number.isFinite(resolved) ? resolved : 0;
94
+ const result = Number.isFinite(resolved) ? resolved : 0;
95
+ cacheResolvedLength({
96
+ element: container,
97
+ value,
98
+ result
99
+ });
100
+ return result;
48
101
  }