From 795838496b91b7bf78a35e7005f1aa8bdd272dda Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 31 Jan 2025 11:25:19 +0100 Subject: [PATCH] fix(client): make search bar only highlight footer when it is selectd. (#58506) --- .../search/searchBar/search-bar-footer.tsx | 6 +- .../search/searchBar/search-bar-optimized.tsx | 5 +- .../search/searchBar/search-bar.tsx | 245 ++++++++---------- .../search/searchBar/search-hits.tsx | 2 +- 4 files changed, 111 insertions(+), 147 deletions(-) diff --git a/client/src/components/search/searchBar/search-bar-footer.tsx b/client/src/components/search/searchBar/search-bar-footer.tsx index 25e34f21536e1e..190c663564ac9d 100644 --- a/client/src/components/search/searchBar/search-bar-footer.tsx +++ b/client/src/components/search/searchBar/search-bar-footer.tsx @@ -6,7 +6,7 @@ import NoHitsSuggestion from './no-hits-suggestion'; interface SearchBarFooterProps { hasHits: boolean; query?: string; - selectedIndex: number; + isSelected: boolean; onMouseEnter: (e: React.SyntheticEvent) => void; onMouseLeave: (e: React.SyntheticEvent) => void; } @@ -14,7 +14,7 @@ interface SearchBarFooterProps { const SearchBarFooter = ({ hasHits, query, - selectedIndex, + isSelected, onMouseEnter, onMouseLeave }: SearchBarFooterProps) => { @@ -27,7 +27,7 @@ const SearchBarFooter = ({ return hasHits ? (
  • diff --git a/client/src/components/search/searchBar/search-bar-optimized.tsx b/client/src/components/search/searchBar/search-bar-optimized.tsx index 5b2db0f5de9eda..83161e695604a2 100644 --- a/client/src/components/search/searchBar/search-bar-optimized.tsx +++ b/client/src/components/search/searchBar/search-bar-optimized.tsx @@ -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): JSX.Element => { +}: { + innerRef: React.RefObject; +}): JSX.Element => { const { t } = useTranslation(); // TODO: Refactor this fallback when all translation files are synced const searchPlaceholder = t('search-bar:placeholder').startsWith( diff --git a/client/src/components/search/searchBar/search-bar.tsx b/client/src/components/search/searchBar/search-bar.tsx index 7d92f9801b1e7d..f973ceb84c147f 100644 --- a/client/src/components/search/searchBar/search-bar.tsx +++ b/client/src/components/search/searchBar/search-bar.tsx @@ -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'; @@ -35,82 +34,71 @@ const mapStateToProps = createSelector( const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({ toggleSearchDropdown, toggleSearchFocused }, dispatch); -export type SearchBarProps = { +type SearchBarProps = { innerRef?: React.RefObject; toggleSearchDropdown: typeof toggleSearchDropdown; toggleSearchFocused: typeof toggleSearchFocused; isDropdownEnabled?: boolean; isSearchFocused?: boolean; - t: TFunction; }; -type SearchBarState = { - index: number; - hits: Array; -}; - -export class SearchBar extends Component { - 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>([]); + // 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 | 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 | 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, query?: string ): boolean | void => { e.preventDefault(); - const { toggleSearchDropdown } = this.props; - const { index, hits } = this.state; const selectedHit = hits[index]; // Disable the search dropdown @@ -142,108 +130,83 @@ export class SearchBar extends Component { : false; }; - handleMouseEnter = (e: React.SyntheticEvent): void => { + const handleMouseEnter = ( + e: React.SyntheticEvent + ): 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): void => { - const { hits } = this.state; - + const handleHits = (currHits: Array): 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 ( - -
    - -
    - - { - this.handleSearch(e); - }} - onInput={this.handleChange} - translations={{ - submitButtonTitle: t('icons.input-search'), - resetButtonTitle: t('icons.input-reset') - }} - placeholder={searchPlaceholder} - onFocus={this.handleFocus} - /> - - {isDropdownEnabled && isSearchFocused && ( - - )} -
    -
    -
    -
    - ); - } + const searchPlaceholder = t('search-bar:placeholder').startsWith( + 'search.placeholder.' + ) + ? t('search.placeholder') + : t('search-bar:placeholder'); + + return ( + +
    + +
    + + { + handleSearch(e); + }} + onInput={handleChange} + translations={{ + submitButtonTitle: t('icons.input-search'), + resetButtonTitle: t('icons.input-reset') + }} + placeholder={searchPlaceholder} + onFocus={handleFocus} + /> + + {isDropdownEnabled && isSearchFocused && ( + + )} +
    +
    +
    +
    + ); } SearchBar.displayName = 'SearchBar'; diff --git a/client/src/components/search/searchBar/search-hits.tsx b/client/src/components/search/searchBar/search-hits.tsx index f0c763fd039112..cf84fc5118f3e4 100644 --- a/client/src/components/search/searchBar/search-hits.tsx +++ b/client/src/components/search/searchBar/search-hits.tsx @@ -55,7 +55,7 @@ function SearchHits({