@atlaskit/top-layer 1.6.1 → 1.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`7793dc62dd564`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/7793dc62dd564) -
8
+ Changing how animations work. Removing `/animations` entry point
9
+
10
+ ## 1.7.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [`406346f89178f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/406346f89178f) -
15
+ Fix a Safari (WebKit) layout bug where `Popover` content could collapse to `0px`. When a
16
+ `max-height: 100%` flex column with a `flex: 1 1 auto; overflow: auto` child is placed inside a
17
+ popover, the user-agent `height: fit-content` default (an indefinite block size) makes the column
18
+ collapse on WebKit. The `Popover` reset now sets `height: auto` to remove the indefinite block
19
+ size; width is left unchanged so anchor-width matching is unaffected.
20
+
21
+ The `Dialog` primitive is intentionally left unchanged for that reset: applying the same reset
22
+ there would stretch the modal `<dialog>` to fill the viewport and force a layout-centering opinion
23
+ onto an otherwise unopinionated primitive. See `notes/decisions/safari-popover-flex-collapse.md`.
24
+
25
+ Also fix a Safari (WebKit) bug where pressing `Escape` while a `popover="auto"` (for example a
26
+ menu) is open inside a modal `Dialog` closed the entire dialog instead of only the popover. On
27
+ WebKit the same Escape also fires the dialog's native `cancel` event. The `Dialog` primitive now
28
+ snapshots — at Escape keydown, before the browser's close action — whether a light-dismissable
29
+ (`auto`/`hint`) popover is open inside it, and if so keeps the dialog open so only the popover is
30
+ dismissed, matching Chromium and Firefox. See
31
+ `notes/decisions/safari-escape-nested-popover-in-dialog.md`.
32
+
3
33
  ## 1.6.1
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -178,6 +178,19 @@ details.
178
178
  | Background inertness (modal dialogs) | ✓ |
179
179
  | Close reasons (Dialog) | ✓ |
180
180
 
181
+ ## Known limitations
182
+
183
+ - **Safari `<dialog>` flex-collapse.** On WebKit, a `max-height: 100%` flex column with a
184
+ `flex: 1 1 auto; overflow: auto` child placed directly inside a `Dialog` collapses to `0px` (the
185
+ block-axis `fit-content` user-agent default is an indefinite height). `Popover` sets
186
+ `height: auto` to avoid this; `Dialog` deliberately does not, to keep the primitive free of layout
187
+ opinions (see
188
+ [`notes/decisions/safari-popover-flex-collapse.md`](notes/decisions/safari-popover-flex-collapse.md)).
189
+ A consumer that builds this shape inside a `Dialog` should give the column a definite height or
190
+ apply its own `height: auto` reset on the surface.
191
+ - TODO: expand this into full public consumer documentation (the recommended workaround and a
192
+ worked example) before the top-layer `Dialog` is broadly adopted.
193
+
181
194
  ## Documentation
182
195
 
183
196
  See [`notes/`](notes/) for project documentation, architecture decisions, migration records, and
@@ -16,6 +16,7 @@ var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
16
16
  var _openLayerObserver = require("@atlaskit/layering/experimental/open-layer-observer");
17
17
  var _useAnimatedVisibility = require("../internal/use-animated-visibility");
18
18
  var _useFocusWrap = require("../internal/use-focus-wrap");
19
+ var _useSafariEscapeFix2 = require("../internal/use-safari-escape-fix");
19
20
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
20
21
  // These animation styles use the non-strict Compiled cssMap because they rely
21
22
  // on `@starting-style`, `[open]`, and `allow-discrete`. Keep them in this
@@ -124,6 +125,14 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
124
125
  }
125
126
  }, [isOpen]);
126
127
 
128
+ // Safari bug: escape closes open dialog and popovers, rather than just innermost popover
129
+ // See notes/decisions/safari-escape-nested-popover-in-dialog.md
130
+ var _useSafariEscapeFix = (0, _useSafariEscapeFix2.useSafariEscapeFix)({
131
+ dialogRef: ownRef,
132
+ isVisible: isVisible
133
+ }),
134
+ shouldIgnoreEscape = _useSafariEscapeFix.shouldIgnoreEscape;
135
+
127
136
  // Handle native Escape (cancel event)
128
137
  var handleCancel = (0, _react.useCallback)(function (event) {
129
138
  // Only handle a cancel targeting THIS dialog. Native `cancel` does not
@@ -135,10 +144,16 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
135
144
  return;
136
145
  }
137
146
  event.preventDefault();
147
+ // Spurious Safari `cancel`: the keydown snapshot saw an open child
148
+ // popover, so this Escape belongs to that popover (light-dismissed
149
+ // natively). Keep the dialog open instead of forwarding to `onClose`.
150
+ if (shouldIgnoreEscape()) {
151
+ return;
152
+ }
138
153
  onClose({
139
154
  reason: 'escape'
140
155
  });
141
- }, [onClose]);
156
+ }, [onClose, shouldIgnoreEscape]);
142
157
 
143
158
  // Handle backdrop click
144
159
  // Attached via bind-event-listener rather than a React prop so we avoid
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useSafariEscapeFix = useSafariEscapeFix;
7
+ var _react = require("react");
8
+ var _bindEventListener = require("bind-event-listener");
9
+ var _isSafari = require("@atlaskit/ds-lib/is-safari");
10
+ /**
11
+ * Safari-only guard: WebKit fires dialog `cancel` on Escape even when a child
12
+ * `auto`/`hint` popover consumed it, closing the dialog too.
13
+ * See notes/decisions/safari-escape-nested-popover-in-dialog.md
14
+ */
15
+ function useSafariEscapeFix(_ref) {
16
+ var dialogRef = _ref.dialogRef,
17
+ isVisible = _ref.isVisible;
18
+ var childPopoverOpenAtEscapeRef = (0, _react.useRef)(false);
19
+ (0, _react.useEffect)(function () {
20
+ if (!(0, _isSafari.isSafari)()) {
21
+ return;
22
+ }
23
+ var dialog = dialogRef.current;
24
+ if (!dialog) {
25
+ return;
26
+ }
27
+
28
+ // Bind to `document`, not the dialog: on Safari focus can be on `<body>`
29
+ // (buttons are not focused on click), where a dialog-scoped listener would
30
+ // never fire. The query below stays scoped to this dialog's subtree.
31
+ return (0, _bindEventListener.bind)(document, {
32
+ type: 'keydown',
33
+ listener: function listener(event) {
34
+ if (event.key !== 'Escape') {
35
+ return;
36
+ }
37
+ try {
38
+ childPopoverOpenAtEscapeRef.current = Boolean(dialog.querySelector('[popover="auto"]:popover-open, [popover="hint"]:popover-open'));
39
+ } catch (_unused) {
40
+ // `:popover-open` is unsupported in some environments (e.g. jsdom);
41
+ // treat as "no child popover" so a plain Escape still closes the dialog.
42
+ childPopoverOpenAtEscapeRef.current = false;
43
+ }
44
+ },
45
+ options: {
46
+ capture: true
47
+ }
48
+ });
49
+ }, [dialogRef, isVisible]);
50
+ var api = (0, _react.useMemo)(function () {
51
+ return {
52
+ shouldIgnoreEscape: function shouldIgnoreEscape() {
53
+ if (childPopoverOpenAtEscapeRef.current) {
54
+ childPopoverOpenAtEscapeRef.current = false;
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ };
60
+ }, []);
61
+ return api;
62
+ }
@@ -14,6 +14,7 @@
14
14
  ._1reoewfl{overflow-x:visible}
15
15
  ._1s9z1a5r{transition-behavior:allow-discrete}
16
16
  ._2hwxidpf{margin-right:0}
17
+ ._4t3i1wug{height:auto}
17
18
  ._5sagdn4k{animation-duration:var(--ds-duration-xshort,.1s)}
18
19
  ._bfhk1j28{background-color:transparent}
19
20
  ._ca0qidpf{padding-top:0}
@@ -42,7 +42,7 @@ var popoverAnimationStyles = {
42
42
  root: "_y44vof1f _k8m011k7 _1oecdn4k _1s9z1a5r _1o51q7pw _5sagdn4k _1pgl1lbj _15ns179n _12t71ign _n70peoah _1psg1ign _o8i7indk _bl8aglyw _156yru3m _8rmvglyw _2qm2ru3m"
43
43
  };
44
44
  var styles = {
45
- root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
45
+ root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28 _4t3i1wug"
46
46
  };
47
47
  /**
48
48
  * Roles registered with the open layer observer as `popup`, so `closeLayers()`
@@ -8,6 +8,7 @@ import noop from '@atlaskit/ds-lib/noop';
8
8
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
9
9
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
10
10
  import { useFocusWrap } from '../internal/use-focus-wrap';
11
+ import { useSafariEscapeFix } from '../internal/use-safari-escape-fix';
11
12
  // These animation styles use the non-strict Compiled cssMap because they rely
12
13
  // on `@starting-style`, `[open]`, and `allow-discrete`. Keep them in this
13
14
  // component so the Compiled transform can statically extract every referenced
@@ -116,6 +117,15 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
116
117
  }
117
118
  }, [isOpen]);
118
119
 
120
+ // Safari bug: escape closes open dialog and popovers, rather than just innermost popover
121
+ // See notes/decisions/safari-escape-nested-popover-in-dialog.md
122
+ const {
123
+ shouldIgnoreEscape
124
+ } = useSafariEscapeFix({
125
+ dialogRef: ownRef,
126
+ isVisible
127
+ });
128
+
119
129
  // Handle native Escape (cancel event)
120
130
  const handleCancel = useCallback(event => {
121
131
  // Only handle a cancel targeting THIS dialog. Native `cancel` does not
@@ -127,10 +137,16 @@ export const Dialog = /*#__PURE__*/forwardRef(function Dialog({
127
137
  return;
128
138
  }
129
139
  event.preventDefault();
140
+ // Spurious Safari `cancel`: the keydown snapshot saw an open child
141
+ // popover, so this Escape belongs to that popover (light-dismissed
142
+ // natively). Keep the dialog open instead of forwarding to `onClose`.
143
+ if (shouldIgnoreEscape()) {
144
+ return;
145
+ }
130
146
  onClose({
131
147
  reason: 'escape'
132
148
  });
133
- }, [onClose]);
149
+ }, [onClose, shouldIgnoreEscape]);
134
150
 
135
151
  // Handle backdrop click
136
152
  // Attached via bind-event-listener rather than a React prop so we avoid
@@ -0,0 +1,58 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import { isSafari } from '@atlaskit/ds-lib/is-safari';
4
+
5
+ /**
6
+ * Safari-only guard: WebKit fires dialog `cancel` on Escape even when a child
7
+ * `auto`/`hint` popover consumed it, closing the dialog too.
8
+ * See notes/decisions/safari-escape-nested-popover-in-dialog.md
9
+ */
10
+ export function useSafariEscapeFix({
11
+ dialogRef,
12
+ isVisible
13
+ }) {
14
+ const childPopoverOpenAtEscapeRef = useRef(false);
15
+ useEffect(() => {
16
+ if (!isSafari()) {
17
+ return;
18
+ }
19
+ const dialog = dialogRef.current;
20
+ if (!dialog) {
21
+ return;
22
+ }
23
+
24
+ // Bind to `document`, not the dialog: on Safari focus can be on `<body>`
25
+ // (buttons are not focused on click), where a dialog-scoped listener would
26
+ // never fire. The query below stays scoped to this dialog's subtree.
27
+ return bind(document, {
28
+ type: 'keydown',
29
+ listener(event) {
30
+ if (event.key !== 'Escape') {
31
+ return;
32
+ }
33
+ try {
34
+ childPopoverOpenAtEscapeRef.current = Boolean(dialog.querySelector('[popover="auto"]:popover-open, [popover="hint"]:popover-open'));
35
+ } catch {
36
+ // `:popover-open` is unsupported in some environments (e.g. jsdom);
37
+ // treat as "no child popover" so a plain Escape still closes the dialog.
38
+ childPopoverOpenAtEscapeRef.current = false;
39
+ }
40
+ },
41
+ options: {
42
+ capture: true
43
+ }
44
+ });
45
+ }, [dialogRef, isVisible]);
46
+ const api = useMemo(() => {
47
+ return {
48
+ shouldIgnoreEscape() {
49
+ if (childPopoverOpenAtEscapeRef.current) {
50
+ childPopoverOpenAtEscapeRef.current = false;
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ };
56
+ }, []);
57
+ return api;
58
+ }
@@ -14,6 +14,7 @@
14
14
  ._1reoewfl{overflow-x:visible}
15
15
  ._1s9z1a5r{transition-behavior:allow-discrete}
16
16
  ._2hwxidpf{margin-right:0}
17
+ ._4t3i1wug{height:auto}
17
18
  ._5sagdn4k{animation-duration:var(--ds-duration-xshort,.1s)}
18
19
  ._bfhk1j28{background-color:transparent}
19
20
  ._ca0qidpf{padding-top:0}
@@ -33,7 +33,7 @@ const popoverAnimationStyles = {
33
33
  root: "_y44vof1f _k8m011k7 _1oecdn4k _1s9z1a5r _1o51q7pw _5sagdn4k _1pgl1lbj _15ns179n _12t71ign _n70peoah _1psg1ign _o8i7indk _bl8aglyw _156yru3m _8rmvglyw _2qm2ru3m"
34
34
  };
35
35
  const styles = {
36
- root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
36
+ root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28 _4t3i1wug"
37
37
  };
38
38
  /**
39
39
  * Roles registered with the open layer observer as `popup`, so `closeLayers()`
@@ -8,6 +8,7 @@ import noop from '@atlaskit/ds-lib/noop';
8
8
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
9
9
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
10
10
  import { useFocusWrap } from '../internal/use-focus-wrap';
11
+ import { useSafariEscapeFix } from '../internal/use-safari-escape-fix';
11
12
  // These animation styles use the non-strict Compiled cssMap because they rely
12
13
  // on `@starting-style`, `[open]`, and `allow-discrete`. Keep them in this
13
14
  // component so the Compiled transform can statically extract every referenced
@@ -115,6 +116,14 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
115
116
  }
116
117
  }, [isOpen]);
117
118
 
119
+ // Safari bug: escape closes open dialog and popovers, rather than just innermost popover
120
+ // See notes/decisions/safari-escape-nested-popover-in-dialog.md
121
+ var _useSafariEscapeFix = useSafariEscapeFix({
122
+ dialogRef: ownRef,
123
+ isVisible: isVisible
124
+ }),
125
+ shouldIgnoreEscape = _useSafariEscapeFix.shouldIgnoreEscape;
126
+
118
127
  // Handle native Escape (cancel event)
119
128
  var handleCancel = useCallback(function (event) {
120
129
  // Only handle a cancel targeting THIS dialog. Native `cancel` does not
@@ -126,10 +135,16 @@ export var Dialog = /*#__PURE__*/forwardRef(function Dialog(_ref, ref) {
126
135
  return;
127
136
  }
128
137
  event.preventDefault();
138
+ // Spurious Safari `cancel`: the keydown snapshot saw an open child
139
+ // popover, so this Escape belongs to that popover (light-dismissed
140
+ // natively). Keep the dialog open instead of forwarding to `onClose`.
141
+ if (shouldIgnoreEscape()) {
142
+ return;
143
+ }
129
144
  onClose({
130
145
  reason: 'escape'
131
146
  });
132
- }, [onClose]);
147
+ }, [onClose, shouldIgnoreEscape]);
133
148
 
134
149
  // Handle backdrop click
135
150
  // Attached via bind-event-listener rather than a React prop so we avoid
@@ -0,0 +1,57 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import { isSafari } from '@atlaskit/ds-lib/is-safari';
4
+
5
+ /**
6
+ * Safari-only guard: WebKit fires dialog `cancel` on Escape even when a child
7
+ * `auto`/`hint` popover consumed it, closing the dialog too.
8
+ * See notes/decisions/safari-escape-nested-popover-in-dialog.md
9
+ */
10
+ export function useSafariEscapeFix(_ref) {
11
+ var dialogRef = _ref.dialogRef,
12
+ isVisible = _ref.isVisible;
13
+ var childPopoverOpenAtEscapeRef = useRef(false);
14
+ useEffect(function () {
15
+ if (!isSafari()) {
16
+ return;
17
+ }
18
+ var dialog = dialogRef.current;
19
+ if (!dialog) {
20
+ return;
21
+ }
22
+
23
+ // Bind to `document`, not the dialog: on Safari focus can be on `<body>`
24
+ // (buttons are not focused on click), where a dialog-scoped listener would
25
+ // never fire. The query below stays scoped to this dialog's subtree.
26
+ return bind(document, {
27
+ type: 'keydown',
28
+ listener: function listener(event) {
29
+ if (event.key !== 'Escape') {
30
+ return;
31
+ }
32
+ try {
33
+ childPopoverOpenAtEscapeRef.current = Boolean(dialog.querySelector('[popover="auto"]:popover-open, [popover="hint"]:popover-open'));
34
+ } catch (_unused) {
35
+ // `:popover-open` is unsupported in some environments (e.g. jsdom);
36
+ // treat as "no child popover" so a plain Escape still closes the dialog.
37
+ childPopoverOpenAtEscapeRef.current = false;
38
+ }
39
+ },
40
+ options: {
41
+ capture: true
42
+ }
43
+ });
44
+ }, [dialogRef, isVisible]);
45
+ var api = useMemo(function () {
46
+ return {
47
+ shouldIgnoreEscape: function shouldIgnoreEscape() {
48
+ if (childPopoverOpenAtEscapeRef.current) {
49
+ childPopoverOpenAtEscapeRef.current = false;
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ };
55
+ }, []);
56
+ return api;
57
+ }
@@ -14,6 +14,7 @@
14
14
  ._1reoewfl{overflow-x:visible}
15
15
  ._1s9z1a5r{transition-behavior:allow-discrete}
16
16
  ._2hwxidpf{margin-right:0}
17
+ ._4t3i1wug{height:auto}
17
18
  ._5sagdn4k{animation-duration:var(--ds-duration-xshort,.1s)}
18
19
  ._bfhk1j28{background-color:transparent}
19
20
  ._ca0qidpf{padding-top:0}
@@ -33,7 +33,7 @@ var popoverAnimationStyles = {
33
33
  root: "_y44vof1f _k8m011k7 _1oecdn4k _1s9z1a5r _1o51q7pw _5sagdn4k _1pgl1lbj _15ns179n _12t71ign _n70peoah _1psg1ign _o8i7indk _bl8aglyw _156yru3m _8rmvglyw _2qm2ru3m"
34
34
  };
35
35
  var styles = {
36
- root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
36
+ root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28 _4t3i1wug"
37
37
  };
38
38
  /**
39
39
  * Roles registered with the open layer observer as `popup`, so `closeLayers()`
@@ -0,0 +1,12 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * Safari-only guard: WebKit fires dialog `cancel` on Escape even when a child
4
+ * `auto`/`hint` popover consumed it, closing the dialog too.
5
+ * See notes/decisions/safari-escape-nested-popover-in-dialog.md
6
+ */
7
+ export declare function useSafariEscapeFix({ dialogRef, isVisible, }: {
8
+ dialogRef: RefObject<HTMLDialogElement | null>;
9
+ isVisible: boolean;
10
+ }): {
11
+ shouldIgnoreEscape: () => boolean;
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/top-layer",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
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",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "integrationTests": {
31
31
  "additionalBrowsers": [
32
- "desktop-firefox"
32
+ "desktop-firefox",
33
+ "desktop-webkit"
33
34
  ]
34
35
  }
35
36
  },
@@ -39,7 +40,7 @@
39
40
  "@atlaskit/ds-lib": "^8.0.0",
40
41
  "@atlaskit/layering": "^4.1.0",
41
42
  "@atlaskit/react-compiler-gating": "^0.2.0",
42
- "@atlaskit/tokens": "^15.7.0",
43
+ "@atlaskit/tokens": "^15.8.0",
43
44
  "@babel/runtime": "^7.0.0",
44
45
  "@compiled/react": "^0.20.0",
45
46
  "bind-event-listener": "^3.0.0",