UNPKG

terra-search-field

Version:

A search component with a field that automatically performs a search callback after user input.

350 lines (298 loc) 9.61 kB
import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; import ThemeContext from 'terra-theme-context'; import Button from 'terra-button'; import * as KeyCode from 'keycode-js'; import IconSearch from 'terra-icon/lib/icon/IconSearch'; import { injectIntl } from 'react-intl'; import styles from './SearchField.module.scss'; const cx = classNamesBind.bind(styles); const Icon = <IconSearch />; const propTypes = { /** * The defaultValue of the search field. Use this to create an uncontrolled search field. */ defaultValue: PropTypes.string, /** * When true, will disable the auto-search. */ disableAutoSearch: PropTypes.bool, /** * Group name value for search group elements. This value is also used as Label name. Default value Search is being used when no value is provided. */ groupName: PropTypes.string, /** * Callback ref to pass into the inner input component. */ inputRefCallback: PropTypes.func, /** * Custom input attributes to apply to the input field such as aria-label. */ // eslint-disable-next-line react/forbid-prop-types inputAttributes: PropTypes.object, /** * @private * The intl object containing translations. This is retrieved from the context automatically by injectIntl. */ intl: PropTypes.shape({ formatMessage: PropTypes.func }).isRequired, /** * Whether or not the field should display as a block. */ isBlock: PropTypes.bool, /** * When true, will disable the field. */ isDisabled: PropTypes.bool, /** * Whether or not the label is visible. Use this prop to include a label above the search field. The label will be same as groupName prop value. */ isLabelVisible: PropTypes.bool, /** * The minimum number of characters to perform a search. */ minimumSearchTextLength: PropTypes.number, /** * Function to trigger when user changes the input value. Provide a function to create a controlled input. */ onChange: PropTypes.func, /** * Function to trigger when user inputs a value. Use when programmatically setting a value. Sends parameter {Event} event. */ onInput: PropTypes.func, /** * A callback to indicate an invalid search. Sends parameter {String} searchText. */ onInvalidSearch: PropTypes.func, /** * A callback to perform search. Sends parameter {String} searchText. */ onSearch: PropTypes.func, /** * Placeholder text to show while the search field is empty. */ placeholder: PropTypes.string, /** * How long the component should wait (in milliseconds) after input before performing an automatic search. */ searchDelay: PropTypes.number, /** * The value of search field. Use this to create a controlled search field. */ value: PropTypes.string, }; const defaultProps = { defaultValue: undefined, disableAutoSearch: false, groupName: 'Search', isBlock: false, isDisabled: false, isLabelVisible: false, minimumSearchTextLength: 2, placeholder: '', searchDelay: 2500, value: undefined, inputAttributes: undefined, }; class SearchField extends React.Component { constructor(props) { super(props); this.handleClear = this.handleClear.bind(this); this.handleTextChange = this.handleTextChange.bind(this); this.handleSearch = this.handleSearch.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleInput = this.handleInput.bind(this); this.setInputRef = this.setInputRef.bind(this); this.updateSearchText = this.updateSearchText.bind(this); this.searchTimeout = null; this.searchText = this.props.defaultValue || this.props.value; this.searchBtnRef = React.createRef(); } componentDidUpdate() { // if consumer updates the value prop with onChange, need to update variable to match this.updateSearchText(this.props.value); } componentWillUnmount() { this.clearSearchTimeout(); } handleClear(event) { // Pass along changes to consuming components using associated props if (this.props.onChange) { this.props.onChange(event, ''); } if (this.props.onInvalidSearch) { this.props.onInvalidSearch(''); } this.updateSearchText(''); // Clear input field if (this.inputRef) { this.inputRef.value = ''; this.inputRef.focus(); } } handleTextChange(event) { const textValue = event.target.value; this.updateSearchText(textValue); if (this.props.onChange) { this.props.onChange(event, textValue); } if (!this.props.disableAutoSearch) { this.clearSearchTimeout(); this.searchTimeout = setTimeout(this.handleSearch, this.props.searchDelay); } } handleInput(event) { const textValue = event.target.value; this.updateSearchText(textValue); if (this.props.onInput) { this.props.onInput(event); } } handleKeyDown(event) { if (event.nativeEvent.keyCode === KeyCode.KEY_RETURN) { event.preventDefault(); // set focus to search button to hide keyboard on mobile devices this.searchBtnRef.current.focus(); this.handleSearch(); } if (event.nativeEvent.keyCode === KeyCode.KEY_ESCAPE) { this.handleClear(event); } } handleSearch() { this.clearSearchTimeout(); const searchText = this.searchText || ''; if (searchText.length >= this.props.minimumSearchTextLength && this.props.onSearch) { this.props.onSearch(searchText); } else if (this.props.onInvalidSearch) { this.props.onInvalidSearch(searchText); } } setInputRef(node) { this.inputRef = node; if (this.props.inputRefCallback) { this.props.inputRefCallback(node); } } updateSearchText(searchText) { if (typeof searchText !== 'undefined' && searchText !== this.searchText) { this.searchText = searchText; // Forcing update for clearButton rerender. this.forceUpdate(); } } clearSearchTimeout() { if (this.searchTimeout) { clearTimeout(this.searchTimeout); this.searchTimeout = null; } } render() { const { defaultValue, disableAutoSearch, groupName, inputRefCallback, inputAttributes, intl, isBlock, isDisabled, isLabelVisible, minimumSearchTextLength, onChange, onInput, onInvalidSearch, onSearch, placeholder, searchDelay, value, ...customProps } = this.props; const theme = this.context; const searchContainerClassNames = cx([ 'search-container', theme.className, ]); const searchFieldClassNames = classNames( cx( 'search-field', { block: isBlock }, ), customProps.className, ); const groupNameValue = groupName === 'Search' ? intl.formatMessage({ id: 'Terra.searchField.search' }) : groupName; const inputAriaLabelText = inputAttributes && Object.prototype.hasOwnProperty.call(inputAttributes, 'aria-label') ? inputAttributes['aria-label'] : groupNameValue; const buttonText = intl.formatMessage({ id: 'Terra.searchField.submit-search' }); const clearText = intl.formatMessage({ id: 'Terra.searchField.clear' }); const additionalInputAttributes = { ...inputAttributes }; const clearIcon = <span className={cx('clear-icon')} />; const inputClass = classNames( cx( 'input', ), additionalInputAttributes.className, ); if (value !== undefined) { additionalInputAttributes.value = value; } else { additionalInputAttributes.defaultValue = defaultValue; } const clearButton = this.searchText && !isDisabled ? ( <Button data-terra-search-field-button="Clear" className={cx('clear')} onClick={this.handleClear} text={clearText} variant="utility" icon={clearIcon} isIconOnly /> ) : undefined; return ( <div className={searchContainerClassNames}> {isLabelVisible && ( <label className={cx('label')}>{groupName}</label> )} <div className={cx('search-role-container')}> <div role="group" aria-label={intl.formatMessage({ id: 'Terra.searchField.search' })} {...customProps} className={searchFieldClassNames}> <div className={cx('input-group')}> <input {...additionalInputAttributes} className={inputClass} type="search" placeholder={placeholder} onChange={this.handleTextChange} disabled={isDisabled} aria-label={inputAriaLabelText} aria-disabled={isDisabled} onKeyDown={this.handleKeyDown} onInput={this.handleInput} ref={this.setInputRef} /> {clearButton} </div> <Button data-terra-search-field-button="Search" className={cx('button')} text={buttonText} onClick={this.handleSearch} isDisabled={isDisabled} icon={Icon} isIconOnly isCompact refCallback={(ref) => { this.searchBtnRef.current = ref; }} /> </div> </div> </div> ); } } SearchField.propTypes = propTypes; SearchField.defaultProps = defaultProps; SearchField.contextType = ThemeContext; export default injectIntl(SearchField);