UNPKG

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