diff --git a/components/plugins/__tests__/plugin-iframe-slot-props.test.tsx b/components/plugins/__tests__/plugin-iframe-slot-props.test.tsx
index eb57628f3..37cc6919a 100644
--- a/components/plugins/__tests__/plugin-iframe-slot-props.test.tsx
+++ b/components/plugins/__tests__/plugin-iframe-slot-props.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, waitFor } from '@testing-library/react';
+import { act, render, waitFor } from '@testing-library/react';
import type { InstalledPlugin } from '@/lib/plugin-types';
const evaluateShouldShow = vi.fn(async () => true);
@@ -12,7 +12,11 @@ vi.mock('@/lib/plugin-sandbox/registry', () => ({
code: '',
tier: 'untrusted',
background: { evaluateShouldShow },
- slotOffers: [{ name: 'email-banner', hasShouldShow, order: 100 }, { name: 'composer-sidebar-right', hasShouldShow: false, order: 100, fill: true }],
+ slotOffers: [
+ { name: 'email-banner', hasShouldShow, order: 100 },
+ { name: 'composer-sidebar-right', hasShouldShow: false, order: 100, fill: true },
+ { name: 'toolbar-actions', hasShouldShow: false, order: 100 },
+ ],
}),
}));
@@ -87,6 +91,43 @@ describe('PluginIframeSlot', () => {
});
});
+describe('PluginIframeSlot with nothing to show yet', () => {
+ type SlotOptions = { onResize: (height: number, width?: number) => void };
+ const reportSize = (height: number, width?: number) => act(() => {
+ ((createSlotInstance.mock.calls[0] as unknown[])[0] as SlotOptions).onResize(height, width);
+ });
+
+ it.each([
+ ['block', 'email-banner', ['ui:email-banner']],
+ ['inline', 'toolbar-actions', ['ui:toolbar']],
+ ] as const)('keeps a %s frame rendered and out of the Tab order until it reports a height', async (_kind, slot, perms) => {
+ activePlugin = plugin([...perms]);
+ const { container } = render();
+ await waitFor(() => expect(createSlotInstance).toHaveBeenCalled());
+ const wrapper = container.querySelector('[data-plugin-iframe-slot]') as HTMLElement;
+
+ expect(wrapper).toHaveAttribute('inert');
+ // Hidden, an isolated frame stops rendering and can never report the height that would unhide it.
+ expect(wrapper.style.visibility).toBe('');
+
+ reportSize(32, 100);
+ expect(wrapper).not.toHaveAttribute('inert');
+ expect(wrapper.style.height).toBe('32px');
+
+ // Its content went away: nothing to land on again, still rendered.
+ reportSize(0, 100);
+ expect(wrapper).toHaveAttribute('inert');
+ expect(wrapper.style.visibility).toBe('');
+ });
+
+ it('leaves a frame that fills its panel alone: its height never comes from the frame', async () => {
+ activePlugin = plugin(['ui:composer-sidebar']);
+ const { container } = render();
+ await waitFor(() => expect(createSlotInstance).toHaveBeenCalled());
+ expect(container.querySelector('[data-plugin-iframe-slot]')).not.toHaveAttribute('inert');
+ });
+});
+
describe('PluginIframeSlot re-renders', () => {
it('does not ask shouldShow again or push props when a re-render brings equal props', async () => {
activePlugin = plugin(['ui:email-banner', 'email:read']);
diff --git a/components/plugins/plugin-iframe-slot.tsx b/components/plugins/plugin-iframe-slot.tsx
index 761b18d12..49faf6ecb 100644
--- a/components/plugins/plugin-iframe-slot.tsx
+++ b/components/plugins/plugin-iframe-slot.tsx
@@ -190,9 +190,12 @@ export function PluginIframeSlot({ pluginId, slot, extraProps, fill = false, min
0 ? undefined : 'hidden' }
- // A frame with nothing to show must not be a stop for Tab, where the focus would vanish.
- : fill ? { flex: '1 1 0%', minHeight: minFillHeight ?? 0 } : { height, minHeight: height, visibility: height > 0 ? undefined : 'hidden' }}
+ ? { display: 'inline-block', verticalAlign: 'middle', height, width: width ?? 0, maxWidth: '100%', overflow: 'hidden' }
+ : fill ? { flex: '1 1 0%', minHeight: minFillHeight ?? 0 } : { height, minHeight: height }}
+ // A frame with nothing to show must not be a stop for Tab, where the
+ // focus would vanish. Not visibility: hidden, because the browser stops
+ // rendering an isolated frame then, and only its rendering reports a height.
+ inert={!fill && height === 0}
data-plugin-iframe-slot={`${pluginId}:${slot}`}
data-plugin-iframe-fill={fill || undefined}
/>