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

we do the same thing as what we did on <Select> components in the
previous commit, for the same reason.

Note I made the id prop required on purpose to prevent creating a11y
issues. The id is necessary to correctly link the label to the input,
but even if there is no input, we need one to link the input error if it
has one.
I figure cases where we actually don't need the id because we know there
is visible label, and no possible error, are pretty rare. And I'd rather
set a couple unnecessary ids across the app to enforce a better a11y by
default for the 99% case.
This commit is contained in:
Emmanuel Pelletier 2023-02-27 19:07:57 +01:00
parent 9d7f1dc2dc
commit af394d7dff
13 changed files with 32 additions and 5 deletions

View File

@ -268,6 +268,7 @@ class LoginDialog extends Component<IProps, IState> {
titleKey = { t('dialog.authenticationRequired') }>
<Input
autoFocus = { true }
id = 'login-dialog-username'
label = { t('dialog.user') }
name = 'username'
onChange = { this._onUsernameChange }
@ -277,6 +278,7 @@ class LoginDialog extends Component<IProps, IState> {
<br />
<Input
className = 'dialog-bottom-margin'
id = 'login-dialog-password'
label = { t('dialog.userPassword') }
name = 'password'
onChange = { this._onPasswordChange }

View File

@ -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;
minRows?: number;
@ -168,7 +174,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 } : {}) }
@ -184,7 +194,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 }
@ -198,6 +208,7 @@ const Input = React.forwardRef<any, IProps>(({
value = { value } />
) : (
<input
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
@ -205,7 +216,7 @@ const Input = React.forwardRef<any, IProps>(({
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
data-testid = { testId }
disabled = { disabled }
{ ...(id ? { id } : {}) }
id = { id }
maxLength = { maxLength }
name = { name }
onChange = { handleChange }
@ -225,7 +236,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>
)}

View File

@ -118,6 +118,7 @@ class ChatInput extends Component<IProps, IState> {
className = 'chat-input'
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
iconClick = { this._toggleSmileysPanel }
id = 'chat-input-messagebox'
maxRows = { 5 }
onChange = { this._onMessageChange }
onKeyPress = { this._onDetectSubmit }

View File

@ -58,6 +58,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'dialog-displayName'
label = { this.props.t('dialog.enterDisplayName') }
name = 'displayName'
onChange = { this._onDisplayNameChange }

View File

@ -40,6 +40,7 @@ function EmbedMeeting({ t, url }: IProps) {
<div className = 'embed-meeting-dialog'>
<Input
accessibilityLabel = { t('dialog.embedMeeting') }
id = 'embed-meeting-input'
readOnly = { true }
textarea = { true }
value = { getEmbedCode() } />

View File

@ -196,6 +196,7 @@ function GifsMenu() {
<Input
autoFocus = { true }
className = { cx(styles.searchField, 'gif-input') }
id = 'gif-search-input'
onChange = { handleSearchKeyChange }
onKeyPress = { onInputKeyPress }
placeholder = { t('giphy.search') }

View File

@ -187,6 +187,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
return (
<Input
className = 'prejoin-input'
id = 'lobby-name-field'
onChange = { this._onChangeDisplayName }
placeholder = { t('lobby.nameField') }
testId = 'lobby.nameField'

View File

@ -131,6 +131,7 @@ function MeetingParticipants({
<Input
className = { styles.search }
clearable = { true }
id = 'participants-search-input'
onChange = { setSearchString }
placeholder = { t('participantsPane.search') }
value = { searchString } />

View File

@ -191,6 +191,7 @@ const PollCreate = ({
<div className = { classes.questionContainer }>
<Input
autoFocus = { true }
id = 'polls-create-input'
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { setQuestion }
@ -205,6 +206,7 @@ const PollCreate = ({
className = { classes.answer }
key = { i }>
<Input
id = { `polls-answer-input-${i}` }
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }

View File

@ -354,6 +354,7 @@ class Prejoin extends Component<IProps, IState> {
autoFocus = { true }
className = 'prejoin-input'
error = { showErrorOnJoin }
id = 'premeeting-name-input'
onChange = { _setName }
onKeyPress = { _onInputKeyPress }
placeholder = { t('dialog.enterDisplayName') }

View File

@ -43,6 +43,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<Props> {
<div className = 'stream-key-form'>
<Input
autoFocus = { true }
id = 'streamkey-input'
label = { t('dialog.streamKey') }
name = 'streamId'
onChange = { this._onInputChange }

View File

@ -93,6 +93,7 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'required-password-input'
label = { this.props.t('dialog.passwordLabel') }
name = 'lockKey'
onChange = { this._onPasswordChanged }

View File

@ -86,6 +86,7 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
autoFocus = { true }
className = 'dialog-bottom-margin'
error = { error }
id = 'shared-video-url-input'
label = { t('dialog.videoLink') }
name = 'sharedVideoUrl'
onChange = { this._onChange }