@atlaskit/top-layer 0.15.0 → 0.17.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 (66) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +4 -4
  3. package/dist/cjs/animations/presets.js +96 -9
  4. package/dist/cjs/dialog/dialog-content.js +33 -4
  5. package/dist/cjs/dialog-scroll-lock/index.js +17 -7
  6. package/dist/cjs/entry-points/animations.js +12 -0
  7. package/dist/cjs/entry-points/use-popover-id.js +3 -1
  8. package/dist/cjs/internal/get-aria-for-trigger.js +8 -3
  9. package/dist/cjs/internal/use-anchor-position-at-point.js +4 -2
  10. package/dist/cjs/internal/use-anchor-position.js +3 -2
  11. package/dist/cjs/internal/use-animated-visibility.js +233 -155
  12. package/dist/cjs/internal/use-focus-wrap.js +5 -3
  13. package/dist/cjs/internal/use-initial-focus.js +196 -50
  14. package/dist/cjs/internal/use-width-from-anchor.js +5 -4
  15. package/dist/cjs/popover/popover.js +58 -9
  16. package/dist/es2019/animations/presets.js +145 -2
  17. package/dist/es2019/dialog/dialog-content.js +33 -4
  18. package/dist/es2019/dialog-scroll-lock/index.js +18 -8
  19. package/dist/es2019/entry-points/animations.js +1 -1
  20. package/dist/es2019/entry-points/use-popover-id.js +3 -1
  21. package/dist/es2019/internal/get-aria-for-trigger.js +8 -3
  22. package/dist/es2019/internal/use-anchor-position-at-point.js +4 -2
  23. package/dist/es2019/internal/use-anchor-position.js +3 -2
  24. package/dist/es2019/internal/use-animated-visibility.js +222 -149
  25. package/dist/es2019/internal/use-focus-wrap.js +5 -4
  26. package/dist/es2019/internal/use-initial-focus.js +189 -45
  27. package/dist/es2019/internal/use-width-from-anchor.js +5 -4
  28. package/dist/es2019/popover/popover.js +59 -10
  29. package/dist/esm/animations/presets.js +93 -9
  30. package/dist/esm/dialog/dialog-content.js +33 -4
  31. package/dist/esm/dialog-scroll-lock/index.js +17 -8
  32. package/dist/esm/entry-points/animations.js +1 -1
  33. package/dist/esm/entry-points/use-popover-id.js +3 -1
  34. package/dist/esm/internal/get-aria-for-trigger.js +8 -3
  35. package/dist/esm/internal/use-anchor-position-at-point.js +4 -2
  36. package/dist/esm/internal/use-anchor-position.js +3 -2
  37. package/dist/esm/internal/use-animated-visibility.js +235 -157
  38. package/dist/esm/internal/use-focus-wrap.js +5 -4
  39. package/dist/esm/internal/use-initial-focus.js +196 -51
  40. package/dist/esm/internal/use-width-from-anchor.js +5 -4
  41. package/dist/esm/popover/popover.js +59 -10
  42. package/dist/types/animations/presets.d.ts +16 -0
  43. package/dist/types/dialog/types.d.ts +9 -1
  44. package/dist/types/dialog-scroll-lock/index.d.ts +22 -6
  45. package/dist/types/entry-points/animations.d.ts +1 -1
  46. package/dist/types/internal/get-aria-for-trigger.d.ts +8 -1
  47. package/dist/types/internal/use-anchor-position-at-point.d.ts +6 -1
  48. package/dist/types/internal/use-anchor-position.d.ts +14 -1
  49. package/dist/types/internal/use-animated-visibility.d.ts +48 -19
  50. package/dist/types/internal/use-focus-wrap.d.ts +6 -1
  51. package/dist/types/internal/use-initial-focus.d.ts +55 -4
  52. package/dist/types/internal/use-width-from-anchor.d.ts +3 -2
  53. package/dist/types/popover/types.d.ts +13 -2
  54. package/dist/types-ts4.5/animations/presets.d.ts +16 -0
  55. package/dist/types-ts4.5/dialog/types.d.ts +9 -1
  56. package/dist/types-ts4.5/dialog-scroll-lock/index.d.ts +22 -6
  57. package/dist/types-ts4.5/entry-points/animations.d.ts +1 -1
  58. package/dist/types-ts4.5/internal/get-aria-for-trigger.d.ts +8 -1
  59. package/dist/types-ts4.5/internal/use-anchor-position-at-point.d.ts +6 -1
  60. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +14 -1
  61. package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +48 -19
  62. package/dist/types-ts4.5/internal/use-focus-wrap.d.ts +6 -1
  63. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +55 -4
  64. package/dist/types-ts4.5/internal/use-width-from-anchor.d.ts +3 -2
  65. package/dist/types-ts4.5/popover/types.d.ts +13 -2
  66. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`2fd74328732bf`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2fd74328732bf) -
8
+ Add dialogMotion animation preset with token-based motion values and backdrop animation
9
+
10
+ ## 0.16.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [`6d0485dce81c4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6d0485dce81c4) -
15
+ `Popover` and `Dialog` now unmount their host element after the exit animation completes, instead
16
+ of leaving an empty `role` / `aria-*` element in the accessibility tree.
17
+
18
+ Other small changes that fall out of this:
19
+ - The popover/dialog `id` stays stable across opens.
20
+ - `ref.current` is only populated while the host element is rendered. Read the ref inside
21
+ `onOpenChange` / `onEnterFinish`, or gate the read on `isOpen`.
22
+ - `onEnterFinish` and `onExitFinish` now fire before the internal phase transition that drives the
23
+ unmount, so consumers reading the host element (or a forwarded ref) inside those callbacks
24
+ always see it attached to the DOM, regardless of React batching semantics.
25
+ - `getAriaForTrigger` returns `aria-controls: undefined` while `isOpen` is `false`; spreading the
26
+ result onto a JSX element strips the attribute from the DOM and avoids a dangling reference.
27
+ - `useAnchorPosition`, `useAnchorPositionAtPoint`, and `useWidthFromAnchor` now require `isOpen`
28
+ so positioning re-runs against the freshly mounted host element on each open.
29
+ `useWidthFromAnchor` also requires `anchorRef`.
30
+ - `DialogScrollLock` now requires an `isOpen` prop so the scroll lock is released the moment
31
+ `isOpen` flips to `false`, regardless of React's unmount timing.
32
+ - `useInitialFocus` now follows the WAI-ARIA APG Combobox Pattern: when a `menu` or `listbox`
33
+ popup is controlled by a focused combobox-like textbox whose `aria-controls` (or legacy
34
+ `aria-owns`) references the popup, initial focus stays on the textbox instead of moving into the
35
+ popup. Navigation is expected to be proxied via `aria-activedescendant`. Both variants are
36
+ detected:
37
+ - **In-popup combobox**: the focused textbox lives inside the popup.
38
+ - **External combobox**: the focused textbox is rendered OUTSIDE the popup (for example
39
+ `react-select`-style menus that portal the listbox). The carve-out also accepts the case where
40
+ the textbox's `aria-controls` references a DESCENDANT of the popup host (such as
41
+ react-select's inner `MenuList` element). Without this carve-out, moving focus into the
42
+ listbox would blur the external textbox and cause libraries like `react-select` to close the
43
+ menu on the same interaction that opened it.
44
+
45
+ Popups that are not opened from a combobox-like trigger continue to receive their
46
+ role-appropriate initial focus.
47
+
3
48
  ## 0.15.0
4
49
 
5
50
  ### Minor Changes
package/README.md CHANGED
@@ -55,8 +55,8 @@ function MyPopup({ isOpen, onClose }: { isOpen: boolean; onClose: () => void })
55
55
  }
56
56
  ```
57
57
 
58
- - Focus restoration is automatic. The browser handles it for outermost popovers; `Popover`
59
- snapshots `document.activeElement` on open and restores it on close for nested cases.
58
+ - Focus restoration is automatic. The browser handles it for outermost popovers; `Popover` snapshots
59
+ `document.activeElement` on open and restores it on close for nested cases.
60
60
  - For default overlay styling (background, shadow, border-radius), wrap content in `PopoverSurface`
61
61
  from `@atlaskit/top-layer/popover-surface`.
62
62
  - For trigger-less or custom-positioned UI (flag, tooltip, react-select menu portal), use `Popover`
@@ -88,8 +88,8 @@ Compound component for modal dialogs using the native `<dialog>` element with `.
88
88
  ## Animations
89
89
 
90
90
  Both `Popover` and `Dialog` support CSS-based entry/exit animations via the `animate` prop.
91
- Animations use
92
- [`@starting-style`](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style) for entry and
91
+ Animations use [`@starting-style`](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style)
92
+ for entry and
93
93
  [`allow-discrete`](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior) on
94
94
  `display`/`overlay` for exit — no JavaScript animation coordination required.
95
95
 
@@ -4,23 +4,80 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.dialogFade = dialogFade;
7
+ exports.dialogMotion = dialogMotion;
7
8
  exports.dialogSlideUpAndFade = dialogSlideUpAndFade;
8
9
  exports.fade = fade;
10
+ exports.popupMotion = popupMotion;
9
11
  exports.scaleAndFade = scaleAndFade;
10
12
  exports.slideAndFade = slideAndFade;
11
13
  var _resolvePlacement = require("../internal/resolve-placement");
12
- // slideAndFade
14
+ // ══════════════════════════════════════════════════════════════════════════════
15
+ // Popover presets
16
+ // ══════════════════════════════════════════════════════════════════════════════
13
17
 
14
- function getSlideAndFadeProperties(_ref) {
15
- var placement = _ref.placement,
16
- distance = _ref.distance;
17
- var pos = "".concat(distance, "px");
18
- var neg = "-".concat(distance, "px");
18
+ // ── popupMotion ──
19
+
20
+ var POPUP_MOTION_CSS = "\n [data-ds-popover-popup-motion] {\n animation: var(--ds-popover-motion-exit);\n animation-fill-mode: forwards;\n transition:\n overlay 150ms allow-discrete,\n display 150ms allow-discrete;\n }\n\n [data-ds-popover-popup-motion]:popover-open {\n animation: var(--ds-popover-motion-enter);\n animation-fill-mode: backwards;\n }\n\n @media (prefers-reduced-motion: reduce) {\n [data-ds-popover-popup-motion],\n [data-ds-popover-popup-motion]:popover-open {\n animation: none;\n transition-duration: 0s;\n }\n }\n";
21
+ function getPopupMotionProperties(_ref) {
22
+ var placement = _ref.placement;
19
23
  var _getPlacement = (0, _resolvePlacement.getPlacement)({
20
24
  placement: placement
21
25
  }),
22
26
  axis = _getPlacement.axis,
23
27
  edge = _getPlacement.edge;
28
+ if (axis === 'block' && edge === 'start') {
29
+ return {
30
+ '--ds-popover-motion-enter': "var(--ds-popup-enter-top, 150ms cubic-bezier(0.4, 1, 0.6, 1) SlideInTop8px, 150ms cubic-bezier(0.4, 1, 0.6, 1) FadeIn0to100)",
31
+ '--ds-popover-motion-exit': "var(--ds-popup-exit-top, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) SlideOutTop8px, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)"
32
+ };
33
+ }
34
+ if (axis === 'inline' && edge === 'start') {
35
+ return {
36
+ '--ds-popover-motion-enter': "var(--ds-popup-enter-left, 150ms cubic-bezier(0.4, 1, 0.6, 1) SlideInLeft8px, 150ms cubic-bezier(0.4, 1, 0.6, 1) FadeIn0to100)",
37
+ '--ds-popover-motion-exit': "var(--ds-popup-exit-left, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) SlideOutLeft8px, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)"
38
+ };
39
+ }
40
+ if (axis === 'inline' && edge === 'end') {
41
+ return {
42
+ '--ds-popover-motion-enter': "var(--ds-popup-enter-right, 150ms cubic-bezier(0.4, 1, 0.6, 1) SlideInRight8px, 150ms cubic-bezier(0.4, 1, 0.6, 1) FadeIn0to100)",
43
+ '--ds-popover-motion-exit': "var(--ds-popup-exit-right, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) SlideOutRight8px, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)"
44
+ };
45
+ }
46
+ // Default: block/end (popover below trigger)
47
+ return {
48
+ '--ds-popover-motion-enter': "var(--ds-popup-enter-bottom, 150ms cubic-bezier(0.4, 1, 0.6, 1) SlideInBottom8px, 150ms cubic-bezier(0.4, 1, 0.6, 1) FadeIn0to100)",
49
+ '--ds-popover-motion-exit': "var(--ds-popup-exit-bottom, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) SlideOutBottom8px, 100ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)"
50
+ };
51
+ }
52
+
53
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
54
+ function popupMotion() {
55
+ return {
56
+ name: 'popup-motion',
57
+ css: POPUP_MOTION_CSS,
58
+ enterDurationMs: 150,
59
+ exitDurationMs: 100,
60
+ getProperties: function getProperties(_ref2) {
61
+ var placement = _ref2.placement;
62
+ return getPopupMotionProperties({
63
+ placement: placement
64
+ });
65
+ }
66
+ };
67
+ }
68
+
69
+ // ── slideAndFade ──
70
+
71
+ function getSlideAndFadeProperties(_ref3) {
72
+ var placement = _ref3.placement,
73
+ distance = _ref3.distance;
74
+ var pos = "".concat(distance, "px");
75
+ var neg = "-".concat(distance, "px");
76
+ var _getPlacement2 = (0, _resolvePlacement.getPlacement)({
77
+ placement: placement
78
+ }),
79
+ axis = _getPlacement2.axis,
80
+ edge = _getPlacement2.edge;
24
81
  if (axis === 'block' && edge === 'start') {
25
82
  return {
26
83
  '--ds-popover-tx': '0',
@@ -73,14 +130,15 @@ var SLIDE_AND_FADE_CSS = "\n[data-ds-popover-slide-and-fade] {\n opacity: 0;\n
73
130
  * <Popover animate={slideAndFade({ distance: 8 })} />
74
131
  * ```
75
132
  */
133
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
76
134
  function slideAndFade(options) {
77
135
  var _options$distance;
78
136
  var distance = (_options$distance = options === null || options === void 0 ? void 0 : options.distance) !== null && _options$distance !== void 0 ? _options$distance : 4;
79
137
  return {
80
138
  name: 'slide-and-fade',
81
139
  css: SLIDE_AND_FADE_CSS,
82
- getProperties: function getProperties(_ref2) {
83
- var placement = _ref2.placement;
140
+ getProperties: function getProperties(_ref4) {
141
+ var placement = _ref4.placement;
84
142
  return getSlideAndFadeProperties({
85
143
  placement: placement,
86
144
  distance: distance
@@ -147,7 +205,36 @@ function scaleAndFade() {
147
205
  // Dialog presets
148
206
  // ══════════════════════════════════════════════════════════════════════════════
149
207
 
150
- // dialogSlideUpAndFade
208
+ // ── dialogMotion ──
209
+
210
+ var DIALOG_MOTION_CSS = "\n [data-ds-dialog-motion] {\n animation: ".concat("var(--ds-modal-exit, 200ms cubic-bezier(0.6, 0, 0.8, 0.6) ScaleOut100to95)", ";\n animation-fill-mode: forwards;\n transition:\n overlay 200ms allow-discrete,\n display 200ms allow-discrete;\n }\n\n [data-ds-dialog-motion][open] {\n animation: ", "var(--ds-modal-enter, 250ms cubic-bezier(0.4, 0, 0, 1) ScaleIn95to100)", ";\n animation-fill-mode: backwards;\n }\n\n [data-ds-dialog-motion]::backdrop {\n animation: ", "var(--ds-blanket-exit, 200ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)", ";\n animation-fill-mode: forwards;\n background-color: var(--ds-blanket, #050C1F75);\n transition:\n overlay 200ms allow-discrete,\n display 200ms allow-discrete;\n }\n\n [data-ds-dialog-motion][open]::backdrop {\n animation: ", "var(--ds-blanket-enter, 250ms cubic-bezier(0.4, 0, 0, 1) FadeIn0to100)", ";\n animation-fill-mode: backwards;\n }\n\n @media (prefers-reduced-motion: reduce) {\n [data-ds-dialog-motion],\n [data-ds-dialog-motion][open],\n [data-ds-dialog-motion]::backdrop,\n [data-ds-dialog-motion][open]::backdrop {\n animation: none;\n transition-duration: 0s;\n }\n }\n");
211
+
212
+ /**
213
+ * Dialog motion animation.
214
+ *
215
+ * Includes backdrop fade animation.
216
+ *
217
+ * @example
218
+ * ```tsx
219
+ * import { dialogMotion } from '@atlaskit/top-layer/animations';
220
+ *
221
+ * <Dialog animate={dialogMotion()} isOpen={isOpen} onClose={handleClose} label="...">
222
+ * ...
223
+ * </Dialog>
224
+ * ```
225
+ */
226
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
227
+ function dialogMotion() {
228
+ // Bare preset name; `Dialog` prefixes it via `data-ds-dialog-{name}`.
229
+ return {
230
+ name: 'motion',
231
+ css: DIALOG_MOTION_CSS,
232
+ enterDurationMs: 250,
233
+ exitDurationMs: 200
234
+ };
235
+ }
236
+
237
+ // ── dialogSlideUpAndFade ──
151
238
 
152
239
  var DIALOG_SLIDE_UP_AND_FADE_CSS = "\n[data-ds-dialog-slide-up-and-fade] {\n opacity: 0;\n transform: translateY(calc(-1 * var(--ds-dialog-ty, 12px)));\n transition:\n opacity 175ms cubic-bezier(0.15, 1, 0.3, 1),\n transform 175ms cubic-bezier(0.15, 1, 0.3, 1),\n overlay 175ms allow-discrete,\n display 175ms allow-discrete;\n}\n\n[data-ds-dialog-slide-up-and-fade][open] {\n opacity: 1;\n transform: none;\n transition-duration: 350ms;\n}\n\n@starting-style {\n [data-ds-dialog-slide-up-and-fade][open] {\n opacity: 0;\n transform: translateY(var(--ds-dialog-ty, 12px));\n }\n}\n\n[data-ds-dialog-slide-up-and-fade]::backdrop {\n background-color: transparent;\n transition:\n background-color 175ms cubic-bezier(0.15, 1, 0.3, 1),\n overlay 175ms allow-discrete,\n display 175ms allow-discrete;\n}\n\n[data-ds-dialog-slide-up-and-fade][open]::backdrop {\n background-color: var(--ds-blanket, #050C1F75);\n transition-duration: 350ms;\n}\n\n@starting-style {\n [data-ds-dialog-slide-up-and-fade][open]::backdrop {\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n [data-ds-dialog-slide-up-and-fade],\n [data-ds-dialog-slide-up-and-fade][open],\n [data-ds-dialog-slide-up-and-fade]::backdrop,\n [data-ds-dialog-slide-up-and-fade][open]::backdrop {\n transition-duration: 0s;\n }\n}\n";
153
240
 
@@ -65,16 +65,30 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
65
65
  elementRef: ownRef,
66
66
  onExitFinish: onExitFinish
67
67
  }),
68
- showChildren = _useAnimatedVisibilit.showChildren,
68
+ phase = _useAnimatedVisibilit.phase,
69
69
  preset = _useAnimatedVisibilit.preset;
70
70
 
71
+ // Pre-computed phase predicate. `isVisible` means "host element is
72
+ // mounted and on screen" (any phase except `closed`), used as a dep
73
+ // signal for the backdrop-click rebind effect so it re-attaches to
74
+ // the fresh <dialog> element after a close → reopen unmount/remount
75
+ // cycle.
76
+ var isVisible = phase !== 'closed';
77
+
71
78
  // Focus wrap
72
79
  // Native <dialog>.showModal() traps focus but wraps through <body> at
73
80
  // the boundary (A → B → C → body → A). This hook intercepts Tab to
74
81
  // wrap directly (A → B → C → A), matching the WAI-ARIA APG pattern.
82
+ //
83
+ // Takes `phase` directly: the listener stays attached while
84
+ // `phase !== 'closed'`, i.e. through the animated-exit window.
85
+ // Gating on `isOpen` would unbind the listener at the start of an
86
+ // exit animation while the dialog is still visible, letting focus
87
+ // escape (WCAG 2.4.3 Focus Order regression).
75
88
  (0, _useFocusWrap.useFocusWrap)({
76
89
  elementRef: ownRef,
77
- role: 'dialog'
90
+ role: 'dialog',
91
+ phase: phase
78
92
  });
79
93
 
80
94
  // Open layer observer registration
@@ -120,6 +134,8 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
120
134
  // Attached via bind-event-listener rather than a React prop so we avoid
121
135
  // a11y lint suppressions on the <dialog> element.
122
136
  // Keyboard dismiss is already handled natively (Escape → onCancel above).
137
+ // `isVisible` is in deps so the listener re-binds to the new <dialog>
138
+ // element after a host unmount / remount cycle.
123
139
  (0, _react.useEffect)(function () {
124
140
  var dialog = ownRef.current;
125
141
  if (!dialog) {
@@ -135,13 +151,26 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
135
151
  }
136
152
  }
137
153
  });
138
- }, [onClose]);
154
+ }, [onClose, isVisible]);
139
155
 
140
156
  // Atomic CSS (Compiled) deduplicates ::backdrop { background-color }
141
157
  // into a single class, making it impossible to toggle between two values
142
158
  // via cssMap entries. An ID-scoped <style> has higher specificity than
143
159
  // any atomic class, so the override always wins.
144
160
  var escapedId = CSS.escape(dialogId);
161
+
162
+ // The `<dialog>` host element is only rendered while open or its exit
163
+ // animation is playing. After exit completes the phase returns to
164
+ // `closed` and we unmount the dialog entirely so it does not leave an
165
+ // empty `role="dialog"` element in the accessibility tree. The element
166
+ // re-mounts on the next `isOpen=true` commit; `showModal()` is called
167
+ // from the existing `useLayoutEffect([isOpen])` on that same commit,
168
+ // and listeners (cancel, click) re-bind via the `[onClose, isVisible]`
169
+ // effect re-running after remount (React clears the ref on unmount, so
170
+ // we need the listener effect to re-run with the fresh element).
171
+ if (!isVisible) {
172
+ return null;
173
+ }
145
174
  return /*#__PURE__*/_react.default.createElement("dialog", (0, _extends2.default)({
146
175
  ref: combinedRef,
147
176
  id: dialogId
@@ -162,5 +191,5 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
162
191
  }), shouldHideBackdrop &&
163
192
  /*#__PURE__*/
164
193
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
165
- _react.default.createElement("style", null, "#".concat(escapedId, "::backdrop{background-color:transparent}")), showChildren ? children : null);
194
+ _react.default.createElement("style", null, "#".concat(escapedId, "::backdrop{background-color:transparent}")), children);
166
195
  });
@@ -9,14 +9,20 @@ var _browserApis = require("@atlaskit/browser-apis");
9
9
  /**
10
10
  * __DialogScrollLock__
11
11
  *
12
- * Prevents background page scrolling while mounted. Designed to be rendered
13
- * inside a `Dialog` so that the scroll lock is automatically removed when the
14
- * dialog closes (unmounts its children).
12
+ * Prevents background page scrolling while a dialog is open. Designed to be
13
+ * rendered inside a `Dialog` and given the same `isOpen` value.
15
14
  *
16
15
  * Native `<dialog>.showModal()` makes background content _inert_ (not
17
16
  * focusable/clickable), but does **not** prevent scroll events from reaching
18
17
  * the document body. This component fills that gap by setting
19
- * `overflow: hidden` on `document.body`.
18
+ * `overflow: hidden` on `document.body` while `isOpen` is `true`.
19
+ *
20
+ * The lock is driven by `isOpen` rather than by mount/unmount so that release
21
+ * happens synchronously in the same commit that flips `isOpen` to `false`.
22
+ * Relying on unmount alone was racy: `Dialog` defers the unmount of its
23
+ * children until the host element's close/toggle event fires, which can be
24
+ * after a consumer or test observes the dialog as hidden, leading to a
25
+ * stale `overflow: hidden` window.
20
26
  *
21
27
  * @example
22
28
  * ```tsx
@@ -24,14 +30,18 @@ var _browserApis = require("@atlaskit/browser-apis");
24
30
  * import { DialogScrollLock } from '@atlaskit/top-layer/dialog-scroll-lock';
25
31
  *
26
32
  * <Dialog isOpen={isOpen} onClose={onClose}>
27
- * <DialogScrollLock />
33
+ * <DialogScrollLock isOpen={isOpen} />
28
34
  * {children}
29
35
  * </Dialog>
30
36
  * ```
31
37
  */
32
- function DialogScrollLock() {
38
+ function DialogScrollLock(_ref) {
39
+ var isOpen = _ref.isOpen;
33
40
  (0, _react.useLayoutEffect)(function () {
34
41
  var _getDocument;
42
+ if (!isOpen) {
43
+ return;
44
+ }
35
45
  var body = (_getDocument = (0, _browserApis.getDocument)()) === null || _getDocument === void 0 ? void 0 : _getDocument.body;
36
46
  if (!body) {
37
47
  return;
@@ -41,6 +51,6 @@ function DialogScrollLock() {
41
51
  return function () {
42
52
  body.style.overflow = previousOverflow;
43
53
  };
44
- }, []);
54
+ }, [isOpen]);
45
55
  return null;
46
56
  }
@@ -9,6 +9,12 @@ Object.defineProperty(exports, "dialogFade", {
9
9
  return _presets.dialogFade;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "dialogMotion", {
13
+ enumerable: true,
14
+ get: function get() {
15
+ return _presets.dialogMotion;
16
+ }
17
+ });
12
18
  Object.defineProperty(exports, "dialogSlideUpAndFade", {
13
19
  enumerable: true,
14
20
  get: function get() {
@@ -21,6 +27,12 @@ Object.defineProperty(exports, "fade", {
21
27
  return _presets.fade;
22
28
  }
23
29
  });
30
+ Object.defineProperty(exports, "popupMotion", {
31
+ enumerable: true,
32
+ get: function get() {
33
+ return _presets.popupMotion;
34
+ }
35
+ });
24
36
  Object.defineProperty(exports, "scaleAndFade", {
25
37
  enumerable: true,
26
38
  get: function get() {
@@ -16,6 +16,8 @@ var _useId = require("@atlaskit/ds-lib/use-id");
16
16
  * own `anchor-name` on the trigger element.
17
17
  */
18
18
  function usePopoverId() {
19
- // prefixing for debug clarity in the DOM
19
+ // `@atlaskit/ds-lib/use-id` already strips characters that React's
20
+ // `useId` produces (`:`, `«`, `»`) which are invalid in HTML ID
21
+ // tokens. The `popover-` prefix is purely for DOM debug clarity.
20
22
  return "popover-".concat((0, _useId.useId)());
21
23
  }
@@ -78,11 +78,16 @@ function getAriaForTrigger(_ref) {
78
78
  var ariaHasPopup = (0, _roleToAriaHasPopup.roleToAriaHasPopup)({
79
79
  role: role
80
80
  });
81
+ // `aria-controls` is set to `undefined` while closed. The `Popover` /
82
+ // `Dialog` primitives unmount their host element after the exit
83
+ // animation finishes, so a closed-state `aria-controls` would point
84
+ // at a node that is not in the DOM. Returning `undefined` here means
85
+ // JSX spread renders no `aria-controls` attribute on the trigger
86
+ // until the target exists, avoiding the dangling reference while
87
+ // keeping the relationship live whenever it is meaningful.
81
88
  return {
82
89
  'aria-haspopup': ariaHasPopup,
83
90
  'aria-expanded': isOpen,
84
- // Keep the trigger-to-popover relationship stable. `aria-expanded`
85
- // tells assistive technology whether the controlled popover is visible.
86
- 'aria-controls': popoverId
91
+ 'aria-controls': isOpen ? popoverId : undefined
87
92
  };
88
93
  }
@@ -75,7 +75,8 @@ function useAnchorPositionAtPoint(_ref) {
75
75
  placement = _ref.placement,
76
76
  getPoint = _ref.getPoint,
77
77
  _ref$isEnabled = _ref.isEnabled,
78
- isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled;
78
+ isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled,
79
+ isOpen = _ref.isOpen;
79
80
  var syntheticAnchorRef = (0, _react.useRef)(null);
80
81
 
81
82
  // `getPoint` is kept in a ref so the layout effect can call the
@@ -122,6 +123,7 @@ function useAnchorPositionAtPoint(_ref) {
122
123
  anchorRef: syntheticAnchorRef,
123
124
  popoverRef: popoverRef,
124
125
  placement: placement,
125
- isEnabled: isEnabled
126
+ isEnabled: isEnabled,
127
+ isOpen: isOpen
126
128
  });
127
129
  }
@@ -221,7 +221,8 @@ function useAnchorPosition(_ref3) {
221
221
  _ref3$forceFallbackPo = _ref3.forceFallbackPositioning,
222
222
  forceFallbackPositioning = _ref3$forceFallbackPo === void 0 ? false : _ref3$forceFallbackPo,
223
223
  _ref3$isEnabled = _ref3.isEnabled,
224
- isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled;
224
+ isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled,
225
+ isOpen = _ref3.isOpen;
225
226
  var id = (0, _react.useId)();
226
227
  // Stabilize `placement` object so the same object literal
227
228
  // value returns an object with the same reference
@@ -481,5 +482,5 @@ function useAnchorPosition(_ref3) {
481
482
  popover.style.removeProperty('opacity');
482
483
  });
483
484
  return undoPositioning;
484
- }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id]);
485
+ }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id, isOpen]);
485
486
  }