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

324 lines
9.8 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";
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 i18n from "@saleor/i18n";
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",
background: fade(theme.palette.primary.main, 0.6),
borderRadius: 24,
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: {
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: {
display: "grid",
gridColumnGap: theme.spacing.unit + "px",
gridTemplateColumns: "20px 1fr",
height: "auto",
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,
onChange
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
}) => (
<div className={classes.container}>
<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
})}
>
<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
})}
>
<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
})}
>
<span className={classes.menuItemLabel}>
{i18n.t("Add new value: {{ value }}", {
context: "add custom option",
value: inputValue
})}
</span>
</MenuItem>
)}
</>
) : (
!loading && (
<MenuItem disabled={true} component="div">
{i18n.t("No results found")}
</MenuItem>
)
)}
</Paper>
)}
</div>
)}
</Downshift>
<div className={classes.chipContainer}>
{displayValues.map(value => (
<div className={classes.chip} key={value.value}>
<div className={classes.chipInner}>
<Typography className={classes.chipLabel} variant="caption">
{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;