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. */}
-
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();
};