@jobber/components 8.27.3-FIXproper-e2c0fc2.2 → 8.28.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/dist/ConfirmationModal/index.cjs +0 -1
- package/dist/ConfirmationModal/index.mjs +0 -1
- package/dist/Dialog/index.cjs +0 -1
- package/dist/Dialog/index.mjs +0 -1
- package/dist/FormatFile/index.cjs +0 -1
- package/dist/FormatFile/index.mjs +0 -1
- package/dist/Gallery/index.cjs +0 -1
- package/dist/Gallery/index.mjs +0 -1
- package/dist/Modal/index.cjs +6 -11
- package/dist/Modal/index.mjs +7 -12
- package/dist/dialogReturnFocus-cjs.js +0 -9
- package/dist/dialogReturnFocus-es.js +0 -9
- package/dist/index.cjs +0 -1
- package/dist/index.mjs +0 -1
- package/dist/sharedHelpers/i18n/common.messages.d.ts +14 -0
- package/dist/sharedHelpers/i18n/index.cjs +51 -0
- package/dist/sharedHelpers/i18n/index.d.ts +3 -0
- package/dist/sharedHelpers/i18n/index.mjs +47 -0
- package/dist/sharedHelpers/i18n/useAtlantisI18n.d.ts +13 -0
- package/package.json +2 -2
- package/dist/Modal/nestedEscape.d.ts +0 -57
- package/dist/nestedEscape-cjs.js +0 -94
- package/dist/nestedEscape-es.js +0 -91
package/dist/Dialog/index.cjs
CHANGED
package/dist/Dialog/index.mjs
CHANGED
package/dist/Gallery/index.cjs
CHANGED
|
@@ -41,7 +41,6 @@ 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');
|
|
45
44
|
require('../AtlantisPortalContent-cjs.js');
|
|
46
45
|
require('../Markdown-cjs.js');
|
|
47
46
|
require('../Emphasis-cjs.js');
|
package/dist/Gallery/index.mjs
CHANGED
|
@@ -39,7 +39,6 @@ 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';
|
|
43
42
|
import '../AtlantisPortalContent-es.js';
|
|
44
43
|
import '../Markdown-es.js';
|
|
45
44
|
import '../Emphasis-es.js';
|
package/dist/Modal/index.cjs
CHANGED
|
@@ -5,7 +5,6 @@ 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');
|
|
9
8
|
var Heading = require('../Heading-cjs.js');
|
|
10
9
|
var Button = require('../Button-cjs.js');
|
|
11
10
|
var ButtonDismiss = require('../ButtonDismiss-cjs.js');
|
|
@@ -43,16 +42,7 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
43
42
|
const containerClassName = classnames(styles$1.container, size === "fullScreen" && styles$1.containerFullScreen);
|
|
44
43
|
jobberHooks.useRefocusOnActivator(open);
|
|
45
44
|
const modalRef = jobberHooks.useFocusTrap(open);
|
|
46
|
-
|
|
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]);
|
|
45
|
+
jobberHooks.useOnKeyDown(handleRequestClose, "Escape");
|
|
56
46
|
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 },
|
|
57
47
|
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 } }),
|
|
58
48
|
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: {
|
|
@@ -65,6 +55,11 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
65
55
|
return (globalThis === null || globalThis === void 0 ? void 0 : globalThis.document)
|
|
66
56
|
? ReactDOM.createPortal(template, document.body)
|
|
67
57
|
: template;
|
|
58
|
+
function handleRequestClose() {
|
|
59
|
+
if (open && onRequestClose) {
|
|
60
|
+
onRequestClose();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
68
63
|
}
|
|
69
64
|
function Header({ title, dismissible, onRequestClose }) {
|
|
70
65
|
return (React.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
|
package/dist/Modal/index.mjs
CHANGED
|
@@ -2,8 +2,7 @@ import React__default, { useRef, useEffect, createContext, useContext } from 're
|
|
|
2
2
|
import ReactDOM__default from 'react-dom';
|
|
3
3
|
import classnames from 'classnames';
|
|
4
4
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
5
|
-
import { useRefocusOnActivator, useFocusTrap } from '@jobber/hooks';
|
|
6
|
-
import { a as addModalEscapeContainment } from '../nestedEscape-es.js';
|
|
5
|
+
import { useRefocusOnActivator, useFocusTrap, useOnKeyDown } from '@jobber/hooks';
|
|
7
6
|
import { H as Heading } from '../Heading-es.js';
|
|
8
7
|
import { B as Button } from '../Button-es.js';
|
|
9
8
|
import { B as ButtonDismiss } from '../ButtonDismiss-es.js';
|
|
@@ -41,16 +40,7 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
41
40
|
const containerClassName = classnames(styles$1.container, size === "fullScreen" && styles$1.containerFullScreen);
|
|
42
41
|
useRefocusOnActivator(open);
|
|
43
42
|
const modalRef = useFocusTrap(open);
|
|
44
|
-
|
|
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]);
|
|
43
|
+
useOnKeyDown(handleRequestClose, "Escape");
|
|
54
44
|
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 },
|
|
55
45
|
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 } }),
|
|
56
46
|
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: {
|
|
@@ -63,6 +53,11 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
|
|
|
63
53
|
return (globalThis === null || globalThis === void 0 ? void 0 : globalThis.document)
|
|
64
54
|
? ReactDOM__default.createPortal(template, document.body)
|
|
65
55
|
: template;
|
|
56
|
+
function handleRequestClose() {
|
|
57
|
+
if (open && onRequestClose) {
|
|
58
|
+
onRequestClose();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
66
61
|
}
|
|
67
62
|
function Header({ title, dismissible, onRequestClose }) {
|
|
68
63
|
return (React__default.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
|
|
@@ -12,7 +12,6 @@ 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');
|
|
16
15
|
|
|
17
16
|
// Copied from Menu's constant so the two surfaces agree on what "mobile" means.
|
|
18
17
|
// (The proposal calls for copying shared Menu values into the component first
|
|
@@ -307,14 +306,6 @@ function useDialogShell() {
|
|
|
307
306
|
function Dialog(_a) {
|
|
308
307
|
var { onRequestClose, size = "base", type = "dialog", finalFocus, children, onOpenChange } = _a, rest = tslib_es6.__rest(_a, ["onRequestClose", "size", "type", "finalFocus", "children", "onOpenChange"]);
|
|
309
308
|
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
|
-
}
|
|
318
309
|
if (!nextOpen) {
|
|
319
310
|
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose(eventDetails);
|
|
320
311
|
}
|
|
@@ -10,7 +10,6 @@ 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';
|
|
14
13
|
|
|
15
14
|
// Copied from Menu's constant so the two surfaces agree on what "mobile" means.
|
|
16
15
|
// (The proposal calls for copying shared Menu values into the component first
|
|
@@ -305,14 +304,6 @@ function useDialogShell() {
|
|
|
305
304
|
function Dialog(_a) {
|
|
306
305
|
var { onRequestClose, size = "base", type = "dialog", finalFocus, children, onOpenChange } = _a, rest = __rest(_a, ["onRequestClose", "size", "type", "finalFocus", "children", "onOpenChange"]);
|
|
307
306
|
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
|
-
}
|
|
316
307
|
if (!nextOpen) {
|
|
317
308
|
onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose(eventDetails);
|
|
318
309
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -180,7 +180,6 @@ 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');
|
|
184
183
|
require('./useFormFieldFocus-cjs.js');
|
|
185
184
|
require('filesize');
|
|
186
185
|
require('./GridCell-cjs.js');
|
package/dist/index.mjs
CHANGED
|
@@ -178,7 +178,6 @@ 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';
|
|
182
181
|
import './useFormFieldFocus-es.js';
|
|
183
182
|
import 'filesize';
|
|
184
183
|
import './GridCell-es.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var AtlantisContext = require('../../AtlantisContext-cjs.js');
|
|
5
|
+
|
|
6
|
+
function defineMessages(messages) {
|
|
7
|
+
return messages;
|
|
8
|
+
}
|
|
9
|
+
function useAtlantisI18n(messages) {
|
|
10
|
+
const { locale } = AtlantisContext.useAtlantisContext();
|
|
11
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
12
|
+
const t = React.useCallback((messageKey, values) => {
|
|
13
|
+
return interpolateMessage(resolveMessage(messages[messageKey], normalizedLocale), values);
|
|
14
|
+
}, [normalizedLocale, messages]);
|
|
15
|
+
return { locale: normalizedLocale, t };
|
|
16
|
+
}
|
|
17
|
+
function resolveMessage(message, normalizedLocale) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const baseLocale = normalizedLocale.split("-")[0];
|
|
20
|
+
return (_b = (_a = message[normalizedLocale]) !== null && _a !== void 0 ? _a : message[baseLocale]) !== null && _b !== void 0 ? _b : message.en;
|
|
21
|
+
}
|
|
22
|
+
function normalizeLocale(locale) {
|
|
23
|
+
return locale.trim().replace(/_/g, "-").toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function interpolateMessage(message, values) {
|
|
26
|
+
if (!values)
|
|
27
|
+
return message;
|
|
28
|
+
return message.replace(/\{(\w+)\}/g, (placeholder, key) => {
|
|
29
|
+
const value = values[key];
|
|
30
|
+
return value === undefined ? placeholder : String(value);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const commonMessages = defineMessages({
|
|
35
|
+
Loading: {
|
|
36
|
+
en: "Loading",
|
|
37
|
+
es: "Cargando",
|
|
38
|
+
},
|
|
39
|
+
Confirm: {
|
|
40
|
+
en: "Confirm",
|
|
41
|
+
es: "Confirmar",
|
|
42
|
+
},
|
|
43
|
+
Cancel: {
|
|
44
|
+
en: "Cancel",
|
|
45
|
+
es: "Cancelar",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
exports.commonMessages = commonMessages;
|
|
50
|
+
exports.defineMessages = defineMessages;
|
|
51
|
+
exports.useAtlantisI18n = useAtlantisI18n;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { u as useAtlantisContext } from '../../AtlantisContext-es.js';
|
|
3
|
+
|
|
4
|
+
function defineMessages(messages) {
|
|
5
|
+
return messages;
|
|
6
|
+
}
|
|
7
|
+
function useAtlantisI18n(messages) {
|
|
8
|
+
const { locale } = useAtlantisContext();
|
|
9
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
10
|
+
const t = useCallback((messageKey, values) => {
|
|
11
|
+
return interpolateMessage(resolveMessage(messages[messageKey], normalizedLocale), values);
|
|
12
|
+
}, [normalizedLocale, messages]);
|
|
13
|
+
return { locale: normalizedLocale, t };
|
|
14
|
+
}
|
|
15
|
+
function resolveMessage(message, normalizedLocale) {
|
|
16
|
+
var _a, _b;
|
|
17
|
+
const baseLocale = normalizedLocale.split("-")[0];
|
|
18
|
+
return (_b = (_a = message[normalizedLocale]) !== null && _a !== void 0 ? _a : message[baseLocale]) !== null && _b !== void 0 ? _b : message.en;
|
|
19
|
+
}
|
|
20
|
+
function normalizeLocale(locale) {
|
|
21
|
+
return locale.trim().replace(/_/g, "-").toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function interpolateMessage(message, values) {
|
|
24
|
+
if (!values)
|
|
25
|
+
return message;
|
|
26
|
+
return message.replace(/\{(\w+)\}/g, (placeholder, key) => {
|
|
27
|
+
const value = values[key];
|
|
28
|
+
return value === undefined ? placeholder : String(value);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const commonMessages = defineMessages({
|
|
33
|
+
Loading: {
|
|
34
|
+
en: "Loading",
|
|
35
|
+
es: "Cargando",
|
|
36
|
+
},
|
|
37
|
+
Confirm: {
|
|
38
|
+
en: "Confirm",
|
|
39
|
+
es: "Confirmar",
|
|
40
|
+
},
|
|
41
|
+
Cancel: {
|
|
42
|
+
en: "Cancel",
|
|
43
|
+
es: "Cancelar",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export { commonMessages, defineMessages, useAtlantisI18n };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AtlantisMessages {
|
|
2
|
+
readonly [messageKey: string]: {
|
|
3
|
+
readonly en: string;
|
|
4
|
+
readonly [locale: string]: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
type MessageValues = Record<string, string | number>;
|
|
8
|
+
export declare function defineMessages<Messages extends AtlantisMessages>(messages: Messages): Messages;
|
|
9
|
+
export declare function useAtlantisI18n<Messages extends AtlantisMessages>(messages: Messages): {
|
|
10
|
+
readonly locale: string;
|
|
11
|
+
readonly t: (messageKey: keyof Messages, values?: MessageValues) => string;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.28.0",
|
|
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": "75d8d5864dc4ae25c056ff90847551a4097eee1f"
|
|
549
549
|
}
|
|
@@ -1,57 +0,0 @@
|
|
|
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;
|
package/dist/nestedEscape-cjs.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
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;
|
package/dist/nestedEscape-es.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
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 };
|