Add dynamic loading
This commit is contained in:
parent
0bf7594ce0
commit
d99321be84
6 changed files with 156 additions and 55 deletions
|
@ -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} />);
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
<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;
|
||||
|
|
|
@ -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: {
|
||||
boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}`
|
||||
"&$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>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default } from "./SingleAutocompleteSelectField";
|
||||
export * from "./SingleAutocompleteSelectField";
|
||||
export * from "./SingleAutocompleteSelectFieldContent";
|
||||
|
|
|
@ -7,6 +7,7 @@ const CardDecorator = storyFn => (
|
|||
style={{
|
||||
margin: "auto",
|
||||
overflow: "visible",
|
||||
position: "relative",
|
||||
width: 400
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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(
|
||||
suggestion =>
|
||||
!inputValue ||
|
||||
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !==
|
||||
-1
|
||||
)
|
||||
.slice(0, 10),
|
||||
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
|
||||
);
|
||||
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
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue