Skip to content

Commit 03659ff

Browse files
authored
feat: SearchInput as a functional comp with forwarded ref (#1218)
* convert the SearchInput component from a class component to a functional component with forwarded reference * update tests and snapshots * add story for substring search input
1 parent 20da49f commit 03659ff

File tree

5 files changed

+471
-335
lines changed

5 files changed

+471
-335
lines changed

src/SearchInput/SearchInput.js

Lines changed: 156 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -10,228 +10,202 @@ import keycode from 'keycode';
1010
import Menu from '../Menu/Menu';
1111
import Popover from '../Popover/Popover';
1212
import PropTypes from 'prop-types';
13-
import React, { PureComponent } from 'react';
13+
import React, { useEffect, useState } from 'react';
1414

15-
class SearchInput extends PureComponent {
16-
constructor(props) {
17-
super(props);
18-
this.state = {
19-
isExpanded: false,
20-
searchExpanded: false,
21-
value: props.inputProps?.value ? props.inputProps.value : ''
22-
};
23-
}
15+
const SearchInput = React.forwardRef( ({
16+
className,
17+
compact,
18+
disabled,
19+
inputProps,
20+
inputGroupAddonProps,
21+
inputGroupProps,
22+
inShellbar,
23+
listProps,
24+
localizedText,
25+
noSearchBtn,
26+
onChange,
27+
onEnter,
28+
onSelect,
29+
placeholder,
30+
popoverProps,
31+
readOnly,
32+
searchList,
33+
subStringSearch,
34+
searchBtnProps,
35+
validationOverlayProps,
36+
validationState,
37+
...rest
38+
}, ref) => {
39+
const [isExpanded, setIsExpanded ] = useState(false);
40+
const [searchExpanded, setSearchExpanded] = useState(false);
41+
const [value, setValue] = useState(inputProps?.value || '');
2442

25-
filterList = (list, query) => {
26-
return this.props.subStringSearch ? list.filter((item) => {
43+
const filterList = (list, query) => {
44+
return subStringSearch ? list.filter((item) => {
2745
return item.text.toLowerCase().includes(query.toLowerCase());
2846
}) : list.filter((item) => item.text.toLowerCase().startsWith(query.toLowerCase()));
29-
}
47+
};
3048

31-
handleKeyPress = event => {
49+
const handleKeyPress = event => {
3250
if (keycode(event) === 'enter') {
33-
this.props.onEnter(this.state.value);
51+
onEnter(value);
3452
}
3553
};
3654

37-
handleListItemClick = (event, item) => {
38-
this.setState({
39-
value: item.text,
40-
isExpanded: false,
41-
searchExpanded: false
42-
});
55+
const handleListItemClick = (event, item) => {
56+
setValue(item.text);
57+
setIsExpanded(false);
58+
setSearchExpanded(false);
4359
item?.callback();
44-
this.props.onSelect(event, item);
60+
onSelect(event, item);
4561
};
4662

47-
handleChange = event => {
63+
const handleChange = event => {
4864
let filteredResult;
49-
if (this.props.searchList) {
50-
filteredResult = this.filterList(this.props.searchList, event.target.value);
65+
if (searchList) {
66+
filteredResult = filterList(searchList, event.target.value);
5167
}
52-
this.setState({
53-
value: event.target.value,
54-
isExpanded: true
55-
});
56-
this.props.onChange(event, filteredResult);
68+
setValue(event.target.value);
69+
setIsExpanded(true);
70+
onChange(event, filteredResult);
5771
};
5872

59-
handleClick = () => {
60-
if (!this.props.readOnly) {
61-
this.setState(prevState => ({
62-
isExpanded: !prevState.isExpanded
63-
}));
73+
const handleClick = () => {
74+
if (!readOnly) {
75+
setIsExpanded(!isExpanded);
6476
}
6577
};
6678

67-
handleClickOutside = () => {
68-
this.setState({
69-
isExpanded: false,
70-
searchExpanded: false
71-
});
72-
};
73-
74-
handleSearchBtn = () => {
75-
this.setState(prevState => ({
76-
searchExpanded: !prevState.searchExpanded
77-
}));
78-
79-
if (this.state.searchExpanded && this.state.isExpanded) {
80-
this.setState({
81-
isExpanded: false
82-
});
83-
}
79+
const handleClickOutside = () => {
80+
setIsExpanded(false);
81+
setSearchExpanded(false);
8482
};
8583

86-
handleEsc = event => {
84+
const handleEsc = event => {
8785
if (
88-
(event.keyCode === 27 && this.state.isExpanded === true) ||
89-
(event.keyCode === 27 && this.state.searchExpanded === true)
86+
(event.keyCode === 27 && isExpanded === true) ||
87+
(event.keyCode === 27 && searchExpanded === true)
9088
) {
91-
this.setState({
92-
isExpanded: false,
93-
searchExpanded: false,
94-
value: ''
95-
});
89+
setIsExpanded(false);
90+
setSearchExpanded(false);
91+
setValue('');
9692
}
9793
};
9894

99-
componentDidMount() {
100-
document.addEventListener('keydown', this.handleEsc, false);
101-
}
102-
componentWillUnmount() {
103-
document.removeEventListener('keydown', this.handleEsc, false);
104-
}
95+
useEffect(() => {
96+
document.addEventListener('keydown', handleEsc, false);
10597

106-
render() {
107-
const {
108-
placeholder,
109-
inShellbar,
110-
onEnter,
111-
searchList,
112-
subStringSearch,
113-
onChange,
114-
onSelect,
115-
noSearchBtn,
116-
compact,
117-
className,
118-
inputProps,
119-
inputGroupAddonProps,
120-
inputGroupProps,
121-
listProps,
122-
searchBtnProps,
123-
popoverProps,
124-
validationOverlayProps,
125-
validationState,
126-
disabled,
127-
readOnly,
128-
localizedText,
129-
...rest
130-
} = this.props;
98+
return () => {
99+
document.removeEventListener('keydown', handleEsc, false);
100+
};
101+
});
131102

132-
let inputGroupClasses = inputGroupProps && inputGroupProps.className;
133103

134-
inputGroupClasses = !inShellbar ? classnames(
135-
inputGroupClasses,
136-
'fd-input-group--control',
137-
{
138-
[`is-${validationState?.state}`]: validationState?.state
139-
}
140-
) : inputGroupClasses;
141-
let filteredResult = this.state.value && this.props.searchList ? this.filterList(this.props.searchList, this.state.value) : this.props.searchList;
142-
const popoverBody = (
143-
<Menu>
144-
<Menu.List {...listProps}>
145-
{filteredResult && filteredResult.length > 0 ? (
146-
filteredResult.map((item, index) => {
147-
return (
148-
subStringSearch ? (<Menu.Item
149-
key={index}
150-
onClick={(e) => this.handleListItemClick(e, item)}>
151-
{item.text}
152-
</Menu.Item>) :
153-
(
154-
<Menu.Item
155-
key={index}
156-
onClick={(e) => this.handleListItemClick(e, item)}>
157-
<strong>{this.state.value}</strong>
158-
{this.state.value && this.state.value.length
159-
? item.text.substring(this.state.value.length)
160-
: item.text}
161-
</Menu.Item>
162-
)
163-
);
164-
})
165-
) : (
166-
<Menu.Item>No result</Menu.Item>
167-
)}
168-
</Menu.List>
169-
</Menu>
170-
);
104+
let inputGroupClasses = inputGroupProps && inputGroupProps.className;
171105

172-
const inputGroup = (
173-
<InputGroup
174-
{...inputGroupProps}
175-
className={inputGroupClasses}
176-
compact={compact}
106+
inputGroupClasses = !inShellbar ? classnames(
107+
inputGroupClasses,
108+
'fd-input-group--control',
109+
{
110+
[`is-${validationState?.state}`]: validationState?.state
111+
}
112+
) : inputGroupClasses;
113+
let filteredResult = value && searchList ? filterList(searchList, value) : searchList;
114+
const popoverBody = (
115+
<Menu>
116+
<Menu.List {...listProps}>
117+
{filteredResult && filteredResult.length > 0 ? (
118+
filteredResult.map((item, index) => {
119+
return (
120+
subStringSearch ? (<Menu.Item
121+
key={index}
122+
onClick={(e) => handleListItemClick(e, item)}>
123+
{item.text}
124+
</Menu.Item>) :
125+
(
126+
<Menu.Item
127+
key={index}
128+
onClick={(e) => handleListItemClick(e, item)}>
129+
<strong>{value}</strong>
130+
{value && value.length
131+
? item.text.substring(value.length)
132+
: item.text}
133+
</Menu.Item>
134+
)
135+
);
136+
})
137+
) : (
138+
<Menu.Item>No result</Menu.Item>
139+
)}
140+
</Menu.List>
141+
</Menu>
142+
);
143+
144+
const inputGroup = (
145+
<InputGroup
146+
{...inputGroupProps}
147+
className={inputGroupClasses}
148+
compact={compact}
149+
disabled={disabled}
150+
readOnly={readOnly}
151+
validationState={validationState}>
152+
<FormInput
153+
{...inputProps}
177154
disabled={disabled}
155+
onChange={handleChange}
156+
onClick={handleClick}
157+
onKeyPress={handleKeyPress}
158+
placeholder={placeholder}
178159
readOnly={readOnly}
179-
validationState={validationState}>
180-
<FormInput
181-
{...inputProps}
182-
disabled={disabled}
183-
onChange={this.handleChange}
184-
onClick={this.handleClick}
185-
onKeyPress={this.handleKeyPress}
186-
placeholder={placeholder}
187-
readOnly={readOnly}
188-
value={this.state.value} />
160+
value={value} />
189161

190-
{ !(noSearchBtn || readOnly) && (
191-
<InputGroup.Addon {...inputGroupAddonProps} isButton>
192-
<Button {...searchBtnProps}
193-
aria-label={localizedText.searchBtnLabel}
194-
disabled={disabled}
195-
glyph='search'
196-
onClick={this.handleClick}
197-
option='transparent' />
198-
</InputGroup.Addon>
199-
)}
200-
</InputGroup>
201-
);
162+
{ !(noSearchBtn || readOnly) && (
163+
<InputGroup.Addon {...inputGroupAddonProps} isButton>
164+
<Button {...searchBtnProps}
165+
aria-label={localizedText.searchBtnLabel}
166+
disabled={disabled}
167+
glyph='search'
168+
onClick={handleClick}
169+
option='transparent' />
170+
</InputGroup.Addon>
171+
)}
172+
</InputGroup>
173+
);
202174

203-
const wrappedInputGroup = (
204-
<FormValidationOverlay
205-
{...validationOverlayProps}
206-
control={inputGroup}
207-
validationState={validationState} />
208-
);
175+
const wrappedInputGroup = (
176+
<FormValidationOverlay
177+
{...validationOverlayProps}
178+
control={inputGroup}
179+
validationState={validationState} />
180+
);
209181

210-
return (
211-
<div {...rest} className={className}>
212-
<Popover
213-
{...popoverProps}
214-
body={
215-
(<>
216-
{validationState &&
182+
return (
183+
<div
184+
{...rest}
185+
className={className}
186+
ref={ref}>
187+
<Popover
188+
{...popoverProps}
189+
body={
190+
(<>
191+
{validationState &&
217192
<FormMessage
218193
{...validationOverlayProps?.formMessageProps}
219194
type={validationState.state}>
220195
{validationState.text}
221196
</FormMessage>
222-
}
223-
{popoverBody}
224-
</>)}
225-
control={wrappedInputGroup}
226-
disableKeyPressHandler
227-
disabled={readOnly}
228-
noArrow
229-
onClickOutside={this.handleClickOutside}
230-
widthSizingType='minTarget' />
231-
</div>
232-
);
233-
}
234-
}
197+
}
198+
{popoverBody}
199+
</>)}
200+
control={wrappedInputGroup}
201+
disableKeyPressHandler
202+
disabled={readOnly}
203+
noArrow
204+
onClickOutside={handleClickOutside}
205+
widthSizingType='minTarget' />
206+
</div>
207+
);
208+
});
235209

236210
SearchInput.displayName = 'SearchInput';
237211

@@ -294,7 +268,6 @@ SearchInput.propTypes = {
294268
/** Text of the validation message */
295269
text: PropTypes.string
296270
}),
297-
/** Callback function when the change event fires on the component */
298271
/**
299272
* Callback function; triggered when a change event is fired on the underlying `<input>`.
300273
*

0 commit comments

Comments
 (0)