Firefox in CI reports one microphone and no outputs, so the menu has a
single selectable row and hovering the second timed out. The test now
drives the pointer by coordinate over the first row, which holds however
many devices a browser reports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The lobby footer reads the same device behaviours as the in-call one, so
the menu already reaches every platform before joining, and stays gated
to desktop during a call. These are the checks that hold it there: the
view model offers the menu pre-join on desktop, Android and iOS, and the
microphone button stands alone with no chevron once a call starts on
mobile.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 5 — AC18, AC19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu gains a third group with the sound-effect volume slider, reading and
writing the same stored value as the slider in settings, so the next effect
plays at the new level. The menu is bounded to the height Radix reports for it
and only the device lists scroll; the heading and the slider stay in place.
Every control is now reachable by keyboard alone. Radix swallows Tab inside
its menus so that the arrow keys walk the items; the audio menu keeps Tab
from it, letting the browser move focus from the device rows to the meter and
on to the slider, while the menu's focus trap keeps that order inside the
menu. The slider's own keys stop at the slider so the menu does not treat
them as navigation. The meter keeps its live-region text on blur: removing it
handed the focus trap an empty active element mid-Tab and pulled focus back
into the menu.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 4 — AC4, AC16, AC17,
AC20, AC21 (manual), AC22, AC27.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The meter sits at the foot of the microphone group and follows the selected
microphone. Its capture starts when the menu opens and stops when it closes,
muted or not, so the microphone is only held while the user is looking at the
level. A denied permission shows a hint in place of the meter.
Keyboard navigation marks the current menu item with a focus border; pointer
use gets the hover background only, since the menu moves DOM focus to
whatever the pointer is over.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 3 — AC9, AC10, AC12,
AC14, AC23, AC24 (manual), AC26.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chevron beside the microphone button now opens a menu headed "Audio
controls" that lists the microphones and, below a separator, the audio
outputs. Selecting either switches the device and keeps the menu open.
Where the browser offers no choice of output, or only one, the speaker
group still names the output in use as a non-selectable row.
The footer view model derives the outputs from the same MediaDevices
helper as the device lists, so the menu reaches both surfaces at once:
in a call on desktop, and before joining on every platform.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 1 — AC1, AC2, AC3,
AC5, AC6, AC7, AC25.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Update ghcr.io/element-hq/element-web:develop Docker digest to a9bd290
* Fix Element Web integration tests
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robin <robin@robin.town>
The widget tests cannot reach what makes a component different, because the
iframe used to guarantee it: that Element Call stays inside the space it was
given, and that two of them can exist in one page. So the development harness
gets driven by Playwright.
Three tests. Two components in one page holding a real call between two devices
of one account. The settings dialog and the reaction picker staying inside the
container the host gave, asserted by bounding box. And the host bridge
reporting in both directions, including a host-initiated mute coming back as a
report of the new state.
The containment test is the one worth having: both of the escapes found by
running the harness by hand — the settings dialog centred on the window, and
the reaction picker at 82vh landing below the container — would have failed it,
and neither was visible to typechecking, linting or the unit tests.
Users and rooms are created through the Synapse admin and client-server APIs
rather than by driving an interface, and the harness now takes its credentials
from its own query string so that a test can say which account and room to use.
A host reading its own URL is proper; it was Element Call doing so that was the
mistake.
Playwright gains a second web server for the harness on port 3001, a Vite dev
server whether or not the app itself is served from Docker, since the harness is
a development page with nothing to build.
This is the mode in which we sent membership events with the 'oldest membership' transport selection algorithm, which stopped being the default back in version 0.21.0. Users will no longer be able to select this mode in developer settings, and admins will no longer be able to select legacy mode through the config either. The app will still continue to support *receiving* membership events with the 'oldest membership' transport selection algorithm from others, however.
* Update ghcr.io/element-hq/element-web:develop Docker digest to 0183c0a
* Update end-to-end tests to work with auto-collapsing room list
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robin <robin@robin.town>
* Add Posthog events for Call reconnect including the reason
* Expose single trackCallReconnecting() entry point on PosthogAnalytics
* Track reconnect duration and align with existing analytics pattern
* Refactor combined$ to return [connected, reason] tuple
* Update firefoxUserPrefs to allow getUserMedia and enumerateDevices on CI
---------
Co-authored-by: Valere <bill.carson@valrsoft.com>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Timo K <toger5@hotmail.de>
* Update compound-web to 9.3.0 and update Buttons from "sm" to "md".
* Clean up the liast bits
* Update snaps too
* fix test, need to dismiss a new dialog
---------
Co-authored-by: Valere <bill.carson@valrsoft.com>