Templating Accessible Components in React Part 2: Inputs
In the first part of this series, we introduced the idea of building reusable accessible components in React to lower the effort needed to build an accessible application, and covered how to build Navigation components. In part 3 we cover templating accessible buttons and how to check for accessibility.
Inputs
Accessible inputs need labels. The input itself should have an id
and the label needs a for
attribute that is associated with the input’s id
. We can also add more aria-labels to the input such as aria-invalid
or aria-required
, and we can create a custom input component that requires a label and renders null
if label is not present.
Below we have created a custom input component where we require the developer to include a label
, requestAttribute
, and type
as props to the component. If those props are not provided we do not show the input and instead we show an error message (similar to the NavItem
).
The
requestAttribute
is the object key you will be sending to the database. You can omit this if it does not fit your needs.
Custom Input Component
import React, { useState } from 'react'
export type CustomInputType =
| 'email'
| 'money'
| 'number'
| 'password'
| 'phone'
| 'text'
| 'zip'
interface IProps {
handleOnBlur: (
requestAttribute: string
) => (e: React.ChangeEvent<HTMLInputElement>) => void
label: string
placeholder?: string
requestAttribute: string
required?: boolean
type: CustomInputType
}
const AccessibleInput: React.FC<IProps> = props => {
const [inputValue, setInputValue] = useState<string | number | undefined>()
const {
label,
placeholder = label,
requestAttribute,
required,
type,
handleOnBlur
} = props
const renderRequiredLabel = (): JSX.Element => (
<span className='input-required'>*</span>
)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setInputValue(e.target.value)
const renderInputNode = (): JSX.Element => {
const inputID: string = label.toLowerCase()
return (
<>
<label htmlFor={inputID}>
{label} {required ? renderRequiredLabel() : null}
</label>
<input
id={inputID}
type={type}
name={inputID}
placeholder={placeholder}
onChange={handleInputChange}
onBlur={handleOnBlur(requestAttribute)}
required={required ?? false}
value={inputValue}
/>
</>
)
}
return <>{label ? renderInputNode() : null}</>
}
export default AccessibleInput
The foundation to this component is very similar to the NavItem
. First, we want to create an inputValue
state variable using the useState
react hook.
const [inputValue, setInputValue] = useState<string | number | undefined>()
Nothing fancy there. We then destructure our props:
const {
label,
placeholder = label,
requestAttribute,
required,
type,
handleOnBlur
} = props
So far, nothing new. The next method is completely optional. The renderRequiredLabel
returns JSX to indicate that the input field is required. If you have your own custom required component or template you would put it here.
The next function sets our inputValue
state variable to the current input of the input field. The renderInputNode
has a little more going on.
First, we want to define what our input id will be by declaring an inputID
variable.
const inputID: string = label.toLowerCase()
We could require the developers to pass this in as a prop but to make the devs’ lives easier, we can infer the input id from the label.
A potential improvement here would be creating a check to ensure that the
label
orinputID
is unique so that you don’t have a label appearing multiple times on a page.
Now let’s get into the actual HTML of the label and input.
return (
<>
<label htmlFor={inputID}>
{label} {required ? renderRequiredLabel() : null}
</label>
<input
id={inputID}
type={type}
name={inputID}
placeholder={placeholder}
onChange={handleInputChange}
onBlur={handleOnBlur(requestAttribute)}
required={required ?? false}
value={inputValue}
/>
</>
)
We return a fragment
with the label
above the input
.
The label
contains an htmlFor
attribute (which is the React equivalent of the for
attribute in HTML) that we set equal to the inputID
we defined above.
<label htmlFor={inputID}>
{label} {required ? renderRequiredLabel() : null}
</label>
The label
child will be the label
text provided as a prop and conditionally, the required label. The input is pretty similar in nature, just a few more attributes we have to define.
We want to define our id
which will be the same id
we created inputID
or whatever we passed to the htmlFor
attribute on the label. It is important that these two labels are the same.
Improvement Opportunity: Create a check to ensure that the
htmlFor
andid
for input are the same
Next, we provide the type
of the input which could be email
, password
, text
, etc. Then we want to add a name for the input which should also correspond to the inputID
. The placeholder is required for accessibility however we do not require our devs to pass it down. If placeholder
is not provided we default it to the label in the props:
const {
label,
placeholder = label,
...
} = props
We do not set a default for the required
prop, therefore, if the required
is null
we want to pass the required
attribute false (not null).
required={required ?? false}
The value
attribute acts as any value attribute in HTML. We pass it the state variable inputValue
which will update onChange
. The handlers we pass to input, onChange
and onBlur
, are action functions for our input and we can define these however we need.
For now, the onChange
invokes the handleInputChange
whenever the user input of the input field changes.
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setInputValue(e.target.value)
We take the event
or e
to get the value of the input and set that as the inputValue
state variable. The onBlur
attribute however, takes the handleOnBlur
prop passed. To show what that does, we can create a barebones form component that uses this input.
Using the Custom Input Component
import React, { useState } from 'react'
import AccessibleInput from './AccessibleInput'
const AccessibleForm: React.FC = () => {
const [apiRequest, setApiRequest] = useState<object>({})
const handleOnBlur = (requestAttribute: string) => (
e: React.ChangeEvent<HTMLInputElement>
) =>
setApiRequest({ ...apiRequest, [requestAttribute]: e.target.value })
return (
<form>
<AccessibleInput
label='First Name'
requestAttribute='first_name'
type='text'
handleOnBlur={handleOnBlur}
/>
<AccessibleInput
label='Last Name'
requestAttribute='last_name'
type='text'
handleOnBlur={handleOnBlur}
/>
<AccessibleInput
label='Password'
requestAttribute='password'
type='password'
handleOnBlur={handleOnBlur}
/>
<AccessibleInput
label='Confirm Password'
requestAttribute='password_confirmation'
type='password'
handleOnBlur={handleOnBlur}
/>
</form>
)
}
export default AccessibleForm
A few things happening here. We start by creating an apiRequest
state variable that will be an object containing your API request. Then we define our handleOnBlur
method.
const handleOnBlur = (requestAttribute: string) => (
e: React.ChangeEvent<HTMLInputElement>
) =>
setApiRequest({ ...apiRequest, [requestAttribute]: e.target.value })
The handleOnBlur
function makes use of chaining. Our first parameter is the requestAttribute
itself and the second parameter is the event
. When we invoke the handleOnBlur
in our AccessibleInput
component we are passing back to the AccessibleForm
component the requestAttribute
that was passed down.
We take that attribute and create a key in the apiRequest
with the value of the input. In practice the apiRequest
object may end up looking something like this:
{
first_name: 'John',
last_name: 'Doe',
password: 'text',
password_confirmation: 'text'
}
That’s it. You can convert this structure into a custom hook and include validation. This simple adjustment now ensures that every single input field is accessible. Doing that in plain HTML may have been a pain for an application with many forms or input fields, but since you have the power of reuse with React, that process is much easier and faster. You may want to add an aria label to the props as well; but for now, this is an accessible input component in its most basic form.
In the next and final post in this series, we'll cover buttons and accessibility checking tools.
Other posts in this series: Navigation, Buttons.