saleor-dashboard/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx

333 lines
9 KiB
TypeScript
Raw Normal View History

import chevronDown from "@assets/images/ChevronDown.svg";
import {
CircularProgress,
MenuItem,
Paper,
Typography
} from "@material-ui/core";
import Add from "@material-ui/icons/Add";
import useElementScroll, {
isScrolledToBottom
} from "@saleor/hooks/useElementScroll";
Use MacawUI (#1229) * Replace withStyleswith useStyles (#1100) * Replace withStyleswith useStyles * Update messages * Use rem as a spacing unit (#1101) * Use rems as spacing units * Fix visual bugs * Update stories * Use macaw-ui as theme provider (#1108) * Use macaw ui as a theme provider * Add react-dom to aliases * Fix jest module resolution * Update useTheme hook usage * Fix test wrapper * Use macaw from git repo * Fix CI * Update stories * Fix aliasing * Extract savebar to macaw ui (#1146) * wip * Use savebar from macaw * Use confirm button from macaw * Improve file structure * Use sidebar context from macaw * Update macaw * Update macaw version * Remove savebar from storybook * Update stories * Use alerts and notifications from macaw (#1166) * Use alerts from macaw * Add notifications from macaw * Update stories * Pin macaw version * Encapsulate limit reached in one component * Remove unused imports * Use backlinks from macaw (#1183) * Use backlink from macaw * Update macaw version * Use macaw sidebar (#1148) * Use sidebar from macaw * Use shipped logo * Use lowercase * Update stories * Use user chip from macaw (#1191) * Use user chip from macaw * Use dedicated components for menu items * Simplify code * Bump version and fix types (#1210) * Rename onBack to onClick * Rename UserChip to UserChipMenu * Rename IMenuItem to SidebarMenuItem * Update macaw version * Fix tables after changes in macaw (#1220) * Update macaw version * Update changelog * Update stories * Fix after rebase * Update to macaw 0.2.0 * Lint files * Update macaw to 0.2.2
2021-07-21 08:59:52 +00:00
import { makeStyles } from "@saleor/macaw-ui";
import { FetchMoreProps } from "@saleor/types";
2019-10-14 11:57:08 +00:00
import classNames from "classnames";
import { GetItemPropsOptions } from "downshift";
import React, { ReactElement } from "react";
2019-10-15 10:33:14 +00:00
import SVG from "react-inlinesvg";
2019-10-14 11:57:08 +00:00
import { FormattedMessage } from "react-intl";
import Hr from "../Hr";
const menuItemHeight = 46;
const maxMenuItems = 5;
2019-10-14 14:17:03 +00:00
const offset = 24;
2019-10-14 11:57:08 +00:00
export type ChoiceValue = string;
export interface SingleAutocompleteChoiceType<
V extends ChoiceValue = ChoiceValue,
L = string
> {
label: L;
value: V;
2019-10-14 11:57:08 +00:00
}
2020-02-10 16:07:17 +00:00
export interface SingleAutocompleteActionType {
label: string;
onClick: () => void;
}
2019-10-14 14:17:03 +00:00
export interface SingleAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
2020-03-18 17:24:55 +00:00
add?: SingleAutocompleteActionType;
choices: Array<SingleAutocompleteChoiceType<string, string | JSX.Element>>;
2019-10-14 11:57:08 +00:00
displayCustomValue: boolean;
emptyOption: boolean;
2020-10-12 10:45:07 +00:00
getItemProps: (options: GetItemPropsOptions) => any;
2019-10-14 11:57:08 +00:00
highlightedIndex: number;
inputValue: string;
isCustomValueSelected: boolean;
selectedItem: any;
}
const useStyles = makeStyles(
2019-10-28 16:16:49 +00:00
theme => ({
2020-02-10 16:07:17 +00:00
add: {
background: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
borderRadius: "100%",
height: 24,
marginRight: theme.spacing(),
width: 24
},
2019-10-15 10:33:14 +00:00
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 10:33:14 +00:00
bottom: 0,
display: "flex",
height: 30,
justifyContent: "center",
opacity: 1,
position: "absolute",
transition: theme.transitions.duration.short + "ms",
width: "100%"
},
2019-10-14 11:57:08 +00:00
content: {
Use MacawUI (#1229) * Replace withStyleswith useStyles (#1100) * Replace withStyleswith useStyles * Update messages * Use rem as a spacing unit (#1101) * Use rems as spacing units * Fix visual bugs * Update stories * Use macaw-ui as theme provider (#1108) * Use macaw ui as a theme provider * Add react-dom to aliases * Fix jest module resolution * Update useTheme hook usage * Fix test wrapper * Use macaw from git repo * Fix CI * Update stories * Fix aliasing * Extract savebar to macaw ui (#1146) * wip * Use savebar from macaw * Use confirm button from macaw * Improve file structure * Use sidebar context from macaw * Update macaw * Update macaw version * Remove savebar from storybook * Update stories * Use alerts and notifications from macaw (#1166) * Use alerts from macaw * Add notifications from macaw * Update stories * Pin macaw version * Encapsulate limit reached in one component * Remove unused imports * Use backlinks from macaw (#1183) * Use backlink from macaw * Update macaw version * Use macaw sidebar (#1148) * Use sidebar from macaw * Use shipped logo * Use lowercase * Update stories * Use user chip from macaw (#1191) * Use user chip from macaw * Use dedicated components for menu items * Simplify code * Bump version and fix types (#1210) * Rename onBack to onClick * Rename UserChip to UserChipMenu * Rename IMenuItem to SidebarMenuItem * Update macaw version * Fix tables after changes in macaw (#1220) * Update macaw version * Update changelog * Update stories * Fix after rebase * Update to macaw 0.2.0 * Lint files * Update macaw to 0.2.2
2021-07-21 08:59:52 +00:00
maxHeight: `calc(${menuItemHeight * maxMenuItems}px + ${theme.spacing(
2
)})`,
2019-10-14 11:57:08 +00:00
overflow: "scroll",
padding: 8
},
2019-10-15 10:33:14 +00:00
hide: {
2019-12-02 15:48:19 +00:00
opacity: 0,
zIndex: -1
2019-10-15 10:33:14 +00:00
},
2019-10-14 11:57:08 +00:00
hr: {
2019-10-28 16:16:49 +00:00
margin: theme.spacing(1, 0)
2019-10-14 11:57:08 +00:00
},
menuItem: {
height: "auto",
whiteSpace: "normal"
},
2019-10-14 14:17:03 +00:00
progress: {},
progressContainer: {
display: "flex",
justifyContent: "center"
},
2019-10-14 11:57:08 +00:00
root: {
2019-10-15 10:33:14 +00:00
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
2019-10-14 11:57:08 +00:00
left: 0,
2019-10-28 16:16:49 +00:00
marginTop: theme.spacing(),
2019-10-15 10:33:14 +00:00
overflow: "hidden",
2019-10-14 11:57:08 +00:00
position: "absolute",
right: 0,
zIndex: 22
}
}),
{
name: "SingleAutocompleteSelectFieldContent"
}
);
function getChoiceIndex(
index: number,
emptyValue: boolean,
customValue: boolean,
add: boolean
2019-10-14 11:57:08 +00:00
) {
let choiceIndex = index;
if (emptyValue) {
choiceIndex += 1;
}
if (customValue || add) {
2019-10-14 11:57:08 +00:00
choiceIndex += 2;
}
return choiceIndex;
}
2020-09-24 10:54:43 +00:00
const sliceSize = 20;
2020-02-10 16:07:17 +00:00
const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFieldContentProps> = props => {
2019-10-14 11:57:08 +00:00
const {
2020-02-10 16:07:17 +00:00
add,
2019-10-14 11:57:08 +00:00
choices,
displayCustomValue,
emptyOption,
getItemProps,
2019-10-14 14:17:03 +00:00
hasMore,
loading,
2019-10-14 11:57:08 +00:00
inputValue,
isCustomValueSelected,
2019-10-14 14:17:03 +00:00
selectedItem,
onFetchMore
2019-10-14 11:57:08 +00:00
} = 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-14 11:57:08 +00:00
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
2019-10-14 14:17:03 +00:00
const [calledForMore, setCalledForMore] = React.useState(false);
const [slice, setSlice] = React.useState(onFetchMore ? 10000 : sliceSize);
const [initialized, setInitialized] = React.useState(false);
2019-10-14 11:57:08 +00:00
2019-10-15 12:12:38 +00:00
const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, offset);
2019-10-14 11:57:08 +00:00
2019-10-14 14:17:03 +00:00
React.useEffect(() => {
if (!calledForMore && onFetchMore && scrolledToBottom) {
onFetchMore();
setCalledForMore(true);
} else if (scrolledToBottom && !onFetchMore) {
2020-09-24 10:54:43 +00:00
setSlice(slice => slice + sliceSize);
2019-10-14 14:17:03 +00:00
}
}, [scrolledToBottom]);
2020-09-24 10:54:43 +00:00
React.useEffect(() => {
if (!onFetchMore) {
setSlice(sliceSize);
}
if (anchor.current?.scrollTo && !initialized) {
2020-10-12 10:45:07 +00:00
anchor.current.scrollTo({
top: 0
});
setInitialized(true);
2020-10-12 10:45:07 +00:00
}
2020-09-24 10:54:43 +00:00
}, [choices?.length]);
React.useEffect(() => {
setInitialized(false);
}, [inputValue]);
2019-10-14 14:17:03 +00:00
React.useEffect(() => {
if (calledForMore && !loading) {
setCalledForMore(false);
}
}, [loading]);
2020-10-12 10:45:07 +00:00
const emptyOptionProps = getItemProps({
item: ""
});
const choicesToDisplay = choices.slice(0, slice);
2019-10-14 11:57:08 +00:00
return (
2019-10-15 10:33:14 +00:00
<Paper className={classes.root}>
<div
className={classes.content}
ref={anchor}
data-test="autocomplete-dropdown"
>
2019-10-14 11:57:08 +00:00
{choices.length > 0 || displayCustomValue ? (
<>
{emptyOption && (
2019-10-14 11:57:08 +00:00
<MenuItem
className={classes.menuItem}
component="div"
data-test="singleautocomplete-select-option"
2020-10-12 10:45:07 +00:00
data-test-type="empty"
{...emptyOptionProps}
2019-10-14 11:57:08 +00:00
>
<Typography color="textSecondary">
<FormattedMessage defaultMessage="None" />
</Typography>
</MenuItem>
)}
2020-02-10 16:07:17 +00:00
{add && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: inputValue
})}
data-test="singleautocomplete-select-option-add"
2020-10-12 10:45:07 +00:00
data-test-type="add"
2020-02-10 16:07:17 +00:00
onClick={add.onClick}
>
<Add color="primary" className={classes.add} />
<Typography color="primary">{add.label}</Typography>
</MenuItem>
)}
2019-10-14 11:57:08 +00:00
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
selected={isCustomValueSelected}
component="div"
{...getItemProps({
item: inputValue
})}
data-test="singleautocomplete-select-option"
2020-10-12 10:45:07 +00:00
data-test-type="custom"
2019-10-14 11:57:08 +00:00
>
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom select input option"
values={{
value: inputValue
}}
/>
</MenuItem>
)}
2020-02-10 16:07:17 +00:00
{choices.length > 0 && (!!add || displayCustomValue) && (
2019-10-14 11:57:08 +00:00
<Hr className={classes.hr} />
)}
{choicesToDisplay.map((suggestion, index) => {
2019-10-14 11:57:08 +00:00
const choiceIndex = getChoiceIndex(
index,
emptyOption,
displayCustomValue,
!!add
2019-10-14 11:57:08 +00:00
);
const key = React.isValidElement(suggestion.label)
? `${index}${suggestion.value}${
((suggestion as unknown) as ReactElement).props
}`
: JSON.stringify(suggestion);
2019-10-14 11:57:08 +00:00
return (
<MenuItem
className={classes.menuItem}
key={key}
2019-10-28 16:16:49 +00:00
selected={selectedItem === suggestion.value}
2019-10-14 11:57:08 +00:00
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
data-test="singleautocomplete-select-option"
2020-10-12 10:45:07 +00:00
data-test-value={suggestion.value}
data-test-type="option"
2019-10-14 11:57:08 +00:00
>
{suggestion.label}
</MenuItem>
);
})}
2019-10-14 14:17:03 +00:00
{hasMore && (
<>
<Hr className={classes.hr} />
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} size={24} />
</div>
</>
)}
2019-10-14 11:57:08 +00:00
</>
) : (
<MenuItem
disabled={true}
component="div"
data-test="singleautocomplete-select-no-options"
2019-10-14 11:57:08 +00:00
>
<FormattedMessage defaultMessage="No results found" />
</MenuItem>
)}
</div>
2019-11-29 12:19:18 +00:00
{choices.length > maxMenuItems && (
<div className={classes.arrowContainer}>
<div
className={classNames(classes.arrowInnerContainer, {
// Needs to be explicitly compared to false because
2019-11-29 12:19:18 +00:00
// scrolledToBottom can be either true, false or undefined
[classes.hide]: scrolledToBottom !== false
})}
>
<SVG src={chevronDown} />
</div>
2019-10-15 10:33:14 +00:00
</div>
2019-11-29 12:19:18 +00:00
)}
2019-10-14 11:57:08 +00:00
</Paper>
);
};
SingleAutocompleteSelectFieldContent.displayName =
"SingleAutocompleteSelectFieldContent";
export default SingleAutocompleteSelectFieldContent;