mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
fix: keep the assistant panel's button in the same place with no message open
This commit is contained in:
@@ -41,6 +41,7 @@ class NoopResizeObserver {
|
||||
vi.stubGlobal('ResizeObserver', NoopResizeObserver);
|
||||
|
||||
const { EmailViewer } = await import('../email-viewer');
|
||||
const { useSettingsStore } = await import('@/stores/settings-store');
|
||||
|
||||
const mailboxes = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
|
||||
@@ -68,8 +69,24 @@ const toolbarItem = () => document.querySelector('[data-overflow-item]') as HTML
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
device.isMobile = false;
|
||||
useSettingsStore.setState({ toolbarPosition: 'top' });
|
||||
});
|
||||
|
||||
const view = (props: { email: Email | null; isLoading: boolean }) => (
|
||||
<EmailViewer mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} {...props} />
|
||||
);
|
||||
/** The panel button: there is exactly one, in a toolbar row at the top of the reader, never in the panel. */
|
||||
const theToggle = () => {
|
||||
const all = document.querySelectorAll('[data-plugin-panel-toggle]');
|
||||
expect(all).toHaveLength(1);
|
||||
const toggle = all[0] as HTMLElement;
|
||||
expect(toggle.closest('[data-viewer-top-toolbar="full-width"]')).toBeTruthy();
|
||||
expect(toggle.closest('[data-plugin-detail-panel]')).toBeNull();
|
||||
const root = document.querySelector('[data-tour="email-viewer"]') ?? toggle.closest('.relative.flex-col');
|
||||
expect(root!.firstElementChild!.contains(toggle)).toBe(true);
|
||||
return toggle;
|
||||
};
|
||||
|
||||
describe('the reader toolbar and the plugin side panel', () => {
|
||||
it('runs the toolbar over the side panel on a desktop', () => {
|
||||
show();
|
||||
@@ -131,6 +148,48 @@ describe('the reader toolbar and the plugin side panel', () => {
|
||||
expect(document.querySelector('[data-plugin-detail-panel]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('puts the panel button in the same toolbar row with no message open, folded or not, and while one loads', () => {
|
||||
const { rerender } = render(view({ email: null, isLoading: false }));
|
||||
const row = () => theToggle().closest('[data-viewer-top-toolbar]') as HTMLElement;
|
||||
expect(row().hasAttribute('data-toolbar-toggle-only')).toBe(true);
|
||||
// As high as the full toolbar.
|
||||
expect(row().firstElementChild!.className).toContain('h-14');
|
||||
expect(theToggle().getAttribute('title')).toBe('plugin_panel_hide');
|
||||
act(() => { fireEvent.click(theToggle()); });
|
||||
// Folded: no strip and no row of the panel's own, the same button in the same row.
|
||||
expect(document.querySelector('[data-plugin-detail-panel]')).toBeNull();
|
||||
expect(document.querySelector('[data-reader-side-panel]')!.children).toHaveLength(0);
|
||||
expect(theToggle().getAttribute('aria-label')).toBe('plugin_panel_show');
|
||||
expect(theToggle().getAttribute('aria-pressed')).toBe('false');
|
||||
act(() => { fireEvent.click(theToggle()); });
|
||||
// Open: the panel holds no button row above the plugin's own header.
|
||||
const panel = document.querySelector('[data-plugin-detail-panel]') as HTMLElement;
|
||||
expect(panel.querySelector('button')).toBeNull();
|
||||
rerender(view({ email: null, isLoading: true }));
|
||||
theToggle();
|
||||
});
|
||||
|
||||
it('keeps the panel button for a scheduled message', () => {
|
||||
const scheduled = { ...email, id: 'e3', isScheduled: true, scheduledSendAt: '2030-01-01T10:00:00Z', scheduledUndoStatus: 'pending' } as unknown as Email;
|
||||
render(view({ email: scheduled, isLoading: false }));
|
||||
theToggle();
|
||||
});
|
||||
|
||||
it('keeps the panel button at the top when the toolbar is below the subject', () => {
|
||||
useSettingsStore.setState({ toolbarPosition: 'below-subject' });
|
||||
render(view({ email, isLoading: false }));
|
||||
expect(theToggle().closest('[data-toolbar-toggle-only]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the side panel frame from a message to none and back', () => {
|
||||
mounts['email-detail-sidebar'] = 0;
|
||||
const { rerender } = render(view({ email, isLoading: false }));
|
||||
rerender(view({ email: null, isLoading: true }));
|
||||
rerender(view({ email: null, isLoading: false }));
|
||||
rerender(view({ email, isLoading: false }));
|
||||
expect(mounts['email-detail-sidebar']).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps the toolbar in the message column on a phone', () => {
|
||||
device.isMobile = true;
|
||||
show();
|
||||
|
||||
@@ -1883,6 +1883,22 @@ export function EmailViewer({
|
||||
setDetailSidebarForcedOpen(false);
|
||||
}
|
||||
};
|
||||
// The one button that shows and hides the plugin side panel, at the end of
|
||||
// the toolbar row in every state of the reader (message, loading, none, scheduled).
|
||||
const panelToggle = hasDetailSidebar && !isMobile ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleDetailSidebar}
|
||||
className={cn("h-8 w-8 shrink-0", mvViewer && "order-10")}
|
||||
title={t(sidebarCollapsed ? 'plugin_panel_show' : 'plugin_panel_hide')}
|
||||
aria-label={t(sidebarCollapsed ? 'plugin_panel_show' : 'plugin_panel_hide')}
|
||||
aria-pressed={!sidebarCollapsed}
|
||||
data-plugin-panel-toggle=""
|
||||
>
|
||||
{sidebarCollapsed ? <PanelRightOpen className="w-4 h-4 text-muted-foreground" /> : <PanelRightClose className="w-4 h-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
) : null;
|
||||
useEffect(() => onPanelRequest((request) => {
|
||||
if (request.slot !== 'email-detail-sidebar') return null;
|
||||
const state = detailPanelRef.current;
|
||||
@@ -3851,31 +3867,29 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [readReceiptResponse, shouldOfferReadReceipt, email?.id, sendReadReceiptNow]);
|
||||
|
||||
// The toolbar row with only the panel button: over the empty or loading
|
||||
// reader, and on top when the user put the toolbar below the subject. Same
|
||||
// height and place as the full toolbar, so the button never moves.
|
||||
const toggleOnlyToolbar = panelToggle && liftToolbar ? (
|
||||
<div
|
||||
data-viewer-top-toolbar="full-width"
|
||||
data-toolbar-toggle-only=""
|
||||
className={cn("bg-background flex-shrink-0", !mvViewer && "border-b border-border")}
|
||||
>
|
||||
<div className={cn("px-2 sm:px-4 lg:px-6 flex items-center justify-end", mvViewer ? "h-12 lg:ps-4" : "h-14")}>
|
||||
{panelToggle}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// The plugin side panel. One element in the same place whatever the viewer
|
||||
// shows (a message, the next one loading, none), so its frame lives on while
|
||||
// the user reads on instead of starting over with every message.
|
||||
const panelToggleInToolbar = !!email;
|
||||
// the user reads on instead of starting over with every message. Its button
|
||||
// is always at the end of the toolbar row above it.
|
||||
const sidePanel = (
|
||||
<>
|
||||
{hasDetailSidebar && !isMobile && (
|
||||
<div className="contents" data-reader-side-panel="">
|
||||
{/* With a message open the toggle sits in its toolbar; without one, here. */}
|
||||
{sidebarCollapsed && !panelToggleInToolbar && (
|
||||
<div className="flex flex-col items-center border-s border-border bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetailSidebarCollapsed(false);
|
||||
if (readerTooNarrow) setDetailSidebarForcedOpen(true);
|
||||
}}
|
||||
className="h-8 w-8 m-1"
|
||||
aria-label={t('plugin_panel_show')}
|
||||
>
|
||||
<PanelRightOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
{!sidebarOverlay && (
|
||||
@@ -3903,22 +3917,6 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
? { width: Math.min(detailSidebarWidth, Math.max(200, viewerWidth - 48)) }
|
||||
: { width: detailSidebarWidth, minWidth: detailSidebarWidth }}
|
||||
>
|
||||
{!panelToggleInToolbar && (
|
||||
<div className="flex items-center justify-end px-1 py-1 border-b border-border shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetailSidebarCollapsed(true);
|
||||
setDetailSidebarForcedOpen(false);
|
||||
}}
|
||||
className="h-7 w-7"
|
||||
aria-label={t('plugin_panel_hide')}
|
||||
>
|
||||
<PanelRightClose className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<PluginSlot name="email-detail-sidebar" extraProps={pluginDetailProps} />
|
||||
</div>
|
||||
@@ -3934,7 +3932,7 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
ref={setViewerRootEl}
|
||||
className={cn("flex-1 flex flex-col h-full bg-background overflow-hidden relative", className)}
|
||||
>
|
||||
{null}
|
||||
{toggleOnlyToolbar}
|
||||
<div className="flex-1 min-h-0 flex flex-row overflow-hidden relative">
|
||||
{content}
|
||||
{sidePanel}
|
||||
@@ -4527,22 +4525,11 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
)}
|
||||
</div>
|
||||
{/* The plugin side panel: shown or hidden for every message alike. */}
|
||||
{hasDetailSidebar && !isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleDetailSidebar}
|
||||
className={cn("h-8 w-8", mvViewer && "order-10")}
|
||||
title={t(sidebarCollapsed ? 'plugin_panel_show' : 'plugin_panel_hide')}
|
||||
aria-label={t(sidebarCollapsed ? 'plugin_panel_show' : 'plugin_panel_hide')}
|
||||
aria-pressed={!sidebarCollapsed}
|
||||
data-plugin-panel-toggle=""
|
||||
>
|
||||
{sidebarCollapsed ? <PanelRightOpen className="w-4 h-4 text-muted-foreground" /> : <PanelRightClose className="w-4 h-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
)}
|
||||
{toolbarPosition === 'top' && panelToggle}
|
||||
</div>
|
||||
)}
|
||||
{/* A scheduled message has no organize actions; the panel button still ends the row. */}
|
||||
{isScheduled && toolbarPosition === 'top' && panelToggle}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4579,7 +4566,7 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
data-tour="email-viewer"
|
||||
className={cn("flex-1 flex flex-col h-full bg-background overflow-hidden relative", className)}
|
||||
>
|
||||
{toolbarPosition === 'top' && liftToolbar && topToolbar}
|
||||
{toolbarPosition === 'top' && liftToolbar ? topToolbar : toggleOnlyToolbar}
|
||||
{/* The message and the side panels, below the toolbar. Overlays anchor here. */}
|
||||
<div className="flex-1 min-h-0 flex flex-row overflow-hidden relative">
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user