diff --git a/README.md b/README.md index 9989c76cde..f411488552 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ $ npm install @salesforce-ux/design-system @salesforce/design-system-react Welcome to the project! :wave: This library is the [React](https://facebook.github.io/react/) implementation of the [Salesforce Lightning Design System](https://www.lightningdesignsystem.com/). This library has a peer dependency on `@salesforce-ux/design-system`, `react`, and `react-dom`. It has been tested with React >=15.4.1 <16 and is stable despite its version number. Please polyfill this library in order to meet your target environment needs. * [Usage](#usage) -* [Getting Started](https://react.lightningdesignsystem.com/getting-started/) +* [Getting started](https://react.lightningdesignsystem.com/getting-started/) * [Documentation and interactive examples](https://react.lightningdesignsystem.com) * [Contributing](CONTRIBUTING.md) -* [Codebase Overview](docs/codebase-overview.md) +* [Codebase overview](docs/codebase-overview.md) +* [Browser compatiblity and polyfills](docs/browser-compatibility.md) * [Usage with Webpack](docs/webpack.md) +* [Open Sourcing Design System React](https://engineering.salesforce.com/open-sourcing-design-system-react-9be45b8bb127) - Medium article ## Usage diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 623b4b190d..be242c8486 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,11 +31,49 @@ These are changes that have backwards-compatible solutions present and that comp ### Latest Release +## Release 0.8.12 + +**Minor features** +* Affix position of Dialogs with `hasStaticAlignment` prop. Allows greater control of Tooltip, Popover, and dialog components. +* Set Datepicker's initial year dropdown menu highlight selection to current selected date. + +## Release 0.8.11 + +**Minor features** +* Allow `input` label of combobox to show as required +* Input, Checkbox, and Radio support initial state (uncontrolled) in order to support applications with server-side form submission that are transitioning to atomic state. + +**Maintenance** +* Lint warnings from a third-party package have been removed from CI tests. + +**Documentation** +* Prop doc typos: + * replaced rendered input block to code block + * fixed broken link to source of Inline Edit Inputs + +## Release 0.8.10 + +**Minor features** +* Combobox supports error messages. + +**Outside SLDS pattern added** +* Multiple selection Combobox error messages should be placed after pillboxes with an additional `slds-has-error` wrapping div. + +**Maintenance** +* Update Dropdown Menu children description +* Update SLDS peer dependency to allow 2.6.0-alphas +* Remove plus-plus (`var++`) instances from library for clarity + +**Notice** +`package.module` has been removed from the NPM module until a transpiled ES6 module build can be published to support it. The current `package.module` is considered broken already for Create React Apps--for instance, so this is not considered a breaking change. Your module bundler will just use the CommonJS build unless you are already transpiling the source code, so no changes should be need to be made. + ## Release 0.8.9 **Minor features** * Combobox menu supports subheadings and line separators. + +**Outside SLDS pattern added** * UX pattern created for Combobox autocomplete that limits subheadings to those that have "child" matching items. ## Release 0.8.8 diff --git a/components/button-group/__tests__/__snapshots__/button-group.snapshot-test.jsx.snap b/components/button-group/__tests__/__snapshots__/button-group.snapshot-test.jsx.snap index 8c63ae1bc0..ab631bec6f 100644 --- a/components/button-group/__tests__/__snapshots__/button-group.snapshot-test.jsx.snap +++ b/components/button-group/__tests__/__snapshots__/button-group.snapshot-test.jsx.snap @@ -24,6 +24,7 @@ exports[`Button Group Checkbox DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleMon" name={undefined} @@ -57,6 +58,7 @@ exports[`Button Group Checkbox DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleTue" name={undefined} @@ -90,6 +92,7 @@ exports[`Button Group Checkbox DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleWed" name={undefined} @@ -123,6 +126,7 @@ exports[`Button Group Checkbox DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleThu" name={undefined} @@ -156,6 +160,7 @@ exports[`Button Group Checkbox DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleFri" name={undefined} @@ -209,6 +214,7 @@ exports[`Button Group Checkbox Error DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleMon" name={undefined} @@ -242,6 +248,7 @@ exports[`Button Group Checkbox Error DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleTue" name={undefined} @@ -275,6 +282,7 @@ exports[`Button Group Checkbox Error DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleWed" name={undefined} @@ -308,6 +316,7 @@ exports[`Button Group Checkbox Error DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleThu" name={undefined} @@ -341,6 +350,7 @@ exports[`Button Group Checkbox Error DOM Snapshot 1`] = ` aria-owns={undefined} aria-required={undefined} checked={undefined} + defaultChecked={undefined} disabled={undefined} id="ButtonGroupExampleFri" name={undefined} diff --git a/components/combobox/__docs__/site-stories.js b/components/combobox/__docs__/site-stories.js index dbd73f090f..2e6d9597e8 100644 --- a/components/combobox/__docs__/site-stories.js +++ b/components/combobox/__docs__/site-stories.js @@ -12,6 +12,7 @@ const siteStories = [ require('raw-loader!@salesforce/design-system-react/components/combobox/__examples__/inline-single.jsx'), require('raw-loader!@salesforce/design-system-react/components/combobox/__examples__/readonly-single.jsx'), require('raw-loader!@salesforce/design-system-react/components/combobox/__examples__/readonly-multiple.jsx'), + require('raw-loader!@salesforce/design-system-react/components/combobox/__examples__/required-input-error-state.jsx'), ]; module.exports = siteStories; diff --git a/components/combobox/__docs__/storybook-stories.jsx b/components/combobox/__docs__/storybook-stories.jsx index 3cd0237094..979bcec7fe 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,17 @@ 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..298f32200e --- /dev/null +++ b/components/combobox/__examples__/required-input-error-state.jsx @@ -0,0 +1,152 @@ +/* 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', + placeholder: 'Search Salesforce', + }} + multiple + options={comboboxFilterAndLimit({ + inputValue: this.state.inputValue, + limit: 10, + options: accountsWithIcon, + selection: this.state.selection, + })} + required + selection={this.state.selection} + value={this.state.inputValue} + /> + + ); + } +} + +Example.displayName = 'ComboboxExample'; +export default Example; // export is replaced with `ReactDOM.render(, mountNode);` at runtime 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..ae62509fdf --- /dev/null +++ b/components/combobox/__examples__/snapshot/base-label-required.jsx @@ -0,0 +1,12 @@ +/* eslint-disable no-console, react/prop-types */ +import React from 'react'; +import RequiredInputErrorStateComponent from '../required-input-error-state'; + +class Example extends React.Component { + render () { + return ; + } +} + +Example.displayName = 'ComboboxExample'; +export default Example; // export is replaced with `ReactDOM.render(, mountNode);` at runtime diff --git a/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap b/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap index d6ca1e7e99..ef244167b6 100644 --- a/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap +++ b/components/combobox/__tests__/__snapshots__/combobox.snapshot-test.jsx.snap @@ -38,6 +38,7 @@ exports[`Base Custom Menu Item Open DOM Snapshot 1`] = ` aria-required={undefined} autoComplete="off" className="slds-input slds-combobox__input" + defaultValue={undefined} disabled={undefined} id="combobox-unique-id" maxLength={undefined} @@ -56,7 +57,7 @@ exports[`Base Custom Menu Item Open DOM Snapshot 1`] = ` onSubmit={undefined} placeholder="Search Salesforce" readOnly={false} - required={undefined} + required={false} role="textbox" style={undefined} tabIndex={undefined} @@ -360,6 +361,118 @@ exports[`Base Custom Menu Item Open HTML Snapshot 1`] = ` " `; +exports[`Base Label Required DOM Snapshot 1`] = ` +
+ +
+
+
+
+ + + + +
+
+
+
+
+ This field is required +
+
+
+
+`; + +exports[`Base Label Required HTML Snapshot 1`] = ` +"
+
+
+
+
+
+
+
+
This field is required
+
+
+
" +`; + exports[`Base Open Custom Class Name DOM Snapshot 1`] = `
, { attachTo: mountNode }); const nodes = getNodes({ wrapper }); expect(nodes.combobox.node.getAttribute('aria-haspopup')).to.equal( diff --git a/components/combobox/__tests__/combobox.snapshot-test.jsx b/components/combobox/__tests__/combobox.snapshot-test.jsx index 442dea2ee8..533ff122aa 100644 --- a/components/combobox/__tests__/combobox.snapshot-test.jsx +++ b/components/combobox/__tests__/combobox.snapshot-test.jsx @@ -8,6 +8,7 @@ import SnapshotBaseOpenMenuSubHeader from '../__examples__/snapshot/base-open-me import SnapshotBaseOpenMenuInheritWidthOf from '../__examples__/snapshot/base-open-menu-inheritWidthOf'; import SnapshotBaseOpenClassName from '../__examples__/snapshot/base-open-class-name'; import SnapshotBaseSelected from '../__examples__/snapshot/base-selected'; +import SnapshotBaseLabelRequired from '../__examples__/snapshot/base-label-required'; import SnapshotInlineSingleSelection from '../__examples__/snapshot/inline-single-selection'; import SnapshotInlineSingleSelectionSelected from '../__examples__/snapshot/inline-single-selection-selected'; import SnapshotInlineMultipleSelection from '../__examples__/snapshot/inline-multiple-selection'; @@ -40,6 +41,12 @@ testDOMandHTML({ Component: SnapshotBaseOpenClassName, }); +testDOMandHTML({ + name: 'Base Label Required', + test, + Component: SnapshotBaseLabelRequired, +}); + testDOMandHTML({ name: 'Base Open Menu Sub Header', test, diff --git a/components/combobox/combobox.jsx b/components/combobox/combobox.jsx index b84d2f4d2d..846f4f2c02 100644 --- a/components/combobox/combobox.jsx +++ b/components/combobox/combobox.jsx @@ -53,6 +53,11 @@ const propTypes = { removePill: PropTypes.string, 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. _Tested with snapshot testing._ + */ + 'aria-describedby': PropTypes.string, /** * CSS classes to be added to tag with `.slds-combobox`. Uses `classNames` [API](https://github.com/JedWatson/classnames). _Tested with snapshot testing._ */ @@ -112,7 +117,11 @@ const propTypes = { onSubmit: PropTypes.func, }), /** - * By default, dialogs will flip their alignment (such as bottom to top) if they extend beyond a boundary element such as a scrolling parent or a window/viewpoint. This is the opposite of "flippable." + * Message to display when the input is in an error state. When this is present, also visually highlights the component as in error. _Tested with snapshot testing._ + */ + errorText: PropTypes.string, + /** + * By default, dialogs will flip their alignment (such as bottom to top) if they extend beyond a boundary element such as a scrolling parent or a window/viewpoint. `hasStaticAlignment` disables this behavior and allows this component to extend beyond boundary elements. _Not tested._ */ hasStaticAlignment: PropTypes.bool, /** @@ -191,6 +200,10 @@ const propTypes = { * Limits auto-complete input submission to one of the provided options. _Tested with mocha testing._ */ predefinedOptionsOnly: PropTypes.bool, + /** + * Applies label styling for a required form element. _Tested with snapshot testing._ + */ + required: PropTypes.bool, /** * Accepts an array of item objects. For single selection, pass in an array of one object. _Tested with snapshot testing._ */ @@ -221,6 +234,7 @@ const defaultProps = { inheritWidthOf: 'target', menuPosition: 'absolute', readOnlyMenuItemVisibleLength: 5, + required: false, selection: [], variant: 'base', }; @@ -252,6 +266,9 @@ class Combobox extends React.Component { checkProps(COMBOBOX, this.props); this.generatedId = shortid.generate(); + if (this.props.errorText) { + this.generatedErrorId = shortid.generate(); + } } componentWillReceiveProps (nextProps) { @@ -320,6 +337,10 @@ class Combobox extends React.Component { ) : null; } + getErrorId () { + return this.props['aria-describedby'] || this.generatedErrorId; + } + /** * Shared class property getter methods */ @@ -680,6 +701,9 @@ class Combobox extends React.Component { { 'slds-is-open': this.getIsOpen(), }, + { + 'slds-has-error': props.errorText, + }, props.className )} aria-expanded={this.getIsOpen()} @@ -696,6 +720,7 @@ class Combobox extends React.Component { ? `${this.getId()}-listbox-option-${this.state.activeOption.id}` : null } + aria-describedby={this.getErrorId()} autoComplete="off" className="slds-combobox__input" containerProps={{ @@ -722,6 +747,7 @@ class Combobox extends React.Component { readOnly={ !!(props.predefinedOptionsOnly && this.state.activeOption) } + required={props.required} role="textbox" value={ props.predefinedOptionsOnly @@ -752,6 +778,16 @@ class Combobox extends React.Component { selection={props.selection} listboxHasFocus={this.state.listboxHasFocus} /> + {props.errorText && ( +
+
+ {props.errorText} +
+
+ )}
); @@ -790,6 +826,9 @@ class Combobox extends React.Component { { 'slds-is-open': this.getIsOpen(), }, + { + 'slds-has-error': props.errorText, + }, props.className )} aria-expanded={this.getIsOpen()} @@ -804,6 +843,7 @@ class Combobox extends React.Component { ? `${this.getId()}-listbox-option-${this.state.activeOption.id}` : null } + aria-describedby={this.getErrorId()} autoComplete="off" className="slds-combobox__input" containerProps={{ @@ -832,6 +872,7 @@ class Combobox extends React.Component { readOnly={ !!(props.predefinedOptionsOnly && this.state.activeOption) } + required={props.required} role="textbox" value={ props.predefinedOptionsOnly @@ -843,6 +884,11 @@ class Combobox extends React.Component { {this.getDialog({ menuRenderer: this.renderMenu({ assistiveText, labels }), })} + {props.errorText && ( +
+ {props.errorText} +
+ )} @@ -878,6 +924,9 @@ class Combobox extends React.Component { { 'slds-is-open': this.getIsOpen(), }, + { + 'slds-has-error': props.errorText, + }, props.className )} aria-expanded={this.getIsOpen()} @@ -894,6 +943,7 @@ class Combobox extends React.Component { }` : null } + aria-describedby={this.getErrorId()} autoComplete="off" className="slds-combobox__input" containerProps={{ @@ -939,6 +989,7 @@ class Combobox extends React.Component { !!(props.predefinedOptionsOnly && this.state.activeOption) || !!props.selection.length } + required={props.required} role="textbox" value={ props.predefinedOptionsOnly @@ -951,6 +1002,11 @@ class Combobox extends React.Component { {this.getDialog({ menuRenderer: this.renderMenu({ assistiveText, labels }), })} + {props.errorText && ( +
+ {props.errorText} +
+ )} @@ -1012,6 +1068,9 @@ class Combobox extends React.Component { { 'slds-is-open': this.getIsOpen(), }, + { + 'slds-has-error': props.errorText, + }, props.className )} aria-expanded={this.getIsOpen()} @@ -1028,6 +1087,7 @@ class Combobox extends React.Component { }` : null } + aria-describedby={this.getErrorId()} autoComplete="off" className="slds-combobox__input" containerProps={{ @@ -1054,6 +1114,7 @@ class Combobox extends React.Component { }} placeholder={labels.placeholderReadOnly} readOnly + required={props.required} role="textbox" value={value} /> @@ -1081,6 +1142,16 @@ class Combobox extends React.Component { variant={this.props.variant} renderAtSelectionLength={2} /> + {props.errorText && ( +
+
+ {props.errorText} +
+
+ )} ); }; @@ -1101,6 +1172,9 @@ class Combobox extends React.Component { { 'slds-is-open': this.getIsOpen(), }, + { + 'slds-has-error': props.errorText, + }, props.className )} aria-expanded={this.getIsOpen()} @@ -1117,6 +1191,7 @@ class Combobox extends React.Component { }` : null } + aria-describedby={this.getErrorId()} autoComplete="off" className="slds-combobox__input" containerProps={{ @@ -1143,6 +1218,7 @@ class Combobox extends React.Component { }} placeholder={labels.placeholderReadOnly} readOnly + required={props.required} role="textbox" value={ (this.state.activeOption && this.state.activeOption.label) || @@ -1152,6 +1228,11 @@ class Combobox extends React.Component { {this.getDialog({ menuRenderer: this.renderMenu({ assistiveText, labels }), })} + {props.errorText && ( +
+ {props.errorText} +
+ )} @@ -1191,9 +1272,10 @@ class Combobox extends React.Component { className={classNames('slds-form-element', props.classNameContainer)} >