@jobber/components 8.27.1 → 8.27.3-FIXproper-e2c0fc2.2
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/dist/ConfirmationModal/index.cjs +1 -0
- package/dist/ConfirmationModal/index.mjs +1 -0
- package/dist/Dialog/index.cjs +1 -0
- package/dist/Dialog/index.mjs +1 -0
- package/dist/FormatFile/index.cjs +1 -0
- package/dist/FormatFile/index.mjs +1 -0
- package/dist/Gallery/index.cjs +1 -0
- package/dist/Gallery/index.mjs +1 -0
- package/dist/Modal/index.cjs +12 -17
- package/dist/Modal/index.mjs +12 -17
- package/dist/Modal/nestedEscape.d.ts +57 -0
- package/dist/dialogReturnFocus-cjs.js +9 -0
- package/dist/dialogReturnFocus-es.js +9 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -0
- package/dist/nestedEscape-cjs.js +94 -0
- package/dist/nestedEscape-es.js +91 -0
- package/package.json +2 -2
package/dist/Dialog/index.cjs
CHANGED
package/dist/Dialog/index.mjs
CHANGED
package/dist/Gallery/index.cjs
CHANGED
|
@@ -41,6 +41,7 @@ require('../Content-cjs.js');
|
|
|
41
41
|
require('../ProgressBar-cjs.js');
|
|
42
42
|
require('../ConfirmationModal-cjs.js');
|
|
43
43
|
require('../Modal/index.cjs');
|
|
44
|
+
require('../nestedEscape-cjs.js');
|
|
44
45
|
require('../AtlantisPortalContent-cjs.js');
|
|
45
46
|
require('../Markdown-cjs.js');
|
|
46
47
|
require('../Emphasis-cjs.js');
|
package/dist/Gallery/index.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import '../Content-es.js';
|
|
|
39
39
|
import '../ProgressBar-es.js';
|
|
40
40
|
import '../ConfirmationModal-es.js';
|
|
41
41
|
import '../Modal/index.mjs';
|
|
42
|
+
import '../nestedEscape-es.js';
|
|
42
43
|
import '../AtlantisPortalContent-es.js';
|
|
43
44
|
import '../Markdown-es.js';
|
|
44
45
|
import '../Emphasis-es.js';
|
package/dist/Modal/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ var ReactDOM = require('react-dom');
|
|
|
5
5
|
var classnames = require('classnames');
|
|
6
6
|
var framerMotion = require('framer-motion');
|
|
7
7
|
var jobberHooks = require('@jobber/hooks');
|
|
8
|
+
var nestedEscape = require('../nestedEscape-cjs.js');
|
|
8
9
|
var Heading = require('../Heading-cjs.js');
|
|
9
10
|
var Button = require('../Button-cjs.js');
|
|
10
11
|
var ButtonDismiss = require('../ButtonDismiss-cjs.js');
|
|
@@ -42,7 +43,17 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
42
43
|
const containerClassName = classnames(styles$1.container, size === "fullScreen" && styles$1.containerFullScreen);
|
|
43
44
|
jobberHooks.useRefocusOnActivator(open);
|
|
44
45
|
const modalRef = jobberHooks.useFocusTrap(open);
|
|
45
|
-
|
|
46
|
+
// Contain Escape so a Modal nested with a Base UI Dialog closes only the
|
|
47
|
+
// top-most layer.
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (!open)
|
|
50
|
+
return;
|
|
51
|
+
return nestedEscape.addModalEscapeContainment({
|
|
52
|
+
getModal: () => modalRef.current,
|
|
53
|
+
onRequestClose,
|
|
54
|
+
});
|
|
55
|
+
}, [open, onRequestClose]);
|
|
56
|
+
const template = (React.createElement(framerMotion.AnimatePresence, null, open && (React.createElement("div", { ref: modalRef, role: "dialog", className: containerClassName, tabIndex: 0, "aria-modal": "true", "aria-labelledby": title ? MODAL_HEADER_ID : undefined, "aria-label": ariaLabel },
|
|
46
57
|
React.createElement(framerMotion.motion.div, { key: styles$1.overlay, className: styles$1.overlay, onClick: onRequestClose, initial: { opacity: 0 }, animate: { opacity: 0.8 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }),
|
|
47
58
|
React.createElement(framerMotion.motion.div, { key: styles$1.modal, className: modalClassName, initial: { scale: 0.9, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.9, opacity: 0 }, transition: {
|
|
48
59
|
duration: 0.2,
|
|
@@ -54,22 +65,6 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
54
65
|
return (globalThis === null || globalThis === void 0 ? void 0 : globalThis.document)
|
|
55
66
|
? ReactDOM.createPortal(template, document.body)
|
|
56
67
|
: template;
|
|
57
|
-
function handleRequestClose() {
|
|
58
|
-
if (open && onRequestClose) {
|
|
59
|
-
onRequestClose();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function handleKeyDown(event) {
|
|
63
|
-
if (event.key === "Escape") {
|
|
64
|
-
// Stop the Escape from bubbling to outer overlays (e.g. a Base UI Dialog
|
|
65
|
-
// this Modal is nested within, whose dismissal listens at the document
|
|
66
|
-
// level). Without this, one Escape closes both layers; instead only the
|
|
67
|
-
// top-most layer — this Modal — should close. Mirrors SideDrawer's
|
|
68
|
-
// Escape containment.
|
|
69
|
-
event.stopPropagation();
|
|
70
|
-
handleRequestClose();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
68
|
}
|
|
74
69
|
function Header({ title, dismissible, onRequestClose }) {
|
|
75
70
|
return (React.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
|
package/dist/Modal/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import ReactDOM__default from 'react-dom';
|
|
|
3
3
|
import classnames from 'classnames';
|
|
4
4
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
5
5
|
import { useRefocusOnActivator, useFocusTrap } from '@jobber/hooks';
|
|
6
|
+
import { a as addModalEscapeContainment } from '../nestedEscape-es.js';
|
|
6
7
|
import { H as Heading } from '../Heading-es.js';
|
|
7
8
|
import { B as Button } from '../Button-es.js';
|
|
8
9
|
import { B as ButtonDismiss } from '../ButtonDismiss-es.js';
|
|
@@ -40,7 +41,17 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
40
41
|
const containerClassName = classnames(styles$1.container, size === "fullScreen" && styles$1.containerFullScreen);
|
|
41
42
|
useRefocusOnActivator(open);
|
|
42
43
|
const modalRef = useFocusTrap(open);
|
|
43
|
-
|
|
44
|
+
// Contain Escape so a Modal nested with a Base UI Dialog closes only the
|
|
45
|
+
// top-most layer.
|
|
46
|
+
React__default.useEffect(() => {
|
|
47
|
+
if (!open)
|
|
48
|
+
return;
|
|
49
|
+
return addModalEscapeContainment({
|
|
50
|
+
getModal: () => modalRef.current,
|
|
51
|
+
onRequestClose,
|
|
52
|
+
});
|
|
53
|
+
}, [open, onRequestClose]);
|
|
54
|
+
const template = (React__default.createElement(AnimatePresence, null, open && (React__default.createElement("div", { ref: modalRef, role: "dialog", className: containerClassName, tabIndex: 0, "aria-modal": "true", "aria-labelledby": title ? MODAL_HEADER_ID : undefined, "aria-label": ariaLabel },
|
|
44
55
|
React__default.createElement(motion.div, { key: styles$1.overlay, className: styles$1.overlay, onClick: onRequestClose, initial: { opacity: 0 }, animate: { opacity: 0.8 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }),
|
|
45
56
|
React__default.createElement(motion.div, { key: styles$1.modal, className: modalClassName, initial: { scale: 0.9, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.9, opacity: 0 }, transition: {
|
|
46
57
|
duration: 0.2,
|
|
@@ -52,22 +63,6 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
52
63
|
return (globalThis === null || globalThis === void 0 ? void 0 : globalThis.document)
|
|
53
64
|
? ReactDOM__default.createPortal(template, document.body)
|
|
54
65
|
: template;
|
|
55
|
-
function handleRequestClose() {
|
|
56
|
-
if (open && onRequestClose) {
|
|
57
|
-
onRequestClose();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function handleKeyDown(event) {
|
|
61
|
-
if (event.key === "Escape") {
|
|
62
|
-
// Stop the Escape from bubbling to outer overlays (e.g. a Base UI Dialog
|
|
63
|
-
// this Modal is nested within, whose dismissal listens at the document
|
|
64
|
-
// level). Without this, one Escape closes both layers; instead only the
|
|
65
|
-
// top-most layer — this Modal — should close. Mirrors SideDrawer's
|
|
66
|
-
// Escape containment.
|
|
67
|
-
event.stopPropagation();
|
|
68
|
-
handleRequestClose();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
66
|
}
|
|
72
67
|
function Header({ title, dismissible, onRequestClose }) {
|
|
73
68
|
return (React__default.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-key containment between the legacy prop-based `Modal` and Base UI's
|
|
3
|
+
* `Dialog`.
|
|
4
|
+
*
|
|
5
|
+
* The problem: both overlays listen for Escape at the `document` level and both
|
|
6
|
+
* portal their content to `<body>`. When one is nested inside the other, a
|
|
7
|
+
* single Escape reaches both listeners, so both layers close. Only the top-most
|
|
8
|
+
* layer should close.
|
|
9
|
+
*
|
|
10
|
+
* The legacy `Modal` can't join Base UI's floating-element tree, so this module
|
|
11
|
+
* coordinates the two with a small, explicit protocol:
|
|
12
|
+
*
|
|
13
|
+
* 1. Top-most check — because each overlay portals to `<body>`, the focused
|
|
14
|
+
* content's *nearest* `[role="dialog"]` ancestor is always its own
|
|
15
|
+
* container. So the Modal only acts when that nearest dialog is itself; if
|
|
16
|
+
* it's a different (nested) dialog, the Modal defers. See
|
|
17
|
+
* {@link isTopMostOverlayForEscape}.
|
|
18
|
+
*
|
|
19
|
+
* 2. Deferral signal — when the Modal is top-most it stamps the native event
|
|
20
|
+
* during the CAPTURE phase (which runs before Base UI's bubble-phase
|
|
21
|
+
* dismissal). An outer Base UI `Dialog` reads that flag in its
|
|
22
|
+
* `onOpenChange` handler and cancels, so the Dialog stays open while the
|
|
23
|
+
* Modal closes. See {@link markEscapeHandledByModal} /
|
|
24
|
+
* {@link isEscapeHandledByModal}.
|
|
25
|
+
*
|
|
26
|
+
* Note: this is a DOM-proximity heuristic rather than a true overlay stack. It
|
|
27
|
+
* holds only because both overlays portal to `<body>` and expose a dialog role.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Whether `container` is the top-most overlay for this Escape — i.e. the
|
|
31
|
+
* Escape originated inside `container` and not inside a more deeply nested
|
|
32
|
+
* dialog/overlay. Returns `true` when the Escape happened outside any dialog
|
|
33
|
+
* (e.g. focus not yet inside the Modal) so the Modal keeps its historical
|
|
34
|
+
* focus-independent close behaviour.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTopMostOverlayForEscape(event: KeyboardEvent, container: Element | null): boolean;
|
|
37
|
+
/** Marks the Escape as owned by a Modal so an outer Dialog can defer. */
|
|
38
|
+
export declare function markEscapeHandledByModal(event: KeyboardEvent): void;
|
|
39
|
+
/** Whether this Escape was already claimed by a nested Modal. */
|
|
40
|
+
export declare function isEscapeHandledByModal(event: Event | undefined): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Wires up Escape containment for a legacy `Modal` and returns a cleanup
|
|
43
|
+
* function. While active it:
|
|
44
|
+
*
|
|
45
|
+
* - marks the event in the CAPTURE phase when the Modal is top-most, so an
|
|
46
|
+
* outer Base UI `Dialog` defers instead of closing; and
|
|
47
|
+
* - closes the Modal in the BUBBLE phase when it is top-most. Bubble phase
|
|
48
|
+
* means a nested overlay (Select, Popover, DatePicker, …) that stops the
|
|
49
|
+
* Escape below the document prevents this from firing, so the Modal
|
|
50
|
+
* correctly stays open in that case.
|
|
51
|
+
*
|
|
52
|
+
* `getContainer` is read lazily so the current Modal element is always used.
|
|
53
|
+
*/
|
|
54
|
+
export declare function addModalEscapeContainment({ getModal, onRequestClose, }: {
|
|
55
|
+
getModal: () => Element | null;
|
|
56
|
+
onRequestClose?: () => void;
|
|
57
|
+
}): () => void;
|
|
@@ -12,6 +12,7 @@ var Heading = require('./Heading-cjs.js');
|
|
|
12
12
|
var BottomSheet = require('./BottomSheet-cjs.js');
|
|
13
13
|
var DrawerTitle = require('./DrawerTitle-cjs.js');
|
|
14
14
|
var DrawerDescription = require('./DrawerDescription-cjs.js');
|
|
15
|
+
var nestedEscape = require('./nestedEscape-cjs.js');
|
|
15
16
|
|
|
16
17
|
// Copied from Menu's constant so the two surfaces agree on what "mobile" means.
|
|
17
18
|
// (The proposal calls for copying shared Menu values into the component first
|
|
@@ -306,6 +307,14 @@ function useDialogShell() {
|
|
|
306
307
|
function Dialog(_a) {
|
|
307
308
|
var { onRequestClose, size = "base", type = "dialog", finalFocus, children, onOpenChange } = _a, rest = tslib_es6.__rest(_a, ["onRequestClose", "size", "type", "finalFocus", "children", "onOpenChange"]);
|
|
308
309
|
function handleOpenChange(nextOpen, eventDetails) {
|
|
310
|
+
if (!nextOpen &&
|
|
311
|
+
eventDetails.reason === "escape-key" &&
|
|
312
|
+
nestedEscape.isEscapeHandledByModal(eventDetails.event)) {
|
|
313
|
+
// A legacy Modal nested inside this Dialog owns this Escape and will close
|
|
314
|
+
// itself; keep the Dialog open so a single Escape closes only the Modal.
|
|
315
|
+
eventDetails.cancel();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
309
318
|
if (!nextOpen) {
|
|
310
319
|
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose(eventDetails);
|
|
311
320
|
}
|
|
@@ -10,6 +10,7 @@ import { H as Heading } from './Heading-es.js';
|
|
|
10
10
|
import { a as BottomSheet, B as BottomSheetScrollAreaContext } from './BottomSheet-es.js';
|
|
11
11
|
import { D as DrawerTitle, a as DrawerClose, b as DialogTitle$1, c as DialogClose$1 } from './DrawerTitle-es.js';
|
|
12
12
|
import { D as DrawerDescription, a as DialogBackdrop, b as DialogPopup, c as DialogDescription, d as createDialogHandle } from './DrawerDescription-es.js';
|
|
13
|
+
import { i as isEscapeHandledByModal } from './nestedEscape-es.js';
|
|
13
14
|
|
|
14
15
|
// Copied from Menu's constant so the two surfaces agree on what "mobile" means.
|
|
15
16
|
// (The proposal calls for copying shared Menu values into the component first
|
|
@@ -304,6 +305,14 @@ function useDialogShell() {
|
|
|
304
305
|
function Dialog(_a) {
|
|
305
306
|
var { onRequestClose, size = "base", type = "dialog", finalFocus, children, onOpenChange } = _a, rest = __rest(_a, ["onRequestClose", "size", "type", "finalFocus", "children", "onOpenChange"]);
|
|
306
307
|
function handleOpenChange(nextOpen, eventDetails) {
|
|
308
|
+
if (!nextOpen &&
|
|
309
|
+
eventDetails.reason === "escape-key" &&
|
|
310
|
+
isEscapeHandledByModal(eventDetails.event)) {
|
|
311
|
+
// A legacy Modal nested inside this Dialog owns this Escape and will close
|
|
312
|
+
// itself; keep the Dialog open so a single Escape closes only the Modal.
|
|
313
|
+
eventDetails.cancel();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
307
316
|
if (!nextOpen) {
|
|
308
317
|
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose(eventDetails);
|
|
309
318
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -180,6 +180,7 @@ require('./clamp-cjs.js');
|
|
|
180
180
|
require('./BottomSheet-cjs.js');
|
|
181
181
|
require('./DrawerTitle-cjs.js');
|
|
182
182
|
require('./DrawerDescription-cjs.js');
|
|
183
|
+
require('./nestedEscape-cjs.js');
|
|
183
184
|
require('./useFormFieldFocus-cjs.js');
|
|
184
185
|
require('filesize');
|
|
185
186
|
require('./GridCell-cjs.js');
|
package/dist/index.mjs
CHANGED
|
@@ -178,6 +178,7 @@ import './clamp-es.js';
|
|
|
178
178
|
import './BottomSheet-es.js';
|
|
179
179
|
import './DrawerTitle-es.js';
|
|
180
180
|
import './DrawerDescription-es.js';
|
|
181
|
+
import './nestedEscape-es.js';
|
|
181
182
|
import './useFormFieldFocus-es.js';
|
|
182
183
|
import 'filesize';
|
|
183
184
|
import './GridCell-es.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape-key containment between the legacy prop-based `Modal` and Base UI's
|
|
5
|
+
* `Dialog`.
|
|
6
|
+
*
|
|
7
|
+
* The problem: both overlays listen for Escape at the `document` level and both
|
|
8
|
+
* portal their content to `<body>`. When one is nested inside the other, a
|
|
9
|
+
* single Escape reaches both listeners, so both layers close. Only the top-most
|
|
10
|
+
* layer should close.
|
|
11
|
+
*
|
|
12
|
+
* The legacy `Modal` can't join Base UI's floating-element tree, so this module
|
|
13
|
+
* coordinates the two with a small, explicit protocol:
|
|
14
|
+
*
|
|
15
|
+
* 1. Top-most check — because each overlay portals to `<body>`, the focused
|
|
16
|
+
* content's *nearest* `[role="dialog"]` ancestor is always its own
|
|
17
|
+
* container. So the Modal only acts when that nearest dialog is itself; if
|
|
18
|
+
* it's a different (nested) dialog, the Modal defers. See
|
|
19
|
+
* {@link isTopMostOverlayForEscape}.
|
|
20
|
+
*
|
|
21
|
+
* 2. Deferral signal — when the Modal is top-most it stamps the native event
|
|
22
|
+
* during the CAPTURE phase (which runs before Base UI's bubble-phase
|
|
23
|
+
* dismissal). An outer Base UI `Dialog` reads that flag in its
|
|
24
|
+
* `onOpenChange` handler and cancels, so the Dialog stays open while the
|
|
25
|
+
* Modal closes. See {@link markEscapeHandledByModal} /
|
|
26
|
+
* {@link isEscapeHandledByModal}.
|
|
27
|
+
*
|
|
28
|
+
* Note: this is a DOM-proximity heuristic rather than a true overlay stack. It
|
|
29
|
+
* holds only because both overlays portal to `<body>` and expose a dialog role.
|
|
30
|
+
*/
|
|
31
|
+
const ESCAPE_KEY = "Escape";
|
|
32
|
+
/** Selector matching either an accessible dialog or alert dialog surface. */
|
|
33
|
+
const DIALOG_ROLE_SELECTOR = '[role="dialog"], [role="alertdialog"]';
|
|
34
|
+
/** Property stamped on the native Escape event to signal Modal ownership. */
|
|
35
|
+
const HANDLED_FLAG = "atlantisHandledByNestedModal";
|
|
36
|
+
/**
|
|
37
|
+
* Whether `container` is the top-most overlay for this Escape — i.e. the
|
|
38
|
+
* Escape originated inside `container` and not inside a more deeply nested
|
|
39
|
+
* dialog/overlay. Returns `true` when the Escape happened outside any dialog
|
|
40
|
+
* (e.g. focus not yet inside the Modal) so the Modal keeps its historical
|
|
41
|
+
* focus-independent close behaviour.
|
|
42
|
+
*/
|
|
43
|
+
function isTopMostOverlayForEscape(event, container) {
|
|
44
|
+
var _a;
|
|
45
|
+
const target = event.target;
|
|
46
|
+
const nearestDialog = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, DIALOG_ROLE_SELECTOR);
|
|
47
|
+
return !nearestDialog || nearestDialog === container;
|
|
48
|
+
}
|
|
49
|
+
/** Marks the Escape as owned by a Modal so an outer Dialog can defer. */
|
|
50
|
+
function markEscapeHandledByModal(event) {
|
|
51
|
+
event[HANDLED_FLAG] = true;
|
|
52
|
+
}
|
|
53
|
+
/** Whether this Escape was already claimed by a nested Modal. */
|
|
54
|
+
function isEscapeHandledByModal(event) {
|
|
55
|
+
return Boolean(event === null || event === void 0 ? void 0 : event[HANDLED_FLAG]);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wires up Escape containment for a legacy `Modal` and returns a cleanup
|
|
59
|
+
* function. While active it:
|
|
60
|
+
*
|
|
61
|
+
* - marks the event in the CAPTURE phase when the Modal is top-most, so an
|
|
62
|
+
* outer Base UI `Dialog` defers instead of closing; and
|
|
63
|
+
* - closes the Modal in the BUBBLE phase when it is top-most. Bubble phase
|
|
64
|
+
* means a nested overlay (Select, Popover, DatePicker, …) that stops the
|
|
65
|
+
* Escape below the document prevents this from firing, so the Modal
|
|
66
|
+
* correctly stays open in that case.
|
|
67
|
+
*
|
|
68
|
+
* `getContainer` is read lazily so the current Modal element is always used.
|
|
69
|
+
*/
|
|
70
|
+
function addModalEscapeContainment({ getModal, onRequestClose, }) {
|
|
71
|
+
function markEscape(event) {
|
|
72
|
+
if (event.key !== ESCAPE_KEY)
|
|
73
|
+
return;
|
|
74
|
+
if (!isTopMostOverlayForEscape(event, getModal()))
|
|
75
|
+
return;
|
|
76
|
+
markEscapeHandledByModal(event);
|
|
77
|
+
}
|
|
78
|
+
function closeOnEscape(event) {
|
|
79
|
+
if (event.key !== ESCAPE_KEY)
|
|
80
|
+
return;
|
|
81
|
+
if (!isTopMostOverlayForEscape(event, getModal()))
|
|
82
|
+
return;
|
|
83
|
+
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose();
|
|
84
|
+
}
|
|
85
|
+
document.addEventListener("keydown", markEscape, true);
|
|
86
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
87
|
+
return () => {
|
|
88
|
+
document.removeEventListener("keydown", markEscape, true);
|
|
89
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exports.addModalEscapeContainment = addModalEscapeContainment;
|
|
94
|
+
exports.isEscapeHandledByModal = isEscapeHandledByModal;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-key containment between the legacy prop-based `Modal` and Base UI's
|
|
3
|
+
* `Dialog`.
|
|
4
|
+
*
|
|
5
|
+
* The problem: both overlays listen for Escape at the `document` level and both
|
|
6
|
+
* portal their content to `<body>`. When one is nested inside the other, a
|
|
7
|
+
* single Escape reaches both listeners, so both layers close. Only the top-most
|
|
8
|
+
* layer should close.
|
|
9
|
+
*
|
|
10
|
+
* The legacy `Modal` can't join Base UI's floating-element tree, so this module
|
|
11
|
+
* coordinates the two with a small, explicit protocol:
|
|
12
|
+
*
|
|
13
|
+
* 1. Top-most check — because each overlay portals to `<body>`, the focused
|
|
14
|
+
* content's *nearest* `[role="dialog"]` ancestor is always its own
|
|
15
|
+
* container. So the Modal only acts when that nearest dialog is itself; if
|
|
16
|
+
* it's a different (nested) dialog, the Modal defers. See
|
|
17
|
+
* {@link isTopMostOverlayForEscape}.
|
|
18
|
+
*
|
|
19
|
+
* 2. Deferral signal — when the Modal is top-most it stamps the native event
|
|
20
|
+
* during the CAPTURE phase (which runs before Base UI's bubble-phase
|
|
21
|
+
* dismissal). An outer Base UI `Dialog` reads that flag in its
|
|
22
|
+
* `onOpenChange` handler and cancels, so the Dialog stays open while the
|
|
23
|
+
* Modal closes. See {@link markEscapeHandledByModal} /
|
|
24
|
+
* {@link isEscapeHandledByModal}.
|
|
25
|
+
*
|
|
26
|
+
* Note: this is a DOM-proximity heuristic rather than a true overlay stack. It
|
|
27
|
+
* holds only because both overlays portal to `<body>` and expose a dialog role.
|
|
28
|
+
*/
|
|
29
|
+
const ESCAPE_KEY = "Escape";
|
|
30
|
+
/** Selector matching either an accessible dialog or alert dialog surface. */
|
|
31
|
+
const DIALOG_ROLE_SELECTOR = '[role="dialog"], [role="alertdialog"]';
|
|
32
|
+
/** Property stamped on the native Escape event to signal Modal ownership. */
|
|
33
|
+
const HANDLED_FLAG = "atlantisHandledByNestedModal";
|
|
34
|
+
/**
|
|
35
|
+
* Whether `container` is the top-most overlay for this Escape — i.e. the
|
|
36
|
+
* Escape originated inside `container` and not inside a more deeply nested
|
|
37
|
+
* dialog/overlay. Returns `true` when the Escape happened outside any dialog
|
|
38
|
+
* (e.g. focus not yet inside the Modal) so the Modal keeps its historical
|
|
39
|
+
* focus-independent close behaviour.
|
|
40
|
+
*/
|
|
41
|
+
function isTopMostOverlayForEscape(event, container) {
|
|
42
|
+
var _a;
|
|
43
|
+
const target = event.target;
|
|
44
|
+
const nearestDialog = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, DIALOG_ROLE_SELECTOR);
|
|
45
|
+
return !nearestDialog || nearestDialog === container;
|
|
46
|
+
}
|
|
47
|
+
/** Marks the Escape as owned by a Modal so an outer Dialog can defer. */
|
|
48
|
+
function markEscapeHandledByModal(event) {
|
|
49
|
+
event[HANDLED_FLAG] = true;
|
|
50
|
+
}
|
|
51
|
+
/** Whether this Escape was already claimed by a nested Modal. */
|
|
52
|
+
function isEscapeHandledByModal(event) {
|
|
53
|
+
return Boolean(event === null || event === void 0 ? void 0 : event[HANDLED_FLAG]);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wires up Escape containment for a legacy `Modal` and returns a cleanup
|
|
57
|
+
* function. While active it:
|
|
58
|
+
*
|
|
59
|
+
* - marks the event in the CAPTURE phase when the Modal is top-most, so an
|
|
60
|
+
* outer Base UI `Dialog` defers instead of closing; and
|
|
61
|
+
* - closes the Modal in the BUBBLE phase when it is top-most. Bubble phase
|
|
62
|
+
* means a nested overlay (Select, Popover, DatePicker, …) that stops the
|
|
63
|
+
* Escape below the document prevents this from firing, so the Modal
|
|
64
|
+
* correctly stays open in that case.
|
|
65
|
+
*
|
|
66
|
+
* `getContainer` is read lazily so the current Modal element is always used.
|
|
67
|
+
*/
|
|
68
|
+
function addModalEscapeContainment({ getModal, onRequestClose, }) {
|
|
69
|
+
function markEscape(event) {
|
|
70
|
+
if (event.key !== ESCAPE_KEY)
|
|
71
|
+
return;
|
|
72
|
+
if (!isTopMostOverlayForEscape(event, getModal()))
|
|
73
|
+
return;
|
|
74
|
+
markEscapeHandledByModal(event);
|
|
75
|
+
}
|
|
76
|
+
function closeOnEscape(event) {
|
|
77
|
+
if (event.key !== ESCAPE_KEY)
|
|
78
|
+
return;
|
|
79
|
+
if (!isTopMostOverlayForEscape(event, getModal()))
|
|
80
|
+
return;
|
|
81
|
+
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose();
|
|
82
|
+
}
|
|
83
|
+
document.addEventListener("keydown", markEscape, true);
|
|
84
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
85
|
+
return () => {
|
|
86
|
+
document.removeEventListener("keydown", markEscape, true);
|
|
87
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { addModalEscapeContainment as a, isEscapeHandledByModal as i };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components",
|
|
3
|
-
"version": "8.27.
|
|
3
|
+
"version": "8.27.3-FIXproper-e2c0fc2.2+e2c0fc29",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -545,5 +545,5 @@
|
|
|
545
545
|
"> 1%",
|
|
546
546
|
"IE 10"
|
|
547
547
|
],
|
|
548
|
-
"gitHead": "
|
|
548
|
+
"gitHead": "e2c0fc291df3952458a5b6151abdc433402c7286"
|
|
549
549
|
}
|