From c5eb55467d8c02f8ec18671b020090fd161791fe Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:53:52 +0100 Subject: [PATCH] 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. --- .../__tests__/viewer-quick-reply-back.test.tsx | 13 +++++++++++++ components/email/email-viewer.tsx | 13 +++++++++++-- components/mail/reply-dock.tsx | 5 ++++- components/ui/__tests__/toast-layout.test.tsx | 2 +- components/ui/toast.tsx | 2 +- hooks/use-bottom-controls.ts | 4 ++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/components/email/__tests__/viewer-quick-reply-back.test.tsx b/components/email/__tests__/viewer-quick-reply-back.test.tsx index df22850a8..24daa4e08 100644 --- a/components/email/__tests__/viewer-quick-reply-back.test.tsx +++ b/components/email/__tests__/viewer-quick-reply-back.test.tsx @@ -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(); + 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 () => { diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index dfdc04a8b..cfa9d09b3 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -1827,7 +1827,16 @@ export function EmailViewer({ const [quickReplyText, setQuickReplyText] = useState(""); const quickReplyRef = useRef(null); const [isQuickReplyFocused, setIsQuickReplyFocused] = useState(false); - const quickReplyBoxRef = useRef(null); + const quickReplyBoxRef = useRef(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 && (
+ {showQuickReply && (
{ const pane = ref.current?.parentElement; @@ -78,7 +81,7 @@ export function ReplyDock({ children }: { children: ReactNode }) { )} {/* Folded, the composer stays mounted, with its text and its undo history. */} -
+
{children}
diff --git a/components/ui/__tests__/toast-layout.test.tsx b/components/ui/__tests__/toast-layout.test.tsx index 335d03634..98b19d867 100644 --- a/components/ui/__tests__/toast-layout.test.tsx +++ b/components/ui/__tests__/toast-layout.test.tsx @@ -40,7 +40,7 @@ describe('toast layout', () => { render(); 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', () => { diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index 33ebeab9b..c2c22ca25 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -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)]", diff --git a/hooks/use-bottom-controls.ts b/hooks/use-bottom-controls.ts index 9dcf2f2de..2500d4468 100644 --- a/hooks/use-bottom-controls.ts +++ b/hooks/use-bottom-controls.ts @@ -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(); };