mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: lift toasts above the quick reply box and a docked reply while they are on screen
The toast-layout assertion changes because toasts now clear a bar taller than 5rem while a composer is open, instead of a fixed 5rem.
This commit is contained in:
@@ -63,6 +63,19 @@ describe('quick reply', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// mail-p2-12: toasts covered the right half of the quick reply box.
|
||||
describe('quick reply and toasts', () => {
|
||||
it('counts as a bottom bar, so toasts sit above it', async () => {
|
||||
document.documentElement.style.removeProperty('--bottom-controls-height');
|
||||
const { unmount } = render(<EmailViewer email={message} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onQuickReply={vi.fn()} />);
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 30)); });
|
||||
expect(document.querySelector('[data-quick-reply-box]')).not.toBeNull();
|
||||
expect(document.documentElement.style.getPropertyValue('--bottom-controls-height')).not.toBe('');
|
||||
unmount();
|
||||
expect(document.documentElement.style.getPropertyValue('--bottom-controls-height')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// mail-p2-8: Undo send on a quick reply brings its text back into this box.
|
||||
describe('quick reply after Undo send', () => {
|
||||
it('takes the undone text back into the box of the message it answered', async () => {
|
||||
|
||||
@@ -1827,7 +1827,16 @@ export function EmailViewer({
|
||||
const [quickReplyText, setQuickReplyText] = useState("");
|
||||
const quickReplyRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isQuickReplyFocused, setIsQuickReplyFocused] = useState(false);
|
||||
const quickReplyBoxRef = useRef<HTMLDivElement>(null);
|
||||
const quickReplyBoxRef = useRef<HTMLDivElement | null>(null);
|
||||
// Toasts ("Archived · Undo", "Sending in 5 seconds") sit above the quick
|
||||
// reply box while it is on screen, not over it (mail-p2-12).
|
||||
const [quickReplyBoxShown, setQuickReplyBoxShown] = useState(false);
|
||||
const quickReplyBarRef = useBottomControls(quickReplyBoxShown);
|
||||
const setQuickReplyBox = useCallback((el: HTMLDivElement | null) => {
|
||||
quickReplyBoxRef.current = el;
|
||||
setQuickReplyBoxShown(!!el);
|
||||
quickReplyBarRef(el);
|
||||
}, [quickReplyBarRef]);
|
||||
// Focused, the box grows by its buttons: bring them up, they'd sit below the fold.
|
||||
useEffect(() => {
|
||||
if (isQuickReplyFocused) quickReplyBoxRef.current?.scrollIntoView?.({ block: 'nearest' });
|
||||
@@ -6393,7 +6402,7 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
)}
|
||||
|
||||
{/* Quick Reply Section - hidden for drafts and while loading a new email */}
|
||||
{showQuickReply && (<div ref={quickReplyBoxRef} className="bg-background border-t border-border px-6 mt-auto max-sm:scroll-mb-16" style={{ paddingBlock: 'var(--density-header-py)' }}>
|
||||
{showQuickReply && (<div ref={setQuickReplyBox} data-quick-reply-box="" className="bg-background border-t border-border px-6 mt-auto max-sm:scroll-mb-16" style={{ paddingBlock: 'var(--density-header-py)' }}>
|
||||
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
|
||||
<div className="flex-shrink-0">
|
||||
<Avatar
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronDown, ChevronUp } from "@/components/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useBottomControls } from "@/hooks/use-bottom-controls";
|
||||
|
||||
/**
|
||||
* Below this reading-pane height a docked reply can't share the pane with
|
||||
@@ -34,6 +35,8 @@ export function ReplyDock({ children }: { children: ReactNode }) {
|
||||
// The reader's toolbar and subject above its scrolling part: they stay.
|
||||
const [readerChrome, setReaderChrome] = useState(0);
|
||||
const [showMessage, setShowMessage] = useState(false);
|
||||
// Toasts sit above the reply and its Send button (mail-p2-12).
|
||||
const barRef = useBottomControls();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pane = ref.current?.parentElement;
|
||||
@@ -78,7 +81,7 @@ export function ReplyDock({ children }: { children: ReactNode }) {
|
||||
</button>
|
||||
)}
|
||||
{/* Folded, the composer stays mounted, with its text and its undo history. */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col", folded && "hidden")}>
|
||||
<div ref={barRef} className={cn("flex min-h-0 flex-1 flex-col", folded && "hidden")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('toast layout', () => {
|
||||
render(<Toasts />);
|
||||
const container = toastContainer();
|
||||
expect(container.className).toContain('max-sm:bottom-[calc(var(--bottom-controls-height,3.75rem)+0.75rem)]');
|
||||
expect(container).toHaveClass('[html[data-composer-open]_&]:bottom-20');
|
||||
expect(container).toHaveClass('[html[data-composer-open]_&]:bottom-[max(5rem,calc(var(--bottom-controls-height,0px)+0.75rem))]');
|
||||
});
|
||||
|
||||
it('sits above a registered bottom bar on the desktop too, such as a form footer', () => {
|
||||
|
||||
@@ -474,7 +474,7 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose:
|
||||
// Newest at the bottom, at the end corner (bottom left in RTL);
|
||||
// lifted over a registered bottom bar (a form's Save/Cancel footer)
|
||||
// and over an open composer's footer.
|
||||
"fixed bottom-[max(1.25rem,calc(var(--bottom-controls-height,0px)+0.75rem))] end-5 z-[99999] flex flex-col gap-2.5 [html[data-composer-open]_&]:bottom-20",
|
||||
"fixed bottom-[max(1.25rem,calc(var(--bottom-controls-height,0px)+0.75rem))] end-5 z-[99999] flex flex-col gap-2.5 [html[data-composer-open]_&]:bottom-[max(5rem,calc(var(--bottom-controls-height,0px)+0.75rem))]",
|
||||
// Phone: full width with 12px margins, above the bottom controls
|
||||
// (the tab bar, or whichever bar registered with useBottomControls).
|
||||
"max-sm:inset-x-3 max-sm:bottom-[calc(var(--bottom-controls-height,3.75rem)+0.75rem)]",
|
||||
|
||||
@@ -48,9 +48,13 @@ export function useBottomControls(active = true): (el: HTMLElement | null) => vo
|
||||
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
||||
observer?.observe(el);
|
||||
window.addEventListener('resize', schedule);
|
||||
// A bar inside a scrolling pane (the quick reply under a message) moves
|
||||
// when the pane scrolls.
|
||||
window.addEventListener('scroll', schedule, { capture: true, passive: true });
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
window.removeEventListener('resize', schedule);
|
||||
window.removeEventListener('scroll', schedule, { capture: true });
|
||||
bars.delete(el);
|
||||
update();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user