Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add aria attributes to RangeInput #1560

Merged
merged 10 commits into from
Sep 20, 2021
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ lib/

# editor config
.vscode
.idea

# happo output directory
.out
60 changes: 52 additions & 8 deletions src/components/forms/RangeInput/RangeInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render, screen, waitFor } from '@testing-library/react'

import { RangeInput } from './RangeInput'

Expand All @@ -17,25 +18,68 @@ describe('RangeInput component', () => {
expect(rangeElement).toBeInTheDocument()
expect(rangeElement).toHaveAttribute('id', 'range-slider-id')
expect(rangeElement).toHaveAttribute('name', 'rangeName')
expect(rangeElement).toHaveAttribute('aria-valuemin', '0')
expect(rangeElement).toHaveAttribute('aria-valuemax', '100')
expect(rangeElement).toHaveAttribute('aria-valuenow', '50')
expect(rangeElement).toHaveClass('usa-range')
expect(rangeElement).toHaveClass('additional-class')
})

it('renders with custom range values', () => {
const min = -15
const max = 60
const { queryByTestId } = render(
<RangeInput
id="range-slider-id"
name="rangeName"
min={0}
max={60}
min={min}
max={max}
step={15}
defaultValue={45}
/>
)
expect(queryByTestId('range')).toHaveAttribute('min', '0')
expect(queryByTestId('range')).toHaveAttribute('max', '60')
expect(queryByTestId('range')).toHaveAttribute('step', '15')
expect(queryByTestId('range')).toHaveAttribute('value', '45')

const rangeElement = queryByTestId('range')

expect(rangeElement).toHaveAttribute('min', '-15')
expect(rangeElement).toHaveAttribute('max', '60')
expect(rangeElement).toHaveAttribute('aria-valuemin', String(min))
expect(rangeElement).toHaveAttribute('aria-valuemax', String(max))
expect(rangeElement).toHaveAttribute(
'aria-valuenow',
String(min + (max - min) / 2)
)
expect(rangeElement).toHaveAttribute('step', '15')
})

it('renders with default value', () => {
const { queryByTestId } = render(
<RangeInput id="range-slider-id" name="rangeName" defaultValue={75} />
)

expect(queryByTestId('range')).toHaveAttribute('aria-valuenow', '75')
})

it('renders with custom aria values and updates aria-valuenow on keyboard actions', () => {
render(
<RangeInput
id="range-slider-id"
name="rangeName"
aria-valuemin={12}
aria-valuemax={58}
aria-valuenow={23}
/>
)
const rangeInput = screen.getByTestId('range')
expect(rangeInput).toHaveAttribute('aria-valuemin', '12')
expect(rangeInput).toHaveAttribute('aria-valuemax', '58')
expect(rangeInput).toHaveAttribute('aria-valuenow', '23')

userEvent.type(rangeInput, '{arrowright}')

waitFor(() => expect(rangeInput).toHaveAttribute('aria-valuenow', '24'))
userEvent.type(rangeInput, '{arrowleft}')
userEvent.type(rangeInput, '{arrowleft}')
waitFor(() => expect(rangeInput).toHaveAttribute('aria-valuenow', '22'))
})

it('renders with step attribute set to value any', () => {
Expand Down
34 changes: 32 additions & 2 deletions src/components/forms/RangeInput/RangeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import classnames from 'classnames'
interface RangeInputProps {
id: string
name: string
min?: number
max?: number
inputRef?:
| string
| ((instance: HTMLInputElement | null) => void)
Expand All @@ -17,9 +19,33 @@ export const RangeInput = ({
inputRef,
...inputProps
}: RangeInputProps & JSX.IntrinsicElements['input']): React.ReactElement => {
// Range defaults to min = 0, max = 100, step = 1, and value = (max/2) if not specified.

const classes = classnames('usa-range', className)
// input range defaults to min = 0, max = 100, step = 1, and value = (max/2) if not specified.
const defaultMin = 0
const defaultMax = 100
const { min, max, defaultValue } = inputProps
const rangeMin = min || defaultMin
const rangeMax = max || defaultMax
const ariaMin = inputProps['aria-valuemin'] || rangeMin
const ariaMax = inputProps['aria-valuemax'] || rangeMax
const calculatedAriaValueNow =
inputProps['aria-valuenow'] ||
defaultValue ||
(rangeMax < rangeMin ? rangeMin : rangeMin + (rangeMax - rangeMin) / 2)
const convertValueType = (
value: string | number | readonly string[]
): number | undefined => {
if (typeof value === 'number' || typeof value === 'string') {
return Number(value)
}
return undefined
}
const [ariaValue, setAriaValue] = React.useState<number | undefined>(
convertValueType(calculatedAriaValueNow)
)
const onValueChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (!inputProps['aria-valuenow']) setAriaValue(e.target.valueAsNumber)
}

return (
<input
Expand All @@ -28,6 +54,10 @@ export const RangeInput = ({
ref={inputRef}
type="range"
{...inputProps}
aria-valuemin={ariaMin}
aria-valuemax={ariaMax}
aria-valuenow={ariaValue}
onChange={(e): void => onValueChange(e)}
/>
)
}
Expand Down