@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
@@ -56,16 +56,30 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
56
56
  elementRef: ownRef,
57
57
  onExitFinish: onExitFinish
58
58
  }),
59
- showChildren = _useAnimatedVisibilit.showChildren,
59
+ phase = _useAnimatedVisibilit.phase,
60
60
  preset = _useAnimatedVisibilit.preset;
61
61
 
62
+ // Pre-computed phase predicate. `isVisible` means "host element is
63
+ // mounted and on screen" (any phase except `closed`), used as a dep
64
+ // signal for the backdrop-click rebind effect so it re-attaches to
65
+ // the fresh <dialog> element after a close → reopen unmount/remount
66
+ // cycle.
67
+ var isVisible = phase !== 'closed';
68
+
62
69
  // Focus wrap
63
70
  // Native <dialog>.showModal() traps focus but wraps through <body> at
64
71
  // the boundary (A → B → C → body → A). This hook intercepts Tab to
65
72
  // wrap directly (A → B → C → A), matching the WAI-ARIA APG pattern.
73
+ //
74
+ // Takes `phase` directly: the listener stays attached while
75
+ // `phase !== 'closed'`, i.e. through the animated-exit window.
76
+ // Gating on `isOpen` would unbind the listener at the start of an
77
+ // exit animation while the dialog is still visible, letting focus
78
+ // escape (WCAG 2.4.3 Focus Order regression).
66
79
  useFocusWrap({
67
80
  elementRef: ownRef,
68
- role: 'dialog'
81
+ role: 'dialog',
82
+ phase: phase
69
83
  });
70
84
 
71
85
  // Open layer observer registration
@@ -111,6 +125,8 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
111
125
  // Attached via bind-event-listener rather than a React prop so we avoid
112
126
  // a11y lint suppressions on the <dialog> element.
113
127
  // Keyboard dismiss is already handled natively (Escape → onCancel above).
128
+ // `isVisible` is in deps so the listener re-binds to the new <dialog>
129
+ // element after a host unmount / remount cycle.
114
130
  useEffect(function () {
115
131
  var dialog = ownRef.current;
116
132
  if (!dialog) {
@@ -126,13 +142,26 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
126
142
  }
127
143
  }
128
144
  });
129
- }, [onClose]);
145
+ }, [onClose, isVisible]);
130
146
 
131
147
  // Atomic CSS (Compiled) deduplicates ::backdrop { background-color }
132
148
  // into a single class, making it impossible to toggle between two values
133
149
  // via cssMap entries. An ID-scoped <style> has higher specificity than
134
150
  // any atomic class, so the override always wins.
135
151
  var escapedId = CSS.escape(dialogId);
152
+
153
+ // The `<dialog>` host element is only rendered while open or its exit
154
+ // animation is playing. After exit completes the phase returns to
155
+ // `closed` and we unmount the dialog entirely so it does not leave an
156
+ // empty `role="dialog"` element in the accessibility tree. The element
157
+ // re-mounts on the next `isOpen=true` commit; `showModal()` is called
158
+ // from the existing `useLayoutEffect([isOpen])` on that same commit,
159
+ // and listeners (cancel, click) re-bind via the `[onClose, isVisible]`
160
+ // effect re-running after remount (React clears the ref on unmount, so
161
+ // we need the listener effect to re-run with the fresh element).
162
+ if (!isVisible) {
163
+ return null;
164
+ }
136
165
  return /*#__PURE__*/React.createElement("dialog", _extends({
137
166
  ref: combinedRef,
138
167
  id: dialogId
@@ -153,5 +182,5 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
153
182
  }), shouldHideBackdrop &&
154
183
  /*#__PURE__*/
155
184
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
156
- React.createElement("style", null, "#".concat(escapedId, "::backdrop{background-color:transparent}")), showChildren ? children : null);
185
+ React.createElement("style", null, "#".concat(escapedId, "::backdrop{background-color:transparent}")), children);
157
186
  });
@@ -1,17 +1,22 @@
1
1
  import { useLayoutEffect } from 'react';
2
2
  import { getDocument } from '@atlaskit/browser-apis';
3
-
4
3
  /**
5
4
  * __DialogScrollLock__
6
5
  *
7
- * Prevents background page scrolling while mounted. Designed to be rendered
8
- * inside a `Dialog` so that the scroll lock is automatically removed when the
9
- * dialog closes (unmounts its children).
6
+ * Prevents background page scrolling while a dialog is open. Designed to be
7
+ * rendered inside a `Dialog` and given the same `isOpen` value.
10
8
  *
11
9
  * Native `<dialog>.showModal()` makes background content _inert_ (not
12
10
  * focusable/clickable), but does **not** prevent scroll events from reaching
13
11
  * the document body. This component fills that gap by setting
14
- * `overflow: hidden` on `document.body`.
12
+ * `overflow: hidden` on `document.body` while `isOpen` is `true`.
13
+ *
14
+ * The lock is driven by `isOpen` rather than by mount/unmount so that release
15
+ * happens synchronously in the same commit that flips `isOpen` to `false`.
16
+ * Relying on unmount alone was racy: `Dialog` defers the unmount of its
17
+ * children until the host element's close/toggle event fires, which can be
18
+ * after a consumer or test observes the dialog as hidden, leading to a
19
+ * stale `overflow: hidden` window.
15
20
  *
16
21
  * @example
17
22
  * ```tsx
@@ -19,14 +24,18 @@ import { getDocument } from '@atlaskit/browser-apis';
19
24
  * import { DialogScrollLock } from '@atlaskit/top-layer/dialog-scroll-lock';
20
25
  *
21
26
  * <Dialog isOpen={isOpen} onClose={onClose}>
22
- * <DialogScrollLock />
27
+ * <DialogScrollLock isOpen={isOpen} />
23
28
  * {children}
24
29
  * </Dialog>
25
30
  * ```
26
31
  */
27
- export function DialogScrollLock() {
32
+ export function DialogScrollLock(_ref) {
33
+ var isOpen = _ref.isOpen;
28
34
  useLayoutEffect(function () {
29
35
  var _getDocument;
36
+ if (!isOpen) {
37
+ return;
38
+ }
30
39
  var body = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.body;
31
40
  if (!body) {
32
41
  return;
@@ -36,6 +45,6 @@ export function DialogScrollLock() {
36
45
  return function () {
37
46
  body.style.overflow = previousOverflow;
38
47
  };
39
- }, []);
48
+ }, [isOpen]);
40
49
  return null;
41
50
  }
@@ -1 +1 @@
1
- export { slideAndFade, fade, scaleAndFade, dialogSlideUpAndFade, dialogFade } from '../animations/presets';
1
+ export { dialogMotion, popupMotion, slideAndFade, fade, scaleAndFade, dialogSlideUpAndFade, dialogFade } from '../animations/presets';
@@ -10,6 +10,8 @@ import { useId } from '@atlaskit/ds-lib/use-id';
10
10
  * own `anchor-name` on the trigger element.
11
11
  */
12
12
  export function usePopoverId() {
13
- // prefixing for debug clarity in the DOM
13
+ // `@atlaskit/ds-lib/use-id` already strips characters that React's
14
+ // `useId` produces (`:`, `«`, `»`) which are invalid in HTML ID
15
+ // tokens. The `popover-` prefix is purely for DOM debug clarity.
14
16
  return "popover-".concat(useId());
15
17
  }
@@ -73,11 +73,16 @@ export function getAriaForTrigger(_ref) {
73
73
  var ariaHasPopup = roleToAriaHasPopup({
74
74
  role: role
75
75
  });
76
+ // `aria-controls` is set to `undefined` while closed. The `Popover` /
77
+ // `Dialog` primitives unmount their host element after the exit
78
+ // animation finishes, so a closed-state `aria-controls` would point
79
+ // at a node that is not in the DOM. Returning `undefined` here means
80
+ // JSX spread renders no `aria-controls` attribute on the trigger
81
+ // until the target exists, avoiding the dangling reference while
82
+ // keeping the relationship live whenever it is meaningful.
76
83
  return {
77
84
  'aria-haspopup': ariaHasPopup,
78
85
  'aria-expanded': isOpen,
79
- // Keep the trigger-to-popover relationship stable. `aria-expanded`
80
- // tells assistive technology whether the controlled popover is visible.
81
- 'aria-controls': popoverId
86
+ 'aria-controls': isOpen ? popoverId : undefined
82
87
  };
83
88
  }
@@ -70,7 +70,8 @@ export function useAnchorPositionAtPoint(_ref) {
70
70
  placement = _ref.placement,
71
71
  getPoint = _ref.getPoint,
72
72
  _ref$isEnabled = _ref.isEnabled,
73
- isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled;
73
+ isEnabled = _ref$isEnabled === void 0 ? true : _ref$isEnabled,
74
+ isOpen = _ref.isOpen;
74
75
  var syntheticAnchorRef = useRef(null);
75
76
 
76
77
  // `getPoint` is kept in a ref so the layout effect can call the
@@ -117,6 +118,7 @@ export function useAnchorPositionAtPoint(_ref) {
117
118
  anchorRef: syntheticAnchorRef,
118
119
  popoverRef: popoverRef,
119
120
  placement: placement,
120
- isEnabled: isEnabled
121
+ isEnabled: isEnabled,
122
+ isOpen: isOpen
121
123
  });
122
124
  }
@@ -214,7 +214,8 @@ export function useAnchorPosition(_ref3) {
214
214
  _ref3$forceFallbackPo = _ref3.forceFallbackPositioning,
215
215
  forceFallbackPositioning = _ref3$forceFallbackPo === void 0 ? false : _ref3$forceFallbackPo,
216
216
  _ref3$isEnabled = _ref3.isEnabled,
217
- isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled;
217
+ isEnabled = _ref3$isEnabled === void 0 ? true : _ref3$isEnabled,
218
+ isOpen = _ref3.isOpen;
218
219
  var id = useId();
219
220
  // Stabilize `placement` object so the same object literal
220
221
  // value returns an object with the same reference
@@ -474,5 +475,5 @@ export function useAnchorPosition(_ref3) {
474
475
  popover.style.removeProperty('opacity');
475
476
  });
476
477
  return undoPositioning;
477
- }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id]);
478
+ }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id, isOpen]);
478
479
  }