fix: render plugin preview modal via portal to escape stacking context

The plugin preview details modal was rendering under sticky workspace
headers (chat-header and ws-tabs-shell) when opened from the chat
composer's @ plugin picker.

Root cause: The modal was rendered from inside the chat composer
subtree, which has its own stacking context. Fixed-position modal
descendants cannot reliably escape above sibling sticky headers.

Solution: Use React Portal to render the modal directly to document.body,
ensuring it renders above all other page content regardless of parent
stacking context.

Fixes #3064
This commit is contained in:
xxiaoxiong 2026-05-27 23:22:44 +08:00
parent a4ec7808a6
commit 167c8eee31

View file

@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useT } from '../i18n';
import { copyToClipboard } from '../lib/copy-to-clipboard';
import { exportAsHtml, exportAsPdf, exportAsZip, openSandboxedPreviewInNewTab } from '../runtime/exports';
@ -456,7 +457,7 @@ export function PreviewModal({
const showTemplateShareMenu = !isCustomView || Boolean(shareTarget?.url);
const canOpenTemplateShareMenu = canExportFiles || Boolean(previewShareUrl);
return (
const modalContent = (
<div className="ds-modal-backdrop" role="dialog" aria-modal="true" aria-label={`${title} preview`}>
<div className={`ds-modal ${fullscreen ? 'ds-modal-fullscreen' : ''}`}>
<header className="ds-modal-header">
@ -864,4 +865,6 @@ export function PreviewModal({
</div>
</div>
);
return createPortal(modalContent, document.body);
}