Files
growstuff/app/javascript/components/AuthDialog.jsx
T
Daniel O'Connorandgoogle-labs-jules[bot] 8875e89345 Dismiss responsive offcanvas when opening auth dialogs (#5259) (#5260)
On iPad and tablets, the main navigation menu is rendered as a
responsive Bootstrap offcanvas drawer (`#navbarSupportedContent.offcanvas-lg`).
When users tapped 'Sign in' in the drawer, the auth modal opened while the
offcanvas remained open in the DOM, allowing Bootstrap's active FocusTrap
on the offcanvas to intercept focusin events on document and steal focus
away from the modal inputs, preventing clicking or typing on iPadOS.

Update `closeOpenOffcanvases` in `Modal.jsx` and `RecordDialog.jsx` to
match all open offcanvases including responsive ones
(`.offcanvas.show, [class*="offcanvas-"].show`) and closest offcanvas
containers, invoke it when auth dialog triggers are clicked in
`AuthDialog.jsx`, restore focus correctly when the modal closes, and add
accompanying tests.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-11-03 04:10:38 +10:30

48 lines
2.0 KiB
React

import React, {useEffect, useRef, useState} from 'react';
import BlueskyModal from './BlueskyModal';
import {closeOpenOffcanvases} from './Modal';
import SignInModal from './SignInModal';
import SignUpModal from './SignUpModal';
// The sign in, sign up and Bluesky dialogs for every server-rendered link to them.
// Nothing of its own is drawn: it listens for clicks on any
// [data-auth-dialog="sign_in"], "sign_up" or "bluesky" link (the menu,
// the home page's buttons) and opens that dialog over the page. The layout
// mounts it once, for visitors who aren't signed in.
//
// The link's own href is the page for the same thing, so it stays the
// fallback without JavaScript.
export default function AuthDialog({urls, recaptcha_site_key: recaptchaSiteKey, site_name: siteName}) {
const [opened, setOpened] = useState(null); // 'sign_in', 'sign_up', 'bluesky' or null
const open = useRef(null);
open.current = setOpened;
useEffect(() => {
function onClick(event) {
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
const trigger = event.target.closest('[data-auth-dialog]');
if (!trigger) return;
event.preventDefault();
closeOpenOffcanvases(trigger);
open.current(trigger.dataset.authDialog);
}
document.addEventListener('click', onClick);
return () => document.removeEventListener('click', onClick);
}, []);
const onBluesky = urls.bluesky ? () => setOpened('bluesky') : undefined;
if (opened === 'sign_in') {
return <SignInModal urls={urls} onClose={() => setOpened(null)} onSwitch={() => setOpened('sign_up')} onBluesky={onBluesky} />;
}
if (opened === 'sign_up') {
return <SignUpModal urls={urls} recaptchaSiteKey={recaptchaSiteKey} siteName={siteName} onClose={() => setOpened(null)} onSwitch={() => setOpened('sign_in')} onBluesky={onBluesky} />;
}
if (opened === 'bluesky') {
return <BlueskyModal urls={urls} onClose={() => setOpened(null)} />;
}
return null;
}