wix-style-react
Version:
594 lines (517 loc) • 21.2 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
import _inherits from "@babel/runtime/helpers/inherits";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Input from '../Input';
import omit from 'omit';
import DropdownLayout from '../DropdownLayout/DropdownLayout';
import { classes } from './InputWithOptions.st.css';
import uniqueId from 'lodash/uniqueId';
import Popover from '../Popover';
import HighlightContext from './HighlightContext';
export var DEFAULT_VALUE_PARSER = function DEFAULT_VALUE_PARSER(option) {
return option.value;
};
export var DEFAULT_POPOVER_PROPS = {
appendTo: 'parent',
flip: false,
fixed: true,
placement: 'bottom'
};
var InputWithOptions = /*#__PURE__*/function (_Component) {
_inherits(InputWithOptions, _Component);
var _super = _createSuper(InputWithOptions);
function InputWithOptions(props) {
var _this;
_classCallCheck(this, InputWithOptions);
_this = _super.call(this, props);
_defineProperty(_assertThisInitialized(_this), "onClickOutside", function () {
// Hide the popover
_this.hideOptions(); // Trigger the ClickOutside callback
if (_this.props.onClickOutside) {
_this.props.onClickOutside();
}
});
_defineProperty(_assertThisInitialized(_this), "input", /*#__PURE__*/React.createRef());
_defineProperty(_assertThisInitialized(_this), "isDropdownLayoutVisible", function () {
return _this.state.showOptions && (_this.props.showOptionsIfEmptyInput || _this.state.inputValue.length > 0);
});
_defineProperty(_assertThisInitialized(_this), "_didSelectOption", function (event) {
var focusedElement = event && event.relatedTarget;
var dropdownContainer = _this.dropdownLayout && _this.dropdownLayout.containerRef.current; // Check if user has focused other input component
var isInput = focusedElement instanceof HTMLInputElement;
if (!focusedElement || !dropdownContainer || isInput) {
return false;
}
var isInDropdown = dropdownContainer.contains(focusedElement); // Returns true if element is the dropdown container or is inside of it
return isInDropdown;
});
_this.state = {
inputValue: props.value || '',
showOptions: false,
lastOptionsShow: 0,
isEditing: false
};
_this.uniqueId = uniqueId('InputWithOptions');
_this._onSelect = _this._onSelect.bind(_assertThisInitialized(_this));
_this._onFocus = _this._onFocus.bind(_assertThisInitialized(_this));
_this._onBlur = _this._onBlur.bind(_assertThisInitialized(_this));
_this._onChange = _this._onChange.bind(_assertThisInitialized(_this));
_this._onKeyDown = _this._onKeyDown.bind(_assertThisInitialized(_this));
_this.focus = _this.focus.bind(_assertThisInitialized(_this));
_this.blur = _this.blur.bind(_assertThisInitialized(_this));
_this.select = _this.select.bind(_assertThisInitialized(_this));
_this.hideOptions = _this.hideOptions.bind(_assertThisInitialized(_this));
_this.showOptions = _this.showOptions.bind(_assertThisInitialized(_this));
_this._onManuallyInput = _this._onManuallyInput.bind(_assertThisInitialized(_this));
_this._renderDropdownLayout = _this._renderDropdownLayout.bind(_assertThisInitialized(_this));
_this._onInputClicked = _this._onInputClicked.bind(_assertThisInitialized(_this));
_this.closeOnSelect = _this.closeOnSelect.bind(_assertThisInitialized(_this));
_this.onCompositionChange = _this.onCompositionChange.bind(_assertThisInitialized(_this));
return _this;
}
_createClass(InputWithOptions, [{
key: "inputClasses",
value: // Abstraction
function inputClasses() {}
}, {
key: "dropdownClasses",
value: function dropdownClasses() {}
}, {
key: "dropdownAdditionalProps",
value: function dropdownAdditionalProps() {}
}, {
key: "inputAdditionalProps",
value: function inputAdditionalProps() {}
/**
* An array of key codes that act as manual submit. Will be used within
* onKeyDown(event).
*
* @returns {KeyboardEvent.key[]}
*/
}, {
key: "getManualSubmitKeys",
value: function getManualSubmitKeys() {
return ['Enter', 'Tab'];
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
if (!this.props.showOptionsIfEmptyInput && (!prevProps.value && this.props.value || !prevState.inputValue && this.state.inputValue)) {
this.showOptions();
} // Clear value in controlled mode
if (prevProps.value !== this.props.value && this.props.value === '') {
this.setState({
inputValue: ''
});
}
}
}, {
key: "onCompositionChange",
value: function onCompositionChange(isComposing) {
this.setState({
isComposing: isComposing
});
}
}, {
key: "renderInput",
value: function renderInput() {
var inputAdditionalProps = this.inputAdditionalProps();
var inputProps = Object.assign(omit(['onChange', 'dataHook', 'dropDirectionUp', 'focusOnSelectedOption', 'onClose', 'onSelect', 'onOptionMarked', 'overflow', 'visible', 'options', 'selectedId', 'tabIndex', 'onClickOutside', 'fixedHeader', 'fixedFooter', 'maxHeightPixels', 'minWidthPixels', 'withArrow', 'closeOnSelect', 'onMouseEnter', 'onMouseLeave', 'itemHeight', 'selectedHighlight', 'inContainer', 'infiniteScroll', 'loadMore', 'hasMore', 'markedOption'], this.props), inputAdditionalProps);
var inputElement = inputProps.inputElement;
return /*#__PURE__*/React.cloneElement(inputElement, _objectSpread(_objectSpread({
menuArrow: true,
ref: this.input
}, inputProps), {}, {
onChange: this._onChange,
onInputClicked: this._onInputClicked,
onFocus: this._onFocus,
onBlur: this._onBlur,
onCompositionChange: this.onCompositionChange,
width: inputElement.props.width,
textOverflow: this.props.textOverflow || inputElement.props.textOverflow,
tabIndex: this.props["native"] ? -1 : 0
}));
}
}, {
key: "_renderDropdownLayout",
value: function _renderDropdownLayout() {
var _this2 = this;
var _this$props = this.props,
highlight = _this$props.highlight,
value = _this$props.value;
var inputOnlyProps = omit(['tabIndex'], Input.propTypes);
var dropdownProps = Object.assign(omit(Object.keys(inputOnlyProps).concat(['dataHook', 'onClickOutside']), this.props), this.dropdownAdditionalProps());
var customStyle = {
marginLeft: this.props.dropdownOffsetLeft
};
return /*#__PURE__*/React.createElement("div", {
className: "".concat(this.uniqueId, " ").concat(this.dropdownClasses()),
style: customStyle,
"data-hook": "dropdown-layout-wrapper"
}, /*#__PURE__*/React.createElement(HighlightContext.Provider, {
value: {
highlight: highlight,
match: value
}
}, /*#__PURE__*/React.createElement(DropdownLayout, _extends({
ref: function ref(dropdownLayout) {
return _this2.dropdownLayout = dropdownLayout;
}
}, dropdownProps, {
dataHook: "inputwithoptions-dropdownlayout",
visible: true,
onClose: this.hideOptions,
onSelect: this._onSelect,
isComposing: this.state.isComposing,
inContainer: true,
tabIndex: -1
}))));
}
}, {
key: "_renderNativeSelect",
value: function _renderNativeSelect() {
var _this3 = this;
var _this$props2 = this.props,
options = _this$props2.options,
onSelect = _this$props2.onSelect,
disabled = _this$props2.disabled;
return /*#__PURE__*/React.createElement("div", {
className: classes.nativeSelectWrapper
}, this.renderInput(), /*#__PURE__*/React.createElement("select", {
disabled: disabled,
"data-hook": "native-select",
className: classes.nativeSelect,
onChange: function onChange(event) {
_this3._onChange(event); // In this case we don't use DropdownLayout so we need to invoke `onSelect` manually
onSelect(options[event.target.selectedIndex]);
}
}, options.map(function (option, index) {
return /*#__PURE__*/React.createElement("option", {
"data-hook": "native-option-".concat(option.id),
"data-index": index,
key: option.id,
value: option.value,
className: classes.nativeOption
}, option.value);
})));
}
}, {
key: "render",
value: function render() {
var _this$props3 = this.props,
_native = _this$props3["native"],
dataHook = _this$props3.dataHook,
popoverProps = _this$props3.popoverProps,
dropDirectionUp = _this$props3.dropDirectionUp,
dropdownWidth = _this$props3.dropdownWidth;
var placement = dropDirectionUp ? 'top' : popoverProps.placement;
var body = popoverProps.appendTo === 'window';
return !_native ? /*#__PURE__*/React.createElement(Popover, _extends({
className: classes.root
}, DEFAULT_POPOVER_PROPS, {
dynamicWidth: body,
excludeClass: this.uniqueId
}, popoverProps, {
width: dropdownWidth,
placement: placement,
dataHook: dataHook,
onKeyDown: this._onKeyDown,
onClickOutside: this.onClickOutside,
shown: this.isDropdownLayoutVisible()
}), /*#__PURE__*/React.createElement(Popover.Element, null, /*#__PURE__*/React.createElement("div", {
"data-input-parent": true,
className: this.inputClasses()
}, this.renderInput())), /*#__PURE__*/React.createElement(Popover.Content, null, this._renderDropdownLayout())) : this._renderNativeSelect();
}
/**
* Shows dropdown options
*/
}, {
key: "showOptions",
value: function showOptions() {
if (!this.state.showOptions) {
this.setState({
showOptions: true,
lastOptionsShow: Date.now()
});
this.props.onOptionsShow && this.props.onOptionsShow();
}
}
/**
* Hides dropdown options
*/
}, {
key: "hideOptions",
value: function hideOptions() {
if (this.state.showOptions) {
this.setState({
showOptions: false
});
this.props.onOptionsHide && this.props.onOptionsHide();
this.props.onClose && this.props.onClose();
}
}
}, {
key: "closeOnSelect",
value: function closeOnSelect() {
return this.props.closeOnSelect;
}
}, {
key: "isReadOnly",
get: function get() {
var _ref = this.inputAdditionalProps() || {},
readOnly = _ref.readOnly;
return readOnly;
}
/**
* Determine if the provided key should cause the dropdown to be opened.
*
* @param {KeyboardEvent.key}
* @returns {boolean}
*/
}, {
key: "shouldOpenDropdown",
value: function shouldOpenDropdown(key) {
var openKeys = this.isReadOnly ? ['Enter', 'Spacebar', ' ', 'ArrowDown'] : ['ArrowDown'];
return openKeys.includes(key);
}
/**
* Determine if the provided key should delegate the keydown event to the
* DropdownLayout.
*
* @param {KeyboardEvent.key}
* @returns {boolean}
*/
}, {
key: "shouldDelegateKeyDown",
value: function shouldDelegateKeyDown(key) {
return this.isReadOnly || !['Spacebar', ' '].includes(key);
}
/**
* Determine if the provided key should cause manual submit.
*
* @param {KeyboardEvent.key}
* @returns {boolean}
*/
}, {
key: "shouldPerformManualSubmit",
value: function shouldPerformManualSubmit(key) {
return this.getManualSubmitKeys().includes(key);
}
}, {
key: "_onManuallyInput",
value: function _onManuallyInput() {
var inputValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
if (this.state.isComposing) {
return;
}
inputValue = inputValue.trim();
var suggestedOption = this.props.options.find(function (element) {
return element.value === inputValue;
});
if (this.props.onManuallyInput) {
this.props.onManuallyInput(inputValue, suggestedOption);
}
}
}, {
key: "_onSelect",
value: function _onSelect(option, isSelectedOption) {
var onSelect = this.props.onSelect;
if (this.closeOnSelect() || isSelectedOption) {
this.hideOptions();
}
if (onSelect) {
onSelect(this.props.highlight ? this.props.options.find(function (opt) {
return opt.id === option.id;
}) : option);
}
}
}, {
key: "_onChange",
value: function _onChange(event) {
this.setState({
inputValue: event.target.value
});
if (this.props.onChange) {
this.props.onChange(event);
} // If the input value is not empty, should show the options
if (event.target.value.trim() && !this.props["native"]) {
this.showOptions();
}
}
}, {
key: "_onInputClicked",
value: function _onInputClicked(event) {
if (this.state.showOptions) {
if (Date.now() - this.state.lastOptionsShow > 2000) {
this.hideOptions();
}
} else {
this.showOptions();
}
if (this.props.onInputClicked) {
this.props.onInputClicked(event);
}
}
}, {
key: "_onFocus",
value: function _onFocus(e) {
/** Don't call onFocus if input is already focused or is disabled
* can occur when input is re-focused after selecting an option
*/
if (this._focused || this.props.disabled) {
return;
}
this._focused = true;
this.setState({
isEditing: false
});
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
/** Checks if focus event is related to selecting an option */
}, {
key: "_onBlur",
value: function _onBlur(event) {
// Don't blur input if selected an option
var stopBlur = this._didSelectOption(event);
if (stopBlur) {
// Restore focus to input element
this.focus();
return;
}
this._focused = false;
if (this.props.onBlur) {
this.props.onBlur(event);
}
}
}, {
key: "_onKeyDown",
value: function _onKeyDown(event) {
if (this.props.disabled) {
return;
}
var key = event.key;
/* Enter - prevent a wrapping form from submitting when hitting Enter */
/* ArrowUp - prevent input's native behaviour from moving the text cursor to the beginning */
if (key === 'Enter' || key === 'ArrowUp') {
event.preventDefault();
}
if (key !== 'ArrowDown' && key !== 'ArrowUp') {
this.setState({
isEditing: true
});
}
if (this.shouldOpenDropdown(key)) {
this.showOptions();
event.preventDefault();
}
if (this.shouldDelegateKeyDown(key)) {
// Delegate event and get result
if (this.dropdownLayout) {
var eventWasHandled = this.dropdownLayout._onKeyDown(event);
if (eventWasHandled || this.isReadOnly) {
return;
}
} // For editing mode, we want to *submit* only for specific keys.
if (this.shouldPerformManualSubmit(key)) {
this._onManuallyInput(this.state.inputValue, event);
var inputIsEmpty = !event.target.value;
if (this.closeOnSelect() || key === 'Tab' && inputIsEmpty) {
this.hideOptions();
}
}
}
}
/**
* Sets focus on the input element
* @param {FocusOptions} options
*/
}, {
key: "focus",
value: function focus() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.input.current && this.input.current.focus(options);
}
/**
* Removes focus on the input element
*/
}, {
key: "blur",
value: function blur() {
this.input.current && this.input.current.blur();
}
/**
* Selects all text in the input element
*/
}, {
key: "select",
value: function select() {
this.input.current && this.input.current.select();
}
}]);
return InputWithOptions;
}(Component);
InputWithOptions.defaultProps = _objectSpread(_objectSpread(_objectSpread({}, Input.defaultProps), DropdownLayout.defaultProps), {}, {
onSelect: function onSelect() {},
options: [],
closeOnSelect: true,
inputElement: /*#__PURE__*/React.createElement(Input, null),
valueParser: DEFAULT_VALUE_PARSER,
dropdownWidth: null,
popoverProps: DEFAULT_POPOVER_PROPS,
dropdownOffsetLeft: '0',
showOptionsIfEmptyInput: true,
autocomplete: 'off',
"native": false
});
InputWithOptions.propTypes = _objectSpread(_objectSpread(_objectSpread({}, Input.propTypes), DropdownLayout.propTypes), {}, {
/** Use a customized input component instead of the default wix-style-react `<Input/>` component */
inputElement: PropTypes.element,
/** Closes DropdownLayout on option selection */
closeOnSelect: PropTypes.bool,
/** A callback which is called when the user performs a Submit-Action.
* Submit-Action triggers are: "Enter", "Tab", [typing any defined delimiters], Paste action.
* `onManuallyInput(values: Array<string>): void - The array of strings is the result of splitting the input value by the given delimiters */
onManuallyInput: PropTypes.func,
/** A callback which is called when options dropdown is shown */
onOptionsShow: PropTypes.func,
/** A callback which is called when options dropdown is hidden */
onOptionsHide: PropTypes.func,
/** Function that receives an option, and should return the value to be displayed. */
valueParser: PropTypes.func,
/** Sets the width of the dropdown */
dropdownWidth: PropTypes.string,
/** Sets the offset of the dropdown from the left */
dropdownOffsetLeft: PropTypes.string,
/** Controls whether to show options if input is empty */
showOptionsIfEmptyInput: PropTypes.bool,
/** Mark in bold word parts based on search pattern */
highlight: PropTypes.bool,
/** Indicates whether to render using the native select element */
"native": PropTypes.bool,
/** common popover props */
popoverProps: PropTypes.shape({
appendTo: PropTypes.oneOf(['window', 'scrollParent', 'parent', 'viewport']),
maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
flip: PropTypes.bool,
fixed: PropTypes.bool,
placement: PropTypes.oneOf(['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']),
dynamicWidth: PropTypes.bool
})
});
InputWithOptions.displayName = 'InputWithOptions';
export default InputWithOptions;