diff --git a/components/ui/__tests__/toast-layout.test.tsx b/components/ui/__tests__/toast-layout.test.tsx index 1b50a2896..a421af676 100644 --- a/components/ui/__tests__/toast-layout.test.tsx +++ b/components/ui/__tests__/toast-layout.test.tsx @@ -207,4 +207,27 @@ describe('toasts and the focused element', () => { act(() => { vi.advanceTimersByTime(20); }); expect(toastContainer()).not.toHaveAttribute('data-dodging'); }); + + // 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', () => { + vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({ + matches: query === '(width < 40rem)', + media: query, onchange: null, + addEventListener: () => {}, removeEventListener: () => {}, addListener: () => {}, removeListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList); + const h = window.innerHeight; + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { + if (this.hasAttribute('data-toast-container')) return rect(h - 140, 12, 366, 80); + if (this.id === 'list') return rect(56, 0, 390, h - 116); + return rect(10, 10, 80, 36); + }); + render(<>
Messages
); + act(() => { (document.getElementById('list') as HTMLElement).focus(); }); + act(() => { toast.success('Moved to Trash'); }); + act(() => { vi.advanceTimersByTime(20); }); + expect(toastContainer()).not.toHaveAttribute('data-dodging'); + expect(toastContainer().className).not.toMatch(/(^|\s)(max-sm:)?top-/); + }); }); diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index 34cccfd5d..28bb16c0a 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -393,7 +393,10 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: const returnFocusRef = useRef(null); // The stack moves to the top corner while it would cover the element that // has the keyboard focus (WCAG 2.4.11): a Save button at the foot of the - // event editor sat right under it. + // event editor sat right under it. Not on a phone: there the top of the + // screen is the header with Back and the message actions, which an Undo + // toast covered after a delete (#1472), and the stack already rides above + // the bottom bars. const [dodge, setDodge] = useState(false); useEffect(() => { @@ -402,7 +405,7 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: const hasToasts = toasts.length > 0; useLayoutEffect(() => { - if (!hasToasts) { + if (!hasToasts || phone) { setDodge(false); return; } @@ -419,7 +422,7 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: document.removeEventListener("focusin", check); window.removeEventListener("resize", check); }; - }, [hasToasts, toasts.length]); + }, [hasToasts, toasts.length, phone]); useEffect(() => { if (!hasToasts) return; const onKeyDown = (e: KeyboardEvent) => { @@ -463,7 +466,7 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: "max-sm:inset-x-3 max-sm:bottom-[calc(var(--bottom-controls-height,3.75rem)+0.75rem)]", "max-sm:[html[data-composer-open]_&]:bottom-[max(5rem,calc(var(--bottom-controls-height,3.75rem)+0.75rem))]", // Out of the way of the focused element: the top end corner. - dodge && "top-5 bottom-auto! max-sm:top-[calc(env(safe-area-inset-top)+0.75rem)]", + dodge && "top-5 bottom-auto!", held && "invisible" )} data-dodging={dodge || undefined}