Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ export RadioButtonGroup from './radio-button-group';
export SLDSRadioGroup from './radio-group';
export RadioGroup from './radio-group';

export SLDSSelect from './select';
export Select from './select';

export SLDSSlider from './slider';
export Slider from './slider';

Expand Down
14 changes: 14 additions & 0 deletions components/select/__docs__/site-stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This object is imported into the documentation site. An example for the documentation site should be part of the pull request for the component. The object key is the kabob case of the "URL folder". In the case of `http://localhost:8080/components/app-launcher/`, `app-launcher` is the `key`. The folder name is created by `components.component` value in `package.json`. The following uses webpack's raw-loader plugin to get "text files" that will be eval()'d by CodeMirror within the documentation site on page load.

/* eslint-env node */
/* eslint-disable global-require */

const siteStories = [
require('raw-loader!@salesforce/design-system-react/components/select/__examples__/base.jsx'),
require('raw-loader!@salesforce/design-system-react/components/select/__examples__/required.jsx'),
require('raw-loader!@salesforce/design-system-react/components/select/__examples__/error.jsx'),
require('raw-loader!@salesforce/design-system-react/components/select/__examples__/disabled.jsx'),
require('raw-loader!@salesforce/design-system-react/components/select/__examples__/multipleSelectionNarrow.jsx'),
];

module.exports = siteStories;
22 changes: 22 additions & 0 deletions components/select/__docs__/storybook-stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { SELECT } from '../../../utilities/constants';
import IconSettings from '../../icon-settings';

import Base from '../__examples__/base';
import Required from '../__examples__/required';
import Error from '../__examples__/error';
import Disabled from '../__examples__/disabled';
import MultipleSelectionNarrow from '../__examples__/multipleSelectionNarrow';

storiesOf(SELECT, module)
.addDecorator((getStory) => (
<div className="slds-p-around_medium">
<IconSettings iconPath="/assets/icons">{getStory()}</IconSettings>
</div>
))
.add('Base', () => <Base />)
.add('Required', () => <Required />)
.add('Error', () => <Error />)
.add('Disabled', () => <Disabled />)
.add('MultipleSelectionNarrow', () => <MultipleSelectionNarrow />);
29 changes: 29 additions & 0 deletions components/select/__examples__/base.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import IconSettings from '~/components/icon-settings';
import Select from '~/components/select';
import SelectContainer from '../private/container';
import Label from '../private/label';

class Example extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Select>
<Label id="select-01" label="Select Label" />
<SelectContainer
id="select-01"
options={[
'Please select',
'Option One',
'Option Two',
'Option Three',
]}
/>
</Select>
</IconSettings>
);
}
}

Example.displayName = 'BaseExample'; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime
export default Example;
30 changes: 30 additions & 0 deletions components/select/__examples__/disabled.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import IconSettings from '~/components/icon-settings';
import Select from '~/components/select';
import SelectContainer from '../private/container';
import Label from '../private/label';

class Example extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Select>
<Label id="select-01" label="Select Label" />
<SelectContainer
id="select-01"
options={[
'Please select',
'Option One',
'Option Two',
'Option Three',
]}
disabled
/>
</Select>
</IconSettings>
);
}
}

Example.displayName = 'DisabledExample'; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime
export default Example;
33 changes: 33 additions & 0 deletions components/select/__examples__/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import IconSettings from '~/components/icon-settings';
import Select from '~/components/select';
import SelectContainer from '../private/container';
import Label from '../private/label';
import Error from '../private/error';

class Example extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Select hasError>
<Label id="select-01" label="Select Label" required />
<SelectContainer
id="select-01"
errorText="This field is required"
aria-describedby="error-01"
options={[
'Please select',
'Option One',
'Option Two',
'Option Three',
]}
/>
<Error id="error-01" errorText="This field is required" />
</Select>
</IconSettings>
);
}
}

Example.displayName = 'ErrorExample'; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime
export default Example;
30 changes: 30 additions & 0 deletions components/select/__examples__/multipleSelectionNarrow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import IconSettings from '~/components/icon-settings';
import Select from '~/components/select';
import SelectContainer from '../private/container';
import Label from '../private/label';

class Example extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Select>
<Label id="select-01" label="Select Label" required />
<SelectContainer
id="select-01"
options={[
'Please select',
'Option One',
'Option Two',
'Option Three',
]}
isMultipleSelection
/>
</Select>
</IconSettings>
);
}
}

Example.displayName = 'MultipleSelectionNarrowExample'; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime
export default Example;
30 changes: 30 additions & 0 deletions components/select/__examples__/required.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import IconSettings from '~/components/icon-settings';
import Select from '~/components/select';
import SelectContainer from '../private/container';
Copy link
Contributor

Choose a reason for hiding this comment

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

No components in private folder should be used by consumers or in examples

import Label from '../private/label';
Copy link
Contributor

Choose a reason for hiding this comment

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

Label should be a prop of Select component.


class Example extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Select>
Copy link
Contributor

Choose a reason for hiding this comment

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

The outputted markup is good, but this should all be one component called Select, since I don't foresee any rearrangement of these such as the label after the select element, etc.

<Label id="select-01" label="Select Label" required />
<SelectContainer
id="select-01"
required
options={[
'Please select',
'Option One',
'Option Two',
'Option Three',
]}
/>
</Select>
</IconSettings>
);
}
}

Example.displayName = 'RequiredExample'; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime
export default Example;
39 changes: 39 additions & 0 deletions components/select/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

// # Select Component

// Implements the [Select design pattern](https://www.lightningdesignsystem.com/components/select/) in React.

import React from 'react';
import PropTypes from 'prop-types';
import classNames from '../../utilities/class-names';

const propTypes = {
/**
* Shows red borders in the input element is there is an error
*/
hasError: PropTypes.bool,
/**
* Shows the content of the Select component.
*/
children: PropTypes.node.isRequired,
};

class Select extends React.Component {
render() {
return (
<div
className={classNames('slds-form-element', {
'slds-has-error': this.props.hasError,
})}
>
{this.props.children}
</div>
);
}
}

Select.propTypes = propTypes;

export default Select;
75 changes: 75 additions & 0 deletions components/select/private/container.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

import React from 'react';
import PropTypes from 'prop-types';
import shortid from 'shortid';
import classNames from '../../../utilities/class-names';

const propTypes = {
/**
* 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.
*/
'aria-describedby': PropTypes.string,
/**
* Every select input must have a unique ID in order to support keyboard navigation and ARIA support.
*/
id: PropTypes.string,
/**
* Highlights the select input as a required field (does not perform any validation).
*/
required: PropTypes.bool,
/**
* Disables the select input and prevents editing the contents.
*/
disabled: PropTypes.bool,
/**
* Displays options in a multiple selection narrow format.
*/
isMultipleSelection: PropTypes.boolean,
/**
* Select options.
*/
options: PropTypes.arrayOf(PropTypes.string),
};

class SelectContainer extends React.Component {
componentWillMount() {
this.generatedId = shortid.generate();
if (this.props.errorText) {
this.generatedErrorId = shortid.generate();
}
}

getId = () => this.props.id || this.generatedId;

getOptions = () =>
this.props.options.map((optionValue) => <option>{optionValue}</option>);

render() {
return (
<div className="slds-form-element__control">
<div
className={classNames({
'slds-select_container': !this.props.isMultipleSelection,
})}
>
<select
className="slds-select"
id={this.getId()}
required={this.props.required || this.props.errorText}
multiple={this.props.isMultipleSelection}
disabled={this.props.disabled}
aria-describedBy={this.props['aria-describedby']}
>
{this.getOptions()}
</select>
</div>
</div>
);
}
}

SelectContainer.propTypes = propTypes;

export default SelectContainer;
28 changes: 28 additions & 0 deletions components/select/private/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/**
* Message to display when the input is in an error state. When this is present, also visually highlights the component as in error.
*/
errorText: PropTypes.oneOfType([PropTypes.node, PropTypes.string]) /**
* Every label must have a unique ID in order to support keyboard navigation and ARIA support.
*/,
id: PropTypes.string.isRequired,
};

/**
* The error text for the Select component
*/
const Error = ({ errorText, id }) => (
<div className="slds-form-element__help" id={id}>
{errorText}
</div>
);

Error.propTypes = propTypes;

export default Error;
43 changes: 43 additions & 0 deletions components/select/private/label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/**
* The label for the select component.
*/
label: PropTypes.string.isRequired /**
* Every label must have a unique ID in order to support keyboard navigation and ARIA support.
*/,
id: PropTypes.string.isRequired /**
* Highlights the input as a required field (does not perform any validation).
*/,
required: PropTypes.bool,
};

const defaultProps = {
required: 'false',
};

/**
* The label for the Select component
*/
const Label = ({ label, id, required }) => (
<label className="slds-form-element__label" htmlFor={id}>
{required === true ? (
<abbr className="slds-required" title="required">
*
</abbr>
) : (
''
)}
{label}
</label>
);

Label.propTypes = propTypes;
Label.defaultProps = defaultProps;

export default Label;
Loading