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:
parent
9d7f1dc2dc
commit
af394d7dff
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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() } />
|
||||
|
|
|
@ -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') }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -131,6 +131,7 @@ function MeetingParticipants({
|
|||
<Input
|
||||
className = { styles.search }
|
||||
clearable = { true }
|
||||
id = 'participants-search-input'
|
||||
onChange = { setSearchString }
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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') }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue