From 6ec81d90d22d868e651ebb2d92ed82cb4975090e Mon Sep 17 00:00:00 2001 From: zskhan Date: Thu, 4 Dec 2025 15:57:00 +0100 Subject: [PATCH 1/3] feat: add aria label and also title on the close screen share icon --- src/i18n/en-US.json | 1 + src/script/components/calling/ChooseScreen.tsx | 2 ++ src/types/i18n.d.ts | 11 ++++++----- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index b913bb0e214..42a8c8a1336 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -329,6 +329,7 @@ "callAccept": "Accept", "callChooseSharedScreen": "Choose a screen to share", "callChooseSharedWindow": "Choose a window to share", + "callChooseScreenCancel": "Close-screen sharing", "callConversationAcceptOrDecline": "{conversationName} is calling. Press control + enter to accept the call or press control + shift + enter to decline the call.", "callDecline": "Decline", "callDegradationAction": "OK", diff --git a/src/script/components/calling/ChooseScreen.tsx b/src/script/components/calling/ChooseScreen.tsx index fdc28ff23f4..317d56ce2f6 100644 --- a/src/script/components/calling/ChooseScreen.tsx +++ b/src/script/components/calling/ChooseScreen.tsx @@ -84,6 +84,8 @@ function ChooseScreen({choose, callState = container.resolve(CallState)}: Choose className="choose-screen-controls-button button-round button-round-dark button-round-md icon-close" data-uie-name="do-choose-screen-cancel" onClick={cancel} + aria-label={t('callChooseScreenCancel')} + title={t('callChooseScreenCancel')} > diff --git a/src/types/i18n.d.ts b/src/types/i18n.d.ts index 7bdae519da5..121c99cc42a 100644 --- a/src/types/i18n.d.ts +++ b/src/types/i18n.d.ts @@ -274,8 +274,6 @@ declare module 'I18n/en-US.json' { 'authLoginTitle': `Log in`; 'authPlaceholderEmail': `Email`; 'authPlaceholderPassword': `Password`; - 'showTogglePasswordLabel': `Show password`; - 'hideTogglePasswordLabel': `Hide password`; 'authPostedResend': `Resend to {email}`; 'authPostedResendAction': `No email showing up?`; 'authPostedResendDetail': `Check your email inbox and follow the instructions.`; @@ -335,6 +333,7 @@ declare module 'I18n/en-US.json' { 'callAccept': `Accept`; 'callChooseSharedScreen': `Choose a screen to share`; 'callChooseSharedWindow': `Choose a window to share`; + 'callChooseScreenCancel': `Close-screen sharing`; 'callConversationAcceptOrDecline': `{conversationName} is calling. Press control + enter to accept the call or press control + shift + enter to decline the call.`; 'callDecline': `Decline`; 'callDegradationAction': `OK`; @@ -434,6 +433,7 @@ declare module 'I18n/en-US.json' { 'cells.options.delete': `Delete`; 'cells.options.deletePermanently': `Delete permanently`; 'cells.options.download': `Download`; + 'cells.options.edit': `Edit`; 'cells.options.label': `More options`; 'cells.options.move': `Move to folder`; 'cells.options.open': `Open`; @@ -441,7 +441,6 @@ declare module 'I18n/en-US.json' { 'cells.options.restore': `Restore`; 'cells.options.share': `Share`; 'cells.options.tags': `Add or Remove Tags`; - 'cells.options.edit': `Edit`; 'cells.pagination.loadMoreResults': `Load More Items`; 'cells.pagination.nextPage': `Next Page`; 'cells.pagination.previousPage': `Previous Page`; @@ -1094,6 +1093,7 @@ declare module 'I18n/en-US.json' { 'guestRoomToggleInfoExtended': `Open this conversation to people outside your team. You can always change it later.`; 'guestRoomToggleInfoHead': `Guest Links`; 'guestRoomToggleName': `Allow Guests`; + 'hideTogglePasswordLabel': `Hide password`; 'historyInfo.learnMore': `Learn more`; 'historyInfo.noHistoryHeadline': `It’s the first time you’re using {brandName} on this device.`; 'historyInfo.noHistoryInfo': `For privacy reasons, your history will not appear here.`; @@ -1825,6 +1825,7 @@ declare module 'I18n/en-US.json' { 'setPassword.button': `Set password`; 'setPassword.headline': `Set password`; 'setPassword.passwordPlaceholder': `Password`; + 'showTogglePasswordLabel': `Show password`; 'ssoLogin.codeInputPlaceholder': `SSO code`; 'ssoLogin.codeOrMailInputPlaceholder': `Email or SSO code`; 'ssoLogin.headline': `Company log in`; @@ -1975,11 +1976,11 @@ declare module 'I18n/en-US.json' { 'userRemainingTimeHours': `{time}h left`; 'userRemainingTimeMinutes': `Less than {time}m left`; 'verify.changeEmail': `Change email`; + 'verify.codeLabel': `Six-digit code`; + 'verify.codePlaceholder': `Input field, enter digit`; 'verify.headline': `You’ve got mail`; 'verify.resendCode': `Resend code`; 'verify.subhead': `Enter the six-digit verification code we sent to{newline}{email}`; - 'verify.codeLabel': `Six-digit code`; - 'verify.codePlaceholder': `Input field, enter digit`; 'videoCallMenuMoreAddReaction': `Add reaction`; 'videoCallMenuMoreAudioSettings': `Audio Settings`; 'videoCallMenuMoreChangeView': `Change view`; From baade052f5743526c194de086570ec1b0a444948 Mon Sep 17 00:00:00 2001 From: zskhan Date: Fri, 5 Dec 2025 00:40:00 +0100 Subject: [PATCH 2/3] feat: replace button with div and assigned role=button to it. Move inline css to styles.ts --- .../calling/GroupVideoGridTile.styles.ts | 99 +++++++++++++++++ .../components/calling/GroupVideoGridTile.tsx | 103 ++++-------------- 2 files changed, 123 insertions(+), 79 deletions(-) create mode 100644 src/script/components/calling/GroupVideoGridTile.styles.ts diff --git a/src/script/components/calling/GroupVideoGridTile.styles.ts b/src/script/components/calling/GroupVideoGridTile.styles.ts new file mode 100644 index 00000000000..b138f2e79a8 --- /dev/null +++ b/src/script/components/calling/GroupVideoGridTile.styles.ts @@ -0,0 +1,99 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +const participantNameColor = (isActivelySpeaking: boolean, isAudioEstablished: boolean) => { + if (!isAudioEstablished) { + return 'var(--gray-60)'; + } + if (isActivelySpeaking) { + return 'var(--app-bg-secondary)'; + } + return 'var(--white)'; +}; + +export const activelySpeakingBoxShadow = `inset 0px 0px 0px 1px var(--group-video-bg), inset 0px 0px 0px 4px var(--accent-color), inset 0px 0px 0px 7px var(--app-bg-secondary)`; + +export const groupVideoBoxShadow = (participantCount: number): string => + participantCount > 1 ? 'inset 0px 0px 0px 2px var(--group-video-bg)' : 'initial'; + +export const groupVideoTileWrapper: CSSObject = { + alignItems: 'center', + backgroundColor: 'var(--group-video-tile-bg)', + borderRadius: '10px', + display: 'flex', + height: '100%', + justifyContent: 'center', + width: '100%', +}; + +export const groupVideoActiveSpeakerTile = (isActivelySpeaking: boolean, participantCount: number): CSSObject => ({ + borderRadius: '8px', + bottom: 0, + boxShadow: isActivelySpeaking ? activelySpeakingBoxShadow : groupVideoBoxShadow(participantCount), + left: 0, + position: 'absolute', + right: 0, + top: 0, + transition: 'box-shadow 0.3s ease-in-out', +}); + +export const groupVideoActiveSpeaker = (isActivelySpeaking: boolean): CSSObject => ({ + filter: isActivelySpeaking ? 'brightness(0.8)' : 'none', +}); + +export const groupVideoParticipantNameWrapper = ( + isActivelySpeaking: boolean, + isAudioEstablished: boolean, +): CSSObject => ({ + overflow: 'hidden', + display: 'flex', + color: participantNameColor(isActivelySpeaking, isAudioEstablished), +}); + +export const groupVideoParticipantName: CSSObject = { + textOverflow: 'ellipsis', + overflow: 'hidden', +}; + +export const groupVideoParticipantAudioStatus = ( + isActivelySpeaking: boolean, + isAudioEstablished: boolean, +): CSSObject => { + const color = participantNameColor(isActivelySpeaking, isAudioEstablished); + return { + color: 'var(--participant-audio-connecting-color)', + flexShrink: 0, + '&::before': { + content: "' • '", + whiteSpace: 'pre', + color, + }, + }; +}; + +export const groupVideoElementVideo = (fitContain: boolean, mirrorSelf: boolean): CSSObject => ({ + objectFit: fitContain ? 'contain' : 'cover', + transform: mirrorSelf ? 'rotateY(180deg)' : 'initial', +}); + +export const groupVideoPauseOverlayLabel = (minimized: boolean): CSSObject => ({ + fontSize: minimized ? '0.6875rem' : '0.875rem', +}); diff --git a/src/script/components/calling/GroupVideoGridTile.tsx b/src/script/components/calling/GroupVideoGridTile.tsx index d64890cce34..045357a2361 100644 --- a/src/script/components/calling/GroupVideoGridTile.tsx +++ b/src/script/components/calling/GroupVideoGridTile.tsx @@ -25,6 +25,16 @@ import {VIDEO_STATE} from '@wireapp/avs'; import {TabIndex} from '@wireapp/react-ui-kit'; import {Avatar, AVATAR_SIZE} from 'Components/Avatar'; +import { + groupVideoActiveSpeaker, + groupVideoActiveSpeakerTile, + groupVideoElementVideo, + groupVideoParticipantAudioStatus, + groupVideoParticipantName, + groupVideoParticipantNameWrapper, + groupVideoPauseOverlayLabel, + groupVideoTileWrapper, +} from 'Components/calling/GroupVideoGridTile.styles'; import * as Icon from 'Components/Icon'; import type {Participant} from 'Repositories/calling/Participant'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; @@ -42,24 +52,6 @@ interface GroupVideoGridTileProps { selfParticipant: Participant; } -const getParticipantNameColor = ({ - isActivelySpeaking, - isAudioEstablished, -}: { - isActivelySpeaking: boolean; - isAudioEstablished: boolean; -}) => { - if (!isAudioEstablished) { - return 'var(--gray-60)'; - } - - if (isActivelySpeaking) { - return 'var(--app-bg-secondary)'; - } - - return 'var(--white)'; -}; - const GroupVideoGridTile = ({ minimized, participant, @@ -95,8 +87,6 @@ const GroupVideoGridTile = ({ const hasPausedVideo = videoState === VIDEO_STATE.PAUSED; const doVideoReconnecting = videoState === VIDEO_STATE.RECONNECTING; const hasActiveVideo = (sharesCamera || sharesScreen) && !!videoStream; - const activelySpeakingBoxShadow = `inset 0px 0px 0px 1px var(--group-video-bg), inset 0px 0px 0px 4px var(--accent-color), inset 0px 0px 0px 7px var(--app-bg-secondary)`; - const groupVideoBoxShadow = participantCount > 1 ? 'inset 0px 0px 0px 2px var(--group-video-bg)' : 'initial'; const handleTileClick = () => onTileDoubleClick(participant?.user.qualifiedId, participant?.clientId); @@ -106,45 +96,21 @@ const GroupVideoGridTile = ({ } }; - const participantNameColor = getParticipantNameColor({isActivelySpeaking, isAudioEstablished}); - const nameContainer = !minimized && ( -
+
{name} {!isAudioEstablished && ( - + {t('videoCallParticipantConnecting')} )} @@ -153,13 +119,16 @@ const GroupVideoGridTile = ({ ); return ( - +
); }; From fabdfa2fd870eb1b9b3b186463181a40babd5554 Mon Sep 17 00:00:00 2001 From: zskhan Date: Fri, 5 Dec 2025 01:26:23 +0100 Subject: [PATCH 3/3] feat: call controls were not working using keyboard. --- src/script/components/calling/GroupVideoGridTile.tsx | 6 ++++-- src/script/util/KeyboardUtil.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/script/components/calling/GroupVideoGridTile.tsx b/src/script/components/calling/GroupVideoGridTile.tsx index 045357a2361..afabceeb5a0 100644 --- a/src/script/components/calling/GroupVideoGridTile.tsx +++ b/src/script/components/calling/GroupVideoGridTile.tsx @@ -126,8 +126,10 @@ const GroupVideoGridTile = ({ onDoubleClick={handleTileClick} onKeyDown={handleEnterTileClick} role="button" - // minimized is passed only from CallingCell where we dont want to focus individual the tile on the tab press - tabIndex={!minimized || isMaximized ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE} + // minimized is passed only from CallingCell where we don't want to focus individual the tile on the tab press + tabIndex={ + (!minimized || isMaximized) && participant !== selfParticipant ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE + } aria-label={`Focus video ${participant?.user.id}`} > {hasActiveVideo ? ( diff --git a/src/script/util/KeyboardUtil.ts b/src/script/util/KeyboardUtil.ts index f4529fc4cfd..6aaef27f941 100644 --- a/src/script/util/KeyboardUtil.ts +++ b/src/script/util/KeyboardUtil.ts @@ -90,7 +90,15 @@ export const handleKeyDown = ({ keys: Array<(typeof KEY)[keyof typeof KEY]>; }) => { if (keys.includes(event.key as (typeof KEY)[keyof typeof KEY])) { + if ('preventDefault' in event) { + event.preventDefault(); + } + if ('stopPropagation' in event) { + event.stopPropagation(); + } + callback(event); + return true; } return true; };