diff --git a/package/src/components/MultiSelect/v1/MultiSelect.js b/package/src/components/MultiSelect/v1/MultiSelect.js new file mode 100644 index 000000000..22f2234ec --- /dev/null +++ b/package/src/components/MultiSelect/v1/MultiSelect.js @@ -0,0 +1,625 @@ +import React, { Component } from "react"; +import isEqual from "lodash.isequal"; +import PropTypes from "prop-types"; +import ReactSelect from "react-select"; +import { applyTheme, CustomPropTypes } from "../../../utils"; + +// This is currently mostly to ensure that this stays above our code examples in the +// style guide UI, which have zIndexes of 2 and 3 in some places. This might need to +// be taken from the theme eventually, though, if people have problems in other places. +const MENU_Z_INDEX = 4; + +const nullDefaultEquals = (value1, value2) => (value1 || null) === (value2 || null); + +// Rather than pass through all props to react-select, we'll keep a whitelist +// to better control the usage and appearance of this component. +const supportedPassthroughProps = [ + "autoFocus", + "backspaceRemovesValue", + "blurInputOnSelect", + "captureMenuScroll", + "closeMenuOnSelect", + "components", + "defaultValue", + "escapeClearsValue", + "formatGroupLabel", + "formatOptionLabel", + "getOptionLabel", + "getOptionValue", + "hideSelectedOptions", + "id", + "inputValue", + "isClearable", + "isLoading", + "isOptionDisabled", + "isRtl", + "isSearchable", + "loadingMessage", + "minMenuHeight", + "maxMenuHeight", + "maxValueHeight", + "menuIsOpen", + "menuPlacement", + "noOptionsMessage", + "onBlur", + "onFocus", + "onInputChange", + "onKeyDown", + "onMenuOpen", + "onMenuClose", + "onMenuScrollToTop", + "onMenuScrollToBottom", + "pageSize", + "placeholder", + "screenReaderStatus", + "scrollMenuIntoView", + "tabSelectsValue", + "value" +]; + +function applyValidationColor(themeProp = "color") { + return (props) => { + let status; + if (props.errors && props.errors.length) { + status = "error"; + } else if (props.hasBeenValidated && props.value && props.value.length) { + status = "success"; + } else if (props.isFocused) { + status = "focus"; + } else { + status = "default"; + } + return applyTheme(`${themeProp}_${status}`)(props); + }; +} + +const getInputBorderColor = applyValidationColor("Input.borderColor"); +const getInputFontSize = applyTheme("Input.fontSize"); +const getSelectOptionHoverColor = applyTheme("MultiSelect.optionHoverColor"); +const getSelectedOptionBackgroundColor = applyTheme("MultiSelect.selectedOptionBackgroundColor"); +const getSelectIndicatorColor = applyTheme("MultiSelect.indicatorColor"); +const getSelectLetterSpacing = applyTheme("MultiSelect.letterSpacing"); +const getSelectTextColor = applyTheme("MultiSelect.textColor"); +const getInputFontFamily = applyTheme("Input.fontFamily"); + +function getCustomStyles(props) { + const { maxWidth } = props; + + // TODO isDark change bg color + return { + container(base) { + return { + ...base, + maxWidth, + fontFamily: getInputFontFamily(props), + fontSize: getInputFontSize(props), + paddingLeft: applyTheme("MultiSelect.paddingLeft")(props), + paddingRight: applyTheme("MultiSelect.paddingRight")(props) + }; + }, + control(base, state) { + return { + ...base, + "borderColor": getInputBorderColor({ ...props, isFocused: state.isFocused }), + "borderTopLeftRadius": applyTheme("MultiSelect.borderTopLeftRadius")(props), + "borderTopRightRadius": applyTheme("MultiSelect.borderTopRightRadius")(props), + "borderBottomLeftRadius": applyTheme("MultiSelect.borderBottomLeftRadius")(props), + "borderBottomRightRadius": applyTheme("MultiSelect.borderBottomRightRadius")(props), + "boxShadow": "none", + "cursor": "pointer", + "&:hover": { + borderColor: getInputBorderColor({ ...props, isFocused: true }) + } + }; + }, + singleValue(base) { + return { + ...base, + letterSpacing: getSelectLetterSpacing(props) + }; + }, + placeholder(base) { + return { + ...base, + letterSpacing: getSelectLetterSpacing(props) + }; + }, + option(base, state) { + let backgroundColor; + if (state.isSelected) { + backgroundColor = getSelectedOptionBackgroundColor(props); + } else if (state.isFocused) { + backgroundColor = getSelectOptionHoverColor(props); + } else { + backgroundColor = "#FFFFFF"; + } + + return { + ...base, + backgroundColor, + "color": getSelectTextColor(props), + "cursor": "pointer", + "letterSpacing": getSelectLetterSpacing(props), + ":hover": { + backgroundColor: getSelectOptionHoverColor(props) + } + }; + }, + dropdownIndicator(base, state) { + return { + ...base, + color: getSelectIndicatorColor(props), + transform: state.selectProps.menuIsOpen ? "rotateX(-180deg)" : "" + }; + }, + menuList(base) { + return { + ...base, + paddingTop: 0, + paddingBottom: 0 + }; + }, + menu(base) { + return { + ...base, + borderTopLeftRadius: applyTheme("SelectMenu.borderTopLeftRadius")(props), + borderTopRightRadius: applyTheme("SelectMenu.borderTopRightRadius")(props), + borderBottomLeftRadius: applyTheme("SelectMenu.borderBottomLeftRadius")(props), + borderBottomRightRadius: applyTheme("SelectMenu.borderBottomRightRadius")(props), + borderBottomStyle: "solid", + borderBottomWidth: applyTheme("SelectMenu.borderBottomWidth")(props), + borderBottomColor: applyTheme("SelectMenu.borderBottomColor")(props), + borderLeftStyle: "solid", + borderLeftWidth: applyTheme("SelectMenu.borderLeftWidth")(props), + borderLeftColor: applyTheme("SelectMenu.borderLeftColor")(props), + borderRightStyle: "solid", + borderRightWidth: applyTheme("SelectMenu.borderRightWidth")(props), + borderRightColor: applyTheme("SelectMenu.borderRightColor")(props), + marginTop: 0, + boxShadow: "none", + zIndex: MENU_Z_INDEX + }; + }, + multiValue(base) { + return { + ...base, + backgroundColor: applyTheme("MultiSelect.multiValueBackgroundColor")(props), + borderStyle: applyTheme("MultiSelect.multiValueBorderStyle")(props), + borderWidth: applyTheme("MultiSelect.multiValueBorderWidth")(props), + borderColor: applyTheme("MultiSelect.multiValueBorderColor")(props), + borderRadius: applyTheme("MultiSelect.multiValueBorderRadius")(props) + }; + }, + multiValueLabel(base) { + return { + ...base, + color: applyTheme("MultiSelect.multiValueLabelColor")(props), + fontSize: getInputFontSize(props) + }; + }, + multiValueRemove(base) { + return { + ...base, + "borderRadius": "0", + "fontSize": getInputFontSize(props), + "marginLeft": applyTheme("MultiSelect.multiValueRemoveLeftSpacing")(props), + ":hover": { + backgroundColor: applyTheme("MultiSelect.multiValueRemoveHoverBackgroundColor")(props), + color: applyTheme("MultiSelect.multiValueRemoveHoverColor")(props) + } + }; + } + }; +} + +class MultiSelect extends Component { + static propTypes = { + /** + * Alphabetize by option label + */ + alphabetize: PropTypes.bool, + /** + * Passed through to react-select package. Focus the control when it is mounted + */ + autoFocus: PropTypes.bool, + + /** + * Passed through to react-select package. Remove the currently focused option when the user presses backspace + */ + backspaceRemovesValue: PropTypes.bool, + + /** + * Passed through to react-select package. + * Remove focus from the input when the user selects an option (handy for dismissing the keyboard on touch devices) + */ + blurInputOnSelect: PropTypes.bool, + + /** + * Passed through to react-select package. When the user reaches the top/bottom of the menu, prevent scroll on the scroll-parent + */ + captureMenuScroll: PropTypes.bool, + + /** + * You can provide a `className` prop that will be applied to the outermost DOM element + * rendered by this component. We do not recommend using this for styling purposes, but + * it can be useful as a selector in some situations. + */ + className: PropTypes.string, + + /** + * Passed through to react-select package. Close the select menu when the user selects an option + */ + closeMenuOnSelect: PropTypes.bool, + + /** + * Passed through to react-select package. Custom components to use + */ + components: PropTypes.object, + + /** + * Passed through to react-select package. default selected values + */ + defaultValue: PropTypes.arrayOf(PropTypes.object), + + /** + * An array of validation errors + */ + errors: PropTypes.array, + + /** + * Passed through to react-select package. Clear all values when the user presses escape AND the menu is closed + */ + escapeClearsValue: PropTypes.bool, + + /** + * Passed through to react-select package. Formats group labels in the menu as React components + */ + formatGroupLabel: PropTypes.func, + + /** + * Passed through to react-select package. Formats option labels in the menu and control as React components + */ + formatOptionLabel: PropTypes.func, + + /** + * Passed through to react-select package. Resolves option data to a string to be displayed as the label by components + */ + getOptionLabel: PropTypes.func, + + /** + * Passed through to react-select package. Resolves option data to a string to compare options and specify value attributes + */ + getOptionValue: PropTypes.func, + + /** + * Enable when a input's value has been validated + */ + hasBeenValidated: PropTypes.bool, + + /** + * Passed through to react-select package. Hide the selected option from the menu + */ + hideSelectedOptions: PropTypes.bool, + + /** + * Passed through to react-select package. The value of the search input + */ + inputValue: PropTypes.string, + + /** + * Passed through to react-select package. Is the select value clearable + */ + isClearable: PropTypes.bool, + + /** + * Passed through to react-select package. Is the select in a state of loading (async) + */ + isLoading: PropTypes.bool, + + /** + * Passed through to react-select package. Override the built-in logic to detect whether an option is disabled + */ + isOptionDisabled: PropTypes.func, + + /** + * Passed through to react-select package as `isDisabled`. Should the user be able to edit this value? + */ + isReadOnly: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + + /** + * Passed through to react-select package. Is the select direction right-to-left + */ + isRtl: PropTypes.bool, + + /** + * Passed through to react-select package. Whether to enable search functionality + */ + isSearchable: PropTypes.bool, + + /** + * Passed through to react-select package. Async: Text to display when loading options + */ + loadingMessage: PropTypes.func, + + /** + * Passed through to react-select package. Maximum height of the menu before scrolling + */ + maxMenuHeight: PropTypes.number, + + /** + * Passed through to react-select package. Maximum height of the value container before scrolling + */ + maxValueHeight: PropTypes.number, + + /** + * Passed through to react-select package. Whether the menu is open + */ + menuIsOpen: PropTypes.bool, + + /** + * Passed through to react-select package. Default placement of the menu in relation to the control. 'auto' will flip + * when there isn't enough space below the control. + */ + menuPlacement: PropTypes.oneOf(["auto", "bottom", "top"]), + + /** + * Passed through to react-select package. Minimum height of the menu before flipping + */ + minMenuHeight: PropTypes.number, + + /** + * A name or object path that determines where in the closest form object this will appear. + */ + name: PropTypes.string, + + /** + * Passed through to react-select package. Text to display when there are no options + */ + noOptionsMessage: PropTypes.func, + + /** + * Passed through to react-select package. Handle blur events on the control + */ + onBlur: PropTypes.func, + + /** + * Called with the new selected value each time the user changes the selection + */ + onChange: PropTypes.func, + + /** + * Called with the new selected value each time the user changes the selection + */ + onChanging: PropTypes.func, + + /** + * Passed through to react-select package. Handle focus events on the control + */ + onFocus: PropTypes.func, + + /** + * Passed through to react-select package. Handle change events on the input + */ + onInputChange: PropTypes.func, + + /** + * Passed through to react-select package. Handle key down events on the select + */ + onKeyDown: PropTypes.func, + + /** + * Passed through to react-select package. Handle the menu closing + */ + onMenuClose: PropTypes.func, + + /** + * Passed through to react-select package. Handle the menu opening + */ + onMenuOpen: PropTypes.func, + + /** + * Passed through to react-select package. Fired when the user scrolls to the bottom of the menu + */ + onMenuScrollToBottom: PropTypes.func, + + /** + * Passed through to react-select package. Fired when the user scrolls to the top of the menu + */ + onMenuScrollToTop: PropTypes.func, + + /** + * The options to show, in Composable Form Spec format. + * @see http://forms.dairystatedesigns.com/user/input/#selection-inputs + */ + options: CustomPropTypes.options, + + /** + * Passed through to react-select package. Number of options to jump in menu when page{up|down} keys are used + */ + pageSize: PropTypes.number, + + /** + * Passed through to react-select package. Placeholder text for the select value + */ + placeholder: PropTypes.string, + + /** + * Passed through to react-select package. Status to relay to screen readers + */ + screenReaderStatus: PropTypes.func, + + /** + * Passed through to react-select package. Whether the menu should be scrolled into view when it opens + */ + scrollMenuIntoView: PropTypes.bool, + + /** + * Passed through to react-select package. Select the currently focused option when the user presses tab + */ + tabSelectsValue: PropTypes.bool, + + /** + * Set this to the current saved value, if editing, or a default value if creating. The closest form implementing + * the Composable Forms spec will pass this automatically. + */ + value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])) + }; + + static defaultProps = { + alphabetize: false, + isReadOnly: false, + isSearchable: false, + onChange() {}, + onChanging() {}, + options: [] + }; + + static isFormInput = true; + + constructor(props) { + super(props); + + this.validateOptions(props.options); + + this.state = { + value: props.value || null + }; + } + + componentWillMount() { + this.handleChanged(this.state.value); + } + + componentWillReceiveProps(nextProps) { + const { options, value } = this.props; + const { options: nextOptions, value: nextValue } = nextProps; + + // Whenever a changed value prop comes in, we reset state to that, thus becoming clean. + if (!nullDefaultEquals(value, nextValue)) { + this.setValue(nextValue); + } + + if (!isEqual(options, nextOptions)) { + this.validateOptions(nextOptions); + } + } + + getValue() { + return this.state.value; + } + + setValue(value) { + this.setState({ value }); + this.handleChanged(value); + } + + resetValue() { + this.setValue(this.props.value); + } + + handleChanged = (value) => { + const { onChange, onChanging } = this.props; + if (value !== this.lastValue) { + this.lastValue = value; + onChanging(value); + onChange(value); + } + }; + + handleSelectLibChanged = (selection) => { + const value = selection.map((item) => item.value); + this.setValue(value.length ? value : null); + }; + + // Input is dirty if value prop doesn't match value state. Whenever a changed + // value prop comes in, we reset state to that, thus becoming clean. + isDirty() { + return (this.state.value || null) !== (this.props.value || null); + } + + // Make sure all option values have the same data type, and record what that is + validateOptions(options) { + (options || []).forEach((option) => { + if (option.optgroup) { + this.validateOptions(option.options); + } else { + const checkDataType = typeof option.value; + if (!this.dataType) { + this.dataType = checkDataType; + } else if (checkDataType !== this.dataType) { + // eslint-disable-next-line + throw new Error( + `All option values must have the same data type. The data type of the first option is "${ + this.dataType + }" while the data type of the ${option.label} option is "${checkDataType}"`); + } + } + }); + } + + sortOptions = (thisOpt, nextOpt) => { + if (thisOpt.options) thisOpt.options.sort(this.sortOptions); + if (nextOpt.options) nextOpt.options.sort(this.sortOptions); + if (thisOpt.label > nextOpt.label) { + return 1; + } else if (nextOpt.label > thisOpt.label) { + return -1; + } + return 0; + }; + + render() { + const { className, alphabetize, isReadOnly, options } = this.props; + const { value } = this.state; + + // Unfortunately right now, react-select optgroup support is just a tad different from the + // composable form spec. Might be able to do a PR to get react-select updated. + const reactSelectOptions = options.map((opt) => { + if (opt.options) { + return { + label: opt.optgroup, + options: opt.options + }; + } + return opt; + }); + + if (alphabetize) { + reactSelectOptions.sort(this.sortOptions); + } + + const passthroughProps = {}; + supportedPassthroughProps.forEach((prop) => { + passthroughProps[prop] = this.props[prop]; + }); + + let optionValue; + if (value !== undefined && value !== null) { + optionValue = []; + reactSelectOptions.forEach((opt) => { + if (opt.options) { + opt.options.forEach((o) => { + if (value.includes(o.value)) optionValue.push(o); + }); + } + if (value.includes(opt.value)) optionValue.push(opt); + }); + } + + return ( + + ); + } +} + +export default MultiSelect; diff --git a/package/src/components/MultiSelect/v1/MultiSelect.md b/package/src/components/MultiSelect/v1/MultiSelect.md new file mode 100644 index 000000000..cb75fb7f8 --- /dev/null +++ b/package/src/components/MultiSelect/v1/MultiSelect.md @@ -0,0 +1,234 @@ +### Overview + +Multi Selects are form elements that allow the user to choose multiple values from a set of options. + +### Usage + +There are two basic types of multi selects: simple and searchable. + +#### Simple multi select + +The simple multi select can be used in cases where there are fewer than 10 options. + +```jsx +const options = [ + { value: 'chocolate', label: 'Chocolate' }, + { value: 'darkchocolate', label: 'Dark Chocolate' }, + { value: 'mintchip', label: 'Mint Chip' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' }, +]; + + +``` + +#### Searchable multi select + +A searchable multi select should be used when there are more than 10 options or when the options are not known until the user starts typing. + +```jsx +const options = [ + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' } +]; + + +``` + +In cases where you want a smaller width for the multi select, set `maxWidth` property to a number of pixels based on how much space your longest option label needs. + +```jsx +const options = [ + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' } +]; + + +``` + +#### Alphabetizing multi select options + +By default the `MultiSelect` will inherit the order of provided options. +To alphabetize by option label apply the `alphabetize` prop. + +```jsx +const options = [ + { value: 'mintchip', label: 'Mint Chip' }, + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' }, + { value: 'darkchocolate', label: 'Dark Chocolate' } +]; + + +``` + +#### Nested multi select options + +```jsx +const options = [{ + optgroup: 'Transportation', + options: [{ + value: "car", + label: 'Car' + }, + { + value: "bike", + label: "Bike" + }, + { + value: "jetpack", + label: "Jetpack" + } + ] + }, + { + optgroup: 'Plants', + options: [{ + value: 'tree', + label: "Tree" + }, + { + value: "cactus", + label: "Cactus" + }, + { + value: "lily", + label: "Lily" + } + ] + }, + { + optgroup: 'Athletes', + options: [{ + value: 'lebron', + label: 'Lebron James' + }, + { + value: "embiid", + label: "Joel Embiid" + }, + { + value: "antetokounmpo", + label: "Giannis Antetokounmpo" + } + ] + } +]; + + +``` + +#### Default multi select values +```jsx +const options = [ + { value: 'mintchip', label: 'Mint Chip' }, + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' }, + { value: 'darkchocolate', label: 'Dark Chocolate' } +]; + + +``` + +### States + +A multi select can be in one of three states: normal, invalid, or valid. Normal state is as shown previously + +#### Invalid state + +When used within a form, a selected value might be deemed invalid, either because the user has not selected a value or because the user has selected a value that is not allowed based on other information entered in the same form. In this case, one or more error objects can be passed in the `errors` prop and will cause the multi select to appear as invalid. + +```jsx +const options = [ + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' } +]; + + +``` + +#### Valid state + +When used within a form, a selected value might be deemed valid after its value has been checked. If the `errors` prop is empty and the `hasBeenValidated` prop is true and there is a selected value, the multi select will appear valid. + +```jsx +const options = [ + { value: 'mintchip', label: 'Mint Chip' }, + { value: 'chocolate', label: 'Chocolate' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'vanilla', label: 'Vanilla' }, + { value: 'darkchocolate', label: 'Dark Chocolate' } +]; + + +``` + +### Theme + +Assume that any theme prop that does not begin with "rui" is within `rui_components`. See [Theming Components](./#!/Theming%20Components). + +| Theme Prop | Default | Description | +| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------- | +| `Input.backgroundColor_dark` | white | Background color when `isDarkBackground` | +| `Input.backgroundColor_default` | black02 | Background color when not `isDarkBackground` | +| `Input.borderColor_default` | black20 | Border color in "default" state | +| `Input.borderColor_error` | red | Border color in "error" state | +| `Input.borderColor_focus` | teal | Border color in "focus" state | +| `Input.borderColor_success` | teal | Border color in "success" state | +| `Input.borderRadius` | 2px | Border radius for all corners | +| `Input.clearButtonColor` | coolGrey | Icon color for the clear button | +| `Input.clearButtonLargeBackgroundColor` | white | Background color for the large clear button | +| `Input.clearButtonLargeBorderColor` | coolGrey | Border color for the large clear button | +| `Input.color_default` | coolGrey500 | Input text color when in "default" state | +| `Input.color_disabled` | black25 | Input text color when in "disabled" state | +| `Input.color_error` | red | Input text color when in "error" state | +| `Input.color_focus` | coolGrey500 | Input text color when in "focus" state | +| `Input.color_success` | black55 | Input text color when in "success" state | +| `Input.fontFamily` | `'Source Sans Pro', 'Helvetica Neue', Helvetica, sans-serif` | Input text font | +| `Input.fontSize` | 14px | Input text font size | +| `Input.horizontalPadding` | 10px | Left and right padding | +| `Input.iconColor_default` | black55 | Icon color when in "default" state | +| `Input.iconColor_disabled` | black25 | Icon color when in "disabled" state | +| `Input.iconColor_error` | red | Icon color when in "error" state | +| `Input.iconColor_success` | forestGreen | Icon color when in "success" state | +| `Input.iconWrapperSize` | 1.429em | Height and width of the input icon | +| `Input.lineHeight` | 1 | Input line height | +| `Input.placeholderColor` | black20 | Placeholder text color | +| `Input.verticalPadding` | 8px | Top and bottom padding | +| `MultiSelect.borderBottomLeftRadius` | 2px | Border bottom left radius | +| `Select.borderBottomRightRadius` | 0 | Border bottom right radius | +| `Select.borderTopLeftRadius` | 0 | Border top left radius | +| `Select.borderTopRightRadius` | 2px | Border top right radius | +| `Select.indicatorColor` | coolGrey500 | Color of the indicator icon | +| `Select.letterSpacing` | 0.3px | Letter spacing | +| `Select.optionHoverColor` | reactionBlue100 | Background color of a menu option while hovering on it | +| `Select.selectedOptionBackgroundColor` | reactionBlue200 | Background color of the currently selected option in the menu | +| `Select.textColor` | coolGrey500 | Color of all text in the component | +| `SelectMenu.borderBottomColor` | black20 | Color of the bottom border of the options menu | +| `SelectMenu.borderBottomLeftRadius` | 2px | Border radius for the bottom left corner of the menu | +| `SelectMenu.borderBottomRightRadius` | 0 | Border radius for the bottom right corner of the menu | +| `SelectMenu.borderBottomWidth` | 1px | Width of the bottom border of the options menu | +| `SelectMenu.borderLeftColor` | black20 | Color of the left border of the options menu | +| `SelectMenu.borderLeftWidth` | 1px | Width of the left border of the options menu | +| `SelectMenu.borderRightColor` | black20 | Color of the right border of the options menu | +| `SelectMenu.borderRightWidth` | 1px | Width of the right border of the options menu | +| `SelectMenu.borderTopLeftRadius` | 0 | Border radius for the top left corner of the menu | +| `SelectMenu.borderTopRightRadius` | 2px | Border radius for the top right corner of the menu | +| `Select.multiValueBackgroundColor` | reactiionBlue100 | Background color for selected values | +| `Select.multiValueBorderStyle` | "solid" | Border style for selected values | +| `Select.multiValueBorderWidth` | 1px | Border width for selected values | +| `Select.multiValueBorderColor` | coolGrey300 | Border color for selected values | +| `Select.multiValueBorderRadius` | 2px | Border radius for selected values | +| `Select.multiValueLabelColor` | black65 | Text color for selected values | +| `Select.multiValueRemoveLeftSpacing` | 5px | Spacing between text and remove icon for selected values | +| `Select.multiValueRemoveBackgroundcolor` | coolGrey300 | Remove icon hover background color for selected values | +| `Select.multiValueRemoveLeftSpacing` | reactionBlue100 | Remove icon hover color for selected values | + +#### Typography + +None diff --git a/package/src/components/MultiSelect/v1/MultiSelect.test.js b/package/src/components/MultiSelect/v1/MultiSelect.test.js new file mode 100644 index 000000000..16187ee89 --- /dev/null +++ b/package/src/components/MultiSelect/v1/MultiSelect.test.js @@ -0,0 +1,33 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import MultiSelect from "./MultiSelect"; + +const OPTIONS = [ + { label: "A", value: "a" }, + { label: "B", value: "b" }, + { label: "C", value: "c" } +]; + +const PROPS = { + className: "react-select", + classNamePrefix: "react-select", + menuIsOpen: true +}; + +test("basic snapshot", () => { + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("alphabetize option snapshot", () => { + const UNORDERED_OPTIONS = [ + { label: "C", value: "c" }, + { label: "A", value: "a" }, + { label: "Z", value: "z" }, + { label: "E", value: "e" } + ]; + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); diff --git a/package/src/components/MultiSelect/v1/__snapshots__/MultiSelect.test.js.snap b/package/src/components/MultiSelect/v1/__snapshots__/MultiSelect.test.js.snap new file mode 100644 index 000000000..3a5e59660 --- /dev/null +++ b/package/src/components/MultiSelect/v1/__snapshots__/MultiSelect.test.js.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`alphabetize option snapshot 1`] = ` +
+
+
+
+ Select... +
+ +
+
+ +
+
+
+
+
+
+ A +
+
+ C +
+
+ E +
+
+ Z +
+
+
+
+
+`; + +exports[`basic snapshot 1`] = ` +
+
+
+
+ Select... +
+ +
+
+ +
+
+
+
+
+
+ A +
+
+ B +
+
+ C +
+
+
+
+
+`; diff --git a/package/src/components/MultiSelect/v1/index.js b/package/src/components/MultiSelect/v1/index.js new file mode 100644 index 000000000..c728fbf27 --- /dev/null +++ b/package/src/components/MultiSelect/v1/index.js @@ -0,0 +1 @@ +export { default } from "./MultiSelect"; diff --git a/package/src/theme/defaultComponentTheme.js b/package/src/theme/defaultComponentTheme.js index a7edf142c..b6553e91d 100644 --- a/package/src/theme/defaultComponentTheme.js +++ b/package/src/theme/defaultComponentTheme.js @@ -558,7 +558,28 @@ const rui_components = { cellPaddingRight: padding.eight, cellPaddingTop: padding.eight }, - + MultiSelect: { + borderBottomLeftRadius: standardBorderRadius, + borderBottomRightRadius: 0, + borderTopLeftRadius: 0, + borderTopRightRadius: standardBorderRadius, + indicatorColor: colors.coolGrey500, + letterSpacing: "0.3px", + optionHoverColor: colors.reactionBlue100, + paddingLeft: padding.two, + paddingRight: padding.two, + selectedOptionBackgroundColor: colors.reactionBlue200, + textColor: colors.coolGrey500, + multiValueBackgroundColor: colors.reactionBlue100, + multiValueBorderColor: colors.coolGrey300, + multiValueBorderStyle: "solid", + multiValueBorderWidth: "1px", + multiValueBorderRadius: standardBorderRadius, + multiValueLabelColor: colors.black65, + multiValueRemoveHoverBackgroundColor: colors.coolGrey300, + multiValueRemoveHoverColor: colors.reactionBlue100, + multiValueRemoveLeftSpacing: "5px" + }, PriceCompare: { typography: { color: colors.black25 @@ -584,7 +605,16 @@ const rui_components = { letterSpacing: "0.3px", optionHoverColor: colors.reactionBlue100, selectedOptionBackgroundColor: colors.reactionBlue200, - textColor: colors.coolGrey500 + textColor: colors.coolGrey500, + multiValueBackgroundColor: colors.reactionBlue100, + multiValueBorderColor: colors.coolGrey300, + multiValueBorderStyle: "solid", + multiValueBorderWidth: "1px", + multiValueBorderRadius: standardBorderRadius, + multiValueLabelColor: colors.black65, + multiValueRemoveHoverBackgroundColor: colors.coolGrey300, + multiValueRemoveHoverColor: colors.reactionBlue100, + multiValueRemoveLeftSpacing: "5px" }, SelectableItemRadioButton: { backgroundColor: colors.white, diff --git a/styleguide.config.js b/styleguide.config.js index 0564be2af..50a168116 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -421,6 +421,7 @@ module.exports = { "Checkbox", "ErrorsBlock", "Field", + "MultiSelect", "PhoneNumberInput", "QuantityInput", "Select", diff --git a/styleguide/src/appComponents.js b/styleguide/src/appComponents.js index 31101114f..70461a663 100644 --- a/styleguide/src/appComponents.js +++ b/styleguide/src/appComponents.js @@ -34,6 +34,7 @@ import InlineAlert from "../../package/src/components/InlineAlert/v1"; import InPageMenuItem from "../../package/src/components/InPageMenuItem/v1"; import Link from "../../package/src/components/Link/v1"; import MiniCartSummary from "../../package/src/components/MiniCartSummary/v1"; +import MultiSelect from "../../package/src/components/MultiSelect/v1"; import PhoneNumberInput from "../../package/src/components/PhoneNumberInput/v1"; import Price from "../../package/src/components/Price/v1"; import ProfileImage from "../../package/src/components/ProfileImage/v1"; @@ -88,6 +89,7 @@ export default { InPageMenuItem, Link, MiniCartSummary, + MultiSelect, PhoneNumberInput, Price, ProfileImage, diff --git a/styleguide/src/sections/InstallingandImporting.md b/styleguide/src/sections/InstallingandImporting.md index 8f3682ffe..817dee5a6 100755 --- a/styleguide/src/sections/InstallingandImporting.md +++ b/styleguide/src/sections/InstallingandImporting.md @@ -61,6 +61,7 @@ import Field from "@reactioncommerce/components/Field/v1"; import InPageMenuItem from "@reactioncommerce/components/InPageMenuItem/v1"; import Link from "@reactioncommerce/components/Link/v1"; import MiniCartSummary from "@reactioncommerce/components/MiniCartSummary/v1"; +import MultiSelect from "@reactioncommerce/components/MultiSelect/v1"; import PhoneNumberInput from "@reactioncommerce/components/PhoneNumberInput/v1"; import Price from "@reactioncommerce/components/Price/v1"; import ProfileImage from "@reactioncommerce/components/ProfileImage/v1"; @@ -108,6 +109,7 @@ export default { InPageMenuItem, Link, MiniCartSummary, + MultiSelect, PhoneNumberInput, Price, ProfileImage,