2019-10-15 15:23:33 +00:00
|
|
|
import chevronDown from "@assets/images/ChevronDown.svg";
|
2021-05-14 08:15:15 +00:00
|
|
|
import {
|
|
|
|
CircularProgress,
|
|
|
|
MenuItem,
|
|
|
|
Paper,
|
|
|
|
Typography
|
|
|
|
} from "@material-ui/core";
|
2020-04-23 15:43:08 +00:00
|
|
|
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";
|
2021-07-21 08:59:52 +00:00
|
|
|
import { makeStyles } from "@saleor/macaw-ui";
|
2019-10-15 15:23:33 +00:00
|
|
|
import { FetchMoreProps } from "@saleor/types";
|
2020-05-14 09:30:32 +00:00
|
|
|
import classNames from "classnames";
|
|
|
|
import { GetItemPropsOptions } from "downshift";
|
|
|
|
import React from "react";
|
|
|
|
import SVG from "react-inlinesvg";
|
|
|
|
import { FormattedMessage } from "react-intl";
|
2020-04-23 15:43:08 +00:00
|
|
|
|
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;
|
2020-04-23 15:43:08 +00:00
|
|
|
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,
|
2020-03-18 17:29:20 +00:00
|
|
|
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: {
|
2021-07-21 08:59:52 +00:00
|
|
|
maxHeight: `calc(${menuItemHeight * maxMenuItems}px + ${theme.spacing(
|
|
|
|
2
|
|
|
|
)})`,
|
2020-04-23 15:43:08 +00:00
|
|
|
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
|
|
|
|
},
|
2022-01-28 12:34:20 +00:00
|
|
|
paddingLeft: theme.spacing(1.5),
|
2019-10-15 15:23:33 +00:00
|
|
|
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",
|
2021-07-01 08:20:35 +00:00
|
|
|
justifyContent: "center",
|
2022-01-28 12:34:20 +00:00
|
|
|
padding: theme.spacing(1, 0)
|
2019-10-15 15:23:33 +00:00
|
|
|
},
|
|
|
|
root: {
|
|
|
|
borderBottomLeftRadius: 8,
|
|
|
|
borderBottomRightRadius: 8,
|
2022-01-28 12:34:20 +00:00
|
|
|
margin: theme.spacing(1, 0),
|
2019-10-15 15:23:33 +00:00
|
|
|
overflow: "hidden",
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-04-23 15:43:08 +00:00
|
|
|
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]);
|
|
|
|
|
2021-07-01 08:20:35 +00:00
|
|
|
const hasValuesToDisplay =
|
|
|
|
displayValues.length > 0 || displayCustomValue || choices.length > 0;
|
2019-10-15 15:23:33 +00:00
|
|
|
return (
|
2022-01-28 12:34:20 +00:00
|
|
|
<Paper className={classes.root} elevation={8}>
|
2021-07-01 08:20:35 +00:00
|
|
|
{hasValuesToDisplay && (
|
2021-07-12 08:50:50 +00:00
|
|
|
<div
|
|
|
|
className={classes.content}
|
|
|
|
ref={anchor}
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test-id="multi-autocomplete-select-content"
|
2021-07-12 08:50:50 +00:00
|
|
|
>
|
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
|
|
|
|
})}
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test-id="multi-autocomplete-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
|
|
|
|
})}
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test-id="multi-autocomplete-select-option-custom"
|
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}
|
2020-04-23 15:43:08 +00:00
|
|
|
disabled={value.disabled}
|
2019-10-15 15:23:33 +00:00
|
|
|
component="div"
|
|
|
|
{...getItemProps({
|
|
|
|
item: value.value
|
|
|
|
})}
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test-id="multi-autocomplete-select-option"
|
2019-10-15 15:23:33 +00:00
|
|
|
>
|
|
|
|
<Checkbox
|
|
|
|
className={classes.checkbox}
|
|
|
|
checked={true}
|
2020-04-23 15:43:08 +00:00
|
|
|
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}
|
2020-04-23 15:43:08 +00:00
|
|
|
disabled={suggestion.disabled}
|
2019-10-15 15:23:33 +00:00
|
|
|
component="div"
|
|
|
|
{...getItemProps({
|
|
|
|
index: choiceIndex,
|
|
|
|
item: suggestion.value
|
|
|
|
})}
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test-id="multi-autocomplete-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>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</>
|
2021-07-01 08:20:35 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!loading && !hasValuesToDisplay && (
|
|
|
|
<MenuItem
|
|
|
|
disabled={true}
|
|
|
|
component="div"
|
2022-01-31 08:37:49 +00:00
|
|
|
data-test="multi-autocomplete-select-no-options"
|
2021-07-01 08:20:35 +00:00
|
|
|
>
|
|
|
|
<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;
|