fix: keep toasts at the bottom instead of over the composer's close button

This commit is contained in:
Linus Rath
2026-11-04 07:16:24 +01:00
parent 43c9ef9ad1
commit 242d0bacfc
2 changed files with 32 additions and 5 deletions
@@ -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(<><Toasts /><textarea id="body" /></>);
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', () => {
+13 -5
View File
@@ -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 }) {