This commit is contained in:
xinsngx 2026-05-31 01:23:31 -04:00 committed by GitHub
commit 41cf71593f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 132 additions and 4 deletions

View file

@ -184,6 +184,40 @@ export function PreviewDrawOverlay({
);
}
function canTryDirectFrameScroll(iframe: HTMLIFrameElement): boolean {
const sandbox = iframe.getAttribute('sandbox');
return sandbox === null || /\ballow-same-origin\b/.test(sandbox);
}
function postFrameScrollBy(win: Window, left: number, top: number): boolean {
try {
win.postMessage({ type: 'od:preview-scroll-by', left, top }, '*');
return true;
} catch {
return false;
}
}
function scrollPreviewIframeBy(iframe: HTMLIFrameElement, left: number, top: number): boolean {
const win = iframe.contentWindow;
if (!win) return false;
if (canTryDirectFrameScroll(iframe)) {
try {
const scrollBy = win.scrollBy;
if (typeof scrollBy === 'function') {
win.scrollBy({ left, top, behavior: 'auto' });
return true;
}
} catch {
// Sandboxed / cross-origin frames throw on Window property reads.
// Fall through to the postMessage bridge injected into srcDoc previews.
}
}
return postFrameScrollBy(win, left, top);
}
function onPointerDown(e: PointerEvent) {
if (!active || sending) return;
(e.target as Element).setPointerCapture?.(e.pointerId);
@ -233,10 +267,10 @@ export function PreviewDrawOverlay({
function onCanvasWheel(e: WheelEvent<HTMLCanvasElement>) {
if (!active || sending) return;
const iframe = activePreviewIframe();
const win = iframe?.contentWindow;
if (!win || typeof win.scrollBy !== 'function') return;
e.preventDefault();
win.scrollBy({ left: e.deltaX, top: e.deltaY, behavior: 'auto' });
if (!iframe) return;
if (scrollPreviewIframeBy(iframe, e.deltaX, e.deltaY)) {
e.preventDefault();
}
}
function clearInk() {

View file

@ -1125,6 +1125,29 @@ function meaningfulDomFallbackTarget(el) {
function previewScrollElement(){
return document.querySelector('.design-canvas') || document.scrollingElement || document.documentElement;
}
function previewScrollBy(left, top){
var dx = Number(left || 0);
var dy = Number(top || 0);
if (!Number.isFinite(dx)) dx = 0;
if (!Number.isFinite(dy)) dy = 0;
if (!dx && !dy) return;
var el = previewScrollElement();
if (!el) return;
try {
if (typeof el.scrollBy === 'function') el.scrollBy({ left: dx, top: dy, behavior: 'auto' });
else {
el.scrollLeft = (el.scrollLeft || 0) + dx;
el.scrollTop = (el.scrollTop || 0) + dy;
}
} catch (_) {
try {
el.scrollLeft = (el.scrollLeft || 0) + dx;
el.scrollTop = (el.scrollTop || 0) + dy;
} catch (__) {}
}
schedulePostTargets();
schedulePostPreviewScroll();
}
function postPreviewScroll(){
var el = previewScrollElement();
if (!el) return;
@ -1322,6 +1345,11 @@ function meaningfulDomFallbackTarget(el) {
schedulePostActiveCommentTarget();
return;
}
if (data.type === 'od:preview-scroll-by') {
previewScrollBy(data.left, data.top);
return;
}
if (data.type === 'od:inspect-mode') {
inspectEnabled = !!data.enabled;
document.documentElement.toggleAttribute('data-od-inspect-mode', inspectEnabled);

View file

@ -158,6 +158,70 @@ describe('PreviewDrawOverlay', () => {
expect(scrollBy).toHaveBeenCalledWith({ left: 12, top: 180, behavior: 'auto' });
});
it('uses the postMessage scroll bridge for sandboxed preview iframes', () => {
const { container } = render(
<PreviewDrawOverlay active>
<iframe title="preview" sandbox="allow-scripts allow-downloads" />
</PreviewDrawOverlay>,
);
const canvas = container.querySelector('canvas');
const iframe = container.querySelector('iframe');
expect(canvas).toBeTruthy();
expect(iframe?.contentWindow).toBeTruthy();
const postMessage = vi.fn();
Object.defineProperty(iframe!.contentWindow!, 'postMessage', {
value: postMessage,
configurable: true,
});
fireEvent.wheel(canvas!, {
deltaX: 8,
deltaY: 96,
});
expect(postMessage).toHaveBeenCalledWith(
{ type: 'od:preview-scroll-by', left: 8, top: 96 },
'*',
);
});
it('falls back to the scroll bridge when direct frame scroll is cross-origin blocked', () => {
const { container } = render(
<PreviewDrawOverlay active>
<iframe title="preview" />
</PreviewDrawOverlay>,
);
const canvas = container.querySelector('canvas');
const iframe = container.querySelector('iframe');
expect(canvas).toBeTruthy();
expect(iframe?.contentWindow).toBeTruthy();
const postMessage = vi.fn();
Object.defineProperty(iframe!.contentWindow!, 'postMessage', {
value: postMessage,
configurable: true,
});
Object.defineProperty(iframe!.contentWindow!, 'scrollBy', {
get() {
throw new DOMException('Blocked a frame from accessing a cross-origin frame.', 'SecurityError');
},
configurable: true,
});
fireEvent.wheel(canvas!, {
deltaX: 4,
deltaY: 72,
});
expect(postMessage).toHaveBeenCalledWith(
{ type: 'od:preview-scroll-by', left: 4, top: 72 },
'*',
);
});
it('closes the draw toolbar from an explicit close button', async () => {
const onActiveChange = vi.fn();
const { getByRole } = render(

View file

@ -119,6 +119,8 @@ describe('buildSrcdoc', () => {
expect(srcdoc).toContain('schedulePostPreviewScroll');
expect(srcdoc).toContain("type: 'od:preview-scroll'");
expect(srcdoc).toContain("type: 'od:preview-scroll-request'");
expect(srcdoc).toContain("data.type === 'od:preview-scroll-by'");
expect(srcdoc).toContain('previewScrollBy(data.left, data.top)');
expect(srcdoc).toContain('data-od-selection-bridge-style');
expect(srcdoc).toContain('html[data-od-comment-mode] body iframe');
expect(srcdoc).toContain('html[data-od-inspect-mode] body iframe');