UNPKG

@vtex/styleguide

Version:

> VTEX Styleguide React components ([Docs](https://vtex.github.io/styleguide))

342 lines (275 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _propTypes = require("prop-types"); var _propTypes2 = _interopRequireDefault(_propTypes); var _react = require("react"); var _react2 = _interopRequireDefault(_react); var _Spinner = require("../Spinner"); var _Spinner2 = _interopRequireDefault(_Spinner); var _hooks = require("./hooks"); var _Option = require("./Option"); var _Option2 = _interopRequireDefault(_Option); var _SearchInput = require("./SearchInput"); var _SearchInput2 = _interopRequireDefault(_SearchInput); var _autocomplete = require("./autocomplete.css"); var _autocomplete2 = _interopRequireDefault(_autocomplete); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var propTypes = { /** Input props. All HTMLInput props can be added too */ input: _propTypes2.default.shape({ /** Clear input handler */ onClear: _propTypes2.default.func.isRequired, /** * Shows the search button and it's a search by term handler * (fired on enter or when clicking the search button) */ onSearch: _propTypes2.default.func, /** Change term handler */ onChange: _propTypes2.default.func.isRequired, /** Term to be searched */ value: _propTypes2.default.string, /** Determine if the input and button should be disabled */ disabled: _propTypes2.default.bool, /** Determine if the input and button should be styled with error borders */ error: _propTypes2.default.bool, /** The error message to be displayed below the input field */ errorMessage: _propTypes2.default.node, /** Prefix element */ prefix: _propTypes2.default.node, /** Suffix element */ suffix: _propTypes2.default.node }).isRequired, /** Options props. More details in the examples */ options: _propTypes2.default.shape({ /** * Determine if a spinner will be shown below the given options * to show that more options will be added */ loading: _propTypes2.default.bool, /** * Function that makes possible to the dev to customly render option. * Called with all props needed: `(props: { key: string, selected: boolean, value: OptionValue, searchTerm: string, roundedBottom: boolean, icon: ReactElement, onClick: () => void }, index: number)` and should return a React Node */ renderOption: _propTypes2.default.func, /** * List of options. * An option could be a string (denoting a search by term) or an object * with `{value: any, label: string}` (denoting the search is related to an entity). */ value: _propTypes2.default.arrayOf(_Option.autocompleteOptionShape).isRequired, /** * Icon representing the entity. * Shown when a value is an object to show the difference */ icon: _propTypes2.default.element, /** * Callback called when an option is selected * (clicked or via arrow keys + enter) */ onSelect: _propTypes2.default.func.isRequired, /** * Last searched terms. Can be used to enhance the Autocomplete experience. * Defined with: `{ * value: OptionValue[], * onChange: (term: string | OptionValue) => any, * label: string * }` */ lastSearched: _propTypes2.default.shape({ /** List of last searched options */ value: _propTypes2.default.arrayOf(_Option.autocompleteOptionShape).isRequired, /** * Last searched change handler. * Called when a term is searched or an option is selected. */ onChange: _propTypes2.default.func, /** Last Searched options's title */ label: _propTypes2.default.node.isRequired }), /** * Selects a size of the input bar, could be set to `small`, `regular` or `large`. * `regular` is the default value. */ size: _propTypes2.default.oneOf(['small', 'regular', 'large']), /** * A custom message to be displayed inside the options dropdown. * It can be a warning, an error, or a hint about the options. */ customMessage: _propTypes2.default.node, /** * Max height value for options dropdown. * `fit-content` is the default value. */ maxHeight: _propTypes2.default.number }).isRequired }; var AutocompleteInput = function AutocompleteInput(_ref) { var _ref$input = _ref.input, value = _ref$input.value, error = _ref$input.error, errorMessage = _ref$input.errorMessage, onClear = _ref$input.onClear, onSearch = _ref$input.onSearch, onChange = _ref$input.onChange, inputProps = _objectWithoutPropertiesLoose(_ref$input, ["value", "error", "errorMessage", "onClear", "onSearch", "onChange"]), _ref$options = _ref.options, onSelect = _ref$options.onSelect, options = _ref$options.value, renderOption = _ref$options.renderOption, loading = _ref$options.loading, _ref$options$lastSear = _ref$options.lastSearched, lastSearched = _ref$options$lastSear === void 0 ? {} : _ref$options$lastSear, icon = _ref$options.icon, size = _ref$options.size, customMessage = _ref$options.customMessage, _ref$options$maxHeigh = _ref$options.maxHeight, maxHeight = _ref$options$maxHeigh === void 0 ? 'fit-content' : _ref$options$maxHeigh; var _useState = (0, _react.useState)(value || ''), term = _useState[0], setTerm = _useState[1]; (0, _react.useEffect)(function updateTermWhenInputValueChanges() { setTerm(value); }, [value]); var _useState2 = (0, _react.useState)(false), showPopover = _useState2[0], setShowPopover = _useState2[1]; var containerRef = (0, _react.useRef)(null); var searching = term.length; var showLastSearched = !searching && lastSearched.value && lastSearched.value.length > 0; var getShowedOptions = function getShowedOptions() { if (showLastSearched) { return lastSearched.value; } if (searching) { return options; } return []; }; var showedOptions = getShowedOptions(); var noSelectedOptionIndex = -1; var _useArrowNavigation = (0, _hooks.useArrowNavigation)(containerRef, showedOptions.length, noSelectedOptionIndex), selectedOptionIndex = _useArrowNavigation[0], setSelectedOptionIndex = _useArrowNavigation[1]; (0, _hooks.useClickOutside)(containerRef, function () { return setShowPopover(false); }); var addToLastSearched = function addToLastSearched(option) { if (lastSearched && lastSearched.onChange) { lastSearched.onChange(option); } }; var handleKeyDown = function handleKeyDown(e) { inputProps.onKeyDown == null ? void 0 : inputProps.onKeyDown(e); if (e.key === 'Enter') { var selectedOption = selectedOptionIndex !== -1 ? showedOptions[selectedOptionIndex] : term; addToLastSearched(selectedOption); setTerm((0, _Option.getTermFromOption)(selectedOption)); if (selectedOptionIndex !== -1) { onSelect(selectedOption); } else { onSearch == null ? void 0 : onSearch((0, _Option.getTermFromOption)(selectedOption)); } setSelectedOptionIndex(-1); setShowPopover(false); } if (e.key === 'Escape' || e.key === 'Tab') { setShowPopover(false); } }; var handleTermChange = function handleTermChange(newTerm) { if (newTerm === void 0) { newTerm = ''; } if (!showPopover) { setShowPopover(true); } setTerm(newTerm); if (onChange) { onChange(newTerm); } setSelectedOptionIndex(noSelectedOptionIndex); }; var handleClear = function handleClear() { setSelectedOptionIndex(noSelectedOptionIndex); setShowPopover(false); setTerm(''); onClear(); }; var handleOptionClick = function handleOptionClick(option) { setTerm((0, _Option.getTermFromOption)(option)); onSelect(option); setShowPopover(false); }; var handleSearch = function handleSearch() { onSearch == null ? void 0 : onSearch(term); setShowPopover(false); }; var getOptionProps = function getOptionProps(option, index) { return { key: (0, _Option.getTermFromOption)(option) + "-" + index, selected: index === selectedOptionIndex, value: option, searchTerm: term, // customMessage should be the last element on dropdown, so should have rounded bottom. roundedBottom: !customMessage && index === showedOptions.length - 1, icon: typeof option !== 'string' && icon ? icon : null, onClick: function onClick() { addToLastSearched(option); handleOptionClick(option); } }; }; var renderOptions = function renderOptions() { return _react2.default.createElement("div", { className: "flex flex-column" }, showLastSearched ? _react2.default.createElement("div", { className: "pa4 b f6" }, lastSearched.label || 'Last searched terms') : null, showedOptions.map(function (option, index) { return renderOption ? renderOption(getOptionProps(option, index), index) : _react2.default.createElement(_Option2.default, getOptionProps(option, index)); })); }; var renderCustomMessage = function renderCustomMessage() { return typeof customMessage !== 'string' ? customMessage : _react2.default.createElement("div", { className: "w-100 pa4 f6 br2 br--bottom bg-base" }, _react2.default.createElement("span", { className: "ml3 c-on-base" }, customMessage)); }; var popoverOpened = showPopover && (!!showedOptions.length || loading || !!customMessage); var errorStyle = error || Boolean(errorMessage); return _react2.default.createElement("div", { ref: containerRef, className: "flex flex-column w-100" }, _react2.default.createElement(_SearchInput2.default, _extends({}, inputProps, { value: selectedOptionIndex === -1 ? term : (0, _Option.getTermFromOption)(showedOptions[selectedOptionIndex]), roundedBottom: !popoverOpened, onKeyDown: handleKeyDown, onFocus: function onFocus() { return setShowPopover(true); }, onSearch: onSearch && handleSearch, onClear: handleClear, onChange: handleTermChange, error: errorStyle, size: size })), _react2.default.createElement("div", { className: "relative" }, popoverOpened ? _react2.default.createElement("div", { style: { maxHeight: maxHeight }, className: "absolute br--bottom br2 bb bl br bw1 b--muted-2 bg-base w-100 z-1 shadow-5 " + _autocomplete2.default.scroll }, renderOptions(), loading && _react2.default.createElement("div", { className: "flex flex-row justify-center items-center pa4" }, _react2.default.createElement(_Spinner2.default, { size: 20 })), renderCustomMessage()) : null, errorMessage && _react2.default.createElement("div", { className: "c-danger t-small mt3 lh-title" }, errorMessage))); }; AutocompleteInput.propTypes = propTypes; exports.default = AutocompleteInput;