Size the device list to the call as it changes

- The bound was taken once when the menu opened, so a host resizing the space
  Element Call is drawn in while the menu is open left it describing a call
  area that no longer exists.
- Follow it with observeElementSize$, which was already here and already used
  by useRootSizeMatches, and only while the menu is open.
- Quantised before it reaches React, so a drag-resize re-renders only when the
  bound itself moves.
- 160 and 0.6 are named now, with the reason each exists.
This commit is contained in:
fkwp
2026-09-17 11:56:31 +02:00
parent ace7e947b4
commit 8508d88714
+37 -4
View File
@@ -28,6 +28,7 @@ import {
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { distinctUntilChanged, map } from "rxjs";
import styles from "./MediaMuteAndSwitchButton.module.css"; import styles from "./MediaMuteAndSwitchButton.module.css";
import { MicButton, VideoButton } from "../button"; import { MicButton, VideoButton } from "../button";
@@ -37,6 +38,7 @@ import {
} from "../state/MediaDevices"; } from "../state/MediaDevices";
import { useMediaDevices } from "../MediaDevicesContext"; import { useMediaDevices } from "../MediaDevicesContext";
import { useRootElement } from "../RootElementContext"; import { useRootElement } from "../RootElementContext";
import { observeElementSize$ } from "../utils/elementSize";
import { MicrophoneLevelMeter } from "./MicrophoneLevelMeter"; import { MicrophoneLevelMeter } from "./MicrophoneLevelMeter";
import { useMicrophoneLevel } from "./useMicrophoneLevel"; import { useMicrophoneLevel } from "./useMicrophoneLevel";
@@ -93,6 +95,23 @@ const BLUR_ID = "blur";
*/ */
const DEFAULT_OUTPUT_ID = "default"; const DEFAULT_OUTPUT_ID = "default";
/**
* The share of the call area the device list may fill.
*
* The menu carries its headings and the level meter as well, and a list that
* took the whole call would hide the call it belongs to.
*/
const LIST_SHARE_OF_CALL = 0.6;
/**
* The shortest the device list may be, whatever the call measures.
*
* A share alone collapses in a small call to a list that shows one device and
* gives no sign that there are others. Scrolling a short list is the better
* failure.
*/
const MIN_LIST_HEIGHT = 160;
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
title, title,
enabled, enabled,
@@ -153,11 +172,25 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
if (menuOpen) setFocusModality("pointer"); if (menuOpen) setFocusModality("pointer");
}, [menuOpen]); }, [menuOpen]);
useEffect(() => { useEffect(() => {
if (menuOpen) if (!menuOpen) return;
setListMaxHeight( // Followed rather than measured once: a host can resize the space Element
Math.max(160, Math.round(rootElement.clientHeight * 0.6)), // Call is drawn in while the menu is open — a panel animating, a window
); // dragged, a phone turned — and a bound taken on opening then describes a
// call area that no longer exists. Quantised before it reaches React, so a
// resize re-renders only when the bound itself moves.
const subscription = observeElementSize$(rootElement)
.pipe(
map(({ height }) =>
Math.max(MIN_LIST_HEIGHT, Math.round(height * LIST_SHARE_OF_CALL)),
),
distinctUntilChanged(),
)
.subscribe(setListMaxHeight);
return (): void => subscription.unsubscribe();
}, [menuOpen, rootElement]); }, [menuOpen, rootElement]);
// Only while the menu is open, so nothing holds a second capture of the
// microphone for the length of a call.
const microphoneState = useMicrophoneLevel( const microphoneState = useMicrophoneLevel(
selectedOption, selectedOption,
menuOpen && iconsAndLabels === "audio", menuOpen && iconsAndLabels === "audio",