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 * as KeyCode from 'keycode-js';
|
6 | import IconSearch from 'terra-icon/lib/icon/IconSearch';
|
7 | import Input from 'terra-form-input';
|
8 | import { injectIntl, intlShape } from 'react-intl';
|
9 | import styles from './SearchField.module.scss';
|
10 |
|
11 | const cx = classNames.bind(styles);
|
12 |
|
13 | const Icon = <IconSearch />;
|
14 |
|
15 | const 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 |
|
90 | const 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 |
|
102 | class 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 |
|
280 | SearchField.propTypes = propTypes;
|
281 | SearchField.defaultProps = defaultProps;
|
282 |
|
283 | export default injectIntl(SearchField);
|