@atlaskit/top-layer 0.14.0 → 0.16.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 (97) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cjs/dialog/create-close-event.js +1 -2
  3. package/dist/cjs/dialog/dialog-content.js +33 -4
  4. package/dist/cjs/dialog-scroll-lock/index.js +17 -7
  5. package/dist/cjs/entry-points/dialog.js +6 -0
  6. package/dist/cjs/entry-points/popover.js +6 -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/create-close-event.js +1 -2
  16. package/dist/cjs/popover/popover.js +58 -9
  17. package/dist/es2019/dialog/create-close-event.js +1 -2
  18. package/dist/es2019/dialog/dialog-content.js +33 -4
  19. package/dist/es2019/dialog-scroll-lock/index.js +18 -8
  20. package/dist/es2019/entry-points/dialog.js +1 -1
  21. package/dist/es2019/entry-points/popover.js +1 -1
  22. package/dist/es2019/entry-points/use-popover-id.js +3 -1
  23. package/dist/es2019/internal/get-aria-for-trigger.js +8 -3
  24. package/dist/es2019/internal/use-anchor-position-at-point.js +4 -2
  25. package/dist/es2019/internal/use-anchor-position.js +3 -2
  26. package/dist/es2019/internal/use-animated-visibility.js +222 -149
  27. package/dist/es2019/internal/use-focus-wrap.js +5 -4
  28. package/dist/es2019/internal/use-initial-focus.js +189 -45
  29. package/dist/es2019/internal/use-width-from-anchor.js +5 -4
  30. package/dist/es2019/popover/create-close-event.js +1 -2
  31. package/dist/es2019/popover/popover.js +59 -10
  32. package/dist/esm/dialog/create-close-event.js +1 -2
  33. package/dist/esm/dialog/dialog-content.js +33 -4
  34. package/dist/esm/dialog-scroll-lock/index.js +17 -8
  35. package/dist/esm/entry-points/dialog.js +1 -1
  36. package/dist/esm/entry-points/popover.js +1 -1
  37. package/dist/esm/entry-points/use-popover-id.js +3 -1
  38. package/dist/esm/internal/get-aria-for-trigger.js +8 -3
  39. package/dist/esm/internal/use-anchor-position-at-point.js +4 -2
  40. package/dist/esm/internal/use-anchor-position.js +3 -2
  41. package/dist/esm/internal/use-animated-visibility.js +235 -157
  42. package/dist/esm/internal/use-focus-wrap.js +5 -4
  43. package/dist/esm/internal/use-initial-focus.js +196 -51
  44. package/dist/esm/internal/use-width-from-anchor.js +5 -4
  45. package/dist/esm/popover/create-close-event.js +1 -2
  46. package/dist/esm/popover/popover.js +59 -10
  47. package/dist/types/animations/types.d.ts +0 -8
  48. package/dist/types/dialog/create-close-event.d.ts +1 -2
  49. package/dist/types/dialog/types.d.ts +9 -1
  50. package/dist/types/dialog-scroll-lock/index.d.ts +22 -6
  51. package/dist/types/entry-points/animations.d.ts +1 -1
  52. package/dist/types/entry-points/dialog.d.ts +1 -1
  53. package/dist/types/entry-points/popover.d.ts +1 -1
  54. package/dist/types/internal/get-aria-for-trigger.d.ts +8 -1
  55. package/dist/types/internal/use-anchor-position-at-point.d.ts +6 -1
  56. package/dist/types/internal/use-anchor-position.d.ts +14 -1
  57. package/dist/types/internal/use-animated-visibility.d.ts +48 -19
  58. package/dist/types/internal/use-focus-wrap.d.ts +6 -1
  59. package/dist/types/internal/use-initial-focus.d.ts +55 -4
  60. package/dist/types/internal/use-width-from-anchor.d.ts +3 -2
  61. package/dist/types/popover/create-close-event.d.ts +1 -2
  62. package/dist/types/popover/types.d.ts +13 -2
  63. package/dist/types-ts4.5/animations/types.d.ts +0 -8
  64. package/dist/types-ts4.5/dialog/create-close-event.d.ts +1 -2
  65. package/dist/types-ts4.5/dialog/types.d.ts +9 -1
  66. package/dist/types-ts4.5/dialog-scroll-lock/index.d.ts +22 -6
  67. package/dist/types-ts4.5/entry-points/animations.d.ts +1 -1
  68. package/dist/types-ts4.5/entry-points/dialog.d.ts +1 -1
  69. package/dist/types-ts4.5/entry-points/popover.d.ts +1 -1
  70. package/dist/types-ts4.5/internal/get-aria-for-trigger.d.ts +8 -1
  71. package/dist/types-ts4.5/internal/use-anchor-position-at-point.d.ts +6 -1
  72. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +14 -1
  73. package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +48 -19
  74. package/dist/types-ts4.5/internal/use-focus-wrap.d.ts +6 -1
  75. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +55 -4
  76. package/dist/types-ts4.5/internal/use-width-from-anchor.d.ts +3 -2
  77. package/dist/types-ts4.5/popover/create-close-event.d.ts +1 -2
  78. package/dist/types-ts4.5/popover/types.d.ts +13 -2
  79. package/package.json +1 -1
  80. package/create-close-event/package.json +0 -17
  81. package/dialog/create-close-event/package.json +0 -17
  82. package/dist/cjs/entry-points/create-close-event.js +0 -19
  83. package/dist/cjs/entry-points/dialog/create-close-event.js +0 -12
  84. package/dist/cjs/entry-points/popover/create-close-event.js +0 -12
  85. package/dist/es2019/entry-points/create-close-event.js +0 -9
  86. package/dist/es2019/entry-points/dialog/create-close-event.js +0 -1
  87. package/dist/es2019/entry-points/popover/create-close-event.js +0 -1
  88. package/dist/esm/entry-points/create-close-event.js +0 -9
  89. package/dist/esm/entry-points/dialog/create-close-event.js +0 -1
  90. package/dist/esm/entry-points/popover/create-close-event.js +0 -1
  91. package/dist/types/entry-points/create-close-event.d.ts +0 -11
  92. package/dist/types/entry-points/dialog/create-close-event.d.ts +0 -2
  93. package/dist/types/entry-points/popover/create-close-event.d.ts +0 -2
  94. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +0 -11
  95. package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +0 -2
  96. package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +0 -2
  97. package/popover/create-close-event/package.json +0 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`6d0485dce81c4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6d0485dce81c4) -
8
+ `Popover` and `Dialog` now unmount their host element after the exit animation completes, instead
9
+ of leaving an empty `role` / `aria-*` element in the accessibility tree.
10
+
11
+ Other small changes that fall out of this:
12
+ - The popover/dialog `id` stays stable across opens.
13
+ - `ref.current` is only populated while the host element is rendered. Read the ref inside
14
+ `onOpenChange` / `onEnterFinish`, or gate the read on `isOpen`.
15
+ - `onEnterFinish` and `onExitFinish` now fire before the internal phase transition that drives the
16
+ unmount, so consumers reading the host element (or a forwarded ref) inside those callbacks
17
+ always see it attached to the DOM, regardless of React batching semantics.
18
+ - `getAriaForTrigger` returns `aria-controls: undefined` while `isOpen` is `false`; spreading the
19
+ result onto a JSX element strips the attribute from the DOM and avoids a dangling reference.
20
+ - `useAnchorPosition`, `useAnchorPositionAtPoint`, and `useWidthFromAnchor` now require `isOpen`
21
+ so positioning re-runs against the freshly mounted host element on each open.
22
+ `useWidthFromAnchor` also requires `anchorRef`.
23
+ - `DialogScrollLock` now requires an `isOpen` prop so the scroll lock is released the moment
24
+ `isOpen` flips to `false`, regardless of React's unmount timing.
25
+ - `useInitialFocus` now follows the WAI-ARIA APG Combobox Pattern: when a `menu` or `listbox`
26
+ popup is controlled by a focused combobox-like textbox whose `aria-controls` (or legacy
27
+ `aria-owns`) references the popup, initial focus stays on the textbox instead of moving into the
28
+ popup. Navigation is expected to be proxied via `aria-activedescendant`. Both variants are
29
+ detected:
30
+ - **In-popup combobox**: the focused textbox lives inside the popup.
31
+ - **External combobox**: the focused textbox is rendered OUTSIDE the popup (for example
32
+ `react-select`-style menus that portal the listbox). The carve-out also accepts the case where
33
+ the textbox's `aria-controls` references a DESCENDANT of the popup host (such as
34
+ react-select's inner `MenuList` element). Without this carve-out, moving focus into the
35
+ listbox would blur the external textbox and cause libraries like `react-select` to close the
36
+ menu on the same interaction that opened it.
37
+
38
+ Popups that are not opened from a combobox-like trigger continue to receive their
39
+ role-appropriate initial focus.
40
+
41
+ ## 0.15.0
42
+
43
+ ### Minor Changes
44
+
45
+ - [`e5b4070f51a93`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/e5b4070f51a93) -
46
+ Remove Top Layer compatibility aliases and expose close-event helpers from the `dialog` and
47
+ `popover` entry points.
48
+
3
49
  ## 0.14.0
4
50
 
5
51
  ### Minor Changes
@@ -16,8 +16,7 @@ exports.createCloseEvent = createCloseEvent;
16
16
  *
17
17
  * @example
18
18
  * ```tsx
19
- * import { createCloseEvent } from '@atlaskit/top-layer/create-close-event';
20
- * import { type TDialogCloseReason } from '@atlaskit/top-layer/dialog';
19
+ * import { createCloseEvent, type TDialogCloseReason } from '@atlaskit/top-layer/dialog';
21
20
  *
22
21
  * function onDialogClose({ reason }: { reason: TDialogCloseReason }) {
23
22
  * legacyOnClose(createCloseEvent({ reason }));
@@ -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,4 +9,10 @@ Object.defineProperty(exports, "Dialog", {
9
9
  return _dialog.Dialog;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "createCloseEvent", {
13
+ enumerable: true,
14
+ get: function get() {
15
+ return _dialog.createCloseEvent;
16
+ }
17
+ });
12
18
  var _dialog = require("../dialog");
@@ -9,4 +9,10 @@ Object.defineProperty(exports, "Popover", {
9
9
  return _popover.Popover;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "createPopoverCloseEvent", {
13
+ enumerable: true,
14
+ get: function get() {
15
+ return _popover.createPopoverCloseEvent;
16
+ }
17
+ });
12
18
  var _popover = require("../popover");
@@ -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
  }