@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
@@ -7,16 +7,16 @@ const menuScope = '[role="menu"]';
7
7
 
8
8
  /**
9
9
  * Returns true if the element belongs to the current menu level of the
10
- * container — i.e. it is NOT inside a nested `role="menu"` sub-container.
10
+ * container, i.e. it is NOT inside a nested `role="menu"` sub-container.
11
11
  *
12
12
  * Uses `element.closest('[role="menu"]')` to determine the nearest menu
13
13
  * scope. If the nearest scope is the container's own `role="menu"` element,
14
- * the element belongs to this level. If it's a different (nested) menu,
14
+ * the element belongs to this level. If it is a different (nested) menu,
15
15
  * the element belongs to a child level and should be excluded.
16
16
  *
17
17
  * Supports two container patterns:
18
18
  *
19
- * **1. Container has `role="menu"` directly** (preferred — used by avatar-group):
19
+ * **1. Container has `role="menu"` directly** (preferred, used by avatar-group):
20
20
  * ```
21
21
  * <div ref={containerRef} role="menu">
22
22
  * <button role="menuitem" /> ← isAtCurrentMenuLevel → true
@@ -28,7 +28,7 @@ const menuScope = '[role="menu"]';
28
28
  * Uses the simple check: `element.closest('[role="menu"]') === container`
29
29
  *
30
30
  * **2. Container is a wrapper without `role="menu"`** (used by dropdown-menu,
31
- * where `MenuGroup` owns `role="menu"` but doesn't accept a ref):
31
+ * where `MenuGroup` owns `role="menu"` but does not accept a ref):
32
32
  * ```
33
33
  * <div ref={containerRef}> ← wrapper (no role="menu")
34
34
  * <MenuGroup role="menu">
@@ -55,7 +55,7 @@ const menuScope = '[role="menu"]';
55
55
  export function isAtCurrentMenuLevel(element, container) {
56
56
  const closestMenu = element.closest(menuScope);
57
57
 
58
- // Container itself has role="menu" — element belongs to this level
58
+ // Container itself has role="menu". Element belongs to this level
59
59
  // if its closest menu is the container.
60
60
  if (container.getAttribute('role') === 'menu') {
61
61
  return closestMenu === container;
@@ -64,18 +64,18 @@ export function isAtCurrentMenuLevel(element, container) {
64
64
  // Container is a wrapper without role="menu" (e.g. dropdown-menu
65
65
  // wraps a MenuGroup). The element belongs to the top level if its
66
66
  // closest role="menu" ancestor is a direct child of the container,
67
- // i.e. there's no nested role="menu" between them.
67
+ // i.e. there is no nested role="menu" between them.
68
68
  if (closestMenu === null) {
69
69
  return true;
70
70
  }
71
71
 
72
- // Walk up from the closest menu to see if it's "owned" by this
72
+ // Walk up from the closest menu to see if it is "owned" by this
73
73
  // container without passing through another role="menu" first.
74
74
  let parent = closestMenu.parentElement;
75
75
  while (parent && parent !== container) {
76
76
  if (parent.getAttribute('role') === 'menu') {
77
- // There's a parent menu between the closest menu and the
78
- // container — the element is in a nested sub-menu.
77
+ // There is a parent menu between the closest menu and the
78
+ // container, so the element is in a nested sub-menu.
79
79
  return false;
80
80
  }
81
81
  parent = parent.parentElement;
@@ -84,6 +84,22 @@ export function isAtCurrentMenuLevel(element, container) {
84
84
  // The closest menu is directly inside the container (no nesting).
85
85
  return parent === container;
86
86
  }
87
+
88
+ /**
89
+ * Args for `useArrowNavigation`.
90
+ *
91
+ * Keyboard contract:
92
+ * - `ArrowDown` / `ArrowUp` move within the current menu level.
93
+ * - `Home` / `End` jump to first / last.
94
+ * - `ArrowRight` / `ArrowLeft` opens / closes nested submenus.
95
+ * - `Tab` / `Shift+Tab` dismiss the menu (matching the ARIA APG menu pattern,
96
+ * plus Radix UI / Headless UI / React Aria / Floating UI conventions).
97
+ * Symmetric in both directions: there is no special-case for the last
98
+ * item that lets Tab leave the menu without dismissing it.
99
+ * - `Page Up` / `Page Down`: NOT handled - extend via your own listener if
100
+ * your menu needs page-style scrolling.
101
+ */
102
+
87
103
  /**
88
104
  * Provides arrow key navigation for menu containers.
89
105
  *
@@ -215,7 +231,7 @@ export function useArrowNavigation({
215
231
  if (onNestedClose) {
216
232
  var _container$parentElem;
217
233
  // Only close if this container is inside another role="menu"
218
- // (i.e. it's a submenu, not the top-level menu).
234
+ // (i.e. it is a submenu, not the top-level menu).
219
235
  // Walk up from the parent to avoid matching the container itself.
220
236
  const isNested = ((_container$parentElem = container.parentElement) === null || _container$parentElem === void 0 ? void 0 : _container$parentElem.closest(menuScope)) !== null;
221
237
  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
 
@@ -159,8 +159,10 @@ var DIALOG_SLIDE_UP_AND_FADE_CSS = "\n[data-ds-dialog-slide-up-and-fade] {\n op
159
159
  export function dialogSlideUpAndFade(options) {
160
160
  var _options$distance2;
161
161
  var distance = (_options$distance2 = options === null || options === void 0 ? void 0 : options.distance) !== null && _options$distance2 !== void 0 ? _options$distance2 : 12;
162
+
163
+ // Bare preset name; `Dialog` prefixes it via `data-ds-dialog-{name}`.
162
164
  return {
163
- name: distance === 12 ? 'dialog-slide-up-and-fade' : "dialog-slide-up-and-fade-".concat(distance),
165
+ name: distance === 12 ? 'slide-up-and-fade' : "slide-up-and-fade-".concat(distance),
164
166
  css: distance === 12 ? DIALOG_SLIDE_UP_AND_FADE_CSS : DIALOG_SLIDE_UP_AND_FADE_CSS.replace(/12px/g, "".concat(distance, "px")),
165
167
  exitDurationMs: 175
166
168
  };
@@ -186,8 +188,9 @@ var DIALOG_FADE_CSS = "\n[data-ds-dialog-fade] {\n opacity: 0;\n transition:\n
186
188
  */
187
189
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
188
190
  export function dialogFade() {
191
+ // Bare preset name; `Dialog` prefixes it via `data-ds-dialog-{name}`.
189
192
  return {
190
- name: 'dialog-fade',
193
+ name: 'fade',
191
194
  css: DIALOG_FADE_CSS,
192
195
  exitDurationMs: 175
193
196
  };
@@ -0,0 +1,124 @@
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
+ var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;\n box-shadow: none;\n}\n\n/* Top and Bottom arrows (vertical hexagon) */\n[data-ds-popover-arrow]::before {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n left: 50%;\n transform: translateX(-50%);\n width: calc(var(--ds-arrow-size, 8px) * 2);\n height: calc(100% + var(--ds-arrow-size, 8px) * 2);\n top: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n 0 var(--ds-arrow-size, 8px),\n 50% 0,\n 100% var(--ds-arrow-size, 8px),\n 100% calc(100% - var(--ds-arrow-size, 8px)),\n 50% 100%,\n 0 calc(100% - var(--ds-arrow-size, 8px))\n );\n}\n\n/* Left and Right arrows (horizontal hexagon) */\n[data-ds-popover-arrow]::after {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n top: 50%;\n transform: translateY(-50%);\n height: calc(var(--ds-arrow-size, 8px) * 2);\n width: calc(100% + var(--ds-arrow-size, 8px) * 2);\n left: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n var(--ds-arrow-size, 8px) 0,\n calc(100% - var(--ds-arrow-size, 8px)) 0,\n 100% 50%,\n calc(100% - var(--ds-arrow-size, 8px)) 100%,\n var(--ds-arrow-size, 8px) 100%,\n 0 50%\n );\n}\n\n/* \u2500\u2500 @position-try rules \u2500\u2500\n * One rule per possible position-area value.\n * Each sets the position-area and the correct margin direction\n * so the arrow on the anchor-facing side is revealed.\n *\n * Margin direction per edge:\n * block-start (above anchor) \u2192 margin-block-end (bottom, facing anchor)\n * block-end (below anchor) \u2192 margin-block-start (top, facing anchor)\n * inline-start (left of anchor) \u2192 margin-inline-end (right, facing anchor)\n * inline-end (right of anchor) \u2192 margin-inline-start (left, facing anchor)\n */\n\n/* Each rule sets two margins: the anchor-facing margin for the gap\n (via --ds-arrow-size) and one cross-axis margin for the shift\n (via one of --ds-cross-axis-shift-margin-start, --ds-cross-axis-shift-margin-end,\n --ds-cross-axis-shift-margin-block-start, or --ds-cross-axis-shift-margin-block-end). The\n side matches the popover's anchored cross-axis edge per align. Only\n one of the four properties is non-zero at a time; useAnchorPosition\n picks it. See notes/architecture/positioning.md for details. */\n/* Single-axis centered (align: center) */\n@position-try --ds-arrow-block-start {\n position-area: block-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end {\n position-area: block-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-inline-start {\n position-area: inline-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end {\n position-area: inline-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n\n/* Block edge + inline span (align: start uses span-inline-end; align: end uses span-inline-start) */\n@position-try --ds-arrow-block-start-span-inline-start {\n position-area: block-start span-inline-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-start-span-inline-end {\n position-area: block-start span-inline-end;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-start {\n position-area: block-end span-inline-start;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-end {\n position-area: block-end span-inline-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n\n/* Inline edge + block span (align: start uses span-block-end; align: end uses span-block-start) */\n@position-try --ds-arrow-inline-start-span-block-start {\n position-area: inline-start span-block-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-start-span-block-end {\n position-area: inline-start span-block-end;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-start {\n position-area: inline-end span-block-start;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-end {\n position-area: inline-end span-block-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n";
24
+
25
+ // ── Placement utilities ──
26
+
27
+ /**
28
+ * Converts a `position-area` value to a CSS rule name by replacing spaces with hyphens
29
+ * and prepending `--ds-arrow-`.
30
+ *
31
+ * @example positionAreaToRuleName('block-end span-inline-start') → '--ds-arrow-block-end-span-inline-start'
32
+ */
33
+ function positionAreaToRuleName(_ref) {
34
+ var positionArea = _ref.positionArea;
35
+ return "--ds-arrow-".concat(positionArea.replace(/ /g, '-'));
36
+ }
37
+
38
+ /**
39
+ * Returns `position-try-fallbacks` using named `@position-try` rules
40
+ * that change both `position-area` and `margin` when flipping.
41
+ *
42
+ * This is required for the arrow technique because built-in keywords
43
+ * (`flip-block`, `flip-inline`) only flip `position-area` - they do
44
+ * not update the margin direction needed to reveal the correct arrow.
45
+ */
46
+ function getTryFallbacks(_ref2) {
47
+ var placement = _ref2.placement;
48
+ var _getPlacement = getPlacement({
49
+ placement: placement
50
+ }),
51
+ axis = _getPlacement.axis,
52
+ edge = _getPlacement.edge,
53
+ align = _getPlacement.align;
54
+ if (align === 'center') {
55
+ // Single-axis: only flip the edge
56
+ var _flippedEdge = edge === 'start' ? 'end' : 'start';
57
+ return positionAreaToRuleName({
58
+ positionArea: placementToPositionArea({
59
+ placement: {
60
+ axis: axis,
61
+ edge: _flippedEdge
62
+ }
63
+ })
64
+ });
65
+ }
66
+
67
+ // Compound placement: try flipping edge, alignment, and both
68
+ var flippedEdge = edge === 'start' ? 'end' : 'start';
69
+ var flippedAlign = align === 'start' ? 'end' : 'start';
70
+ return [positionAreaToRuleName({
71
+ positionArea: placementToPositionArea({
72
+ placement: {
73
+ axis: axis,
74
+ edge: flippedEdge,
75
+ align: align
76
+ }
77
+ })
78
+ }), positionAreaToRuleName({
79
+ positionArea: placementToPositionArea({
80
+ placement: {
81
+ axis: axis,
82
+ edge: edge,
83
+ align: flippedAlign
84
+ }
85
+ })
86
+ }), positionAreaToRuleName({
87
+ positionArea: placementToPositionArea({
88
+ placement: {
89
+ axis: axis,
90
+ edge: flippedEdge,
91
+ align: flippedAlign
92
+ }
93
+ })
94
+ })].join(', ');
95
+ }
96
+
97
+ /**
98
+ * Creates an arrow preset for popover content.
99
+ *
100
+ * The returned object is passed to `PopoverContent`'s `arrow` prop.
101
+ * It carries the CSS string and fallback logic so that consumers who
102
+ * do not use arrows pay no bundle cost.
103
+ *
104
+ * The arrows inherit their background from the popover element.
105
+ * When `arrow` is active, `PopoverContent` does not reset `background`
106
+ * to `transparent`, allowing the UA default (`canvas`) or a consumer-set
107
+ * background to show through.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * import { arrow } from '@atlaskit/top-layer/arrow';
112
+ *
113
+ * const myArrow = arrow();
114
+ *
115
+ * <Popup.Content arrow={myArrow} />
116
+ * ```
117
+ */
118
+ export var arrow = once(function () {
119
+ return {
120
+ name: 'arrow',
121
+ css: ARROW_CSS,
122
+ getTryFallbacks: getTryFallbacks
123
+ };
124
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -16,7 +16,7 @@ var styles = {
16
16
  };
17
17
 
18
18
  /**
19
- * Low-level `<dialog>` primitive. No visual opinions — no width, height,
19
+ * Low-level `<dialog>` primitive. No visual opinions - no width, height,
20
20
  * background, border-radius, or layout. Consumers provide their own styling.
21
21
  *
22
22
  * Visibility is controlled declaratively via `isOpen`:
@@ -27,6 +27,10 @@ var styles = {
27
27
  *
28
28
  * Close flow: we never call `dialog.close()` from event handlers. We always call
29
29
  * `onClose`; the consumer decides whether to set `isOpen={false}`.
30
+ *
31
+ * Accessibility: render at least one focusable element (typically a close
32
+ * button) when `isOpen` becomes `true`. Tab is always trapped inside the
33
+ * dialog, so keyboard users need somewhere to land.
30
34
  */
31
35
  export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
32
36
  var children = _ref.children,
@@ -132,18 +136,19 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
132
136
  return /*#__PURE__*/React.createElement("dialog", _extends({
133
137
  ref: combinedRef,
134
138
  id: dialogId
135
- // Modern browsers infer modal semantics from `.showModal()` but
136
- // some assistive tech still keys off `aria-modal="true"`. Setting
137
- // it explicitly is recommended by WAI-ARIA APG dialog pattern.
139
+ // `aria-modal` is intentionally NOT set: native `<dialog>.showModal()`
140
+ // already conveys modal semantics to assistive tech, and double-
141
+ // declaring it forecloses non-modal use cases (consumers calling
142
+ // `.show()` would still appear modal). Modern AT (NVDA / JAWS /
143
+ // VoiceOver) infer modality from the platform accessibility API.
138
144
  ,
139
- "aria-modal": "true",
140
145
  "aria-label": label,
141
146
  "aria-labelledby": label ? undefined : labelledBy,
142
147
  // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
143
148
  style: style,
144
149
  onCancel: handleCancel,
145
150
  "data-testid": testId
146
- }, preset ? _defineProperty({}, "data-ds-".concat(preset.name), '') : undefined, {
151
+ }, preset ? _defineProperty({}, "data-ds-dialog-".concat(preset.name), '') : undefined, {
147
152
  className: ax([styles.dialog])
148
153
  }), shouldHideBackdrop &&
149
154
  /*#__PURE__*/
@@ -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
  var selectors = function () {
14
15
  // Common exclusion filter applied to every selector.
15
16
  var not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
@@ -17,9 +18,54 @@ var selectors = function () {
17
18
  var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled).concat(not)];
18
19
  return {
19
20
  focusable: focusable.join(','),
20
- focused: focusable.join(':focus,') + ':focus'
21
+ // Build the `:focus` selector
22
+ focused: focusable.map(function (selector) {
23
+ return "".concat(selector, ":focus");
24
+ }).join(',')
21
25
  };
22
26
  }();
27
+ var topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]';
28
+
29
+ /**
30
+ * Returns true when `element` and `container` resolve to the same nearest
31
+ * top-layer host (or both resolve to `null`, meaning the document layer).
32
+ */
33
+ function isInSameLayer(_ref) {
34
+ var element = _ref.element,
35
+ container = _ref.container;
36
+ var elementLayer = element.closest(topLayerSelector);
37
+ var containerLayer = container.closest(topLayerSelector);
38
+ // Both elements are in the "document" layer
39
+ if (!elementLayer && !containerLayer) {
40
+ return true;
41
+ }
42
+ return elementLayer === containerLayer;
43
+ }
44
+
45
+ /**
46
+ * Returns `true` when `document.activeElement` is inside a nested top-layer
47
+ * descendant of `container` (a `[popover]`, `<dialog>`, `[role="dialog"]` or
48
+ * `[role="alertdialog"]` that is itself a descendant of `container`).
49
+ */
50
+ export function isNestedLayerFocused(_ref2) {
51
+ var _getDocument;
52
+ var container = _ref2.container;
53
+ var 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: container
67
+ });
68
+ }
23
69
 
24
70
  /**
25
71
  * An optional callback to restrict which focusable elements are included.
@@ -29,17 +75,21 @@ var selectors = function () {
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
- function getFocusables(_ref) {
39
- var container = _ref.container,
40
- filter = _ref.filter;
83
+ function getFocusables(_ref3) {
84
+ var container = _ref3.container,
85
+ filter = _ref3.filter;
41
86
  return Array.from(container.querySelectorAll(selectors.focusable)).filter(function (element) {
42
87
  return element instanceof HTMLElement;
88
+ }).filter(function (element) {
89
+ return isInSameLayer({
90
+ element: element,
91
+ container: container
92
+ });
43
93
  }).filter(function (element) {
44
94
  return !filter || filter(element, container);
45
95
  });
@@ -48,9 +98,10 @@ function getFocusables(_ref) {
48
98
  /**
49
99
  * Returns the first focusable element within the container.
50
100
  */
51
- export function getFirstFocusable(_ref2) {
52
- var container = _ref2.container,
53
- filter = _ref2.filter;
101
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
102
+ export function getFirstFocusable(_ref4) {
103
+ var container = _ref4.container,
104
+ filter = _ref4.filter;
54
105
  var items = getFocusables({
55
106
  container: container,
56
107
  filter: filter
@@ -62,9 +113,9 @@ export function getFirstFocusable(_ref2) {
62
113
  * Returns the last focusable element within the container.
63
114
  */
64
115
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
65
- export function getLastFocusable(_ref3) {
66
- var container = _ref3.container,
67
- filter = _ref3.filter;
116
+ export function getLastFocusable(_ref5) {
117
+ var container = _ref5.container,
118
+ filter = _ref5.filter;
68
119
  var items = getFocusables({
69
120
  container: container,
70
121
  filter: filter
@@ -80,11 +131,11 @@ export function getLastFocusable(_ref3) {
80
131
  * the focused element is not in the list of focusable elements.
81
132
  */
82
133
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
83
- export function getNextFocusable(_ref4) {
134
+ export function getNextFocusable(_ref6) {
84
135
  var _all$nextIndex;
85
- var container = _ref4.container,
86
- direction = _ref4.direction,
87
- filter = _ref4.filter;
136
+ var container = _ref6.container,
137
+ direction = _ref6.direction,
138
+ filter = _ref6.filter;
88
139
  var all = getFocusables({
89
140
  container: container,
90
141
  filter: filter
package/dist/esm/index.js CHANGED
@@ -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,32 @@
1
+ var 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(_ref) {
8
+ var _cacheForElement$get;
9
+ var element = _ref.element,
10
+ value = _ref.value;
11
+ if (!canCacheResolvedLength(value)) {
12
+ return null;
13
+ }
14
+ var cacheForElement = elementCaches.get(element);
15
+ return (_cacheForElement$get = cacheForElement === null || cacheForElement === void 0 ? void 0 : cacheForElement.get(value)) !== null && _cacheForElement$get !== void 0 ? _cacheForElement$get : null;
16
+ }
17
+ function cacheResolvedLength(_ref2) {
18
+ var _elementCaches$get;
19
+ var element = _ref2.element,
20
+ value = _ref2.value,
21
+ result = _ref2.result;
22
+ if (!canCacheResolvedLength(value)) {
23
+ return;
24
+ }
25
+ var cacheForElement = (_elementCaches$get = elementCaches.get(element)) !== null && _elementCaches$get !== void 0 ? _elementCaches$get : new Map();
26
+ cacheForElement.set(value, result);
27
+ elementCaches.set(element, cacheForElement);
28
+ }
29
+
1
30
  /**
2
31
  * Resolves a CSS length value to a pixel number using a hidden DOM probe.
3
32
  *
@@ -8,27 +37,39 @@
8
37
  * the browser do the math.
9
38
  *
10
39
  * 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).
40
+ * block, font size, and custom-property scope as the popover.
14
41
  *
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.
42
+ * Cost: one synchronous reflow on first cacheable resolution per
43
+ * (container, value) pair, then a `WeakMap` lookup. Resolutions involving
44
+ * `var(` are intentionally not cached.
18
45
  */
19
- export function resolveCssLengthToPixels(_ref) {
20
- var value = _ref.value,
21
- container = _ref.container;
46
+ export function resolveCssLengthToPixels(_ref3) {
47
+ var value = _ref3.value,
48
+ container = _ref3.container;
22
49
  if (typeof value === 'number') {
23
50
  return value;
24
51
  }
25
52
 
53
+ // Fast path: unitless zero is universally zero pixels - accept `'0'` and
54
+ // `'-0'` without a probe. (Consumers occasionally pass `'0'` instead of
55
+ // the number `0`.)
56
+ if (value === '0' || value === '-0') {
57
+ return 0;
58
+ }
59
+
26
60
  // Fast path: bare `${n}px` strings come from the consumer normalisation
27
61
  // in `getPlacement`. Avoid a reflow when we can.
28
62
  var pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
29
63
  if (pxMatch) {
30
64
  return Number(pxMatch[1]);
31
65
  }
66
+ var cached = getCachedResolvedLength({
67
+ element: container,
68
+ value: value
69
+ });
70
+ if (cached !== null) {
71
+ return cached;
72
+ }
32
73
  var probe = container.ownerDocument.createElement('div');
33
74
  probe.style.position = 'absolute';
34
75
  probe.style.visibility = 'hidden';
@@ -40,8 +81,18 @@ export function resolveCssLengthToPixels(_ref) {
40
81
  // preserved. `getComputedStyle` returns the resolved pixel value for
41
82
  // margin-* properties even when the source was a token / calc / var.
42
83
  probe.style.marginLeft = value;
84
+ // Hide the probe from assistive tech and event targeting; defensive
85
+ // against `getElementsByTagName('div')` walks in consumer code.
86
+ probe.setAttribute('aria-hidden', 'true');
87
+ probe.setAttribute('inert', '');
43
88
  container.appendChild(probe);
44
89
  var resolved = parseFloat(getComputedStyle(probe).marginLeft);
45
90
  container.removeChild(probe);
46
- return Number.isFinite(resolved) ? resolved : 0;
91
+ var result = Number.isFinite(resolved) ? resolved : 0;
92
+ cacheResolvedLength({
93
+ element: container,
94
+ value: value,
95
+ result: result
96
+ });
97
+ return result;
47
98
  }
@@ -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
  }