fix(ui): constrain focus trap to top dialog (#44188)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d9c9c50809
commit
beb6216343
@@ -0,0 +1,48 @@
|
||||
import { expect, test, type Route } from "@playwright/test"
|
||||
|
||||
const server = "http://127.0.0.1:4097"
|
||||
|
||||
test("nested server dialog keeps focus inside the top layer", async ({ page }) => {
|
||||
await page.addInitScript((server) => {
|
||||
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [server] }))
|
||||
}, server)
|
||||
await page.route("**/*", async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.origin !== server) return route.fallback()
|
||||
if (url.pathname === "/api/event") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/event-stream",
|
||||
body: 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n',
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/api/global/health" || url.pathname === "/api/health") {
|
||||
return json(route, { healthy: true, version: "2.0.0" })
|
||||
}
|
||||
return json(route, {})
|
||||
})
|
||||
|
||||
await page.goto("/")
|
||||
await page.keyboard.press("Control+,")
|
||||
const settings = page.locator(".settings-dialog")
|
||||
await expect(settings).toBeVisible()
|
||||
await settings.getByRole("tab", { name: "Servers" }).click()
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
|
||||
const username = editor.getByPlaceholder("username")
|
||||
const password = editor.getByPlaceholder("password")
|
||||
await username.click()
|
||||
await expect(username).toBeFocused()
|
||||
await username.fill("kit")
|
||||
await expect(username).toHaveValue("kit")
|
||||
await page.keyboard.press("Tab")
|
||||
await expect(password).toBeFocused()
|
||||
await password.fill("secret")
|
||||
await expect(password).toHaveValue("secret")
|
||||
})
|
||||
|
||||
function json(route: Route, body: unknown, status = 200) {
|
||||
return route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) })
|
||||
}
|
||||
@@ -81,6 +81,7 @@ function init() {
|
||||
let dispose: (() => void) | undefined
|
||||
let setClosing: ((closing: boolean) => void) | undefined
|
||||
|
||||
// Stacked dialogs render as sibling portals, so only the top layer may own the focus trap.
|
||||
const node = runWithOwner(owner, () =>
|
||||
createRoot((d: () => void) => {
|
||||
dispose = d
|
||||
@@ -88,7 +89,7 @@ function init() {
|
||||
setClosing = setClosingSignal
|
||||
return (
|
||||
<Kobalte
|
||||
modal
|
||||
modal={stack().length <= layer + 1}
|
||||
open={!closing()}
|
||||
onOpenChange={(open: boolean) => {
|
||||
if (open) return
|
||||
|
||||
Reference in New Issue
Block a user