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

341 lines
9.4 KiB
TypeScript
Raw Normal View History

2019-10-15 15:23:33 +00:00
import chevronDown from "@assets/images/ChevronDown.svg";
import {
CircularProgress,
MenuItem,
Paper,
Typography
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
2019-10-15 15:23:33 +00:00
import Checkbox from "@saleor/components/Checkbox";
import useElementScroll, {
isScrolledToBottom
} from "@saleor/hooks/useElementScroll";
import { makeStyles } from "@saleor/theme";
2019-10-15 15:23:33 +00:00
import { FetchMoreProps } from "@saleor/types";
import classNames from "classnames";
import { GetItemPropsOptions } from "downshift";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
2019-10-15 15:23:33 +00:00
import Hr from "../Hr";
const menuItemHeight = 46;
const maxMenuItems = 5;
const offset = 24;
2020-02-10 16:07:17 +00:00
export interface MultiAutocompleteActionType {
label: string;
onClick: () => void;
}
2019-10-15 15:23:33 +00:00
export interface MultiAutocompleteChoiceType {
label: string;
value: any;
disabled?: boolean;
2019-10-15 15:23:33 +00:00
}
export interface MultiAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
2020-03-18 17:24:55 +00:00
add?: MultiAutocompleteActionType;
2019-10-15 15:23:33 +00:00
choices: MultiAutocompleteChoiceType[];
displayCustomValue: boolean;
displayValues: MultiAutocompleteChoiceType[];
getItemProps: (options: GetItemPropsOptions) => void;
highlightedIndex: number;
inputValue: string;
}
const useStyles = makeStyles(
2019-10-28 16:16:49 +00:00
theme => ({
2020-03-18 17:24:55 +00:00
add: {
background: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
borderRadius: "100%",
height: 24,
margin: theme.spacing(),
2020-03-18 17:24:55 +00:00
width: 24
},
2019-10-15 15:23:33 +00:00
addIcon: {
height: 24,
margin: 9,
width: 20
},
arrowContainer: {
position: "relative"
},
arrowInnerContainer: {
alignItems: "center",
2019-10-28 16:16:49 +00:00
background:
theme.palette.type === "light"
? theme.palette.grey[50]
: theme.palette.grey[900],
2019-10-15 15:23:33 +00:00
bottom: 0,
display: "flex",
height: 30,
justifyContent: "center",
opacity: 1,
position: "absolute",
transition: theme.transitions.duration.short + "ms",
width: "100%"
},
checkbox: {
height: 24,
width: 20
},
content: {
2019-10-28 16:16:49 +00:00
maxHeight: menuItemHeight * maxMenuItems + theme.spacing(2),
overflowY: "scroll",
2019-10-15 15:23:33 +00:00
padding: 8
},
hide: {
2019-12-02 15:48:19 +00:00
opacity: 0,
zIndex: -1
2019-10-15 15:23:33 +00:00
},
hr: {
2019-10-28 16:16:49 +00:00
margin: theme.spacing(1, 0)
2019-10-15 15:23:33 +00:00
},
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,
display: "grid",
2019-10-28 16:16:49 +00:00
gridColumnGap: theme.spacing(1),
2019-10-15 15:23:33 +00:00
gridTemplateColumns: "30px 1fr",
height: "auto",
2020-03-18 17:24:55 +00:00
marginBottom: theme.spacing(0.5),
2019-10-15 15:23:33 +00:00
padding: 0,
whiteSpace: "normal"
},
menuItemLabel: {
overflowWrap: "break-word"
},
progress: {},
progressContainer: {
display: "flex",
justifyContent: "center",
padding: `${theme.spacing(1)}px 0`
2019-10-15 15:23:33 +00:00
},
root: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
left: 0,
2019-10-28 16:16:49 +00:00
marginTop: theme.spacing(),
2019-10-15 15:23:33 +00:00
overflow: "hidden",
position: "absolute",
right: 0,
zIndex: 22
}
}),
{
name: "MultiAutocompleteSelectFieldContent"
}
);
function getChoiceIndex(
index: number,
displayValues: MultiAutocompleteChoiceType[],
2020-03-18 17:24:55 +00:00
displayCustomValue: boolean,
add: boolean
2019-10-15 15:23:33 +00:00
) {
let choiceIndex = index;
2020-03-18 17:24:55 +00:00
if (add || displayCustomValue) {
2019-10-15 15:23:33 +00:00
choiceIndex += 2;
}
if (displayValues.length > 0) {
choiceIndex += 1 + displayValues.length;
}
return choiceIndex;
}
const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectFieldContentProps> = props => {
2019-10-15 15:23:33 +00:00
const {
2020-02-10 16:07:17 +00:00
add,
2019-10-15 15:23:33 +00:00
choices,
displayCustomValue,
displayValues,
getItemProps,
hasMore,
highlightedIndex,
loading,
inputValue,
onFetchMore
} = props;
2020-02-10 16:07:17 +00:00
if (!!add && !!displayCustomValue) {
throw new Error("Add and custom value cannot be displayed simultaneously");
}
2019-10-15 15:23:33 +00:00
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const [calledForMore, setCalledForMore] = React.useState(false);
const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, offset);
React.useEffect(() => {
if (!calledForMore && onFetchMore && scrolledToBottom) {
onFetchMore();
setCalledForMore(true);
}
}, [scrolledToBottom]);
React.useEffect(() => {
if (calledForMore && !loading) {
setCalledForMore(false);
}
}, [loading]);
const hasValuesToDisplay =
displayValues.length > 0 || displayCustomValue || choices.length > 0;
2019-10-15 15:23:33 +00:00
return (
<Paper className={classes.root}>
{hasValuesToDisplay && (
<div className={classes.content} ref={anchor}>
2019-10-15 15:23:33 +00:00
<>
2020-02-10 16:07:17 +00:00
{add && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: inputValue
})}
data-test="multiautocomplete-select-option-add"
2020-02-10 16:07:17 +00:00
onClick={add.onClick}
>
<AddIcon color="primary" className={classes.addIcon} />
<Typography color="primary">{add.label}</Typography>
</MenuItem>
)}
2019-10-15 15:23:33 +00:00
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
key="customValue"
component="div"
{...getItemProps({
item: inputValue
})}
data-test="multiautocomplete-select-option"
2019-10-15 15:23:33 +00:00
>
<AddIcon className={classes.addIcon} color="primary" />
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom select input option"
values={{
value: inputValue
}}
/>
</MenuItem>
)}
{(choices.length > 0 || displayValues.length > 0) &&
displayCustomValue && <Hr className={classes.hr} />}
{displayValues.map(value => (
<MenuItem
className={classes.menuItem}
key={value.value}
selected={true}
disabled={value.disabled}
2019-10-15 15:23:33 +00:00
component="div"
{...getItemProps({
item: value.value
})}
data-test="multiautocomplete-select-option"
2019-10-15 15:23:33 +00:00
>
<Checkbox
className={classes.checkbox}
checked={true}
disabled={value.disabled}
2019-10-15 15:23:33 +00:00
disableRipple
/>
<span className={classes.menuItemLabel}>{value.label}</span>
</MenuItem>
))}
{displayValues.length > 0 && choices.length > 0 && (
<Hr className={classes.hr} />
)}
{choices.map((suggestion, index) => {
const choiceIndex = getChoiceIndex(
index,
displayValues,
2020-03-18 17:24:55 +00:00
displayCustomValue,
!!add
2019-10-15 15:23:33 +00:00
);
return (
<MenuItem
className={classes.menuItem}
key={suggestion.value}
selected={highlightedIndex === choiceIndex}
disabled={suggestion.disabled}
2019-10-15 15:23:33 +00:00
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
data-test="multiautocomplete-select-option"
2019-10-15 15:23:33 +00:00
>
<Checkbox
checked={false}
2020-04-27 09:11:21 +00:00
disabled={suggestion.disabled}
2019-10-15 15:23:33 +00:00
className={classes.checkbox}
disableRipple
/>
<span className={classes.menuItemLabel}>
{suggestion.label}
</span>
</MenuItem>
);
})}
</>
</div>
)}
{!loading && !hasValuesToDisplay && (
<MenuItem
disabled={true}
component="div"
data-test="multiautocomplete-select-no-options"
>
<FormattedMessage defaultMessage={"No results found"} />
</MenuItem>
)}
{(hasMore || loading) && (
<>
{hasMore && <Hr className={classes.hr} />}
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} size={24} />
</div>
</>
)}
2019-11-29 12:19:18 +00:00
{choices.length > maxMenuItems && (
<div className={classes.arrowContainer}>
<div
className={classNames(classes.arrowInnerContainer, {
// Needs to be explicitely compared to false because
// scrolledToBottom can be either true, false or undefined
[classes.hide]: scrolledToBottom !== false
})}
>
<SVG src={chevronDown} />
</div>
2019-10-15 15:23:33 +00:00
</div>
2019-11-29 12:19:18 +00:00
)}
2019-10-15 15:23:33 +00:00
</Paper>
);
};
MultiAutocompleteSelectFieldContent.displayName =
"MultiAutocompleteSelectFieldContent";
export default MultiAutocompleteSelectFieldContent;