diff --git a/.gitignore b/.gitignore index 68843c871c..56d91db76a 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ storybook-static/ # editor config .vscode +.idea # happo output directory diff --git a/src/components/forms/RangeInput/RangeInput.test.tsx b/src/components/forms/RangeInput/RangeInput.test.tsx index 638aaf5468..191e042809 100644 --- a/src/components/forms/RangeInput/RangeInput.test.tsx +++ b/src/components/forms/RangeInput/RangeInput.test.tsx @@ -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' @@ -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( ) - 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( + + ) + + expect(queryByTestId('range')).toHaveAttribute('aria-valuenow', '75') + }) + + it('renders with custom aria values and updates aria-valuenow on keyboard actions', () => { + render( + + ) + 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', () => { diff --git a/src/components/forms/RangeInput/RangeInput.tsx b/src/components/forms/RangeInput/RangeInput.tsx index ebe783cb09..1e95fe3f19 100644 --- a/src/components/forms/RangeInput/RangeInput.tsx +++ b/src/components/forms/RangeInput/RangeInput.tsx @@ -4,6 +4,8 @@ import classnames from 'classnames' interface RangeInputProps { id: string name: string + min?: number + max?: number inputRef?: | string | ((instance: HTMLInputElement | null) => void) @@ -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( + convertValueType(calculatedAriaValueNow) + ) + const onValueChange: React.ChangeEventHandler = (e) => { + if (!inputProps['aria-valuenow']) setAriaValue(e.target.valueAsNumber) + } return ( onValueChange(e)} /> ) }