mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
Globally improve accessibility for screen reader users (#12969)
feat(a11y): Globally improve accessibility for screen reader users
This commit is contained in:
committed by
GitHub
parent
7538bfc713
commit
51a4e7daa3
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import { FocusOn } from 'react-focus-on';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { keyframes } from 'tss-react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
@@ -183,7 +183,7 @@ const BaseDialog = ({
|
||||
<div
|
||||
className = { classes.backdrop }
|
||||
onClick = { onBackdropClick } />
|
||||
<FocusLock
|
||||
<FocusOn
|
||||
className = { classes.focusLock }
|
||||
returnFocus = {
|
||||
|
||||
@@ -196,14 +196,16 @@ const BaseDialog = ({
|
||||
isElementInTheViewport
|
||||
}>
|
||||
<div
|
||||
aria-describedby = { description }
|
||||
aria-labelledby = { title ?? t(titleKey ?? '') }
|
||||
aria-description = { description }
|
||||
aria-label = { title ?? t(titleKey ?? '') }
|
||||
aria-modal = { true }
|
||||
className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
|
||||
role = 'dialog'>
|
||||
data-autofocus = { true }
|
||||
role = 'dialog'
|
||||
tabIndex = { -1 }>
|
||||
{children}
|
||||
</div>
|
||||
</FocusLock>
|
||||
</FocusOn>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -156,8 +156,8 @@ const Checkbox = ({
|
||||
const isMobile = isMobileBrowser();
|
||||
|
||||
return (
|
||||
<div className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
|
||||
<label className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
|
||||
<label className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
|
||||
<div className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
|
||||
<input
|
||||
checked = { checked }
|
||||
disabled = { disabled }
|
||||
@@ -165,13 +165,14 @@ const Checkbox = ({
|
||||
onChange = { onChange }
|
||||
type = 'checkbox' />
|
||||
<Icon
|
||||
aria-hidden = { true }
|
||||
className = 'checkmark'
|
||||
color = { disabled ? theme.palette.icon03 : theme.palette.icon01 }
|
||||
size = { 18 }
|
||||
src = { IconCheck } />
|
||||
</label>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>{label}</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
@@ -76,6 +76,9 @@ export interface IProps {
|
||||
|
||||
/**
|
||||
* You can use this item as a tab. Defaults to button if not set.
|
||||
*
|
||||
* If no onClick handler is provided, we assume the context menu item is
|
||||
* not interactive and no role will be set.
|
||||
*/
|
||||
role?: 'tab' | 'button';
|
||||
|
||||
@@ -179,6 +182,28 @@ const ContextMenuItem = ({
|
||||
const { classes: styles, cx } = useStyles();
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
// only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
|
||||
if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
|
||||
if (onKeyPress) {
|
||||
onKeyPress(e);
|
||||
}
|
||||
}, [ onClick, onKeyPress, onKeyDown ]);
|
||||
|
||||
let tabIndex: undefined | 0 | -1;
|
||||
|
||||
if (role === 'tab') {
|
||||
tabIndex = selected ? 0 : -1;
|
||||
}
|
||||
|
||||
if (role === 'button' && !disabled) {
|
||||
tabIndex = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-controls = { controls }
|
||||
@@ -196,12 +221,9 @@ const ContextMenuItem = ({
|
||||
key = { text }
|
||||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||
role = { role }
|
||||
tabIndex = { role === 'tab'
|
||||
? selected ? 0 : -1
|
||||
: disabled ? undefined : 0
|
||||
}>
|
||||
onKeyPress = { disabled ? undefined : onKeyPressHandler }
|
||||
role = { onClick ? role : undefined }
|
||||
tabIndex = { onClick ? tabIndex : undefined }>
|
||||
{customIcon ? customIcon
|
||||
: icon && <Icon
|
||||
className = { styles.contextMenuItemIcon }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { hideDialog } from '../../../dialog/actions';
|
||||
import { IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { operatesWithEnterKey } from '../../functions.web';
|
||||
|
||||
import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
|
||||
import Button from './Button';
|
||||
@@ -108,8 +109,13 @@ const Dialog = ({
|
||||
}, [ onCancel ]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit?.();
|
||||
if (onSubmit && (
|
||||
(document.activeElement && !operatesWithEnterKey(document.activeElement))
|
||||
|| !document.activeElement
|
||||
)) {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit();
|
||||
}
|
||||
}, [ onSubmit ]);
|
||||
|
||||
return (
|
||||
@@ -124,11 +130,11 @@ const Dialog = ({
|
||||
title = { title }
|
||||
titleKey = { titleKey }>
|
||||
<div className = { classes.header }>
|
||||
<p
|
||||
<h1
|
||||
className = { classes.title }
|
||||
id = 'dialog-title'>
|
||||
{title ?? t(titleKey ?? '')}
|
||||
</p>
|
||||
</h1>
|
||||
{!hideCloseButton && (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||
@@ -160,6 +166,7 @@ const Dialog = ({
|
||||
accessibilityLabel = { t(ok.translationKey ?? '') }
|
||||
disabled = { ok.disabled }
|
||||
id = 'modal-dialog-ok-button'
|
||||
isSubmit = { true }
|
||||
labelKey = { ok.translationKey }
|
||||
onClick = { submit } />}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { MoveFocusInside } from 'react-focus-lock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
@@ -317,20 +316,19 @@ const DialogWithTabs = ({
|
||||
<BaseDialog
|
||||
className = { cx(classes.dialog, className) }
|
||||
onClose = { onClose }
|
||||
size = 'large'>
|
||||
size = 'large'
|
||||
titleKey = { titleKey }>
|
||||
{(!isMobile || !selectedTab) && (
|
||||
<div
|
||||
aria-orientation = 'vertical'
|
||||
className = { classes.sidebar }
|
||||
role = { isMobile ? undefined : 'tablist' }>
|
||||
<div className = { classes.titleContainer }>
|
||||
<MoveFocusInside>
|
||||
<h2
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{t(titleKey ?? '')}
|
||||
</h2>
|
||||
</MoveFocusInside>
|
||||
<h1
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{t(titleKey ?? '')}
|
||||
</h1>
|
||||
{isMobile && closeIcon}
|
||||
</div>
|
||||
{tabs.map((tab, index) => {
|
||||
@@ -366,11 +364,11 @@ const DialogWithTabs = ({
|
||||
{isMobile && (
|
||||
<div className = { cx(classes.buttonContainer, classes.header) }>
|
||||
<span className = { classes.backContainer }>
|
||||
<h2
|
||||
<h1
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||
</h2>
|
||||
</h1>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.Back') }
|
||||
icon = { IconArrowBack }
|
||||
@@ -401,13 +399,13 @@ const DialogWithTabs = ({
|
||||
<div
|
||||
className = { cx(classes.buttonContainer, classes.footer) }>
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Cancel') }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.Cancel') }
|
||||
id = 'modal-dialog-cancel-button'
|
||||
labelKey = { 'dialog.Cancel' }
|
||||
onClick = { onClose }
|
||||
type = 'tertiary' />
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Ok') }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.Ok') }
|
||||
id = 'modal-dialog-ok-button'
|
||||
labelKey = { 'dialog.Ok' }
|
||||
onClick = { onSubmit } />
|
||||
|
||||
@@ -15,7 +15,13 @@ interface IProps extends IInputProps {
|
||||
bottomLabel?: string;
|
||||
className?: string;
|
||||
iconClick?: () => void;
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* The id to set on the input element.
|
||||
* This is required because we need it internally to tie the input to its
|
||||
* info (label, error) so that screen reader users don't get lost.
|
||||
*/
|
||||
id: string;
|
||||
maxLength?: number;
|
||||
maxRows?: number;
|
||||
maxValue?: number;
|
||||
@@ -187,7 +193,11 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
|
||||
return (
|
||||
<div className = { cx(styles.inputContainer, className) }>
|
||||
{label && <span className = { cx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
|
||||
{label && <label
|
||||
className = { cx(styles.label, isMobile && 'is-mobile') }
|
||||
htmlFor = { id } >
|
||||
{label}
|
||||
</label>}
|
||||
<div className = { styles.fieldContainer }>
|
||||
{icon && <Icon
|
||||
{ ...(iconClick ? { tabIndex: 0 } : {}) }
|
||||
@@ -203,7 +213,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
className = { cx(styles.input, isMobile && 'is-mobile',
|
||||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
id = { id }
|
||||
maxLength = { maxLength }
|
||||
maxRows = { maxRows }
|
||||
minRows = { minRows }
|
||||
@@ -217,6 +227,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
value = { value } />
|
||||
) : (
|
||||
<input
|
||||
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
|
||||
aria-label = { accessibilityLabel }
|
||||
autoComplete = { autoComplete }
|
||||
autoFocus = { autoFocus }
|
||||
@@ -224,7 +235,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
data-testid = { testId }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
id = { id }
|
||||
{ ...(mode ? { inputmode: mode } : {}) }
|
||||
{ ...(type === 'number' ? { max: maxValue } : {}) }
|
||||
maxLength = { maxLength }
|
||||
@@ -249,7 +260,9 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
</button>}
|
||||
</div>
|
||||
{bottomLabel && (
|
||||
<span className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
|
||||
<span
|
||||
className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }
|
||||
id = { `${id}-description` }>
|
||||
{bottomLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface IProps {
|
||||
error?: boolean;
|
||||
errorDialog?: JSX.Element | null;
|
||||
filterValue?: string;
|
||||
id: string;
|
||||
isOpen?: boolean;
|
||||
items: MultiSelectItem[];
|
||||
noMatchesText?: string;
|
||||
@@ -101,6 +102,7 @@ const MultiSelect = ({
|
||||
error,
|
||||
errorDialog,
|
||||
placeholder,
|
||||
id,
|
||||
items,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
@@ -145,6 +147,7 @@ const MultiSelect = ({
|
||||
<Input
|
||||
autoFocus = { autoFocus }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
onChange = { onFilterChange }
|
||||
placeholder = { placeholder }
|
||||
ref = { inputRef }
|
||||
|
||||
@@ -28,6 +28,12 @@ interface ISelectProps {
|
||||
*/
|
||||
error?: boolean;
|
||||
|
||||
/**
|
||||
* Id of the <select> element.
|
||||
* Necessary for screen reader users, to link the label and error to the select.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Label to be displayed above the select.
|
||||
*/
|
||||
@@ -140,6 +146,7 @@ const Select = ({
|
||||
className,
|
||||
disabled,
|
||||
error,
|
||||
id,
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
@@ -149,11 +156,17 @@ const Select = ({
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{label && <span className = { cx(classes.label, isMobile && 'is-mobile') }>{label}</span>}
|
||||
{label && <label
|
||||
className = { cx(classes.label, isMobile && 'is-mobile') }
|
||||
htmlFor = { id } >
|
||||
{label}
|
||||
</label>}
|
||||
<div className = { classes.selectContainer }>
|
||||
<select
|
||||
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
|
||||
className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
onChange = { onChange }
|
||||
value = { value }>
|
||||
{options.map(option => (<option
|
||||
@@ -167,7 +180,9 @@ const Select = ({
|
||||
src = { IconArrowDown } />
|
||||
</div>
|
||||
{bottomLabel && (
|
||||
<span className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
|
||||
<span
|
||||
className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }
|
||||
id = { `${id}-description` }>
|
||||
{bottomLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,7 @@ const useStyles = makeStyles()(theme => {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
position: 'absolute',
|
||||
zIndex: 5,
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
backgroundColor: theme.palette.ui10,
|
||||
@@ -73,8 +74,38 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
height: 0,
|
||||
width: 0
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
cursor: 'pointer',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0,
|
||||
|
||||
'&.focus-visible + .toggle-checkbox-ring': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
|
||||
}
|
||||
},
|
||||
|
||||
checkboxRing: {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 6,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '12px',
|
||||
|
||||
'&.is-mobile': {
|
||||
borderRadius: '32px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -88,7 +119,7 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<label
|
||||
<span
|
||||
className = { cx('toggle-container', styles.container, checked && styles.containerOn,
|
||||
isMobile && 'is-mobile', disabled && 'disabled', className) }>
|
||||
<input
|
||||
@@ -98,8 +129,9 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
|
||||
className = { styles.checkbox }
|
||||
disabled = { disabled }
|
||||
onChange = { change } />
|
||||
<div className = { cx('toggle-checkbox-ring', styles.checkboxRing, isMobile && 'is-mobile') } />
|
||||
<div className = { cx('toggle', styles.toggle, checked && styles.toggleOn, isMobile && 'is-mobile') } />
|
||||
</label>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user