mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Show the focus border for the keyboard alone
The menu focuses whatever the pointer is over, so `:focus-visible` was being asked a question it cannot answer. Chromium calls every focus after any key press keyboard-driven, so one Escape left the border following the mouse for the rest of the session; Firefox never calls a programmatic focus keyboard-driven, so the border never appeared there at all. Neither is what D13 asks for. The menu now notes which of the two reached the current item, and the border follows that. Nothing existing covers this: Compound exports no such helper, the repo has no react-aria, and Radix marks both modalities alike. `:focus-visible:not(:hover)` would hide the border under the cursor but still leave Firefox without one. Two acceptance criteria named checks that could not fail for the reason they exist. AC24 was manual and now names an e2e test that reads the painted outline as the pointer and keyboard take turns. AC15 asserted the attribute its stylesheet keys off rather than the greying, and now names the meter's own story; deleting the CSS rule fails that story and not the unit test. Both edits made with the owner's authorisation. Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md — AC15, AC24. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,40 @@ test("audio menu stays inside a short window", async ({ browser }) => {
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("the focus border follows the keyboard and not the pointer", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const context = await browser.newContext({ reducedMotion: "reduce" });
|
||||
const page = await context.newPage();
|
||||
await page.goto("/");
|
||||
await SpaHelpers.createCall(page, "Focus", "Focus border", true);
|
||||
await page.getByTestId("videoTile").first().waitFor();
|
||||
|
||||
const chevron = page.getByRole("button", { name: "Microphone" });
|
||||
const rows = page.getByRole("menu").getByRole("menuitemradio");
|
||||
const borderOfFocused = async (): Promise<string> =>
|
||||
page.evaluate(() => {
|
||||
const el = document.activeElement;
|
||||
return el === null ? "none" : getComputedStyle(el).outlineStyle;
|
||||
});
|
||||
|
||||
// Reached with the pointer: the hover background carries it, no border.
|
||||
await chevron.click();
|
||||
await page.getByRole("menu").waitFor();
|
||||
await rows.nth(1).hover();
|
||||
expect(await borderOfFocused()).toBe("none");
|
||||
|
||||
// Reached with the keyboard: a border marks where the keyboard is. The menu
|
||||
// moves focus to whatever the pointer is over, so the browser cannot tell
|
||||
// these two apart on its own.
|
||||
await page.keyboard.press("ArrowDown");
|
||||
expect(await borderOfFocused()).toBe("solid");
|
||||
|
||||
// And back, on the next movement of the pointer.
|
||||
await rows.nth(0).hover();
|
||||
expect(await borderOfFocused()).toBe("none");
|
||||
});
|
||||
|
||||
async function firstUncheckedIndex(
|
||||
rows: Locator,
|
||||
count: number,
|
||||
|
||||
Reference in New Issue
Block a user