Add dynamic loading

This commit is contained in:
dominik-zeglen 2019-10-14 16:17:03 +02:00
parent 0bf7594ce0
commit d99321be84
6 changed files with 156 additions and 55 deletions

View file

@ -6,10 +6,14 @@ import { maybe } from "@saleor/misc";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { ChoiceProvider } from "@saleor/storybook/mock";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { countries } from "./fixtures";
import SingleAutocompleteSelectField, {
SingleAutocompleteSelectFieldProps
} from "./SingleAutocompleteSelectField";
import SingleAutocompleteSelectFieldContent, {
SingleAutocompleteSelectFieldContentProps
} from "./SingleAutocompleteSelectFieldContent";
const suggestions = countries.map(c => ({ label: c.name, value: c.code }));
@ -20,11 +24,16 @@ const props: SingleAutocompleteSelectFieldProps = {
loading: false,
name: "country",
onChange: () => undefined,
placeholder: "Select country"
placeholder: "Select country",
value: suggestions[0].value
};
const Story: React.FC<
Partial<SingleAutocompleteSelectFieldProps>
Partial<
SingleAutocompleteSelectFieldProps & {
enableLoadMore: boolean;
}
>
> = storyProps => {
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
@ -32,14 +41,12 @@ const Story: React.FC<
<Form initial={{ country: suggestions[0].value }}>
{({ change, data }) => (
<ChoiceProvider choices={suggestions}>
{({ choices, loading, fetchChoices }) => {
const handleSelect = (event: React.ChangeEvent<any>) => {
const value: string = event.target.value;
const match = choices.find(choice => choice.value === value);
const label = maybe(() => match.label, value);
setDisplayValue(label);
change(event);
};
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => {
const handleSelect = createSingleAutocompleteSelectHandler(
change,
setDisplayValue,
choices
);
return (
<SingleAutocompleteSelectField
@ -48,9 +55,11 @@ const Story: React.FC<
choices={choices}
fetchChoices={fetchChoices}
helperText={`Value: ${data.country}`}
loading={loading}
onChange={handleSelect}
value={data.country}
loading={loading}
hasMore={storyProps.enableLoadMore ? hasMore : false}
onFetchMore={storyProps.enableLoadMore ? fetchMore : undefined}
{...storyProps}
/>
);
@ -61,9 +70,35 @@ const Story: React.FC<
);
};
storiesOf("Generics / SingleAutocompleteSelectField", module)
const contentProps: SingleAutocompleteSelectFieldContentProps = {
choices: suggestions.slice(0, 10),
displayCustomValue: false,
emptyOption: false,
getItemProps: () => undefined,
hasMore: false,
highlightedIndex: 0,
inputValue: suggestions[0].label,
isCustomValueSelected: false,
loading: false,
onFetchMore: () => undefined,
selectedItem: suggestions[0].value
};
storiesOf("Generics / Select with autocomplete", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("with loaded data", () => <Story />)
.add("with loading data", () => <Story loading={true} />)
.add("with custom option", () => <Story allowCustomValues={true} />);
.add("default", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} />
))
.add("can load more", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} hasMore={true} />
))
.add("no data", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} choices={[]} />
))
.add("interactive", () => <Story />)
.add("interactive with custom option", () => (
<Story allowCustomValues={true} />
))
.add("interactive with empty option", () => <Story emptyOption={true} />)
.add("interactive with load more", () => <Story enableLoadMore={true} />);

View file

@ -1,5 +1,4 @@
import { Omit } from "@material-ui/core";
import CircularProgress from "@material-ui/core/CircularProgress";
import { InputProps } from "@material-ui/core/Input";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
@ -11,6 +10,7 @@ import SingleAutocompleteSelectFieldContent, {
} from "./SingleAutocompleteSelectFieldContent";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { FetchMoreProps } from "@saleor/types";
import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce";
@ -21,15 +21,15 @@ const styles = createStyles({
}
});
export interface SingleAutocompleteSelectFieldProps {
export interface SingleAutocompleteSelectFieldProps
extends Partial<FetchMoreProps> {
error?: boolean;
name: string;
displayValue: string;
emptyOption?: boolean;
choices: SingleAutocompleteChoiceType[];
value?: string;
value: string;
disabled?: boolean;
loading?: boolean;
placeholder?: string;
allowCustomValues?: boolean;
helperText?: string;
@ -61,6 +61,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
displayValue,
emptyOption,
error,
hasMore,
helperText,
label,
loading,
@ -70,6 +71,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
InputProps,
fetchChoices,
onChange,
onFetchMore,
...props
}: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const [prevDisplayValue] = useStateFromProps(displayValue);
@ -132,11 +134,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
}),
endAdornment: (
<div>
{loading ? (
<CircularProgress size={20} />
) : (
<ArrowDropdownIcon onClick={toggleMenu} />
)}
</div>
),
error,
@ -156,10 +154,13 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
displayCustomValue={displayCustomValue}
emptyOption={emptyOption}
getItemProps={getItemProps}
hasMore={hasMore}
highlightedIndex={highlightedIndex}
loading={loading}
inputValue={inputValue}
isCustomValueSelected={isCustomValueSelected}
selectedItem={selectedItem}
onFetchMore={onFetchMore}
/>
)}
</div>
@ -208,4 +209,5 @@ export class SingleAutocompleteSelectField extends React.Component<
);
}
}
export default SingleAutocompleteSelectField;

View file

@ -1,3 +1,4 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { Theme } from "@material-ui/core/styles";
@ -9,16 +10,19 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import useElementScroll from "@saleor/hooks/useElementScroll";
import { FetchMoreProps } from "@saleor/types";
import Hr from "../Hr";
const menuItemHeight = 46;
const maxMenuItems = 5;
const offset = 24;
export interface SingleAutocompleteChoiceType {
label: string;
value: any;
}
interface SingleAutocompleteSelectFieldContentProps {
export interface SingleAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
choices: SingleAutocompleteChoiceType[];
displayCustomValue: boolean;
emptyOption: boolean;
@ -43,6 +47,11 @@ const useStyles = makeStyles(
height: "auto",
whiteSpace: "normal"
},
progress: {},
progressContainer: {
display: "flex",
justifyContent: "center"
},
root: {
borderRadius: 4,
left: 0,
@ -52,7 +61,9 @@ const useStyles = makeStyles(
zIndex: 22
},
shadow: {
"&$shadowLine": {
boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}`
}
},
shadowLine: {
boxShadow: `0px 0px 0px 0px ${theme.palette.grey[50]}`,
@ -89,21 +100,38 @@ const SingleAutocompleteSelectFieldContent: React.FC<
displayCustomValue,
emptyOption,
getItemProps,
hasMore,
highlightedIndex,
loading,
inputValue,
isCustomValueSelected,
selectedItem
selectedItem,
onFetchMore
} = props;
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const [calledForMore, setCalledForMore] = React.useState(false);
const dropShadow = anchor.current
? scrollPosition.y + anchor.current.clientHeight <
const scrolledToBottom = anchor.current
? scrollPosition.y + anchor.current.clientHeight + offset >=
anchor.current.scrollHeight
: false;
React.useEffect(() => {
if (!calledForMore && onFetchMore && scrolledToBottom) {
onFetchMore();
setCalledForMore(true);
}
}, [scrolledToBottom]);
React.useEffect(() => {
if (calledForMore && !loading) {
setCalledForMore(false);
}
}, [loading]);
return (
<Paper className={classes.root} square>
<div className={classes.content} ref={anchor}>
@ -172,6 +200,14 @@ const SingleAutocompleteSelectFieldContent: React.FC<
</MenuItem>
);
})}
{hasMore && (
<>
<Hr className={classes.hr} />
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} size={24} />
</div>
</>
)}
</>
) : (
<MenuItem
@ -185,7 +221,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<
</div>
<div
className={classNames(classes.shadowLine, {
[classes.shadow]: dropShadow
[classes.shadow]: !scrolledToBottom
})}
/>
</Paper>

View file

@ -1,2 +1,3 @@
export { default } from "./SingleAutocompleteSelectField";
export * from "./SingleAutocompleteSelectField";
export * from "./SingleAutocompleteSelectFieldContent";

View file

@ -7,6 +7,7 @@ const CardDecorator = storyFn => (
style={{
margin: "auto",
overflow: "visible",
position: "relative",
width: 400
}}
>

View file

@ -1,36 +1,41 @@
import React from "react";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent";
interface ChoiceProviderProps {
children: (props: {
choices: Array<{
label: string;
value: string;
}>;
choices: SingleAutocompleteChoiceType[];
hasMore: boolean;
loading: boolean;
fetchChoices(value: string);
fetchChoices: (value: string) => void;
fetchMore: () => void;
}) => React.ReactElement<any>;
choices: Array<{
label: string;
value: string;
}>;
choices: SingleAutocompleteChoiceType[];
}
interface ChoiceProviderState {
choices: Array<{
label: string;
value: string;
}>;
choices: SingleAutocompleteChoiceType[];
filteredChoices: SingleAutocompleteChoiceType[];
first: number;
loading: boolean;
timeout: any;
}
const step = 5;
export class ChoiceProvider extends React.Component<
ChoiceProviderProps,
ChoiceProviderState
> {
state = { choices: [], loading: false, timeout: null };
state = {
choices: [],
filteredChoices: [],
first: step,
loading: false,
timeout: null
};
handleChange = (inputValue: string) => {
if (this.state.loading) {
if (!!this.state.timeout) {
clearTimeout(this.state.timeout);
}
const timeout = setTimeout(() => this.fetchChoices(inputValue), 500);
@ -40,16 +45,35 @@ export class ChoiceProvider extends React.Component<
});
};
fetchChoices = (inputValue: string) => {
handleFetchMore = () => {
if (!!this.state.timeout) {
clearTimeout(this.state.timeout);
}
const timeout = setTimeout(this.fetchMore, 500);
this.setState({
choices: this.props.choices
.filter(
loading: true,
timeout
});
};
fetchMore = () =>
this.setState(prevState => ({
filteredChoices: prevState.choices.slice(0, prevState.first + step),
first: prevState.first + step,
loading: false,
timeout: null
}));
fetchChoices = (inputValue: string) => {
const choices = this.props.choices.filter(
suggestion =>
!inputValue ||
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !==
-1
)
.slice(0, 10),
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1
);
this.setState({
choices,
filteredChoices: choices.slice(0, step),
first: step,
loading: false,
timeout: null
});
@ -57,8 +81,10 @@ export class ChoiceProvider extends React.Component<
render() {
return this.props.children({
choices: this.state.choices,
choices: this.state.filteredChoices,
fetchChoices: this.handleChange,
fetchMore: this.handleFetchMore,
hasMore: this.state.choices.length > this.state.filteredChoices.length,
loading: this.state.loading
});
}