diff --git a/src/storybook/stories/components/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx similarity index 80% rename from src/storybook/stories/components/SingleAutocompleteSelectField.tsx rename to src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 0827f0b70..e332f3647 100644 --- a/src/storybook/stories/components/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -2,28 +2,16 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import Form from "@saleor/components/Form"; +import { maybe } from "@saleor/misc"; +import CardDecorator from "@saleor/storybook/CardDecorator"; +import Decorator from "@saleor/storybook/Decorator"; +import { ChoiceProvider } from "@saleor/storybook/mock"; +import { countries } from "./fixtures"; import SingleAutocompleteSelectField, { SingleAutocompleteSelectFieldProps -} from "@saleor/components/SingleAutocompleteSelectField"; -import { maybe } from "@saleor/misc"; -import CardDecorator from "../../CardDecorator"; -import Decorator from "../../Decorator"; -import { ChoiceProvider } from "../../mock"; +} from "./SingleAutocompleteSelectField"; -const suggestions = [ - "Afghanistan", - "Burundi", - "Comoros", - "Egypt", - "Equatorial Guinea", - "Greenland", - "Isle of Man", - "Israel", - "Italy", - "United States", - "Wallis and Futuna", - "Zimbabwe" -].map(c => ({ label: c, value: c.toLocaleLowerCase().replace(/\s+/, "_") })); +const suggestions = countries.map(c => ({ label: c.name, value: c.code })); const props: SingleAutocompleteSelectFieldProps = { choices: undefined, diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 1f1cf92c3..742a67fb2 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,50 +1,26 @@ import { Omit } from "@material-ui/core"; import CircularProgress from "@material-ui/core/CircularProgress"; import { InputProps } from "@material-ui/core/Input"; -import MenuItem from "@material-ui/core/MenuItem"; -import Paper from "@material-ui/core/Paper"; -import { - createStyles, - Theme, - withStyles, - WithStyles -} from "@material-ui/core/styles"; +import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; import Downshift from "downshift"; import React from "react"; -import { FormattedMessage } from "react-intl"; import { compareTwoStrings } from "string-similarity"; +import SingleAutocompleteSelectFieldContent, { + SingleAutocompleteChoiceType +} from "./SingleAutocompleteSelectFieldContent"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import Debounce, { DebounceProps } from "../Debounce"; -const styles = (theme: Theme) => - createStyles({ - container: { - flexGrow: 1, - position: "relative" - }, - menuItem: { - height: "auto", - whiteSpace: "normal" - }, - paper: { - borderRadius: 4, - left: 0, - marginTop: theme.spacing.unit, - padding: 8, - position: "absolute", - right: 0, - zIndex: 22 - } - }); +const styles = createStyles({ + container: { + flexGrow: 1, + position: "relative" + } +}); -export interface SingleAutocompleteChoiceType { - label: string; - value: any; -} export interface SingleAutocompleteSelectFieldProps { error?: boolean; name: string; @@ -97,6 +73,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { ...props }: SingleAutocompleteSelectFieldProps & WithStyles) => { const [prevDisplayValue] = useStateFromProps(displayValue); + const handleChange = item => onChange({ target: { @@ -131,11 +108,20 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { choices && selectedItem ? choices.filter(c => c.value === selectedItem).length === 0 : false; + const hasInputValueChanged = prevDisplayValue !== displayValue; - if (prevDisplayValue !== displayValue) { + if (hasInputValueChanged) { reset({ inputValue: displayValue }); } + const displayCustomValue = + inputValue.length > 0 && + allowCustomValues && + !choices.find( + choice => + choice.label.toLowerCase() === inputValue.toLowerCase() + ); + return (
{isOpen && (!!inputValue || !!choices.length) && ( - - {choices.length > 0 || allowCustomValues ? ( - <> - {emptyOption && ( - - - - - - )} - {choices.map((suggestion, index) => { - const choiceIndex = index + (emptyOption ? 1 : 0); - - return ( - - {suggestion.label} - - ); - })} - {allowCustomValues && - !!inputValue && - !choices.find( - choice => - choice.label.toLowerCase() === - inputValue.toLowerCase() - ) && ( - - - - )} - - ) : ( - - - - )} - + )}
); diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx new file mode 100644 index 000000000..4775e063e --- /dev/null +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -0,0 +1,197 @@ +import MenuItem from "@material-ui/core/MenuItem"; +import Paper from "@material-ui/core/Paper"; +import { Theme } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import classNames from "classnames"; +import { GetItemPropsOptions } from "downshift"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import useElementScroll from "@saleor/hooks/useElementScroll"; +import Hr from "../Hr"; + +const menuItemHeight = 46; +const maxMenuItems = 5; + +export interface SingleAutocompleteChoiceType { + label: string; + value: any; +} +interface SingleAutocompleteSelectFieldContentProps { + choices: SingleAutocompleteChoiceType[]; + displayCustomValue: boolean; + emptyOption: boolean; + getItemProps: (options: GetItemPropsOptions) => void; + highlightedIndex: number; + inputValue: string; + isCustomValueSelected: boolean; + selectedItem: any; +} + +const useStyles = makeStyles( + (theme: Theme) => ({ + content: { + maxHeight: menuItemHeight * maxMenuItems + theme.spacing.unit * 2, + overflow: "scroll", + padding: 8 + }, + hr: { + margin: `${theme.spacing.unit}px 0` + }, + menuItem: { + height: "auto", + whiteSpace: "normal" + }, + root: { + borderRadius: 4, + left: 0, + marginTop: theme.spacing.unit, + position: "absolute", + right: 0, + zIndex: 22 + }, + shadow: { + boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}` + }, + shadowLine: { + boxShadow: `0px 0px 0px 0px ${theme.palette.grey[50]}`, + height: 1, + transition: theme.transitions.duration.short + "ms" + } + }), + { + name: "SingleAutocompleteSelectFieldContent" + } +); + +function getChoiceIndex( + index: number, + emptyValue: boolean, + customValue: boolean +) { + let choiceIndex = index; + if (emptyValue) { + choiceIndex += 1; + } + if (customValue) { + choiceIndex += 2; + } + + return choiceIndex; +} + +const SingleAutocompleteSelectFieldContent: React.FC< + SingleAutocompleteSelectFieldContentProps +> = props => { + const { + choices, + displayCustomValue, + emptyOption, + getItemProps, + highlightedIndex, + inputValue, + isCustomValueSelected, + selectedItem + } = props; + + const classes = useStyles(props); + const anchor = React.useRef(); + const scrollPosition = useElementScroll(anchor); + + const dropShadow = anchor.current + ? scrollPosition.y + anchor.current.clientHeight < + anchor.current.scrollHeight + : false; + + return ( + +
+ {choices.length > 0 || displayCustomValue ? ( + <> + {emptyOption && ( + + + + + + )} + {displayCustomValue && ( + + + + )} + {choices.length > 0 && displayCustomValue && ( +
+ )} + {choices.map((suggestion, index) => { + const choiceIndex = getChoiceIndex( + index, + emptyOption, + displayCustomValue + ); + + return ( + + {suggestion.label} + + ); + })} + + ) : ( + + + + )} +
+
+ + ); +}; + +SingleAutocompleteSelectFieldContent.displayName = + "SingleAutocompleteSelectFieldContent"; +export default SingleAutocompleteSelectFieldContent; diff --git a/src/components/SingleAutocompleteSelectField/fixtures.ts b/src/components/SingleAutocompleteSelectField/fixtures.ts new file mode 100644 index 000000000..77eefc369 --- /dev/null +++ b/src/components/SingleAutocompleteSelectField/fixtures.ts @@ -0,0 +1,245 @@ +export const countries = [ + { name: "Afghanistan", code: "AF" }, + { name: "Ă…land Islands", code: "AX" }, + { name: "Albania", code: "AL" }, + { name: "Algeria", code: "DZ" }, + { name: "American Samoa", code: "AS" }, + { name: "AndorrA", code: "AD" }, + { name: "Angola", code: "AO" }, + { name: "Anguilla", code: "AI" }, + { name: "Antarctica", code: "AQ" }, + { name: "Antigua and Barbuda", code: "AG" }, + { name: "Argentina", code: "AR" }, + { name: "Armenia", code: "AM" }, + { name: "Aruba", code: "AW" }, + { name: "Australia", code: "AU" }, + { name: "Austria", code: "AT" }, + { name: "Azerbaijan", code: "AZ" }, + { name: "Bahamas", code: "BS" }, + { name: "Bahrain", code: "BH" }, + { name: "Bangladesh", code: "BD" }, + { name: "Barbados", code: "BB" }, + { name: "Belarus", code: "BY" }, + { name: "Belgium", code: "BE" }, + { name: "Belize", code: "BZ" }, + { name: "Benin", code: "BJ" }, + { name: "Bermuda", code: "BM" }, + { name: "Bhutan", code: "BT" }, + { name: "Bolivia", code: "BO" }, + { name: "Bosnia and Herzegovina", code: "BA" }, + { name: "Botswana", code: "BW" }, + { name: "Bouvet Island", code: "BV" }, + { name: "Brazil", code: "BR" }, + { name: "British Indian Ocean Territory", code: "IO" }, + { name: "Brunei Darussalam", code: "BN" }, + { name: "Bulgaria", code: "BG" }, + { name: "Burkina Faso", code: "BF" }, + { name: "Burundi", code: "BI" }, + { name: "Cambodia", code: "KH" }, + { name: "Cameroon", code: "CM" }, + { name: "Canada", code: "CA" }, + { name: "Cape Verde", code: "CV" }, + { name: "Cayman Islands", code: "KY" }, + { name: "Central African Republic", code: "CF" }, + { name: "Chad", code: "TD" }, + { name: "Chile", code: "CL" }, + { name: "China", code: "CN" }, + { name: "Christmas Island", code: "CX" }, + { name: "Cocos (Keeling) Islands", code: "CC" }, + { name: "Colombia", code: "CO" }, + { name: "Comoros", code: "KM" }, + { name: "Congo", code: "CG" }, + { name: "Congo, The Democratic Republic of the", code: "CD" }, + { name: "Cook Islands", code: "CK" }, + { name: "Costa Rica", code: "CR" }, + { name: "Cote D'Ivoire", code: "CI" }, + { name: "Croatia", code: "HR" }, + { name: "Cuba", code: "CU" }, + { name: "Cyprus", code: "CY" }, + { name: "Czech Republic", code: "CZ" }, + { name: "Denmark", code: "DK" }, + { name: "Djibouti", code: "DJ" }, + { name: "Dominica", code: "DM" }, + { name: "Dominican Republic", code: "DO" }, + { name: "Ecuador", code: "EC" }, + { name: "Egypt", code: "EG" }, + { name: "El Salvador", code: "SV" }, + { name: "Equatorial Guinea", code: "GQ" }, + { name: "Eritrea", code: "ER" }, + { name: "Estonia", code: "EE" }, + { name: "Ethiopia", code: "ET" }, + { name: "Falkland Islands (Malvinas)", code: "FK" }, + { name: "Faroe Islands", code: "FO" }, + { name: "Fiji", code: "FJ" }, + { name: "Finland", code: "FI" }, + { name: "France", code: "FR" }, + { name: "French Guiana", code: "GF" }, + { name: "French Polynesia", code: "PF" }, + { name: "French Southern Territories", code: "TF" }, + { name: "Gabon", code: "GA" }, + { name: "Gambia", code: "GM" }, + { name: "Georgia", code: "GE" }, + { name: "Germany", code: "DE" }, + { name: "Ghana", code: "GH" }, + { name: "Gibraltar", code: "GI" }, + { name: "Greece", code: "GR" }, + { name: "Greenland", code: "GL" }, + { name: "Grenada", code: "GD" }, + { name: "Guadeloupe", code: "GP" }, + { name: "Guam", code: "GU" }, + { name: "Guatemala", code: "GT" }, + { name: "Guernsey", code: "GG" }, + { name: "Guinea", code: "GN" }, + { name: "Guinea-Bissau", code: "GW" }, + { name: "Guyana", code: "GY" }, + { name: "Haiti", code: "HT" }, + { name: "Heard Island and Mcdonald Islands", code: "HM" }, + { name: "Holy See (Vatican City State)", code: "VA" }, + { name: "Honduras", code: "HN" }, + { name: "Hong Kong", code: "HK" }, + { name: "Hungary", code: "HU" }, + { name: "Iceland", code: "IS" }, + { name: "India", code: "IN" }, + { name: "Indonesia", code: "ID" }, + { name: "Iran, Islamic Republic Of", code: "IR" }, + { name: "Iraq", code: "IQ" }, + { name: "Ireland", code: "IE" }, + { name: "Isle of Man", code: "IM" }, + { name: "Israel", code: "IL" }, + { name: "Italy", code: "IT" }, + { name: "Jamaica", code: "JM" }, + { name: "Japan", code: "JP" }, + { name: "Jersey", code: "JE" }, + { name: "Jordan", code: "JO" }, + { name: "Kazakhstan", code: "KZ" }, + { name: "Kenya", code: "KE" }, + { name: "Kiribati", code: "KI" }, + { name: "Korea, Democratic People'S Republic of", code: "KP" }, + { name: "Korea, Republic of", code: "KR" }, + { name: "Kuwait", code: "KW" }, + { name: "Kyrgyzstan", code: "KG" }, + { name: "Lao People'S Democratic Republic", code: "LA" }, + { name: "Latvia", code: "LV" }, + { name: "Lebanon", code: "LB" }, + { name: "Lesotho", code: "LS" }, + { name: "Liberia", code: "LR" }, + { name: "Libyan Arab Jamahiriya", code: "LY" }, + { name: "Liechtenstein", code: "LI" }, + { name: "Lithuania", code: "LT" }, + { name: "Luxembourg", code: "LU" }, + { name: "Macao", code: "MO" }, + { name: "Macedonia, The Former Yugoslav Republic of", code: "MK" }, + { name: "Madagascar", code: "MG" }, + { name: "Malawi", code: "MW" }, + { name: "Malaysia", code: "MY" }, + { name: "Maldives", code: "MV" }, + { name: "Mali", code: "ML" }, + { name: "Malta", code: "MT" }, + { name: "Marshall Islands", code: "MH" }, + { name: "Martinique", code: "MQ" }, + { name: "Mauritania", code: "MR" }, + { name: "Mauritius", code: "MU" }, + { name: "Mayotte", code: "YT" }, + { name: "Mexico", code: "MX" }, + { name: "Micronesia, Federated States of", code: "FM" }, + { name: "Moldova, Republic of", code: "MD" }, + { name: "Monaco", code: "MC" }, + { name: "Mongolia", code: "MN" }, + { name: "Montserrat", code: "MS" }, + { name: "Morocco", code: "MA" }, + { name: "Mozambique", code: "MZ" }, + { name: "Myanmar", code: "MM" }, + { name: "Namibia", code: "NA" }, + { name: "Nauru", code: "NR" }, + { name: "Nepal", code: "NP" }, + { name: "Netherlands", code: "NL" }, + { name: "Netherlands Antilles", code: "AN" }, + { name: "New Caledonia", code: "NC" }, + { name: "New Zealand", code: "NZ" }, + { name: "Nicaragua", code: "NI" }, + { name: "Niger", code: "NE" }, + { name: "Nigeria", code: "NG" }, + { name: "Niue", code: "NU" }, + { name: "Norfolk Island", code: "NF" }, + { name: "Northern Mariana Islands", code: "MP" }, + { name: "Norway", code: "NO" }, + { name: "Oman", code: "OM" }, + { name: "Pakistan", code: "PK" }, + { name: "Palau", code: "PW" }, + { name: "Palestinian Territory, Occupied", code: "PS" }, + { name: "Panama", code: "PA" }, + { name: "Papua New Guinea", code: "PG" }, + { name: "Paraguay", code: "PY" }, + { name: "Peru", code: "PE" }, + { name: "Philippines", code: "PH" }, + { name: "Pitcairn", code: "PN" }, + { name: "Poland", code: "PL" }, + { name: "Portugal", code: "PT" }, + { name: "Puerto Rico", code: "PR" }, + { name: "Qatar", code: "QA" }, + { name: "Reunion", code: "RE" }, + { name: "Romania", code: "RO" }, + { name: "Russian Federation", code: "RU" }, + { name: "RWANDA", code: "RW" }, + { name: "Saint Helena", code: "SH" }, + { name: "Saint Kitts and Nevis", code: "KN" }, + { name: "Saint Lucia", code: "LC" }, + { name: "Saint Pierre and Miquelon", code: "PM" }, + { name: "Saint Vincent and the Grenadines", code: "VC" }, + { name: "Samoa", code: "WS" }, + { name: "San Marino", code: "SM" }, + { name: "Sao Tome and Principe", code: "ST" }, + { name: "Saudi Arabia", code: "SA" }, + { name: "Senegal", code: "SN" }, + { name: "Serbia and Montenegro", code: "CS" }, + { name: "Seychelles", code: "SC" }, + { name: "Sierra Leone", code: "SL" }, + { name: "Singapore", code: "SG" }, + { name: "Slovakia", code: "SK" }, + { name: "Slovenia", code: "SI" }, + { name: "Solomon Islands", code: "SB" }, + { name: "Somalia", code: "SO" }, + { name: "South Africa", code: "ZA" }, + { name: "South Georgia and the South Sandwich Islands", code: "GS" }, + { name: "Spain", code: "ES" }, + { name: "Sri Lanka", code: "LK" }, + { name: "Sudan", code: "SD" }, + { name: "Suriname", code: "SR" }, + { name: "Svalbard and Jan Mayen", code: "SJ" }, + { name: "Swaziland", code: "SZ" }, + { name: "Sweden", code: "SE" }, + { name: "Switzerland", code: "CH" }, + { name: "Syrian Arab Republic", code: "SY" }, + { name: "Taiwan, Province of China", code: "TW" }, + { name: "Tajikistan", code: "TJ" }, + { name: "Tanzania, United Republic of", code: "TZ" }, + { name: "Thailand", code: "TH" }, + { name: "Timor-Leste", code: "TL" }, + { name: "Togo", code: "TG" }, + { name: "Tokelau", code: "TK" }, + { name: "Tonga", code: "TO" }, + { name: "Trinidad and Tobago", code: "TT" }, + { name: "Tunisia", code: "TN" }, + { name: "Turkey", code: "TR" }, + { name: "Turkmenistan", code: "TM" }, + { name: "Turks and Caicos Islands", code: "TC" }, + { name: "Tuvalu", code: "TV" }, + { name: "Uganda", code: "UG" }, + { name: "Ukraine", code: "UA" }, + { name: "United Arab Emirates", code: "AE" }, + { name: "United Kingdom", code: "GB" }, + { name: "United States", code: "US" }, + { name: "United States Minor Outlying Islands", code: "UM" }, + { name: "Uruguay", code: "UY" }, + { name: "Uzbekistan", code: "UZ" }, + { name: "Vanuatu", code: "VU" }, + { name: "Venezuela", code: "VE" }, + { name: "Viet Nam", code: "VN" }, + { name: "Virgin Islands, British", code: "VG" }, + { name: "Virgin Islands, U.S.", code: "VI" }, + { name: "Wallis and Futuna", code: "WF" }, + { name: "Western Sahara", code: "EH" }, + { name: "Yemen", code: "YE" }, + { name: "Zambia", code: "ZM" }, + { name: "Zimbabwe", code: "ZW" } +]; diff --git a/src/storybook/config.js b/src/storybook/config.js index 89b3a57cf..9b7e2c7ec 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -39,7 +39,6 @@ function loadStories() { require("./stories/components/RichTextEditor"); require("./stories/components/SaveButtonBar"); require("./stories/components/SaveFilterTabDialog"); - require("./stories/components/SingleAutocompleteSelectField"); require("./stories/components/SingleSelectField"); require("./stories/components/Skeleton"); require("./stories/components/StatusLabel"); diff --git a/src/storybook/mock.tsx b/src/storybook/mock.tsx index a977fd8fe..7e40ca5da 100644 --- a/src/storybook/mock.tsx +++ b/src/storybook/mock.tsx @@ -1,16 +1,14 @@ import React from "react"; interface ChoiceProviderProps { - children: (( - props: { - choices: Array<{ - label: string; - value: string; - }>; - loading: boolean; - fetchChoices(value: string); - } - ) => React.ReactElement); + children: (props: { + choices: Array<{ + label: string; + value: string; + }>; + loading: boolean; + fetchChoices(value: string); + }) => React.ReactElement; choices: Array<{ label: string; value: string; @@ -31,7 +29,7 @@ export class ChoiceProvider extends React.Component< > { state = { choices: [], loading: false, timeout: null }; - handleChange = inputValue => { + handleChange = (inputValue: string) => { if (this.state.loading) { clearTimeout(this.state.timeout); } @@ -42,22 +40,16 @@ export class ChoiceProvider extends React.Component< }); }; - fetchChoices = inputValue => { - let count = 0; + fetchChoices = (inputValue: string) => { this.setState({ - choices: this.props.choices.filter(suggestion => { - const keep = - (!inputValue || + choices: this.props.choices + .filter( + suggestion => + !inputValue || suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== - -1) && - count < 5; - - if (keep) { - count += 1; - } - - return keep; - }), + -1 + ) + .slice(0, 10), loading: false, timeout: null });