@prenta/admin 0.97.4 → 0.97.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @actuate-media/cms-admin
2
2
 
3
+ ## 0.97.5
4
+
5
+ ### Patch Changes
6
+
7
+ - c3ae495: Fix Post Details (and other `Modal` + `onDismiss` surfaces) discarding the form when clicking a nested dialog such as the media picker — outside interactions now ignore events that belong to another `[role="dialog"]`.
8
+
3
9
  ## 0.97.4
4
10
 
5
11
  ### Patch Changes
@@ -1,5 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @vitest-environment happy-dom
3
+ import * as Dialog from '@radix-ui/react-dialog';
4
+ import { useState } from 'react';
3
5
  import { describe, expect, it, vi } from 'vitest';
4
6
  import { fireEvent, render, screen } from '@testing-library/react';
5
7
  /**
@@ -9,7 +11,14 @@ import { fireEvent, render, screen } from '@testing-library/react';
9
11
  * Escape / close both fire `onClose`. Hand-rolled `<div>` overlays gave us none
10
12
  * of this.
11
13
  */
12
- const { Modal } = await import('../../components/ui/Modal.js');
14
+ const { Modal, isNestedDialogEvent } = await import('../../components/ui/Modal.js');
15
+ function NestedDialog({ onOpenChange }) {
16
+ const [open, setOpen] = useState(true);
17
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: (next) => {
18
+ setOpen(next);
19
+ onOpenChange?.(next);
20
+ }, children: _jsx(Dialog.Portal, { children: _jsxs(Dialog.Content, { "aria-describedby": undefined, children: [_jsx(Dialog.Title, { children: "Media picker" }), _jsx("button", { type: "button", children: "Choose image" })] }) }) }));
21
+ }
13
22
  describe('Modal accessibility', () => {
14
23
  it('does not render content while closed', () => {
15
24
  render(_jsx(Modal, { open: false, onClose: () => { }, title: "Edit page", children: _jsx("p", { children: "Body" }) }));
@@ -33,5 +42,56 @@ describe('Modal accessibility', () => {
33
42
  fireEvent.keyDown(document.body, { key: 'Escape' });
34
43
  expect(onClose).toHaveBeenCalled();
35
44
  });
45
+ it('routes Escape through onDismiss when provided', () => {
46
+ const onClose = vi.fn();
47
+ const onDismiss = vi.fn();
48
+ render(_jsx(Modal, { open: true, onClose: onClose, onDismiss: onDismiss, title: "Post details", children: _jsx("p", { children: "Body" }) }));
49
+ fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
50
+ expect(onDismiss).toHaveBeenCalled();
51
+ expect(onClose).not.toHaveBeenCalled();
52
+ });
53
+ it('keeps the parent open when interacting with a portaled nested dialog', async () => {
54
+ const onDismiss = vi.fn();
55
+ render(_jsx(Modal, { open: true, onClose: () => { }, onDismiss: onDismiss, title: "Post details", description: "Edit post details", children: _jsx("p", { children: "Body" }) }));
56
+ render(_jsx(NestedDialog, {}));
57
+ await new Promise((resolve) => setTimeout(resolve, 0));
58
+ onDismiss.mockClear();
59
+ const chooseImage = screen.getByRole('button', { name: 'Choose image' });
60
+ fireEvent.pointerDown(chooseImage);
61
+ fireEvent.focusIn(chooseImage);
62
+ expect(onDismiss).not.toHaveBeenCalled();
63
+ });
64
+ it('closes only the portaled nested dialog on Escape', async () => {
65
+ const onDismiss = vi.fn();
66
+ const onNestedOpenChange = vi.fn();
67
+ render(_jsx(Modal, { open: true, onClose: () => { }, onDismiss: onDismiss, title: "Post details", description: "Edit post details", children: _jsx("p", { children: "Body" }) }));
68
+ render(_jsx(NestedDialog, { onOpenChange: onNestedOpenChange }));
69
+ await new Promise((resolve) => setTimeout(resolve, 0));
70
+ onDismiss.mockClear();
71
+ onNestedOpenChange.mockClear();
72
+ fireEvent.keyDown(screen.getByRole('dialog', { name: 'Media picker' }), { key: 'Escape' });
73
+ expect(onNestedOpenChange).toHaveBeenCalledWith(false);
74
+ expect(screen.queryByRole('dialog', { name: 'Media picker' })).toBeNull();
75
+ expect(onDismiss).not.toHaveBeenCalled();
76
+ });
77
+ });
78
+ describe('isNestedDialogEvent (UpChat #27)', () => {
79
+ it('detects events that belong to a different dialog than the Modal content', () => {
80
+ const parent = document.createElement('div');
81
+ parent.setAttribute('role', 'dialog');
82
+ const nested = document.createElement('div');
83
+ nested.setAttribute('role', 'dialog');
84
+ const button = document.createElement('button');
85
+ nested.appendChild(button);
86
+ document.body.append(parent, nested);
87
+ // MediaPicker / ConfirmDialog portals sit outside this Modal's content —
88
+ // their clicks must not count as "interact outside" for dismiss.
89
+ expect(isNestedDialogEvent(button, parent)).toBe(true);
90
+ expect(isNestedDialogEvent(button, nested)).toBe(false);
91
+ expect(isNestedDialogEvent(parent, parent)).toBe(false);
92
+ expect(isNestedDialogEvent(null, parent)).toBe(false);
93
+ parent.remove();
94
+ nested.remove();
95
+ });
36
96
  });
37
97
  //# sourceMappingURL=modal.render.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"modal.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/components/modal.render.test.tsx"],"names":[],"mappings":";AAAA,gCAAgC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAElE;;;;;;GAMG;AAEH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAA;AAE9D,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAC,WAAW,YACtD,+BAAW,GACL,CACT,CAAA;QACD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAC,WAAW,YAC9C,+BAAW,GACL,CACT,CAAA;QACD,sEAAsE;QACtE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;QACtE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAC,WAAW,YAC7C,+BAAW,GACL,CACT,CAAA;QACD,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAA;QACrE,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAC,WAAW,YAC7C,+BAAW,GACL,CACT,CAAA;QACD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;QACnD,MAAM,CAAC,OAAO,CAAC,CAAC,gBAAgB,EAAE,CAAA;IACpC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"modal.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/components/modal.render.test.tsx"],"names":[],"mappings":";AAAA,gCAAgC;AAChC,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAElE;;;;;;GAMG;AAEH,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAA;AAEnF,SAAS,YAAY,CAAC,EAAE,YAAY,EAA8C;IAChF,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAEtC,OAAO,CACL,KAAC,MAAM,CAAC,IAAI,IACV,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,OAAO,CAAC,IAAI,CAAC,CAAA;YACb,YAAY,EAAE,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC,YAED,KAAC,MAAM,CAAC,MAAM,cACZ,MAAC,MAAM,CAAC,OAAO,wBAAmB,SAAS,aACzC,KAAC,MAAM,CAAC,KAAK,+BAA4B,EACzC,iBAAQ,IAAI,EAAC,QAAQ,6BAAsB,IAC5B,GACH,GACJ,CACf,CAAA;AACH,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAC,WAAW,YACtD,+BAAW,GACL,CACT,CAAA;QACD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAC,WAAW,YAC9C,+BAAW,GACL,CACT,CAAA;QACD,sEAAsE;QACtE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;QACtE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAC,WAAW,YAC7C,+BAAW,GACL,CACT,CAAA;QACD,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAA;QACrE,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAC,WAAW,YAC7C,+BAAW,GACL,CACT,CAAA;QACD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;QACnD,MAAM,CAAC,OAAO,CAAC,CAAC,gBAAgB,EAAE,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACvB,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACzB,MAAM,CACJ,KAAC,KAAK,IAAC,IAAI,QAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAC,cAAc,YACtE,+BAAW,GACL,CACT,CAAA;QACD,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;QAChE,MAAM,CAAC,SAAS,CAAC,CAAC,gBAAgB,EAAE,CAAA;QACpC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACzB,MAAM,CACJ,KAAC,KAAK,IACJ,IAAI,QACJ,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EACjB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAC,cAAc,EACpB,WAAW,EAAC,mBAAmB,YAE/B,+BAAW,GACL,CACT,CAAA;QACD,MAAM,CAAC,KAAC,YAAY,KAAG,CAAC,CAAA;QACxB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;QACtD,SAAS,CAAC,SAAS,EAAE,CAAA;QAErB,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAA;QACxE,SAAS,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;QAClC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAE9B,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACzB,MAAM,kBAAkB,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QAClC,MAAM,CACJ,KAAC,KAAK,IACJ,IAAI,QACJ,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EACjB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAC,cAAc,EACpB,WAAW,EAAC,mBAAmB,YAE/B,+BAAW,GACL,CACT,CAAA;QACD,MAAM,CAAC,KAAC,YAAY,IAAC,YAAY,EAAE,kBAAkB,GAAI,CAAC,CAAA;QAC1D,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;QACtD,SAAS,CAAC,SAAS,EAAE,CAAA;QACrB,kBAAkB,CAAC,SAAS,EAAE,CAAA;QAE9B,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;QAE1F,MAAM,CAAC,kBAAkB,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAA;QACtD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QACzE,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;QAC5C,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;QAC5C,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;QAC/C,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;QAC1B,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAEpC,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACtD,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAErD,MAAM,CAAC,MAAM,EAAE,CAAA;QACf,MAAM,CAAC,MAAM,EAAE,CAAA;IACjB,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -32,6 +32,14 @@ export interface ModalProps {
32
32
  */
33
33
  onDismiss?: () => void;
34
34
  }
35
+ /**
36
+ * True when the event originated inside a *different* `[role="dialog"]` than
37
+ * `content` (e.g. MediaPickerModal / ConfirmDialog portaled above this Modal).
38
+ * Nested dialogs must own their own dismiss; treating their clicks as "outside"
39
+ * would discard the parent form before the nested selection can apply
40
+ * (UpChat #27 / featured-image picker).
41
+ */
42
+ export declare function isNestedDialogEvent(target: EventTarget | null, content: EventTarget | null): boolean;
35
43
  /**
36
44
  * The canonical admin modal. Built on Radix `Dialog`, which provides the
37
45
  * accessibility contract for free: focus is trapped while open and restored to
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Modal.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtD,QAAA,MAAM,gBAAgB;;;IAGpB,mFAAmF;;CAE3E,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,gBAAgB,CAAA;AAErD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,SAAS,CAAA;IACnB,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAW,EACX,WAAW,EACX,eAAe,EACf,SAAS,GACV,EAAE,UAAU,2CAmEZ"}
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Modal.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAU,MAAM,OAAO,CAAA;AAG9D,QAAA,MAAM,gBAAgB;;;IAGpB,mFAAmF;;CAE3E,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,gBAAgB,CAAA;AAErD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,SAAS,CAAA;IACnB,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,GAAG,IAAI,EAC1B,OAAO,EAAE,WAAW,GAAG,IAAI,GAC1B,OAAO,CAIT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAW,EACX,WAAW,EACX,eAAe,EACf,SAAS,GACV,EAAE,UAAU,2CA2EZ"}
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as Dialog from '@radix-ui/react-dialog';
4
4
  import { X } from 'lucide-react';
5
+ import { useRef } from 'react';
5
6
  import { adminPortalContainer } from '../../lib/portal-container.js';
6
7
  const MODAL_SIZE_CLASS = {
7
8
  md: 'max-w-lg',
@@ -9,6 +10,19 @@ const MODAL_SIZE_CLASS = {
9
10
  /** Wide form dialogs (post/page field editors) — body scrolls inside the shell. */
10
11
  xl: 'flex max-h-[min(92vh,880px)] max-w-5xl flex-col',
11
12
  };
13
+ /**
14
+ * True when the event originated inside a *different* `[role="dialog"]` than
15
+ * `content` (e.g. MediaPickerModal / ConfirmDialog portaled above this Modal).
16
+ * Nested dialogs must own their own dismiss; treating their clicks as "outside"
17
+ * would discard the parent form before the nested selection can apply
18
+ * (UpChat #27 / featured-image picker).
19
+ */
20
+ export function isNestedDialogEvent(target, content) {
21
+ if (!(target instanceof Element) || !(content instanceof Element))
22
+ return false;
23
+ const owningDialog = target.closest('[role="dialog"]');
24
+ return Boolean(owningDialog && owningDialog !== content);
25
+ }
12
26
  /**
13
27
  * The canonical admin modal. Built on Radix `Dialog`, which provides the
14
28
  * accessibility contract for free: focus is trapped while open and restored to
@@ -22,6 +36,7 @@ const MODAL_SIZE_CLASS = {
22
36
  */
23
37
  export function Modal({ open, onClose, title, children, actions, size = 'md', description, initialFocusRef, onDismiss, }) {
24
38
  const scrollBody = size === 'xl';
39
+ const contentRef = useRef(null);
25
40
  function handleDismissAttempt() {
26
41
  if (onDismiss)
27
42
  onDismiss();
@@ -31,7 +46,13 @@ export function Modal({ open, onClose, title, children, actions, size = 'md', de
31
46
  return (_jsx(Dialog.Root, { open: open, onOpenChange: (next) => {
32
47
  if (!next)
33
48
  handleDismissAttempt();
34
- }, children: _jsxs(Dialog.Portal, { container: adminPortalContainer(), children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-60 bg-black/50" }), _jsxs(Dialog.Content, { className: `bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none ${MODAL_SIZE_CLASS[size]}`, onInteractOutside: (event) => {
49
+ }, children: _jsxs(Dialog.Portal, { container: adminPortalContainer(), children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-60 bg-black/50" }), _jsxs(Dialog.Content, { ref: contentRef, className: `bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none ${MODAL_SIZE_CLASS[size]}`, onInteractOutside: (event) => {
50
+ // Radix dispatches this custom event on the outside target, so
51
+ // `event.currentTarget` is not the parent Dialog.Content.
52
+ if (isNestedDialogEvent(event.target, contentRef.current)) {
53
+ event.preventDefault();
54
+ return;
55
+ }
35
56
  if (!onDismiss)
36
57
  return;
37
58
  event.preventDefault();
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../../src/components/ui/Modal.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AAEpE,MAAM,gBAAgB,GAAG;IACvB,EAAE,EAAE,UAAU;IACd,EAAE,EAAE,WAAW;IACf,mFAAmF;IACnF,EAAE,EAAE,iDAAiD;CAC7C,CAAA;AA+BV;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,GAAG,IAAI,EACX,WAAW,EACX,eAAe,EACf,SAAS,GACE;IACX,MAAM,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAEhC,SAAS,oBAAoB;QAC3B,IAAI,SAAS;YAAE,SAAS,EAAE,CAAA;;YACrB,OAAO,EAAE,CAAA;IAChB,CAAC;IAED,OAAO,CACL,KAAC,MAAM,CAAC,IAAI,IACV,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,IAAI,CAAC,IAAI;gBAAE,oBAAoB,EAAE,CAAA;QACnC,CAAC,YAED,MAAC,MAAM,CAAC,MAAM,IAAC,SAAS,EAAE,oBAAoB,EAAE,aAC9C,KAAC,MAAM,CAAC,OAAO,IAAC,SAAS,EAAC,gCAAgC,GAAG,EAC7D,MAAC,MAAM,CAAC,OAAO,IACb,SAAS,EAAE,+KAA+K,gBAAgB,CAAC,IAAI,CAAC,EAAE,EAClN,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE;wBAC3B,IAAI,CAAC,SAAS;4BAAE,OAAM;wBACtB,KAAK,CAAC,cAAc,EAAE,CAAA;wBACtB,oBAAoB,EAAE,CAAA;oBACxB,CAAC,EACD,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;wBACzB,IAAI,CAAC,SAAS;4BAAE,OAAM;wBACtB,KAAK,CAAC,cAAc,EAAE,CAAA;wBACtB,oBAAoB,EAAE,CAAA;oBACxB,CAAC,EACD,eAAe,EACb,eAAe;wBACb,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;4BACR,IAAI,eAAe,CAAC,OAAO,EAAE,CAAC;gCAC5B,KAAK,CAAC,cAAc,EAAE,CAAA;gCACtB,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;4BACjC,CAAC;wBACH,CAAC;wBACH,CAAC,CAAC,SAAS,aAGf,eAAK,SAAS,EAAC,oEAAoE,aACjF,KAAC,MAAM,CAAC,KAAK,IAAC,SAAS,EAAC,qCAAqC,YAAE,KAAK,GAAgB,EACpF,KAAC,MAAM,CAAC,KAAK,IAAC,OAAO,kBACnB,iBACE,IAAI,EAAC,QAAQ,gBACF,cAAc,EACzB,SAAS,EAAC,sKAAsK,YAEhL,KAAC,CAAC,IAAC,SAAS,EAAC,SAAS,wBAAe,GAC9B,GACI,IACX,EACL,WAAW,IAAI,CACd,KAAC,MAAM,CAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,YACpE,WAAW,GACO,CACtB,EACD,cAAK,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAC,WAAW,YAClF,QAAQ,GACL,EACL,OAAO,IAAI,CACV,cAAK,SAAS,EAAC,yDAAyD,YAAE,OAAO,GAAO,CACzF,IACc,IACH,GACJ,CACf,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../../src/components/ui/Modal.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/admin",
3
- "version": "0.97.4",
3
+ "version": "0.97.5",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -98,15 +98,15 @@
98
98
  "tailwindcss": "^4.0.0",
99
99
  "typescript": "^5.7.0",
100
100
  "vitest": "^4.1.0",
101
- "@prenta/core": "0.123.0",
102
- "@prenta/plugin-ai": "0.6.0",
103
- "@prenta/plugin-commerce": "0.2.0",
104
101
  "@prenta/component-blocks": "0.3.0",
102
+ "@prenta/plugin-ai": "0.6.0",
105
103
  "@prenta/plugin-blocks": "0.2.0",
106
- "@prenta/plugin-media": "0.2.0",
104
+ "@prenta/plugin-commerce": "0.2.0",
105
+ "@prenta/core": "0.123.0",
107
106
  "@prenta/plugin-forms": "0.1.0",
108
- "@prenta/plugin-seo": "0.1.0",
107
+ "@prenta/plugin-media": "0.2.0",
109
108
  "@prenta/plugin-navigation": "0.3.0",
109
+ "@prenta/plugin-seo": "0.1.0",
110
110
  "@prenta/sections-react": "0.17.2"
111
111
  },
112
112
  "scripts": {
@@ -1,4 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
+ import * as Dialog from '@radix-ui/react-dialog'
3
+ import { useState } from 'react'
2
4
  import { describe, expect, it, vi } from 'vitest'
3
5
  import { fireEvent, render, screen } from '@testing-library/react'
4
6
 
@@ -10,7 +12,28 @@ import { fireEvent, render, screen } from '@testing-library/react'
10
12
  * of this.
11
13
  */
12
14
 
13
- const { Modal } = await import('../../components/ui/Modal.js')
15
+ const { Modal, isNestedDialogEvent } = await import('../../components/ui/Modal.js')
16
+
17
+ function NestedDialog({ onOpenChange }: { onOpenChange?: (open: boolean) => void }) {
18
+ const [open, setOpen] = useState(true)
19
+
20
+ return (
21
+ <Dialog.Root
22
+ open={open}
23
+ onOpenChange={(next) => {
24
+ setOpen(next)
25
+ onOpenChange?.(next)
26
+ }}
27
+ >
28
+ <Dialog.Portal>
29
+ <Dialog.Content aria-describedby={undefined}>
30
+ <Dialog.Title>Media picker</Dialog.Title>
31
+ <button type="button">Choose image</button>
32
+ </Dialog.Content>
33
+ </Dialog.Portal>
34
+ </Dialog.Root>
35
+ )
36
+ }
14
37
 
15
38
  describe('Modal accessibility', () => {
16
39
  it('does not render content while closed', () => {
@@ -54,4 +77,89 @@ describe('Modal accessibility', () => {
54
77
  fireEvent.keyDown(document.body, { key: 'Escape' })
55
78
  expect(onClose).toHaveBeenCalled()
56
79
  })
80
+
81
+ it('routes Escape through onDismiss when provided', () => {
82
+ const onClose = vi.fn()
83
+ const onDismiss = vi.fn()
84
+ render(
85
+ <Modal open onClose={onClose} onDismiss={onDismiss} title="Post details">
86
+ <p>Body</p>
87
+ </Modal>,
88
+ )
89
+ fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
90
+ expect(onDismiss).toHaveBeenCalled()
91
+ expect(onClose).not.toHaveBeenCalled()
92
+ })
93
+
94
+ it('keeps the parent open when interacting with a portaled nested dialog', async () => {
95
+ const onDismiss = vi.fn()
96
+ render(
97
+ <Modal
98
+ open
99
+ onClose={() => {}}
100
+ onDismiss={onDismiss}
101
+ title="Post details"
102
+ description="Edit post details"
103
+ >
104
+ <p>Body</p>
105
+ </Modal>,
106
+ )
107
+ render(<NestedDialog />)
108
+ await new Promise((resolve) => setTimeout(resolve, 0))
109
+ onDismiss.mockClear()
110
+
111
+ const chooseImage = screen.getByRole('button', { name: 'Choose image' })
112
+ fireEvent.pointerDown(chooseImage)
113
+ fireEvent.focusIn(chooseImage)
114
+
115
+ expect(onDismiss).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it('closes only the portaled nested dialog on Escape', async () => {
119
+ const onDismiss = vi.fn()
120
+ const onNestedOpenChange = vi.fn()
121
+ render(
122
+ <Modal
123
+ open
124
+ onClose={() => {}}
125
+ onDismiss={onDismiss}
126
+ title="Post details"
127
+ description="Edit post details"
128
+ >
129
+ <p>Body</p>
130
+ </Modal>,
131
+ )
132
+ render(<NestedDialog onOpenChange={onNestedOpenChange} />)
133
+ await new Promise((resolve) => setTimeout(resolve, 0))
134
+ onDismiss.mockClear()
135
+ onNestedOpenChange.mockClear()
136
+
137
+ fireEvent.keyDown(screen.getByRole('dialog', { name: 'Media picker' }), { key: 'Escape' })
138
+
139
+ expect(onNestedOpenChange).toHaveBeenCalledWith(false)
140
+ expect(screen.queryByRole('dialog', { name: 'Media picker' })).toBeNull()
141
+ expect(onDismiss).not.toHaveBeenCalled()
142
+ })
143
+ })
144
+
145
+ describe('isNestedDialogEvent (UpChat #27)', () => {
146
+ it('detects events that belong to a different dialog than the Modal content', () => {
147
+ const parent = document.createElement('div')
148
+ parent.setAttribute('role', 'dialog')
149
+ const nested = document.createElement('div')
150
+ nested.setAttribute('role', 'dialog')
151
+ const button = document.createElement('button')
152
+ nested.appendChild(button)
153
+ document.body.append(parent, nested)
154
+
155
+ // MediaPicker / ConfirmDialog portals sit outside this Modal's content —
156
+ // their clicks must not count as "interact outside" for dismiss.
157
+ expect(isNestedDialogEvent(button, parent)).toBe(true)
158
+ expect(isNestedDialogEvent(button, nested)).toBe(false)
159
+ expect(isNestedDialogEvent(parent, parent)).toBe(false)
160
+ expect(isNestedDialogEvent(null, parent)).toBe(false)
161
+
162
+ parent.remove()
163
+ nested.remove()
164
+ })
57
165
  })
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as Dialog from '@radix-ui/react-dialog'
4
4
  import { X } from 'lucide-react'
5
- import { type ReactNode, type RefObject } from 'react'
5
+ import { type ReactNode, type RefObject, useRef } from 'react'
6
6
  import { adminPortalContainer } from '../../lib/portal-container.js'
7
7
 
8
8
  const MODAL_SIZE_CLASS = {
@@ -41,6 +41,22 @@ export interface ModalProps {
41
41
  onDismiss?: () => void
42
42
  }
43
43
 
44
+ /**
45
+ * True when the event originated inside a *different* `[role="dialog"]` than
46
+ * `content` (e.g. MediaPickerModal / ConfirmDialog portaled above this Modal).
47
+ * Nested dialogs must own their own dismiss; treating their clicks as "outside"
48
+ * would discard the parent form before the nested selection can apply
49
+ * (UpChat #27 / featured-image picker).
50
+ */
51
+ export function isNestedDialogEvent(
52
+ target: EventTarget | null,
53
+ content: EventTarget | null,
54
+ ): boolean {
55
+ if (!(target instanceof Element) || !(content instanceof Element)) return false
56
+ const owningDialog = target.closest('[role="dialog"]')
57
+ return Boolean(owningDialog && owningDialog !== content)
58
+ }
59
+
44
60
  /**
45
61
  * The canonical admin modal. Built on Radix `Dialog`, which provides the
46
62
  * accessibility contract for free: focus is trapped while open and restored to
@@ -64,6 +80,7 @@ export function Modal({
64
80
  onDismiss,
65
81
  }: ModalProps) {
66
82
  const scrollBody = size === 'xl'
83
+ const contentRef = useRef<HTMLDivElement>(null)
67
84
 
68
85
  function handleDismissAttempt() {
69
86
  if (onDismiss) onDismiss()
@@ -80,8 +97,15 @@ export function Modal({
80
97
  <Dialog.Portal container={adminPortalContainer()}>
81
98
  <Dialog.Overlay className="fixed inset-0 z-60 bg-black/50" />
82
99
  <Dialog.Content
100
+ ref={contentRef}
83
101
  className={`bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none ${MODAL_SIZE_CLASS[size]}`}
84
102
  onInteractOutside={(event) => {
103
+ // Radix dispatches this custom event on the outside target, so
104
+ // `event.currentTarget` is not the parent Dialog.Content.
105
+ if (isNestedDialogEvent(event.target, contentRef.current)) {
106
+ event.preventDefault()
107
+ return
108
+ }
85
109
  if (!onDismiss) return
86
110
  event.preventDefault()
87
111
  handleDismissAttempt()