Skip to content

Commit

Permalink
Add aria described by to inputs and associate id to error label (#327)
Browse files Browse the repository at this point in the history
* Add aria described by to inputs and associate id to error label

* minor bump to v3.18.0
  • Loading branch information
chawes13 authored Apr 3, 2019
1 parent f514539 commit d94ef40
Show file tree
Hide file tree
Showing 21 changed files with 370 additions and 213 deletions.
359 changes: 196 additions & 163 deletions docs.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@launchpadlab/lp-components",
"version": "3.17.1",
"version": "3.18.0",
"engines": {
"node": "^8.0.0"
},
Expand Down
3 changes: 2 additions & 1 deletion src/forms/inputs/checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
replaceEmptyStringValue,
} from '../helpers'
import { LabeledField } from '../labels'
import { compose } from '../../utils'
import { compose, generateInputErrorId } from '../../utils'

/**
*
Expand Down Expand Up @@ -56,6 +56,7 @@ function Checkbox (props) {
checked: value,
onBlur,
onChange: () => onChange(!value),
'aria-describedby': generateInputErrorId(name),
...rest
}}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/forms/inputs/date-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { compose } from '../../utils'
* - Adds name and error labels.
*
* With the exception of the `input` and `meta` props, all props are passed down to the `DatePicker` component.
* A full list of props supported by this component can be found [here](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md).
* A full list of props supported by this component can be found [here](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). Note that unfortunately `aria-*` props are **not** supported.
*
* _Note: this component requires special styles in order to render correctly. To include these styles in your project, follow the directions in the main [README](README.md#dateinput-styles) file._
*
Expand Down
3 changes: 2 additions & 1 deletion src/forms/inputs/file-input/file-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { buttonClasses, fieldPropTypes, isImageType, omitLabelProps } from '../.
import { LabeledField } from '../../labels'
import FilePreview from './file-preview'
import ImagePreview from './image-preview';
import { noop } from '../../../utils'
import { noop, generateInputErrorId } from '../../../utils'

/**
*
Expand Down Expand Up @@ -117,6 +117,7 @@ class FileInput extends React.Component {
type: 'file',
onChange: this.loadFile,
accept,
'aria-describedby': generateInputErrorId(name),
}}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/forms/inputs/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { blurDirty, fieldPropTypes, omitLabelProps } from '../helpers'
import { LabeledField } from '../labels'
import { compose } from '../../utils'
import { compose, generateInputErrorId } from '../../utils'

/**
*
Expand Down Expand Up @@ -65,6 +65,7 @@ function Input (props) {
value,
onBlur,
onChange,
'aria-describedby': generateInputErrorId(name),
...rest
}}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/forms/inputs/range-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { blurDirty, fieldPropTypes, omitLabelProps } from '../helpers'
import { LabeledField } from '../labels'
import { compose } from '../../utils'
import { compose, generateInputErrorId } from '../../utils'

/**
*
Expand Down Expand Up @@ -81,6 +81,7 @@ function RangeInput (props) {
min,
max,
step,
'aria-describedby': generateInputErrorId(name),
...rest
}}
/>
Expand Down
8 changes: 7 additions & 1 deletion src/forms/inputs/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
omitLabelProps
} from '../helpers'
import { LabeledField } from '../labels'
import { compose, serializeOptions, serializeOptionGroups } from '../../utils'
import {
compose,
generateInputErrorId,
serializeOptions,
serializeOptionGroups
} from '../../utils'

/**
*
Expand Down Expand Up @@ -118,6 +123,7 @@ function Select (props) {
value,
onBlur,
onChange,
'aria-describedby': generateInputErrorId(name),
...rest
}}
>
Expand Down
3 changes: 2 additions & 1 deletion src/forms/inputs/textarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import { blurDirty, fieldPropTypes, omitLabelProps } from '../helpers'
import { LabeledField } from '../labels'
import { compose } from '../../utils'
import { compose, generateInputErrorId } from '../../utils'

/**
*
Expand Down Expand Up @@ -72,6 +72,7 @@ function Textarea (props) {
value,
onBlur,
onChange,
'aria-describedby': generateInputErrorId(name),
...rest,
}}
/>
Expand Down
11 changes: 9 additions & 2 deletions src/forms/labels/input-error.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { generateInputErrorId } from '../../utils'

/**
*
Expand All @@ -12,6 +13,8 @@ import classnames from 'classnames'
* - If the input is `invalid` and `touched`, the label will be shown
* - If the `error` prop is set to a string, the label will display that text
* - If the `error` prop is set to an array of strings, the label will display those errors separated by commas
*
* This label supports accessibility by adding a uniquely generated id to the span which should be referenced by the input using `aria-describedby`.
*
* In addition to the props below, any extra props will be passed directly to the inner `<span>` element.
*
Expand All @@ -20,6 +23,7 @@ import classnames from 'classnames'
* @param {String|Array} error - An error message or array of error messages to display
* @param {Boolean} invalid - Whether the associated input has an invalid value
* @param {String} touched - Whether the associated input has been touched
* @param {String} name - The name of the input (used to generate a unique ID)
*
* @example
*
Expand All @@ -37,7 +41,7 @@ import classnames from 'classnames'
* onBlur,
* onChange,
* }}
* <InputError { ...{ error, invalid, touched } } />
* <InputError { ...{ error, invalid, touched, name } } />
* </div>
* )
* }
Expand All @@ -52,18 +56,21 @@ const propTypes = {
invalid: PropTypes.bool,
touched: PropTypes.bool,
className: PropTypes.string,
name: PropTypes.string,
}

const defaultProps = {
error: null,
invalid: false,
touched: false,
className: '',
name: '',
}

function InputError ({ error, invalid, touched, className, ...rest }) {
function InputError ({ error, invalid, touched, name, className, ...rest }) {
return (touched && invalid)
? <span
id={ generateInputErrorId(name) }
className={ classnames('error-message', className) }
{ ...rest }
>
Expand Down
2 changes: 1 addition & 1 deletion src/forms/labels/labeled-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function LabeledField ({
<fieldset className={ classnames(className, { 'error': touched && invalid }) }>
<InputLabel { ...{ hint, label, name, id, tooltip, required, requiredIndicator } } />
{ children }
{ !hideErrorLabel && <InputError { ...{ error, invalid, touched } } /> }
{ !hideErrorLabel && <InputError { ...{ error, invalid, touched, name } } /> }
</fieldset>
)
}
Expand Down
23 changes: 23 additions & 0 deletions src/utils/generate-input-error-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* A utility for generating a unique id for an input error label. This logic
* is centralized to facilitate reference by multiple input components.
*
* @name generateInputErrorId
* @param {String} name - The name of the input
* @returns {String} String representing error id
*
* @example
*
* const name = 'cardNumber'
*
* generateInputErrorId(name)
*
* // 'cardNumberError'
*/

function generateInputErrorId (name) {
if (!name) return ''
return name + "Error"
}

export default generateInputErrorId
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export compareAtPath from './compare-at-path'
export serializeOptions from './serialize-options'
export serializeOptionGroups from './serialize-option-groups'
export stripNamespace from './strip-namespace'
export generateInputErrorId from './generate-input-error-id'
15 changes: 14 additions & 1 deletion test/forms/inputs/checkbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,17 @@ test('Checkbox toggles value when clicked', () => {
wrapper.find('input').simulate('change')
const newValue = onChange.mock.calls[0][0]
expect(newValue).toEqual(true)
})
})

test('Checkbox is given an aria described by attribute', () => {
const name = "test"
const props = {
input: {
name,
value: false,
},
meta: {}
}
const wrapper = mount(<Checkbox { ...props }/>)
expect(wrapper.find('input').prop('aria-describedby')).toContain(name)
})
24 changes: 15 additions & 9 deletions test/forms/inputs/file-input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FileInput } from '../../../src/'

const name = 'my.file.input'

test('Fileinput renders thumbnail with value as src when file is an image', () => {
test('FileInput renders thumbnail with value as src when file is an image', () => {
const value = 'foo'
const file = { name: 'fileName', type: 'image/png' }
const props = { input: { name, value }, meta: {} }
Expand All @@ -13,35 +13,35 @@ test('Fileinput renders thumbnail with value as src when file is an image', () =
expect(wrapper.find('img').props().src).toEqual(value)
})

test('Fileinput renders file name when file is non-image type or value is empty', () => {
test('FileInput renders file name when file is non-image type or value is empty', () => {
const file = { name: 'fileName', type: 'application/pdf' }
const props = { input: { name, value: '' }, meta: {} }
const wrapper = mount(<FileInput { ...props } />)
wrapper.setState({ file })
expect(wrapper.find('p').text()).toEqual('fileName')
})

test('Fileinput sets thumbnail placeholder', () => {
test('FileInput sets thumbnail placeholder', () => {
const thumbnail = 'thumb'
const props = { input: { name, value: '' }, meta: {}, thumbnail }
const wrapper = mount(<FileInput { ...props }/>)
expect(wrapper.find('img').props().src).toEqual(thumbnail)
})

test('Fileinput hides preview correctly', () => {
test('FileInput hides preview correctly', () => {
const props = { input: { name, value: '' }, meta: {}, hidePreview: true }
const wrapper = mount(<FileInput { ...props }/>)
expect(wrapper.find('img').exists()).toEqual(false)
})

test('Fileinput sets custom preview from children', () => {
test('FileInput sets custom preview from children', () => {
const Preview = () => <p> My preview </p>
const props = { input: { name, value: '' }, meta: {} }
const wrapper = mount(<FileInput { ...props }><Preview/></FileInput>)
expect(wrapper.find('p').exists()).toEqual(true)
})

test('Fileinput sets custom preview from props', () => {
test('FileInput sets custom preview from props', () => {
const Preview = ({ file, }) => <p>{ file && file.name }</p> // eslint-disable-line react/prop-types
const props = { input: { name, value: '' }, meta: {} }
const wrapper = mount(<FileInput previewComponent={ Preview } { ...props }/>)
Expand All @@ -50,14 +50,14 @@ test('Fileinput sets custom preview from props', () => {
expect(wrapper.find('p').text()).toEqual('fileName')
})

test('Fileinput passes extra props to custom preview', () => {
test('FileInput passes extra props to custom preview', () => {
const Preview = ({ message }) => <p>{ message }</p> // eslint-disable-line react/prop-types
const props = { input: { name, value: '' }, meta: {}, message: 'FOO' }
const wrapper = mount(<FileInput previewComponent={ Preview } { ...props }/>)
expect(wrapper.find('p').text()).toEqual('FOO')
})

test('Fileinput passes value to custom preview', () => {
test('FileInput passes value to custom preview', () => {
const Preview = ({ value }) => <p>{ value }</p> // eslint-disable-line react/prop-types
const value = 'foo'
const props = { input: { name, value }, meta: {} }
Expand Down Expand Up @@ -85,11 +85,17 @@ test('FileInput passes accept attribute to input component', () => {
expect(wrapper.find('input').prop('accept')).toEqual('image/*')
})

test('FileInput is given an aria described by attribute', () => {
const props = { input: { name, value: '' }, meta: {} }
const wrapper = mount(<FileInput { ...props }/>)
expect(wrapper.find('input').prop('aria-describedby')).toContain(name)
})

// Creates a mock FileReader that passes the given file data to its onload() handler
export function createMockFileReader (fileData) {
return class MockFileReader {
readAsDataURL () {
this.onload({ target: { result: fileData } })
}
}
}
}
6 changes: 6 additions & 0 deletions test/forms/inputs/input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ test('Input renders children', () => {
expect(wrapper.find(Wrapped).exists()).toEqual(true)
})

test('Input is given an aria described by attribute', () => {
const props = { input, meta: {} }
const wrapper = mount(<Input { ...props }/>)
expect(wrapper.find('input').prop('aria-describedby')).toContain(name)
})

test('Input id defaults to name when no id is provided', () => {
const props = { input, meta: {} }
const wrapper = mount(<Input { ...props } />)
Expand Down
8 changes: 7 additions & 1 deletion test/forms/inputs/range-input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ test('RangeInput sets the `min`, `max`, and `step` correctly', () => {
expect(wrapper.find('input').props().min).toEqual(5)
expect(wrapper.find('input').props().max).toEqual(50)
expect(wrapper.find('input').props().step).toEqual(10)
})
})

test('RangeInput has aria described by attribute', () => {
const props = { input, meta: {} }
const wrapper = mount(<RangeInput { ...props }/>)
expect(wrapper.find('input').prop('aria-describedby')).toContain(name)
})
Loading

0 comments on commit d94ef40

Please sign in to comment.