Selaa lähdekoodia

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

feat(a11y): make (most) UI elements reachable via keyboard
factor2
Emmanuel Pelletier 2 vuotta sitten
vanhempi
commit
c81777a475
No account linked to committer's email address
24 muutettua tiedostoa jossa 247 lisäystä ja 32 poistoa
  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 Näytä tiedosto

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

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

+ 16
- 2
react/features/base/label/components/web/Label.tsx Näytä tiedosto

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

+ 36
- 3
react/features/base/popover/components/Popover.web.tsx Näytä tiedosto

1
 import React, { Component, ReactNode } from 'react';
1
 import React, { Component, ReactNode } from 'react';
2
+import ReactFocusLock from 'react-focus-lock';
2
 
3
 
3
 import { IReduxState } from '../../../app/types';
4
 import { IReduxState } from '../../../app/types';
4
 import DialogPortal from '../../../toolbox/components/web/DialogPortal';
5
 import DialogPortal from '../../../toolbox/components/web/DialogPortal';
34
      */
35
      */
35
     disablePopover?: boolean;
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
      * An id attribute to apply to the root of the {@code Popover}
51
      * An id attribute to apply to the root of the {@code Popover}
39
      * component.
52
      * component.
186
      * @returns {ReactElement}
199
      * @returns {ReactElement}
187
      */
200
      */
188
     render() {
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
         if (overflowDrawer) {
213
         if (overflowDrawer) {
192
             return (
214
             return (
197
                     { children }
219
                     { children }
198
                     <JitsiPortal>
220
                     <JitsiPortal>
199
                         <Drawer
221
                         <Drawer
222
+                            headingId = { headingId }
200
                             isOpen = { visible }
223
                             isOpen = { visible }
201
                             onClose = { this._onHideDialog }>
224
                             onClose = { this._onHideDialog }>
202
                             { content }
225
                             { content }
214
                 onKeyPress = { this._onKeyPress }
237
                 onKeyPress = { this._onKeyPress }
215
                 { ...(trigger === 'hover' ? {
238
                 { ...(trigger === 'hover' ? {
216
                     onMouseEnter: this._onShowDialog,
239
                     onMouseEnter: this._onShowDialog,
217
-                    onMouseLeave: this._onHideDialog
240
+                    onMouseLeave: this._onHideDialog,
241
+                    tabIndex: 0
218
                 } : {}) }
242
                 } : {}) }
219
                 ref = { this._containerRef }>
243
                 ref = { this._containerRef }>
220
                 { visible && (
244
                 { visible && (
222
                         getRef = { this._setContextMenuRef }
246
                         getRef = { this._setContextMenuRef }
223
                         setSize = { this._setContextMenuStyle }
247
                         setSize = { this._setContextMenuStyle }
224
                         style = { this.state.contextMenuStyle }>
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
                     </DialogPortal>
259
                     </DialogPortal>
227
                 )}
260
                 )}
228
                 { children }
261
                 { children }

+ 1
- 2
react/features/base/react/components/web/MeetingsList.js Näytä tiedosto

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

+ 1
- 1
react/features/base/toolbox/components/ToolboxItem.web.js Näytä tiedosto

84
             onKeyDown: disabled ? undefined : onKeyDown,
84
             onKeyDown: disabled ? undefined : onKeyDown,
85
             onKeyPress: this._onKeyPress,
85
             onKeyPress: this._onKeyPress,
86
             tabIndex: 0,
86
             tabIndex: 0,
87
-            role: showLabel ? 'menuitem' : 'button'
87
+            role: 'button'
88
         };
88
         };
89
 
89
 
90
         const elementType = showLabel ? 'li' : 'div';
90
         const elementType = showLabel ? 'li' : 'div';

+ 1
- 1
react/features/base/toolbox/components/web/OverflowMenuItem.js Näytä tiedosto

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

+ 1
- 0
react/features/base/toolbox/components/web/ToolboxButtonWithIconPopup.js Näytä tiedosto

121
             <div className = 'settings-button-small-icon-container'>
121
             <div className = 'settings-button-small-icon-container'>
122
                 <Popover
122
                 <Popover
123
                     content = { popoverContent }
123
                     content = { popoverContent }
124
+                    headingLabel = { ariaLabel }
124
                     onPopoverClose = { onPopoverClose }
125
                     onPopoverClose = { onPopoverClose }
125
                     onPopoverOpen = { onPopoverOpen }
126
                     onPopoverOpen = { onPopoverOpen }
126
                     position = 'top'
127
                     position = 'top'

+ 1
- 1
react/features/base/toolbox/components/web/ToolboxItem.js Näytä tiedosto

65
             onClick: disabled ? undefined : onClick,
65
             onClick: disabled ? undefined : onClick,
66
             onKeyPress: this._onKeyPress,
66
             onKeyPress: this._onKeyPress,
67
             tabIndex: 0,
67
             tabIndex: 0,
68
-            role: showLabel ? 'menuitem' : 'button'
68
+            role: 'button'
69
         };
69
         };
70
 
70
 
71
         const elementType = showLabel ? 'li' : 'div';
71
         const elementType = showLabel ? 'li' : 'div';

+ 3
- 1
react/features/base/ui/components/web/BaseDialog.tsx Näytä tiedosto

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

+ 1
- 1
react/features/base/ui/components/web/ContextMenu.tsx Näytä tiedosto

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

+ 2
- 1
react/features/base/ui/components/web/ContextMenuItem.tsx Näytä tiedosto

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

+ 24
- 1
react/features/conference/components/web/ConferenceInfo.js Näytä tiedosto

9
 import E2EELabel from '../../../e2ee/components/E2EELabel';
9
 import E2EELabel from '../../../e2ee/components/E2EELabel';
10
 import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
10
 import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
11
 import RecordingLabel from '../../../recording/components/web/RecordingLabel';
11
 import RecordingLabel from '../../../recording/components/web/RecordingLabel';
12
+import { showToolbox } from '../../../toolbox/actions';
12
 import { isToolboxVisible } from '../../../toolbox/functions.web';
13
 import { isToolboxVisible } from '../../../toolbox/functions.web';
13
 import TranscribingLabel from '../../../transcribing/components/TranscribingLabel.web';
14
 import TranscribingLabel from '../../../transcribing/components/TranscribingLabel.web';
14
 import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
15
 import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
33
      */
34
      */
34
     _conferenceInfo: Object,
35
     _conferenceInfo: Object,
35
 
36
 
37
+    /**
38
+     * Invoked to active other features of the app.
39
+     */
40
+    dispatch: Function;
41
+
36
     /**
42
     /**
37
      * Indicates whether the component should be visible or not.
43
      * Indicates whether the component should be visible or not.
38
      */
44
      */
113
 
119
 
114
         this._renderAutoHide = this._renderAutoHide.bind(this);
120
         this._renderAutoHide = this._renderAutoHide.bind(this);
115
         this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
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
     _renderAutoHide: () => void;
139
     _renderAutoHide: () => void;
181
      */
202
      */
182
     render() {
203
     render() {
183
         return (
204
         return (
184
-            <div className = 'details-container' >
205
+            <div
206
+                className = 'details-container'
207
+                onFocus = { this._onTabIn }>
185
                 { this._renderAlwaysVisible() }
208
                 { this._renderAlwaysVisible() }
186
                 { this._renderAutoHide() }
209
                 { this._renderAutoHide() }
187
             </div>
210
             </div>

+ 2
- 1
react/features/connection-indicator/components/web/ConnectionIndicator.tsx Näytä tiedosto

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

+ 107
- 8
react/features/filmstrip/components/web/Thumbnail.tsx Näytä tiedosto

3
 import { withStyles } from '@mui/styles';
3
 import { withStyles } from '@mui/styles';
4
 import clsx from 'clsx';
4
 import clsx from 'clsx';
5
 import debounce from 'lodash/debounce';
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
 import { connect } from 'react-redux';
8
 import { connect } from 'react-redux';
8
 
9
 
9
 import { createScreenSharingIssueEvent } from '../../../analytics/AnalyticsEvents';
10
 import { createScreenSharingIssueEvent } from '../../../analytics/AnalyticsEvents';
12
 // @ts-ignore
13
 // @ts-ignore
13
 import { Avatar } from '../../../base/avatar';
14
 import { Avatar } from '../../../base/avatar';
14
 import { isMobileBrowser } from '../../../base/environment/utils';
15
 import { isMobileBrowser } from '../../../base/environment/utils';
16
+import { translate } from '../../../base/i18n/functions';
15
 import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
17
 import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
16
 // @ts-ignore
18
 // @ts-ignore
17
 import { VideoTrack } from '../../../base/media';
19
 import { VideoTrack } from '../../../base/media';
28
 import { IParticipant } from '../../../base/participants/types';
30
 import { IParticipant } from '../../../base/participants/types';
29
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
31
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
30
 import { isTestModeEnabled } from '../../../base/testing/functions';
32
 import { isTestModeEnabled } from '../../../base/testing/functions';
33
+// @ts-ignore
34
+import { Tooltip } from '../../../base/tooltip';
31
 import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions';
35
 import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions';
32
 import {
36
 import {
33
     getLocalAudioTrack,
37
     getLocalAudioTrack,
97
 /**
101
 /**
98
  * The type of the React {@code Component} props of {@link Thumbnail}.
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
      * The audio track related to the participant.
107
      * The audio track related to the participant.
372
             height: '100%',
376
             height: '100%',
373
             backgroundColor: `${theme.palette.uiBackground}`,
377
             backgroundColor: `${theme.palette.uiBackground}`,
374
             opacity: 0.8
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
      */
407
      */
388
     timeoutHandle?: number;
408
     timeoutHandle?: number;
389
 
409
 
410
+    /**
411
+     * Ref to the container of the thumbnail.
412
+     */
413
+    containerRef?: RefObject<HTMLSpanElement>;
414
+
390
     /**
415
     /**
391
      * Timeout used to detect double tapping.
416
      * Timeout used to detect double tapping.
392
      * It is active while user has tapped once.
417
      * It is active while user has tapped once.
414
             displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
439
             displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
415
         };
440
         };
416
         this.timeoutHandle = undefined;
441
         this.timeoutHandle = undefined;
417
-
442
+        this.containerRef = createRef<HTMLSpanElement>();
418
         this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
443
         this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
419
         this._onCanPlay = this._onCanPlay.bind(this);
444
         this._onCanPlay = this._onCanPlay.bind(this);
420
         this._onClick = this._onClick.bind(this);
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
         this._onMouseEnter = this._onMouseEnter.bind(this);
449
         this._onMouseEnter = this._onMouseEnter.bind(this);
422
         this._onMouseMove = debounce(this._onMouseMove.bind(this), 100, {
450
         this._onMouseMove = debounce(this._onMouseMove.bind(this), 100, {
423
             leading: true,
451
             leading: true,
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
      * Mouse enter handler.
810
      * Mouse enter handler.
736
      *
811
      *
808
      * @returns {ReactElement}
883
      * @returns {ReactElement}
809
      */
884
      */
810
     _renderFakeParticipant() {
885
     _renderFakeParticipant() {
811
-        const { _isMobile, _participant: { avatarURL } } = this.props;
886
+        const { _isMobile, _participant: { avatarURL, pinned, name } } = this.props;
812
         const styles = this._getStyles();
887
         const styles = this._getStyles();
813
         const containerClassName = this._getContainerClassName();
888
         const containerClassName = this._getContainerClassName();
814
 
889
 
815
         return (
890
         return (
816
             <span
891
             <span
892
+                aria-label = { this.props.t(pinned ? 'unpinParticipant' : 'pinParticipant', {
893
+                    participantName: name
894
+                }) }
817
                 className = { containerClassName }
895
                 className = { containerClassName }
818
                 id = 'sharedVideoContainer'
896
                 id = 'sharedVideoContainer'
819
                 onClick = { this._onClick }
897
                 onClick = { this._onClick }
898
+                onKeyDown = { this._onTogglePinButtonKeyDown }
820
                 { ...(_isMobile ? {} : {
899
                 { ...(_isMobile ? {} : {
821
                     onMouseEnter: this._onMouseEnter,
900
                     onMouseEnter: this._onMouseEnter,
822
                     onMouseMove: this._onMouseMove,
901
                     onMouseMove: this._onMouseMove,
823
                     onMouseLeave: this._onMouseLeave
902
                     onMouseLeave: this._onMouseLeave
824
                 }) }
903
                 }) }
825
-                style = { styles.thumbnail }>
904
+                role = 'button'
905
+                style = { styles.thumbnail }
906
+                tabIndex = { 0 }>
826
                 {avatarURL ? (
907
                 {avatarURL ? (
827
                     <img
908
                     <img
828
                         className = 'sharedVideoAvatar'
909
                         className = 'sharedVideoAvatar'
981
             _thumbnailType,
1062
             _thumbnailType,
982
             _videoTrack,
1063
             _videoTrack,
983
             classes,
1064
             classes,
984
-            filmstripType
1065
+            filmstripType,
1066
+            t
985
         } = this.props;
1067
         } = this.props;
986
-        const { id } = _participant || {};
1068
+        const { id, name, pinned } = _participant || {};
987
         const { isHovered, popoverVisible } = this.state;
1069
         const { isHovered, popoverVisible } = this.state;
988
         const styles = this._getStyles();
1070
         const styles = this._getStyles();
989
         let containerClassName = this._getContainerClassName();
1071
         let containerClassName = this._getContainerClassName();
992
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
1074
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
993
         const videoTrackId = jitsiVideoTrack?.getId();
1075
         const videoTrackId = jitsiVideoTrack?.getId();
994
         const videoEventListeners: any = {};
1076
         const videoEventListeners: any = {};
1077
+        const pinButtonLabel = t(pinned ? 'unpinParticipant' : 'pinParticipant', {
1078
+            participantName: name
1079
+        });
995
 
1080
 
996
         if (local) {
1081
         if (local) {
997
             if (_isMobilePortrait) {
1082
             if (_isMobilePortrait) {
1022
                     ? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1107
                     ? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1023
                     : `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1108
                     : `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
1024
                 }
1109
                 }
1110
+                onBlur = { this._onBlur }
1111
+                onFocus = { this._onFocus }
1025
                 { ...(_isMobile
1112
                 { ...(_isMobile
1026
                     ? {
1113
                     ? {
1027
                         onTouchEnd: this._onTouchEnd,
1114
                         onTouchEnd: this._onTouchEnd,
1035
                         onMouseLeave: this._onMouseLeave
1122
                         onMouseLeave: this._onMouseLeave
1036
                     }
1123
                     }
1037
                 ) }
1124
                 ) }
1125
+                ref = { this.containerRef }
1038
                 style = { styles.thumbnail }>
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
                 {!_gifSrc && (local
1138
                 {!_gifSrc && (local
1040
                     ? <span id = 'localVideoWrapper'>{video}</span>
1139
                     ? <span id = 'localVideoWrapper'>{video}</span>
1041
                     : video)}
1140
                     : video)}
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 Näytä tiedosto

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

+ 1
- 0
react/features/settings/components/web/audio/AudioSettingsPopup.tsx Näytä tiedosto

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

+ 1
- 0
react/features/settings/components/web/video/VideoSettingsPopup.tsx Näytä tiedosto

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

+ 33
- 3
react/features/toolbox/components/web/Drawer.tsx Näytä tiedosto

1
-import React, { ReactNode, useCallback } from 'react';
1
+import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
2
+import ReactFocusLock from 'react-focus-lock';
2
 import { makeStyles } from 'tss-react/mui';
3
 import { makeStyles } from 'tss-react/mui';
3
 
4
 
4
 import { DRAWER_MAX_HEIGHT } from '../../constants';
5
 import { DRAWER_MAX_HEIGHT } from '../../constants';
16
      */
17
      */
17
     className?: string;
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
      * Whether the drawer should be shown or not.
26
      * Whether the drawer should be shown or not.
21
      */
27
      */
45
 function Drawer({
51
 function Drawer({
46
     children,
52
     children,
47
     className = '',
53
     className = '',
54
+    headingId,
48
     isOpen,
55
     isOpen,
49
     onClose
56
     onClose
50
 }: IProps) {
57
 }: IProps) {
71
         onClose?.();
78
         onClose?.();
72
     }, [ onClose ]);
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
     return (
95
     return (
75
         isOpen ? (
96
         isOpen ? (
76
             <div
97
             <div
77
                 className = 'drawer-menu-container'
98
                 className = 'drawer-menu-container'
78
-                onClick = { handleOutsideClick }>
99
+                onClick = { handleOutsideClick }
100
+                onKeyDown = { handleEscKey }>
79
                 <div
101
                 <div
80
                     className = { `drawer-menu ${styles.drawer} ${className}` }
102
                     className = { `drawer-menu ${styles.drawer} ${className}` }
81
                     onClick = { handleInsideClick }>
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
                 </div>
113
                 </div>
84
             </div>
114
             </div>
85
         ) : null
115
         ) : null

+ 2
- 1
react/features/toolbox/components/web/HangupMenuButton.tsx Näytä tiedosto

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

+ 1
- 0
react/features/toolbox/components/web/OverflowMenuButton.tsx Näytä tiedosto

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

+ 1
- 0
react/features/toolbox/components/web/Toolbox.tsx Näytä tiedosto

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

+ 1
- 0
react/features/video-menu/components/web/LocalVideoMenuTriggerButton.tsx Näytä tiedosto

216
             isMobileBrowser() || _showLocalVideoFlipButton || _showHideSelfViewButton
216
             isMobileBrowser() || _showLocalVideoFlipButton || _showHideSelfViewButton
217
                 ? <Popover
217
                 ? <Popover
218
                     content = { content }
218
                     content = { content }
219
+                    headingLabel = { t('dialog.localUserControls') }
219
                     id = 'local-video-menu-trigger'
220
                     id = 'local-video-menu-trigger'
220
                     onPopoverClose = { this._onPopoverClose }
221
                     onPopoverClose = { this._onPopoverClose }
221
                     onPopoverOpen = { this._onPopoverOpen }
222
                     onPopoverOpen = { this._onPopoverOpen }

+ 1
- 0
react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.tsx Näytä tiedosto

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

Loading…
Peruuta
Tallenna