@wix/design-system
Version:
@wix/design-system
227 lines • 9.65 kB
JavaScript
import React, { Component } from 'react';
import InputWithOptions from '../InputWithOptions';
import { Search as SearchIcon, SearchSmall } from '@wix/wix-ui-icons-common';
import { StringUtils } from '../utils/StringUtils';
import { st, classes } from './Search.st.css.js';
import Input from '../Input/Input';
import { WixStyleReactDefaultsOverrideContext } from '../WixStyleReactDefaultsOverrideProvider';
import { IconThemeContext } from '../WixDesignSystemIconThemeProvider/IconThemeContext';
// because lodash debounce is not compatible with jest timeout mocks
function debounce(fn, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(context, args), wait);
};
}
/**
* Search component with suggestions based on input value listed in dropdown
*/
class Search extends Component {
constructor(props) {
super(props);
this.searchInput = React.createRef();
this.contentElementRef = React.createRef();
this._onContentTransitionEnd = () => {
this.props.onExpandTransitionEnd?.(this.state.collapsed);
};
this._onContentTransitionStart = () => {
this.props.onExpandTransitionStart?.(this.state.collapsed);
};
/**
* Creates an onChange debounced function
*/
this._createDebouncedOnChange = () => {
const { debounceMs, onChange } = this.props;
return debounceMs > 0 ? debounce(onChange, debounceMs) : onChange;
};
this._getIsControlled = () => 'value' in this.props && 'onChange' in this.props;
this._getFilteredOptions = () => {
const { options, predicate } = this.props;
const searchText = this._currentValue();
if (!searchText || !searchText.length) {
return options;
}
const filterFn = predicate || this._stringFilter;
return options.filter(filterFn);
};
this._stringFilter = option => {
const searchText = this._currentValue();
return StringUtils.includesCaseInsensitive(option.value, searchText.trim());
};
this._onChange = e => {
e.persist();
this.setState({
inputValue: e.target.value,
}, () => {
this._onChangeHandler(e);
});
};
this._onClear = event => {
const { expandable } = this.props;
const { collapsed } = this.state;
const stateChanges = {};
if (!this._getIsControlled()) {
stateChanges.inputValue = '';
}
if (expandable && !collapsed && this._currentValue === '') {
stateChanges.collapsed = true;
this.searchInput.current && this.searchInput.current.blur();
}
this.setState(stateChanges, () => {
this._onClearHandler(event);
});
};
this._onClearHandler = event => {
const { onClear } = this.props;
if (onClear)
onClear(event);
};
this._currentValue = () => this.state.inputValue;
this._onFocus = event => {
const { onFocus } = this.props;
if (this.state.collapsed && this.props.expandable) {
this.setState({
collapsed: false,
});
}
onFocus && onFocus(event);
};
this._onBlur = async (event) => {
const { onBlur } = this.props;
onBlur && (await onBlur(event));
if (!this.state.collapsed && this.props.expandable) {
const value = this._currentValue();
if (value === '') {
this.setState({
collapsed: true,
});
}
}
};
this._onWrapperClick = () => {
if (!this.props.expandable ||
(this.props.expandable && this.state.collapsed)) {
this.searchInput.current && this.searchInput.current.focus();
}
};
this._onWrapperMouseDown = e => {
// We need to capture mouse down and prevent it's event if the input
// is already open
if (this.props.expandable && !this.state.collapsed) {
const value = this._currentValue();
if (value === '') {
e.preventDefault();
}
}
};
/**
* Sets focus on the input element
*/
this.focus = () => {
this.searchInput.current && this.searchInput.current.focus();
};
/**
* Removes focus on the input element
*/
this.blur = () => {
this.searchInput.current && this.searchInput.current.blur();
};
/**
* Clears the input.
*
* @param event
*/
this.clear = event => {
this.searchInput.current && this.searchInput.current.clear(event);
};
const initialValue = this._getIsControlled()
? props.value
: props.defaultValue || '';
this._onChangeHandler = this._createDebouncedOnChange();
this.state = {
inputValue: initialValue,
collapsed: props.expandable && !initialValue && !props.autoFocus,
};
}
_addContentElementListeners() {
const contentElement = this.contentElementRef.current;
if (!contentElement) {
return;
}
contentElement.addEventListener('transitionstart', this._onContentTransitionStart);
contentElement.addEventListener('transitionend', this._onContentTransitionEnd);
}
_removeContentElementListeners() {
const contentElement = this.contentElementRef.current;
if (!contentElement) {
return;
}
contentElement.removeEventListener('transitionstart', this._onContentTransitionStart);
contentElement.removeEventListener('transitionend', this._onContentTransitionEnd);
}
componentDidMount() {
this._addContentElementListeners();
}
componentWillUnmount() {
this._removeContentElementListeners();
}
componentDidUpdate(prevProps) {
const { value, expandable } = this.props;
if (prevProps.value !== value) {
// Collapse the expandable input when the input has no focus & no value in it.
const collapsed = expandable &&
this.searchInput.current &&
!this.searchInput.current._focused &&
prevProps.value &&
!value
? true
: this.props.collapsed;
this.setState({ inputValue: value, collapsed });
}
if (prevProps.debounceMs !== this.props.debounceMs ||
prevProps.onChange !== this.props.onChange) {
this._onChangeHandler = this._createDebouncedOnChange();
}
}
render() {
const { input: inputPropsDefaults } = this.context;
const { defaultValue, dataHook, expandWidth, highlight, ...restProps } = this.props;
const { expandable, size = inputPropsDefaults.size } = restProps;
const { collapsed, inputValue } = this.state;
const contentStyle = expandable && !collapsed ? { width: expandWidth } : undefined;
return (React.createElement(IconThemeContext.Consumer, null, ({ icons = {} }) => {
const SearchIcons = {
Search: icons.Search?.Search || SearchIcon,
SearchSmall: icons.Search?.SearchSmall || SearchSmall,
};
return (React.createElement("div", { "data-hook": dataHook, className: st(classes.root, {
expandable,
collapsed: expandable && collapsed,
expandDirection: restProps.expandDirection,
size,
border: restProps.border,
}), onClick: this._onWrapperClick, onMouseDown: this._onWrapperMouseDown, "data-expandable": expandable || null, "data-collapsed": (expandable && collapsed) || null },
React.createElement("div", { ref: this.contentElementRef, "data-hook": "search-content", className: classes.content, style: contentStyle },
React.createElement(InputWithOptions, { ...restProps, showDrawerOnMobile: false, value: inputValue, ref: this.searchInput, prefix: React.createElement(Input.IconAffix, null, size === 'small' ? (React.createElement(SearchIcons.SearchSmall, null)) : (React.createElement(SearchIcons.Search, null))), dataHook: "search-inputwithoptions", menuArrow: false, closeOnSelect: true, options: this._getFilteredOptions(), customDropdownContent: !this._getFilteredOptions().length &&
this.props.renderEmptyState?.(inputValue), onClear: restProps.clearButton ? this._onClear : undefined, onChange: this._onChange, onFocus: this._onFocus, onBlur: this._onBlur, highlight: highlight }))));
}));
}
}
Search.contextType = WixStyleReactDefaultsOverrideContext;
Search.displayName = 'Search';
Search.defaultProps = {
...InputWithOptions.defaultProps,
clearButton: true,
placeholder: 'Search',
expandable: false,
expandWidth: '100%',
expandDirection: 'right',
debounceMs: 0,
onChange: () => { },
highlight: true,
border: 'round',
};
export default Search;
//# sourceMappingURL=Search.js.map