fix: keep the undo toast off the header on phones #1472

This commit is contained in:
Linus Rath
2026-10-31 16:15:58 +01:00
parent a0da9eb6c2
commit c13b87c0e8
2 changed files with 30 additions and 4 deletions
@@ -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(<><Toasts /><div id="list" tabIndex={-1}>Messages</div></>);
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-/);
});
});
+7 -4
View File
@@ -393,7 +393,10 @@ export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose:
const returnFocusRef = useRef<HTMLElement | null>(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}