Skip to content

Commit d2d4c31

Browse files
committed
feat: enhance video background settings with accessibility improvements and focus management
1 parent 986edeb commit d2d4c31

3 files changed

Lines changed: 100 additions & 25 deletions

File tree

apps/webapp/src/script/components/calling/FullscreenVideoCall.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {useToggleState} from 'src/script/hooks/useToggleState';
5959
import {CallViewTab} from 'src/script/view_model/CallingViewModel';
6060
import {useKoSubscribableChildren} from 'Util/componentUtil';
6161
import {isDetachedCallingFeatureEnabled} from 'Util/isDetachedCallingFeatureEnabled';
62-
import {handleKeyDown, KEY} from 'Util/keyboardUtil';
62+
import {handleKeyDown, isTabKey, KEY} from 'Util/keyboardUtil';
6363
import {t} from 'Util/localizerUtil';
6464
import {preventFocusOutside} from 'Util/util';
6565

@@ -254,10 +254,14 @@ const FullscreenVideoCall = ({
254254
return;
255255
}
256256

257+
if (!isTabKey(event)) {
258+
return;
259+
}
260+
257261
event.preventDefault();
258262
event.stopPropagation();
259263

260-
preventFocusOutside(event, 'video-calling', targetDocument);
264+
preventFocusOutside(event, 'video-calling-wrapper', targetDocument);
261265
};
262266

263267
targetDocument.addEventListener('keydown', onKeyDown);
@@ -301,6 +305,7 @@ const FullscreenVideoCall = ({
301305

302306
return (
303307
<div
308+
id="video-calling-wrapper"
304309
data-uie-name="fullscreen-video-call"
305310
className={cx('video-calling-wrapper', {
306311
'app--small-offset': hasOffset && isMiniMode,

apps/webapp/src/script/components/calling/VideoControls/VideoBackgroundSettings/VideoBackgroundSettings.styles.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export const sectionLabelStyles: CSSObject = {
7979
marginBottom: 8,
8080
};
8181

82+
export const closeButtonStyles: CSSObject = {
83+
'&:focus-visible': {
84+
outlineOffset: '0.4rem',
85+
},
86+
};
87+
8288
/** 2-column grid for blur and virtual background tiles. */
8389
export const tileGridStyles: CSSObject = {
8490
display: 'grid',
@@ -97,6 +103,10 @@ export const tileButtonStyles: CSSObject = {
97103
padding: 0,
98104
textAlign: 'center',
99105

106+
'&:focus-visible': {
107+
outline: 'none',
108+
},
109+
100110
'&:focus-visible .bg-tile__preview': {
101111
outline: '2px solid var(--accent-color-focus)',
102112
outlineOffset: 2,

apps/webapp/src/script/components/calling/VideoControls/VideoBackgroundSettings/VideoBackgroundSettings.tsx

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
*/
1919

20-
import {ChangeEvent, CSSProperties, ReactNode} from 'react';
20+
import {ChangeEvent, CSSProperties, ReactNode, useEffect, useId, useRef} from 'react';
2121

2222
import {BlurHighIcon, BlurLowIcon, Checkbox, CheckboxLabel, CircleIcon} from '@wireapp/react-ui-kit';
2323

@@ -31,6 +31,7 @@ import {
3131
backgroundSettingsScrollableContentStyles,
3232
backgroundSettingsTitleStyles,
3333
backgroundSettingsWrapperStyles,
34+
closeButtonStyles,
3435
sectionLabelStyles,
3536
tileButtonStyles,
3637
tileGridStyles,
@@ -60,10 +61,26 @@ const isEffectSelected = (selected: BackgroundEffectSelection, candidate: Backgr
6061
return true;
6162
};
6263

64+
const getBackgroundEffectLabel = (effect: BackgroundEffectSelection, backgrounds: BuiltinBackground[]): string => {
65+
switch (effect.type) {
66+
case 'none':
67+
return t('videoCallBackgroundNoEffect');
68+
case 'blur':
69+
return effect.level === 'low' ? t('videoCallBackgroundBlurLow') : t('videoCallBackgroundBlurHigh');
70+
case 'virtual': {
71+
const background = backgrounds.find(({id}) => id === effect.backgroundId);
72+
return background ? t(background.labelKey) : t('videoCallBackgroundVirtual');
73+
}
74+
default:
75+
return t('videoCallBackgroundEffectsLabel');
76+
}
77+
};
78+
6379
interface BackgroundTileProps {
6480
effect: BackgroundEffectSelection;
6581
selectedEffect: BackgroundEffectSelection;
6682
onSelectEffect: (effect: BackgroundEffectSelection) => void;
83+
ariaLabel: string;
6784
previewContent?: ReactNode;
6885
previewStyle?: CSSProperties;
6986
}
@@ -72,6 +89,7 @@ const BackgroundTile = ({
7289
effect,
7390
selectedEffect,
7491
onSelectEffect,
92+
ariaLabel,
7593
previewContent,
7694
previewStyle,
7795
}: BackgroundTileProps) => {
@@ -81,7 +99,9 @@ const BackgroundTile = ({
8199
type="button"
82100
css={tileButtonStyles}
83101
data-selected={selected}
84-
aria-pressed={selected}
102+
role="radio"
103+
aria-checked={selected}
104+
aria-label={ariaLabel}
85105
onClick={() => onSelectEffect(effect)}
86106
>
87107
<div css={tilePreviewStyles} style={previewStyle} className="bg-tile__preview">
@@ -99,25 +119,54 @@ export const VideoBackgroundSettings = ({
99119
onEnableHighQualityBlur,
100120
onClose,
101121
}: VideoBackgroundSettingsProps) => {
122+
const titleId = useId();
123+
const blurSectionId = useId();
124+
const virtualSectionId = useId();
125+
const closeButtonRef = useRef<HTMLButtonElement>(null);
126+
127+
useEffect(() => {
128+
closeButtonRef.current?.focus();
129+
}, []);
130+
102131
const handleEnableHighQualityBlur = (event: ChangeEvent<HTMLInputElement>) => {
103132
onEnableHighQualityBlur(event);
104133
};
105134

135+
const noneEffect: BackgroundEffectSelection = {type: 'none'};
136+
const lowBlurEffect: BackgroundEffectSelection = {type: 'blur', level: 'low'};
137+
const highBlurEffect: BackgroundEffectSelection = {type: 'blur', level: 'high'};
138+
106139
return (
107-
<div css={backgroundSettingsWrapperStyles} data-uie-name="video-background-settings">
140+
<div
141+
css={backgroundSettingsWrapperStyles}
142+
data-uie-name="video-background-settings"
143+
role="region"
144+
aria-labelledby={titleId}
145+
>
108146
<div css={backgroundSettingsHeaderStyles}>
109-
<span css={backgroundSettingsTitleStyles}>{t('videoCallBackgroundEffectsLabel')}</span>
110-
<button type="button" className="icon-button" onClick={onClose} title={t('modalCloseButton')}>
147+
<h2 id={titleId} css={backgroundSettingsTitleStyles}>
148+
{t('videoCallBackgroundEffectsLabel')}
149+
</h2>
150+
<button
151+
ref={closeButtonRef}
152+
type="button"
153+
css={closeButtonStyles}
154+
className="icon-button"
155+
onClick={onClose}
156+
aria-label={t('modalCloseButton')}
157+
title={t('modalCloseButton')}
158+
>
111159
<Icon.CloseIcon width={12} height={12} />
112160
</button>
113161
</div>
114162

115163
<FadingScrollbar css={backgroundSettingsScrollableContentStyles}>
116164
{/* No background effect — full-width tile */}
117165
<BackgroundTile
118-
effect={{type: 'none'}}
166+
effect={noneEffect}
119167
selectedEffect={selectedEffect}
120168
onSelectEffect={onSelectEffect}
169+
ariaLabel={getBackgroundEffectLabel(noneEffect, backgrounds)}
121170
previewContent={
122171
<div css={tilePreviewContentStyles}>
123172
<CircleIcon />
@@ -128,12 +177,15 @@ export const VideoBackgroundSettings = ({
128177

129178
{/* Blur section */}
130179
<div>
131-
<div css={sectionLabelStyles}>{t('videoCallBackgroundBlurSectionLabel')}</div>
132-
<div css={tileGridStyles}>
180+
<h3 id={blurSectionId} css={sectionLabelStyles}>
181+
{t('videoCallBackgroundBlurSectionLabel')}
182+
</h3>
183+
<div css={tileGridStyles} role="group" aria-labelledby={blurSectionId}>
133184
<BackgroundTile
134-
effect={{type: 'blur', level: 'low'}}
185+
effect={lowBlurEffect}
135186
selectedEffect={selectedEffect}
136187
onSelectEffect={onSelectEffect}
188+
ariaLabel={getBackgroundEffectLabel(lowBlurEffect, backgrounds)}
137189
previewContent={
138190
<div css={tilePreviewContentStyles}>
139191
<BlurLowIcon />
@@ -142,9 +194,10 @@ export const VideoBackgroundSettings = ({
142194
}
143195
/>
144196
<BackgroundTile
145-
effect={{type: 'blur', level: 'high'}}
197+
effect={highBlurEffect}
146198
selectedEffect={selectedEffect}
147199
onSelectEffect={onSelectEffect}
200+
ariaLabel={getBackgroundEffectLabel(highBlurEffect, backgrounds)}
148201
previewContent={
149202
<div css={tilePreviewContentStyles}>
150203
<BlurHighIcon />
@@ -171,19 +224,26 @@ export const VideoBackgroundSettings = ({
171224

172225
{/* Virtual backgrounds section */}
173226
<div>
174-
<div css={sectionLabelStyles}>{t('videoCallBackgroundVirtualSectionLabel')}</div>
175-
<div css={tileGridStyles}>
176-
{backgrounds.map(background => (
177-
<BackgroundTile
178-
key={background.id}
179-
effect={{type: 'virtual', backgroundId: background.id}}
180-
selectedEffect={selectedEffect}
181-
onSelectEffect={onSelectEffect}
182-
previewStyle={{
183-
backgroundImage: `url(${background.imageUrl}), ${background.previewGradient}`,
184-
}}
185-
/>
186-
))}
227+
<h3 id={virtualSectionId} css={sectionLabelStyles}>
228+
{t('videoCallBackgroundVirtualSectionLabel')}
229+
</h3>
230+
<div css={tileGridStyles} role="group" aria-labelledby={virtualSectionId}>
231+
{backgrounds.map(background => {
232+
const virtualEffect: BackgroundEffectSelection = {type: 'virtual', backgroundId: background.id};
233+
234+
return (
235+
<BackgroundTile
236+
key={background.id}
237+
effect={virtualEffect}
238+
selectedEffect={selectedEffect}
239+
onSelectEffect={onSelectEffect}
240+
ariaLabel={getBackgroundEffectLabel(virtualEffect, backgrounds)}
241+
previewStyle={{
242+
backgroundImage: `url(${background.imageUrl}), ${background.previewGradient}`,
243+
}}
244+
/>
245+
);
246+
})}
187247
</div>
188248
</div>
189249
</FadingScrollbar>

0 commit comments

Comments
 (0)