feat(dialogs): the focus starts inside a dialog and Tab stays there

Every dialog, the shared frame and the older modal alike, moves the focus in when it opens: on a desktop into the first text field, on a phone onto the panel so no keyboard pops up. Tab wraps around inside instead of running through the page behind, and closing hands the focus back to what opened it. Fields that open a list on focus are skipped, and the older modal's close button got a label and a tooltip. (#1302)
This commit is contained in:
Maurice
2026-10-01 09:50:31 +02:00
parent c80ee6b12a
commit 44cc596c39
10 changed files with 278 additions and 19 deletions
@@ -89,6 +89,8 @@ export default function AddressInput({ value, onChange, placeholder, className }
value={value}
placeholder={placeholder}
onChange={e => { onChange(e.target.value); setOpen(true); search(e.target.value) }}
// Opens its list on focus, so a dialog must not focus it by itself (#1302).
data-no-autofocus
onFocus={() => setOpen(true)}
onKeyDown={onKey}
className={className}
@@ -113,6 +113,8 @@ export default function AirportSelect({ value, onChange, placeholder, style }: P
value={query}
placeholder={placeholder ?? t('airport.searchPlaceholder')}
onChange={(e) => { setQuery(e.target.value); setOpen(true); if (value) onChange(null) }}
// Opens its list on focus, so a dialog must not focus it by itself (#1302).
data-no-autofocus
onFocus={() => setOpen(true)}
onKeyDown={onKey}
className="bg-transparent text-content"
@@ -115,6 +115,8 @@ export default function LocationSelect({ value, onChange, placeholder, style, pl
value={query}
placeholder={placeholder ?? t('reservations.searchLocation')}
onChange={(e) => { setQuery(e.target.value); setOpen(true); setHighlight(-1); if (value) onChange(null) }}
// Opens its list on focus, so a dialog must not focus it by itself (#1302).
data-no-autofocus
onFocus={() => setOpen(true)}
onKeyDown={onKey}
className="bg-transparent text-content"
@@ -939,7 +939,9 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
labelId={titleId}
onClose={onClose}
eyebrow={title}
// Never focused on opening: a new place starts at the search, an edit at the dialog.
// Not autoFocused itself: a new place starts at the search, and otherwise the
// dialog's own first-field focus lands here on a desktop (#1302), so a place
// brought in from the map or opened to edit is ready for its name.
titleInput={{
value: form.name,
onChange: value => handleChange('name', value),
@@ -256,6 +256,8 @@ function StopPicker({ label, value, onPick, quickPicks, near, placeholder }: {
<input
value={display}
onChange={e => search(e.target.value)}
// Opens its list on focus, so a dialog must not focus it by itself (#1302).
data-no-autofocus
onFocus={() => setOpen(true)}
placeholder={placeholder}
className="text-content"