@atlaskit/top-layer 3.2.0 → 4.1.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/AGENTS.md +3 -1
- package/CHANGELOG.md +18 -0
- package/README.md +0 -14
- package/dist/cjs/dialog/dialog-content.js +47 -20
- package/dist/cjs/focus/is-in-same-layer.js +1 -7
- package/dist/cjs/focus/selectors.js +1 -1
- package/dist/cjs/internal/get-aria-for-trigger.js +23 -9
- package/dist/cjs/internal/role-types.js +1 -64
- package/dist/cjs/internal/use-anchor-position.js +2 -3
- package/dist/cjs/internal/use-animated-visibility.js +36 -39
- package/dist/cjs/popover/popover.js +18 -20
- package/dist/es2019/dialog/dialog-content.js +48 -22
- package/dist/es2019/focus/is-in-same-layer.js +1 -7
- package/dist/es2019/focus/selectors.js +1 -1
- package/dist/es2019/internal/get-aria-for-trigger.js +23 -10
- package/dist/es2019/internal/role-types.js +0 -59
- package/dist/es2019/internal/use-anchor-position.js +2 -3
- package/dist/es2019/internal/use-animated-visibility.js +38 -39
- package/dist/es2019/popover/popover.js +15 -17
- package/dist/esm/dialog/dialog-content.js +49 -22
- package/dist/esm/focus/is-in-same-layer.js +1 -7
- package/dist/esm/focus/selectors.js +1 -1
- package/dist/esm/internal/get-aria-for-trigger.js +23 -10
- package/dist/esm/internal/role-types.js +0 -58
- package/dist/esm/internal/use-anchor-position.js +2 -3
- package/dist/esm/internal/use-animated-visibility.js +38 -41
- package/dist/esm/popover/popover.js +17 -19
- package/dist/types/dialog/dialog-content.d.ts +2 -2
- package/dist/types/dialog/index.d.ts +1 -1
- package/dist/types/dialog/types.d.ts +17 -3
- package/dist/types/entry-points/dialog.d.ts +1 -1
- package/dist/types/internal/get-aria-for-trigger.d.ts +21 -7
- package/dist/types/internal/role-types.d.ts +0 -32
- package/dist/types/internal/use-animated-visibility.d.ts +7 -1
- package/dist/types/popover/popover.d.ts +1 -5
- package/dist/types/popover/types.d.ts +1 -1
- package/package.json +3 -30
- package/animations/package.json +0 -10
- package/dist/cjs/entry-points/animations.js +0 -1
- package/dist/cjs/internal/role-to-aria-has-popup.js +0 -37
- package/dist/cjs/internal/slot.js +0 -65
- package/dist/es2019/entry-points/animations.js +0 -0
- package/dist/es2019/internal/role-to-aria-has-popup.js +0 -32
- package/dist/es2019/internal/slot.js +0 -57
- package/dist/esm/entry-points/animations.js +0 -0
- package/dist/esm/internal/role-to-aria-has-popup.js +0 -31
- package/dist/esm/internal/slot.js +0 -59
- package/dist/types/entry-points/animations.d.ts +0 -0
- package/dist/types/internal/role-to-aria-has-popup.d.ts +0 -13
- package/dist/types/internal/slot.d.ts +0 -29
package/AGENTS.md
CHANGED
|
@@ -110,4 +110,6 @@ const [state, setState] = useState<TState>({ type: 'idle' });
|
|
|
110
110
|
|
|
111
111
|
## Versioning
|
|
112
112
|
|
|
113
|
-
This package is
|
|
113
|
+
This package is on stable releases. Follow semantic versioning: use `major` for public API removals
|
|
114
|
+
or incompatible behavior changes, `minor` for public API additions, and `patch` for compatible fixes
|
|
115
|
+
and internal implementation changes.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @atlaskit/top-layer
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`ccb9237bc10d9`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ccb9237bc10d9) -
|
|
8
|
+
Add the `dismissedBy` API to control Dialog dismissal consistently across browsers. Align Dialog
|
|
9
|
+
with Popover by notifying `onClose` after native dismissal, ensure both primitives unmount after
|
|
10
|
+
native dismissal, and simplify Modal close handling behind `platform-dst-top-layer`.
|
|
11
|
+
|
|
12
|
+
## 4.0.0
|
|
13
|
+
|
|
14
|
+
### Major Changes
|
|
15
|
+
|
|
16
|
+
- [`eff4aad608ff2`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/eff4aad608ff2) -
|
|
17
|
+
Remove the obsolete `@atlaskit/top-layer/animations` entry point and simplify internal helpers.
|
|
18
|
+
Use the built-in `shouldAnimate` behavior on `Popover` and `Dialog`, with phase-specific xcss
|
|
19
|
+
overrides when customization is required.
|
|
20
|
+
|
|
3
21
|
## 3.2.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -92,20 +92,6 @@ for entry and
|
|
|
92
92
|
[`allow-discrete`](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior) on
|
|
93
93
|
`display`/`overlay` for exit — no JavaScript animation coordination required.
|
|
94
94
|
|
|
95
|
-
Presets are available from `@atlaskit/top-layer/animations`:
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
import {
|
|
99
|
-
// Popover presets
|
|
100
|
-
slideAndFade,
|
|
101
|
-
fade,
|
|
102
|
-
scaleAndFade,
|
|
103
|
-
// Dialog presets
|
|
104
|
-
dialogSlideUpAndFade,
|
|
105
|
-
dialogFade,
|
|
106
|
-
} from '@atlaskit/top-layer/animations';
|
|
107
|
-
```
|
|
108
|
-
|
|
109
95
|
### Popover animation
|
|
110
96
|
|
|
111
97
|
Pass `shouldAnimate` to enable the default popover entry and exit animation:
|
|
@@ -54,8 +54,8 @@ var backdropStyles = {
|
|
|
54
54
|
*
|
|
55
55
|
* Handles native `cancel` event (Escape) and backdrop click detection.
|
|
56
56
|
*
|
|
57
|
-
* Close flow:
|
|
58
|
-
*
|
|
57
|
+
* Close flow: allowed user dismissal closes the native dialog before `onClose`
|
|
58
|
+
* notifies the consumer. The consumer must synchronize `isOpen` in response.
|
|
59
59
|
*
|
|
60
60
|
* Accessibility: render at least one focusable element (typically a close
|
|
61
61
|
* button) when `isOpen` becomes `true`. Tab is always trapped inside the
|
|
@@ -65,6 +65,8 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
65
65
|
var children = _ref.children,
|
|
66
66
|
isOpen = _ref.isOpen,
|
|
67
67
|
onClose = _ref.onClose,
|
|
68
|
+
_ref$dismissedBy = _ref.dismissedBy,
|
|
69
|
+
dismissedBy = _ref$dismissedBy === void 0 ? 'escape-and-outside-click' : _ref$dismissedBy,
|
|
68
70
|
onEnterFinish = _ref.onEnterFinish,
|
|
69
71
|
onExitFinish = _ref.onExitFinish,
|
|
70
72
|
_ref$shouldAnimate = _ref.shouldAnimate,
|
|
@@ -82,6 +84,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
82
84
|
var dialogId = providedId !== null && providedId !== void 0 ? providedId : generatedId;
|
|
83
85
|
var ownRef = (0, _react.useRef)(null);
|
|
84
86
|
var combinedRef = (0, _mergeRefs.default)([ownRef, ref]);
|
|
87
|
+
var closeReasonRef = (0, _react.useRef)(null);
|
|
85
88
|
var _useAnimatedVisibilit = (0, _useAnimatedVisibility.useAnimatedVisibility)({
|
|
86
89
|
isOpen: isOpen,
|
|
87
90
|
animationKind: 'dialog',
|
|
@@ -90,7 +93,8 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
90
93
|
onEnterFinish: onEnterFinish,
|
|
91
94
|
onExitFinish: onExitFinish
|
|
92
95
|
}),
|
|
93
|
-
phase = _useAnimatedVisibilit.phase
|
|
96
|
+
phase = _useAnimatedVisibilit.phase,
|
|
97
|
+
onToggle = _useAnimatedVisibilit.onToggle;
|
|
94
98
|
|
|
95
99
|
// True while the host element is mounted (any phase except `closed`).
|
|
96
100
|
// Used as a dep so listener-rebind effects re-attach after a remount.
|
|
@@ -121,6 +125,7 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
127
|
if (isOpen) {
|
|
128
|
+
closeReasonRef.current = null;
|
|
124
129
|
if (!dialog.open) {
|
|
125
130
|
dialog.showModal();
|
|
126
131
|
}
|
|
@@ -153,45 +158,67 @@ var Dialog = exports.Dialog = /*#__PURE__*/(0, _react.forwardRef)(function Dialo
|
|
|
153
158
|
if (event.target !== event.currentTarget) {
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
156
|
-
|
|
161
|
+
|
|
157
162
|
// Spurious Safari `cancel`: the keydown snapshot saw an open child
|
|
158
163
|
// popover, so this Escape belongs to that popover (light-dismissed
|
|
159
164
|
// natively). Keep the dialog open instead of forwarding to `onClose`.
|
|
160
165
|
if (shouldIgnoreEscape()) {
|
|
166
|
+
event.preventDefault();
|
|
161
167
|
return;
|
|
162
168
|
}
|
|
163
|
-
onClose({
|
|
164
|
-
reason: 'escape'
|
|
165
|
-
});
|
|
166
|
-
}, [onClose, shouldIgnoreEscape]);
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
// Without native `closedby`, cancel Escape only when dismissal is disabled.
|
|
171
|
+
// Allowed dismissal follows Popover behavior: the browser closes the element
|
|
172
|
+
// and `onClose` notifies the controlled owner of the reason.
|
|
173
|
+
if (dismissedBy === 'none') {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
closeReasonRef.current = 'escape';
|
|
178
|
+
}, [dismissedBy, shouldIgnoreEscape]);
|
|
179
|
+
|
|
180
|
+
// Handle backdrop interaction
|
|
169
181
|
// Attached via bind-event-listener rather than a React prop so we avoid
|
|
170
182
|
// a11y lint suppressions on the <dialog> element.
|
|
171
|
-
//
|
|
183
|
+
// The click listener provides light dismiss consistently across browsers.
|
|
172
184
|
// `isVisible` is in deps so the listener re-binds to the new <dialog>
|
|
173
185
|
// element after a host unmount / remount cycle.
|
|
174
|
-
(0, _react.
|
|
186
|
+
(0, _react.useLayoutEffect)(function () {
|
|
175
187
|
var dialog = ownRef.current;
|
|
176
188
|
if (!dialog) {
|
|
177
189
|
return;
|
|
178
190
|
}
|
|
179
|
-
return (0, _bindEventListener.
|
|
191
|
+
return (0, _bindEventListener.bindAll)(dialog, [{
|
|
180
192
|
type: 'click',
|
|
181
193
|
listener: function listener(event) {
|
|
182
|
-
if (event.target === event.currentTarget) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
if (event.target === event.currentTarget && dismissedBy === 'escape-and-outside-click') {
|
|
195
|
+
closeReasonRef.current = 'overlay-click';
|
|
196
|
+
// Calls native close because this is emulating the native light-dismiss behavior
|
|
197
|
+
dialog.close();
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
200
|
+
}, {
|
|
201
|
+
type: 'toggle',
|
|
202
|
+
listener: function listener(event) {
|
|
203
|
+
if (event.newState === 'closed') {
|
|
204
|
+
var reason = closeReasonRef.current;
|
|
205
|
+
closeReasonRef.current = null;
|
|
206
|
+
if (reason) {
|
|
207
|
+
onClose({
|
|
208
|
+
reason: reason
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Visibility cleanup runs after close-reason handling.
|
|
214
|
+
onToggle(event);
|
|
215
|
+
}
|
|
216
|
+
}]);
|
|
217
|
+
}, [dismissedBy, onClose, onToggle, isVisible]);
|
|
190
218
|
|
|
191
219
|
// Unmount the `<dialog>` once exit completes so it does not leave an
|
|
192
220
|
// empty `role="dialog"` element in the accessibility tree. On the next
|
|
193
|
-
// open the element remounts and
|
|
194
|
-
// effects re-run against the fresh element.
|
|
221
|
+
// open the element remounts and its effects run against the fresh element.
|
|
195
222
|
if (!isVisible) {
|
|
196
223
|
return null;
|
|
197
224
|
}
|
|
@@ -13,11 +13,5 @@ var topLayerSelector = '[popover], dialog, [role="dialog"], [role="alertdialog"]
|
|
|
13
13
|
function isInSameLayer(_ref) {
|
|
14
14
|
var element = _ref.element,
|
|
15
15
|
container = _ref.container;
|
|
16
|
-
|
|
17
|
-
var containerLayer = container.closest(topLayerSelector);
|
|
18
|
-
// Both elements are in the "document" layer
|
|
19
|
-
if (!elementLayer && !containerLayer) {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
return elementLayer === containerLayer;
|
|
16
|
+
return element.closest(topLayerSelector) === container.closest(topLayerSelector);
|
|
23
17
|
}
|
|
@@ -8,7 +8,7 @@ var selectors = exports.selectors = function () {
|
|
|
8
8
|
// Common exclusion filter applied to every selector.
|
|
9
9
|
var not = ':not([tabindex="-1"]):not([aria-disabled="true"]):not([aria-hidden="true"])';
|
|
10
10
|
var notDisabled = ":not([disabled])".concat(not);
|
|
11
|
-
var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled)
|
|
11
|
+
var focusable = ["a[href]".concat(not), "area[href]".concat(not), "input".concat(notDisabled, ":not([type=\"hidden\"])"), "select".concat(notDisabled), "textarea".concat(notDisabled), "button".concat(notDisabled), "iframe".concat(not), "audio[controls]".concat(not), "video[controls]".concat(not), "[contenteditable]:not([contenteditable=\"false\"])".concat(not), "[tabindex]".concat(notDisabled)];
|
|
12
12
|
return {
|
|
13
13
|
focusable: focusable.join(','),
|
|
14
14
|
// Build the `:focus` selector
|
|
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.getAriaForTrigger = getAriaForTrigger;
|
|
7
|
-
var _roleToAriaHasPopup = require("./role-to-aria-has-popup");
|
|
8
7
|
/**
|
|
9
8
|
* Roles accepted by `getAriaForTrigger`.
|
|
10
9
|
*
|
|
@@ -14,12 +13,34 @@ var _roleToAriaHasPopup = require("./role-to-aria-has-popup");
|
|
|
14
13
|
* manually for tooltip triggers.
|
|
15
14
|
*/
|
|
16
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Valid values for the `aria-haspopup` attribute.
|
|
18
|
+
*
|
|
19
|
+
* Derived from the HTML spec - maps popover roles to what the trigger
|
|
20
|
+
* announces. `undefined` means the attribute is omitted entirely (used for
|
|
21
|
+
* non-popup roles like `tooltip`/`status`/`alert`/`note`/`log` where
|
|
22
|
+
* `aria-haspopup` would be misleading).
|
|
23
|
+
*
|
|
24
|
+
* `true` is intentionally NOT in the union - the runtime only ever produces
|
|
25
|
+
* the explicit string forms, so widening the type would invite consumers to
|
|
26
|
+
* pass `true` and get an `aria-haspopup="true"` serialisation that the
|
|
27
|
+
* runtime no longer emits.
|
|
28
|
+
*/
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Non-undefined subset of `TAriaHasPopupValue`. Every role accepted by
|
|
19
32
|
* `getAriaForTrigger` maps to a concrete `aria-haspopup` string, so the
|
|
20
33
|
* trigger always receives a defined attribute value.
|
|
21
34
|
*/
|
|
22
35
|
|
|
36
|
+
var roleToAriaHasPopup = {
|
|
37
|
+
dialog: 'dialog',
|
|
38
|
+
alertdialog: 'dialog',
|
|
39
|
+
menu: 'menu',
|
|
40
|
+
listbox: 'listbox',
|
|
41
|
+
tree: 'tree',
|
|
42
|
+
grid: 'grid'
|
|
43
|
+
};
|
|
23
44
|
/**
|
|
24
45
|
* Returns the ARIA attributes to spread onto a click-activated popover trigger.
|
|
25
46
|
*
|
|
@@ -71,13 +92,6 @@ function getAriaForTrigger(_ref) {
|
|
|
71
92
|
var role = _ref.role,
|
|
72
93
|
isOpen = _ref.isOpen,
|
|
73
94
|
popoverId = _ref.popoverId;
|
|
74
|
-
// `roleToAriaHasPopup` accepts a wider role union (including tooltip-family
|
|
75
|
-
// roles that map to `undefined`). `TAriaForTriggerRole` excludes those, so
|
|
76
|
-
// the returned value is guaranteed to be a defined string. The cast narrows
|
|
77
|
-
// the wider helper signature to match.
|
|
78
|
-
var ariaHasPopup = (0, _roleToAriaHasPopup.roleToAriaHasPopup)({
|
|
79
|
-
role: role
|
|
80
|
-
});
|
|
81
95
|
// `aria-controls` is set to `undefined` while closed. The `Popover` /
|
|
82
96
|
// `Dialog` primitives unmount their host element after the exit
|
|
83
97
|
// animation finishes, so a closed-state `aria-controls` would point
|
|
@@ -86,7 +100,7 @@ function getAriaForTrigger(_ref) {
|
|
|
86
100
|
// until the target exists, avoiding the dangling reference while
|
|
87
101
|
// keeping the relationship live whenever it is meaningful.
|
|
88
102
|
return {
|
|
89
|
-
'aria-haspopup':
|
|
103
|
+
'aria-haspopup': roleToAriaHasPopup[role],
|
|
90
104
|
'aria-expanded': isOpen,
|
|
91
105
|
'aria-controls': isOpen ? popoverId : undefined
|
|
92
106
|
};
|
|
@@ -1,64 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.shouldFocusIntoPopover = shouldFocusIntoPopover;
|
|
7
|
-
/**
|
|
8
|
-
* Valid values for the `aria-haspopup` attribute.
|
|
9
|
-
*
|
|
10
|
-
* Derived from the HTML spec - maps popover roles to what the trigger
|
|
11
|
-
* announces. `undefined` means the attribute is omitted entirely (used for
|
|
12
|
-
* non-popup roles like `tooltip`/`status`/`alert`/`note`/`log` where
|
|
13
|
-
* `aria-haspopup` would be misleading).
|
|
14
|
-
*
|
|
15
|
-
* `true` is intentionally NOT in the union - the runtime only ever produces
|
|
16
|
-
* the explicit string forms, so widening the type would invite consumers to
|
|
17
|
-
* pass `true` and get an `aria-haspopup="true"` serialisation that the
|
|
18
|
-
* runtime no longer emits.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Roles that require an accessible name (`label` or `labelledBy`) per WCAG 4.1.2.
|
|
23
|
-
* Without a name, assistive technology cannot identify these landmarks.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Roles where the accessible name is derived from content or provided externally
|
|
28
|
-
* (e.g. a combobox labels its listbox, tooltip content IS the accessible name).
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
// Shared ARIA role discrimination types
|
|
32
|
-
//
|
|
33
|
-
// These encode the WCAG 4.1.2 constraint: roles like `dialog`, `alertdialog`,
|
|
34
|
-
// and `menu` MUST have an accessible name (either `label` or `labelledBy`).
|
|
35
|
-
// TypeScript enforces this at compile time via discriminated unions.
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* ARIA role props where a role is always required.
|
|
39
|
-
*
|
|
40
|
-
* Used by `TPopupContentProps`. Popup content must always declare
|
|
41
|
-
* its role so assistive technology can identify the layer.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* ARIA role props where a role is optional.
|
|
46
|
-
*
|
|
47
|
-
* Used by `TPopoverProps`. The low-level primitive allows omitting
|
|
48
|
-
* the role for cases where it is set externally or not needed.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
var rolesThatMoveFocus = new Set(['dialog', 'alertdialog', 'menu', 'listbox', 'tree', 'grid']);
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns `true` when the given popover role moves focus into the popover
|
|
55
|
-
* on open, and `false` otherwise (including when `role` is `undefined`).
|
|
56
|
-
*/
|
|
57
|
-
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
|
|
58
|
-
function shouldFocusIntoPopover(_ref) {
|
|
59
|
-
var role = _ref.role;
|
|
60
|
-
if (!role) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return rolesThatMoveFocus.has(role);
|
|
64
|
-
}
|
|
1
|
+
"use strict";
|
|
@@ -324,11 +324,10 @@ function useAnchorPosition(_ref3) {
|
|
|
324
324
|
* See `notes/decisions/anchor-name-lifetime.md`
|
|
325
325
|
*/
|
|
326
326
|
trigger.style.setProperty('anchor-name', anchorName);
|
|
327
|
-
|
|
327
|
+
return (0, _setStyle.setStyle)({
|
|
328
328
|
element: popover,
|
|
329
329
|
styles: popoverStyles
|
|
330
|
-
})
|
|
331
|
-
return _undoPositioning;
|
|
330
|
+
});
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
// JS fallback. The popover is already in the top layer via
|
|
@@ -117,7 +117,7 @@ function runOnTransitionEndOrTimeout(_ref) {
|
|
|
117
117
|
* `entering`, `open`, `exiting`). The host element is rendered while
|
|
118
118
|
* `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
|
|
119
119
|
* while the CSS exit transition plays (or, with animation disabled,
|
|
120
|
-
* while we wait for the browser `toggle`
|
|
120
|
+
* while we wait for the browser's closing `toggle` event to fire on the
|
|
121
121
|
* still-attached element):
|
|
122
122
|
*
|
|
123
123
|
* ```
|
|
@@ -136,6 +136,7 @@ function useAnimatedVisibility(_ref2) {
|
|
|
136
136
|
onEnterFinish = _ref2.onEnterFinish,
|
|
137
137
|
onExitFinish = _ref2.onExitFinish;
|
|
138
138
|
var willAnimate = shouldAnimate && !(0, _reducedMotion.prefersReducedMotion)();
|
|
139
|
+
var isNativeClosedRef = (0, _react.useRef)(false);
|
|
139
140
|
|
|
140
141
|
// Promote `closed → entering`/`closed → open` synchronously during
|
|
141
142
|
// render so the host element mounts in the same commit as the open
|
|
@@ -149,16 +150,23 @@ function useAnimatedVisibility(_ref2) {
|
|
|
149
150
|
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
|
|
150
151
|
phase = _useState2[0],
|
|
151
152
|
setPhase = _useState2[1];
|
|
152
|
-
|
|
153
|
+
|
|
154
|
+
// Rearm only after controlled state and the native lifecycle have both
|
|
155
|
+
// reached closed. Resetting as soon as `isOpen` becomes false can erase a
|
|
156
|
+
// closing toggle that was handled in the same React batch.
|
|
157
|
+
if (!isOpen && phase === 'closed') {
|
|
158
|
+
isNativeClosedRef.current = false;
|
|
159
|
+
}
|
|
160
|
+
if (isOpen && phase === 'closed' && !isNativeClosedRef.current) {
|
|
153
161
|
setPhase(willAnimate ? 'entering' : 'open');
|
|
154
162
|
}
|
|
155
163
|
if (!isOpen && (phase === 'open' || phase === 'entering')) {
|
|
156
164
|
// Close intent. Move to `exiting` to gate the unmount on either
|
|
157
|
-
// the exit transition (animated) or the toggle
|
|
165
|
+
// the exit transition (animated) or the closing toggle event
|
|
158
166
|
// (non-animated). Includes interrupted entry (`entering → exiting`).
|
|
159
167
|
setPhase('exiting');
|
|
160
168
|
}
|
|
161
|
-
if (isOpen && phase === 'exiting') {
|
|
169
|
+
if (isOpen && phase === 'exiting' && !isNativeClosedRef.current) {
|
|
162
170
|
// Reopen mid-exit. With animation, jump back to `entering` and let
|
|
163
171
|
// the entry transition restart from current visual state. Without
|
|
164
172
|
// animation, jump straight to `open` (the handshake we were waiting
|
|
@@ -247,49 +255,37 @@ function useAnimatedVisibility(_ref2) {
|
|
|
247
255
|
});
|
|
248
256
|
}, [phase, willAnimate, animationKind, elementRef]);
|
|
249
257
|
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// native focus restoration both run against the still-attached
|
|
254
|
-
// element. A naive `setTimeout(0)` occasionally beats the `toggle`
|
|
255
|
-
// task in Chromium and drops focus restoration.
|
|
256
|
-
//
|
|
257
|
-
// `useLayoutEffect` so the listener binds in the same commit as the
|
|
258
|
-
// show/hide layout effect that calls `hidePopover()` / `dialog.close()`.
|
|
259
|
-
// The toggle/close event is queued as a task and cannot run until the
|
|
260
|
-
// frame completes. SSR-safe: `phase` starts at `'closed'`.
|
|
261
|
-
//
|
|
262
|
-
// `onExitFinish` is fired separately (see effect below) so consumers
|
|
263
|
-
// observe it the moment `isOpen` flips to `false`.
|
|
258
|
+
// Native dismissal is authoritative. Its toggle can arrive while controlled
|
|
259
|
+
// state still says open, so remember that native state and do not
|
|
260
|
+
// reopen until `isOpen` has first moved to false.
|
|
264
261
|
(0, _react.useLayoutEffect)(function () {
|
|
265
|
-
if (phase
|
|
266
|
-
|
|
262
|
+
if (phase === 'exiting' && !willAnimate && isNativeClosedRef.current) {
|
|
263
|
+
setPhase('closed');
|
|
267
264
|
}
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
}, [phase, willAnimate]);
|
|
266
|
+
var onToggle = (0, _react.useCallback)(function (event) {
|
|
267
|
+
if (event.newState === 'open') {
|
|
268
|
+
isNativeClosedRef.current = false;
|
|
270
269
|
return;
|
|
271
270
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return current === 'exiting' ? 'closed' : current;
|
|
275
|
-
});
|
|
271
|
+
if (event.newState !== 'closed') {
|
|
272
|
+
return;
|
|
276
273
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
unmount();
|
|
282
|
-
}
|
|
274
|
+
isNativeClosedRef.current = true;
|
|
275
|
+
setPhase(function (current) {
|
|
276
|
+
if (current === 'open' || current === 'entering') {
|
|
277
|
+
return 'exiting';
|
|
283
278
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
279
|
+
if (current === 'exiting' && !willAnimate) {
|
|
280
|
+
return 'closed';
|
|
281
|
+
}
|
|
282
|
+
return current;
|
|
283
|
+
});
|
|
284
|
+
}, [willAnimate]);
|
|
289
285
|
|
|
290
286
|
// Non-animated exit callback. No `transitionend` to wait for, so fire
|
|
291
287
|
// `onExitFinish` on the transition into `exiting` (the unmount itself
|
|
292
|
-
// is gated by the toggle
|
|
288
|
+
// is gated by the closing toggle handshake above).
|
|
293
289
|
var prevPhaseForExitRef = (0, _react.useRef)('closed');
|
|
294
290
|
(0, _react.useEffect)(function () {
|
|
295
291
|
var prevPhase = prevPhaseForExitRef.current;
|
|
@@ -304,6 +300,7 @@ function useAnimatedVisibility(_ref2) {
|
|
|
304
300
|
}
|
|
305
301
|
}, [phase, willAnimate]);
|
|
306
302
|
return {
|
|
307
|
-
phase: phase
|
|
303
|
+
phase: phase,
|
|
304
|
+
onToggle: onToggle
|
|
308
305
|
};
|
|
309
306
|
}
|
|
@@ -16,7 +16,7 @@ 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
18
|
var _resolvePlacement2 = require("../internal/resolve-placement");
|
|
19
|
-
var
|
|
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");
|
|
@@ -97,11 +97,7 @@ var POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree', '
|
|
|
97
97
|
* ### 📜 Browser dismiss is non-cancellable
|
|
98
98
|
*
|
|
99
99
|
* Per the popover spec, `beforetoggle` is cancellable on open transitions
|
|
100
|
-
* only, not on close.
|
|
101
|
-
* (e.g. a click on a logically-associated external trigger) must reconcile
|
|
102
|
-
* inside their own `onClose` handler by calling `showPopover()` again.
|
|
103
|
-
* `Popover` does not self-heal so consumers do not pay a reconcile cost
|
|
104
|
-
* by default.
|
|
100
|
+
* only, not on close.
|
|
105
101
|
*
|
|
106
102
|
* - Spec: <https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute>
|
|
107
103
|
* - <https://github.com/whatwg/html/issues/8973>
|
|
@@ -148,7 +144,8 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
148
144
|
onEnterFinish: onEnterFinish,
|
|
149
145
|
onExitFinish: onExitFinish
|
|
150
146
|
}),
|
|
151
|
-
phase = _useAnimatedVisibilit.phase
|
|
147
|
+
phase = _useAnimatedVisibilit.phase,
|
|
148
|
+
onToggle = _useAnimatedVisibilit.onToggle;
|
|
152
149
|
|
|
153
150
|
// True while the host element is mounted (any phase except `closed`).
|
|
154
151
|
// Used as a dep so listener-rebind effects re-attach after a remount.
|
|
@@ -221,6 +218,7 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
221
218
|
var handleToggle = (0, _react.useCallback)(function (event) {
|
|
222
219
|
var element = ownRef.current;
|
|
223
220
|
if (event.newState === 'open') {
|
|
221
|
+
onToggle(event);
|
|
224
222
|
return;
|
|
225
223
|
}
|
|
226
224
|
if (event.newState === 'closed') {
|
|
@@ -232,11 +230,11 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
232
230
|
// click target keeps focus per HTML spec
|
|
233
231
|
// (`focusPreviousElement=false`).
|
|
234
232
|
var previouslyFocused = previouslyFocusedElementRef.current;
|
|
235
|
-
var
|
|
233
|
+
var reason = closeReasonRef.current;
|
|
236
234
|
var isProgrammatic = programmaticCloseRef.current;
|
|
237
|
-
var shouldRestore = previouslyFocused !== null && previouslyFocused.isConnected && (0,
|
|
235
|
+
var shouldRestore = previouslyFocused !== null && previouslyFocused.isConnected && (0, _shouldFocusIntoPopover.shouldFocusIntoPopover)({
|
|
238
236
|
role: role
|
|
239
|
-
}) && (
|
|
237
|
+
}) && (reason === 'escape' || isProgrammatic);
|
|
240
238
|
if (shouldRestore && previouslyFocused) {
|
|
241
239
|
previouslyFocused.focus({
|
|
242
240
|
preventScroll: true
|
|
@@ -250,18 +248,18 @@ var Popover = exports.Popover = /*#__PURE__*/(0, _react.forwardRef)(function Pop
|
|
|
250
248
|
// Reset reason so a stale 'escape' from a race with Escape keydown
|
|
251
249
|
// does not corrupt the next browser-dismiss cycle.
|
|
252
250
|
closeReasonRef.current = 'light-dismiss';
|
|
253
|
-
|
|
251
|
+
} else {
|
|
252
|
+
// Browser dismiss (Escape/click-outside).
|
|
253
|
+
var _reason = closeReasonRef.current;
|
|
254
|
+
closeReasonRef.current = 'light-dismiss';
|
|
255
|
+
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
256
|
+
onClose === null || onClose === void 0 || onClose({
|
|
257
|
+
reason: _reason
|
|
258
|
+
});
|
|
254
259
|
}
|
|
255
|
-
|
|
256
|
-
// Browser dismiss (Escape/click-outside).
|
|
257
|
-
var reason = closeReasonRef.current;
|
|
258
|
-
closeReasonRef.current = 'light-dismiss';
|
|
259
|
-
// Optional: `manual` mode has no `onClose` in the forwarded type.
|
|
260
|
-
onClose === null || onClose === void 0 || onClose({
|
|
261
|
-
reason: reason
|
|
262
|
-
});
|
|
263
260
|
}
|
|
264
|
-
|
|
261
|
+
onToggle(event);
|
|
262
|
+
}, [onClose, onToggle, role]);
|
|
265
263
|
|
|
266
264
|
// Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
|
|
267
265
|
// below, so listeners are attached when `showPopover()` synchronously
|