@vtex/styleguide
Version:
> VTEX Styleguide React components ([Docs](https://vtex.github.io/styleguide))
342 lines (275 loc) • 11.6 kB
JavaScript
"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;