Tooltip: improve accessibility for call and voice messages (#12489)

* Move to `AccessibilityButton`

* Update snapshots

* Add tests

* Update snapshots
This commit is contained in:
Florian Duros 2024-05-07 12:20:52 +02:00 committed by GitHub
parent caef3c1921
commit febb60ee45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 167 additions and 34 deletions

View file

@ -17,14 +17,11 @@ limitations under the License.
import React, { ComponentProps, ReactNode } from "react"; import React, { ComponentProps, ReactNode } from "react";
import classNames from "classnames"; import classNames from "classnames";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../audio/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import AccessibleButton from "../elements/AccessibleButton";
type Props = Omit< type Props = Omit<ComponentProps<typeof AccessibleButton>, "title" | "onClick" | "disabled" | "element" | "ref"> & {
ComponentProps<typeof AccessibleTooltipButton>,
"title" | "onClick" | "disabled" | "element" | "ref"
> & {
// Playback instance to manipulate. Cannot change during the component lifecycle. // Playback instance to manipulate. Cannot change during the component lifecycle.
playback: Playback; playback: Playback;
@ -61,7 +58,7 @@ export default class PlayPauseButton extends React.PureComponent<Props> {
}); });
return ( return (
<AccessibleTooltipButton <AccessibleButton
data-testid="play-pause-button" data-testid="play-pause-button"
className={classes} className={classes}
title={isPlaying ? _t("action|pause") : _t("action|play")} title={isPlaying ? _t("action|pause") : _t("action|play")}

View file

@ -20,10 +20,8 @@ import React, { ComponentProps, createRef, useState, forwardRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu"; import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu";
import DialpadContextMenu from "../../context_menus/DialpadContextMenu"; import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
import { Alignment } from "../../elements/Tooltip";
import { import {
alwaysMenuProps, alwaysMenuProps,
alwaysAboveRightOf, alwaysAboveRightOf,
@ -34,7 +32,7 @@ import {
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import DeviceContextMenu from "../../context_menus/DeviceContextMenu"; import DeviceContextMenu from "../../context_menus/DeviceContextMenu";
import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler"; import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler";
import { ButtonEvent } from "../../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
// Height of the header duplicated from CSS because we need to subtract it from our max // Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video // height to get the max height of the video
@ -42,29 +40,34 @@ const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the butt
const CONTROLS_HIDE_DELAY = 2000; const CONTROLS_HIDE_DELAY = 2000;
type ButtonProps = Omit<ComponentProps<typeof AccessibleTooltipButton>, "title" | "element"> & { type ButtonProps = Omit<ComponentProps<typeof AccessibleButton>, "title" | "element"> & {
state: boolean; state: boolean;
onLabel?: string; onLabel?: string;
offLabel?: string; offLabel?: string;
forceHide?: boolean;
onHover?: (hovering: boolean) => void;
}; };
const LegacyCallViewToggleButton = forwardRef<HTMLElement, ButtonProps>( const LegacyCallViewToggleButton = forwardRef<HTMLElement, ButtonProps>(
({ children, state: isOn, className, onLabel, offLabel, ...props }, ref) => { ({ children, state: isOn, className, onLabel, offLabel, forceHide, onHover, ...props }, ref) => {
const classes = classNames("mx_LegacyCallViewButtons_button", className, { const classes = classNames("mx_LegacyCallViewButtons_button", className, {
mx_LegacyCallViewButtons_button_on: isOn, mx_LegacyCallViewButtons_button_on: isOn,
mx_LegacyCallViewButtons_button_off: !isOn, mx_LegacyCallViewButtons_button_off: !isOn,
}); });
const title = forceHide ? undefined : isOn ? onLabel : offLabel;
return ( return (
<AccessibleTooltipButton <AccessibleButton
ref={ref} ref={ref}
className={classes} className={classes}
title={isOn ? onLabel : offLabel} title={title}
alignment={Alignment.Top} placement="top"
onTooltipOpenChange={onHover}
{...props} {...props}
> >
{children} {children}
</AccessibleTooltipButton> </AccessibleButton>
); );
}, },
); );
@ -265,7 +268,7 @@ export default class LegacyCallViewButtons extends React.Component<IProps, IStat
onClick={this.onDialpadClick} onClick={this.onDialpadClick}
isExpanded={this.state.showDialpad} isExpanded={this.state.showDialpad}
title={_t("voip|dialpad")} title={_t("voip|dialpad")}
alignment={Alignment.Top} placement="top"
/> />
)} )}
<LegacyCallViewDropdownButton <LegacyCallViewDropdownButton
@ -311,14 +314,14 @@ export default class LegacyCallViewButtons extends React.Component<IProps, IStat
ref={this.contextMenuButton} ref={this.contextMenuButton}
isExpanded={this.state.showMoreMenu} isExpanded={this.state.showMoreMenu}
title={_t("voip|more_button")} title={_t("voip|more_button")}
alignment={Alignment.Top} placement="top"
/> />
)} )}
<AccessibleTooltipButton <AccessibleButton
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup" className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
onClick={this.props.handlers.onHangupClick} onClick={this.props.handlers.onHangupClick}
title={_t("voip|hangup")} title={_t("voip|hangup")}
alignment={Alignment.Top} placement="top"
/> />
</div> </div>
); );

View file

@ -19,7 +19,7 @@ import React from "react";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import RoomAvatar from "../../avatars/RoomAvatar"; import RoomAvatar from "../../avatars/RoomAvatar";
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; import AccessibleButton from "../../elements/AccessibleButton";
interface LegacyCallControlsProps { interface LegacyCallControlsProps {
onExpand?: () => void; onExpand?: () => void;
@ -31,21 +31,21 @@ const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExp
return ( return (
<div className="mx_LegacyCallViewHeader_controls"> <div className="mx_LegacyCallViewHeader_controls">
{onMaximize && ( {onMaximize && (
<AccessibleTooltipButton <AccessibleButton
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen" className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
onClick={onMaximize} onClick={onMaximize}
title={_t("voip|maximise")} title={_t("voip|maximise")}
/> />
)} )}
{onPin && ( {onPin && (
<AccessibleTooltipButton <AccessibleButton
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin" className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
onClick={onPin} onClick={onPin}
title={_t("action|pin")} title={_t("action|pin")}
/> />
)} )}
{onExpand && ( {onExpand && (
<AccessibleTooltipButton <AccessibleButton
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_expand" className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_expand"
onClick={onExpand} onClick={onExpand}
title={_t("voip|expand")} title={_t("voip|expand")}

View file

@ -36,13 +36,12 @@ import {
LiveContentType, LiveContentType,
} from "../components/views/rooms/LiveContentSummary"; } from "../components/views/rooms/LiveContentSummary";
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall"; import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
import { ButtonEvent } from "../components/views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton";
import { useDispatcher } from "../hooks/useDispatcher"; import { useDispatcher } from "../hooks/useDispatcher";
import { ActionPayload } from "../dispatcher/payloads"; import { ActionPayload } from "../dispatcher/payloads";
import { Call } from "../models/Call"; import { Call } from "../models/Call";
import { AudioID } from "../LegacyCallHandler"; import { AudioID } from "../LegacyCallHandler";
import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter"; import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter";
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
import { CallStore, CallStoreEvent } from "../stores/CallStore"; import { CallStore, CallStoreEvent } from "../stores/CallStore";
export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`;
@ -195,7 +194,7 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element {
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined} disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
/> />
</div> </div>
<AccessibleTooltipButton <AccessibleButton
className="mx_IncomingCallToast_closeButton" className="mx_IncomingCallToast_closeButton"
onClick={onCloseClick} onClick={onCloseClick}
title={_t("action|close")} title={_t("action|close")}

View file

@ -25,7 +25,6 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../LegacyCallHandler"
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import RoomAvatar from "../components/views/avatars/RoomAvatar"; import RoomAvatar from "../components/views/avatars/RoomAvatar";
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton";
export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`; export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`;
@ -136,7 +135,7 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>
<AccessibleTooltipButton <AccessibleButton
className={silenceClass} className={silenceClass}
disabled={callForcedSilent} disabled={callForcedSilent}
onClick={this.onSilenceClick} onClick={this.onSilenceClick}

View file

@ -29,7 +29,6 @@ import Spinner from "../../../components/views/elements/Spinner";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton";
interface VoiceBroadcastHeaderProps { interface VoiceBroadcastHeaderProps {
linkToRoom?: boolean; linkToRoom?: boolean;
@ -95,14 +94,14 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
}); });
const microphoneLine = microphoneLabel && ( const microphoneLine = microphoneLabel && (
<AccessibleTooltipButton <AccessibleButton
className={microphoneLineClasses} className={microphoneLineClasses}
onClick={onMicrophoneLineClick} onClick={onMicrophoneLineClick}
title={_t("voip|change_input_device")} title={_t("voip|change_input_device")}
> >
<MicrophoneIcon className="mx_Icon mx_Icon_16" /> <MicrophoneIcon className="mx_Icon mx_Icon_16" />
<span>{microphoneLabel}</span> <span>{microphoneLabel}</span>
</AccessibleTooltipButton> </AccessibleButton>
); );
const onRoomAvatarOrNameClick = (): void => { const onRoomAvatarOrNameClick = (): void => {

View file

@ -32,7 +32,7 @@ import { Icon as MicrophoneIcon } from "../../../../res/img/compound/mic-16px.sv
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection";
import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu";
import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton"; import AccessibleButton from "../../../components/views/elements/AccessibleButton";
interface VoiceBroadcastRecordingPipProps { interface VoiceBroadcastRecordingPipProps {
recording: VoiceBroadcastRecording; recording: VoiceBroadcastRecording;
@ -92,12 +92,12 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
) : ( ) : (
<div className="mx_VoiceBroadcastBody_controls"> <div className="mx_VoiceBroadcastBody_controls">
{toggleControl} {toggleControl}
<AccessibleTooltipButton <AccessibleButton
onClick={(): void => setShowDeviceSelect(true)} onClick={(): void => setShowDeviceSelect(true)}
title={_t("voip|change_input_device")} title={_t("voip|change_input_device")}
> >
<MicrophoneIcon className="mx_Icon mx_Icon_16 mx_Icon_alert" /> <MicrophoneIcon className="mx_Icon mx_Icon_16 mx_Icon_alert" />
</AccessibleTooltipButton> </AccessibleButton>
<VoiceBroadcastControl <VoiceBroadcastControl
icon={<StopIcon className="mx_Icon mx_Icon_16" />} icon={<StopIcon className="mx_Icon mx_Icon_16" />}
label="Stop Recording" label="Stop Recording"

View file

@ -0,0 +1,67 @@
/*
*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
import React from "react";
import { render } from "@testing-library/react";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import LegacyCallViewButtons from "../../../../../src/components/views/voip/LegacyCallView/LegacyCallViewButtons";
import { createTestClient } from "../../../../test-utils";
describe("LegacyCallViewButtons", () => {
const matrixClient = createTestClient();
const roomId = "test-room-id";
const renderButtons = () => {
const call = new MatrixCall({
client: matrixClient,
roomId,
});
return render(
<LegacyCallViewButtons
call={call}
handlers={{
onScreenshareClick: jest.fn(),
onToggleSidebarClick: jest.fn(),
onHangupClick: jest.fn(),
onMicMuteClick: jest.fn(),
onVidMuteClick: jest.fn(),
}}
buttonsVisibility={{
vidMute: true,
screensharing: true,
sidebar: true,
contextMenu: true,
dialpad: true,
}}
buttonsState={{
micMuted: false,
vidMuted: false,
sidebarShown: false,
screensharing: false,
}}
/>,
);
};
it("should render the buttons", () => {
const { container } = renderButtons();
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LegacyCallViewButtons should render the buttons 1`] = `
<div>
<div
class="mx_LegacyCallViewButtons"
>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Dialpad"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dialpad"
role="button"
tabindex="0"
/>
<div
aria-label="Mute microphone"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_mic mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
>
<div
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dropdownButton mx_LegacyCallViewButtons_dropdownButton_collapsed mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
/>
</div>
<div
aria-label="Turn off camera"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_vid mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
>
<div
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dropdownButton mx_LegacyCallViewButtons_dropdownButton_collapsed mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
/>
</div>
<div
aria-label="Start sharing your screen"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_screensharing mx_LegacyCallViewButtons_button_off"
role="button"
tabindex="0"
/>
<div
aria-label="Show sidebar"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_sidebar mx_LegacyCallViewButtons_button_off"
role="button"
tabindex="0"
/>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="More"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_more"
role="button"
tabindex="0"
/>
<div
aria-label="Hangup"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
role="button"
tabindex="0"
/>
</div>
</div>
`;

View file

@ -271,6 +271,7 @@ export function createTestClient(): MatrixClient {
getMediaConfig: jest.fn(), getMediaConfig: jest.fn(),
baseUrl: "https://matrix-client.matrix.org", baseUrl: "https://matrix-client.matrix.org",
matrixRTC: createStubMatrixRTC(), matrixRTC: createStubMatrixRTC(),
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
} as unknown as MatrixClient; } as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client); client.reEmitter = new ReEmitter(client);