fix(web): portal plugin details modal (#3065)

Signed-off-by: jaehanbyun <awbrg789@naver.com>
This commit is contained in:
jaehanbyun 2026-05-28 13:03:41 +09:00 committed by GitHub
parent 39ae2cbc57
commit 62972f14a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 17 deletions

View file

@ -19,6 +19,7 @@
// same callback wiring.
import type { InstalledPluginRecord } from '@open-design/contracts';
import { createPortal } from 'react-dom';
import { inferPluginPreview } from './plugins-home/preview';
import { PluginScenarioDetail } from './plugin-details/PluginScenarioDetail';
import { PluginExampleDetail } from './plugin-details/PluginExampleDetail';
@ -39,9 +40,10 @@ export function PluginDetailsModal({
isApplying,
}: Props) {
const preview = inferPluginPreview(record);
let detail: JSX.Element;
if (preview.kind === 'media') {
return (
detail = (
<PluginMediaDetail
record={record}
onClose={onClose}
@ -49,10 +51,8 @@ export function PluginDetailsModal({
isApplying={isApplying}
/>
);
}
if (preview.kind === 'html') {
return (
} else if (preview.kind === 'html') {
detail = (
<PluginExampleDetail
record={record}
exampleStem={
@ -63,10 +63,8 @@ export function PluginDetailsModal({
isApplying={isApplying}
/>
);
}
if (preview.kind === 'design') {
return (
} else if (preview.kind === 'design') {
detail = (
<PluginDesignSystemDetail
record={record}
onClose={onClose}
@ -74,14 +72,17 @@ export function PluginDetailsModal({
isApplying={isApplying}
/>
);
} else {
detail = (
<PluginScenarioDetail
record={record}
onClose={onClose}
onUse={onUse}
isApplying={isApplying}
/>
);
}
return (
<PluginScenarioDetail
record={record}
onClose={onClose}
onUse={onUse}
isApplying={isApplying}
/>
);
if (typeof document === 'undefined') return detail;
return createPortal(detail, document.body);
}

View file

@ -0,0 +1,86 @@
// @vitest-environment jsdom
import { cleanup, render } from '@testing-library/react';
import type { InstalledPluginRecord } from '@open-design/contracts';
import { afterEach, describe, expect, it } from 'vitest';
import { PluginDetailsModal } from '../../src/components/PluginDetailsModal';
import { I18nProvider } from '../../src/i18n';
function makePlugin(
id: string,
preview?: Record<string, unknown>,
): InstalledPluginRecord {
return {
id,
title: id,
version: '0.1.0',
sourceKind: 'bundled',
source: '/tmp',
trust: 'bundled',
capabilitiesGranted: [],
manifest: {
name: id,
version: '0.1.0',
title: id,
od: {
kind: 'scenario',
...(preview ? { preview } : {}),
useCase: {
query: 'Generate a preview.',
},
},
},
fsPath: '/tmp',
installedAt: 0,
updatedAt: 0,
};
}
function renderInsideStackingContext(record: InstalledPluginRecord) {
const host = document.createElement('div');
host.className = 'composer';
document.body.appendChild(host);
render(
<I18nProvider>
<div className="composer-shell">
<PluginDetailsModal record={record} onClose={() => {}} onUse={() => {}} />
</div>
</I18nProvider>,
{ container: host },
);
return host;
}
describe('PluginDetailsModal layering', () => {
afterEach(() => {
cleanup();
document.body.innerHTML = '';
});
it('portals rich preview details to the document body so sticky chat and workspace headers cannot cover them', () => {
const host = renderInsideStackingContext(
makePlugin('video-plugin', {
type: 'video',
poster: 'https://cdn.example/poster.jpg',
video: 'https://cdn.example/clip.mp4',
}),
);
const backdrop = document.body.querySelector('.ds-modal-backdrop');
expect(backdrop).toBeTruthy();
expect(backdrop?.parentElement).toBe(document.body);
expect(host.querySelector('.ds-modal-backdrop')).toBeNull();
});
it('portals fallback scenario details through the same top-level layer', () => {
const host = renderInsideStackingContext(makePlugin('scenario-plugin'));
const backdrop = document.body.querySelector('.plugin-details-modal-backdrop');
expect(backdrop).toBeTruthy();
expect(backdrop?.parentElement).toBe(document.body);
expect(host.querySelector('.plugin-details-modal-backdrop')).toBeNull();
});
});