@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.
@@ -7,7 +7,6 @@ require('../Modal/index.cjs');
7
7
  require('react-dom');
8
8
  require('classnames');
9
9
  require('framer-motion');
10
- require('../nestedEscape-cjs.js');
11
10
  require('../Heading-cjs.js');
12
11
  require('../Typography-cjs.js');
13
12
  require('../Button-cjs.js');
@@ -5,7 +5,6 @@ import '../Modal/index.mjs';
5
5
  import 'react-dom';
6
6
  import 'classnames';
7
7
  import 'framer-motion';
8
- import '../nestedEscape-es.js';
9
8
  import '../Heading-es.js';
10
9
  import '../Typography-es.js';
11
10
  import '../Button-es.js';
@@ -25,7 +25,6 @@ require('../Heading-cjs.js');
25
25
  require('../BottomSheet-cjs.js');
26
26
  require('../DrawerTitle-cjs.js');
27
27
  require('../DrawerDescription-cjs.js');
28
- require('../nestedEscape-cjs.js');
29
28
 
30
29
 
31
30
 
@@ -23,4 +23,3 @@ import '../Heading-es.js';
23
23
  import '../BottomSheet-es.js';
24
24
  import '../DrawerTitle-es.js';
25
25
  import '../DrawerDescription-es.js';
26
- import '../nestedEscape-es.js';
@@ -20,7 +20,6 @@ require('@jobber/hooks');
20
20
  require('../Modal/index.cjs');
21
21
  require('react-dom');
22
22
  require('framer-motion');
23
- require('../nestedEscape-cjs.js');
24
23
  require('../Heading-cjs.js');
25
24
  require('../ButtonDismiss-cjs.js');
26
25
  require('../noop-cjs.js');
@@ -18,7 +18,6 @@ import '@jobber/hooks';
18
18
  import '../Modal/index.mjs';
19
19
  import 'react-dom';
20
20
  import 'framer-motion';
21
- import '../nestedEscape-es.js';
22
21
  import '../Heading-es.js';
23
22
  import '../ButtonDismiss-es.js';
24
23
  import '../noop-es.js';
@@ -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');
@@ -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';
@@ -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
- // 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]);
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" },
@@ -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
- // 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]);
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,14 @@
1
+ export declare const commonMessages: {
2
+ Loading: {
3
+ en: string;
4
+ es: string;
5
+ };
6
+ Confirm: {
7
+ en: string;
8
+ es: string;
9
+ };
10
+ Cancel: {
11
+ en: string;
12
+ es: string;
13
+ };
14
+ };
@@ -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,3 @@
1
+ export { commonMessages } from "./common.messages";
2
+ export { defineMessages, useAtlantisI18n } from "./useAtlantisI18n";
3
+ export type { AtlantisMessages } from "./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.27.3-FIXproper-e2c0fc2.2+e2c0fc29",
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": "e2c0fc291df3952458a5b6151abdc433402c7286"
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;
@@ -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;
@@ -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 };