wix-style-react
Version:
wix-style-react
393 lines (338 loc) • 9.95 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import WixComponent from '../BaseComponents/WixComponent';
import Input from '../Input';
import omit from 'omit';
import DropdownLayout, {
DIVIDER_OPTION_VALUE,
} from '../DropdownLayout/DropdownLayout';
import Highlighter from '../Highlighter/Highlighter';
import { chainEventHandlers } from '../utils/ChainEventHandlers';
export const DEFAULT_VALUE_PARSER = option => option.value;
class InputWithOptions extends WixComponent {
// Abstraction
inputClasses() {}
dropdownClasses() {}
dropdownAdditionalProps() {}
inputAdditionalProps() {}
/**
* An array of key codes that act as manual submit. Will be used within
* onKeyDown(event).
*
* @returns {KeyboardEvent.key[]}
*/
getManualSubmitKeys() {
return ['Enter', 'Tab'];
}
constructor(props) {
super(props);
this.state = {
inputValue: props.value || '',
showOptions: false,
lastOptionsShow: 0,
isEditing: false,
};
this._onSelect = this._onSelect.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onBlur = this._onBlur.bind(this);
this._onChange = this._onChange.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this.focus = this.focus.bind(this);
this.blur = this.blur.bind(this);
this.select = this.select.bind(this);
this.hideOptions = this.hideOptions.bind(this);
this.showOptions = this.showOptions.bind(this);
this._onManuallyInput = this._onManuallyInput.bind(this);
this._renderDropdownLayout = this._renderDropdownLayout.bind(this);
this._onInputClicked = this._onInputClicked.bind(this);
this.closeOnSelect = this.closeOnSelect.bind(this);
this.onCompositionChange = this.onCompositionChange.bind(this);
}
componentDidUpdate(prevProps, prevState) {
super.componentDidUpdate(prevProps);
if (
!this.props.showOptionsIfEmptyInput &&
((!prevProps.value && this.props.value) ||
(!prevState.inputValue && this.state.inputValue))
) {
this.showOptions();
}
}
onCompositionChange(isComposing) {
this.setState({ isComposing });
}
onClickOutside() {
this.hideOptions();
}
renderInput() {
const inputAdditionalProps = this.inputAdditionalProps();
const inputProps = Object.assign(
omit(
Object.keys(DropdownLayout.propTypes).concat(['onChange', 'dataHook']),
this.props,
),
inputAdditionalProps,
);
const { inputElement } = inputProps;
return React.cloneElement(inputElement, {
menuArrow: true,
ref: input => (this.input = input),
...inputProps,
onKeyDown: chainEventHandlers(
inputAdditionalProps && inputAdditionalProps.onKeyDown,
this._onKeyDown,
),
theme: this.props.theme,
onChange: this._onChange,
onInputClicked: this._onInputClicked,
onFocus: this._onFocus,
onBlur: this._onBlur,
onCompositionChange: this.onCompositionChange,
width: inputElement.props.width,
textOverflow: inputElement.props.textOverflow,
});
}
_processOptions(options) {
return !this.props.highlight
? options
: options.map(option => {
return {
...option,
value:
option.value === DIVIDER_OPTION_VALUE ? (
option.value
) : (
<Highlighter
match={this.state.inputValue}
dataHook={`highlighter-${option.id}`}
>
{option.value}
</Highlighter>
),
};
});
}
_renderDropdownLayout() {
const inputOnlyProps = omit(['tabIndex'], Input.propTypes);
const dropdownProps = Object.assign(
omit(Object.keys(inputOnlyProps).concat(['dataHook']), this.props),
this.dropdownAdditionalProps(),
);
const customStyle = { marginLeft: this.props.dropdownOffsetLeft };
if (this.props.dropdownWidth) {
customStyle.width = this.props.dropdownWidth;
}
const isDropdownLayoutVisible =
this.state.showOptions &&
(this.props.showOptionsIfEmptyInput || this.state.inputValue.length > 0);
return (
<div
className={this.dropdownClasses()}
style={customStyle}
data-hook="dropdown-layout-wrapper"
>
<DropdownLayout
ref={dropdownLayout => (this.dropdownLayout = dropdownLayout)}
{...dropdownProps}
options={this._processOptions(dropdownProps.options)}
theme={this.props.theme}
visible={isDropdownLayoutVisible}
onClose={this.hideOptions}
onSelect={this._onSelect}
isComposing={this.state.isComposing}
/>
</div>
);
}
render() {
const { dropDirectionUp } = this.props;
return (
<div>
{dropDirectionUp ? this._renderDropdownLayout() : null}
<div data-input-parent className={this.inputClasses()}>
{this.renderInput()}
</div>
{!dropDirectionUp ? this._renderDropdownLayout() : null}
</div>
);
}
hideOptions() {
if (this.state.showOptions) {
this.setState({ showOptions: false });
}
}
showOptions() {
this.setState({ showOptions: true, lastOptionsShow: Date.now() });
}
closeOnSelect() {
return this.props.closeOnSelect;
}
get isReadOnly() {
const { readOnly } = this.inputAdditionalProps() || {};
return readOnly;
}
/**
* Determine if the provided key should cause the dropdown to be opened.
*
* @param {KeyboardEvent.key}
* @returns {boolean}
*/
shouldOpenDropdown(key) {
const 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}
*/
shouldDelegateKeyDown(key) {
return this.isReadOnly || !['Spacebar', ' '].includes(key);
}
/**
* Determine if the provided key should cause manual submit.
*
* @param {KeyboardEvent.key}
* @returns {boolean}
*/
shouldPerformManualSubmit(key) {
return this.getManualSubmitKeys().includes(key);
}
_onManuallyInput(inputValue = '') {
if (this.state.isComposing) {
return;
}
inputValue = inputValue.trim();
if (this.closeOnSelect()) {
this.hideOptions();
}
const suggestedOption = this.props.options.find(
element => element.value === inputValue,
);
if (this.props.onManuallyInput) {
this.props.onManuallyInput(inputValue, suggestedOption);
}
}
_onSelect(option, isSelectedOption) {
this.showOptions();
const { onSelect } = this.props;
if (this.closeOnSelect()) {
this.setState({ showOptions: false });
}
if (isSelectedOption) {
this.setState({ showOptions: false });
}
if (onSelect) {
onSelect(
this.props.highlight
? this.props.options.find(opt => opt.id === option.id)
: option,
);
}
}
_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.showOptions();
}
}
_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);
}
}
_onFocus(e) {
if (this.props.disabled) {
return;
}
this._focused = true;
this.setState({ isEditing: false });
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
_onBlur(e) {
this._focused = false;
if (this.props.onBlur) {
this.props.onBlur(e);
}
}
_onKeyDown(event) {
if (this.props.disabled) {
return;
}
const { key } = event;
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
const 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);
}
}
}
focus(options = {}) {
this.input.focus(options);
}
blur() {
this.input.blur();
}
select() {
this.input.select();
}
}
InputWithOptions.defaultProps = {
...Input.defaultProps,
...DropdownLayout.defaultProps,
onSelect: () => {},
options: [],
closeOnSelect: true,
inputElement: <Input />,
valueParser: DEFAULT_VALUE_PARSER,
dropdownWidth: null,
dropdownOffsetLeft: '0',
showOptionsIfEmptyInput: true,
autocomplete: 'off',
};
InputWithOptions.propTypes = {
...Input.propTypes,
...DropdownLayout.propTypes,
autocomplete: PropTypes.string,
inputElement: PropTypes.element,
closeOnSelect: PropTypes.bool,
onManuallyInput: PropTypes.func,
/** Function that receives an option, and should return the value to be displayed. By default returns `option.value`. */
valueParser: PropTypes.func,
dropdownWidth: PropTypes.string,
dropdownOffsetLeft: PropTypes.string,
/** Controls whether to show options if input is empty */
showOptionsIfEmptyInput: PropTypes.bool,
highlight: PropTypes.bool,
};
InputWithOptions.displayName = 'InputWithOptions';
export default InputWithOptions;