@prenta/admin 0.97.3 → 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 +20 -0
- package/dist/__tests__/components/modal.render.test.js +62 -2
- package/dist/__tests__/components/modal.render.test.js.map +1 -1
- package/dist/__tests__/views/canvas-surface-shared.test.js +16 -0
- package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +8 -0
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +22 -1
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.js +31 -4
- package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/components/modal.render.test.tsx +109 -1
- package/src/__tests__/views/canvas-surface-shared.test.ts +19 -0
- package/src/components/ui/Modal.tsx +25 -1
- package/src/views/page-editor/SitePreviewFrame.tsx +35 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.97.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d165fe1: Site preview measures from a sentinel, not from padding arithmetic
|
|
14
|
+
|
|
15
|
+
`SitePreviewFrame` was the third copy of `container.clientWidth - 48` and the one left alone when the page and post canvases were unified, because it scales an **iframe** rather than a rendered surface and cannot render `CanvasSurface`.
|
|
16
|
+
|
|
17
|
+
Sharing the component was never the point; not writing `p-6` down a second time, in a second unit, four hundred lines from the class it mirrors, was. The 48 disagreed with reality the moment a scrollbar appeared and would have rotted silently if the gutter ever changed — nothing failing, the preview just scaling slightly wrong.
|
|
18
|
+
|
|
19
|
+
It now measures from a zero-height sentinel rendered as the first child of the padded container, subject to the same padding, scrollbar and host scaling as the frame, so its `offsetWidth` IS the usable width. Height still comes from the container — a zero-height sentinel can say nothing about it — but the vertical gutter is derived from the measured horizontal one rather than hardcoded again.
|
|
20
|
+
|
|
21
|
+
The source-level test that pins the other two canvases gained a third case, kept separate because the assertion is about the measurement and not about rendering the shared surface.
|
|
22
|
+
|
|
3
23
|
## 0.97.3
|
|
4
24
|
|
|
5
25
|
### 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;
|
|
1
|
+
{"version":3,"file":"modal.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/components/modal.render.test.tsx"],"names":[],"mappings":"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"}
|
|
@@ -49,5 +49,21 @@ describe('canvas surface is shared, not copied', () => {
|
|
|
49
49
|
expect(read(rel), `${rel} re-implements the scale`).not.toMatch(/fitScale/);
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
+
/**
|
|
53
|
+
* `SitePreviewFrame` was the third copy of `clientWidth - 48` and the one
|
|
54
|
+
* followup 2 left alone, because it scales an IFRAME rather than a rendered
|
|
55
|
+
* surface and cannot render `CanvasSurface`. It can still measure the same
|
|
56
|
+
* way, and now does: the padding arithmetic is gone even though the
|
|
57
|
+
* component is not shared.
|
|
58
|
+
*
|
|
59
|
+
* Kept apart from CANVASES on purpose — the assertion here is about the
|
|
60
|
+
* measurement, not about rendering the shared surface, and folding it in
|
|
61
|
+
* would make the other two tests claim something untrue about it.
|
|
62
|
+
*/
|
|
63
|
+
it('the site preview measures from a sentinel too, though it cannot share the surface', () => {
|
|
64
|
+
const preview = read('src/views/page-editor/SitePreviewFrame.tsx');
|
|
65
|
+
expect(preview, 'SitePreviewFrame still does padding arithmetic').not.toMatch(/clientWidth\s*-\s*\d/);
|
|
66
|
+
expect(preview, 'SitePreviewFrame does not measure from a sentinel').toMatch(/sentinelRef/);
|
|
67
|
+
});
|
|
52
68
|
});
|
|
53
69
|
//# sourceMappingURL=canvas-surface-shared.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-surface-shared.test.js","sourceRoot":"","sources":["../../../src/__tests__/views/canvas-surface-shared.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C;;;;;;;;;;;;;GAaG;AAEH,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAEhE;;;;;GAKG;AACH,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE,CAC3B,YAAY,CAAC,GAAG,GAAG,GAAG,EAAE,MAAM,CAAC;KAC5B,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;KAChC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AAEjC,MAAM,QAAQ,GAAG;IACf,wCAAwC;IACxC,4CAA4C;CAC7C,CAAA;AAED,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,gCAAgC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QACtF,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,6BAA6B,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;QAClF,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,yCAAyC,CAAC,CAAA;QAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QACtC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAEnC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,6BAA6B,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;YACjF,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,0BAA0B,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"canvas-surface-shared.test.js","sourceRoot":"","sources":["../../../src/__tests__/views/canvas-surface-shared.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C;;;;;;;;;;;;;GAaG;AAEH,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAEhE;;;;;GAKG;AACH,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE,CAC3B,YAAY,CAAC,GAAG,GAAG,GAAG,EAAE,MAAM,CAAC;KAC5B,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;KAChC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AAEjC,MAAM,QAAQ,GAAG;IACf,wCAAwC;IACxC,4CAA4C;CAC7C,CAAA;AAED,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,gCAAgC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QACtF,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,6BAA6B,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;QAClF,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,yCAAyC,CAAC,CAAA;QAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QACtC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAEnC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,6BAA6B,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;YACjF,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,0BAA0B,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC,CAAC,CAAA;IAEF;;;;;;;;;;OAUG;IACH,EAAE,CAAC,mFAAmF,EAAE,GAAG,EAAE;QAC3F,MAAM,OAAO,GAAG,IAAI,CAAC,4CAA4C,CAAC,CAAA;QAClE,MAAM,CAAC,OAAO,EAAE,gDAAgD,CAAC,CAAC,GAAG,CAAC,OAAO,CAC3E,sBAAsB,CACvB,CAAA;QACD,MAAM,CAAC,OAAO,EAAE,mDAAmD,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IAC7F,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,
|
|
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;
|
|
1
|
+
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../../src/components/ui/Modal.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SitePreviewFrame.d.ts","sourceRoot":"","sources":["../../../src/views/page-editor/SitePreviewFrame.tsx"],"names":[],"mappings":"AAKA,OAAO,EAUL,KAAK,qBAAqB,EAE3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,qBAAqB;IAC7B,uEAAuE;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,QAAQ,EAAE,UAAU,CAAA;IACpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,qBAAqB,EAAE,CAAA;IAClC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxE,kDAAkD;IAClD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,sEAAsE;IACtE,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAA;CACpE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,QAAQ,EACR,SAAS,EACT,KAAsB,EACtB,UAAiB,EACjB,QAAQ,EACR,oBAAwB,EACxB,eAAe,EACf,YAAY,EACZ,oBAAoB,GACrB,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"SitePreviewFrame.d.ts","sourceRoot":"","sources":["../../../src/views/page-editor/SitePreviewFrame.tsx"],"names":[],"mappings":"AAKA,OAAO,EAUL,KAAK,qBAAqB,EAE3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,qBAAqB;IAC7B,uEAAuE;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,QAAQ,EAAE,UAAU,CAAA;IACpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,qBAAqB,EAAE,CAAA;IAClC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxE,kDAAkD;IAClD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,sEAAsE;IACtE,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAA;CACpE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,QAAQ,EACR,SAAS,EACT,KAAsB,EACtB,UAAiB,EACjB,QAAQ,EACR,oBAAwB,EACxB,eAAe,EACf,YAAY,EACZ,oBAAoB,GACrB,EAAE,qBAAqB,2CAiLvB"}
|
|
@@ -14,6 +14,7 @@ import { getViewport } from './viewports.js';
|
|
|
14
14
|
export function SitePreviewFrame({ src, viewport, reloadKey, title = 'Site preview', selectedId = null, sections, suppressSectionsEcho = 0, onSelectSection, onCanvasEdit, onCanvasRequestMedia, }) {
|
|
15
15
|
const vp = getViewport(viewport);
|
|
16
16
|
const containerRef = useRef(null);
|
|
17
|
+
const sentinelRef = useRef(null);
|
|
17
18
|
const iframeRef = useRef(null);
|
|
18
19
|
const [scale, setScale] = useState(1);
|
|
19
20
|
const [frameHeight, setFrameHeight] = useState(600);
|
|
@@ -21,13 +22,38 @@ export function SitePreviewFrame({ src, viewport, reloadKey, title = 'Site previ
|
|
|
21
22
|
const [canvasReady, setCanvasReady] = useState(false);
|
|
22
23
|
const canvasOrigin = canvasOriginFromSrc(src);
|
|
23
24
|
const lastSuppressRef = useRef(suppressSectionsEcho);
|
|
25
|
+
/**
|
|
26
|
+
* Measure from a zero-height sentinel, never `clientWidth - 48`.
|
|
27
|
+
*
|
|
28
|
+
* The 48 was `p-6` written down a second time, in a second unit, four
|
|
29
|
+
* hundred lines from the class it mirrors. It disagrees with reality the
|
|
30
|
+
* moment a scrollbar appears, and it rots silently if the gutter ever
|
|
31
|
+
* changes — nothing fails, the preview just scales slightly wrong.
|
|
32
|
+
*
|
|
33
|
+
* The sentinel is subject to the same padding, the same scrollbar and the
|
|
34
|
+
* same host scaling as the frame, so its `offsetWidth` IS the usable width
|
|
35
|
+
* and there is no arithmetic to get wrong. Same rule as `CanvasSurface`,
|
|
36
|
+
* which is where this was first learned; this file is the third copy of the
|
|
37
|
+
* arithmetic and the last one to go.
|
|
38
|
+
*
|
|
39
|
+
* Height still comes from the container, because the sentinel is
|
|
40
|
+
* deliberately zero-height and can say nothing about it — but it is read as
|
|
41
|
+
* `clientHeight - (clientWidth - sentinelWidth)`, deriving the vertical
|
|
42
|
+
* gutter from the measured horizontal one rather than hardcoding it again.
|
|
43
|
+
* That holds for the symmetric `p-6` this uses and degrades to the old
|
|
44
|
+
* behaviour rather than breaking if it ever stops being symmetric.
|
|
45
|
+
*/
|
|
24
46
|
useLayoutEffect(() => {
|
|
25
47
|
const container = containerRef.current;
|
|
26
|
-
|
|
48
|
+
const sentinel = sentinelRef.current;
|
|
49
|
+
if (!container || !sentinel)
|
|
27
50
|
return;
|
|
28
51
|
const recompute = () => {
|
|
29
|
-
const availableW =
|
|
30
|
-
|
|
52
|
+
const availableW = sentinel.offsetWidth;
|
|
53
|
+
if (availableW <= 0)
|
|
54
|
+
return;
|
|
55
|
+
const gutter = Math.max(0, container.clientWidth - availableW);
|
|
56
|
+
const availableH = container.clientHeight - gutter;
|
|
31
57
|
const next = Math.min(1, availableW / vp.width);
|
|
32
58
|
setScale(next > 0 ? next : 1);
|
|
33
59
|
setFrameHeight(availableH > 0 ? availableH : 600);
|
|
@@ -35,6 +61,7 @@ export function SitePreviewFrame({ src, viewport, reloadKey, title = 'Site previ
|
|
|
35
61
|
recompute();
|
|
36
62
|
const ro = new ResizeObserver(recompute);
|
|
37
63
|
ro.observe(container);
|
|
64
|
+
ro.observe(sentinel);
|
|
38
65
|
return () => ro.disconnect();
|
|
39
66
|
}, [vp.width]);
|
|
40
67
|
useEffect(() => {
|
|
@@ -128,7 +155,7 @@ export function SitePreviewFrame({ src, viewport, reloadKey, title = 'Site previ
|
|
|
128
155
|
}, PREVIEW_SECTIONS_DEBOUNCE_MS);
|
|
129
156
|
return () => window.clearTimeout(timer);
|
|
130
157
|
}, [canvasOrigin, loaded, canvasReady, sections, reloadKey, suppressSectionsEcho]);
|
|
131
|
-
return (_jsxs("div", { ref: containerRef, className: "bg-muted relative h-full overflow-hidden p-6", children: [(!src || !loaded) && (_jsxs("div", { className: "absolute inset-0 z-10 flex flex-col items-center justify-center gap-2", "aria-live": "polite", children: [_jsx(Loader2, { className: "text-muted-foreground h-6 w-6 animate-spin", "aria-hidden": true }), _jsx("span", { className: "text-muted-foreground text-sm", children: "Loading site preview\u2026" })] })), src && (_jsx("div", { className: "mx-auto", style: { width: vp.width * scale, height: frameHeight }, children: _jsx("iframe", { ref: iframeRef, src: src, title: title, onLoad: () => setLoaded(true), className: "border-border bg-card rounded-xl border shadow-sm", style: {
|
|
158
|
+
return (_jsxs("div", { ref: containerRef, className: "bg-muted relative h-full overflow-hidden p-6", children: [_jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "h-0" }), (!src || !loaded) && (_jsxs("div", { className: "absolute inset-0 z-10 flex flex-col items-center justify-center gap-2", "aria-live": "polite", children: [_jsx(Loader2, { className: "text-muted-foreground h-6 w-6 animate-spin", "aria-hidden": true }), _jsx("span", { className: "text-muted-foreground text-sm", children: "Loading site preview\u2026" })] })), src && (_jsx("div", { className: "mx-auto", style: { width: vp.width * scale, height: frameHeight }, children: _jsx("iframe", { ref: iframeRef, src: src, title: title, onLoad: () => setLoaded(true), className: "border-border bg-card rounded-xl border shadow-sm", style: {
|
|
132
159
|
width: vp.width,
|
|
133
160
|
height: frameHeight / scale,
|
|
134
161
|
transform: `scale(${scale})`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SitePreviewFrame.js","sourceRoot":"","sources":["../../../src/views/page-editor/SitePreviewFrame.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAA;AACzE,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,4BAA4B,EAC5B,uBAAuB,GAKxB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAE,WAAW,EAAmB,MAAM,gBAAgB,CAAA;AAqC7D;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,GAAG,EACH,QAAQ,EACR,SAAS,EACT,KAAK,GAAG,cAAc,EACtB,UAAU,GAAG,IAAI,EACjB,QAAQ,EACR,oBAAoB,GAAG,CAAC,EACxB,eAAe,EACf,YAAY,EACZ,oBAAoB,GACE;IACtB,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,YAAY,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACjD,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACrC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IACnD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACrD,MAAM,YAAY,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAA;IAC7C,MAAM,eAAe,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAA;IAEpD,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAA;QACtC,IAAI,CAAC,SAAS;YAAE,OAAM;
|
|
1
|
+
{"version":3,"file":"SitePreviewFrame.js","sourceRoot":"","sources":["../../../src/views/page-editor/SitePreviewFrame.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.
|
|
3
|
+
"version": "0.97.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -99,10 +99,10 @@
|
|
|
99
99
|
"typescript": "^5.7.0",
|
|
100
100
|
"vitest": "^4.1.0",
|
|
101
101
|
"@prenta/component-blocks": "0.3.0",
|
|
102
|
-
"@prenta/core": "0.123.0",
|
|
103
102
|
"@prenta/plugin-ai": "0.6.0",
|
|
104
103
|
"@prenta/plugin-blocks": "0.2.0",
|
|
105
104
|
"@prenta/plugin-commerce": "0.2.0",
|
|
105
|
+
"@prenta/core": "0.123.0",
|
|
106
106
|
"@prenta/plugin-forms": "0.1.0",
|
|
107
107
|
"@prenta/plugin-media": "0.2.0",
|
|
108
108
|
"@prenta/plugin-navigation": "0.3.0",
|
|
@@ -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
|
})
|
|
@@ -58,4 +58,23 @@ describe('canvas surface is shared, not copied', () => {
|
|
|
58
58
|
expect(read(rel), `${rel} re-implements the scale`).not.toMatch(/fitScale/)
|
|
59
59
|
}
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `SitePreviewFrame` was the third copy of `clientWidth - 48` and the one
|
|
64
|
+
* followup 2 left alone, because it scales an IFRAME rather than a rendered
|
|
65
|
+
* surface and cannot render `CanvasSurface`. It can still measure the same
|
|
66
|
+
* way, and now does: the padding arithmetic is gone even though the
|
|
67
|
+
* component is not shared.
|
|
68
|
+
*
|
|
69
|
+
* Kept apart from CANVASES on purpose — the assertion here is about the
|
|
70
|
+
* measurement, not about rendering the shared surface, and folding it in
|
|
71
|
+
* would make the other two tests claim something untrue about it.
|
|
72
|
+
*/
|
|
73
|
+
it('the site preview measures from a sentinel too, though it cannot share the surface', () => {
|
|
74
|
+
const preview = read('src/views/page-editor/SitePreviewFrame.tsx')
|
|
75
|
+
expect(preview, 'SitePreviewFrame still does padding arithmetic').not.toMatch(
|
|
76
|
+
/clientWidth\s*-\s*\d/,
|
|
77
|
+
)
|
|
78
|
+
expect(preview, 'SitePreviewFrame does not measure from a sentinel').toMatch(/sentinelRef/)
|
|
79
|
+
})
|
|
61
80
|
})
|
|
@@ -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()
|
|
@@ -73,6 +73,7 @@ export function SitePreviewFrame({
|
|
|
73
73
|
}: SitePreviewFrameProps) {
|
|
74
74
|
const vp = getViewport(viewport)
|
|
75
75
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
76
|
+
const sentinelRef = useRef<HTMLDivElement>(null)
|
|
76
77
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
77
78
|
const [scale, setScale] = useState(1)
|
|
78
79
|
const [frameHeight, setFrameHeight] = useState(600)
|
|
@@ -81,12 +82,36 @@ export function SitePreviewFrame({
|
|
|
81
82
|
const canvasOrigin = canvasOriginFromSrc(src)
|
|
82
83
|
const lastSuppressRef = useRef(suppressSectionsEcho)
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Measure from a zero-height sentinel, never `clientWidth - 48`.
|
|
87
|
+
*
|
|
88
|
+
* The 48 was `p-6` written down a second time, in a second unit, four
|
|
89
|
+
* hundred lines from the class it mirrors. It disagrees with reality the
|
|
90
|
+
* moment a scrollbar appears, and it rots silently if the gutter ever
|
|
91
|
+
* changes — nothing fails, the preview just scales slightly wrong.
|
|
92
|
+
*
|
|
93
|
+
* The sentinel is subject to the same padding, the same scrollbar and the
|
|
94
|
+
* same host scaling as the frame, so its `offsetWidth` IS the usable width
|
|
95
|
+
* and there is no arithmetic to get wrong. Same rule as `CanvasSurface`,
|
|
96
|
+
* which is where this was first learned; this file is the third copy of the
|
|
97
|
+
* arithmetic and the last one to go.
|
|
98
|
+
*
|
|
99
|
+
* Height still comes from the container, because the sentinel is
|
|
100
|
+
* deliberately zero-height and can say nothing about it — but it is read as
|
|
101
|
+
* `clientHeight - (clientWidth - sentinelWidth)`, deriving the vertical
|
|
102
|
+
* gutter from the measured horizontal one rather than hardcoding it again.
|
|
103
|
+
* That holds for the symmetric `p-6` this uses and degrades to the old
|
|
104
|
+
* behaviour rather than breaking if it ever stops being symmetric.
|
|
105
|
+
*/
|
|
84
106
|
useLayoutEffect(() => {
|
|
85
107
|
const container = containerRef.current
|
|
86
|
-
|
|
108
|
+
const sentinel = sentinelRef.current
|
|
109
|
+
if (!container || !sentinel) return
|
|
87
110
|
const recompute = () => {
|
|
88
|
-
const availableW =
|
|
89
|
-
|
|
111
|
+
const availableW = sentinel.offsetWidth
|
|
112
|
+
if (availableW <= 0) return
|
|
113
|
+
const gutter = Math.max(0, container.clientWidth - availableW)
|
|
114
|
+
const availableH = container.clientHeight - gutter
|
|
90
115
|
const next = Math.min(1, availableW / vp.width)
|
|
91
116
|
setScale(next > 0 ? next : 1)
|
|
92
117
|
setFrameHeight(availableH > 0 ? availableH : 600)
|
|
@@ -94,6 +119,7 @@ export function SitePreviewFrame({
|
|
|
94
119
|
recompute()
|
|
95
120
|
const ro = new ResizeObserver(recompute)
|
|
96
121
|
ro.observe(container)
|
|
122
|
+
ro.observe(sentinel)
|
|
97
123
|
return () => ro.disconnect()
|
|
98
124
|
}, [vp.width])
|
|
99
125
|
|
|
@@ -186,6 +212,12 @@ export function SitePreviewFrame({
|
|
|
186
212
|
|
|
187
213
|
return (
|
|
188
214
|
<div ref={containerRef} className="bg-muted relative h-full overflow-hidden p-6">
|
|
215
|
+
{/*
|
|
216
|
+
* Zero-height sentinel — see the measurement effect above. Must stay the
|
|
217
|
+
* first child and must stay zero-height. `absolute` would defeat it: it
|
|
218
|
+
* has to sit in the padding box to inherit the gutter it is measuring.
|
|
219
|
+
*/}
|
|
220
|
+
<div ref={sentinelRef} aria-hidden className="h-0" />
|
|
189
221
|
{(!src || !loaded) && (
|
|
190
222
|
<div
|
|
191
223
|
className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2"
|