瀏覽代碼

Make (most) UI elements reachable via keyboard (#12657)

feat(a11y): make (most) UI elements reachable via keyboard
factor2
Emmanuel Pelletier 2 年之前
父節點
當前提交
c81777a475
No account linked to committer's email address
共有 24 個文件被更改,包括 247 次插入32 次删除
  1. 2
    0
      lang/main.json
  2. 5
    2
      react/features/base/components/participants-pane-list/ListItem.tsx
  3. 16
    2
      react/features/base/label/components/web/Label.tsx
  4. 36
    3
      react/features/base/popover/components/Popover.web.tsx
  5. 1
    2
      react/features/base/react/components/web/MeetingsList.js
  6. 1
    1
      react/features/base/toolbox/components/ToolboxItem.web.js
  7. 1
    1
      react/features/base/toolbox/components/web/OverflowMenuItem.js
  8. 1
    0
      react/features/base/toolbox/components/web/ToolboxButtonWithIconPopup.js
  9. 1
    1
      react/features/base/toolbox/components/web/ToolboxItem.js
  10. 3
    1
      react/features/base/ui/components/web/BaseDialog.tsx
  11. 1
    1
      react/features/base/ui/components/web/ContextMenu.tsx
  12. 2
    1
      react/features/base/ui/components/web/ContextMenuItem.tsx
  13. 24
    1
      react/features/conference/components/web/ConferenceInfo.js
  14. 2
    1
      react/features/connection-indicator/components/web/ConnectionIndicator.tsx
  15. 107
    8
      react/features/filmstrip/components/web/Thumbnail.tsx
  16. 3
    3
      react/features/filmstrip/components/web/styles.ts
  17. 1
    0
      react/features/settings/components/web/audio/AudioSettingsPopup.tsx
  18. 1
    0
      react/features/settings/components/web/video/VideoSettingsPopup.tsx
  19. 33
    3
      react/features/toolbox/components/web/Drawer.tsx
  20. 2
    1
      react/features/toolbox/components/web/HangupMenuButton.tsx
  21. 1
    0
      react/features/toolbox/components/web/OverflowMenuButton.tsx
  22. 1
    0
      react/features/toolbox/components/web/Toolbox.tsx
  23. 1
    0
      react/features/video-menu/components/web/LocalVideoMenuTriggerButton.tsx
  24. 1
    0
      react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.tsx

+ 2
- 0
lang/main.json 查看文件

@@ -773,6 +773,7 @@
773 773
     },
774 774
     "passwordDigitsOnly": "Up to {{number}} digits",
775 775
     "passwordSetRemotely": "Set by another participant",
776
+    "pinParticipant": "{{participantName}} - Pin",
776 777
     "pinnedParticipant": "The participant is pinned",
777 778
     "polls": {
778 779
         "answer": {
@@ -1249,6 +1250,7 @@
1249 1250
         "subtitlesOff": "Off",
1250 1251
         "tr": "TR"
1251 1252
     },
1253
+    "unpinParticipant": "{{participantName}} - Unpin",
1252 1254
     "userMedia": {
1253 1255
         "androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
1254 1256
         "chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",

+ 5
- 2
react/features/base/components/participants-pane-list/ListItem.tsx 查看文件

@@ -88,7 +88,7 @@ const useStyles = makeStyles()(theme => {
88 88
             boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)',
89 89
             minHeight: '40px',
90 90
 
91
-            '&:hover': {
91
+            '&:hover, &:focus-within': {
92 92
                 backgroundColor: theme.palette.ui02,
93 93
 
94 94
                 '& .indicators': {
@@ -97,6 +97,8 @@ const useStyles = makeStyles()(theme => {
97 97
 
98 98
                 '& .actions': {
99 99
                     display: 'flex',
100
+                    position: 'relative',
101
+                    top: 'auto',
100 102
                     boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
101 103
                     backgroundColor: theme.palette.ui02
102 104
                 }
@@ -154,7 +156,8 @@ const useStyles = makeStyles()(theme => {
154 156
         },
155 157
 
156 158
         actionsContainer: {
157
-            display: 'none',
159
+            position: 'absolute',
160
+            top: '-1000px',
158 161
             boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
159 162
             backgroundColor: theme.palette.ui02
160 163
         },

+ 16
- 2
react/features/base/label/components/web/Label.tsx 查看文件

@@ -1,4 +1,4 @@
1
-import React from 'react';
1
+import React, { useCallback } from 'react';
2 2
 import { makeStyles } from 'tss-react/mui';
3 3
 
4 4
 import Icon from '../../../icons/components/Icon';
@@ -92,13 +92,27 @@ const Label = ({
92 92
 }: IProps) => {
93 93
     const { classes, cx } = useStyles();
94 94
 
95
+    const onKeyPress = useCallback(event => {
96
+        if (!onClick) {
97
+            return;
98
+        }
99
+
100
+        if (event.key === 'Enter' || event.key === ' ') {
101
+            event.preventDefault();
102
+            onClick();
103
+        }
104
+    }, [ onClick ]);
105
+
95 106
     return (
96 107
         <div
97 108
             className = { cx(classes.label, onClick && classes.clickable,
98 109
                 color && classes[color], className
99 110
             ) }
100 111
             id = { id }
101
-            onClick = { onClick }>
112
+            onClick = { onClick }
113
+            onKeyPress = { onKeyPress }
114
+            role = { onClick ? 'button' : undefined }
115
+            tabIndex = { onClick ? 0 : undefined }>
102 116
             {icon && <Icon
103 117
                 color = { iconColor }
104 118
                 size = '16'

+ 36
- 3
react/features/base/popover/components/Popover.web.tsx 查看文件

@@ -1,4 +1,5 @@
1 1
 import React, { Component, ReactNode } from 'react';
2
+import ReactFocusLock from 'react-focus-lock';
2 3
 
3 4
 import { IReduxState } from '../../../app/types';
4 5
 import DialogPortal from '../../../toolbox/components/web/DialogPortal';
@@ -34,6 +35,18 @@ interface IProps {
34 35
      */
35 36
     disablePopover?: boolean;
36 37
 
38
+    /**
39
+     * The id of the dom element acting as the Popover label (matches aria-labelledby).
40
+     */
41
+    headingId?: string;
42
+
43
+    /**
44
+     * String acting as the Popover label (matches aria-label).
45
+     *
46
+     * If headingId is set, this will not be used.
47
+     */
48
+    headingLabel?: string;
49
+
37 50
     /**
38 51
      * An id attribute to apply to the root of the {@code Popover}
39 52
      * component.
@@ -186,7 +199,16 @@ class Popover extends Component<IProps, IState> {
186 199
      * @returns {ReactElement}
187 200
      */
188 201
     render() {
189
-        const { children, className, content, id, overflowDrawer, visible, trigger } = this.props;
202
+        const { children,
203
+            className,
204
+            content,
205
+            headingId,
206
+            headingLabel,
207
+            id,
208
+            overflowDrawer,
209
+            visible,
210
+            trigger
211
+        } = this.props;
190 212
 
191 213
         if (overflowDrawer) {
192 214
             return (
@@ -197,6 +219,7 @@ class Popover extends Component<IProps, IState> {
197 219
                     { children }
198 220
                     <JitsiPortal>
199 221
                         <Drawer
222
+                            headingId = { headingId }
200 223
                             isOpen = { visible }
201 224
                             onClose = { this._onHideDialog }>
202 225
                             { content }
@@ -214,7 +237,8 @@ class Popover extends Component<IProps, IState> {
214 237
                 onKeyPress = { this._onKeyPress }
215 238
                 { ...(trigger === 'hover' ? {
216 239
                     onMouseEnter: this._onShowDialog,
217
-                    onMouseLeave: this._onHideDialog
240
+                    onMouseLeave: this._onHideDialog,
241
+                    tabIndex: 0
218 242
                 } : {}) }
219 243
                 ref = { this._containerRef }>
220 244
                 { visible && (
@@ -222,7 +246,16 @@ class Popover extends Component<IProps, IState> {
222 246
                         getRef = { this._setContextMenuRef }
223 247
                         setSize = { this._setContextMenuStyle }
224 248
                         style = { this.state.contextMenuStyle }>
225
-                        {this._renderContent()}
249
+                        <ReactFocusLock
250
+                            lockProps = {{
251
+                                role: 'dialog',
252
+                                'aria-modal': true,
253
+                                'aria-labelledby': headingId,
254
+                                'aria-label': !headingId && headingLabel ? headingLabel : undefined
255
+                            }}
256
+                            returnFocus = { true }>
257
+                            {this._renderContent()}
258
+                        </ReactFocusLock>
226 259
                     </DialogPortal>
227 260
                 )}
228 261
                 { children }

+ 1
- 2
react/features/base/react/components/web/MeetingsList.js 查看文件

@@ -115,7 +115,6 @@ class MeetingsList extends Component<Props> {
115 115
                 <Container
116 116
                     aria-label = { t('welcomepage.recentList') }
117 117
                     className = 'meetings-list'
118
-                    role = 'menu'
119 118
                     tabIndex = '-1'>
120 119
                     {
121 120
                         meetings.length === 0
@@ -243,7 +242,7 @@ class MeetingsList extends Component<Props> {
243 242
                 key = { index }
244 243
                 onClick = { onPress }
245 244
                 onKeyPress = { onKeyPress }
246
-                role = 'menuitem'
245
+                role = 'button'
247 246
                 tabIndex = { 0 }>
248 247
                 <Container className = 'left-column'>
249 248
                     <Text className = 'title'>

+ 1
- 1
react/features/base/toolbox/components/ToolboxItem.web.js 查看文件

@@ -84,7 +84,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
84 84
             onKeyDown: disabled ? undefined : onKeyDown,
85 85
             onKeyPress: this._onKeyPress,
86 86
             tabIndex: 0,
87
-            role: showLabel ? 'menuitem' : 'button'
87
+            role: 'button'
88 88
         };
89 89
 
90 90
         const elementType = showLabel ? 'li' : 'div';

+ 1
- 1
react/features/base/toolbox/components/web/OverflowMenuItem.js 查看文件

@@ -123,7 +123,7 @@ class OverflowMenuItem extends Component<Props> {
123 123
                 className = { className }
124 124
                 onClick = { disabled ? null : onClick }
125 125
                 onKeyPress = { this._onKeyPress }
126
-                role = 'menuitem'
126
+                role = 'button'
127 127
                 tabIndex = { 0 }>
128 128
                 <span className = 'overflow-menu-item-icon'>
129 129
                     <Icon

+ 1
- 0
react/features/base/toolbox/components/web/ToolboxButtonWithIconPopup.js 查看文件

@@ -121,6 +121,7 @@ export default function ToolboxButtonWithIconPopup(props: Props) {
121 121
             <div className = 'settings-button-small-icon-container'>
122 122
                 <Popover
123 123
                     content = { popoverContent }
124
+                    headingLabel = { ariaLabel }
124 125
                     onPopoverClose = { onPopoverClose }
125 126
                     onPopoverOpen = { onPopoverOpen }
126 127
                     position = 'top'

+ 1
- 1
react/features/base/toolbox/components/web/ToolboxItem.js 查看文件

@@ -65,7 +65,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
65 65
             onClick: disabled ? undefined : onClick,
66 66
             onKeyPress: this._onKeyPress,
67 67
             tabIndex: 0,
68
-            role: showLabel ? 'menuitem' : 'button'
68
+            role: 'button'
69 69
         };
70 70
 
71 71
         const elementType = showLabel ? 'li' : 'div';

+ 3
- 1
react/features/base/ui/components/web/BaseDialog.tsx 查看文件

@@ -182,7 +182,9 @@ const BaseDialog = ({
182 182
             <div
183 183
                 className = { classes.backdrop }
184 184
                 onClick = { onBackdropClick } />
185
-            <FocusLock className = { classes.focusLock }>
185
+            <FocusLock
186
+                className = { classes.focusLock }
187
+                returnFocus = { true }>
186 188
                 <div
187 189
                     aria-describedby = { description }
188 190
                     aria-labelledby = { title ?? t(titleKey ?? '') }

+ 1
- 1
react/features/base/ui/components/web/ContextMenu.tsx 查看文件

@@ -262,7 +262,7 @@ const ContextMenu = ({
262 262
             onMouseEnter = { onMouseEnter }
263 263
             onMouseLeave = { onMouseLeave }
264 264
             ref = { containerRef }
265
-            role = { role ?? 'menu' }
265
+            role = { role }
266 266
             tabIndex = { tabIndex }>
267 267
             {children}
268 268
         </div>;

+ 2
- 1
react/features/base/ui/components/web/ContextMenuItem.tsx 查看文件

@@ -172,7 +172,8 @@ const ContextMenuItem = ({
172 172
             onClick = { disabled ? undefined : onClick }
173 173
             onKeyDown = { disabled ? undefined : onKeyDown }
174 174
             onKeyPress = { disabled ? undefined : onKeyPress }
175
-            role = 'menuitem'>
175
+            role = 'button'
176
+            tabIndex = { disabled ? undefined : 0 }>
176 177
             {customIcon ? customIcon
177 178
                 : icon && <Icon
178 179
                     className = { styles.contextMenuItemIcon }

+ 24
- 1
react/features/conference/components/web/ConferenceInfo.js 查看文件

@@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
9 9
 import E2EELabel from '../../../e2ee/components/E2EELabel';
10 10
 import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
11 11
 import RecordingLabel from '../../../recording/components/web/RecordingLabel';
12
+import { showToolbox } from '../../../toolbox/actions';
12 13
 import { isToolboxVisible } from '../../../toolbox/functions.web';
13 14
 import TranscribingLabel from '../../../transcribing/components/TranscribingLabel.web';
14 15
 import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
@@ -33,6 +34,11 @@ type Props = {
33 34
      */
34 35
     _conferenceInfo: Object,
35 36
 
37
+    /**
38
+     * Invoked to active other features of the app.
39
+     */
40
+    dispatch: Function;
41
+
36 42
     /**
37 43
      * Indicates whether the component should be visible or not.
38 44
      */
@@ -113,6 +119,21 @@ class ConferenceInfo extends Component<Props> {
113 119
 
114 120
         this._renderAutoHide = this._renderAutoHide.bind(this);
115 121
         this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
122
+        this._onTabIn = this._onTabIn.bind(this);
123
+    }
124
+
125
+    _onTabIn: () => void;
126
+
127
+    /**
128
+     * Callback invoked when the component is focused to show the conference
129
+     * info if necessary.
130
+     *
131
+     * @returns {void}
132
+     */
133
+    _onTabIn() {
134
+        if (this.props._conferenceInfo.autoHide?.length && !this.props._visible) {
135
+            this.props.dispatch(showToolbox());
136
+        }
116 137
     }
117 138
 
118 139
     _renderAutoHide: () => void;
@@ -181,7 +202,9 @@ class ConferenceInfo extends Component<Props> {
181 202
      */
182 203
     render() {
183 204
         return (
184
-            <div className = 'details-container' >
205
+            <div
206
+                className = 'details-container'
207
+                onFocus = { this._onTabIn }>
185 208
                 { this._renderAlwaysVisible() }
186 209
                 { this._renderAutoHide() }
187 210
             </div>

+ 2
- 1
react/features/connection-indicator/components/web/ConnectionIndicator.tsx 查看文件

@@ -216,7 +216,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
216 216
      */
217 217
     render() {
218 218
         // @ts-ignore
219
-        const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props;
219
+        const { enableStatsDisplay, participantId, statsPopoverPosition, classes, t } = this.props;
220 220
         const visibilityClass = this._getVisibilityClass();
221 221
 
222 222
         // @ts-ignore
@@ -233,6 +233,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
233 233
                     inheritedStats = { this.state.stats }
234 234
                     participantId = { participantId } /> }
235 235
                 disablePopover = { !enableStatsDisplay }
236
+                headingLabel = { t('videothumbnail.connectionInfo') }
236 237
                 id = 'participant-connection-indicator'
237 238
                 onPopoverClose = { this._onHidePopover }
238 239
                 onPopoverOpen = { this._onShowPopover }

+ 107
- 8
react/features/filmstrip/components/web/Thumbnail.tsx 查看文件

@@ -3,7 +3,8 @@ import { Theme } from '@mui/material';
3 3
 import { withStyles } from '@mui/styles';
4 4
 import clsx from 'clsx';
5 5
 import debounce from 'lodash/debounce';
6
-import React, { Component } from 'react';
6
+import React, { Component, KeyboardEvent, RefObject, createRef } from 'react';
7
+import { WithTranslation } from 'react-i18next';
7 8
 import { connect } from 'react-redux';
8 9
 
9 10
 import { createScreenSharingIssueEvent } from '../../../analytics/AnalyticsEvents';
@@ -12,6 +13,7 @@ import { IReduxState } from '../../../app/types';
12 13
 // @ts-ignore
13 14
 import { Avatar } from '../../../base/avatar';
14 15
 import { isMobileBrowser } from '../../../base/environment/utils';
16
+import { translate } from '../../../base/i18n/functions';
15 17
 import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
16 18
 // @ts-ignore
17 19
 import { VideoTrack } from '../../../base/media';
@@ -28,6 +30,8 @@ import {
28 30
 import { IParticipant } from '../../../base/participants/types';
29 31
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
30 32
 import { isTestModeEnabled } from '../../../base/testing/functions';
33
+// @ts-ignore
34
+import { Tooltip } from '../../../base/tooltip';
31 35
 import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions';
32 36
 import {
33 37
     getLocalAudioTrack,
@@ -97,7 +101,7 @@ export interface IState {
97 101
 /**
98 102
  * The type of the React {@code Component} props of {@link Thumbnail}.
99 103
  */
100
-export interface IProps {
104
+export interface IProps extends WithTranslation {
101 105
 
102 106
     /**
103 107
      * The audio track related to the participant.
@@ -372,6 +376,22 @@ const defaultStyles = (theme: Theme) => {
372 376
             height: '100%',
373 377
             backgroundColor: `${theme.palette.uiBackground}`,
374 378
             opacity: 0.8
379
+        },
380
+
381
+        keyboardPinButton: {
382
+            position: 'absolute' as const,
383
+            zIndex: 10,
384
+
385
+            /* this button is only for keyboard/screen reader users,
386
+            an onClick handler is already set elsewhere for mouse users, so make sure
387
+            we can't click on it */
388
+            pointerEvents: 'none' as const,
389
+
390
+            // make room for the border to correctly show up
391
+            left: '3px',
392
+            right: '3px',
393
+            bottom: '3px',
394
+            top: '3px'
375 395
         }
376 396
     };
377 397
 };
@@ -387,6 +407,11 @@ class Thumbnail extends Component<IProps, IState> {
387 407
      */
388 408
     timeoutHandle?: number;
389 409
 
410
+    /**
411
+     * Ref to the container of the thumbnail.
412
+     */
413
+    containerRef?: RefObject<HTMLSpanElement>;
414
+
390 415
     /**
391 416
      * Timeout used to detect double tapping.
392 417
      * It is active while user has tapped once.
@@ -414,10 +439,13 @@ class Thumbnail extends Component<IProps, IState> {
414 439
             displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
415 440
         };
416 441
         this.timeoutHandle = undefined;
417
-
442
+        this.containerRef = createRef<HTMLSpanElement>();
418 443
         this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
419 444
         this._onCanPlay = this._onCanPlay.bind(this);
420 445
         this._onClick = this._onClick.bind(this);
446
+        this._onTogglePinButtonKeyDown = this._onTogglePinButtonKeyDown.bind(this);
447
+        this._onFocus = this._onFocus.bind(this);
448
+        this._onBlur = this._onBlur.bind(this);
421 449
         this._onMouseEnter = this._onMouseEnter.bind(this);
422 450
         this._onMouseMove = debounce(this._onMouseMove.bind(this), 100, {
423 451
             leading: true,
@@ -731,6 +759,53 @@ class Thumbnail extends Component<IProps, IState> {
731 759
         }
732 760
     }
733 761
 
762
+    /**
763
+     * This is called as a onKeydown handler on the keyboard-only button to toggle pin.
764
+     *
765
+     * @param {KeyboardEvent} event - The keydown event.
766
+     * @returns {void}
767
+     */
768
+    _onTogglePinButtonKeyDown(event: KeyboardEvent) {
769
+        if (event.key === 'Enter' || event.key === ' ') {
770
+            this._onClick();
771
+        }
772
+    }
773
+
774
+    /**
775
+     * Keyboard focus handler.
776
+     *
777
+     * When navigating with keyboard, make things behave as we
778
+     * hover with the mouse, to make the UI show up.
779
+     *
780
+     * @returns {void}
781
+     */
782
+    _onFocus() {
783
+        this.setState({ isHovered: true });
784
+    }
785
+
786
+    /**
787
+     * Keyboard blur handler.
788
+     *
789
+     * When navigating with keyboard, make things behave as we
790
+     * hover with the mouse, to make the UI show up.
791
+     *
792
+     * @returns {void}
793
+     */
794
+    _onBlur() {
795
+        // we need this timeout trick so that we get the actual document.activeElement value
796
+        // instead of document.body
797
+        setTimeout(() => {
798
+            // we also explicitly check for popovers, because the thumbnail can show popovers,
799
+            // and they are not rendered in the thumbnail DOM element
800
+            if (
801
+                !this.containerRef?.current?.contains(document.activeElement)
802
+                && document.activeElement?.closest('.popover') === null
803
+            ) {
804
+                this.setState({ isHovered: false });
805
+            }
806
+        }, 0);
807
+    }
808
+
734 809
     /**
735 810
      * Mouse enter handler.
736 811
      *
@@ -808,21 +883,27 @@ class Thumbnail extends Component<IProps, IState> {
808 883
      * @returns {ReactElement}
809 884
      */
810 885
     _renderFakeParticipant() {
811
-        const { _isMobile, _participant: { avatarURL } } = this.props;
886
+        const { _isMobile, _participant: { avatarURL, pinned, name } } = this.props;
812 887
         const styles = this._getStyles();
813 888
         const containerClassName = this._getContainerClassName();
814 889
 
815 890
         return (
816 891
             <span
892
+                aria-label = { this.props.t(pinned ? 'unpinParticipant' : 'pinParticipant', {
893
+                    participantName: name
894
+                }) }
817 895
                 className = { containerClassName }
818 896
                 id = 'sharedVideoContainer'
819 897
                 onClick = { this._onClick }
898
+                onKeyDown = { this._onTogglePinButtonKeyDown }
820 899
                 { ...(_isMobile ? {} : {
821 900
                     onMouseEnter: this._onMouseEnter,
822 901
                     onMouseMove: this._onMouseMove,
823 902
                     onMouseLeave: this._onMouseLeave
824 903
                 }) }
825
-                style = { styles.thumbnail }>
904
+                role = 'button'
905
+                style = { styles.thumbnail }
906
+                tabIndex = { 0 }>
826 907
                 {avatarURL ? (
827 908
                     <img
828 909
                         className = 'sharedVideoAvatar'
@@ -981,9 +1062,10 @@ class Thumbnail extends Component<IProps, IState> {
981 1062
             _thumbnailType,
982 1063
             _videoTrack,
983 1064
             classes,
984
-            filmstripType
1065
+            filmstripType,
1066
+            t
985 1067
         } = this.props;
986
-        const { id } = _participant || {};
1068
+        const { id, name, pinned } = _participant || {};
987 1069
         const { isHovered, popoverVisible } = this.state;
988 1070
         const styles = this._getStyles();
989 1071
         let containerClassName = this._getContainerClassName();
@@ -992,6 +1074,9 @@ class Thumbnail extends Component<IProps, IState> {
992 1074
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
993 1075
         const videoTrackId = jitsiVideoTrack?.getId();
994 1076
         const videoEventListeners: any = {};
1077
+        const pinButtonLabel = t(pinned ? 'unpinParticipant' : 'pinParticipant', {
1078
+            participantName: name
1079
+        });
995 1080
 
996 1081
         if (local) {
997 1082
             if (_isMobilePortrait) {
@@ -1022,6 +1107,8 @@ class Thumbnail extends Component<IProps, IState> {
1022 1107
                     ? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1023 1108
                     : `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1024 1109
                 }
1110
+                onBlur = { this._onBlur }
1111
+                onFocus = { this._onFocus }
1025 1112
                 { ...(_isMobile
1026 1113
                     ? {
1027 1114
                         onTouchEnd: this._onTouchEnd,
@@ -1035,7 +1122,19 @@ class Thumbnail extends Component<IProps, IState> {
1035 1122
                         onMouseLeave: this._onMouseLeave
1036 1123
                     }
1037 1124
                 ) }
1125
+                ref = { this.containerRef }
1038 1126
                 style = { styles.thumbnail }>
1127
+                {/* this "button" is invisible, only here so that
1128
+                keyboard/screen reader users can pin/unpin */}
1129
+                <Tooltip
1130
+                    content = { pinButtonLabel }>
1131
+                    <span
1132
+                        aria-label = { pinButtonLabel }
1133
+                        className = { classes.keyboardPinButton }
1134
+                        onKeyDown = { this._onTogglePinButtonKeyDown }
1135
+                        role = 'button'
1136
+                        tabIndex = { 0 } />
1137
+                </Tooltip>
1039 1138
                 {!_gifSrc && (local
1040 1139
                     ? <span id = 'localVideoWrapper'>{video}</span>
1041 1140
                     : video)}
@@ -1322,4 +1421,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any): Object {
1322 1421
     };
1323 1422
 }
1324 1423
 
1325
-export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail));
1424
+export default connect(_mapStateToProps)(withStyles(defaultStyles)(translate(Thumbnail)));

+ 3
- 3
react/features/filmstrip/components/web/styles.ts 查看文件

@@ -26,7 +26,7 @@ export const styles = (theme: Theme) => {
26 26
             transition: 'opacity .3s',
27 27
             zIndex: 1,
28 28
 
29
-            '&:hover': {
29
+            '&:hover, &:focus-within': {
30 30
                 backgroundColor: theme.palette.ui02
31 31
             }
32 32
         },
@@ -70,7 +70,7 @@ export const styles = (theme: Theme) => {
70 70
             right: 0,
71 71
             bottom: 0,
72 72
 
73
-            '&:hover': {
73
+            '&:hover, &:focus-within': {
74 74
                 '& .resizable-filmstrip': {
75 75
                     backgroundColor: BACKGROUND_COLOR
76 76
                 },
@@ -106,7 +106,7 @@ export const styles = (theme: Theme) => {
106 106
         filmstripBackground: {
107 107
             backgroundColor: theme.palette.uiBackground,
108 108
 
109
-            '&:hover': {
109
+            '&:hover, &:focus-within': {
110 110
                 backgroundColor: theme.palette.uiBackground
111 111
             }
112 112
         },

+ 1
- 0
react/features/settings/components/web/audio/AudioSettingsPopup.tsx 查看文件

@@ -75,6 +75,7 @@ function AudioSettingsPopup({
75 75
                     outputDevices = { outputDevices }
76 76
                     setAudioInputDevice = { setAudioInputDevice }
77 77
                     setAudioOutputDevice = { setAudioOutputDevice } /> }
78
+                headingId = 'audio-settings-button'
78 79
                 onPopoverClose = { onClose }
79 80
                 position = { popupPlacement }
80 81
                 trigger = 'click'

+ 1
- 0
react/features/settings/components/web/video/VideoSettingsPopup.tsx 查看文件

@@ -62,6 +62,7 @@ function VideoSettingsPopup({
62 62
                     setVideoInputDevice = { setVideoInputDevice }
63 63
                     toggleVideoSettings = { onClose }
64 64
                     videoDeviceIds = { videoDeviceIds } /> }
65
+                headingId = 'video-settings-button'
65 66
                 onPopoverClose = { onClose }
66 67
                 position = { popupPlacement }
67 68
                 trigger = 'click'

+ 33
- 3
react/features/toolbox/components/web/Drawer.tsx 查看文件

@@ -1,4 +1,5 @@
1
-import React, { ReactNode, useCallback } from 'react';
1
+import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
2
+import ReactFocusLock from 'react-focus-lock';
2 3
 import { makeStyles } from 'tss-react/mui';
3 4
 
4 5
 import { DRAWER_MAX_HEIGHT } from '../../constants';
@@ -16,6 +17,11 @@ interface IProps {
16 17
      */
17 18
     className?: string;
18 19
 
20
+    /**
21
+     * The id of the dom element acting as the Drawer label.
22
+     */
23
+    headingId?: string;
24
+
19 25
     /**
20 26
      * Whether the drawer should be shown or not.
21 27
      */
@@ -45,6 +51,7 @@ const useStyles = makeStyles()(theme => {
45 51
 function Drawer({
46 52
     children,
47 53
     className = '',
54
+    headingId,
48 55
     isOpen,
49 56
     onClose
50 57
 }: IProps) {
@@ -71,15 +78,38 @@ function Drawer({
71 78
         onClose?.();
72 79
     }, [ onClose ]);
73 80
 
81
+    /**
82
+     * Handles pressing the escape key, closing the drawer.
83
+     *
84
+     * @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
85
+     * @returns {void}
86
+     */
87
+    const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
88
+        if (event.key === 'Escape') {
89
+            event.preventDefault();
90
+            event.stopPropagation();
91
+            onClose?.();
92
+        }
93
+    }, [ onClose ]);
94
+
74 95
     return (
75 96
         isOpen ? (
76 97
             <div
77 98
                 className = 'drawer-menu-container'
78
-                onClick = { handleOutsideClick }>
99
+                onClick = { handleOutsideClick }
100
+                onKeyDown = { handleEscKey }>
79 101
                 <div
80 102
                     className = { `drawer-menu ${styles.drawer} ${className}` }
81 103
                     onClick = { handleInsideClick }>
82
-                    {children}
104
+                    <ReactFocusLock
105
+                        lockProps = {{
106
+                            role: 'dialog',
107
+                            'aria-modal': true,
108
+                            'aria-labelledby': `#${headingId}`
109
+                        }}
110
+                        returnFocus = { true }>
111
+                        {children}
112
+                    </ReactFocusLock>
83 113
                 </div>
84 114
             </div>
85 115
         ) : null

+ 2
- 1
react/features/toolbox/components/web/HangupMenuButton.tsx 查看文件

@@ -83,12 +83,13 @@ class HangupMenuButton extends Component<IProps> {
83 83
      * @returns {ReactElement}
84 84
      */
85 85
     render() {
86
-        const { children, isOpen } = this.props;
86
+        const { children, isOpen, t } = this.props;
87 87
 
88 88
         return (
89 89
             <div className = 'toolbox-button-wth-dialog context-menu'>
90 90
                 <Popover
91 91
                     content = { children }
92
+                    headingLabel = { t('toolbar.accessibilityLabel.hangup') }
92 93
                     onPopoverClose = { this._onCloseDialog }
93 94
                     position = 'top'
94 95
                     trigger = 'click'

+ 1
- 0
react/features/toolbox/components/web/OverflowMenuButton.tsx 查看文件

@@ -123,6 +123,7 @@ const OverflowMenuButton = ({
123 123
                 ) : (
124 124
                     <Popover
125 125
                         content = { children }
126
+                        headingId = 'overflow-context-menu'
126 127
                         onPopoverClose = { onCloseDialog }
127 128
                         onPopoverOpen = { onOpenDialog }
128 129
                         position = 'top'

+ 1
- 0
react/features/toolbox/components/web/Toolbox.tsx 查看文件

@@ -1466,6 +1466,7 @@ class Toolbox extends Component<IProps> {
1466 1466
                                     accessibilityLabel = { t(toolbarAccLabel) }
1467 1467
                                     className = { classes.contextMenu }
1468 1468
                                     hidden = { false }
1469
+                                    id = 'overflow-context-menu'
1469 1470
                                     inDrawer = { _overflowDrawer }
1470 1471
                                     onKeyDown = { this._onEscKey }>
1471 1472
                                     {overflowMenuButtons.reduce((acc, val) => {

+ 1
- 0
react/features/video-menu/components/web/LocalVideoMenuTriggerButton.tsx 查看文件

@@ -216,6 +216,7 @@ class LocalVideoMenuTriggerButton extends Component<IProps> {
216 216
             isMobileBrowser() || _showLocalVideoFlipButton || _showHideSelfViewButton
217 217
                 ? <Popover
218 218
                     content = { content }
219
+                    headingLabel = { t('dialog.localUserControls') }
219 220
                     id = 'local-video-menu-trigger'
220 221
                     onPopoverClose = { this._onPopoverClose }
221 222
                     onPopoverOpen = { this._onPopoverOpen }

+ 1
- 0
react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.tsx 查看文件

@@ -190,6 +190,7 @@ class RemoteVideoMenuTriggerButton extends Component<IProps> {
190 190
         return (
191 191
             <Popover
192 192
                 content = { content }
193
+                headingLabel = { this.props.t('dialog.remoteUserControls', { username }) }
193 194
                 id = 'remote-video-menu-trigger'
194 195
                 onPopoverClose = { this._onPopoverClose }
195 196
                 onPopoverOpen = { this._onPopoverOpen }

Loading…
取消
儲存