@atlaskit/top-layer 6.2.3 → 6.2.5

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,23 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 6.2.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [`b82a5dc33d636`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b82a5dc33d636) -
8
+ The testing polyfill now fires a cancelable cancel event when Escape targets a dialog. Unless
9
+ keydown or cancel is prevented, the dialog closes and emits its existing closing beforetoggle,
10
+ toggle and close events. Escape respects closedby and dialog/popover opening order.
11
+
12
+ ## 6.2.4
13
+
14
+ ### Patch Changes
15
+
16
+ - [`4247678302a95`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/4247678302a95) -
17
+ Fixed server-side rendering errors and warnings in the top-layer popover, dialog and popper paths
18
+ (`platform-dst-top-layer`).
19
+ - Updated dependencies
20
+
3
21
  ## 6.2.3
4
22
 
5
23
  ### Patch Changes
@@ -14,6 +14,7 @@ var _react2 = require("@compiled/react");
14
14
  var _bindEventListener = require("bind-event-listener");
15
15
  var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
16
16
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
17
+ var _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
17
18
  var _useNotifyOpenLayerObserver = require("@atlaskit/layering/use-notify-open-layer-observer");
18
19
  var _useAnimatedVisibility = require("../internal/use-animated-visibility");
19
20
  var _useFocusWrap = require("../internal/use-focus-wrap");
@@ -83,7 +84,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
83
84
  var dismissedByRef = (0, _react.useRef)(dismissedBy);
84
85
  var onCloseRef = (0, _react.useRef)(onClose);
85
86
  // Keep native event handlers stable while ensuring they read the latest committed props.
86
- (0, _react.useLayoutEffect)(function () {
87
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
87
88
  dismissedByRef.current = dismissedBy;
88
89
  onCloseRef.current = onClose;
89
90
  }, [dismissedBy, onClose]);
@@ -102,7 +103,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
102
103
  // Ordering is important: bind native lifecycle listeners before the later
103
104
  // layout effect calls showModal() or close(), because `beforetoggle` fires
104
105
  // synchronously during those calls.
105
- (0, _react.useLayoutEffect)(function () {
106
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
106
107
  if (!isMounted) {
107
108
  return;
108
109
  }
@@ -161,7 +162,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
161
162
  // No-op: no current use case for programmatic close via OpenLayerObserver.
162
163
  onClose: _noop.default
163
164
  });
164
- (0, _react.useLayoutEffect)(function () {
165
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
165
166
  var dialog = ownRef.current;
166
167
  if (!dialog) {
167
168
  return;
@@ -4,8 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.DialogScrollLock = DialogScrollLock;
7
- var _react = require("react");
8
7
  var _browserApis = require("@atlaskit/browser-apis");
8
+ var _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
9
9
  /**
10
10
  * __DialogScrollLock__
11
11
  *
@@ -37,7 +37,7 @@ var _browserApis = require("@atlaskit/browser-apis");
37
37
  */
38
38
  function DialogScrollLock(_ref) {
39
39
  var isOpen = _ref.isOpen;
40
- (0, _react.useLayoutEffect)(function () {
40
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
41
41
  var _getDocument;
42
42
  if (!isOpen) {
43
43
  return;
@@ -8,6 +8,7 @@ exports.useAnchoredPopoverAtPoint = useAnchoredPopoverAtPoint;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
10
10
  var _react = require("react");
11
+ var _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
11
12
  var _useAnchoredPopover = require("./use-anchored-popover");
12
13
  var _excluded = ["getPoint", "isEnabled"];
13
14
  /**
@@ -97,7 +98,7 @@ function useAnchoredPopoverAtPoint(_ref2) {
97
98
  // effect must not depend on anything the delegated effect does not: a dependency
98
99
  // here alone re-creates the element without that hook writing an `anchor-name`
99
100
  // onto it, and positioning silently dies.
100
- (0, _react.useLayoutEffect)(function () {
101
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
101
102
  if (!isEnabled) {
102
103
  return;
103
104
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.useAnchoredPopover = useAnchoredPopover;
7
7
  var _react = require("react");
8
8
  var _useId = require("@atlaskit/ds-lib/use-id");
9
+ var _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
9
10
  var _applyAnchorPositioning = require("./anchor-positioning/apply-anchor-positioning");
10
11
  var _anchoredPopoverSize = require("./anchored-popover-size");
11
12
  var _applyJavascriptFallbackPositioning = require("./javascript-fallback/apply-javascript-fallback-positioning");
@@ -214,7 +215,7 @@ function useAnchoredPopover(_ref3) {
214
215
  // element (the point hook remounts its `<div>`).
215
216
  var fallbackAnchorName = useNewAnchorName();
216
217
  var stablePlacement = useStablePlacement(placement);
217
- (0, _react.useLayoutEffect)(function () {
218
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
218
219
  if (!isEnabled) {
219
220
  return;
220
221
  }
@@ -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 _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
11
12
  var _isNativeElementOpen = require("./is-native-element-open");
12
13
  var _reducedMotion = require("./reduced-motion");
13
14
  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; }
@@ -208,7 +209,7 @@ function useAnimatedVisibility(_ref4) {
208
209
  var onEnterFinishRef = (0, _react.useRef)(onEnterFinish);
209
210
  var onExitFinishRef = (0, _react.useRef)(onExitFinish);
210
211
  var willAnimateRef = (0, _react.useRef)(willAnimate);
211
- (0, _react.useLayoutEffect)(function () {
212
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
212
213
  onEnterFinishRef.current = onEnterFinish;
213
214
  onExitFinishRef.current = onExitFinish;
214
215
  willAnimateRef.current = willAnimate;
@@ -324,7 +325,7 @@ function useAnimatedVisibility(_ref4) {
324
325
  onSettled: settleExit
325
326
  });
326
327
  }, [elementRef, settleExit]);
327
- (0, _react.useLayoutEffect)(function () {
328
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
328
329
  return function () {
329
330
  // Prevent a pending animation or microtask from notifying after unmount.
330
331
  cancelExitSettlementRef.current();
@@ -14,6 +14,7 @@ var _react2 = require("@compiled/react");
14
14
  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
+ var _useLayoutEffect = require("@atlaskit/ds-lib/use-layout-effect");
17
18
  var _useNotifyOpenLayerObserver = require("@atlaskit/layering/use-notify-open-layer-observer");
18
19
  var _isNativeElementOpen = require("../internal/is-native-element-open");
19
20
  var _resolvePlacement2 = require("../internal/resolve-placement");
@@ -172,7 +173,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
172
173
  // Keep the semantic callback current without rebinding native lifecycle
173
174
  // listeners during the same commit that calls showPopover()/hidePopover().
174
175
  var onCloseRef = (0, _react.useRef)(onClose);
175
- (0, _react.useLayoutEffect)(function () {
176
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
176
177
  onCloseRef.current = onClose;
177
178
  }, [onClose]);
178
179
 
@@ -295,7 +296,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
295
296
  // first dispatch, dropping the `focusRestorationTargetRef` snapshot
296
297
  // and breaking nested-popover focus restoration on close. React runs
297
298
  // layout effects in source order on the same commit.
298
- (0, _react.useLayoutEffect)(function () {
299
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
299
300
  var element = ownRef.current;
300
301
  if (!element) {
301
302
  return;
@@ -344,7 +345,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
344
345
  // Show/hide based on isOpen. `showPopover`/`hidePopover` are no-ops when
345
346
  // already in the target state. Try/catch guards `InvalidStateError` on
346
347
  // disconnected elements (StrictMode/concurrent/unmount edge cases).
347
- (0, _react.useLayoutEffect)(function () {
348
+ (0, _useLayoutEffect.useLayoutEffect)(function () {
348
349
  var element = ownRef.current;
349
350
  if (!element) {
350
351
  return;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  var _bindEventListener = require("bind-event-listener");
4
+ // Node supports findLast, but the package defaults to older array type declarations.
5
+ /// <reference lib="es2023.array" />
4
6
  /* eslint-disable compat/compat */
5
7
  /**
6
8
  * Popover API & HTMLDialogElement polyfills for jsdom.
@@ -482,40 +484,133 @@ function handleDocumentPointerup(event) {
482
484
  });
483
485
  }
484
486
 
485
- // Topmost still-connected popover in `stack`, pruning disconnected entries
486
- // (test pollution where a popover was removed from the DOM without hidePopover).
487
- // Polyfill-only defensive cleanup; not in the spec.
488
- function topmostConnected(_ref18) {
489
- var stack = _ref18.stack;
490
- while (stack.length > 0) {
491
- var candidate = stack[stack.length - 1];
492
- if (candidate.isConnected) {
493
- return candidate;
494
- }
495
- stack.pop();
487
+ /**
488
+ * Represents one dialog or popover opening so queued Escape requests cannot close a later opening.
489
+ */
490
+
491
+ /**
492
+ * Tracks dialog and popover Escape targets in opening order.
493
+ */
494
+ var escapeLayers = [];
495
+
496
+ /**
497
+ * Removes an opening from Escape tracking so it cannot receive a later dismissal request.
498
+ */
499
+ function unregisterEscapeLayer(_ref18) {
500
+ var element = _ref18.element;
501
+ var index = escapeLayers.findIndex(function (layer) {
502
+ return layer.element === element;
503
+ });
504
+ if (index !== -1) {
505
+ escapeLayers.splice(index, 1);
496
506
  }
497
- return null;
498
507
  }
499
508
 
500
- // Escape closes the topmost open popover. Hints are dismissed before autos.
501
- // Spec: https://html.spec.whatwg.org/multipage/popover.html#close-watcher
509
+ /**
510
+ * Records a new opening, replacing any stale registration for the same element.
511
+ */
512
+ function registerEscapeLayer(_ref19) {
513
+ var layer = _ref19.layer;
514
+ unregisterEscapeLayer({
515
+ element: layer.element
516
+ });
517
+ escapeLayers.push(layer);
518
+ }
519
+
520
+ /**
521
+ * Checks whether a tracked element is still connected and open, including direct DOM changes.
522
+ */
523
+ function isEscapeLayerOpen(_ref20) {
524
+ var layer = _ref20.layer;
525
+ return layer.element.isConnected && (layer.type === 'dialog' ? layer.element.open : isPopoverOpen({
526
+ element: layer.element
527
+ }));
528
+ }
529
+
530
+ /**
531
+ * Removes registrations for closed or disconnected elements before choosing an Escape target.
532
+ */
533
+ function pruneClosedEscapeLayers() {
534
+ escapeLayers.filter(function (layer) {
535
+ return !isEscapeLayerOpen({
536
+ layer: layer
537
+ });
538
+ }).forEach(function (layer) {
539
+ return unregisterEscapeLayer({
540
+ element: layer.element
541
+ });
542
+ });
543
+ }
544
+
545
+ /**
546
+ * Checks whether a layer accepts Escape, using closedby and modality for dialogs.
547
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#computed-closed-by-state
548
+ */
549
+ function acceptsEscape(_ref21) {
550
+ var _layer$element$getAtt;
551
+ var layer = _ref21.layer;
552
+ if (layer.type === 'popover') {
553
+ return true;
554
+ }
555
+ var closedBy = (_layer$element$getAtt = layer.element.getAttribute('closedby')) === null || _layer$element$getAtt === void 0 ? void 0 : _layer$element$getAtt.toLowerCase();
556
+ if (closedBy === 'none') {
557
+ return false;
558
+ }
559
+ if (closedBy === 'any' || closedBy === 'closerequest') {
560
+ return true;
561
+ }
562
+ // Missing or invalid closedby enables Escape for modal dialogs only.
563
+ return Boolean(layer.element._isModal);
564
+ }
565
+
566
+ /**
567
+ * Dismisses the latest eligible Escape target, allowing application handlers to prevent dialog cancellation.
568
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#set-up-the-dialog-close-watcher
569
+ */
502
570
  function handleDocumentKeydown(event) {
503
- if (event.key !== 'Escape') {
571
+ // A prevented key event must not invoke its default dismissal action.
572
+ if (event.key !== 'Escape' || event.defaultPrevented) {
504
573
  return;
505
574
  }
506
- var topHint = topmostConnected({
507
- stack: openHintStack
575
+ pruneClosedEscapeLayers();
576
+
577
+ // Capture this opening so handlers cannot redirect the same Escape to another layer.
578
+ var layer = escapeLayers.findLast(function (candidate) {
579
+ return acceptsEscape({
580
+ layer: candidate
581
+ });
508
582
  });
509
- if (topHint) {
510
- topHint.hidePopover();
583
+ if (!layer) {
511
584
  return;
512
585
  }
513
- var topAuto = topmostConnected({
514
- stack: openAutoStack
515
- });
516
- if (topAuto) {
517
- topAuto.hidePopover();
586
+ if (layer.type === 'popover') {
587
+ // Popover dismissal is synchronous.
588
+ layer.element.hidePopover();
589
+ return;
518
590
  }
591
+
592
+ // Defer the dialog default action until application keydown listeners have run.
593
+ setTimeout(function () {
594
+ // Handlers may prevent Escape, change eligibility, or close and reopen the target.
595
+ if (event.defaultPrevented || !escapeLayers.includes(layer) || !isEscapeLayerOpen({
596
+ layer: layer
597
+ }) || !acceptsEscape({
598
+ layer: layer
599
+ })) {
600
+ return;
601
+ }
602
+ var allowed = layer.element.dispatchEvent(new Event('cancel', {
603
+ cancelable: true,
604
+ bubbles: false
605
+ }));
606
+ // The cancel handler can close or reopen the dialog; only close the original opening.
607
+ if (allowed && escapeLayers.includes(layer) && isEscapeLayerOpen({
608
+ layer: layer
609
+ })) {
610
+ // close() dispatches beforetoggle and queues toggle and close notifications.
611
+ layer.element.close();
612
+ }
613
+ }, 0);
519
614
  }
520
615
  if (typeof HTMLElement !== 'undefined') {
521
616
  var schedulePopoverToggle = createToggleScheduler();
@@ -597,6 +692,12 @@ if (typeof HTMLElement !== 'undefined') {
597
692
  });
598
693
  if (stack && !stack.includes(this)) {
599
694
  stack.push(this);
695
+ registerEscapeLayer({
696
+ layer: {
697
+ type: 'popover',
698
+ element: this
699
+ }
700
+ });
600
701
  }
601
702
  clearOpacityOnOpenToggle({
602
703
  element: this
@@ -632,6 +733,9 @@ if (typeof HTMLElement !== 'undefined') {
632
733
  cancelable: false
633
734
  });
634
735
  this.removeAttribute('data-popover-open');
736
+ unregisterEscapeLayer({
737
+ element: this
738
+ });
635
739
  var stack = stackForMode({
636
740
  mode: getPopoverMode({
637
741
  element: this
@@ -763,6 +867,12 @@ if (typeof HTMLDialogElement !== 'undefined') {
763
867
  });
764
868
  this.setAttribute('open', '');
765
869
  this._isModal = true;
870
+ registerEscapeLayer({
871
+ layer: {
872
+ type: 'dialog',
873
+ element: this
874
+ }
875
+ });
766
876
  scheduleDialogToggle({
767
877
  dialog: this,
768
878
  newState: 'open',
@@ -785,6 +895,13 @@ if (typeof HTMLDialogElement !== 'undefined') {
785
895
  });
786
896
  this.setAttribute('open', '');
787
897
  this._isModal = false;
898
+ // Non-modal dialogs can opt into Escape through closedby.
899
+ registerEscapeLayer({
900
+ layer: {
901
+ type: 'dialog',
902
+ element: this
903
+ }
904
+ });
788
905
  scheduleDialogToggle({
789
906
  dialog: this,
790
907
  newState: 'open',
@@ -808,6 +925,9 @@ if (typeof HTMLDialogElement !== 'undefined') {
808
925
  cancelable: false
809
926
  });
810
927
  this.removeAttribute('open');
928
+ unregisterEscapeLayer({
929
+ element: this
930
+ });
811
931
  this._isModal = false;
812
932
  if (returnValue !== undefined) {
813
933
  this.returnValue = returnValue;
@@ -1,11 +1,12 @@
1
1
  /* dialog-content.tsx generated by @compiled/babel-plugin v3.0.2 */
2
2
  import "./dialog-content.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { forwardRef, useCallback, useId, useLayoutEffect, useRef } from 'react';
4
+ import React, { forwardRef, useCallback, useId, useRef } from 'react';
5
5
  import { cx } from '@compiled/react';
6
6
  import { bindAll } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
8
  import noop from '@atlaskit/ds-lib/noop';
9
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
9
10
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
11
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
11
12
  import { useFocusWrap } from '../internal/use-focus-wrap';
@@ -1,5 +1,5 @@
1
- import { useLayoutEffect } from 'react';
2
1
  import { getDocument } from '@atlaskit/browser-apis';
2
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
3
3
  /**
4
4
  * __DialogScrollLock__
5
5
  *
@@ -7,7 +7,8 @@
7
7
  * See `notes/decisions/anchored-popover-at-point.md` for why this is a sibling
8
8
  * hook instead of an anchor union inside `useAnchoredPopover`.
9
9
  */
10
- import { useLayoutEffect, useRef } from 'react';
10
+ import { useRef } from 'react';
11
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
11
12
  import { useAnchoredPopover } from './use-anchored-popover';
12
13
 
13
14
  /**
@@ -10,8 +10,9 @@
10
10
  * See `notes/architecture/positioning.md` and
11
11
  * `notes/decisions/fit-available-space.md`.
12
12
  */
13
- import { useLayoutEffect, useMemo } from 'react';
13
+ import { useMemo } from 'react';
14
14
  import { useId } from '@atlaskit/ds-lib/use-id';
15
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
15
16
  import { applyAnchorPositioning } from './anchor-positioning/apply-anchor-positioning';
16
17
  import { getAnchoredPopoverSizeDeclarations } from './anchored-popover-size';
17
18
  import { applyJavaScriptFallbackPositioning } from './javascript-fallback/apply-javascript-fallback-positioning';
@@ -1,4 +1,5 @@
1
- import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
1
+ import { useCallback, useEffect, useReducer, useRef } from 'react';
2
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
2
3
  import { isNativeElementOpen } from './is-native-element-open';
3
4
  import { prefersReducedMotion } from './reduced-motion';
4
5
  function runCancellableSettlement({
@@ -1,11 +1,12 @@
1
1
  /* popover.tsx generated by @compiled/babel-plugin v3.0.2 */
2
2
  import "./popover.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { forwardRef, useCallback, useId, useLayoutEffect, useMemo, useRef } from 'react';
4
+ import React, { forwardRef, useCallback, useId, useMemo, useRef } from 'react';
5
5
  import { cx } from '@compiled/react';
6
6
  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
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
9
10
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
11
  import { isNativeElementOpen } from '../internal/is-native-element-open';
11
12
  import { resolvePlacement } from '../internal/resolve-placement';
@@ -1,3 +1,5 @@
1
+ // Node supports findLast, but the package defaults to older array type declarations.
2
+ /// <reference lib="es2023.array" />
1
3
  /* eslint-disable compat/compat */
2
4
  /**
3
5
  * Popover API & HTMLDialogElement polyfills for jsdom.
@@ -495,41 +497,129 @@ function handleDocumentPointerup(event) {
495
497
  });
496
498
  }
497
499
 
498
- // Topmost still-connected popover in `stack`, pruning disconnected entries
499
- // (test pollution where a popover was removed from the DOM without hidePopover).
500
- // Polyfill-only defensive cleanup; not in the spec.
501
- function topmostConnected({
502
- stack
500
+ /**
501
+ * Represents one dialog or popover opening so queued Escape requests cannot close a later opening.
502
+ */
503
+
504
+ /**
505
+ * Tracks dialog and popover Escape targets in opening order.
506
+ */
507
+ const escapeLayers = [];
508
+
509
+ /**
510
+ * Removes an opening from Escape tracking so it cannot receive a later dismissal request.
511
+ */
512
+ function unregisterEscapeLayer({
513
+ element
503
514
  }) {
504
- while (stack.length > 0) {
505
- const candidate = stack[stack.length - 1];
506
- if (candidate.isConnected) {
507
- return candidate;
508
- }
509
- stack.pop();
515
+ const index = escapeLayers.findIndex(layer => layer.element === element);
516
+ if (index !== -1) {
517
+ escapeLayers.splice(index, 1);
510
518
  }
511
- return null;
512
519
  }
513
520
 
514
- // Escape closes the topmost open popover. Hints are dismissed before autos.
515
- // Spec: https://html.spec.whatwg.org/multipage/popover.html#close-watcher
521
+ /**
522
+ * Records a new opening, replacing any stale registration for the same element.
523
+ */
524
+ function registerEscapeLayer({
525
+ layer
526
+ }) {
527
+ unregisterEscapeLayer({
528
+ element: layer.element
529
+ });
530
+ escapeLayers.push(layer);
531
+ }
532
+
533
+ /**
534
+ * Checks whether a tracked element is still connected and open, including direct DOM changes.
535
+ */
536
+ function isEscapeLayerOpen({
537
+ layer
538
+ }) {
539
+ return layer.element.isConnected && (layer.type === 'dialog' ? layer.element.open : isPopoverOpen({
540
+ element: layer.element
541
+ }));
542
+ }
543
+
544
+ /**
545
+ * Removes registrations for closed or disconnected elements before choosing an Escape target.
546
+ */
547
+ function pruneClosedEscapeLayers() {
548
+ escapeLayers.filter(layer => !isEscapeLayerOpen({
549
+ layer
550
+ })).forEach(layer => unregisterEscapeLayer({
551
+ element: layer.element
552
+ }));
553
+ }
554
+
555
+ /**
556
+ * Checks whether a layer accepts Escape, using closedby and modality for dialogs.
557
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#computed-closed-by-state
558
+ */
559
+ function acceptsEscape({
560
+ layer
561
+ }) {
562
+ var _layer$element$getAtt;
563
+ if (layer.type === 'popover') {
564
+ return true;
565
+ }
566
+ const closedBy = (_layer$element$getAtt = layer.element.getAttribute('closedby')) === null || _layer$element$getAtt === void 0 ? void 0 : _layer$element$getAtt.toLowerCase();
567
+ if (closedBy === 'none') {
568
+ return false;
569
+ }
570
+ if (closedBy === 'any' || closedBy === 'closerequest') {
571
+ return true;
572
+ }
573
+ // Missing or invalid closedby enables Escape for modal dialogs only.
574
+ return Boolean(layer.element._isModal);
575
+ }
576
+
577
+ /**
578
+ * Dismisses the latest eligible Escape target, allowing application handlers to prevent dialog cancellation.
579
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#set-up-the-dialog-close-watcher
580
+ */
516
581
  function handleDocumentKeydown(event) {
517
- if (event.key !== 'Escape') {
582
+ // A prevented key event must not invoke its default dismissal action.
583
+ if (event.key !== 'Escape' || event.defaultPrevented) {
518
584
  return;
519
585
  }
520
- const topHint = topmostConnected({
521
- stack: openHintStack
522
- });
523
- if (topHint) {
524
- topHint.hidePopover();
586
+ pruneClosedEscapeLayers();
587
+
588
+ // Capture this opening so handlers cannot redirect the same Escape to another layer.
589
+ const layer = escapeLayers.findLast(candidate => acceptsEscape({
590
+ layer: candidate
591
+ }));
592
+ if (!layer) {
525
593
  return;
526
594
  }
527
- const topAuto = topmostConnected({
528
- stack: openAutoStack
529
- });
530
- if (topAuto) {
531
- topAuto.hidePopover();
595
+ if (layer.type === 'popover') {
596
+ // Popover dismissal is synchronous.
597
+ layer.element.hidePopover();
598
+ return;
532
599
  }
600
+
601
+ // Defer the dialog default action until application keydown listeners have run.
602
+ setTimeout(() => {
603
+ // Handlers may prevent Escape, change eligibility, or close and reopen the target.
604
+ if (event.defaultPrevented || !escapeLayers.includes(layer) || !isEscapeLayerOpen({
605
+ layer
606
+ }) || !acceptsEscape({
607
+ layer
608
+ })) {
609
+ return;
610
+ }
611
+ const allowed = layer.element.dispatchEvent(new Event('cancel', {
612
+ cancelable: true,
613
+ bubbles: false
614
+ }));
615
+ // The cancel handler can close or reopen the dialog; only close the original opening.
616
+ if (allowed && escapeLayers.includes(layer) && isEscapeLayerOpen({
617
+ layer
618
+ })) {
619
+ // close() dispatches beforetoggle and queues toggle and close notifications.
620
+ layer.element.close();
621
+ }
622
+ }, 0);
533
623
  }
534
624
  if (typeof HTMLElement !== 'undefined') {
535
625
  const schedulePopoverToggle = createToggleScheduler();
@@ -611,6 +701,12 @@ if (typeof HTMLElement !== 'undefined') {
611
701
  });
612
702
  if (stack && !stack.includes(this)) {
613
703
  stack.push(this);
704
+ registerEscapeLayer({
705
+ layer: {
706
+ type: 'popover',
707
+ element: this
708
+ }
709
+ });
614
710
  }
615
711
  clearOpacityOnOpenToggle({
616
712
  element: this
@@ -646,6 +742,9 @@ if (typeof HTMLElement !== 'undefined') {
646
742
  cancelable: false
647
743
  });
648
744
  this.removeAttribute('data-popover-open');
745
+ unregisterEscapeLayer({
746
+ element: this
747
+ });
649
748
  const stack = stackForMode({
650
749
  mode: getPopoverMode({
651
750
  element: this
@@ -773,6 +872,12 @@ if (typeof HTMLDialogElement !== 'undefined') {
773
872
  });
774
873
  this.setAttribute('open', '');
775
874
  this._isModal = true;
875
+ registerEscapeLayer({
876
+ layer: {
877
+ type: 'dialog',
878
+ element: this
879
+ }
880
+ });
776
881
  scheduleDialogToggle({
777
882
  dialog: this,
778
883
  newState: 'open',
@@ -795,6 +900,13 @@ if (typeof HTMLDialogElement !== 'undefined') {
795
900
  });
796
901
  this.setAttribute('open', '');
797
902
  this._isModal = false;
903
+ // Non-modal dialogs can opt into Escape through closedby.
904
+ registerEscapeLayer({
905
+ layer: {
906
+ type: 'dialog',
907
+ element: this
908
+ }
909
+ });
798
910
  scheduleDialogToggle({
799
911
  dialog: this,
800
912
  newState: 'open',
@@ -818,6 +930,9 @@ if (typeof HTMLDialogElement !== 'undefined') {
818
930
  cancelable: false
819
931
  });
820
932
  this.removeAttribute('open');
933
+ unregisterEscapeLayer({
934
+ element: this
935
+ });
821
936
  this._isModal = false;
822
937
  if (returnValue !== undefined) {
823
938
  this.returnValue = returnValue;
@@ -1,11 +1,12 @@
1
1
  /* dialog-content.tsx generated by @compiled/babel-plugin v3.0.2 */
2
2
  import "./dialog-content.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { forwardRef, useCallback, useId, useLayoutEffect, useRef } from 'react';
4
+ import React, { forwardRef, useCallback, useId, useRef } from 'react';
5
5
  import { cx } from '@compiled/react';
6
6
  import { bindAll } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
8
  import noop from '@atlaskit/ds-lib/noop';
9
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
9
10
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
11
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
11
12
  import { useFocusWrap } from '../internal/use-focus-wrap';
@@ -1,5 +1,5 @@
1
- import { useLayoutEffect } from 'react';
2
1
  import { getDocument } from '@atlaskit/browser-apis';
2
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
3
3
  /**
4
4
  * __DialogScrollLock__
5
5
  *
@@ -12,7 +12,8 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
12
12
  * See `notes/decisions/anchored-popover-at-point.md` for why this is a sibling
13
13
  * hook instead of an anchor union inside `useAnchoredPopover`.
14
14
  */
15
- import { useLayoutEffect, useRef } from 'react';
15
+ import { useRef } from 'react';
16
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
16
17
  import { useAnchoredPopover } from './use-anchored-popover';
17
18
 
18
19
  /**
@@ -10,8 +10,9 @@
10
10
  * See `notes/architecture/positioning.md` and
11
11
  * `notes/decisions/fit-available-space.md`.
12
12
  */
13
- import { useLayoutEffect, useMemo } from 'react';
13
+ import { useMemo } from 'react';
14
14
  import { useId } from '@atlaskit/ds-lib/use-id';
15
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
15
16
  import { applyAnchorPositioning } from './anchor-positioning/apply-anchor-positioning';
16
17
  import { getAnchoredPopoverSizeDeclarations } from './anchored-popover-size';
17
18
  import { applyJavaScriptFallbackPositioning } from './javascript-fallback/apply-javascript-fallback-positioning';
@@ -2,7 +2,8 @@ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  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
- import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
5
+ import { useCallback, useEffect, useReducer, useRef } from 'react';
6
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
6
7
  import { isNativeElementOpen } from './is-native-element-open';
7
8
  import { prefersReducedMotion } from './reduced-motion';
8
9
  function runCancellableSettlement(_ref) {
@@ -1,11 +1,12 @@
1
1
  /* popover.tsx generated by @compiled/babel-plugin v3.0.2 */
2
2
  import "./popover.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { forwardRef, useCallback, useId, useLayoutEffect, useMemo, useRef } from 'react';
4
+ import React, { forwardRef, useCallback, useId, useMemo, useRef } from 'react';
5
5
  import { cx } from '@compiled/react';
6
6
  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
+ import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
9
10
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
11
  import { isNativeElementOpen } from '../internal/is-native-element-open';
11
12
  import { resolvePlacement } from '../internal/resolve-placement';
@@ -1,3 +1,5 @@
1
+ // Node supports findLast, but the package defaults to older array type declarations.
2
+ /// <reference lib="es2023.array" />
1
3
  /* eslint-disable compat/compat */
2
4
  /**
3
5
  * Popover API & HTMLDialogElement polyfills for jsdom.
@@ -480,40 +482,133 @@ function handleDocumentPointerup(event) {
480
482
  });
481
483
  }
482
484
 
483
- // Topmost still-connected popover in `stack`, pruning disconnected entries
484
- // (test pollution where a popover was removed from the DOM without hidePopover).
485
- // Polyfill-only defensive cleanup; not in the spec.
486
- function topmostConnected(_ref18) {
487
- var stack = _ref18.stack;
488
- while (stack.length > 0) {
489
- var candidate = stack[stack.length - 1];
490
- if (candidate.isConnected) {
491
- return candidate;
492
- }
493
- stack.pop();
485
+ /**
486
+ * Represents one dialog or popover opening so queued Escape requests cannot close a later opening.
487
+ */
488
+
489
+ /**
490
+ * Tracks dialog and popover Escape targets in opening order.
491
+ */
492
+ var escapeLayers = [];
493
+
494
+ /**
495
+ * Removes an opening from Escape tracking so it cannot receive a later dismissal request.
496
+ */
497
+ function unregisterEscapeLayer(_ref18) {
498
+ var element = _ref18.element;
499
+ var index = escapeLayers.findIndex(function (layer) {
500
+ return layer.element === element;
501
+ });
502
+ if (index !== -1) {
503
+ escapeLayers.splice(index, 1);
494
504
  }
495
- return null;
496
505
  }
497
506
 
498
- // Escape closes the topmost open popover. Hints are dismissed before autos.
499
- // Spec: https://html.spec.whatwg.org/multipage/popover.html#close-watcher
507
+ /**
508
+ * Records a new opening, replacing any stale registration for the same element.
509
+ */
510
+ function registerEscapeLayer(_ref19) {
511
+ var layer = _ref19.layer;
512
+ unregisterEscapeLayer({
513
+ element: layer.element
514
+ });
515
+ escapeLayers.push(layer);
516
+ }
517
+
518
+ /**
519
+ * Checks whether a tracked element is still connected and open, including direct DOM changes.
520
+ */
521
+ function isEscapeLayerOpen(_ref20) {
522
+ var layer = _ref20.layer;
523
+ return layer.element.isConnected && (layer.type === 'dialog' ? layer.element.open : isPopoverOpen({
524
+ element: layer.element
525
+ }));
526
+ }
527
+
528
+ /**
529
+ * Removes registrations for closed or disconnected elements before choosing an Escape target.
530
+ */
531
+ function pruneClosedEscapeLayers() {
532
+ escapeLayers.filter(function (layer) {
533
+ return !isEscapeLayerOpen({
534
+ layer: layer
535
+ });
536
+ }).forEach(function (layer) {
537
+ return unregisterEscapeLayer({
538
+ element: layer.element
539
+ });
540
+ });
541
+ }
542
+
543
+ /**
544
+ * Checks whether a layer accepts Escape, using closedby and modality for dialogs.
545
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#computed-closed-by-state
546
+ */
547
+ function acceptsEscape(_ref21) {
548
+ var _layer$element$getAtt;
549
+ var layer = _ref21.layer;
550
+ if (layer.type === 'popover') {
551
+ return true;
552
+ }
553
+ var closedBy = (_layer$element$getAtt = layer.element.getAttribute('closedby')) === null || _layer$element$getAtt === void 0 ? void 0 : _layer$element$getAtt.toLowerCase();
554
+ if (closedBy === 'none') {
555
+ return false;
556
+ }
557
+ if (closedBy === 'any' || closedBy === 'closerequest') {
558
+ return true;
559
+ }
560
+ // Missing or invalid closedby enables Escape for modal dialogs only.
561
+ return Boolean(layer.element._isModal);
562
+ }
563
+
564
+ /**
565
+ * Dismisses the latest eligible Escape target, allowing application handlers to prevent dialog cancellation.
566
+ * Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#set-up-the-dialog-close-watcher
567
+ */
500
568
  function handleDocumentKeydown(event) {
501
- if (event.key !== 'Escape') {
569
+ // A prevented key event must not invoke its default dismissal action.
570
+ if (event.key !== 'Escape' || event.defaultPrevented) {
502
571
  return;
503
572
  }
504
- var topHint = topmostConnected({
505
- stack: openHintStack
573
+ pruneClosedEscapeLayers();
574
+
575
+ // Capture this opening so handlers cannot redirect the same Escape to another layer.
576
+ var layer = escapeLayers.findLast(function (candidate) {
577
+ return acceptsEscape({
578
+ layer: candidate
579
+ });
506
580
  });
507
- if (topHint) {
508
- topHint.hidePopover();
581
+ if (!layer) {
509
582
  return;
510
583
  }
511
- var topAuto = topmostConnected({
512
- stack: openAutoStack
513
- });
514
- if (topAuto) {
515
- topAuto.hidePopover();
584
+ if (layer.type === 'popover') {
585
+ // Popover dismissal is synchronous.
586
+ layer.element.hidePopover();
587
+ return;
516
588
  }
589
+
590
+ // Defer the dialog default action until application keydown listeners have run.
591
+ setTimeout(function () {
592
+ // Handlers may prevent Escape, change eligibility, or close and reopen the target.
593
+ if (event.defaultPrevented || !escapeLayers.includes(layer) || !isEscapeLayerOpen({
594
+ layer: layer
595
+ }) || !acceptsEscape({
596
+ layer: layer
597
+ })) {
598
+ return;
599
+ }
600
+ var allowed = layer.element.dispatchEvent(new Event('cancel', {
601
+ cancelable: true,
602
+ bubbles: false
603
+ }));
604
+ // The cancel handler can close or reopen the dialog; only close the original opening.
605
+ if (allowed && escapeLayers.includes(layer) && isEscapeLayerOpen({
606
+ layer: layer
607
+ })) {
608
+ // close() dispatches beforetoggle and queues toggle and close notifications.
609
+ layer.element.close();
610
+ }
611
+ }, 0);
517
612
  }
518
613
  if (typeof HTMLElement !== 'undefined') {
519
614
  var schedulePopoverToggle = createToggleScheduler();
@@ -595,6 +690,12 @@ if (typeof HTMLElement !== 'undefined') {
595
690
  });
596
691
  if (stack && !stack.includes(this)) {
597
692
  stack.push(this);
693
+ registerEscapeLayer({
694
+ layer: {
695
+ type: 'popover',
696
+ element: this
697
+ }
698
+ });
598
699
  }
599
700
  clearOpacityOnOpenToggle({
600
701
  element: this
@@ -630,6 +731,9 @@ if (typeof HTMLElement !== 'undefined') {
630
731
  cancelable: false
631
732
  });
632
733
  this.removeAttribute('data-popover-open');
734
+ unregisterEscapeLayer({
735
+ element: this
736
+ });
633
737
  var stack = stackForMode({
634
738
  mode: getPopoverMode({
635
739
  element: this
@@ -761,6 +865,12 @@ if (typeof HTMLDialogElement !== 'undefined') {
761
865
  });
762
866
  this.setAttribute('open', '');
763
867
  this._isModal = true;
868
+ registerEscapeLayer({
869
+ layer: {
870
+ type: 'dialog',
871
+ element: this
872
+ }
873
+ });
764
874
  scheduleDialogToggle({
765
875
  dialog: this,
766
876
  newState: 'open',
@@ -783,6 +893,13 @@ if (typeof HTMLDialogElement !== 'undefined') {
783
893
  });
784
894
  this.setAttribute('open', '');
785
895
  this._isModal = false;
896
+ // Non-modal dialogs can opt into Escape through closedby.
897
+ registerEscapeLayer({
898
+ layer: {
899
+ type: 'dialog',
900
+ element: this
901
+ }
902
+ });
786
903
  scheduleDialogToggle({
787
904
  dialog: this,
788
905
  newState: 'open',
@@ -806,6 +923,9 @@ if (typeof HTMLDialogElement !== 'undefined') {
806
923
  cancelable: false
807
924
  });
808
925
  this.removeAttribute('open');
926
+ unregisterEscapeLayer({
927
+ element: this
928
+ });
809
929
  this._isModal = false;
810
930
  if (returnValue !== undefined) {
811
931
  this.returnValue = returnValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "6.2.3",
3
+ "version": "6.2.5",
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",