Skip to content

Commit bad3b8e

Browse files
committed
feat(search): adding initial masthead search components (wip)
1 parent cbdbcea commit bad3b8e

13 files changed

+387
-1
lines changed
1.96 KB
Binary file not shown.
41.5 KB
Binary file not shown.
Binary file not shown.
17.5 KB
Binary file not shown.
2.19 KB
Binary file not shown.

packages/react/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"promise": "^8.0.1",
110110
"prop-types": "^15.7.2",
111111
"react": "^16.8.6",
112+
"react-autosuggest": "^9.4.3",
112113
"react-dom": "^16.8.6",
113114
"react-test-renderer": "^16.8.6",
114115
"requestanimationframe": "^0.0.23",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2018
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React, { useReducer } from 'react';
9+
import PropTypes from 'prop-types';
10+
import { SearchTypeaheadAPI } from '@ibmdotcom/services';
11+
import { escapeRegExp } from '@ibmdotcom/utilities';
12+
import { MastheadSearchInput, MastheadSearchSuggestion } from './';
13+
14+
15+
/**
16+
* Converts the string to lower case and trims extra white space
17+
*
18+
* @param {string} valueString The text field
19+
* @returns {string} lower cased and trimmed text
20+
*/
21+
const _trimAndLower = valueString => valueString.toLowerCase().trim();
22+
23+
/**
24+
* When a suggestion item is clicked, we populate the input with its name field
25+
*
26+
* @param {object} suggestion The individual object from the data
27+
* @returns {*} The name val
28+
*/
29+
const _getSuggestionValue = suggestion => suggestion.name;
30+
31+
/**
32+
* Initial state of the autocomplete component
33+
*
34+
* @type {{val: string, prevSuggestions: Array, suggestions: Array, suggestionContainerVisible: boolean}}
35+
* @private
36+
*/
37+
const _initialState = {
38+
val: '',
39+
suggestions: [],
40+
prevSuggestions: [],
41+
suggestionContainerVisible: false,
42+
};
43+
44+
/**
45+
* Reducer for the useReducer hook
46+
*
47+
* @param {object} state The state
48+
* @param {object} action contains the type and payload
49+
* @returns {*} the new state value
50+
* @private
51+
*/
52+
function _reducer(state, action) {
53+
switch (action.type) {
54+
case 'setVal':
55+
return Object.assign({}, state, {val: action.payload.val});
56+
case 'emptySuggestions':
57+
return Object.assign({}, state, {suggestions: []});
58+
case 'setPrevSuggestions':
59+
return Object.assign({}, state, {prevSuggestions: action.payload.prevSuggestions});
60+
case 'setSuggestionsToPrevious':
61+
return Object.assign({}, state, {suggestions: state.prevSuggestions});
62+
case 'showSuggestionsContainer':
63+
return Object.assign({}, state, {suggestionContainerVisible: true});
64+
case 'hideSuggestionsContainer':
65+
return Object.assign({}, state, {suggestionContainerVisible: false});
66+
default:
67+
return state;
68+
}
69+
}
70+
71+
/**
72+
* MastheadSearch component which includes autosuggestion results from the
73+
* SearchTypeaheadAPI
74+
*
75+
* The search field utilizes "react-autosuggest". Documentation available here:
76+
* http://react-autosuggest.js.org/
77+
* https://github.com/moroshko/react-autosuggest
78+
*
79+
* @param {string} placeHolderText Placeholder text for the search field
80+
* @param {number} renderValue Number of characters to begin showing suggestions
81+
* @param {Function} getSuggestionValue Function for getting the suggestion value
82+
* @class
83+
*/
84+
const MastheadSearch = ({
85+
placeHolderText,
86+
renderValue,
87+
}) => {
88+
const [state, dispatch] = useReducer(_reducer, _initialState);
89+
90+
/**
91+
* When the input field changes, we set the new val to our state
92+
*
93+
* @param {event} event The callback event
94+
* @param {string} newValue The new val of the input
95+
*/
96+
function onChange(event, { newValue }) {
97+
dispatch({type: 'setVal', payload: {val: newValue}});
98+
}
99+
100+
/**
101+
* Autosuggest will pass through all these props to the input.
102+
*
103+
* @type {{onBlur: onBlur, onChange: onChange, placeholder: *, value: *, onFocus: onFocus}}
104+
*/
105+
const inputProps = {
106+
placeholder: placeHolderText,
107+
value: state.val,
108+
onChange,
109+
onFocus: (e) => { e.target.placeholder = ''; },
110+
onBlur: (e) => { e.target.placeholder = placeHolderText; },
111+
};
112+
113+
/**
114+
* Renders the input bar with the search icon
115+
*
116+
* @param {object} componentInputProps contains the input props
117+
* @returns {*} The rendered component
118+
*/
119+
function renderInputComponent(componentInputProps) {
120+
return (
121+
<MastheadSearchInput
122+
componentInputProps={componentInputProps}
123+
dispatch={dispatch}
124+
/>
125+
);
126+
}
127+
128+
/**
129+
* Renders the Suggestion Value with the function for the adding the suggestion
130+
*
131+
* @param {object} suggestion The suggestion
132+
* @param {string} query The query being searched for
133+
* @param {boolean} isHighlighted Whether the suggestion is currently highlighted by the user
134+
* @returns {*} The suggestion value
135+
*/
136+
function renderSuggestionValue(suggestion, {query, isHighlighted}) {
137+
return (
138+
<MastheadSearchSuggestion
139+
suggestion={suggestion}
140+
query={query}
141+
isHighlighted={isHighlighted}
142+
getSuggestionValue={_getSuggestionValue}
143+
/>
144+
);
145+
}
146+
147+
/**
148+
* This function is called everytime we need new suggestions. If input has
149+
* changed, we fetch for new suggestions else we return the previous
150+
* suggestions
151+
*
152+
* Available reason values:
153+
* https://github.com/moroshko/react-autosuggest#onsuggestionsfetchrequested-required
154+
*
155+
* @param {object} request Object response from when onSuggestionsFetchRequested is called
156+
* @param {string} request.value the current value of the input
157+
* @param {string} request.reason string describing why onSuggestionsFetchRequested was called
158+
*/
159+
function onSuggestionsFetchRequest(request) {
160+
const searchValue = _trimAndLower(escapeRegExp(request.value));
161+
162+
if (request.reason === 'input-changed') { // if the search input has changed
163+
SearchTypeaheadAPI.getResults(searchValue).then((response) => {
164+
dispatch({type: 'setPrevSuggestions', payload: {prevSuggestions: response}});
165+
dispatch({type: 'setSuggestionsToPrevious'});
166+
dispatch({type: 'showSuggestionsContainer'});
167+
});
168+
} else {
169+
dispatch({type: 'setSuggestionsToPrevious'});
170+
dispatch({type: 'showSuggestionsContainer'});
171+
}
172+
}
173+
174+
/**
175+
* Called every time we clear suggestions
176+
*/
177+
function onSuggestionsClearedRequested() {
178+
dispatch({type: 'emptySuggestions'});
179+
dispatch({type: 'hideSuggestionsContainer'});
180+
}
181+
182+
/**
183+
* Only render suggestions if we have more than the renderValue
184+
*
185+
* @param {string} value Name of the suggestion
186+
* @returns {boolean} Whether or not to display the value
187+
*/
188+
function shouldRenderSuggestions(value) {
189+
return value.trim().length >= renderValue;
190+
}
191+
192+
return (
193+
<div
194+
data-autoid="masthead__search"
195+
>
196+
<Autosuggest
197+
suggestions={state.suggestions} // The state value of suggestion
198+
onSuggestionsFetchRequested={onSuggestionsFetchRequest} // Method to fetch data (should be async call)
199+
onSuggestionsClearRequested={onSuggestionsClearedRequested} // When input bar loses focus
200+
getSuggestionValue={_getSuggestionValue} // Name of suggestion
201+
renderSuggestion={renderSuggestionValue} // How to display a suggestion
202+
onSuggestionSelected={null} // When a suggestion is selected
203+
highlightFirstSuggestion // First suggestion is highlighted by default
204+
inputProps={inputProps}
205+
renderInputComponent={renderInputComponent}
206+
shouldRenderSuggestions={shouldRenderSuggestions}
207+
/>
208+
</div>
209+
);
210+
};
211+
212+
/**
213+
* @property propTypes
214+
* @description Defined property types for component
215+
* @type {{items: *}}
216+
*/
217+
MastheadSearch.propTypes = {
218+
placeHolderText: PropTypes.string,
219+
renderValue: PropTypes.number,
220+
};
221+
222+
/**
223+
*
224+
* @type {{setSuggestionValue: (function(*=): {name: *}), placeHolderText: string, renderValue: number}}
225+
*/
226+
MastheadSearch.defaultProps = {
227+
placeHolderText: '',
228+
renderValue: 3,
229+
};
230+
231+
// Export the react component
232+
export default MastheadSearch;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
4+
/**
5+
* Renders the input bar with the search icon
6+
*
7+
* @param {object} componentInputProps contains the input props
8+
* @param {Function} dispatch for component reducer
9+
* @returns {*} The rendered component
10+
*/
11+
export const MastheadSearchInput = (
12+
componentInputProps,
13+
dispatch
14+
) => (
15+
<div>
16+
<input
17+
{...componentInputProps}
18+
/*className={styles.input}*/
19+
/>
20+
<button
21+
type="button"
22+
className={styles.closeButton}
23+
onClick={() => dispatch({type: 'setVal', payload: {val: ''}})}
24+
>
25+
<CloseButtonIcon className={styles.closeIcon}/>
26+
</button>
27+
</div>
28+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import parse from 'autosuggest-highlight/parse';
4+
5+
/**
6+
* Matches a suggestion name with the query
7+
*
8+
* @param {*}regexp The regex expression containg the query and the global match flag
9+
* @param {string} haystack The suggestion
10+
* @returns {Array} Array of matches
11+
* @private
12+
*/
13+
function _matchAll(regexp, haystack) {
14+
const matches = [];
15+
let match = regexp.exec(haystack);
16+
while (match) {
17+
matches.push([
18+
match.index,
19+
match.index + match[0].length,
20+
]);
21+
match = regexp.exec(haystack);
22+
}
23+
return matches;
24+
}
25+
26+
/**
27+
* The rendered suggestion in the suggestion list
28+
*
29+
* @param {object} suggestion The individual object from the data
30+
* @param {string} query The query being searched for
31+
* @param {boolean} isHighlighted Whether the suggestion is currently highlighted by the user
32+
* @param {Function} getSuggestionValue Gets the suggestion value
33+
* @returns {*} The individual suggested item with styles
34+
* @class
35+
*/
36+
export const MastheadSearchSuggestion = (
37+
suggestion,
38+
query,
39+
isHighlighted,
40+
getSuggestionValue,
41+
) => {
42+
const suggestionValue = getSuggestionValue(suggestion);
43+
const matches = _matchAll(new RegExp(query, 'gi'), suggestionValue);
44+
const parts = parse(suggestionValue, matches);
45+
46+
return (
47+
<div
48+
className={classNames(
49+
styles.suggestion,
50+
{[styles.highlightedContainer]: isHighlighted},
51+
)}
52+
>
53+
<div className={styles.suggestionText}>
54+
{
55+
parts.map((part, index) => (
56+
<span
57+
className={classNames({[styles.highlightText]: part.highlight})}
58+
key={index}
59+
>
60+
{part.text}
61+
</span>
62+
))
63+
}
64+
</div>
65+
</div>
66+
);
67+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @module Utilities/escaperegexp
3+
*/
4+
5+
/**
6+
* Utiltity function for escaping regex expressions
7+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
8+
*
9+
* @param {string} str String to escape regex
10+
* @returns {*} Final string with escaped regex
11+
*/
12+
export function escapeRegExp(str) {
13+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2018
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export * from './escaperegexp';

packages/utilities/src/utilities/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
export * from './escaperegexp';
89
export * from './serialize';

0 commit comments

Comments
 (0)