From 58acef747bb4e1c553c7a0c4d8a8df716be59c01 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 26 Feb 2018 22:31:07 -0800 Subject: [PATCH 01/57] add required field --- .../combobox/__docs__/storybook-stories.jsx | 4 + .../required-input-error-state.jsx | 151 ++++++++++++++++++ .../snapshot/base-label-required.jsx | 95 +++++++++++ .../combobox.snapshot-test.jsx.snap | 99 ++++++++++++ .../__tests__/combobox.snapshot-test.jsx | 7 + components/combobox/combobox.jsx | 25 +++ 6 files changed, 381 insertions(+) create mode 100644 components/combobox/__examples__/required-input-error-state.jsx create mode 100644 components/combobox/__examples__/snapshot/base-label-required.jsx diff --git a/components/combobox/__docs__/storybook-stories.jsx b/components/combobox/__docs__/storybook-stories.jsx index 3cd0237094..24120c1b2e 100644 --- a/components/combobox/__docs__/storybook-stories.jsx +++ b/components/combobox/__docs__/storybook-stories.jsx @@ -7,6 +7,7 @@ import Base from '../__examples__/base'; import BaseMenuSubHeader from '../__examples__/base-menu-subheader'; import BaseMenuSeparator from '../__examples__/base-menu-separator'; import BaseInheritMenuWidth from '../__examples__/base-inherit-menu-width.jsx'; +import RequiredInputErrorState from '../__examples__/required-input-error-state'; import PredefinedOptionsOnly from '../__examples__/base-predefined-options-only'; import InlineSingle from '../__examples__/inline-single'; import InlineMultiple from '../__examples__/inline-multiple'; @@ -30,6 +31,7 @@ import SnapshotReadonlyMultipleSelection from '../__examples__/snapshot/readonly import SnapshotReadonlyMultipleSelectionSingleItemSelected from '../__examples__/snapshot/readonly-multiple-selection-single-item-selected'; import SnapshotReadonlyMultipleSelectionMultipleItemsSelected from '../__examples__/snapshot/readonly-multiple-selection-multiple-items-selected'; import SnapshotReadonlySingleSelectionCustomMenuItemOpen from '../__examples__/snapshot/readonly-single-selection-custom-menu-item'; +import SnapshotBaseLabelRequired from '../__examples__/snapshot/base-label-required'; storiesOf(COMBOBOX, module) .addDecorator((getStory) => ( @@ -54,11 +56,13 @@ storiesOf(COMBOBOX, module) .add('Readonly Single Selection Custom Menu Item', () => ( )) + .add('Required Input in Error State', () => ) .add('Snapshot Base Open', () => ) .add('Snapshot Base Custom Menu Item Open', () => ( )) .add('Snapshot Base Selected', () => ) + .add('Snapshot Base Label Required', () => ) .add('Snapshot Base Open Menu Sub Header Separator', () => ( )) diff --git a/components/combobox/__examples__/required-input-error-state.jsx b/components/combobox/__examples__/required-input-error-state.jsx new file mode 100644 index 0000000000..ff82b82c30 --- /dev/null +++ b/components/combobox/__examples__/required-input-error-state.jsx @@ -0,0 +1,151 @@ +/* eslint-disable no-console, react/prop-types */ +import React from 'react'; +import Combobox from '~/components/combobox'; +import Icon from '~/components/icon'; +import comboboxFilterAndLimit from '~/components/combobox/filter'; +import IconSettings from '~/components/icon-settings'; + +const accounts = [ + { + id: '1', + label: 'Acme', + subTitle: 'Account • San Francisco', + type: 'account', + }, + { + id: '2', + label: 'Salesforce.com, Inc.', + subTitle: 'Account • San Francisco', + type: 'account', + }, + { + id: '3', + label: "Paddy's Pub", + subTitle: 'Account • Boston, MA', + type: 'account', + }, + { + id: '4', + label: 'Tyrell Corp', + subTitle: 'Account • San Francisco, CA', + type: 'account', + }, + { + id: '5', + label: 'Paper St. Soap Company', + subTitle: 'Account • Beloit, WI', + type: 'account', + }, + { + id: '6', + label: 'Nakatomi Investments', + subTitle: 'Account • Chicago, IL', + type: 'account', + }, + { id: '7', label: 'Acme Landscaping', subTitle: '\u00A0', type: 'account' }, + { + id: '8', + label: 'Acme Construction', + subTitle: 'Account • Grand Marais, MN', + type: 'account', + }, +]; + +const accountsWithIcon = accounts.map((elem) => + Object.assign(elem, { + icon: , + }) +); + +class Example extends React.Component { + constructor (props) { + super(props); + + this.state = { + inputValue: '', + selection: [], + }; + } + + render () { + return ( + + { + if (this.props.action) { + this.props.action('onChange')(event, value); + } else if (console) { + console.log('onChange', event, value); + } + this.setState({ inputValue: value }); + }, + onRequestRemoveSelectedOption: (event, data) => { + this.setState({ + inputValue: '', + selection: data.selection, + }); + }, + onSubmit: (event, { value }) => { + if (this.props.action) { + this.props.action('onChange')(event, value); + } else if (console) { + console.log('onChange', event, value); + } + this.setState({ + inputValue: 'input not found', + selection: [ + ...this.state.selection, + { + label: value, + icon: ( + + ), + }, + ], + }); + }, + onSelect: (event, data) => { + if (this.props.action) { + this.props.action('onSelect')( + event, + ...Object.keys(data).map((key) => data[key]) + ); + } else if (console) { + console.log('onSelect', event, data); + } + this.setState({ + inputValue: '', + selection: data.selection, + }); + }, + }} + errorText="This field is required" + labels={{ + label: 'Search', + labelRequired: true, + placeholder: 'Search Salesforce', + }} + multiple + options={comboboxFilterAndLimit({ + inputValue: this.state.inputValue, + limit: 10, + options: accountsWithIcon, + selection: this.state.selection, + })} + selection={this.state.selection} + value={this.state.inputValue} + /> + + ); + } +} + +Example.displayName = 'ComboboxExample'; +export default Example; // export is replaced with `ReactDOM.render(, mountNode);` at runtime \ No newline at end of file diff --git a/components/combobox/__examples__/snapshot/base-label-required.jsx b/components/combobox/__examples__/snapshot/base-label-required.jsx new file mode 100644 index 0000000000..60bf854a22 --- /dev/null +++ b/components/combobox/__examples__/snapshot/base-label-required.jsx @@ -0,0 +1,95 @@ +/* eslint-disable no-console, react/prop-types */ +import React from 'react'; +import Combobox from '~/components/combobox/combobox'; +import Icon from '~/components/icon'; +import escapeRegExp from 'lodash.escaperegexp'; +import IconSettings from '~/components/icon-settings'; + +const accounts = [ + { + id: '1', + label: 'Acme', + subTitle: 'Account • San Francisco', + type: 'account', + }, + { + id: '2', + label: 'Salesforce.com, Inc.', + subTitle: 'Account • San Francisco', + type: 'account', + }, +]; + +const accountsWithIcon = accounts.map((elem) => + Object.assign(elem, { + icon: , + }) +); + +class Example extends React.Component { + constructor (props) { + super(props); + + this.state = { + inputValue: '', + selection: [], + }; + } + + render () { + return ( + + { + console.log('onChange', value); + this.setState({ inputValue: value }); + }} + onRequestRemoveSelectedOption={(event, data) => { + this.setState({ + inputValue: '', + selection: [], + }); + }} + onSubmit={(event, { value }) => { + console.log('onSubmit', value); + this.setState({ + selection: [ + { + label: value, + icon: ( + + ), + }, + ], + }); + }} + onSelect={(event, data) => { + console.log('onSelect', data); + this.setState({ selection: data.selection }); + }} + options={accountsWithIcon} + selection={this.state.selection} + value={ + this.state.selectedOption + ? this.state.selectedOption.label + : this.state.inputValue + } + /> + + ); + } +} + +Example.displayName = 'ComboboxExample'; +export default Example; // export is replaced with `ReactDOM.render(, mountNode);` at runtime \ No newline at end of file diff --git a/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap b/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap index d6ca1e7e99..4cc3548e55 100644 --- a/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap +++ b/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap @@ -360,6 +360,105 @@ exports[`Base Custom Menu Item Open HTML Snapshot 1`] = ` " `; +exports[`Base Label Required DOM Snapshot 1`] = ` +
+ +
+
+
+
+ + + + +
+
+
+
+
+`; + +exports[`Base Label Required HTML Snapshot 1`] = ` +"
+
+
+
+
+
+
+
+
" +`; + exports[`Base Open Custom Class Name DOM Snapshot 1`] = `
+ {this.props.errorText && ( +
+ {this.props.errorText} +
+ )} {this.getDialog({ menuRenderer: this.renderMenu({ assistiveText, labels }), })} @@ -1194,6 +1218,7 @@ class Combobox extends React.Component { assistiveText={this.props.assistiveText.label} htmlFor={this.getId()} label={labels.label} + required={labels.labelRequired} /> {variantExists ? subRenders[this.props.variant][multipleOrSingle]( From 680889eea0d3ff43056d2922265094e867549704 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 1 Mar 2018 17:34:14 -0800 Subject: [PATCH 02/57] adding more tests --- .../snapshot/base-label-required.jsx | 2 ++ .../combobox.snapshot-test.jsx.snap | 34 ++++++++++++++----- .../__tests__/combobox.browser-test.jsx | 12 ++++++- components/combobox/combobox.jsx | 3 +- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/components/combobox/__examples__/snapshot/base-label-required.jsx b/components/combobox/__examples__/snapshot/base-label-required.jsx index 60bf854a22..a97833ff99 100644 --- a/components/combobox/__examples__/snapshot/base-label-required.jsx +++ b/components/combobox/__examples__/snapshot/base-label-required.jsx @@ -40,6 +40,8 @@ class Example extends React.Component { return (
-
+
@@ -383,11 +385,11 @@ exports[`Base Label Required DOM Snapshot 1`] = ` className="slds-combobox_container" >
+
+ Oops, this field is required! +
@@ -450,9 +458,11 @@ exports[`Base Label Required HTML Snapshot 1`] = ` "
-
+
+
Oops, this field is required!
@@ -470,6 +480,7 @@ exports[`Base Open Custom Class Name DOM Snapshot 1`] = ` className="slds-combobox_container" >
-
+
@@ -696,6 +708,7 @@ exports[`Base Open DOM Snapshot 1`] = ` className="slds-combobox_container" >
-
+
@@ -910,6 +923,7 @@ exports[`Base Open Menu Inherit Width Of Menu DOM Snapshot 1`] = ` className="slds-combobox_container" >
-
+
@@ -1066,6 +1080,7 @@ exports[`Base Open Menu Sub Header DOM Snapshot 1`] = ` className="slds-combobox_container" >
-
+
@@ -1217,6 +1232,7 @@ exports[`Base Selected DOM Snapshot 1`] = ` className="slds-combobox_container" >
-
+
diff --git a/components/combobox/__tests__/combobox.browser-test.jsx b/components/combobox/__tests__/combobox.browser-test.jsx index dc546ba88c..b7a0d44bee 100644 --- a/components/combobox/__tests__/combobox.browser-test.jsx +++ b/components/combobox/__tests__/combobox.browser-test.jsx @@ -211,12 +211,13 @@ describe('SLDSCombobox', function () { destroyMountNode({ wrapper, mountNode }); }); - it('has aria-haspopup, aria-expanded is false when closed, aria-expanded is true when open, ', function () { + it('has aria-haspopup, aria-expanded is false when closed, aria-expanded is true when open, has aria-describedby', function () { wrapper = mount(, { attachTo: mountNode }); const nodes = getNodes({ wrapper }); expect(nodes.combobox.node.getAttribute('aria-haspopup')).to.equal( 'listbox' ); + expect(nodes.combobox.node.getAttribute('aria-describedby')).to.not.be.null; // closed expect(nodes.combobox.node.getAttribute('aria-expanded')).to.equal( 'false' @@ -418,5 +419,14 @@ describe('SLDSCombobox', function () { .text() ).to.equal('No matches found.'); }); + + it('Should show the error text', function () { + const errorMessage = 'Field required.'; + wrapper = mount(, { attachTo: mountNode }); + expect(wrapper.props().errorText).to.equal( + errorMessage + ); + expect(wrapper.find('.slds-form-element__help').length).to.equal(1); + }); }); }); diff --git a/components/combobox/combobox.jsx b/components/combobox/combobox.jsx index f9edd9c009..3e807d0c19 100644 --- a/components/combobox/combobox.jsx +++ b/components/combobox/combobox.jsx @@ -54,7 +54,8 @@ const propTypes = { selectedListboxLabel: PropTypes.string, }), /** - * The `aria-describedby` attribute is used to indicate the IDs of the elements that describe the object. It is used to establish a relationship between widgets or groups and text that described them. This is very similar to aria-labelledby: a label describes the essence of an object, while a description provides more information that the user might need. + * The `aria-describedby` attribute is used to indicate the IDs of the elements that describe the object. It is used to establish a relationship between widgets or groups and text that described them. + * This is very similar to aria-labelledby: a label describes the essence of an object, while a description provides more information that the user might need. _Tested with snapshot testing._ */ 'aria-describedby': PropTypes.string, /** From 919c4e95a6299bee7c5c589f77778b33ee41c582 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 1 Mar 2018 21:22:25 -0800 Subject: [PATCH 03/57] fix bug on assistive text --- components/combobox/combobox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/combobox/combobox.jsx b/components/combobox/combobox.jsx index 3e807d0c19..96ebc5f52b 100644 --- a/components/combobox/combobox.jsx +++ b/components/combobox/combobox.jsx @@ -1216,7 +1216,7 @@ class Combobox extends React.Component { className={classNames('slds-form-element', props.classNameContainer)} >