Skip to content

Commit 4ba426f

Browse files
committed
refactor: refactor search component to instead use react autosuggest library instead of react-autocomplete; new library has much more consistent behavior + better community support
1 parent f3e5467 commit 4ba426f

File tree

3 files changed

+117
-83
lines changed

3 files changed

+117
-83
lines changed

packages/uikit-workshop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"@reach/visually-hidden": "^0.1.1",
7373
"react-html-parser": "^2.0.2",
7474
"fuse.js": "^3.2.1",
75-
"react-autocomplete": "^1.8.1",
75+
"react-autosuggest": "^9.4.2",
7676
"classnames": "^2.2.6",
7777
"react-animate-height": "^2.0.4",
7878
"@skatejs/renderer-preact": "^0.3.3",

packages/uikit-workshop/src/scripts/components/pl-search/pl-search.js

Lines changed: 109 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import classNames from 'classnames';
66
import Mousetrap from 'mousetrap';
77

88
import VisuallyHidden from '@reach/visually-hidden';
9-
import ReactAutocomplete from 'react-autocomplete';
9+
import Autosuggest from 'react-autosuggest';
1010

1111
import { urlHandler } from '../../utils';
1212
import { BaseComponent } from '../base-component';
@@ -20,26 +20,32 @@ class Search extends BaseComponent {
2020
this.useShadow = false;
2121
this.defaultMaxResults = 10;
2222

23+
// Autosuggest is a controlled component.
24+
// This means that you need to provide an input value
25+
// and an onChange handler that updates this value (see below).
26+
// Suggestions also need to be provided to the Autosuggest,
27+
// and they are initially empty because the Autosuggest is closed.
2328
this.state = {
24-
isOpen: false,
2529
value: '',
30+
suggestions: [],
2631
};
2732

2833
this.receiveIframeMessage = this.receiveIframeMessage.bind(this);
34+
this.onChange = this.onChange.bind(this);
2935
this.toggleSearch = this.toggleSearch.bind(this);
30-
this.clearSearch = this.clearSearch.bind(this);
36+
// this.clearSearch = this.clearSearch.bind(this);
3137
this.closeSearch = this.closeSearch.bind(this);
3238
this.openSearch = this.openSearch.bind(this);
3339

34-
this.data = [];
40+
this.items = [];
3541
for (const patternType in window.patternPaths) {
3642
if (window.patternPaths.hasOwnProperty(patternType)) {
3743
for (const pattern in window.patternPaths[patternType]) {
3844
if (window.patternPaths[patternType].hasOwnProperty(pattern)) {
3945
const obj = {};
4046
obj.label = patternType + '-' + pattern;
4147
obj.id = window.patternPaths[patternType][pattern];
42-
this.data.push(obj);
48+
this.items.push(obj);
4349
}
4450
}
4551
}
@@ -58,7 +64,7 @@ class Search extends BaseComponent {
5864
}
5965

6066
rendered() {
61-
this.inputElement = this.input;
67+
this.inputElement = this.querySelector('.js-c-typeahead__input');
6268
}
6369

6470
static props = {
@@ -71,19 +77,6 @@ class Search extends BaseComponent {
7177
// External Redux store not yet in use
7278
_stateChanged(state) {}
7379

74-
// update the iframe via the history api handler
75-
passPath(item) {
76-
this.setState({ value: item });
77-
this.closeSearch();
78-
const obj = JSON.stringify({
79-
event: 'patternLab.updatePath',
80-
path: urlHandler.getFileName(item),
81-
});
82-
document
83-
.querySelector('.pl-js-iframe')
84-
.contentWindow.postMessage(obj, urlHandler.targetOrigin);
85-
}
86-
8780
toggleSearch() {
8881
if (!this.state.isOpen) {
8982
this.openSearch();
@@ -131,10 +124,14 @@ class Search extends BaseComponent {
131124
}
132125
}
133126

134-
// highlights keywords in the search results in a react-friendly way + limits total number / max displayed
135-
filterAndLimitResults() {
136-
const data = this.data;
127+
getSuggestionValue = suggestion => suggestion.label;
137128

129+
renderSuggestion(item, { query, isHighlighted }) {
130+
return <span>{item.highlightedLabel}</span>;
131+
}
132+
133+
// highlights keywords in the search results in a react-friendly way + limits total number / max displayed
134+
getSuggestions(value) {
138135
const maxResults = this.props.maxResults
139136
? this.props.maxResults
140137
: this.defaultMaxResults;
@@ -150,8 +147,8 @@ class Search extends BaseComponent {
150147
minMatchCharLength: 1,
151148
keys: ['label'],
152149
};
153-
const fuse = new Fuse(data, fuseOptions);
154-
const results = fuse.search(this.state.value ? this.state.value : '');
150+
const fuse = new Fuse(this.items, fuseOptions);
151+
const results = fuse.search(value);
155152

156153
const highlighter = function(item) {
157154
const resultItem = item;
@@ -202,9 +199,51 @@ class Search extends BaseComponent {
202199
}
203200
}
204201

202+
// Autosuggest calls this when a search result is selected
203+
onChange = (event, { newValue }) => {
204+
const patternName = urlHandler.getFileName(newValue);
205+
206+
if (patternName) {
207+
const obj = JSON.stringify({
208+
event: 'patternLab.updatePath',
209+
path: patternName,
210+
});
211+
212+
document
213+
.querySelector('.pl-js-iframe')
214+
.contentWindow.postMessage(obj, urlHandler.targetOrigin);
215+
}
216+
217+
this.setState({
218+
value: newValue,
219+
});
220+
};
221+
222+
// Autosuggest calls this every time you need to update suggestions.
223+
onSuggestionsFetchRequested = ({ value }) => {
224+
this.setState({ isOpen: true });
225+
226+
this.setState({
227+
suggestions: this.getSuggestions(value),
228+
});
229+
};
230+
231+
// Autosuggest calls this every time you need to clear suggestions.
232+
onSuggestionsClearRequested = () => {
233+
this.setState({
234+
suggestions: [],
235+
});
236+
237+
this.setState({ isOpen: false });
238+
};
239+
240+
onSuggestionSelected() {
241+
this.setState({ isOpen: false });
242+
}
243+
205244
render() {
206-
const open = this.state.isOpen;
207-
const currentValue = this.state.value;
245+
const { value, suggestions } = this.state;
246+
208247
const shouldShowClearButton = this.props.showClearButton
209248
? this.props.showClearButton
210249
: true;
@@ -213,61 +252,54 @@ class Search extends BaseComponent {
213252
? this.props.clearButtonText
214253
: 'Clear Search Results';
215254

255+
// no CSS for these Autosuggest selectors yet -- not yet needed
256+
const theme = {
257+
container: classNames('pl-c-typeahead'),
258+
containerOpen: classNames('pl-c-typeahead--open'),
259+
input: classNames('pl-c-typeahead__input', 'js-c-typeahead__input', {
260+
[`pl-c-typeahead__input--with-clear-button`]: shouldShowClearButton,
261+
}),
262+
inputOpen: classNames('pl-c-typeahead__input--open'),
263+
inputFocused: classNames('pl-c-typeahead__input--focused'),
264+
suggestionsContainer: classNames('pl-c-typeahead__menu'),
265+
suggestionsContainerOpen: classNames('pl-is-open'),
266+
suggestionsList: classNames('pl-c-typeahead__results'),
267+
suggestion: classNames('pl-c-typeahead__result'),
268+
suggestionFirst: classNames('pl-c-typeahead__result--first'),
269+
suggestionHighlighted: classNames('pl-has-cursor'),
270+
sectionContainer: classNames(
271+
'pl-c-typeahead__section-container'
272+
) /* [1] */,
273+
sectionContainerFirst: classNames(
274+
'pl-c-typeahead__section-container--first'
275+
) /* [1] */,
276+
sectionTitle: classNames('pl-c-typeahead__section-title') /* [1] */,
277+
};
278+
279+
// Autosuggest will pass through all these props to the input.
280+
const inputProps = {
281+
placeholder: this.props.placeholder
282+
? this.props.placeholder
283+
: 'Find a Pattern',
284+
value,
285+
onChange: this.onChange,
286+
};
287+
216288
return (
217-
<div className={'pl-c-typeahead-wrapper'}>
218-
<ReactAutocomplete
219-
items={this.filterAndLimitResults()}
220-
ref={el => (this.input = el)}
221-
wrapperProps={{
222-
className: classNames('pl-c-typeahead'),
223-
}}
224-
//setting autoHighlight to false seems to help prevent an occasional JS error from firing (relating to the dom-scroll-into-view library)
225-
autoHighlight={false}
226-
onMenuVisibilityChange={isOpen => this.setState({ isOpen })}
227-
getItemValue={item => item.label}
228-
renderItem={(item, highlighted) => (
229-
<div
230-
className={classNames('pl-c-typeahead__result', {
231-
[`pl-has-cursor`]: highlighted,
232-
})}
233-
key={item.id}
234-
>
235-
{item.highlightedLabel}
236-
</div>
237-
)}
238-
inputProps={{
239-
className: classNames('pl-c-typeahead__input', {
240-
[`pl-c-typeahead__input--with-clear-button`]: shouldShowClearButton,
241-
}),
242-
placeholder: this.props.placeholder
243-
? this.props.placeholder
244-
: 'Find a Pattern',
245-
}}
246-
renderMenu={(items, value, style) => (
247-
<div
248-
className={classNames('pl-c-typeahead__menu', {
249-
[`pl-is-open`]: open,
250-
})}
251-
>
252-
<div
253-
className={'pl-c-typeahead__results'}
254-
style={{ ...style, ...this.menuStyle }}
255-
children={items}
256-
/>
257-
</div>
258-
)}
259-
value={this.state.value}
260-
onChange={e => {
261-
e.target.value !== '' && e.target.value !== undefined
262-
? this.setState({ value: e.target.value })
263-
: this.setState({ value: '' });
264-
}}
265-
onSelect={value => this.passPath(value)}
289+
<div className={classNames('pl-c-typeahead-wrapper')}>
290+
<Autosuggest
291+
theme={theme}
292+
suggestions={suggestions}
293+
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
294+
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
295+
getSuggestionValue={this.getSuggestionValue}
296+
renderSuggestion={this.renderSuggestion}
297+
inputProps={inputProps}
266298
/>
267299
{shouldShowClearButton && (
268300
<button
269301
className={classNames('pl-c-typeahead__clear-button', {
270-
[`pl-is-visible`]: currentValue !== '',
302+
[`pl-is-visible`]: value !== '',
271303
})}
272304
onClick={() => {
273305
this.clearSearch();
@@ -280,7 +312,7 @@ class Search extends BaseComponent {
280312
width="16"
281313
className={'pl-c-typeahead__clear-button-icon'}
282314
>
283-
<title>Clear Search Results</title>
315+
<title>{clearButtonText}</title>
284316
<path d="M12.207 10.793l-1.414 1.414-2.793-2.793-2.793 2.793-1.414-1.414 2.793-2.793-2.793-2.793 1.414-1.414 2.793 2.793 2.793-2.793 1.414 1.414-2.793 2.793 2.793 2.793z" />
285317
</svg>
286318
</button>

packages/uikit-workshop/src/scripts/components/pl-search/pl-search.scss

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pl-search {
2020
padding: 0.3rem 0.5rem;
2121
display: inline-block;
2222

23-
@media screen and (min-width: $pl-bp-med){
23+
@media screen and (min-width: $pl-bp-med) {
2424
margin-left: 1rem;
2525
flex-direction: row;
2626
flex-shrink: 1;
@@ -53,7 +53,7 @@ pl-search {
5353
color: $pl-color-gray-87;
5454
}
5555

56-
@media screen and (min-width: $pl-bp-med){
56+
@media screen and (min-width: $pl-bp-med) {
5757
flex-direction: row;
5858

5959
.pl-c-body--theme-sidebar & {
@@ -82,7 +82,7 @@ pl-search {
8282
transition: all 0.1s ease;
8383
max-width: 100%;
8484
padding: 0.31rem 0.5rem;
85-
font-size: 16px; // prevent zooming in on mobile
85+
font-size: 16px; // prevent zooming in on mobile
8686

8787
.pl-c-body--theme-sidebar & {
8888
border-radius: 0;
@@ -118,7 +118,6 @@ pl-search {
118118
transition: all 0.1s ease;
119119
}
120120

121-
122121
&:hover,
123122
&:focus {
124123
color: $pl-color-white;
@@ -162,7 +161,7 @@ pl-search {
162161
background-color: $pl-color-gray-02;
163162
}
164163

165-
&.pl-is-open:not(.pl-is-empty) {
164+
&.pl-is-open {
166165
max-height: 120rem;
167166
max-height: calc(var(--viewport-height) - 4rem);
168167
opacity: 1;
@@ -181,6 +180,9 @@ pl-search {
181180
}
182181

183182
.pl-c-typeahead__results {
183+
list-style: none;
184+
margin: 0;
185+
padding: 0;
184186
background-color: inherit;
185187
border-color: transparent;
186188
border-width: 1px;

0 commit comments

Comments
 (0)