Skip to content

Commit 7958384

Browse files
fix(client): make search bar only highlight footer when it is selectd. (freeCodeCamp#58506)
1 parent 8961b6b commit 7958384

File tree

4 files changed

+111
-147
lines changed

4 files changed

+111
-147
lines changed

client/src/components/search/searchBar/search-bar-footer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import NoHitsSuggestion from './no-hits-suggestion';
66
interface SearchBarFooterProps {
77
hasHits: boolean;
88
query?: string;
9-
selectedIndex: number;
9+
isSelected: boolean;
1010
onMouseEnter: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
1111
onMouseLeave: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
1212
}
1313

1414
const SearchBarFooter = ({
1515
hasHits,
1616
query,
17-
selectedIndex,
17+
isSelected,
1818
onMouseEnter,
1919
onMouseLeave
2020
}: SearchBarFooterProps) => {
@@ -27,7 +27,7 @@ const SearchBarFooter = ({
2727

2828
return hasHits ? (
2929
<li
30-
className={`ais-Hits-item ${selectedIndex === 5 ? 'selected' : ''}`}
30+
className={`ais-Hits-item ${isSelected ? 'selected' : ''}`}
3131
onMouseEnter={onMouseEnter}
3232
onMouseLeave={onMouseLeave}
3333
>

client/src/components/search/searchBar/search-bar-optimized.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { useTranslation } from 'react-i18next';
33
import Magnifier from '../../../assets/icons/magnifier';
44
import InputReset from '../../../assets/icons/input-reset';
55
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
6-
import type { SearchBarProps } from './search-bar';
76

87
const SearchBarOptimized = ({
98
innerRef
10-
}: Pick<SearchBarProps, 'innerRef'>): JSX.Element => {
9+
}: {
10+
innerRef: React.RefObject<HTMLDivElement>;
11+
}): JSX.Element => {
1112
const { t } = useTranslation();
1213
// TODO: Refactor this fallback when all translation files are synced
1314
const searchPlaceholder = t('search-bar:placeholder').startsWith(

client/src/components/search/searchBar/search-bar.tsx

Lines changed: 104 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { isEqual } from 'lodash-es';
2-
import React, { Component } from 'react';
2+
import React, { useCallback, useEffect, useRef, useState } from 'react';
33
import { HotKeys, ObserveKeys } from 'react-hotkeys';
4-
import type { TFunction } from 'i18next';
5-
import { withTranslation } from 'react-i18next';
4+
import { useTranslation, withTranslation } from 'react-i18next';
65
import { SearchBox } from 'react-instantsearch';
76
import { connect } from 'react-redux';
87
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
@@ -35,82 +34,71 @@ const mapStateToProps = createSelector(
3534
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
3635
bindActionCreators({ toggleSearchDropdown, toggleSearchFocused }, dispatch);
3736

38-
export type SearchBarProps = {
37+
type SearchBarProps = {
3938
innerRef?: React.RefObject<HTMLDivElement>;
4039
toggleSearchDropdown: typeof toggleSearchDropdown;
4140
toggleSearchFocused: typeof toggleSearchFocused;
4241
isDropdownEnabled?: boolean;
4342
isSearchFocused?: boolean;
44-
t: TFunction;
4543
};
46-
type SearchBarState = {
47-
index: number;
48-
hits: Array<Hit>;
49-
};
50-
51-
export class SearchBar extends Component<SearchBarProps, SearchBarState> {
52-
static displayName: string;
53-
constructor(props: SearchBarProps) {
54-
super(props);
55-
56-
this.handleChange = this.handleChange.bind(this);
57-
this.handleSearch = this.handleSearch.bind(this);
58-
this.handleMouseEnter = this.handleMouseEnter.bind(this);
59-
this.handleMouseLeave = this.handleMouseLeave.bind(this);
60-
this.handleFocus = this.handleFocus.bind(this);
61-
this.handleHits = this.handleHits.bind(this);
62-
this.state = {
63-
index: -1,
64-
hits: []
65-
};
66-
}
67-
68-
componentDidMount(): void {
69-
const { t } = this.props;
7044

71-
document.addEventListener('click', this.handleFocus);
72-
73-
const searchInput = document.querySelector('.ais-SearchBox-input');
74-
if (searchInput) {
75-
searchInput.setAttribute('aria-label', t('search.label'));
76-
}
77-
}
78-
79-
componentWillUnmount(): void {
80-
document.removeEventListener('click', this.handleFocus);
81-
}
45+
const keyMap = {
46+
indexUp: ['up'],
47+
indexDown: ['down']
48+
};
8249

83-
handleChange = (): void => {
84-
const { isSearchFocused, toggleSearchFocused } = this.props;
50+
export function SearchBar({
51+
isDropdownEnabled,
52+
isSearchFocused,
53+
innerRef,
54+
toggleSearchDropdown,
55+
toggleSearchFocused
56+
}: SearchBarProps): JSX.Element {
57+
const { t } = useTranslation();
58+
const [index, setIndex] = useState(-1);
59+
const [hits, setHits] = useState<Array<Hit>>([]);
60+
// We need a ref because we have to get the current value of hits in handlers
61+
const hitsRef = useRef(hits);
62+
63+
const handleChange = (): void => {
8564
if (!isSearchFocused) {
8665
toggleSearchFocused(true);
8766
}
8867

89-
this.setState({
90-
index: -1
91-
});
68+
setIndex(-1);
9269
};
9370

94-
handleFocus = (e: React.FocusEvent<Node> | Event): AnyAction | void => {
95-
const { toggleSearchFocused } = this.props;
96-
const isSearchFocused = this.props.innerRef?.current?.contains(
97-
e.target as HTMLElement
98-
);
99-
if (!isSearchFocused) {
100-
// Reset if user clicks outside of
101-
// search bar / closes dropdown
102-
this.setState({ index: -1 });
71+
const handleFocus = useCallback(
72+
(e: React.FocusEvent<Node> | Event): AnyAction | void => {
73+
const isSearchFocused = innerRef?.current?.contains(
74+
e.target as HTMLElement
75+
);
76+
if (!isSearchFocused) {
77+
// Reset if user clicks outside of
78+
// search bar / closes dropdown
79+
setIndex(-1);
80+
}
81+
return toggleSearchFocused(isSearchFocused);
82+
},
83+
[innerRef, toggleSearchFocused]
84+
);
85+
86+
useEffect(() => {
87+
document.addEventListener('click', handleFocus);
88+
const searchInput = document.querySelector('.ais-SearchBox-input');
89+
if (searchInput) {
90+
searchInput.setAttribute('aria-label', t('search.label'));
10391
}
104-
return toggleSearchFocused(isSearchFocused);
105-
};
92+
return () => {
93+
document.removeEventListener('click', handleFocus);
94+
};
95+
}, [handleFocus, t]);
10696

107-
handleSearch = (
97+
const handleSearch = (
10898
e: React.SyntheticEvent<HTMLFormElement, Event>,
10999
query?: string
110100
): boolean | void => {
111101
e.preventDefault();
112-
const { toggleSearchDropdown } = this.props;
113-
const { index, hits } = this.state;
114102
const selectedHit = hits[index];
115103

116104
// Disable the search dropdown
@@ -142,108 +130,83 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
142130
: false;
143131
};
144132

145-
handleMouseEnter = (e: React.SyntheticEvent<HTMLElement, Event>): void => {
133+
const handleMouseEnter = (
134+
e: React.SyntheticEvent<HTMLElement, Event>
135+
): void => {
146136
e.persist();
147137

148-
this.setState(({ hits }) => {
138+
if (e.target instanceof HTMLElement) {
149139
const hitsTitles = hits.map(hit => hit.title);
140+
const targetText = e.target.textContent;
141+
const hoveredIndex = targetText ? hitsTitles.indexOf(targetText) : -1;
150142

151-
if (e.target instanceof HTMLElement) {
152-
const targetText = e.target.textContent;
153-
const hoveredIndex = targetText ? hitsTitles.indexOf(targetText) : -1;
154-
155-
return { index: hoveredIndex };
156-
}
157-
158-
return { index: -1 };
159-
});
143+
setIndex(hoveredIndex);
144+
}
145+
setIndex(-1);
160146
};
161147

162-
handleMouseLeave = (): void => {
163-
this.setState({
164-
index: -1
165-
});
148+
const handleMouseLeave = () => {
149+
setIndex(-1);
166150
};
167151

168-
handleHits = (currHits: Array<Hit>): void => {
169-
const { hits } = this.state;
170-
152+
const handleHits = (currHits: Array<Hit>): void => {
171153
if (!isEqual(hits, currHits)) {
172-
this.setState({
173-
index: -1,
174-
hits: currHits
175-
});
154+
setIndex(-1);
155+
hitsRef.current = currHits;
156+
setHits(currHits);
176157
}
177158
};
178159

179-
keyMap = {
180-
indexUp: ['up'],
181-
indexDown: ['down']
182-
};
183-
184-
keyHandlers = {
160+
const keyHandlers = {
185161
indexUp: (e: KeyboardEvent | undefined): void => {
186162
e?.preventDefault();
187-
this.setState(({ index, hits }) => ({
188-
index: index === -1 ? hits.length : index - 1
189-
}));
163+
setIndex(index => (index === -1 ? hitsRef.current.length : index - 1));
190164
},
191165
indexDown: (e: KeyboardEvent | undefined): void => {
192166
e?.preventDefault();
193-
this.setState(({ index, hits }) => ({
194-
index: index === hits.length ? -1 : index + 1
195-
}));
167+
setIndex(index => (index === hitsRef.current.length ? -1 : index + 1));
196168
}
197169
};
198170

199-
render(): JSX.Element {
200-
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
201-
const { index } = this.state;
202-
// TODO: Refactor this fallback when all translation files are synced
203-
const searchPlaceholder = t('search-bar:placeholder').startsWith(
204-
'search.placeholder.'
205-
)
206-
? t('search.placeholder')
207-
: t('search-bar:placeholder');
208-
209-
return (
210-
<WithInstantSearch>
211-
<div
212-
className='fcc_searchBar'
213-
data-testid='fcc_searchBar'
214-
ref={innerRef}
215-
>
216-
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
217-
<div className='fcc_search_wrapper'>
218-
<ObserveKeys except={['Space']}>
219-
<SearchBox
220-
data-playwright-test-label='header-search'
221-
onSubmit={e => {
222-
this.handleSearch(e);
223-
}}
224-
onInput={this.handleChange}
225-
translations={{
226-
submitButtonTitle: t('icons.input-search'),
227-
resetButtonTitle: t('icons.input-reset')
228-
}}
229-
placeholder={searchPlaceholder}
230-
onFocus={this.handleFocus}
231-
/>
232-
</ObserveKeys>
233-
{isDropdownEnabled && isSearchFocused && (
234-
<SearchHits
235-
handleMouseEnter={this.handleMouseEnter}
236-
handleMouseLeave={this.handleMouseLeave}
237-
handleHits={this.handleHits}
238-
selectedIndex={index}
239-
/>
240-
)}
241-
</div>
242-
</HotKeys>
243-
</div>
244-
</WithInstantSearch>
245-
);
246-
}
171+
const searchPlaceholder = t('search-bar:placeholder').startsWith(
172+
'search.placeholder.'
173+
)
174+
? t('search.placeholder')
175+
: t('search-bar:placeholder');
176+
177+
return (
178+
<WithInstantSearch>
179+
<div className='fcc_searchBar' data-testid='fcc_searchBar' ref={innerRef}>
180+
<HotKeys handlers={keyHandlers} keyMap={keyMap}>
181+
<div className='fcc_search_wrapper'>
182+
<ObserveKeys except={['Space']}>
183+
<SearchBox
184+
data-playwright-test-label='header-search'
185+
onSubmit={e => {
186+
handleSearch(e);
187+
}}
188+
onInput={handleChange}
189+
translations={{
190+
submitButtonTitle: t('icons.input-search'),
191+
resetButtonTitle: t('icons.input-reset')
192+
}}
193+
placeholder={searchPlaceholder}
194+
onFocus={handleFocus}
195+
/>
196+
</ObserveKeys>
197+
{isDropdownEnabled && isSearchFocused && (
198+
<SearchHits
199+
handleMouseEnter={handleMouseEnter}
200+
handleMouseLeave={handleMouseLeave}
201+
handleHits={handleHits}
202+
selectedIndex={index}
203+
/>
204+
)}
205+
</div>
206+
</HotKeys>
207+
</div>
208+
</WithInstantSearch>
209+
);
247210
}
248211

249212
SearchBar.displayName = 'SearchBar';

client/src/components/search/searchBar/search-hits.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function SearchHits({
5555
<SearchBarFooter
5656
hasHits={!noHits}
5757
query={results?.query}
58-
selectedIndex={selectedIndex}
58+
isSelected={selectedIndex === allHits.length}
5959
onMouseEnter={handleMouseEnter}
6060
onMouseLeave={handleMouseLeave}
6161
/>

0 commit comments

Comments
 (0)