fix(a11y/forms) correctly label <Select> components

it's important for some screen readers to have inputs and labels
correctly linked together via html attributes. Without doing this, some
users can have trouble knowing what the form input is about.

Select inputs had visible labels but they were actually not tied to
their inputs. Bottom labels too, where additional info is shown, or I
guess errors also. Now the label, additional info and potential errors
are correctly linked.
This commit is contained in:
Emmanuel Pelletier 2023-03-06 15:40:34 +01:00
parent 506e0523c6
commit 9d7f1dc2dc
6 changed files with 22 additions and 2 deletions

View File

@ -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>
)}

View File

@ -163,6 +163,7 @@ class DeviceSelector extends Component<IProps> {
return (
<Select
id = { this.props.id }
label = { this.props.t(this.props.label) }
onChange = { this._onSelect }
options = { options.items }

View File

@ -351,6 +351,7 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, State> {
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
? t('settings.desktopShareHighFpsWarning')
: t('settings.desktopShareWarning') }
id = 'more-framerate-select'
label = { t('settings.desktopShareFramerate') }
onChange = { this._onFramerateItemSelect }
options = { frameRateItems }

View File

@ -100,6 +100,7 @@ class StreamKeyPicker extends PureComponent<IProps> {
return (
<div className = 'broadcast-dropdown dropdown-menu'>
<Select
id = 'streamkeypicker-select'
label = { t('liveStreaming.choose') }
onChange = { this._onSelect }
options = { dropdownItems }

View File

@ -167,6 +167,7 @@ class MoreTab extends AbstractDialogTab<Props, any> {
key = 'maxStageParticipants'>
<div className = 'dropdown-menu'>
<Select
id = 'more-maxStageParticipants-select'
label = { t('settings.maxStageParticipants') }
onChange = { this._onMaxStageParticipantsSelect }
options = { maxParticipantsItems }

View File

@ -212,6 +212,7 @@ class ProfileTab extends AbstractDialogTab<IProps, any> {
return (
<Select
className = { classes.bottomMargin }
id = 'more-language-select'
label = { t('settings.language') }
onChange = { this._onLanguageItemSelect }
options = { languageItems }