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

EuiDescribedFormGroup component #707

Merged
merged 14 commits into from
May 3, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Added `direction` prop to EuiFlexGroup ([#711](https://github.com/elastic/eui/pull/711))
- Added `EuiEmptyPrompt` which can be used as a placeholder over empty tables and lists ([#711](https://github.com/elastic/eui/pull/711))
- Added `EuiTabbedContent` ([#737](https://github.com/elastic/eui/pull/737))
- Added `EuiDescriptiveFormRow` component, a wrapper around `EuiFormRow`(s) to display it horizontally on desktop ([#707](https://github.com/elastic/eui/pull/707))

**Bug fixes**

Expand Down
26 changes: 26 additions & 0 deletions src-docs/src/views/form_layouts/form_layouts_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EuiCode,
EuiForm,
EuiFormRow,
EuiDescriptiveFormRow,
EuiCheckboxGroup,
EuiFieldNumber,
EuiFieldPassword,
Expand All @@ -28,6 +29,10 @@ import FormRows from './form_rows';
const formRowsSource = require('!!raw-loader!./form_rows');
const formRowsHtml = renderToHtml(FormRows);

import DescriptiveFormRows from './form_rows_descriptive';
const descriptiveFormRowsSource = require('!!raw-loader!./form_rows_descriptive');
const descriptiveFormRowsHtml = renderToHtml(DescriptiveFormRows);

import FullWidth from './full_width';
const fullWidthSource = require('!!raw-loader!./full_width');
const fullWidthHtml = renderToHtml(FullWidth);
Expand Down Expand Up @@ -73,6 +78,7 @@ export const FormLayoutsExample = {
EuiFieldText,
EuiForm,
EuiFormRow,
EuiDescriptiveFormRow,
EuiFilePicker,
EuiRange,
EuiRadioGroup,
Expand All @@ -81,6 +87,26 @@ export const FormLayoutsExample = {
EuiTextArea,
},
demo: <FormRows />,
}, {
title: 'Descriptive form rows',
source: [{
type: GuideSectionTypes.JS,
code: descriptiveFormRowsSource,
}, {
type: GuideSectionTypes.HTML,
code: descriptiveFormRowsHtml,
}],
text: (
<p>
Use <EuiCode>DescriptiveFormRows</EuiCode> component to associate multiple <EuiCode>EuiFormRows</EuiCode>.
It can also simply be used with one <EuiCode>EuiFormRows</EuiCode> as a way to display help text (or additional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small typo: EuiFormRows -> EuiFormRow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

text) next to the field instead of below (on mobile, will revert to being stacked).
</p>
),
props: {
EuiDescriptiveFormRow,
},
demo: <DescriptiveFormRows />,
}, {
title: 'Full-width',
source: [{
Expand Down
1 change: 0 additions & 1 deletion src-docs/src/views/form_layouts/form_rows.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export default class extends Component {
>
<EuiSwitch
name="switch"

label="Should we do this?"
checked={this.state.isSwitchChecked}
onChange={this.onSwitchChange}
Expand Down
190 changes: 190 additions & 0 deletions src-docs/src/views/form_layouts/form_rows_descriptive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, {
Component,
} from 'react';

import {
EuiButton,
EuiCheckboxGroup,
EuiCode,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiDescriptiveFormRow,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the name of this component EuiDescriptiveFormRow or EuiDescriptiveFormRows? Could I suggest we change the name to EuiDescribedFormGroup? Since this component isn't coupled to form rows any more and implying otherwise could make things confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to EuiDescribedFormGroup 😁

EuiFilePicker,
EuiRange,
EuiSelect,
EuiSwitch,
} from '../../../../src/components';

import makeId from '../../../../src/components/form/form_row/make_id';

export default class extends Component {
constructor(props) {
super(props);

const idPrefix = makeId();

this.state = {
isSwitchChecked: false,
checkboxes: [{
id: `${idPrefix}0`,
label: 'Option one',
}, {
id: `${idPrefix}1`,
label: 'Option two is checked by default',
}, {
id: `${idPrefix}2`,
label: 'Option three',
}],
checkboxIdToSelectedMap: {
[`${idPrefix}1`]: true,
},
radios: [{
id: `${idPrefix}4`,
label: 'Option one',
}, {
id: `${idPrefix}5`,
label: 'Option two is selected by default',
}, {
id: `${idPrefix}6`,
label: 'Option three',
}],
radioIdSelected: `${idPrefix}5`,
};
}

onSwitchChange = () => {
this.setState({
isSwitchChecked: !this.state.isSwitchChecked,
});
}

onCheckboxChange = optionId => {
const newCheckboxIdToSelectedMap = ({ ...this.state.checkboxIdToSelectedMap, ...{
[optionId]: !this.state.checkboxIdToSelectedMap[optionId],
} });

this.setState({
checkboxIdToSelectedMap: newCheckboxIdToSelectedMap,
});
}

onRadioChange = optionId => {
this.setState({
radioIdSelected: optionId,
});
}

render() {
return (
<EuiForm>
<EuiDescriptiveFormRow
id="single-example"
title={<h3>Single text field</h3>}
paddingSize="m"
description={
<span>
When using this with a single field where the text here serves as the help text for the input,
it is a good idea to give it <EuiCode>someID</EuiCode> and pass <EuiCode>someID-description</EuiCode> to
the form row&apos;s <EuiCode>describedByIds</EuiCode> prop.
</span>
}
>
<EuiFormRow
label="Text field"
describedByIds={['single-example-description']}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a consumer, I'd have to know about the implementation details of EuiDescriptiveFormRow to know that single-example-description is an available ID, and that it's associated with the element containing the description. As a maintainer of the component, I may not be aware that consumers have created dependencies upon this ID and I might change it unwittingly and break screen-reader accessibility for a bunch of UIs (which we probably wouldn't discover for awhile).

I think the solution is to apply the ID directly to the EuiFlexItem component which encompasses the title and the description. This way the consumer can just reference the same ID they're passing in already. I tested this and it seems to work well.

If we make this change, we'd have to update the documentation too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it so that the user will pass in idAria (React didn't like "ariaId") prop, which gets put on the EuiText description. They can pass the same ID to describedByIds so there won't be a case where they have to know how the description ID is generated.

If they pass id it'll just unfold to the top level div container for EuiDescribedFormRow 🙂

>
<EuiFieldText name="first" />
</EuiFormRow>
</EuiDescriptiveFormRow>

<EuiDescriptiveFormRow
title={<strong>Multiple fields</strong>}
paddingSize="s"
description="Here are three form rows. The first form row does not have a title."
>
<EuiFormRow
hasEmptyLabelSpace
helpText={
<span>
We do not pass <EuiCode>describedByIds</EuiCode> when there are multiple form rows.
</span>
}
>
<EuiSelect
hasNoInitialSelection
options={[
{ value: 'option_one', text: 'Option one' },
{ value: 'option_two', text: 'Option two' },
{ value: 'option_three', text: 'Option three' },
]}
/>
</EuiFormRow>

<EuiFormRow
label="File picker"
>
<EuiFilePicker />
</EuiFormRow>

<EuiFormRow
label="Range"
>
<EuiRange
min={0}
max={100}
name="range"
id="range"
/>
</EuiFormRow>
</EuiDescriptiveFormRow>

<EuiDescriptiveFormRow
title={<h2>Full width</h2>}
paddingSize="l"
description={
<span>
By default, <EuiCode>EuiDescriptiveFormRow</EuiCode> will be double the default width of form elements.
However, you can pass <EuiCode>fullWidth</EuiCode> prop to this, the individual field and row components
to expand to their container.
</span>
}
fullWidth
>
<EuiFormRow
label="Use a switch instead of a single checkbox"
fullWidth
>
<EuiSwitch
name="switch"
label="Should we do this?"
checked={this.state.isSwitchChecked}
onChange={this.onSwitchChange}
fullWidth
/>
</EuiFormRow>

<EuiFormRow>
<EuiFieldText name="second" />
</EuiFormRow>

<EuiFormRow
label="Checkboxes"
fullWidth
>
<EuiCheckboxGroup
options={this.state.checkboxes}
idToSelectedMap={this.state.checkboxIdToSelectedMap}
onChange={this.onCheckboxChange}
fullWidth
/>
</EuiFormRow>
</EuiDescriptiveFormRow>

<EuiButton type="submit" fill>
Save form
</EuiButton>
</EuiForm>
);
}
}
1 change: 1 addition & 0 deletions src-docs/src/views/form_layouts/full_width.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default () => (
fullWidth
/>
</EuiFormRow>

<EuiFormRow
label="Often useful for text areas"
fullWidth
Expand Down
1 change: 1 addition & 0 deletions src/components/form/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@import 'form_help_text/index';
@import 'form_label/index';
@import 'form_row/index';
@import 'form_row_descriptive/index';
@import 'radio/index';
@import 'range/index';
@import 'select/index';
Expand Down
18 changes: 18 additions & 0 deletions src/components/form/form_row/__snapshots__/form_row.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`EuiFormRow behavior onBlur is called in child 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label={
Expand Down Expand Up @@ -38,6 +39,7 @@ exports[`EuiFormRow behavior onBlur is called in child 1`] = `

exports[`EuiFormRow behavior onBlur works in parent even if not in child 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label={
Expand Down Expand Up @@ -74,6 +76,7 @@ exports[`EuiFormRow behavior onBlur works in parent even if not in child 1`] = `

exports[`EuiFormRow behavior onFocus is called in child 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label={
Expand Down Expand Up @@ -110,6 +113,7 @@ exports[`EuiFormRow behavior onFocus is called in child 1`] = `

exports[`EuiFormRow behavior onFocus works in parent even if not in child 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label={
Expand Down Expand Up @@ -157,6 +161,20 @@ exports[`EuiFormRow is rendered 1`] = `
</div>
`;

exports[`EuiFormRow props describedByIds is rendered 1`] = `
<div
className="euiFormRow"
id="generated-id-row"
>
<input
aria-describedby="generated-id-additional"
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
/>
</div>
`;

exports[`EuiFormRow props error as array is rendered 1`] = `
<div
class="euiFormRow"
Expand Down
9 changes: 7 additions & 2 deletions src/components/form/form_row/form_row.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export class EuiFormRow extends Component {
const onChildFocus = get(this.props, 'children.props.onFocus');
if (onChildFocus) {
onChildFocus(...args);

}

this.setState({
Expand Down Expand Up @@ -60,6 +59,7 @@ export class EuiFormRow extends Component {
hasEmptyLabelSpace,
fullWidth,
className,
describedByIds,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove all these describedByIds references now, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I think this is a pretty nice improvement because it allows people to associate form row fields with whatever other help text they want. I think it's OK to leave it in, but I would definitely add another bullet to the CHANGELOG mentioning the addition of this prop, and document it with an example in the docs site.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine. We can keep it in but we should have @aphelionz check it to make sure it doesn't duplicate the reading of the title and description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I'd love to hear @aphelionz's thoughts on this. I tested it in VoiceOver on OS X and this does repeat the title and description, but I think that's just what happens when you associate elements with other elements using aria-labelledby and aria-desrcribedby. They'll be read once as a regular part of the page text and then again as information associated with a specific element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add aria-hidden or some such thing to one of the pairs? It's not the worst thing in the world to read something twice (better than not reading it at all) but not ideal.

Copy link
Contributor

@cjcenizal cjcenizal May 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, to clarify the <p> in my second example is the result of the user specifying title={<p>This describes the input</p>}. I was referring to heading in the HTML sense, not in the sense of the title prop.

Copy link
Contributor

@aphelionz aphelionz May 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been staring at the WCAG docs trying to figure out which criterion this would fall under.... is the text required to be understood succinctly by a user before requiring input? Then it's a level A... however if it's not it falls to level AA.

Level A: https://www.w3.org/WAI/WCAG20/quickref/#minimize-error-cues

vs

Level AA: https://www.w3.org/WAI/WCAG20/quickref/#qr-navigation-mechanisms-descriptive

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should ensure that users use heading tags for the title by doing:

  title: PropTypes.node.isRequired,
  headingLevel: PropTypes.oneOf(HEADING_LEVELS).isRequired,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @jen-huang's latest changes are sound and result in a good-enough UX. Maybe we can come back and reassess accessibility later on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adjusted the aria and IDing a bit with the last update, but I think more consideration is needed for handling accessibility. I created #762 for more work 🙂

...rest
} = this.props;

Expand Down Expand Up @@ -109,7 +109,7 @@ export class EuiFormRow extends Component {
);
}

const describingIds = [];
const describingIds = [...describedByIds];
if (optionalHelpText) {
describingIds.push(optionalHelpText.props.id);
}
Expand Down Expand Up @@ -154,9 +154,14 @@ EuiFormRow.propTypes = {
helpText: PropTypes.node,
hasEmptyLabelSpace: PropTypes.bool,
fullWidth: PropTypes.bool,
/**
* IDs of additional elements that should be part of children's `aria-describedby`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for the descriptions!

*/
describedByIds: PropTypes.array,
};

EuiFormRow.defaultProps = {
hasEmptyLabelSpace: false,
fullWidth: false,
describedByIds: [],
};
11 changes: 11 additions & 0 deletions src/components/form/form_row/form_row.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ describe('EuiFormRow', () => {
.toMatchSnapshot();
});

test('describedByIds is rendered', () => {
const component = shallow(
<EuiFormRow describedByIds={['generated-id-additional']}>
<input/>
</EuiFormRow>
);

expect(component)
.toMatchSnapshot();
});

test('id is rendered', () => {
const component = render(
<EuiFormRow id="id">
Expand Down
Loading