mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
- Was watched at the document, so a key pressed in another Element Call on the same page — or in the host's own page — lit this menu's ring. Element Call can be mounted more than once, and the menu is portalled out of the root. - Watched on the menu instead, which is the element that holds focus. - A callback ref, not an effect on `menuOpen`: that state is ours and the open menu is Radix's, and they do not commit together, so the effect could run before there was anything to attach to. - Recorded on the menu rather than in state, so every item it can focus answers to it — the camera menu's blur toggle kept the browser's own ring otherwise, which follows the pointer. - Nothing re-renders when the modality changes now, so the reset folds in with the listeners and the second effect on that state goes. - Comments moved to the code they describe; three unrelated ones had stacked up on one declaration.
128 lines
4.6 KiB
CSS
128 lines
4.6 KiB
CSS
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
.container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
|
border-radius: 32px;
|
|
transition: background-color 0.2s ease-in-out;
|
|
}
|
|
.containerOpen {
|
|
background-color: var(--cpd-color-bg-action-primary-pressed);
|
|
}
|
|
.chevronIconOpen > svg {
|
|
color: var(--cpd-color-icon-on-solid-primary);
|
|
}
|
|
.menuButton {
|
|
width: 40px;
|
|
background-color: transparent !important;
|
|
}
|
|
.itemIcon {
|
|
color: var(--cpd-color-text-secondary);
|
|
}
|
|
|
|
.rotate {
|
|
animation: spinner 1.5s linear infinite;
|
|
}
|
|
@keyframes spinner {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Compound underlines every menu heading. The design has a single line, the
|
|
one dividing the speaker list from the microphone list, so the headings
|
|
carry none and that divider is drawn explicitly. */
|
|
.menu h3 {
|
|
border-block-end: none;
|
|
padding-block-end: var(--cpd-space-2x);
|
|
}
|
|
|
|
/* The menu zeroes separator spacing, but the design gives the heading below
|
|
the divider room to breathe. */
|
|
.menu [role="separator"] {
|
|
margin-block-end: var(--cpd-space-5x);
|
|
}
|
|
|
|
/* Only the device lists scroll; the level meter stays put beneath them.
|
|
The bound comes from the measured height of the call area, set by the
|
|
component: the menu is portalled outside the root, so neither a container
|
|
query nor a viewport unit describes the space it is allowed to fill. */
|
|
.deviceList {
|
|
overflow-y: auto;
|
|
min-block-size: 0;
|
|
max-block-size: var(--device-list-max-height);
|
|
/* The heading stands over the head of this list and the level meter over its
|
|
foot. Scrolling a row flush to either edge would put it underneath one of
|
|
them, which is how a row reached by the keyboard ends up half-readable;
|
|
this keeps both their heights clear for anything the browser scrolls to.
|
|
Set by the component, which is the only thing that can measure them. */
|
|
scroll-padding-block: var(--device-list-scroll-padding-start, 0)
|
|
var(--device-list-scroll-padding-end, 0);
|
|
}
|
|
|
|
/* Each section's heading stays at the top of the scrollport while any of that
|
|
section is still in view, so a long list never leaves you wondering which
|
|
kind of device you are looking at. It leaves with its own section, because
|
|
sticky only holds while the group it belongs to is in view.
|
|
|
|
Opaque, and held a border width clear of the sides, for the same reason as
|
|
the meter below: this is a positioned element, so it paints above the
|
|
outline the menu draws its frame with, and would swallow it. */
|
|
.sectionHeading {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
background: var(--cpd-color-bg-canvas-default);
|
|
margin-inline: var(--cpd-border-width-1);
|
|
margin-block-start: var(--cpd-border-width-1);
|
|
}
|
|
|
|
/* The meter belongs to the microphone section: it stays at the bottom of the
|
|
scrollport while that section is in view, and leaves with it when the list
|
|
is scrolled up to the speakers. The wrapper is deliberately unpositioned —
|
|
a positioned one paints above the menu's outline and swallows the frame
|
|
along this whole section. Sticky is resolved against the scroll container,
|
|
so it does not need one. */
|
|
|
|
.stickyMeter {
|
|
position: sticky;
|
|
inset-block-end: 0;
|
|
/* Opaque, so the list does not show through it as it scrolls past. The menu
|
|
draws its frame as an outline inset by one border width, and the device
|
|
rows are transparent at rest, so this is the only thing that can cover it:
|
|
hold it clear on the sides and the bottom. */
|
|
background: var(--cpd-color-bg-canvas-default);
|
|
margin-inline: var(--cpd-border-width-1);
|
|
margin-block-end: var(--cpd-border-width-1);
|
|
}
|
|
|
|
/* Radix focuses whatever the pointer is over, so the browser's own ring marks
|
|
the item under the mouse. It cannot distinguish the two modalities here, so
|
|
it is suppressed and replaced by one that can.
|
|
|
|
Every kind of item the menu can focus, not only the device rows: the camera
|
|
menu's blur toggle is a checkbox item and a child of the menu rather than of
|
|
the list, and left out it kept the browser's own ring and followed the
|
|
pointer with it. */
|
|
.menu [role^="menuitem"]:focus,
|
|
.menu [role^="menuitem"]:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
/* Shown only when the keyboard is what moved the focus. */
|
|
.menu[data-focus-modality="keyboard"] [role^="menuitem"]:focus {
|
|
outline: var(--cpd-border-width-2) solid var(--cpd-color-border-focused);
|
|
outline-offset: calc(-1 * var(--cpd-border-width-2));
|
|
}
|