React, accessibility

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.

Read the first post here.

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 or inputID 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 and id 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.

Header photo by Sigmund on Unsplash