diff --git a/components/ui/__tests__/toast-layout.test.tsx b/components/ui/__tests__/toast-layout.test.tsx
index a421af676..335d03634 100644
--- a/components/ui/__tests__/toast-layout.test.tsx
+++ b/components/ui/__tests__/toast-layout.test.tsx
@@ -208,6 +208,25 @@ describe('toasts and the focused element', () => {
expect(toastContainer()).not.toHaveAttribute('data-dodging');
});
+ // With the focus in the composer's text or the quick reply box, "Email
+ // sent" and "Sending in 5 seconds" moved to the top and covered the
+ // composer's close button and the reader's toolbar. A large field the
+ // stack only overlaps at a corner keeps the toasts at the bottom.
+ it('stays at the bottom over a corner of a large focused field', () => {
+ const h = window.innerHeight;
+ vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
+ if (this.hasAttribute('data-toast-container')) return rect(h - 100, 600, 360, 80);
+ if (this.id === 'body') return rect(h - 400, 300, 700, 360);
+ return rect(10, 10, 80, 36);
+ });
+ render(<>>);
+ act(() => { (document.getElementById('body') as HTMLElement).focus(); });
+ act(() => { toast.success('Email sent'); });
+ act(() => { vi.advanceTimersByTime(20); });
+ expect(toastContainer()).not.toHaveAttribute('data-dodging');
+ expect(toastContainer()).not.toHaveClass('top-5');
+ });
+
// On a phone the top of the screen is the header with Back and the message
// actions; an Undo toast that moved there blocked them. (#1472)
it('stays at the bottom on a phone, clear of the header', () => {
diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx
index a3d96a2be..a8a742715 100644
--- a/components/ui/toast.tsx
+++ b/components/ui/toast.tsx
@@ -358,10 +358,14 @@ export function ToastItem({ toast, onClose, held = false, onFocusLost }: ToastPr
}
/**
- * True when the toast stack, in its usual place at the bottom, overlaps the
- * element that has the focus. The stack's own box is measured where it is
- * now; while it sits at the top, its bottom place is worked out from the gap
- * it left there (data-bottom-gap).
+ * True when the toast stack, in its usual place at the bottom, hides at
+ * least half of the element that has the focus: a button or a field. A
+ * large element it only overlaps at a corner (the composer's text, the
+ * quick reply box) stays usable, and moving the stack to the top for it put
+ * the toasts over the composer's close button and the reader's toolbar. The
+ * stack's own box is measured where it is now; while it sits at the top,
+ * its bottom place is worked out from the gap it left there
+ * (data-bottom-gap).
*/
export function stackWouldCoverFocus(stack: HTMLElement | null): boolean {
if (!stack) return false;
@@ -379,7 +383,11 @@ export function stackWouldCoverFocus(stack: HTMLElement | null): boolean {
stack.dataset.bottomGap = String(Math.max(0, window.innerHeight - box.bottom));
}
const f = focused.getBoundingClientRect();
- return f.right > box.left && f.left < box.right && f.bottom > top && f.top < bottom;
+ const overlapWidth = Math.min(f.right, box.right) - Math.max(f.left, box.left);
+ const overlapHeight = Math.min(f.bottom, bottom) - Math.max(f.top, top);
+ if (overlapWidth <= 0 || overlapHeight <= 0) return false;
+ const area = Math.max(f.width, 0) * Math.max(f.height, 0);
+ return area === 0 || overlapWidth * overlapHeight >= area / 2;
}
export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: (id: string) => void }) {