@atlaskit/top-layer 5.0.0 → 5.0.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 5.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`7ab1bd8a22db7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/7ab1bd8a22db7) -
8
+ Preserve focus moved outside a popover before or during closing. Restore focus based on whether
9
+ the popover held focus, rather than its role.
10
+ - Updated dependencies
11
+
3
12
  ## 5.0.0
4
13
 
5
14
  ### Major Changes
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isNativeElementOpen = isNativeElementOpen;
7
+ function isNativeElementOpen(_ref) {
8
+ var element = _ref.element;
9
+ if (element instanceof HTMLDialogElement) {
10
+ return element.open;
11
+ }
12
+
13
+ // Some DOM implementations throw for unsupported pseudo-classes.
14
+ try {
15
+ return element.matches(':popover-open');
16
+ } catch (_unused) {
17
+ return false;
18
+ }
19
+ }
@@ -8,6 +8,7 @@ exports.useAnimatedVisibility = useAnimatedVisibility;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
9
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
10
  var _react = require("react");
11
+ var _isNativeElementOpen = require("./is-native-element-open");
11
12
  var _reducedMotion = require("./reduced-motion");
12
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
13
14
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -45,19 +46,6 @@ function runOnAnimationsSettled(_ref2) {
45
46
  }
46
47
  });
47
48
  }
48
- function isNativeElementOpen(_ref3) {
49
- var element = _ref3.element;
50
- if (element instanceof HTMLDialogElement) {
51
- return element.open;
52
- }
53
-
54
- // Some DOM implementations throw for unsupported pseudo-classes.
55
- try {
56
- return element.matches(':popover-open');
57
- } catch (_unused) {
58
- return false;
59
- }
60
- }
61
49
 
62
50
  /**
63
51
  * Visibility lifecycle phase shared between `useAnimatedVisibility`,
@@ -86,9 +74,9 @@ function isNativeElementOpen(_ref3) {
86
74
  * ```
87
75
  */
88
76
 
89
- function getInitialVisibilityState(_ref4) {
90
- var isOpen = _ref4.isOpen,
91
- willAnimate = _ref4.willAnimate;
77
+ function getInitialVisibilityState(_ref3) {
78
+ var isOpen = _ref3.isOpen,
79
+ willAnimate = _ref3.willAnimate;
92
80
  if (!isOpen) {
93
81
  return {
94
82
  phase: 'closed',
@@ -184,12 +172,12 @@ function getNextVisibilityState(state, action) {
184
172
  * phase: open ──────────────── exiting ─── (exit settle) ─── closed
185
173
  * ```
186
174
  */
187
- function useAnimatedVisibility(_ref5) {
188
- var isOpen = _ref5.isOpen,
189
- shouldAnimate = _ref5.shouldAnimate,
190
- elementRef = _ref5.elementRef,
191
- onEnterFinish = _ref5.onEnterFinish,
192
- onExitFinish = _ref5.onExitFinish;
175
+ function useAnimatedVisibility(_ref4) {
176
+ var isOpen = _ref4.isOpen,
177
+ shouldAnimate = _ref4.shouldAnimate,
178
+ elementRef = _ref4.elementRef,
179
+ onEnterFinish = _ref4.onEnterFinish,
180
+ onExitFinish = _ref4.onExitFinish;
193
181
  var willAnimate = shouldAnimate && !(0, _reducedMotion.prefersReducedMotion)();
194
182
  var _useReducer = (0, _react.useReducer)(getNextVisibilityState, {
195
183
  isOpen: isOpen,
@@ -274,7 +262,7 @@ function useAnimatedVisibility(_ref5) {
274
262
  queueMicrotask(function () {
275
263
  // A controlled close can be interrupted by isOpen changing back to true,
276
264
  // which reopens the element before this microtask runs.
277
- if (isNativeElementOpen({
265
+ if ((0, _isNativeElementOpen.isNativeElementOpen)({
278
266
  element: element
279
267
  })) {
280
268
  return;
@@ -15,8 +15,8 @@ var _bindEventListener = require("bind-event-listener");
15
15
  var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
16
16
  var _once = _interopRequireDefault(require("@atlaskit/ds-lib/once"));
17
17
  var _useNotifyOpenLayerObserver = require("@atlaskit/layering/use-notify-open-layer-observer");
18
+ var _isNativeElementOpen = require("../internal/is-native-element-open");
18
19
  var _resolvePlacement2 = require("../internal/resolve-placement");
19
- var _shouldFocusIntoPopover = require("../internal/should-focus-into-popover");
20
20
  var _useAnimatedVisibility = require("../internal/use-animated-visibility");
21
21
  var _useFocusWrap = require("../internal/use-focus-wrap");
22
22
  var _useInitialFocus = require("../internal/use-initial-focus");
@@ -147,14 +147,12 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
147
147
  // `useId()` colons are invalid in CSS selectors and popover target attributes.
148
148
  var popoverId = idProp !== null && idProp !== void 0 ? idProp : "popover-".concat(autoId.replace(/:/g, ''));
149
149
 
150
- // Keep semantic callbacks current without rebinding native lifecycle
150
+ // Keep the semantic callback current without rebinding native lifecycle
151
151
  // listeners during the same commit that calls showPopover()/hidePopover().
152
152
  var onCloseRef = (0, _react.useRef)(onClose);
153
- var roleRef = (0, _react.useRef)(role);
154
153
  (0, _react.useLayoutEffect)(function () {
155
154
  onCloseRef.current = onClose;
156
- roleRef.current = role;
157
- }, [onClose, role]);
155
+ }, [onClose]);
158
156
 
159
157
  // Register with open layer observer so `closeLayers()` and open-count
160
158
  // subscriptions work. Only popup-like roles register as `popup`; passive
@@ -188,65 +186,60 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
188
186
  return popupMotionStyles[axis][edge];
189
187
  }, [placement, shouldAnimate]);
190
188
 
191
- // Prevents toggle handler from calling onClose for our own hidePopover() calls.
192
- var programmaticCloseRef = (0, _react.useRef)(false);
193
-
194
- // Set in capture-phase keydown so the toggle handler knows close reason.
189
+ // Tracks the close reason, including our own hidePopover() calls.
195
190
  var closeReasonRef = (0, _react.useRef)('light-dismiss');
196
191
 
197
192
  // Snapshot of pre-open focus, used to restore focus for nested popovers
198
- // with focus-capturing roles (browser only restores the outermost).
199
- var previouslyFocusedElementRef = (0, _react.useRef)(null);
193
+ // because the browser only restores the outermost.
194
+ var focusRestorationTargetRef = (0, _react.useRef)(null);
200
195
  var handleBeforeToggle = (0, _react.useCallback)(function (event) {
201
- if (event.newState !== 'open') {
196
+ var popover = event.currentTarget;
197
+ if (!(popover instanceof HTMLElement)) {
202
198
  return;
203
199
  }
204
- // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- need active element snapshot
205
- var active = document.activeElement;
206
- previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
207
- }, []);
208
- var restorePreviouslyFocusedElement = (0, _react.useCallback)(function () {
209
- var previouslyFocused = previouslyFocusedElementRef.current;
210
- previouslyFocusedElementRef.current = null;
211
- if (previouslyFocused !== null && previouslyFocused.isConnected && (0, _shouldFocusIntoPopover.shouldFocusIntoPopover)({
212
- role: roleRef.current
213
- })) {
214
- previouslyFocused.focus({
215
- preventScroll: true
216
- });
200
+ var ownerDocument = popover.ownerDocument;
201
+ var activeElement = ownerDocument.activeElement;
202
+ if (event.newState === 'open') {
203
+ focusRestorationTargetRef.current = activeElement instanceof HTMLElement ? activeElement : null;
204
+ return;
205
+ }
206
+
207
+ // Check before closing so focus already on body is not mistaken for focus lost during close.
208
+ if (closeReasonRef.current === 'light-dismiss' || !popover.contains(activeElement)) {
209
+ focusRestorationTargetRef.current = null;
217
210
  }
218
211
  }, []);
219
- var handleToggleClosed = (0, _react.useCallback)(function (_event) {
220
- var _onCloseRef$current2;
221
- // Nested-popover focus restoration fallback (browser only
222
- // restores outermost; Firefox skips nested entirely).
223
- // Restore only when the role moved focus in on open AND the
224
- // close was Escape/programmatic. For light dismiss, the
225
- // click target keeps focus per HTML spec
226
- // (`focusPreviousElement=false`).
227
- var reason = closeReasonRef.current;
228
- var isProgrammatic = programmaticCloseRef.current;
229
- if (reason === 'escape' || isProgrammatic) {
230
- restorePreviouslyFocusedElement();
231
- } else {
232
- previouslyFocusedElementRef.current = null;
212
+ var handleToggleClosed = (0, _react.useCallback)(function (event) {
213
+ var popover = event.currentTarget;
214
+ if (!(popover instanceof HTMLElement)) {
215
+ return;
233
216
  }
234
-
217
+ var focusRestorationTarget = focusRestorationTargetRef.current;
218
+ focusRestorationTargetRef.current = null;
219
+ var reason = closeReasonRef.current;
235
220
  // Reset reason so a stale 'escape' from a race with Escape keydown
236
221
  // does not corrupt the next browser-dismiss cycle.
237
222
  closeReasonRef.current = 'light-dismiss';
238
-
239
- // Programmatic close: consumer already knows.
240
- if (isProgrammatic) {
241
- return;
223
+ if (focusRestorationTarget) {
224
+ var ownerDocument = popover.ownerDocument;
225
+ // Native restoration has finished. Preserve focus moved outside the popover.
226
+ var activeElementAfterClose = ownerDocument.activeElement;
227
+ if (activeElementAfterClose === ownerDocument.body || popover.contains(activeElementAfterClose)) {
228
+ // Match native popover restoration, which does not scroll the viewport.
229
+ focusRestorationTarget.focus({
230
+ preventScroll: true
231
+ });
232
+ }
242
233
  }
243
234
 
244
- // Browser dismiss (Escape/click-outside).
245
- // Optional: `manual` mode has no `onClose` in the forwarded type.
246
- (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
247
- reason: reason
248
- });
249
- }, [restorePreviouslyFocusedElement]);
235
+ // Programmatic closes are already known to the consumer.
236
+ if (reason !== 'programmatic') {
237
+ var _onCloseRef$current2;
238
+ (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
239
+ reason: reason
240
+ });
241
+ }
242
+ }, []);
250
243
  var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
251
244
  isOpen: isOpen,
252
245
  shouldAnimate: shouldAnimate,
@@ -261,7 +254,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
261
254
 
262
255
  // Focus management: initial focus on entry (role-dependent), Tab cycling
263
256
  // for dialog roles. Restoration is native for outermost popovers; nested
264
- // focus-capturing roles are handled above via the `beforetoggle` snapshot.
257
+ // popovers are handled above via the `beforetoggle` snapshot.
265
258
  // Passing `phase` keeps `useFocusWrap` attached through the animated-exit
266
259
  // window (WCAG 2.4.3 guard). See notes/architecture/focus.md.
267
260
  (0, _useFocusWrap.useFocusWrap)({
@@ -278,7 +271,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
278
271
  // Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
279
272
  // below, so listeners are attached when `showPopover()` synchronously
280
273
  // dispatches `beforetoggle`. A regular `useEffect` would miss that
281
- // first dispatch, dropping the `previouslyFocusedElementRef` snapshot
274
+ // first dispatch, dropping the `focusRestorationTargetRef` snapshot
282
275
  // and breaking nested-popover focus restoration on close. React runs
283
276
  // layout effects in source order on the same commit.
284
277
  (0, _react.useLayoutEffect)(function () {
@@ -291,7 +284,10 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
291
284
  var unbindEscape = (0, _bindEventListener.bind)(element, {
292
285
  type: 'keydown',
293
286
  listener: function listener(event) {
294
- if (event.key === 'Escape') {
287
+ // Ignore Escape events dispatched after native close.
288
+ if (event.key === 'Escape' && (0, _isNativeElementOpen.isNativeElementOpen)({
289
+ element: element
290
+ })) {
295
291
  closeReasonRef.current = 'escape';
296
292
  }
297
293
  },
@@ -308,7 +304,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
308
304
  onToggle(event);
309
305
  }
310
306
  });
311
- // Snapshot `document.activeElement` before `useInitialFocus` moves it,
307
+ // Snapshot the active element before `useInitialFocus` moves it,
312
308
  // then advance the shared visibility lifecycle.
313
309
  var unbindBeforeToggle = (0, _bindEventListener.bind)(element, {
314
310
  type: 'beforetoggle',
@@ -333,7 +329,6 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
333
329
  return;
334
330
  }
335
331
  if (isOpen) {
336
- programmaticCloseRef.current = false;
337
332
  // Clear stale 'escape' from a prior cycle: the ref outlives
338
333
  // the host element, and interrupted close lifecycles may not reach
339
334
  // `handleToggleClosed` to reset it.
@@ -342,13 +337,13 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
342
337
  element.showPopover();
343
338
  } catch (_unused) {}
344
339
  return function () {
345
- programmaticCloseRef.current = true;
340
+ closeReasonRef.current = 'programmatic';
346
341
  try {
347
342
  element.hidePopover();
348
343
  } catch (_unused2) {}
349
344
  };
350
345
  }
351
- programmaticCloseRef.current = true;
346
+ closeReasonRef.current = 'programmatic';
352
347
  try {
353
348
  element.hidePopover();
354
349
  } catch (_unused3) {}
@@ -0,0 +1,14 @@
1
+ export function isNativeElementOpen({
2
+ element
3
+ }) {
4
+ if (element instanceof HTMLDialogElement) {
5
+ return element.open;
6
+ }
7
+
8
+ // Some DOM implementations throw for unsupported pseudo-classes.
9
+ try {
10
+ return element.matches(':popover-open');
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
2
+ import { isNativeElementOpen } from './is-native-element-open';
2
3
  import { prefersReducedMotion } from './reduced-motion';
3
4
  function runCancellableSettlement({
4
5
  schedule,
@@ -34,20 +35,6 @@ function runOnAnimationsSettled({
34
35
  }
35
36
  });
36
37
  }
37
- function isNativeElementOpen({
38
- element
39
- }) {
40
- if (element instanceof HTMLDialogElement) {
41
- return element.open;
42
- }
43
-
44
- // Some DOM implementations throw for unsupported pseudo-classes.
45
- try {
46
- return element.matches(':popover-open');
47
- } catch {
48
- return false;
49
- }
50
- }
51
38
 
52
39
  /**
53
40
  * Visibility lifecycle phase shared between `useAnimatedVisibility`,
@@ -7,8 +7,8 @@ import { bind } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
8
  import once from '@atlaskit/ds-lib/once';
9
9
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
+ import { isNativeElementOpen } from '../internal/is-native-element-open';
10
11
  import { resolvePlacement } from '../internal/resolve-placement';
11
- import { shouldFocusIntoPopover } from '../internal/should-focus-into-popover';
12
12
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
13
13
  import { useFocusWrap } from '../internal/use-focus-wrap';
14
14
  import { useInitialFocus } from '../internal/use-initial-focus';
@@ -138,14 +138,12 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
138
138
  // `useId()` colons are invalid in CSS selectors and popover target attributes.
139
139
  const popoverId = idProp !== null && idProp !== void 0 ? idProp : `popover-${autoId.replace(/:/g, '')}`;
140
140
 
141
- // Keep semantic callbacks current without rebinding native lifecycle
141
+ // Keep the semantic callback current without rebinding native lifecycle
142
142
  // listeners during the same commit that calls showPopover()/hidePopover().
143
143
  const onCloseRef = useRef(onClose);
144
- const roleRef = useRef(role);
145
144
  useLayoutEffect(() => {
146
145
  onCloseRef.current = onClose;
147
- roleRef.current = role;
148
- }, [onClose, role]);
146
+ }, [onClose]);
149
147
 
150
148
  // Register with open layer observer so `closeLayers()` and open-count
151
149
  // subscriptions work. Only popup-like roles register as `popup`; passive
@@ -180,65 +178,66 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
180
178
  return popupMotionStyles[axis][edge];
181
179
  }, [placement, shouldAnimate]);
182
180
 
183
- // Prevents toggle handler from calling onClose for our own hidePopover() calls.
184
- const programmaticCloseRef = useRef(false);
185
-
186
- // Set in capture-phase keydown so the toggle handler knows close reason.
181
+ // Tracks the close reason, including our own hidePopover() calls.
187
182
  const closeReasonRef = useRef('light-dismiss');
188
183
 
189
184
  // Snapshot of pre-open focus, used to restore focus for nested popovers
190
- // with focus-capturing roles (browser only restores the outermost).
191
- const previouslyFocusedElementRef = useRef(null);
185
+ // because the browser only restores the outermost.
186
+ const focusRestorationTargetRef = useRef(null);
192
187
  const handleBeforeToggle = useCallback(event => {
193
- if (event.newState !== 'open') {
188
+ const popover = event.currentTarget;
189
+ if (!(popover instanceof HTMLElement)) {
194
190
  return;
195
191
  }
196
- // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- need active element snapshot
197
- const active = document.activeElement;
198
- previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
199
- }, []);
200
- const restorePreviouslyFocusedElement = useCallback(() => {
201
- const previouslyFocused = previouslyFocusedElementRef.current;
202
- previouslyFocusedElementRef.current = null;
203
- if (previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
204
- role: roleRef.current
205
- })) {
206
- previouslyFocused.focus({
207
- preventScroll: true
208
- });
192
+ const {
193
+ ownerDocument
194
+ } = popover;
195
+ const {
196
+ activeElement
197
+ } = ownerDocument;
198
+ if (event.newState === 'open') {
199
+ focusRestorationTargetRef.current = activeElement instanceof HTMLElement ? activeElement : null;
200
+ return;
201
+ }
202
+
203
+ // Check before closing so focus already on body is not mistaken for focus lost during close.
204
+ if (closeReasonRef.current === 'light-dismiss' || !popover.contains(activeElement)) {
205
+ focusRestorationTargetRef.current = null;
209
206
  }
210
207
  }, []);
211
- const handleToggleClosed = useCallback(_event => {
212
- var _onCloseRef$current2;
213
- // Nested-popover focus restoration fallback (browser only
214
- // restores outermost; Firefox skips nested entirely).
215
- // Restore only when the role moved focus in on open AND the
216
- // close was Escape/programmatic. For light dismiss, the
217
- // click target keeps focus per HTML spec
218
- // (`focusPreviousElement=false`).
219
- const reason = closeReasonRef.current;
220
- const isProgrammatic = programmaticCloseRef.current;
221
- if (reason === 'escape' || isProgrammatic) {
222
- restorePreviouslyFocusedElement();
223
- } else {
224
- previouslyFocusedElementRef.current = null;
208
+ const handleToggleClosed = useCallback(event => {
209
+ const popover = event.currentTarget;
210
+ if (!(popover instanceof HTMLElement)) {
211
+ return;
225
212
  }
226
-
213
+ const focusRestorationTarget = focusRestorationTargetRef.current;
214
+ focusRestorationTargetRef.current = null;
215
+ const reason = closeReasonRef.current;
227
216
  // Reset reason so a stale 'escape' from a race with Escape keydown
228
217
  // does not corrupt the next browser-dismiss cycle.
229
218
  closeReasonRef.current = 'light-dismiss';
230
-
231
- // Programmatic close: consumer already knows.
232
- if (isProgrammatic) {
233
- return;
219
+ if (focusRestorationTarget) {
220
+ const {
221
+ ownerDocument
222
+ } = popover;
223
+ // Native restoration has finished. Preserve focus moved outside the popover.
224
+ const activeElementAfterClose = ownerDocument.activeElement;
225
+ if (activeElementAfterClose === ownerDocument.body || popover.contains(activeElementAfterClose)) {
226
+ // Match native popover restoration, which does not scroll the viewport.
227
+ focusRestorationTarget.focus({
228
+ preventScroll: true
229
+ });
230
+ }
234
231
  }
235
232
 
236
- // Browser dismiss (Escape/click-outside).
237
- // Optional: `manual` mode has no `onClose` in the forwarded type.
238
- (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 ? void 0 : _onCloseRef$current2.call(onCloseRef, {
239
- reason
240
- });
241
- }, [restorePreviouslyFocusedElement]);
233
+ // Programmatic closes are already known to the consumer.
234
+ if (reason !== 'programmatic') {
235
+ var _onCloseRef$current2;
236
+ (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 ? void 0 : _onCloseRef$current2.call(onCloseRef, {
237
+ reason
238
+ });
239
+ }
240
+ }, []);
242
241
  const {
243
242
  phase,
244
243
  isMounted,
@@ -254,7 +253,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
254
253
 
255
254
  // Focus management: initial focus on entry (role-dependent), Tab cycling
256
255
  // for dialog roles. Restoration is native for outermost popovers; nested
257
- // focus-capturing roles are handled above via the `beforetoggle` snapshot.
256
+ // popovers are handled above via the `beforetoggle` snapshot.
258
257
  // Passing `phase` keeps `useFocusWrap` attached through the animated-exit
259
258
  // window (WCAG 2.4.3 guard). See notes/architecture/focus.md.
260
259
  useFocusWrap({
@@ -271,7 +270,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
271
270
  // Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
272
271
  // below, so listeners are attached when `showPopover()` synchronously
273
272
  // dispatches `beforetoggle`. A regular `useEffect` would miss that
274
- // first dispatch, dropping the `previouslyFocusedElementRef` snapshot
273
+ // first dispatch, dropping the `focusRestorationTargetRef` snapshot
275
274
  // and breaking nested-popover focus restoration on close. React runs
276
275
  // layout effects in source order on the same commit.
277
276
  useLayoutEffect(() => {
@@ -284,7 +283,10 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
284
283
  const unbindEscape = bind(element, {
285
284
  type: 'keydown',
286
285
  listener: event => {
287
- if (event.key === 'Escape') {
286
+ // Ignore Escape events dispatched after native close.
287
+ if (event.key === 'Escape' && isNativeElementOpen({
288
+ element
289
+ })) {
288
290
  closeReasonRef.current = 'escape';
289
291
  }
290
292
  },
@@ -301,7 +303,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
301
303
  onToggle(event);
302
304
  }
303
305
  });
304
- // Snapshot `document.activeElement` before `useInitialFocus` moves it,
306
+ // Snapshot the active element before `useInitialFocus` moves it,
305
307
  // then advance the shared visibility lifecycle.
306
308
  const unbindBeforeToggle = bind(element, {
307
309
  type: 'beforetoggle',
@@ -326,7 +328,6 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
326
328
  return;
327
329
  }
328
330
  if (isOpen) {
329
- programmaticCloseRef.current = false;
330
331
  // Clear stale 'escape' from a prior cycle: the ref outlives
331
332
  // the host element, and interrupted close lifecycles may not reach
332
333
  // `handleToggleClosed` to reset it.
@@ -335,13 +336,13 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
335
336
  element.showPopover();
336
337
  } catch {}
337
338
  return () => {
338
- programmaticCloseRef.current = true;
339
+ closeReasonRef.current = 'programmatic';
339
340
  try {
340
341
  element.hidePopover();
341
342
  } catch {}
342
343
  };
343
344
  }
344
- programmaticCloseRef.current = true;
345
+ closeReasonRef.current = 'programmatic';
345
346
  try {
346
347
  element.hidePopover();
347
348
  } catch {}
@@ -0,0 +1,13 @@
1
+ export function isNativeElementOpen(_ref) {
2
+ var element = _ref.element;
3
+ if (element instanceof HTMLDialogElement) {
4
+ return element.open;
5
+ }
6
+
7
+ // Some DOM implementations throw for unsupported pseudo-classes.
8
+ try {
9
+ return element.matches(':popover-open');
10
+ } catch (_unused) {
11
+ return false;
12
+ }
13
+ }
@@ -3,6 +3,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
3
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
4
4
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
5
  import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
6
+ import { isNativeElementOpen } from './is-native-element-open';
6
7
  import { prefersReducedMotion } from './reduced-motion';
7
8
  function runCancellableSettlement(_ref) {
8
9
  var schedule = _ref.schedule,
@@ -38,19 +39,6 @@ function runOnAnimationsSettled(_ref2) {
38
39
  }
39
40
  });
40
41
  }
41
- function isNativeElementOpen(_ref3) {
42
- var element = _ref3.element;
43
- if (element instanceof HTMLDialogElement) {
44
- return element.open;
45
- }
46
-
47
- // Some DOM implementations throw for unsupported pseudo-classes.
48
- try {
49
- return element.matches(':popover-open');
50
- } catch (_unused) {
51
- return false;
52
- }
53
- }
54
42
 
55
43
  /**
56
44
  * Visibility lifecycle phase shared between `useAnimatedVisibility`,
@@ -79,9 +67,9 @@ function isNativeElementOpen(_ref3) {
79
67
  * ```
80
68
  */
81
69
 
82
- function getInitialVisibilityState(_ref4) {
83
- var isOpen = _ref4.isOpen,
84
- willAnimate = _ref4.willAnimate;
70
+ function getInitialVisibilityState(_ref3) {
71
+ var isOpen = _ref3.isOpen,
72
+ willAnimate = _ref3.willAnimate;
85
73
  if (!isOpen) {
86
74
  return {
87
75
  phase: 'closed',
@@ -177,12 +165,12 @@ function getNextVisibilityState(state, action) {
177
165
  * phase: open ──────────────── exiting ─── (exit settle) ─── closed
178
166
  * ```
179
167
  */
180
- export function useAnimatedVisibility(_ref5) {
181
- var isOpen = _ref5.isOpen,
182
- shouldAnimate = _ref5.shouldAnimate,
183
- elementRef = _ref5.elementRef,
184
- onEnterFinish = _ref5.onEnterFinish,
185
- onExitFinish = _ref5.onExitFinish;
168
+ export function useAnimatedVisibility(_ref4) {
169
+ var isOpen = _ref4.isOpen,
170
+ shouldAnimate = _ref4.shouldAnimate,
171
+ elementRef = _ref4.elementRef,
172
+ onEnterFinish = _ref4.onEnterFinish,
173
+ onExitFinish = _ref4.onExitFinish;
186
174
  var willAnimate = shouldAnimate && !prefersReducedMotion();
187
175
  var _useReducer = useReducer(getNextVisibilityState, {
188
176
  isOpen: isOpen,
@@ -7,8 +7,8 @@ import { bind } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
8
  import once from '@atlaskit/ds-lib/once';
9
9
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
+ import { isNativeElementOpen } from '../internal/is-native-element-open';
10
11
  import { resolvePlacement } from '../internal/resolve-placement';
11
- import { shouldFocusIntoPopover } from '../internal/should-focus-into-popover';
12
12
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
13
13
  import { useFocusWrap } from '../internal/use-focus-wrap';
14
14
  import { useInitialFocus } from '../internal/use-initial-focus';
@@ -138,14 +138,12 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
138
138
  // `useId()` colons are invalid in CSS selectors and popover target attributes.
139
139
  var popoverId = idProp !== null && idProp !== void 0 ? idProp : "popover-".concat(autoId.replace(/:/g, ''));
140
140
 
141
- // Keep semantic callbacks current without rebinding native lifecycle
141
+ // Keep the semantic callback current without rebinding native lifecycle
142
142
  // listeners during the same commit that calls showPopover()/hidePopover().
143
143
  var onCloseRef = useRef(onClose);
144
- var roleRef = useRef(role);
145
144
  useLayoutEffect(function () {
146
145
  onCloseRef.current = onClose;
147
- roleRef.current = role;
148
- }, [onClose, role]);
146
+ }, [onClose]);
149
147
 
150
148
  // Register with open layer observer so `closeLayers()` and open-count
151
149
  // subscriptions work. Only popup-like roles register as `popup`; passive
@@ -179,65 +177,60 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
179
177
  return popupMotionStyles[axis][edge];
180
178
  }, [placement, shouldAnimate]);
181
179
 
182
- // Prevents toggle handler from calling onClose for our own hidePopover() calls.
183
- var programmaticCloseRef = useRef(false);
184
-
185
- // Set in capture-phase keydown so the toggle handler knows close reason.
180
+ // Tracks the close reason, including our own hidePopover() calls.
186
181
  var closeReasonRef = useRef('light-dismiss');
187
182
 
188
183
  // Snapshot of pre-open focus, used to restore focus for nested popovers
189
- // with focus-capturing roles (browser only restores the outermost).
190
- var previouslyFocusedElementRef = useRef(null);
184
+ // because the browser only restores the outermost.
185
+ var focusRestorationTargetRef = useRef(null);
191
186
  var handleBeforeToggle = useCallback(function (event) {
192
- if (event.newState !== 'open') {
187
+ var popover = event.currentTarget;
188
+ if (!(popover instanceof HTMLElement)) {
193
189
  return;
194
190
  }
195
- // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- need active element snapshot
196
- var active = document.activeElement;
197
- previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
198
- }, []);
199
- var restorePreviouslyFocusedElement = useCallback(function () {
200
- var previouslyFocused = previouslyFocusedElementRef.current;
201
- previouslyFocusedElementRef.current = null;
202
- if (previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
203
- role: roleRef.current
204
- })) {
205
- previouslyFocused.focus({
206
- preventScroll: true
207
- });
191
+ var ownerDocument = popover.ownerDocument;
192
+ var activeElement = ownerDocument.activeElement;
193
+ if (event.newState === 'open') {
194
+ focusRestorationTargetRef.current = activeElement instanceof HTMLElement ? activeElement : null;
195
+ return;
196
+ }
197
+
198
+ // Check before closing so focus already on body is not mistaken for focus lost during close.
199
+ if (closeReasonRef.current === 'light-dismiss' || !popover.contains(activeElement)) {
200
+ focusRestorationTargetRef.current = null;
208
201
  }
209
202
  }, []);
210
- var handleToggleClosed = useCallback(function (_event) {
211
- var _onCloseRef$current2;
212
- // Nested-popover focus restoration fallback (browser only
213
- // restores outermost; Firefox skips nested entirely).
214
- // Restore only when the role moved focus in on open AND the
215
- // close was Escape/programmatic. For light dismiss, the
216
- // click target keeps focus per HTML spec
217
- // (`focusPreviousElement=false`).
218
- var reason = closeReasonRef.current;
219
- var isProgrammatic = programmaticCloseRef.current;
220
- if (reason === 'escape' || isProgrammatic) {
221
- restorePreviouslyFocusedElement();
222
- } else {
223
- previouslyFocusedElementRef.current = null;
203
+ var handleToggleClosed = useCallback(function (event) {
204
+ var popover = event.currentTarget;
205
+ if (!(popover instanceof HTMLElement)) {
206
+ return;
224
207
  }
225
-
208
+ var focusRestorationTarget = focusRestorationTargetRef.current;
209
+ focusRestorationTargetRef.current = null;
210
+ var reason = closeReasonRef.current;
226
211
  // Reset reason so a stale 'escape' from a race with Escape keydown
227
212
  // does not corrupt the next browser-dismiss cycle.
228
213
  closeReasonRef.current = 'light-dismiss';
229
-
230
- // Programmatic close: consumer already knows.
231
- if (isProgrammatic) {
232
- return;
214
+ if (focusRestorationTarget) {
215
+ var ownerDocument = popover.ownerDocument;
216
+ // Native restoration has finished. Preserve focus moved outside the popover.
217
+ var activeElementAfterClose = ownerDocument.activeElement;
218
+ if (activeElementAfterClose === ownerDocument.body || popover.contains(activeElementAfterClose)) {
219
+ // Match native popover restoration, which does not scroll the viewport.
220
+ focusRestorationTarget.focus({
221
+ preventScroll: true
222
+ });
223
+ }
233
224
  }
234
225
 
235
- // Browser dismiss (Escape/click-outside).
236
- // Optional: `manual` mode has no `onClose` in the forwarded type.
237
- (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
238
- reason: reason
239
- });
240
- }, [restorePreviouslyFocusedElement]);
226
+ // Programmatic closes are already known to the consumer.
227
+ if (reason !== 'programmatic') {
228
+ var _onCloseRef$current2;
229
+ (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 || _onCloseRef$current2.call(onCloseRef, {
230
+ reason: reason
231
+ });
232
+ }
233
+ }, []);
241
234
  var _useAnimatedVisibilit = useAnimatedVisibility({
242
235
  isOpen: isOpen,
243
236
  shouldAnimate: shouldAnimate,
@@ -252,7 +245,7 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
252
245
 
253
246
  // Focus management: initial focus on entry (role-dependent), Tab cycling
254
247
  // for dialog roles. Restoration is native for outermost popovers; nested
255
- // focus-capturing roles are handled above via the `beforetoggle` snapshot.
248
+ // popovers are handled above via the `beforetoggle` snapshot.
256
249
  // Passing `phase` keeps `useFocusWrap` attached through the animated-exit
257
250
  // window (WCAG 2.4.3 guard). See notes/architecture/focus.md.
258
251
  useFocusWrap({
@@ -269,7 +262,7 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
269
262
  // Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
270
263
  // below, so listeners are attached when `showPopover()` synchronously
271
264
  // dispatches `beforetoggle`. A regular `useEffect` would miss that
272
- // first dispatch, dropping the `previouslyFocusedElementRef` snapshot
265
+ // first dispatch, dropping the `focusRestorationTargetRef` snapshot
273
266
  // and breaking nested-popover focus restoration on close. React runs
274
267
  // layout effects in source order on the same commit.
275
268
  useLayoutEffect(function () {
@@ -282,7 +275,10 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
282
275
  var unbindEscape = bind(element, {
283
276
  type: 'keydown',
284
277
  listener: function listener(event) {
285
- if (event.key === 'Escape') {
278
+ // Ignore Escape events dispatched after native close.
279
+ if (event.key === 'Escape' && isNativeElementOpen({
280
+ element: element
281
+ })) {
286
282
  closeReasonRef.current = 'escape';
287
283
  }
288
284
  },
@@ -299,7 +295,7 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
299
295
  onToggle(event);
300
296
  }
301
297
  });
302
- // Snapshot `document.activeElement` before `useInitialFocus` moves it,
298
+ // Snapshot the active element before `useInitialFocus` moves it,
303
299
  // then advance the shared visibility lifecycle.
304
300
  var unbindBeforeToggle = bind(element, {
305
301
  type: 'beforetoggle',
@@ -324,7 +320,6 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
324
320
  return;
325
321
  }
326
322
  if (isOpen) {
327
- programmaticCloseRef.current = false;
328
323
  // Clear stale 'escape' from a prior cycle: the ref outlives
329
324
  // the host element, and interrupted close lifecycles may not reach
330
325
  // `handleToggleClosed` to reset it.
@@ -333,13 +328,13 @@ export var Popover = /*#__PURE__*/forwardRef(function Popover(_ref, ref) {
333
328
  element.showPopover();
334
329
  } catch (_unused) {}
335
330
  return function () {
336
- programmaticCloseRef.current = true;
331
+ closeReasonRef.current = 'programmatic';
337
332
  try {
338
333
  element.hidePopover();
339
334
  } catch (_unused2) {}
340
335
  };
341
336
  }
342
- programmaticCloseRef.current = true;
337
+ closeReasonRef.current = 'programmatic';
343
338
  try {
344
339
  element.hidePopover();
345
340
  } catch (_unused3) {}
@@ -0,0 +1,3 @@
1
+ export declare function isNativeElementOpen({ element }: {
2
+ element: HTMLElement;
3
+ }): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Low-level layering primitives based on popover and dialog",
5
5
  "homepage": "https://atlassian.design/components/top-layer",
6
6
  "repository": "https://bitbucket.org/atlassian/atlassian-frontend-mirror",
@@ -40,7 +40,7 @@
40
40
  "@atlaskit/ds-lib": "^8.0.0",
41
41
  "@atlaskit/layering": "^5.2.0",
42
42
  "@atlaskit/react-compiler-gating": "^0.2.0",
43
- "@atlaskit/tokens": "^16.12.0",
43
+ "@atlaskit/tokens": "^17.0.0",
44
44
  "@babel/runtime": "^7.0.0",
45
45
  "@compiled/react": "^1.0.2",
46
46
  "bind-event-listener": "^3.0.0",