Skip to content

Commit

Permalink
fix(client): make search bar only highlight footer when it is selectd. (
Browse files Browse the repository at this point in the history
  • Loading branch information
ojeytonwilliams authored Jan 31, 2025
1 parent 8961b6b commit 7958384
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 147 deletions.
6 changes: 3 additions & 3 deletions client/src/components/search/searchBar/search-bar-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import NoHitsSuggestion from './no-hits-suggestion';
interface SearchBarFooterProps {
hasHits: boolean;
query?: string;
selectedIndex: number;
isSelected: boolean;
onMouseEnter: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
onMouseLeave: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
}

const SearchBarFooter = ({
hasHits,
query,
selectedIndex,
isSelected,
onMouseEnter,
onMouseLeave
}: SearchBarFooterProps) => {
Expand All @@ -27,7 +27,7 @@ const SearchBarFooter = ({

return hasHits ? (
<li
className={`ais-Hits-item ${selectedIndex === 5 ? 'selected' : ''}`}
className={`ais-Hits-item ${isSelected ? 'selected' : ''}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useTranslation } from 'react-i18next';
import Magnifier from '../../../assets/icons/magnifier';
import InputReset from '../../../assets/icons/input-reset';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
import type { SearchBarProps } from './search-bar';

const SearchBarOptimized = ({
innerRef
}: Pick<SearchBarProps, 'innerRef'>): JSX.Element => {
}: {
innerRef: React.RefObject<HTMLDivElement>;
}): JSX.Element => {
const { t } = useTranslation();
// TODO: Refactor this fallback when all translation files are synced
const searchPlaceholder = t('search-bar:placeholder').startsWith(
Expand Down
245 changes: 104 additions & 141 deletions client/src/components/search/searchBar/search-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { isEqual } from 'lodash-es';
import React, { Component } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { HotKeys, ObserveKeys } from 'react-hotkeys';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { useTranslation, withTranslation } from 'react-i18next';
import { SearchBox } from 'react-instantsearch';
import { connect } from 'react-redux';
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
Expand Down Expand Up @@ -35,82 +34,71 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
bindActionCreators({ toggleSearchDropdown, toggleSearchFocused }, dispatch);

export type SearchBarProps = {
type SearchBarProps = {
innerRef?: React.RefObject<HTMLDivElement>;
toggleSearchDropdown: typeof toggleSearchDropdown;
toggleSearchFocused: typeof toggleSearchFocused;
isDropdownEnabled?: boolean;
isSearchFocused?: boolean;
t: TFunction;
};
type SearchBarState = {
index: number;
hits: Array<Hit>;
};

export class SearchBar extends Component<SearchBarProps, SearchBarState> {
static displayName: string;
constructor(props: SearchBarProps) {
super(props);

this.handleChange = this.handleChange.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleHits = this.handleHits.bind(this);
this.state = {
index: -1,
hits: []
};
}

componentDidMount(): void {
const { t } = this.props;

document.addEventListener('click', this.handleFocus);

const searchInput = document.querySelector('.ais-SearchBox-input');
if (searchInput) {
searchInput.setAttribute('aria-label', t('search.label'));
}
}

componentWillUnmount(): void {
document.removeEventListener('click', this.handleFocus);
}
const keyMap = {
indexUp: ['up'],
indexDown: ['down']
};

handleChange = (): void => {
const { isSearchFocused, toggleSearchFocused } = this.props;
export function SearchBar({
isDropdownEnabled,
isSearchFocused,
innerRef,
toggleSearchDropdown,
toggleSearchFocused
}: SearchBarProps): JSX.Element {
const { t } = useTranslation();
const [index, setIndex] = useState(-1);
const [hits, setHits] = useState<Array<Hit>>([]);
// We need a ref because we have to get the current value of hits in handlers
const hitsRef = useRef(hits);

const handleChange = (): void => {
if (!isSearchFocused) {
toggleSearchFocused(true);
}

this.setState({
index: -1
});
setIndex(-1);
};

handleFocus = (e: React.FocusEvent<Node> | Event): AnyAction | void => {
const { toggleSearchFocused } = this.props;
const isSearchFocused = this.props.innerRef?.current?.contains(
e.target as HTMLElement
);
if (!isSearchFocused) {
// Reset if user clicks outside of
// search bar / closes dropdown
this.setState({ index: -1 });
const handleFocus = useCallback(
(e: React.FocusEvent<Node> | Event): AnyAction | void => {
const isSearchFocused = innerRef?.current?.contains(
e.target as HTMLElement
);
if (!isSearchFocused) {
// Reset if user clicks outside of
// search bar / closes dropdown
setIndex(-1);
}
return toggleSearchFocused(isSearchFocused);
},
[innerRef, toggleSearchFocused]
);

useEffect(() => {
document.addEventListener('click', handleFocus);
const searchInput = document.querySelector('.ais-SearchBox-input');
if (searchInput) {
searchInput.setAttribute('aria-label', t('search.label'));
}
return toggleSearchFocused(isSearchFocused);
};
return () => {
document.removeEventListener('click', handleFocus);
};
}, [handleFocus, t]);

handleSearch = (
const handleSearch = (
e: React.SyntheticEvent<HTMLFormElement, Event>,
query?: string
): boolean | void => {
e.preventDefault();
const { toggleSearchDropdown } = this.props;
const { index, hits } = this.state;
const selectedHit = hits[index];

// Disable the search dropdown
Expand Down Expand Up @@ -142,108 +130,83 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
: false;
};

handleMouseEnter = (e: React.SyntheticEvent<HTMLElement, Event>): void => {
const handleMouseEnter = (
e: React.SyntheticEvent<HTMLElement, Event>
): void => {
e.persist();

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

if (e.target instanceof HTMLElement) {
const targetText = e.target.textContent;
const hoveredIndex = targetText ? hitsTitles.indexOf(targetText) : -1;

return { index: hoveredIndex };
}

return { index: -1 };
});
setIndex(hoveredIndex);
}
setIndex(-1);
};

handleMouseLeave = (): void => {
this.setState({
index: -1
});
const handleMouseLeave = () => {
setIndex(-1);
};

handleHits = (currHits: Array<Hit>): void => {
const { hits } = this.state;

const handleHits = (currHits: Array<Hit>): void => {
if (!isEqual(hits, currHits)) {
this.setState({
index: -1,
hits: currHits
});
setIndex(-1);
hitsRef.current = currHits;
setHits(currHits);
}
};

keyMap = {
indexUp: ['up'],
indexDown: ['down']
};

keyHandlers = {
const keyHandlers = {
indexUp: (e: KeyboardEvent | undefined): void => {
e?.preventDefault();
this.setState(({ index, hits }) => ({
index: index === -1 ? hits.length : index - 1
}));
setIndex(index => (index === -1 ? hitsRef.current.length : index - 1));
},
indexDown: (e: KeyboardEvent | undefined): void => {
e?.preventDefault();
this.setState(({ index, hits }) => ({
index: index === hits.length ? -1 : index + 1
}));
setIndex(index => (index === hitsRef.current.length ? -1 : index + 1));
}
};

render(): JSX.Element {
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
const { index } = this.state;
// TODO: Refactor this fallback when all translation files are synced
const searchPlaceholder = t('search-bar:placeholder').startsWith(
'search.placeholder.'
)
? t('search.placeholder')
: t('search-bar:placeholder');

return (
<WithInstantSearch>
<div
className='fcc_searchBar'
data-testid='fcc_searchBar'
ref={innerRef}
>
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
<div className='fcc_search_wrapper'>
<ObserveKeys except={['Space']}>
<SearchBox
data-playwright-test-label='header-search'
onSubmit={e => {
this.handleSearch(e);
}}
onInput={this.handleChange}
translations={{
submitButtonTitle: t('icons.input-search'),
resetButtonTitle: t('icons.input-reset')
}}
placeholder={searchPlaceholder}
onFocus={this.handleFocus}
/>
</ObserveKeys>
{isDropdownEnabled && isSearchFocused && (
<SearchHits
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
handleHits={this.handleHits}
selectedIndex={index}
/>
)}
</div>
</HotKeys>
</div>
</WithInstantSearch>
);
}
const searchPlaceholder = t('search-bar:placeholder').startsWith(
'search.placeholder.'
)
? t('search.placeholder')
: t('search-bar:placeholder');

return (
<WithInstantSearch>
<div className='fcc_searchBar' data-testid='fcc_searchBar' ref={innerRef}>
<HotKeys handlers={keyHandlers} keyMap={keyMap}>
<div className='fcc_search_wrapper'>
<ObserveKeys except={['Space']}>
<SearchBox
data-playwright-test-label='header-search'
onSubmit={e => {
handleSearch(e);
}}
onInput={handleChange}
translations={{
submitButtonTitle: t('icons.input-search'),
resetButtonTitle: t('icons.input-reset')
}}
placeholder={searchPlaceholder}
onFocus={handleFocus}
/>
</ObserveKeys>
{isDropdownEnabled && isSearchFocused && (
<SearchHits
handleMouseEnter={handleMouseEnter}
handleMouseLeave={handleMouseLeave}
handleHits={handleHits}
selectedIndex={index}
/>
)}
</div>
</HotKeys>
</div>
</WithInstantSearch>
);
}

SearchBar.displayName = 'SearchBar';
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/search/searchBar/search-hits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function SearchHits({
<SearchBarFooter
hasHits={!noHits}
query={results?.query}
selectedIndex={selectedIndex}
isSelected={selectedIndex === allHits.length}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
Expand Down

0 comments on commit 7958384

Please sign in to comment.