mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
Merge branch 'feat/boot-skeleton'
This commit is contained in:
@@ -400,43 +400,55 @@ function MobileHeaderSkeleton() {
|
||||
* same component): the toolbar, the subject block and the sender block of the
|
||||
* default reader, then the message body's tinted sheet.
|
||||
*/
|
||||
export function MailMessageSkeleton({ className }: { className?: string }) {
|
||||
/**
|
||||
* The reader's top toolbar while a message loads. The loaded reader lifts its
|
||||
* toolbar above the message row on a tablet or desktop (EmailViewer's
|
||||
* `liftToolbar`); its loading state draws this there too, so the row below
|
||||
* doesn't move when the message arrives.
|
||||
*/
|
||||
export function MailMessageToolbarSkeleton() {
|
||||
const button = "h-8 w-20 rounded-md";
|
||||
return (
|
||||
<div aria-hidden="true" className={cn("flex-1 min-w-0 flex flex-col h-full bg-background overflow-hidden", className)}>
|
||||
<div className="bg-background flex-shrink-0 border-b border-border">
|
||||
<div className="px-2 sm:px-4 lg:px-6 flex items-center h-14">
|
||||
{/* Phones: back, then icon-over-label actions. */}
|
||||
<div className="sm:hidden flex items-center justify-between gap-0.5 w-full">
|
||||
<span className="flex items-center justify-center h-9 w-9 -ms-1 flex-shrink-0">
|
||||
<div className="bg-background flex-shrink-0 border-b border-border">
|
||||
<div className="px-2 sm:px-4 lg:px-6 flex items-center h-14">
|
||||
{/* Phones: back, then icon-over-label actions. */}
|
||||
<div className="sm:hidden flex items-center justify-between gap-0.5 w-full">
|
||||
<span className="flex items-center justify-center h-9 w-9 -ms-1 flex-shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</span>
|
||||
{repeat(6, (i) => (
|
||||
<span className="flex flex-col items-center gap-1 px-1 flex-1">
|
||||
<Block className="w-5 h-5 rounded" />
|
||||
<Bar className="h-2" w={i === 5 ? "3.5rem" : "2.25rem"} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="max-sm:hidden flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Back to the list, where the reader has the list's place (tablet, focus). */}
|
||||
<span className="hidden md:max-lg:flex md:boot-focus:flex items-center justify-center h-9 w-9 -ms-1 flex-shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</span>
|
||||
{repeat(6, (i) => (
|
||||
<span className="flex flex-col items-center gap-1 px-1 flex-1">
|
||||
<Block className="w-5 h-5 rounded" />
|
||||
<Bar className="h-2" w={i === 5 ? "3.5rem" : "2.25rem"} />
|
||||
</span>
|
||||
))}
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
</div>
|
||||
<div className="max-sm:hidden flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Back to the list, where the reader has the list's place (tablet, focus). */}
|
||||
<span className="hidden md:max-lg:flex md:boot-focus:flex items-center justify-center h-9 w-9 -ms-1 flex-shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</span>
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
<Block className={cn(button, "max-md:hidden")} />
|
||||
<Block className="h-8 w-8 rounded-md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Block className={button} />
|
||||
<Block className={button} />
|
||||
<Block className={cn(button, "max-md:hidden")} />
|
||||
<Block className="h-8 w-8 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MailMessageSkeleton({ className, toolbar = true }: { className?: string; toolbar?: boolean }) {
|
||||
return (
|
||||
<div aria-hidden="true" className={cn("flex-1 min-w-0 flex flex-col h-full bg-background overflow-hidden", className)}>
|
||||
{toolbar ? <MailMessageToolbarSkeleton /> : null}
|
||||
<div className="bg-background border-b border-border">
|
||||
<div className="px-4 lg:px-6" style={{ paddingBlock: "var(--density-header-py)" }}>
|
||||
<div className="flex items-start justify-between gap-2 lg:gap-4">
|
||||
|
||||
@@ -131,6 +131,33 @@ describe('the reader toolbar and the plugin side panel', () => {
|
||||
expect(document.querySelector('[data-plugin-detail-panel]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('loads a message with its toolbar where the loaded reader puts it', () => {
|
||||
// The loading state lifts the toolbar above the message row too, so the
|
||||
// row stays put when the message arrives: the same element at the same
|
||||
// place (a moved row counts as a layout shift).
|
||||
const view = (props: { email: Email | null; isLoading: boolean }) => (
|
||||
<EmailViewer mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} {...props} />
|
||||
);
|
||||
const { container, rerender } = render(view({ email: null, isLoading: true }));
|
||||
const root = container.firstElementChild as HTMLElement;
|
||||
expect(root.children).toHaveLength(2);
|
||||
const row = root.children[1];
|
||||
expect(row.className).toContain('flex-row');
|
||||
expect(root.children[0].querySelector('.sk-shape')).toBeTruthy();
|
||||
rerender(view({ email, isLoading: false }));
|
||||
expect(root.querySelector('[data-viewer-top-toolbar="full-width"]')).toBe(root.children[0]);
|
||||
expect(root.children[1]).toBe(row);
|
||||
});
|
||||
|
||||
it('loads a message on a phone with the toolbar in the message column', () => {
|
||||
device.isMobile = true;
|
||||
const { container } = render(
|
||||
<EmailViewer email={null} isLoading mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} />,
|
||||
);
|
||||
const root = container.firstElementChild as HTMLElement;
|
||||
expect(root.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps the toolbar in the message column on a phone', () => {
|
||||
device.isMobile = true;
|
||||
show();
|
||||
|
||||
@@ -117,7 +117,7 @@ import {
|
||||
Pencil,
|
||||
Paperclip,
|
||||
} from "@/components/icons";
|
||||
import { MailMessageSkeleton } from "@/components/boot-skeleton/mail-skeleton";
|
||||
import { MailMessageSkeleton, MailMessageToolbarSkeleton } from "@/components/boot-skeleton/mail-skeleton";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
import type { Attachment as PostalMimeAttachment } from 'postal-mime';
|
||||
@@ -3930,12 +3930,12 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const shell = (content: React.ReactNode) => (
|
||||
const shell = (content: React.ReactNode, top: React.ReactNode = null) => (
|
||||
<div
|
||||
ref={setViewerRootEl}
|
||||
className={cn("flex-1 flex flex-col h-full bg-background overflow-hidden relative", className)}
|
||||
>
|
||||
{null}
|
||||
{top}
|
||||
<div className="flex-1 min-h-0 flex flex-row overflow-hidden relative">
|
||||
{content}
|
||||
{sidePanel}
|
||||
@@ -3947,8 +3947,11 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
// Show loading skeleton while email is being fetched: the page skeleton's
|
||||
// message pane, so a linked message keeps the shape it had before the app
|
||||
// started.
|
||||
// Its toolbar sits where the loaded reader puts its own: above the message
|
||||
// row when lifted, so the row doesn't move when the message arrives.
|
||||
if (isLoading && !email) {
|
||||
return shell(<MailMessageSkeleton />);
|
||||
const lifted = toolbarPosition === 'top' && liftToolbar;
|
||||
return shell(<MailMessageSkeleton toolbar={!lifted} />, lifted ? <MailMessageToolbarSkeleton /> : null);
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
|
||||
Reference in New Issue
Block a user