From d9bad70eba0c4d14bb55d290549ab581b88d13e9 Mon Sep 17 00:00:00 2001 From: Scott Gould Date: Fri, 10 Nov 2017 20:31:55 +0000 Subject: [PATCH] Tie field to label without explicit id, and to supporting elements (#130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tie field to label without explicit id, and to supporting elements via aria attributes * Don’t need a map * Tweak id generation and add test * Add comment about make_id * Add changelog entries * Tweak changelog entries --- CHANGELOG.md | 2 ++ .../__snapshots__/form_row.test.js.snap | 4 ++- src/components/form/form_row/form_row.js | 26 +++++++++++++--- src/components/form/form_row/form_row.test.js | 30 ++++++++++++++++++- src/components/form/form_row/make_id.js | 5 ++++ 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/components/form/form_row/make_id.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ed2a2927e..8e40079ec31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - Changed the hover states of `EuiButtonEmpty` to look more like links. [#135](https://github.com/elastic/eui/pull/135) - Added `transparentBackground` prop to `EuiCodeBlock`. Made light theme the default. `EuiCode` now just wraps `EuiCodeBlock` so you can do inline highlighting. [#138](https://github.com/elastic/eui/pull/138) +- `EuiFormRow` generates its own unique `id` prop if none is provided. [(#130)](https://github.com/elastic/eui/pull/130) +- `EuiFormRow` associates help text and errors with the field element via ARIA attributes. [(#130)](https://github.com/elastic/eui/pull/130) # [`0.0.1`](https://github.com/elastic/eui/tree/v0.0.1) Initial Release diff --git a/src/components/form/form_row/__snapshots__/form_row.test.js.snap b/src/components/form/form_row/__snapshots__/form_row.test.js.snap index 30c0e39e62f..ef7e2fe5cf5 100644 --- a/src/components/form/form_row/__snapshots__/form_row.test.js.snap +++ b/src/components/form/form_row/__snapshots__/form_row.test.js.snap @@ -6,6 +6,8 @@ exports[`EuiFormRow is rendered 1`] = ` class="euiFormRow testClass1 testClass2" data-test-subj="test subject string" > - + `; diff --git a/src/components/form/form_row/form_row.js b/src/components/form/form_row/form_row.js index 8a44ec0daed..972bac4d2bf 100644 --- a/src/components/form/form_row/form_row.js +++ b/src/components/form/form_row/form_row.js @@ -9,12 +9,15 @@ import { EuiFormHelpText } from '../form_help_text'; import { EuiFormErrorText } from '../form_error_text'; import { EuiFormLabel } from '../form_label'; +import makeId from './make_id'; + export class EuiFormRow extends Component { constructor(props) { super(props); this.state = { isFocused: false, + id: props.id || makeId() }; this.onFocus = this.onFocus.bind(this); @@ -40,13 +43,14 @@ export class EuiFormRow extends Component { isInvalid, error, label, - id, hasEmptyLabelSpace, fullWidth, className, ...rest } = this.props; + const { id } = this.state; + const classes = classNames( 'euiFormRow', { @@ -60,7 +64,7 @@ export class EuiFormRow extends Component { if (helpText) { optionalHelpText = ( - + {helpText} ); @@ -70,8 +74,8 @@ export class EuiFormRow extends Component { if (error) { const errorTexts = Array.isArray(error) ? error : [error]; - optionalErrors = errorTexts.map(error => ( - + optionalErrors = errorTexts.map((error, i) => ( + {error} )); @@ -91,10 +95,24 @@ export class EuiFormRow extends Component { ); } + const describingIds = []; + if (optionalHelpText) { + describingIds.push(optionalHelpText.props.id); + } + if (optionalErrors) { + optionalErrors.forEach(error => describingIds.push(error.props.id)); + } + + const optionalProps = {}; + if (describingIds.length > 0) { + optionalProps[`aria-describedby`] = describingIds.join(` `); + } + const field = cloneElement(children, { id, onFocus: this.onFocus, onBlur: this.onBlur, + ...optionalProps }); return ( diff --git a/src/components/form/form_row/form_row.test.js b/src/components/form/form_row/form_row.test.js index 4120c12b3df..af6ca65e5c8 100644 --- a/src/components/form/form_row/form_row.test.js +++ b/src/components/form/form_row/form_row.test.js @@ -1,9 +1,11 @@ import React from 'react'; -import { render } from 'enzyme'; +import { shallow, render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; import { EuiFormRow } from './form_row'; +jest.mock(`./make_id`, () => () => `generated-id`); + describe('EuiFormRow', () => { test('is rendered', () => { const component = render( @@ -15,4 +17,30 @@ describe('EuiFormRow', () => { expect(component) .toMatchSnapshot(); }); + + test('ties together parts for accessibility', () => { + const props = { + label: `Label`, + helpText: `Help text`, + error: [ + `Error one`, + `Error two` + ] + }; + + const tree = shallow( + + + + ); + + expect(tree.find(`EuiFormLabel`).prop(`htmlFor`)).toEqual(`generated-id`); + expect(tree.find(`EuiFormHelpText`).prop(`id`)).toEqual(`generated-id-help`); + expect(tree.find(`EuiFormErrorText`).at(0).prop(`id`)).toEqual(`generated-id-error-0`); + expect(tree.find(`EuiFormErrorText`).at(1).prop(`id`)).toEqual(`generated-id-error-1`); + + expect(tree.find(`input`).prop(`id`)).toEqual(`generated-id`); + expect(tree.find(`input`).prop(`aria-describedby`)) + .toEqual(`generated-id-help generated-id-error-0 generated-id-error-1`); + }); }); diff --git a/src/components/form/form_row/make_id.js b/src/components/form/form_row/make_id.js new file mode 100644 index 00000000000..f6c3bc542ec --- /dev/null +++ b/src/components/form/form_row/make_id.js @@ -0,0 +1,5 @@ +// Generate statistically almost-certainly-unique `id`s for associating form +// inputs with their labels and other descriptive text elements. +export default function makeId() { + return Math.random().toString(36).slice(-8); +}