UNPKG

wix-style-react

Version:
367 lines 17.8 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; 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'; import Input from '../Input/Input'; import { optionValidator } from '../DropdownLayout/DropdownLayout'; import { WixStyleReactDefaultsOverrideContext } from '../WixStyleReactDefaultsOverrideProvider'; // 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(); /** * 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, }; } 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("div", { "data-hook": dataHook, className: st(classes.root, { expandable, expanded: expandable && collapsed, size, }), onClick: this._onWrapperClick, onMouseDown: this._onWrapperMouseDown, "data-expandable": expandable || null, "data-collapsed": (expandable && collapsed) || null }, React.createElement("div", { className: classes.content, style: contentStyle }, React.createElement(InputWithOptions, { ...restProps, value: inputValue, ref: this.searchInput, prefix: React.createElement(Input.IconAffix, null, size === 'small' ? React.createElement(SearchSmall, null) : React.createElement(SearchIcon, null)), dataHook: "search-inputwithoptions", menuArrow: false, closeOnSelect: true, options: this._getFilteredOptions(), onClear: restProps.clearButton ? this._onClear : undefined, onChange: this._onChange, onFocus: this._onFocus, onBlur: this._onBlur, highlight: highlight })))); } } Search.contextType = WixStyleReactDefaultsOverrideContext; Search.displayName = 'Search'; Search.propTypes = { /** Associate a control with the regions that it controls */ ariaControls: PropTypes.string, /** Associate a region with its descriptions. Similar to aria-controls but instead associating descriptions to the region and description identifiers are separated with a space. */ ariaDescribedby: PropTypes.string, /** Define a string that labels the current element in case where a text label is not visible on the screen */ ariaLabel: PropTypes.string, /** Sets the value of native autocomplete attribute (consult the [HTML spec](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete) for possible values) */ autocomplete: PropTypes.string, /** Focus the element on mount (standard React input autoFocus) */ autoFocus: PropTypes.bool, /** Control the border style of input */ border: PropTypes.oneOf(['standard', 'round', 'bottomLine', 'none']), /** Specifies a CSS class name to be appended to the component’s root element */ className: PropTypes.string, /** Displays clear button (X) on a non-empty input */ clearButton: PropTypes.bool, /** Closes DropdownLayout when option is selected */ closeOnSelect: PropTypes.bool, /** Applies a data-hook HTML attribute that can be used in the tests */ dataHook: PropTypes.string, /** Specifies the `onChange` debounce in milliseconds */ debounceMs: PropTypes.number, /** Defines the initial value of an input for those who want to use this component un-controlled */ defaultValue: PropTypes.string, /** Specifies whether the input should be disabled or not */ disabled: PropTypes.bool, /** Restricts input editing */ disableEditing: PropTypes.bool, /** Sets the width of the dropdown in pixels */ dropdownWidth: PropTypes.string, /** Specifies whether to collapse input to search icon only. Once clicked, icon will expand to a full search input. */ expandable: PropTypes.bool, /** Specifies the width of an input in an expanded state */ expandWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** USED FOR TESTING - forces focus state on the input */ forceFocus: PropTypes.bool, /** USED FOR TESTING - forces hover state on the input */ forceHover: PropTypes.bool, /** Specifies whether there are more items to be loaded */ hasMore: PropTypes.bool, /** Specifies whether the status suffix should be hidden */ hideStatusSuffix: PropTypes.bool, /** Highlight word parts that match search criteria in bold */ highlight: PropTypes.bool, /** Assigns an unique identifier for the root element */ id: PropTypes.string, /** Specifies whether lazy loading of the dropdown layout items is enabled */ infiniteScroll: PropTypes.bool, /** Defines a callback function which is called on a request to render more list items */ loadMore: PropTypes.func, /** Sets the maximum height of the `dropdownLayout` in pixels */ maxHeightPixels: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Sets the maximum number of characters that can be entered into a field */ maxLength: PropTypes.number, /** Sets the minimum width of dropdownLayout in pixels */ minWidthPixels: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Reference element data when a form is submitted */ name: PropTypes.string, /** Specifies whether input shouldn’t have rounded corners on its left */ noLeftBorderRadius: PropTypes.bool, /** Specifies whether input shouldn’t have rounded corners on its right */ noRightBorderRadius: PropTypes.bool, /** Defines a standard input `onBlur` callback */ onBlur: PropTypes.func, /** Defines a standard input `onChange` callback */ onChange: PropTypes.func, /** Displays clear button (X) on a non-empty input and calls a callback function with no arguments */ onClear: PropTypes.func, /** Defines a callback function which is called whenever the user presses the escape key */ onClose: PropTypes.func, /** Defines a callback function called on `compositionstart`/`compositionend` events */ onCompositionChange: PropTypes.func, /** Defines a callback handler that is called when the presses -enter- */ onEnterPressed: PropTypes.func, /** Defines a callback handler that is called when the user presses -escape- */ onEscapePressed: PropTypes.func, /** Defines a standard input `onFocus` callback */ onFocus: PropTypes.func, /** Defines a standard input `onClick` callback */ onInputClicked: PropTypes.func, /** Defines a standard input `onKeyDown` callback */ onKeyDown: PropTypes.func, /** Defines a standard input `onKeyUp` callback */ onKeyUp: PropTypes.func, /** Defines a callback function 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, /** Defines a callback function which is called whenever the user enters dropdown layout with the mouse cursor */ onMouseEnter: PropTypes.func, /** Defines a callback function which is called whenever the user exits from dropdown layout with a mouse cursor */ onMouseLeave: PropTypes.func, /** Defines a callback function which is called whenever an option becomes focused (hovered/active). Receives the relevant option object from the original `props.options array`. */ onOptionMarked: PropTypes.func, /** Defines a callback function which is called when options dropdown is hidden */ onOptionsHide: PropTypes.func, /** Defines a callback function which is called when the options dropdown is shown */ onOptionsShow: PropTypes.func, /** Defines a callback handler that is called when user pastes text from a clipboard (using mouse or keyboard shortcut) */ onPaste: PropTypes.func, /** Defines a callback function which is called whenever user selects a different option in the list */ onSelect: PropTypes.func, /** Array of objects: * - `id <string / number>` *required*: the id of the option, should be unique; * - value `<function / string / node>` *required*: can be a string, react element or a builder function; * - disabled `<bool>` *default value- false*: whether this option is disabled or not; * - linkTo `<string>`: when provided the option will be an anchor to the given value; * - title `<bool>` *default value- false* **deprecated**: please use `listItemSectionBuilder` for rendering a title; * - overrideStyle `<bool>` *default value- false* **deprecated**: please use `overrideOptionStyle` for override option styles; * - overrideOptionStyle `<bool>` *default value- false* - when set to `true`, the option will be responsible for its own styles. No styles will be applied from the DropdownLayout itself; * - label `<string>`: the string displayed within an input when the option is selected. This is used when using `<DropdownLayout/>` with an `<Input/>`. */ options: PropTypes.arrayOf(optionValidator), /** Handles container overflow */ overflow: PropTypes.string, /** Sets a pattern that the typed value must match to be valid (regex) */ pattern: PropTypes.string, /** Sets a placeholder message to display */ placeholder: PropTypes.string, /** Allows to pass common popover props. Check `<Popover/>` API for a full list. */ 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, }), /** Defines a custom function for options filtering */ predicate: PropTypes.func, /** Specifies whether input is read only */ readOnly: PropTypes.bool, /** Specifies that an input must be filled out before submitting the form */ required: PropTypes.bool, /** Specifies selected option by its id */ selectedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Controls whether to show options if input is empty */ showOptionsIfEmptyInput: PropTypes.bool, /** Controls the size of the input */ size: PropTypes.oneOf(['small', 'medium', 'large']), /** Specify the status of a field */ status: PropTypes.oneOf(['error', 'warning', 'loading']), /** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */ statusMessage: PropTypes.node, /** Indicates that element can be focused and where it participates in sequential keyboard navigation */ tabIndex: PropTypes.number, /** Handles text overflow behavior. It can either `clip` (default) or display `ellipsis`. */ textOverflow: PropTypes.string, /** Controls the placement of a status tooltip */ tooltipPlacement: PropTypes.string, /** Specifies the type of `<input>` element to display. The default type is text. */ type: PropTypes.string, /** Specifies the current value of the element */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; Search.defaultProps = { ...InputWithOptions.defaultProps, clearButton: true, placeholder: 'Search', expandable: false, expandWidth: '100%', debounceMs: 0, onChange: () => { }, highlight: true, border: 'round', }; export default Search; //# sourceMappingURL=Search.js.map