UNPKG

7.34 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import classNames from 'classnames/bind';
4import Button from 'terra-button';
5import * as KeyCode from 'keycode-js';
6import IconSearch from 'terra-icon/lib/icon/IconSearch';
7import Input from 'terra-form-input';
8import { injectIntl, intlShape } from 'react-intl';
9import styles from './SearchField.module.scss';
10
11const cx = classNames.bind(styles);
12
13const Icon = <IconSearch />;
14
15const propTypes = {
16 /**
17 * The defaultValue of the search field. Use this to create an uncontrolled search field.
18 */
19 defaultValue: PropTypes.string,
20
21 /**
22 * When true, will disable the auto-search.
23 */
24 disableAutoSearch: PropTypes.bool,
25
26 /**
27 * Callback ref to pass into the inner input component.
28 */
29 inputRefCallback: PropTypes.func,
30
31 /**
32 * Custom input attributes to apply to the input field such as aria-label.
33 */
34 // eslint-disable-next-line react/forbid-prop-types
35 inputAttributes: PropTypes.object,
36
37 /**
38 * @private
39 * The intl object containing translations. This is retrieved from the context automatically by injectIntl.
40 */
41 intl: intlShape.isRequired,
42
43 /**
44 * Whether or not the field should display as a block.
45 */
46 isBlock: PropTypes.bool,
47
48 /**
49 * When true, will disable the field.
50 */
51 isDisabled: PropTypes.bool,
52
53 /**
54 * The minimum number of characters to perform a search.
55 */
56 minimumSearchTextLength: PropTypes.number,
57
58 /**
59 * Function to trigger when user changes the input value. Provide a function to create a controlled input.
60 */
61 onChange: PropTypes.func,
62
63 /**
64 * A callback to indicate an invalid search. Sends parameter {String} searchText.
65 */
66 onInvalidSearch: PropTypes.func,
67
68 /**
69 * A callback to perform search. Sends parameter {String} searchText.
70 */
71 onSearch: PropTypes.func,
72
73 /**
74 * Placeholder text to show while the search field is empty.
75 */
76 placeholder: PropTypes.string,
77
78 /**
79 * How long the component should wait (in milliseconds) after input before performing an automatic search.
80 */
81 searchDelay: PropTypes.number,
82
83 /**
84 * The value of search field. Use this to create a controlled search field.
85 */
86 value: PropTypes.string,
87
88};
89
90const defaultProps = {
91 defaultValue: undefined,
92 disableAutoSearch: false,
93 isBlock: false,
94 isDisabled: false,
95 minimumSearchTextLength: 2,
96 placeholder: '',
97 searchDelay: 250,
98 value: undefined,
99 inputAttributes: undefined,
100};
101
102class SearchField extends React.Component {
103 constructor(props) {
104 super(props);
105
106 this.handleClear = this.handleClear.bind(this);
107 this.handleTextChange = this.handleTextChange.bind(this);
108 this.handleSearch = this.handleSearch.bind(this);
109 this.handleKeyDown = this.handleKeyDown.bind(this);
110 this.setInputRef = this.setInputRef.bind(this);
111 this.updateSearchText = this.updateSearchText.bind(this);
112
113 this.searchTimeout = null;
114 this.state = { searchText: this.props.defaultValue || this.props.value };
115 }
116
117 componentDidUpdate() {
118 // if consumer updates the value prop with onChange, need to update state to match
119 this.updateSearchText(this.props.value);
120 }
121
122 componentWillUnmount() {
123 this.clearSearchTimeout();
124 }
125
126 setInputRef(node) {
127 this.inputRef = node;
128 if (this.props.inputRefCallback) {
129 this.props.inputRefCallback(node);
130 }
131 }
132
133 handleClear(event) {
134 // Pass along changes to consuming components using associated props
135 if (this.props.onChange) {
136 this.props.onChange(event, '');
137 }
138
139 if (this.props.onInvalidSearch) {
140 this.props.onInvalidSearch('');
141 }
142 this.setState({ searchText: '' });
143
144 // Clear input field
145 if (this.inputRef) {
146 this.inputRef.value = '';
147 this.inputRef.focus();
148 }
149 }
150
151 handleTextChange(event) {
152 const textValue = event.target.value;
153 this.updateSearchText(textValue);
154
155 if (this.props.onChange) {
156 this.props.onChange(event, textValue);
157 }
158
159 if (!this.searchTimeout && !this.props.disableAutoSearch) {
160 this.searchTimeout = setTimeout(this.handleSearch, this.props.searchDelay);
161 }
162 }
163
164 updateSearchText(searchText) {
165 if (searchText !== undefined && searchText !== this.state.searchText) {
166 this.setState({ searchText });
167 }
168 }
169
170 handleKeyDown(event) {
171 if (event.nativeEvent.keyCode === KeyCode.KEY_RETURN) {
172 this.handleSearch();
173 }
174 if (event.nativeEvent.keyCode === KeyCode.KEY_ESCAPE) {
175 this.handleClear(event);
176 }
177 }
178
179 handleSearch() {
180 this.clearSearchTimeout();
181
182 const searchText = this.state.searchText || '';
183
184 if (searchText.length >= this.props.minimumSearchTextLength && this.props.onSearch) {
185 this.props.onSearch(searchText);
186 } else if (this.props.onInvalidSearch) {
187 this.props.onInvalidSearch(searchText);
188 }
189 }
190
191 clearSearchTimeout() {
192 if (this.searchTimeout) {
193 clearTimeout(this.searchTimeout);
194 this.searchTimeout = null;
195 }
196 }
197
198 render() {
199 const {
200 defaultValue,
201 disableAutoSearch,
202 inputRefCallback,
203 inputAttributes,
204 intl,
205 isBlock,
206 isDisabled,
207 minimumSearchTextLength,
208 onChange,
209 onInvalidSearch,
210 onSearch,
211 placeholder,
212 searchDelay,
213 value,
214 ...customProps
215 } = this.props;
216
217 const searchFieldClassNames = cx([
218 'search-field',
219 { block: isBlock },
220 customProps.className,
221 ]);
222
223 const inputText = inputAttributes && Object.prototype.hasOwnProperty.call(inputAttributes, 'aria-label') ? inputAttributes['aria-label'] : intl.formatMessage({ id: 'Terra.searchField.search' });
224
225 const buttonText = intl.formatMessage({ id: 'Terra.searchField.submit-search' });
226 const clearText = intl.formatMessage({ id: 'Terra.searchField.clear' });
227 const additionalInputAttributes = { ...inputAttributes };
228 const clearIcon = <span className={cx('clear-icon')} />;
229
230 if (value !== undefined) {
231 additionalInputAttributes.value = value;
232 } else {
233 additionalInputAttributes.defaultValue = defaultValue;
234 }
235
236 const clearButton = this.state.searchText && !isDisabled
237 ? (
238 <Button
239 className={cx('clear')}
240 onClick={this.handleClear}
241 text={clearText}
242 variant="utility"
243 icon={clearIcon}
244 isIconOnly
245 />
246 )
247 : undefined;
248
249 return (
250 <div {...customProps} className={searchFieldClassNames}>
251 <div className={cx('input-group')}>
252 <Input
253 className={cx('input')}
254 type="search"
255 placeholder={placeholder}
256 onChange={this.handleTextChange}
257 disabled={isDisabled}
258 ariaLabel={inputText}
259 aria-disabled={isDisabled}
260 onKeyDown={this.handleKeyDown}
261 refCallback={this.setInputRef}
262 {...additionalInputAttributes}
263 />
264 {clearButton}
265 </div>
266 <Button
267 className={cx('button')}
268 text={buttonText}
269 onClick={this.handleSearch}
270 isDisabled={isDisabled}
271 icon={Icon}
272 isIconOnly
273 isCompact
274 />
275 </div>
276 );
277 }
278}
279
280SearchField.propTypes = propTypes;
281SearchField.defaultProps = defaultProps;
282
283export default injectIntl(SearchField);