Make (most) UI elements reachable via keyboard (#12657)
feat(a11y): make (most) UI elements reachable via keyboard
This commit is contained in:
parent
778bca3031
commit
c81777a475
|
@ -773,6 +773,7 @@
|
|||
},
|
||||
"passwordDigitsOnly": "Up to {{number}} digits",
|
||||
"passwordSetRemotely": "Set by another participant",
|
||||
"pinParticipant": "{{participantName}} - Pin",
|
||||
"pinnedParticipant": "The participant is pinned",
|
||||
"polls": {
|
||||
"answer": {
|
||||
|
@ -1249,6 +1250,7 @@
|
|||
"subtitlesOff": "Off",
|
||||
"tr": "TR"
|
||||
},
|
||||
"unpinParticipant": "{{participantName}} - Unpin",
|
||||
"userMedia": {
|
||||
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
|
|
|
@ -88,7 +88,7 @@ const useStyles = makeStyles()(theme => {
|
|||
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)',
|
||||
minHeight: '40px',
|
||||
|
||||
'&:hover': {
|
||||
'&:hover, &:focus-within': {
|
||||
backgroundColor: theme.palette.ui02,
|
||||
|
||||
'& .indicators': {
|
||||
|
@ -97,6 +97,8 @@ const useStyles = makeStyles()(theme => {
|
|||
|
||||
'& .actions': {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
top: 'auto',
|
||||
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
|
||||
backgroundColor: theme.palette.ui02
|
||||
}
|
||||
|
@ -154,7 +156,8 @@ const useStyles = makeStyles()(theme => {
|
|||
},
|
||||
|
||||
actionsContainer: {
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
top: '-1000px',
|
||||
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
|
||||
backgroundColor: theme.palette.ui02
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
|
@ -92,13 +92,27 @@ const Label = ({
|
|||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const onKeyPress = useCallback(event => {
|
||||
if (!onClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}, [ onClick ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.label, onClick && classes.clickable,
|
||||
color && classes[color], className
|
||||
) }
|
||||
id = { id }
|
||||
onClick = { onClick }>
|
||||
onClick = { onClick }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = { onClick ? 'button' : undefined }
|
||||
tabIndex = { onClick ? 0 : undefined }>
|
||||
{icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { Component, ReactNode } from 'react';
|
||||
import ReactFocusLock from 'react-focus-lock';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import DialogPortal from '../../../toolbox/components/web/DialogPortal';
|
||||
|
@ -34,6 +35,18 @@ interface IProps {
|
|||
*/
|
||||
disablePopover?: boolean;
|
||||
|
||||
/**
|
||||
* The id of the dom element acting as the Popover label (matches aria-labelledby).
|
||||
*/
|
||||
headingId?: string;
|
||||
|
||||
/**
|
||||
* String acting as the Popover label (matches aria-label).
|
||||
*
|
||||
* If headingId is set, this will not be used.
|
||||
*/
|
||||
headingLabel?: string;
|
||||
|
||||
/**
|
||||
* An id attribute to apply to the root of the {@code Popover}
|
||||
* component.
|
||||
|
@ -186,7 +199,16 @@ class Popover extends Component<IProps, IState> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { children, className, content, id, overflowDrawer, visible, trigger } = this.props;
|
||||
const { children,
|
||||
className,
|
||||
content,
|
||||
headingId,
|
||||
headingLabel,
|
||||
id,
|
||||
overflowDrawer,
|
||||
visible,
|
||||
trigger
|
||||
} = this.props;
|
||||
|
||||
if (overflowDrawer) {
|
||||
return (
|
||||
|
@ -197,6 +219,7 @@ class Popover extends Component<IProps, IState> {
|
|||
{ children }
|
||||
<JitsiPortal>
|
||||
<Drawer
|
||||
headingId = { headingId }
|
||||
isOpen = { visible }
|
||||
onClose = { this._onHideDialog }>
|
||||
{ content }
|
||||
|
@ -214,7 +237,8 @@ class Popover extends Component<IProps, IState> {
|
|||
onKeyPress = { this._onKeyPress }
|
||||
{ ...(trigger === 'hover' ? {
|
||||
onMouseEnter: this._onShowDialog,
|
||||
onMouseLeave: this._onHideDialog
|
||||
onMouseLeave: this._onHideDialog,
|
||||
tabIndex: 0
|
||||
} : {}) }
|
||||
ref = { this._containerRef }>
|
||||
{ visible && (
|
||||
|
@ -222,7 +246,16 @@ class Popover extends Component<IProps, IState> {
|
|||
getRef = { this._setContextMenuRef }
|
||||
setSize = { this._setContextMenuStyle }
|
||||
style = { this.state.contextMenuStyle }>
|
||||
<ReactFocusLock
|
||||
lockProps = {{
|
||||
role: 'dialog',
|
||||
'aria-modal': true,
|
||||
'aria-labelledby': headingId,
|
||||
'aria-label': !headingId && headingLabel ? headingLabel : undefined
|
||||
}}
|
||||
returnFocus = { true }>
|
||||
{this._renderContent()}
|
||||
</ReactFocusLock>
|
||||
</DialogPortal>
|
||||
)}
|
||||
{ children }
|
||||
|
|
|
@ -115,7 +115,6 @@ class MeetingsList extends Component<Props> {
|
|||
<Container
|
||||
aria-label = { t('welcomepage.recentList') }
|
||||
className = 'meetings-list'
|
||||
role = 'menu'
|
||||
tabIndex = '-1'>
|
||||
{
|
||||
meetings.length === 0
|
||||
|
@ -243,7 +242,7 @@ class MeetingsList extends Component<Props> {
|
|||
key = { index }
|
||||
onClick = { onPress }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = 'menuitem'
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Container className = 'left-column'>
|
||||
<Text className = 'title'>
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
|||
onKeyDown: disabled ? undefined : onKeyDown,
|
||||
onKeyPress: this._onKeyPress,
|
||||
tabIndex: 0,
|
||||
role: showLabel ? 'menuitem' : 'button'
|
||||
role: 'button'
|
||||
};
|
||||
|
||||
const elementType = showLabel ? 'li' : 'div';
|
||||
|
|
|
@ -123,7 +123,7 @@ class OverflowMenuItem extends Component<Props> {
|
|||
className = { className }
|
||||
onClick = { disabled ? null : onClick }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'menuitem'
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<span className = 'overflow-menu-item-icon'>
|
||||
<Icon
|
||||
|
|
|
@ -121,6 +121,7 @@ export default function ToolboxButtonWithIconPopup(props: Props) {
|
|||
<div className = 'settings-button-small-icon-container'>
|
||||
<Popover
|
||||
content = { popoverContent }
|
||||
headingLabel = { ariaLabel }
|
||||
onPopoverClose = { onPopoverClose }
|
||||
onPopoverOpen = { onPopoverOpen }
|
||||
position = 'top'
|
||||
|
|
|
@ -65,7 +65,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
|||
onClick: disabled ? undefined : onClick,
|
||||
onKeyPress: this._onKeyPress,
|
||||
tabIndex: 0,
|
||||
role: showLabel ? 'menuitem' : 'button'
|
||||
role: 'button'
|
||||
};
|
||||
|
||||
const elementType = showLabel ? 'li' : 'div';
|
||||
|
|
|
@ -182,7 +182,9 @@ const BaseDialog = ({
|
|||
<div
|
||||
className = { classes.backdrop }
|
||||
onClick = { onBackdropClick } />
|
||||
<FocusLock className = { classes.focusLock }>
|
||||
<FocusLock
|
||||
className = { classes.focusLock }
|
||||
returnFocus = { true }>
|
||||
<div
|
||||
aria-describedby = { description }
|
||||
aria-labelledby = { title ?? t(titleKey ?? '') }
|
||||
|
|
|
@ -262,7 +262,7 @@ const ContextMenu = ({
|
|||
onMouseEnter = { onMouseEnter }
|
||||
onMouseLeave = { onMouseLeave }
|
||||
ref = { containerRef }
|
||||
role = { role ?? 'menu' }
|
||||
role = { role }
|
||||
tabIndex = { tabIndex }>
|
||||
{children}
|
||||
</div>;
|
||||
|
|
|
@ -172,7 +172,8 @@ const ContextMenuItem = ({
|
|||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||
role = 'menuitem'>
|
||||
role = 'button'
|
||||
tabIndex = { disabled ? undefined : 0 }>
|
||||
{customIcon ? customIcon
|
||||
: icon && <Icon
|
||||
className = { styles.contextMenuItemIcon }
|
||||
|
|
|
@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
|
|||
import E2EELabel from '../../../e2ee/components/E2EELabel';
|
||||
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/web/RecordingLabel';
|
||||
import { showToolbox } from '../../../toolbox/actions';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import TranscribingLabel from '../../../transcribing/components/TranscribingLabel.web';
|
||||
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
|
||||
|
@ -33,6 +34,11 @@ type Props = {
|
|||
*/
|
||||
_conferenceInfo: Object,
|
||||
|
||||
/**
|
||||
* Invoked to active other features of the app.
|
||||
*/
|
||||
dispatch: Function;
|
||||
|
||||
/**
|
||||
* Indicates whether the component should be visible or not.
|
||||
*/
|
||||
|
@ -113,6 +119,21 @@ class ConferenceInfo extends Component<Props> {
|
|||
|
||||
this._renderAutoHide = this._renderAutoHide.bind(this);
|
||||
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
|
||||
this._onTabIn = this._onTabIn.bind(this);
|
||||
}
|
||||
|
||||
_onTabIn: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when the component is focused to show the conference
|
||||
* info if necessary.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabIn() {
|
||||
if (this.props._conferenceInfo.autoHide?.length && !this.props._visible) {
|
||||
this.props.dispatch(showToolbox());
|
||||
}
|
||||
}
|
||||
|
||||
_renderAutoHide: () => void;
|
||||
|
@ -181,7 +202,9 @@ class ConferenceInfo extends Component<Props> {
|
|||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = 'details-container' >
|
||||
<div
|
||||
className = 'details-container'
|
||||
onFocus = { this._onTabIn }>
|
||||
{ this._renderAlwaysVisible() }
|
||||
{ this._renderAutoHide() }
|
||||
</div>
|
||||
|
|
|
@ -216,7 +216,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
|
|||
*/
|
||||
render() {
|
||||
// @ts-ignore
|
||||
const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props;
|
||||
const { enableStatsDisplay, participantId, statsPopoverPosition, classes, t } = this.props;
|
||||
const visibilityClass = this._getVisibilityClass();
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -233,6 +233,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
|
|||
inheritedStats = { this.state.stats }
|
||||
participantId = { participantId } /> }
|
||||
disablePopover = { !enableStatsDisplay }
|
||||
headingLabel = { t('videothumbnail.connectionInfo') }
|
||||
id = 'participant-connection-indicator'
|
||||
onPopoverClose = { this._onHidePopover }
|
||||
onPopoverOpen = { this._onShowPopover }
|
||||
|
|
|
@ -3,7 +3,8 @@ import { Theme } from '@mui/material';
|
|||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, KeyboardEvent, RefObject, createRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createScreenSharingIssueEvent } from '../../../analytics/AnalyticsEvents';
|
||||
|
@ -12,6 +13,7 @@ import { IReduxState } from '../../../app/types';
|
|||
// @ts-ignore
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
|
||||
// @ts-ignore
|
||||
import { VideoTrack } from '../../../base/media';
|
||||
|
@ -28,6 +30,8 @@ import {
|
|||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { isTestModeEnabled } from '../../../base/testing/functions';
|
||||
// @ts-ignore
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions';
|
||||
import {
|
||||
getLocalAudioTrack,
|
||||
|
@ -97,7 +101,7 @@ export interface IState {
|
|||
/**
|
||||
* The type of the React {@code Component} props of {@link Thumbnail}.
|
||||
*/
|
||||
export interface IProps {
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The audio track related to the participant.
|
||||
|
@ -372,6 +376,22 @@ const defaultStyles = (theme: Theme) => {
|
|||
height: '100%',
|
||||
backgroundColor: `${theme.palette.uiBackground}`,
|
||||
opacity: 0.8
|
||||
},
|
||||
|
||||
keyboardPinButton: {
|
||||
position: 'absolute' as const,
|
||||
zIndex: 10,
|
||||
|
||||
/* this button is only for keyboard/screen reader users,
|
||||
an onClick handler is already set elsewhere for mouse users, so make sure
|
||||
we can't click on it */
|
||||
pointerEvents: 'none' as const,
|
||||
|
||||
// make room for the border to correctly show up
|
||||
left: '3px',
|
||||
right: '3px',
|
||||
bottom: '3px',
|
||||
top: '3px'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -387,6 +407,11 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
*/
|
||||
timeoutHandle?: number;
|
||||
|
||||
/**
|
||||
* Ref to the container of the thumbnail.
|
||||
*/
|
||||
containerRef?: RefObject<HTMLSpanElement>;
|
||||
|
||||
/**
|
||||
* Timeout used to detect double tapping.
|
||||
* It is active while user has tapped once.
|
||||
|
@ -414,10 +439,13 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
|
||||
};
|
||||
this.timeoutHandle = undefined;
|
||||
|
||||
this.containerRef = createRef<HTMLSpanElement>();
|
||||
this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
|
||||
this._onCanPlay = this._onCanPlay.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onTogglePinButtonKeyDown = this._onTogglePinButtonKeyDown.bind(this);
|
||||
this._onFocus = this._onFocus.bind(this);
|
||||
this._onBlur = this._onBlur.bind(this);
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseMove = debounce(this._onMouseMove.bind(this), 100, {
|
||||
leading: true,
|
||||
|
@ -731,6 +759,53 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called as a onKeydown handler on the keyboard-only button to toggle pin.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The keydown event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTogglePinButtonKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
this._onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard focus handler.
|
||||
*
|
||||
* When navigating with keyboard, make things behave as we
|
||||
* hover with the mouse, to make the UI show up.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFocus() {
|
||||
this.setState({ isHovered: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard blur handler.
|
||||
*
|
||||
* When navigating with keyboard, make things behave as we
|
||||
* hover with the mouse, to make the UI show up.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onBlur() {
|
||||
// we need this timeout trick so that we get the actual document.activeElement value
|
||||
// instead of document.body
|
||||
setTimeout(() => {
|
||||
// we also explicitly check for popovers, because the thumbnail can show popovers,
|
||||
// and they are not rendered in the thumbnail DOM element
|
||||
if (
|
||||
!this.containerRef?.current?.contains(document.activeElement)
|
||||
&& document.activeElement?.closest('.popover') === null
|
||||
) {
|
||||
this.setState({ isHovered: false });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse enter handler.
|
||||
*
|
||||
|
@ -808,21 +883,27 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFakeParticipant() {
|
||||
const { _isMobile, _participant: { avatarURL } } = this.props;
|
||||
const { _isMobile, _participant: { avatarURL, pinned, name } } = this.props;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-label = { this.props.t(pinned ? 'unpinParticipant' : 'pinParticipant', {
|
||||
participantName: name
|
||||
}) }
|
||||
className = { containerClassName }
|
||||
id = 'sharedVideoContainer'
|
||||
onClick = { this._onClick }
|
||||
onKeyDown = { this._onTogglePinButtonKeyDown }
|
||||
{ ...(_isMobile ? {} : {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseMove: this._onMouseMove,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}) }
|
||||
style = { styles.thumbnail }>
|
||||
role = 'button'
|
||||
style = { styles.thumbnail }
|
||||
tabIndex = { 0 }>
|
||||
{avatarURL ? (
|
||||
<img
|
||||
className = 'sharedVideoAvatar'
|
||||
|
@ -981,9 +1062,10 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
_thumbnailType,
|
||||
_videoTrack,
|
||||
classes,
|
||||
filmstripType
|
||||
filmstripType,
|
||||
t
|
||||
} = this.props;
|
||||
const { id } = _participant || {};
|
||||
const { id, name, pinned } = _participant || {};
|
||||
const { isHovered, popoverVisible } = this.state;
|
||||
const styles = this._getStyles();
|
||||
let containerClassName = this._getContainerClassName();
|
||||
|
@ -992,6 +1074,9 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
|
||||
const videoTrackId = jitsiVideoTrack?.getId();
|
||||
const videoEventListeners: any = {};
|
||||
const pinButtonLabel = t(pinned ? 'unpinParticipant' : 'pinParticipant', {
|
||||
participantName: name
|
||||
});
|
||||
|
||||
if (local) {
|
||||
if (_isMobilePortrait) {
|
||||
|
@ -1022,6 +1107,8 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
|
||||
: `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
|
||||
}
|
||||
onBlur = { this._onBlur }
|
||||
onFocus = { this._onFocus }
|
||||
{ ...(_isMobile
|
||||
? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
|
@ -1035,7 +1122,19 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
onMouseLeave: this._onMouseLeave
|
||||
}
|
||||
) }
|
||||
ref = { this.containerRef }
|
||||
style = { styles.thumbnail }>
|
||||
{/* this "button" is invisible, only here so that
|
||||
keyboard/screen reader users can pin/unpin */}
|
||||
<Tooltip
|
||||
content = { pinButtonLabel }>
|
||||
<span
|
||||
aria-label = { pinButtonLabel }
|
||||
className = { classes.keyboardPinButton }
|
||||
onKeyDown = { this._onTogglePinButtonKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 } />
|
||||
</Tooltip>
|
||||
{!_gifSrc && (local
|
||||
? <span id = 'localVideoWrapper'>{video}</span>
|
||||
: video)}
|
||||
|
@ -1322,4 +1421,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any): Object {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail));
|
||||
export default connect(_mapStateToProps)(withStyles(defaultStyles)(translate(Thumbnail)));
|
||||
|
|
|
@ -26,7 +26,7 @@ export const styles = (theme: Theme) => {
|
|||
transition: 'opacity .3s',
|
||||
zIndex: 1,
|
||||
|
||||
'&:hover': {
|
||||
'&:hover, &:focus-within': {
|
||||
backgroundColor: theme.palette.ui02
|
||||
}
|
||||
},
|
||||
|
@ -70,7 +70,7 @@ export const styles = (theme: Theme) => {
|
|||
right: 0,
|
||||
bottom: 0,
|
||||
|
||||
'&:hover': {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
@ -106,7 +106,7 @@ export const styles = (theme: Theme) => {
|
|||
filmstripBackground: {
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
|
||||
'&:hover': {
|
||||
'&:hover, &:focus-within': {
|
||||
backgroundColor: theme.palette.uiBackground
|
||||
}
|
||||
},
|
||||
|
|
|
@ -75,6 +75,7 @@ function AudioSettingsPopup({
|
|||
outputDevices = { outputDevices }
|
||||
setAudioInputDevice = { setAudioInputDevice }
|
||||
setAudioOutputDevice = { setAudioOutputDevice } /> }
|
||||
headingId = 'audio-settings-button'
|
||||
onPopoverClose = { onClose }
|
||||
position = { popupPlacement }
|
||||
trigger = 'click'
|
||||
|
|
|
@ -62,6 +62,7 @@ function VideoSettingsPopup({
|
|||
setVideoInputDevice = { setVideoInputDevice }
|
||||
toggleVideoSettings = { onClose }
|
||||
videoDeviceIds = { videoDeviceIds } /> }
|
||||
headingId = 'video-settings-button'
|
||||
onPopoverClose = { onClose }
|
||||
position = { popupPlacement }
|
||||
trigger = 'click'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { ReactNode, useCallback } from 'react';
|
||||
import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
|
||||
import ReactFocusLock from 'react-focus-lock';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { DRAWER_MAX_HEIGHT } from '../../constants';
|
||||
|
@ -16,6 +17,11 @@ interface IProps {
|
|||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The id of the dom element acting as the Drawer label.
|
||||
*/
|
||||
headingId?: string;
|
||||
|
||||
/**
|
||||
* Whether the drawer should be shown or not.
|
||||
*/
|
||||
|
@ -45,6 +51,7 @@ const useStyles = makeStyles()(theme => {
|
|||
function Drawer({
|
||||
children,
|
||||
className = '',
|
||||
headingId,
|
||||
isOpen,
|
||||
onClose
|
||||
}: IProps) {
|
||||
|
@ -71,15 +78,38 @@ function Drawer({
|
|||
onClose?.();
|
||||
}, [ onClose ]);
|
||||
|
||||
/**
|
||||
* Handles pressing the escape key, closing the drawer.
|
||||
*
|
||||
* @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClose?.();
|
||||
}
|
||||
}, [ onClose ]);
|
||||
|
||||
return (
|
||||
isOpen ? (
|
||||
<div
|
||||
className = 'drawer-menu-container'
|
||||
onClick = { handleOutsideClick }>
|
||||
onClick = { handleOutsideClick }
|
||||
onKeyDown = { handleEscKey }>
|
||||
<div
|
||||
className = { `drawer-menu ${styles.drawer} ${className}` }
|
||||
onClick = { handleInsideClick }>
|
||||
<ReactFocusLock
|
||||
lockProps = {{
|
||||
role: 'dialog',
|
||||
'aria-modal': true,
|
||||
'aria-labelledby': `#${headingId}`
|
||||
}}
|
||||
returnFocus = { true }>
|
||||
{children}
|
||||
</ReactFocusLock>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
|
|
@ -83,12 +83,13 @@ class HangupMenuButton extends Component<IProps> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { children, isOpen } = this.props;
|
||||
const { children, isOpen, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'toolbox-button-wth-dialog context-menu'>
|
||||
<Popover
|
||||
content = { children }
|
||||
headingLabel = { t('toolbar.accessibilityLabel.hangup') }
|
||||
onPopoverClose = { this._onCloseDialog }
|
||||
position = 'top'
|
||||
trigger = 'click'
|
||||
|
|
|
@ -123,6 +123,7 @@ const OverflowMenuButton = ({
|
|||
) : (
|
||||
<Popover
|
||||
content = { children }
|
||||
headingId = 'overflow-context-menu'
|
||||
onPopoverClose = { onCloseDialog }
|
||||
onPopoverOpen = { onOpenDialog }
|
||||
position = 'top'
|
||||
|
|
|
@ -1466,6 +1466,7 @@ class Toolbox extends Component<IProps> {
|
|||
accessibilityLabel = { t(toolbarAccLabel) }
|
||||
className = { classes.contextMenu }
|
||||
hidden = { false }
|
||||
id = 'overflow-context-menu'
|
||||
inDrawer = { _overflowDrawer }
|
||||
onKeyDown = { this._onEscKey }>
|
||||
{overflowMenuButtons.reduce((acc, val) => {
|
||||
|
|
|
@ -216,6 +216,7 @@ class LocalVideoMenuTriggerButton extends Component<IProps> {
|
|||
isMobileBrowser() || _showLocalVideoFlipButton || _showHideSelfViewButton
|
||||
? <Popover
|
||||
content = { content }
|
||||
headingLabel = { t('dialog.localUserControls') }
|
||||
id = 'local-video-menu-trigger'
|
||||
onPopoverClose = { this._onPopoverClose }
|
||||
onPopoverOpen = { this._onPopoverOpen }
|
||||
|
|
|
@ -190,6 +190,7 @@ class RemoteVideoMenuTriggerButton extends Component<IProps> {
|
|||
return (
|
||||
<Popover
|
||||
content = { content }
|
||||
headingLabel = { this.props.t('dialog.remoteUserControls', { username }) }
|
||||
id = 'remote-video-menu-trigger'
|
||||
onPopoverClose = { this._onPopoverClose }
|
||||
onPopoverOpen = { this._onPopoverOpen }
|
||||
|
|
Loading…
Reference in New Issue