fix: keep the assistant's chat usable in a short side panel it shares with another plugin

This commit is contained in:
Linus Rath
2026-11-03 05:26:04 +01:00
parent 8877686ae4
commit b9dedcdafb
4 changed files with 46 additions and 3 deletions
@@ -52,6 +52,13 @@ describe('PluginIframeSlot', () => {
expect(wrapper.style.flex).toBe('1 1 0%');
});
it('keeps a filling frame at its minimum height when it is given one', async () => {
activePlugin = plugin(['ui:composer-sidebar']);
const { container } = render(<PluginIframeSlot pluginId="p1" slot="composer-sidebar-right" extraProps={{}} fill minFillHeight={280} />);
await waitFor(() => expect(createSlotInstance).toHaveBeenCalled());
expect((container.querySelector('[data-plugin-iframe-slot]') as HTMLElement).style.minHeight).toBe('280px');
});
it('mounts a banner-only plugin without the opened message', async () => {
activePlugin = plugin(['ui:email-banner']);
render(<PluginIframeSlot pluginId="p1" slot="email-banner" extraProps={{ email: EMAIL }} />);
@@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import React from 'react';
import { PluginSlot, SHARED_FILL_MIN_HEIGHT } from '../plugin-slot';
let offers: Array<{ pluginId: string; fill?: boolean }> = [];
vi.mock('@/hooks/use-plugin-slot-offers', () => ({ usePluginSlotOffers: () => offers }));
vi.mock('../plugin-iframe-slot', () => ({
PluginIframeSlot: ({ pluginId, minFillHeight }: { pluginId: string; minFillHeight?: number }) => (
<div data-testid={pluginId} data-min={minFillHeight ?? ''} />
),
}));
describe('PluginSlot', () => {
it('keeps a filling frame usable next to another frame in a short panel', () => {
offers = [{ pluginId: 'notes' }, { pluginId: 'chat', fill: true }];
render(<PluginSlot name="email-detail-sidebar" />);
expect(screen.getByTestId('chat').dataset.min).toBe(String(SHARED_FILL_MIN_HEIGHT));
expect(screen.getByTestId('notes').dataset.min).toBe('');
});
it('lets a filling frame alone in its panel take whatever height there is', () => {
offers = [{ pluginId: 'chat', fill: true }];
render(<PluginSlot name="email-detail-sidebar" />);
expect(screen.getByTestId('chat').dataset.min).toBe('');
});
});
+4 -2
View File
@@ -25,6 +25,8 @@ interface Props {
extraProps?: Record<string, unknown>;
/** The frame fills its panel and scrolls inside, instead of growing with its content. */
fill?: boolean;
/** With `fill`: the height the frame keeps however little room its panel has. */
minFillHeight?: number;
/**
* Told whether the frame is up: false when the plugin's shouldShow says no
* or its frame fails to start, true once it has started. For a slot that
@@ -45,7 +47,7 @@ interface FloatingDropdownState {
ariaLabel?: string;
}
export function PluginIframeSlot({ pluginId, slot, extraProps, fill = false, onShownChange }: Props) {
export function PluginIframeSlot({ pluginId, slot, extraProps, fill = false, minFillHeight, onShownChange }: Props) {
const wrapperRef = useRef<HTMLDivElement>(null);
const instanceRef = useRef<SandboxInstance | null>(null);
const onShownChangeRef = useRef(onShownChange);
@@ -189,7 +191,7 @@ export function PluginIframeSlot({ pluginId, slot, extraProps, fill = false, onS
ref={wrapperRef}
style={inline
? { display: 'inline-block', verticalAlign: 'middle', height, width: width ?? 0, maxWidth: '100%', overflow: 'hidden' }
: fill ? { flex: '1 1 0%', minHeight: 0 } : { height, minHeight: height }}
: fill ? { flex: '1 1 0%', minHeight: minFillHeight ?? 0 } : { height, minHeight: height }}
data-plugin-iframe-slot={`${pluginId}:${slot}`}
data-plugin-iframe-fill={fill || undefined}
/>
+8 -1
View File
@@ -13,6 +13,9 @@ interface PluginSlotProps {
extraProps?: Record<string, unknown>;
}
/** Height a filling frame keeps when it shares its panel with other frames. */
export const SHARED_FILL_MIN_HEIGHT = 280;
export function PluginSlot({ name, className, extraProps }: PluginSlotProps) {
// Without a retired plugin's offer while Bulwark does its job (host-features.ts).
const offers = usePluginSlotOffers(name);
@@ -21,8 +24,11 @@ export function PluginSlot({ name, className, extraProps }: PluginSlotProps) {
// Inline slots line up next to each other in a toolbar row. A panel slot
// with a frame that fills the panel takes the panel's height and shares
// it between such frames.
// it between such frames. Next to another frame it keeps a usable height
// however short the panel is: the panel scrolls instead of squeezing it
// away, input and all.
const fills = offers.some((offer) => offer.fill);
const minFillHeight = offers.length > 1 ? SHARED_FILL_MIN_HEIGHT : undefined;
return (
<div
className={isInlineSlot(name)
@@ -37,6 +43,7 @@ export function PluginSlot({ name, className, extraProps }: PluginSlotProps) {
slot={name}
extraProps={extraProps}
fill={offer.fill}
minFillHeight={offer.fill ? minFillHeight : undefined}
/>
))}
</div>