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}