Keep provider settings above the model selector (#2476)

* fix(app): keep provider settings above model selector

Desktop web comboboxes used the browser top layer, so ordinary modal portals could not cover them. Keep web overlays in one relative layer stack while preserving native bottom-sheet stacking.

* fix(app): route overlay keyboard ownership

* fix(app): preserve nested overlay ownership

* fix(app): route command center overlay input

* fix(app): preserve global overlay ancestry

* fix(app): register global dialog hosts
This commit is contained in:
Mohamed Boudra
2026-07-27 14:04:29 +02:00
committed by GitHub
parent e1b1ca569d
commit 2acb10fce9
14 changed files with 720 additions and 201 deletions
@@ -100,11 +100,7 @@ async function selectMode(page: Page, label: string): Promise<void> {
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await searchInput.fill(label);
const option = page
.getByRole("dialog")
.last()
.getByText(new RegExp(`^${escapeRegex(label)}$`, "i"))
.first();
const option = popup.getByText(new RegExp(`^${escapeRegex(label)}$`, "i")).first();
await expect(option).toBeVisible({ timeout: 10_000 });
await option.click({ force: true });
await expect(searchInput).not.toBeVisible({ timeout: 5_000 });
@@ -1,3 +1,4 @@
import type { Locator } from "@playwright/test";
import { expect, test, type Page } from "./fixtures";
import { expectComposerVisible } from "./helpers/composer";
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
@@ -63,6 +64,33 @@ async function closeSheetByHeaderButton(page: Page, testId: string) {
await expect(sheet).not.toBeVisible({ timeout: 10_000 });
}
async function expectOverlayAbove(page: Page, frontTestId: string, backTestId: string) {
const frontCoversBack = await page.evaluate(
({ frontTestId: frontId, backTestId: backId }) => {
const front = document.querySelector(`[data-testid="${frontId}"]`);
const back = document.querySelector(`[data-testid="${backId}"]`);
if (!(front instanceof HTMLElement) || !(back instanceof HTMLElement)) return false;
const frontRect = front.getBoundingClientRect();
const backRect = back.getBoundingClientRect();
const left = Math.max(frontRect.left, backRect.left);
const right = Math.min(frontRect.right, backRect.right);
const top = Math.max(frontRect.top, backRect.top);
const bottom = Math.min(frontRect.bottom, backRect.bottom);
if (left >= right || top >= bottom) return false;
const topElement = document.elementFromPoint((left + right) / 2, (top + bottom) / 2);
return topElement != null && front.contains(topElement);
},
{ frontTestId, backTestId },
);
expect(frontCoversBack).toBe(true);
}
async function hasFocusWithin(locator: Locator): Promise<boolean> {
return locator.evaluate((element) => element.contains(document.activeElement));
}
async function expectProviderSettingsVisible(page: Page) {
await expect(page.getByTestId("provider-settings-sheet")).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole("button", { name: "Add model" })).toBeVisible();
@@ -89,7 +117,69 @@ async function exerciseProviderSettingsStack(page: Page) {
await expectProviderSettingsVisible(page);
}
test.describe("provider settings bottom-sheet stack", () => {
test.describe("provider settings overlay stack", () => {
test("provider settings covers the desktop model selector without closing it", async ({
page,
}) => {
const session = await seedMockAgentWorkspace({
repoPrefix: "provider-modal-layer-",
title: "Provider modal layer e2e",
});
try {
await openAgentRoute(page, session);
await expectComposerVisible(page);
await page.getByRole("button", { name: /Select model/ }).click();
const selector = page.getByTestId("combobox-desktop-container");
await expect(selector).toBeVisible({ timeout: 10_000 });
const searchInput = page.getByRole("textbox", { name: /search models/i });
await expect(searchInput).toBeFocused();
await page.keyboard.press("Shift+Tab");
await expect.poll(() => hasFocusWithin(selector)).toBe(true);
await page.keyboard.press("Shift+?");
const shortcuts = page.getByTestId("keyboard-shortcuts-dialog");
await expect(shortcuts).toBeVisible({ timeout: 10_000 });
await expect(page.getByPlaceholder("Search shortcuts")).toBeFocused();
await page.keyboard.press("Escape");
await expect(shortcuts).not.toBeVisible({ timeout: 10_000 });
await expect(selector).toBeVisible();
await page.keyboard.press("ControlOrMeta+K");
const commandCenter = page.getByTestId("command-center-panel");
await expect(commandCenter).toBeVisible({ timeout: 10_000 });
await expect(commandCenter.getByTestId("command-center-input")).toBeFocused();
await page.keyboard.press("Escape");
await expect(commandCenter).not.toBeVisible({ timeout: 10_000 });
await expect(selector).toBeVisible();
await page.keyboard.press("ControlOrMeta+K");
await expect(commandCenter).toBeVisible({ timeout: 10_000 });
await commandCenter.getByText("Add project", { exact: true }).click();
const addProject = page.getByTestId("add-project-flow");
await expect(addProject).toBeVisible({ timeout: 10_000 });
await expect(addProject.getByTestId("add-project-flow-input")).toBeFocused();
await page.keyboard.press("Escape");
await expect(addProject).not.toBeVisible({ timeout: 10_000 });
await expect(selector).toBeVisible();
const settingsButton = page.getByTestId("selector-header-settings-mock");
await settingsButton.click();
const settings = page.getByTestId("provider-settings-sheet");
await expect(settings).toBeVisible({ timeout: 10_000 });
await expectOverlayAbove(page, "provider-settings-sheet", "combobox-desktop-container");
await page.keyboard.press("Escape");
await expect(settings).not.toBeVisible({ timeout: 10_000 });
await expect(selector).toBeVisible();
await expect(settingsButton).toBeFocused();
} finally {
await session.cleanup();
}
});
test("provider settings and children close back through the model selector stack", async ({
page,
}) => {