saleor-dashboard/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx

353 lines
11 KiB
TypeScript
Raw Normal View History

2019-06-19 14:40:52 +00:00
import CircularProgress from "@material-ui/core/CircularProgress";
2019-08-09 11:14:35 +00:00
import IconButton from "@material-ui/core/IconButton";
2019-06-19 14:40:52 +00:00
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
2019-08-09 11:14:35 +00:00
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
2019-06-19 14:40:52 +00:00
import Downshift, { ControllerStateAndHelpers } from "downshift";
2019-08-09 10:26:22 +00:00
import React from "react";
import { FormattedMessage } from "react-intl";
2019-08-09 11:14:35 +00:00
import { compareTwoStrings } from "string-similarity";
2019-06-19 14:40:52 +00:00
2019-08-09 11:14:35 +00:00
import { fade } from "@material-ui/core/styles/colorManipulator";
import Checkbox from "@saleor/components/Checkbox";
import Debounce, { DebounceProps } from "@saleor/components/Debounce";
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
import Hr from "../Hr";
2019-06-19 14:40:52 +00:00
2019-08-09 11:14:35 +00:00
export interface MultiAutocompleteChoiceType {
2019-06-19 14:40:52 +00:00
label: string;
value: string;
}
const styles = (theme: Theme) =>
createStyles({
2019-08-09 11:14:35 +00:00
checkbox: {
height: 24,
width: 20
},
2019-06-19 14:40:52 +00:00
chip: {
2019-08-09 11:14:35 +00:00
width: "100%"
},
chipClose: {
height: 32,
padding: 0,
width: 32
},
chipContainer: {
display: "flex",
flexDirection: "column",
marginTop: theme.spacing.unit
},
chipInner: {
"& svg": {
color: theme.palette.primary.contrastText
},
alignItems: "center",
2019-09-02 13:26:06 +00:00
background: fade(theme.palette.primary.main, 0.8),
borderRadius: 18,
2019-08-09 11:14:35 +00:00
color: theme.palette.primary.contrastText,
display: "flex",
justifyContent: "space-between",
margin: `${theme.spacing.unit}px 0`,
paddingLeft: theme.spacing.unit * 2,
paddingRight: theme.spacing.unit
},
chipLabel: {
2019-09-12 14:38:40 +00:00
color: theme.palette.primary.contrastText
2019-06-19 14:40:52 +00:00
},
container: {
flexGrow: 1,
position: "relative"
},
2019-08-09 11:14:35 +00:00
hr: {
margin: `${theme.spacing.unit}px 0`
},
menuItem: {
"&:focus": {
backgroundColor: [
theme.palette.background.default,
"!important"
] as any,
color: theme.palette.primary.main,
fontWeight: 400
},
"&:hover": {
backgroundColor: [
theme.palette.background.default,
"!important"
] as any,
color: theme.palette.primary.main,
fontWeight: 700
},
borderRadius: 4,
2019-08-09 11:14:35 +00:00
display: "grid",
gridColumnGap: theme.spacing.unit + "px",
gridTemplateColumns: "30px 1fr",
2019-08-09 11:14:35 +00:00
height: "auto",
padding: 0,
2019-08-09 11:14:35 +00:00
whiteSpace: "normal"
},
menuItemLabel: {
overflowWrap: "break-word"
},
2019-06-19 14:40:52 +00:00
paper: {
left: 0,
marginTop: theme.spacing.unit,
padding: theme.spacing.unit,
position: "absolute",
right: 0,
zIndex: 2
}
});
2019-08-09 11:14:35 +00:00
export interface MultiAutocompleteSelectFieldProps {
allowCustomValues?: boolean;
displayValues: MultiAutocompleteChoiceType[];
2019-06-19 14:40:52 +00:00
name: string;
2019-08-09 11:14:35 +00:00
choices: MultiAutocompleteChoiceType[];
value: string[];
2019-06-19 14:40:52 +00:00
loading?: boolean;
placeholder?: string;
helperText?: string;
label?: string;
2019-08-09 11:14:35 +00:00
fetchChoices?: (value: string) => void;
onChange: (event: React.ChangeEvent<any>) => void;
2019-06-19 14:40:52 +00:00
}
const DebounceAutocomplete: React.ComponentType<
DebounceProps<string>
> = Debounce;
2019-08-09 11:14:35 +00:00
export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
2019-06-19 14:40:52 +00:00
name: "MultiAutocompleteSelectField"
})(
({
2019-08-09 11:14:35 +00:00
allowCustomValues,
2019-06-19 14:40:52 +00:00
choices,
classes,
2019-08-09 11:14:35 +00:00
displayValues,
2019-06-19 14:40:52 +00:00
helperText,
label,
loading,
name,
placeholder,
value,
fetchChoices,
2019-08-27 13:29:00 +00:00
onChange,
...props
2019-08-09 11:14:35 +00:00
}: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
2019-06-19 14:40:52 +00:00
const handleSelect = (
2019-08-09 11:14:35 +00:00
item: string,
downshiftOpts?: ControllerStateAndHelpers
2019-06-19 14:40:52 +00:00
) => {
2019-08-09 11:14:35 +00:00
if (downshiftOpts) {
downshiftOpts.reset({ inputValue: "" });
}
onChange({
target: { name, value: item }
} as any);
2019-06-19 14:40:52 +00:00
};
2019-08-09 11:14:35 +00:00
const suggestions = choices.filter(choice => !value.includes(choice.value));
2019-06-19 14:40:52 +00:00
2019-08-09 11:14:35 +00:00
return (
<>
<Downshift
onInputValueChange={fetchChoices}
onSelect={handleSelect}
itemToString={() => ""}
>
{({
getInputProps,
getItemProps,
isOpen,
toggleMenu,
highlightedIndex,
inputValue
}) => (
2019-08-27 13:29:00 +00:00
<div className={classes.container} {...props}>
2019-08-09 11:14:35 +00:00
<TextField
InputProps={{
...getInputProps({
placeholder
}),
endAdornment: (
<div>
{loading ? (
<CircularProgress size={20} />
) : (
<ArrowDropdownIcon onClick={toggleMenu} />
)}
</div>
),
id: undefined,
onClick: toggleMenu
}}
helperText={helperText}
label={label}
fullWidth={true}
/>
{isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square>
{choices.length > 0 ||
displayValues.length > 0 ||
allowCustomValues ? (
<>
{displayValues.map(value => (
<MenuItem
className={classes.menuItem}
key={value.value}
selected={true}
component="div"
{...getItemProps({
item: value.value
})}
2019-08-27 13:29:00 +00:00
data-tc="multiautocomplete-select-option"
2019-08-09 11:14:35 +00:00
>
<Checkbox
className={classes.checkbox}
checked={true}
disableRipple
/>
<span className={classes.menuItemLabel}>
{value.label}
</span>
</MenuItem>
))}
{displayValues.length > 0 && suggestions.length > 0 && (
<Hr className={classes.hr} />
)}
{suggestions.map((suggestion, index) => (
<MenuItem
className={classes.menuItem}
key={suggestion.value}
selected={highlightedIndex === index + value.length}
component="div"
{...getItemProps({
item: suggestion.value
})}
2019-08-27 13:29:00 +00:00
data-tc="multiautocomplete-select-option"
2019-08-09 11:14:35 +00:00
>
<Checkbox
checked={value.includes(suggestion.value)}
className={classes.checkbox}
disableRipple
/>
<span className={classes.menuItemLabel}>
{suggestion.label}
</span>
</MenuItem>
))}
{allowCustomValues &&
inputValue &&
!choices.find(
choice =>
choice.label.toLowerCase() ===
inputValue.toLowerCase()
) && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
component="div"
{...getItemProps({
item: inputValue
})}
2019-08-27 13:29:00 +00:00
data-tc="multiautocomplete-select-option"
2019-08-09 11:14:35 +00:00
>
<span className={classes.menuItemLabel}>
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom option to select input"
values={{
value: inputValue
}}
/>
2019-08-09 11:14:35 +00:00
</span>
</MenuItem>
)}
</>
) : (
!loading && (
2019-08-27 13:29:00 +00:00
<MenuItem
disabled={true}
component="div"
data-tc="multiautocomplete-select-no-options"
>
<FormattedMessage defaultMessage="No results found" />
2019-08-09 11:14:35 +00:00
</MenuItem>
)
)}
</Paper>
)}
</div>
)}
</Downshift>
<div className={classes.chipContainer}>
{displayValues.map(value => (
<div className={classes.chip} key={value.value}>
<div className={classes.chipInner}>
2019-09-12 14:38:40 +00:00
<Typography className={classes.chipLabel}>
2019-08-09 11:14:35 +00:00
{value.label}
</Typography>
<IconButton
className={classes.chipClose}
onClick={() => handleSelect(value.value)}
>
<CloseIcon fontSize="small" />
</IconButton>
</div>
</div>
))}
</div>
</>
2019-06-19 14:40:52 +00:00
);
2019-08-09 11:14:35 +00:00
}
);
const MultiAutocompleteSelectField: React.FC<
MultiAutocompleteSelectFieldProps
> = ({ choices, fetchChoices, ...props }) => {
const [query, setQuery] = React.useState("");
if (fetchChoices) {
2019-06-19 14:40:52 +00:00
return (
<DebounceAutocomplete debounceFn={fetchChoices}>
2019-08-09 11:14:35 +00:00
{debounceFn => (
<MultiAutocompleteSelectFieldComponent
choices={choices}
{...props}
fetchChoices={debounceFn}
/>
2019-06-19 14:40:52 +00:00
)}
</DebounceAutocomplete>
);
}
2019-08-09 11:14:35 +00:00
const sortedChoices = choices.sort((a, b) => {
const ratingA = compareTwoStrings(query, a.label);
const ratingB = compareTwoStrings(query, b.label);
if (ratingA > ratingB) {
return -1;
}
if (ratingA < ratingB) {
return 1;
}
return 0;
});
return (
<MultiAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")}
choices={sortedChoices}
{...props}
/>
);
};
2019-06-19 14:40:52 +00:00
MultiAutocompleteSelectField.displayName = "MultiAutocompleteSelectField";
export default MultiAutocompleteSelectField;