Skip to content

Commit 0741b2b

Browse files
committed
feat: add MultiSelect component
Signed-off-by: Will Lopez <[email protected]>
1 parent bb0cd66 commit 0741b2b

File tree

15 files changed

+743
-6
lines changed

15 files changed

+743
-6
lines changed

package/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
"dependencies": {
2424
"@babel/runtime": "~7.3.1",
2525
"accounting-js": "~1.1.1",
26+
"clsx": "^1.0.4",
2627
"lodash.debounce": "~4.0.8",
2728
"lodash.get": "~4.4.2",
2829
"lodash.isempty": "~4.4.0",
2930
"lodash.isequal": "~4.5.0",
3031
"lodash.uniqueid": "~4.0.1",
3132
"mdi-material-ui": "~5.8.0",
32-
"react-is": "~16.4.1"
33+
"react-is": "~16.4.1",
34+
"react-select": "^3.0.4"
3335
},
3436
"devDependencies": {
3537
"@babel/cli": "~7.2.3",
@@ -86,8 +88,8 @@
8688
"access": "public"
8789
},
8890
"peerDependencies": {
89-
"@reactioncommerce/components-context": "~1.2.0",
9091
"@material-ui/core": ">=4.2.0 < 5",
92+
"@reactioncommerce/components-context": "~1.2.0",
9193
"prop-types": "~15.6.2",
9294
"react-dom": "~16.8.6"
9395
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React, { useState } from "react";
2+
import PropTypes from "prop-types";
3+
import Select from "react-select";
4+
import AsyncSelect from "react-select/async";
5+
import { makeStyles, useTheme } from "@material-ui/core/styles";
6+
import {
7+
Control,
8+
Menu,
9+
MultiValue,
10+
NoOptionsMessage,
11+
Option,
12+
Placeholder,
13+
ValueContainer
14+
} from "./helpers";
15+
16+
const useStyles = makeStyles((theme) => ({
17+
root: {
18+
flexGrow: 1,
19+
height: 250,
20+
minWidth: 290
21+
},
22+
input: {
23+
display: "flex",
24+
padding: 0,
25+
height: "auto"
26+
},
27+
valueContainer: {
28+
display: "flex",
29+
alignItems: "center",
30+
flexWrap: "wrap",
31+
flex: 1,
32+
overflow: "hidden",
33+
paddingLeft: theme.spacing(1),
34+
paddingRight: theme.spacing(1)
35+
},
36+
chip: {
37+
margin: theme.spacing(0.5, 0.25)
38+
},
39+
noOptionsMessage: {
40+
padding: theme.spacing(1, 2)
41+
},
42+
placeholder: {
43+
position: "absolute",
44+
left: theme.spacing(1),
45+
fontSize: theme.typography.fontSize
46+
},
47+
paper: {
48+
position: "absolute",
49+
zIndex: 1,
50+
marginTop: theme.spacing(1),
51+
left: 0,
52+
right: 0
53+
},
54+
divider: {
55+
height: theme.spacing(2)
56+
}
57+
}));
58+
59+
// Rather than pass through all props to react-select, we'll keep a whitelist
60+
// to better control the usage and appearance of this component.
61+
const supportedPassthroughProps = [
62+
"async",
63+
"cacheOptions",
64+
"classes",
65+
"defaultOptions",
66+
"loadOptions",
67+
"placeholder",
68+
"onSelection",
69+
"options"
70+
];
71+
72+
// Custom components for various aspects of the select
73+
const components = {
74+
Control,
75+
Menu,
76+
MultiValue,
77+
NoOptionsMessage,
78+
Option,
79+
Placeholder,
80+
ValueContainer
81+
};
82+
83+
/**
84+
* @name MultiSelect
85+
* @summary A Select component that supports selecting multiple options, and
86+
* loading options asynchronously and synchronously.
87+
* @param {Object} props - component props
88+
* @returns {React.Component} A React component
89+
*/
90+
const MultiSelect = React.forwardRef(function MultiSelect(props, ref) {
91+
const defaultClasses = useStyles();
92+
const theme = useTheme();
93+
const [value, setValue] = useState(null);
94+
95+
const passThroughProps = {};
96+
supportedPassthroughProps.forEach((supportedProp) => {
97+
passThroughProps[supportedProp] = props[supportedProp];
98+
});
99+
100+
const { classes, isAsync, onSelection } = props;
101+
const SelectComponent = isAsync ? AsyncSelect : Select;
102+
103+
/**
104+
*
105+
* @param {String} selectedValue The selected value
106+
* @returns {undefined} nothing
107+
*/
108+
function handleChangeMulti(selectedValue) {
109+
setValue(selectedValue);
110+
onSelection(selectedValue);
111+
}
112+
113+
const selectStyles = {
114+
input: (base) => ({
115+
...base,
116+
"color": theme.palette.text.primary,
117+
"& input": {
118+
font: "inherit"
119+
}
120+
})
121+
};
122+
123+
return (
124+
<div className={defaultClasses.root}>
125+
<SelectComponent
126+
classes={{ ...defaultClasses, ...classes }}
127+
components={components}
128+
isMulti={true}
129+
inputId="react-select-multiple"
130+
onChange={handleChangeMulti}
131+
ref={ref}
132+
styles={selectStyles}
133+
innerRef={ref}
134+
TextFieldProps={{
135+
InputLabelProps: {
136+
htmlFor: "react-select-multiple",
137+
shrink: true
138+
}
139+
}}
140+
value={value}
141+
{...props}
142+
/>
143+
</div>
144+
);
145+
});
146+
147+
MultiSelect.defaultProps = {
148+
placeholder: "Select options"
149+
};
150+
151+
MultiSelect.propTypes = {
152+
/**
153+
* When provided options will be cached
154+
*/
155+
cacheOptions: PropTypes.bool, // eslint-disable-line react/boolean-prop-naming
156+
/**
157+
* Additional classes to customize the Select component
158+
*/
159+
classes: PropTypes.string,
160+
/**
161+
* The defaultOptions prop determines "when" your remote request is initially fired.
162+
* There are two valid values for this property.
163+
* Providing an option array to this prop will populate the initial set of options
164+
* used when opening the select, at which point the remote load only occurs
165+
* when filtering the options (typing in the control).
166+
* Providing the prop by itself (or with 'true') tells the control to immediately
167+
* fire the remote request, described by your loadOptions,
168+
* to get those initial values for the Select.
169+
*/
170+
defaultOptions: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.bool]),
171+
/**
172+
* Set to true if options will be fetched asynchronously.
173+
*/
174+
isAsync: PropTypes.bool,
175+
/**
176+
* A function that returns a Promise which will load the options
177+
*/
178+
loadOptions: PropTypes.func,
179+
/**
180+
* Function to call when the selected value changes
181+
*/
182+
onSelection: PropTypes.func,
183+
/**
184+
* The select options
185+
*/
186+
options: PropTypes.arrayOf(PropTypes.object),
187+
/**
188+
* The placeholder string
189+
*/
190+
placeholder: PropTypes.string
191+
};
192+
193+
export default MultiSelect;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Multi value select with options provided synchronously
2+
```jsx
3+
const options = [
4+
{ value: "mens", label: "Mens" },
5+
{ value: "womens", label: "Womens" },
6+
{ value: "kids", label: "Kids" }
7+
];
8+
9+
// Log selected value
10+
function handleOnSelection(value) {
11+
console.log("Selected value: ", value);
12+
}
13+
14+
<MultiSelect
15+
onSelection={handleOnSelection}
16+
options={options}
17+
placeholder="Select tags"
18+
/>
19+
```
20+
21+
Multi value select with options provided asynchronously
22+
```jsx
23+
const options = [
24+
{ value: "mens", label: "Mens" },
25+
{ value: "womens", label: "Womens" },
26+
{ value: "kids", label: "Kids" }
27+
];
28+
29+
const filterOptions = (inputValue) => options.filter((i) => {
30+
return i.label.toLowerCase().includes(inputValue.toLowerCase());
31+
});
32+
33+
const promiseOptions = (inputValue) =>
34+
new Promise((resolve) => {
35+
setTimeout(() => {
36+
resolve(filterOptions(inputValue));
37+
}, 1000);
38+
});
39+
40+
// Log selected value
41+
function handleOnSelection(value) {
42+
console.log("Selected value: ", value);
43+
}
44+
45+
<MultiSelect
46+
isAsync
47+
cacheOptions
48+
defaultOptions
49+
loadOptions={promiseOptions}
50+
onSelection={handleOnSelection}
51+
placeholder="Select tags"
52+
/>
53+
```
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { TextField } from "@material-ui/core";
4+
import Input from "./Input";
5+
6+
/**
7+
* @name Control
8+
* @param {Object} props Component props
9+
* @returns {React.Component} A React component
10+
*/
11+
export default function Control(props) {
12+
const {
13+
children,
14+
innerProps,
15+
innerRef,
16+
selectProps: { classes, TextFieldProps }
17+
} = props;
18+
19+
return (
20+
<TextField
21+
fullWidth
22+
variant="outlined"
23+
InputProps={{
24+
inputComponent: Input,
25+
inputProps: {
26+
className: classes.input,
27+
ref: innerRef,
28+
children,
29+
...innerProps
30+
}
31+
}}
32+
{...TextFieldProps}
33+
/>
34+
);
35+
}
36+
37+
Control.propTypes = {
38+
/**
39+
* Children to render.
40+
*/
41+
children: PropTypes.node,
42+
/**
43+
* The mouse down event and the innerRef to pass down to the controller element.
44+
*/
45+
innerProps: PropTypes.shape({
46+
onMouseDown: PropTypes.func.isRequired
47+
}).isRequired,
48+
innerRef: PropTypes.oneOfType([
49+
PropTypes.oneOf([null]),
50+
PropTypes.func,
51+
PropTypes.shape({
52+
current: PropTypes.any.isRequired
53+
})
54+
]).isRequired,
55+
selectProps: PropTypes.object.isRequired
56+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
/**
5+
* @name Input
6+
* @param {Function } inputRef Input reference
7+
* @param {Object} props Component props
8+
* @returns {React.Component} A React Component
9+
*/
10+
export default function Input({ inputRef, ...props }) {
11+
return <div ref={inputRef} {...props} />;
12+
}
13+
14+
Input.propTypes = {
15+
inputRef: PropTypes.oneOfType([
16+
PropTypes.func,
17+
PropTypes.shape({
18+
current: PropTypes.any.isRequired
19+
})
20+
])
21+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { Paper } from "@material-ui/core";
4+
5+
/**
6+
* @name Menu
7+
* @param {Object} props Component props
8+
* @returns {React.Component} A React component
9+
*/
10+
export default function Menu(props) {
11+
return (
12+
<Paper
13+
square
14+
className={props.selectProps.classes.paper}
15+
{...props.innerProps}
16+
>
17+
{props.children}
18+
</Paper>
19+
);
20+
}
21+
22+
Menu.propTypes = {
23+
/**
24+
* The children to be rendered.
25+
*/
26+
children: PropTypes.element.isRequired,
27+
/**
28+
* Props to be passed to the menu wrapper.
29+
*/
30+
innerProps: PropTypes.object.isRequired,
31+
selectProps: PropTypes.object.isRequired
32+
};

0 commit comments

Comments
 (0)