1 | import React from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import classNames from 'classnames/bind';
|
4 | import Button from 'terra-button';
|
5 | import KeyCode from 'keycode-js';
|
6 | import IconSearch from 'terra-icon/lib/icon/IconSearch';
|
7 | import Input from 'terra-form-input';
|
8 | import styles from './SearchField.module.scss';
|
9 |
|
10 | const cx = classNames.bind(styles);
|
11 |
|
12 | const Icon = <IconSearch />;
|
13 |
|
14 | const 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 |
|
82 | const 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 |
|
94 | const 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 |
|
103 | class 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 |
|
278 | SearchField.propTypes = propTypes;
|
279 | SearchField.defaultProps = defaultProps;
|
280 | SearchField.contextTypes = contextTypes;
|
281 |
|
282 | export default SearchField;
|