UNPKG

@wix/design-system

Version:

@wix/design-system

227 lines 9.65 kB
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