mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(ui): open dialogs with their input focused
This commit is contained in:
@@ -117,7 +117,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
await models.getByRole("button", { name: "Connect an integration", exact: true }).click()
|
||||
await expect(models).toBeHidden()
|
||||
const providers = page.getByRole("dialog", { name: "Connect an integration", exact: true })
|
||||
await expect(providers.getByRole("searchbox", { name: "Search providers", exact: true })).toBeVisible()
|
||||
await expect(providers.getByRole("searchbox", { name: "Search providers", exact: true })).toBeFocused()
|
||||
await expect(page.getByRole("dialog")).toHaveCount(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
// Focus starts on the dialog's close button, outside the list, so listen at the
|
||||
// Focus starts on the dialog itself, outside the list, so listen at the
|
||||
// document level while the dialog is mounted instead of on the list container.
|
||||
let listEl: HTMLDivElement | undefined
|
||||
onMount(() => {
|
||||
|
||||
@@ -4,6 +4,12 @@ import { useDialogLayer } from "../../context/dialog"
|
||||
import { useI18n } from "../../context/i18n"
|
||||
import "./dialog.css"
|
||||
|
||||
const TEXT_FIELD = [
|
||||
'input:not([type="hidden"], [type="checkbox"], [type="radio"], [type="button"], [type="submit"], [type="reset"], [type="range"], [type="file"], [type="color"], [type="image"]):not(:disabled, [readonly])',
|
||||
"textarea:not(:disabled, [readonly])",
|
||||
'[contenteditable="true"]',
|
||||
].join(", ")
|
||||
|
||||
export interface DialogProps extends ParentProps {
|
||||
size?: "normal" | "large" | "x-large"
|
||||
variant?: "default" | "settings"
|
||||
@@ -120,15 +126,30 @@ export function Dialog(props: DialogProps) {
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
onOpenAutoFocus={(e) => {
|
||||
const autofocusEl =
|
||||
e.currentTarget instanceof HTMLElement ? e.currentTarget.querySelector<HTMLElement>("[autofocus]") : null
|
||||
const content = e.currentTarget
|
||||
|
||||
if (autofocusEl) {
|
||||
e.preventDefault()
|
||||
// Kobalte attaches its focus trap after this event, so focusing now would leave the trap with no
|
||||
// element to return to. Deferring, as Kobalte's own autofocus does, lets it pull stray focus back.
|
||||
setTimeout(() => autofocusEl.focus({ preventScroll: true }))
|
||||
}
|
||||
if (!(content instanceof HTMLElement)) return
|
||||
// Kobalte would focus the first tabbable element, usually the close button, which Escape already covers.
|
||||
e.preventDefault()
|
||||
// Kobalte attaches its focus trap after this event, so focusing now would leave the trap with no element
|
||||
// to return to. Deferring, as Kobalte's own autofocus does, also lets children that focus themselves on
|
||||
// mount keep that focus.
|
||||
setTimeout(() => {
|
||||
const autofocus = content.querySelector<HTMLElement>("[autofocus]")
|
||||
const active = document.activeElement
|
||||
const kept =
|
||||
active instanceof HTMLElement &&
|
||||
active !== content &&
|
||||
content.contains(active) &&
|
||||
!active.closest('[data-slot="dialog-close-button"]')
|
||||
? active
|
||||
: undefined
|
||||
const field = Array.from(content.querySelectorAll<HTMLElement>(TEXT_FIELD)).find(
|
||||
(element) => element.getClientRects().length > 0,
|
||||
)
|
||||
const target = autofocus ?? kept ?? field ?? content
|
||||
target.focus({ preventScroll: true })
|
||||
})
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
|
||||
Reference in New Issue
Block a user