Make the screen share volume button accessible on mobile

In landscape orientation the button would be buried underneath the footer, which would block interaction with it. This commit changes the footer to not show in cases where a button has been pressed.
This commit is contained in:
Robin
2026-03-09 10:30:42 +01:00
parent 3bbbac23a0
commit 313b8285d9
4 changed files with 26 additions and 63 deletions

View File

@@ -65,6 +65,7 @@ Please see LICENSE in the repository root for full details.
.footer.overlay.hidden { .footer.overlay.hidden {
display: grid; display: grid;
opacity: 0; opacity: 0;
pointer-events: none;
} }
.footer.overlay:has(:focus-visible) { .footer.overlay:has(:focus-visible) {

View File

@@ -9,8 +9,8 @@ import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk"; import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk";
import { import {
type FC, type FC,
type PointerEvent, type MouseEvent as ReactMouseEvent,
type TouchEvent, type PointerEvent as ReactPointerEvent,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -110,8 +110,6 @@ import { ObservableScope } from "../state/ObservableScope.ts";
const logger = rootLogger.getChild("[InCallView]"); const logger = rootLogger.getChild("[InCallView]");
const maxTapDurationMs = 400;
export interface ActiveCallProps extends Omit< export interface ActiveCallProps extends Omit<
InCallViewProps, InCallViewProps,
"vm" | "livekitRoom" | "connState" "vm" | "livekitRoom" | "connState"
@@ -334,40 +332,20 @@ export const InCallView: FC<InCallViewProps> = ({
) : null; ) : null;
}, [ringOverlay]); }, [ringOverlay]);
// Ideally we could detect taps by listening for click events and checking const onViewClick = useCallback(
// that the pointerType of the event is "touch", but this isn't yet supported (e: ReactMouseEvent) => {
// in Safari: https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#browser_compatibility if (
// Instead we have to watch for sufficiently fast touch events. (e.nativeEvent as PointerEvent).pointerType === "touch" &&
const touchStart = useRef<number | null>(null); // If an interactive element was tapped, don't count this as a tap on the screen
const onTouchStart = useCallback(() => (touchStart.current = Date.now()), []); (e.target as Element).closest?.("button, input") === null
const onTouchEnd = useCallback(() => { )
const start = touchStart.current; vm.tapScreen();
if (start !== null && Date.now() - start <= maxTapDurationMs)
vm.tapScreen();
touchStart.current = null;
}, [vm]);
const onTouchCancel = useCallback(() => (touchStart.current = null), []);
// We also need to tell the footer controls to prevent touch events from
// bubbling up, or else the footer will be dismissed before a click/change
// event can be registered on the control
const onControlsTouchEnd = useCallback(
(e: TouchEvent) => {
// Somehow applying pointer-events: none to the controls when the footer
// is hidden is not enough to stop clicks from happening as the footer
// becomes visible, so we check manually whether the footer is shown
if (showFooter) {
e.stopPropagation();
vm.tapControls();
} else {
e.preventDefault();
}
}, },
[vm, showFooter], [vm],
); );
const onPointerMove = useCallback( const onPointerMove = useCallback(
(e: PointerEvent) => { (e: ReactPointerEvent) => {
if (e.pointerType === "mouse") vm.hoverScreen(); if (e.pointerType === "mouse") vm.hoverScreen();
}, },
[vm], [vm],
@@ -667,7 +645,6 @@ export const InCallView: FC<InCallViewProps> = ({
key="audio" key="audio"
muted={!audioEnabled} muted={!audioEnabled}
onClick={toggleAudio ?? undefined} onClick={toggleAudio ?? undefined}
onTouchEnd={onControlsTouchEnd}
disabled={toggleAudio === null} disabled={toggleAudio === null}
data-testid="incall_mute" data-testid="incall_mute"
/>, />,
@@ -675,7 +652,6 @@ export const InCallView: FC<InCallViewProps> = ({
key="video" key="video"
muted={!videoEnabled} muted={!videoEnabled}
onClick={toggleVideo ?? undefined} onClick={toggleVideo ?? undefined}
onTouchEnd={onControlsTouchEnd}
disabled={toggleVideo === null} disabled={toggleVideo === null}
data-testid="incall_videomute" data-testid="incall_videomute"
/>, />,
@@ -687,7 +663,6 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.shareScreen} className={styles.shareScreen}
enabled={sharingScreen} enabled={sharingScreen}
onClick={vm.toggleScreenSharing} onClick={vm.toggleScreenSharing}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_screenshare" data-testid="incall_screenshare"
/>, />,
); );
@@ -699,18 +674,11 @@ export const InCallView: FC<InCallViewProps> = ({
key="raise_hand" key="raise_hand"
className={styles.raiseHand} className={styles.raiseHand}
identifier={`${client.getUserId()}:${client.getDeviceId()}`} identifier={`${client.getUserId()}:${client.getDeviceId()}`}
onTouchEnd={onControlsTouchEnd}
/>, />,
); );
} }
if (layout.type !== "pip") if (layout.type !== "pip")
buttons.push( buttons.push(<SettingsButton key="settings" onClick={openSettings} />);
<SettingsButton
key="settings"
onClick={openSettings}
onTouchEnd={onControlsTouchEnd}
/>,
);
buttons.push( buttons.push(
<EndCallButton <EndCallButton
@@ -718,7 +686,6 @@ export const InCallView: FC<InCallViewProps> = ({
onClick={function (): void { onClick={function (): void {
vm.hangup(); vm.hangup();
}} }}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_leave" data-testid="incall_leave"
/>, />,
); );
@@ -751,7 +718,6 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.layout} className={styles.layout}
layout={gridMode} layout={gridMode}
setLayout={setGridMode} setLayout={setGridMode}
onTouchEnd={onControlsTouchEnd}
/> />
)} )}
</div> </div>
@@ -760,12 +726,13 @@ export const InCallView: FC<InCallViewProps> = ({
const allConnections = useBehavior(vm.allConnections$); const allConnections = useBehavior(vm.allConnections$);
return ( return (
// The onClick handler here exists to control the visibility of the footer,
// and the footer is also viewable by moving focus into it, so this is fine.
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div <div
className={styles.inRoom} className={styles.inRoom}
ref={containerRef} ref={containerRef}
onTouchStart={onTouchStart} onClick={onViewClick}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
onPointerMove={onPointerMove} onPointerMove={onPointerMove}
onPointerOut={onPointerOut} onPointerOut={onPointerOut}
> >

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { type ChangeEvent, type FC, type TouchEvent, useCallback } from "react"; import { type ChangeEvent, type FC, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { import {
@@ -22,15 +22,9 @@ interface Props {
layout: Layout; layout: Layout;
setLayout: (layout: Layout) => void; setLayout: (layout: Layout) => void;
className?: string; className?: string;
onTouchEnd?: (e: TouchEvent) => void;
} }
export const LayoutToggle: FC<Props> = ({ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
layout,
setLayout,
className,
onTouchEnd,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const onChange = useCallback( const onChange = useCallback(
@@ -47,7 +41,6 @@ export const LayoutToggle: FC<Props> = ({
value="spotlight" value="spotlight"
checked={layout === "spotlight"} checked={layout === "spotlight"}
onChange={onChange} onChange={onChange}
onTouchEnd={onTouchEnd}
/> />
</Tooltip> </Tooltip>
<SpotlightIcon aria-hidden width={24} height={24} /> <SpotlightIcon aria-hidden width={24} height={24} />
@@ -58,7 +51,6 @@ export const LayoutToggle: FC<Props> = ({
value="grid" value="grid"
checked={layout === "grid"} checked={layout === "grid"}
onChange={onChange} onChange={onChange}
onTouchEnd={onTouchEnd}
/> />
</Tooltip> </Tooltip>
<GridIcon aria-hidden width={24} height={24} /> <GridIcon aria-hidden width={24} height={24} />

View File

@@ -84,7 +84,6 @@ Please see LICENSE in the repository root for full details.
.expand { .expand {
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
opacity: 0;
padding: var(--cpd-space-2x); padding: var(--cpd-space-2x);
border: none; border: none;
border-radius: var(--cpd-radius-pill-effect); border-radius: var(--cpd-radius-pill-effect);
@@ -148,17 +147,21 @@ Please see LICENSE in the repository root for full details.
} }
} }
.expand:active { .expand:active, .expand[data-state="open"] {
background: var(--cpd-color-gray-100); background: var(--cpd-color-gray-100);
} }
@media (hover) { @media (hover) {
.tile > div > button {
opacity: 0;
}
.tile:hover > div > button { .tile:hover > div > button {
opacity: 1; opacity: 1;
} }
} }
.tile:has(:focus-visible) > div > button { .tile:has(:focus-visible) > div > button,
.tile > div:has([data-state="open"]) > button {
opacity: 1; opacity: 1;
} }