Skip to content

Instantly share code, notes, and snippets.

@peterbe
Created August 25, 2022 16:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peterbe/095658ee05b806de365cbfd40b7f45f9 to your computer and use it in GitHub Desktop.
Save peterbe/095658ee05b806de365cbfd40b7f45f9 to your computer and use it in GitHub Desktop.
diff --git a/components/Search.tsx b/components/Search.tsx
index 5bffca38715..80a07dd66af 100644
--- a/components/Search.tsx
+++ b/components/Search.tsx
@@ -9,6 +9,8 @@ import { useTranslation } from 'components/hooks/useTranslation'
import { sendEvent, EventType } from 'components/lib/events'
import { useMainContext } from './context/MainContext'
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
+import { useRecentSearches } from 'components/hooks/useRecentSearches'
+import type { RecentSearch } from 'components/hooks/useRecentSearches'
import { useQuery } from 'components/hooks/useQuery'
import { Link } from 'components/Link'
import { useLanguages } from './context/LanguagesContext'
@@ -86,11 +88,13 @@ export function Search({
revalidateOnReconnect: false,
}
)
+ const { recentSearches, addRecentSearch } = useRecentSearches()
const [previousResults, setPreviousResults] = useState<SearchResult[] | undefined>()
useEffect(() => {
if (results) {
setPreviousResults(results)
+ addRecentSearch(query, results.length)
} else if (!query) {
setPreviousResults(undefined)
}
@@ -156,6 +160,9 @@ export function Search({
// Close panel if overlay is clicked
function closeSearch() {
setLocalQuery('')
+ if (inputRef.current) {
+ inputRef.current.blur()
+ }
}
// Prevent the page from refreshing when you "submit" the form
@@ -166,6 +173,11 @@ export function Search({
}
}
+ const [inputFocussed, setInputFocussed] = useState(false)
+
+ const showRecentSearches = inputFocussed && recentSearches.length > 0
+ const showResults = Boolean(query || showRecentSearches)
+
const SearchResults = (
<>
<div
@@ -176,20 +188,30 @@ export function Search({
'pt-9 color-bg-default color-shadow-medium position-absolute top-0 right-0',
styles.resultsContainer,
isHeaderSearch && styles.resultsContainerHeader,
- query ? 'd-block' : 'd-none',
- query && styles.resultsContainerOpen
+ showResults ? 'd-block' : 'd-none',
+ showResults && styles.resultsContainerOpen
)}
>
- <ShowSearchResults
- anchorRef={inputRef}
- isHeaderSearch={isHeaderSearch}
- isMobileSearch={isMobileSearch}
- isLoading={isLoading}
- results={previousResults}
- closeSearch={closeSearch}
- debug={debug}
- query={query}
- />
+ {query ? (
+ <ShowSearchResults
+ anchorRef={inputRef}
+ isHeaderSearch={isHeaderSearch}
+ isMobileSearch={isMobileSearch}
+ isLoading={isLoading}
+ results={previousResults}
+ closeSearch={closeSearch}
+ debug={debug}
+ query={query}
+ />
+ ) : showRecentSearches ? (
+ <ShowRecentSearches
+ anchorRef={inputRef}
+ isHeaderSearch={isHeaderSearch}
+ isMobileSearch={isMobileSearch}
+ searches={recentSearches}
+ closeSearch={closeSearch}
+ />
+ ) : null}
</div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
@@ -220,8 +242,8 @@ export function Search({
variant === 'expanded' && 'py-3',
isHeaderSearch && styles.searchInputHeader,
!isHeaderSearch && 'width-full',
- isHeaderSearch && query && styles.searchInputExpanded,
- isHeaderSearch && query && 'position-absolute top-0 right-0'
+ isHeaderSearch && showResults && styles.searchInputExpanded,
+ isHeaderSearch && showResults && 'position-absolute top-0 right-0'
)}
style={{
background: `var(--color-canvas-default) url("/assets/images/octicons/search-${iconSize}.svg") no-repeat ${
@@ -237,6 +259,16 @@ export function Search({
maxLength={512}
onChange={onSearch}
value={localQuery}
+ onFocus={() => {
+ console.log('Focussed!')
+
+ setInputFocussed(true)
+ }}
+ onBlur={() => {
+ console.log('Blurred')
+
+ setInputFocussed(false)
+ }}
/>
<button className="d-none" type="submit" title="Submit the search query." hidden />
</form>
@@ -520,3 +552,113 @@ function ShowSearchResults({
</p>
)
}
+
+function ShowRecentSearches({
+ anchorRef,
+ isHeaderSearch,
+ isMobileSearch,
+ searches,
+ closeSearch,
+}: {
+ anchorRef: RefObject<HTMLElement>
+ isHeaderSearch: boolean
+ isMobileSearch: boolean
+
+ searches: RecentSearch[]
+ closeSearch: () => void
+}) {
+ const { clearRecentSearches } = useRecentSearches()
+
+ const ActionListResults = (
+ <div
+ data-testid="search-results"
+ className={cx(
+ 'mt-3',
+ isHeaderSearch && styles.headerSearchResults,
+ isHeaderSearch && 'overflow-auto'
+ )}
+ >
+ <ActionList
+ items={searches.map(({ locale, version, query, results }) => {
+ let href = `/${locale}`
+ if (version !== DEFAULT_VERSION) {
+ href += `/${version}`
+ }
+ href += `?${new URLSearchParams({ query }).toString()}`
+ return {
+ key: locale + version + query,
+ text: query,
+ renderItem: () => (
+ <ActionList.Item as="div">
+ <div
+ data-testid="recent-search"
+ className={cx('list-style-none', styles.resultsContainer)}
+ >
+ <div className={cx('py-2 px-3')}>
+ <Link href={href} className="no-underline color-fg-default">
+ <i>{query}</i>
+ </Link>{' '}
+ found {results}
+ </div>
+ </div>
+ </ActionList.Item>
+ ),
+ }
+ })}
+ />
+ <button
+ className="btn btn-outline float-right"
+ onClick={() => {
+ clearRecentSearches()
+ }}
+ >
+ clear
+ </button>
+ </div>
+ )
+
+ console.log('Rendering Overlay')
+
+ return (
+ <div>
+ {!isHeaderSearch && !isMobileSearch ? (
+ <>
+ <Overlay
+ initialFocusRef={anchorRef}
+ returnFocusRef={anchorRef}
+ ignoreClickRefs={[anchorRef]}
+ onEscape={() => closeSearch()}
+ onClickOutside={() => closeSearch()}
+ aria-labelledby="title"
+ sx={
+ isHeaderSearch
+ ? {
+ background: 'none',
+ boxShadow: 'none',
+ position: 'static',
+ overflowY: 'auto',
+ maxHeight: '80vh',
+ maxWidth: '96%',
+ margin: '1.5em 2em 0 0.5em',
+ scrollbarWidth: 'none',
+ }
+ : window.innerWidth < 1012
+ ? {
+ marginTop: '28rem',
+ marginLeft: '5rem',
+ }
+ : {
+ marginTop: '15rem',
+ marginLeft: '5rem',
+ }
+ }
+ >
+ {ActionListResults}
+ </Overlay>
+ </>
+ ) : (
+ ActionListResults
+ )}
+ </div>
+ )
+}
diff --git a/components/hooks/useRecentSearches.ts b/components/hooks/useRecentSearches.ts
new file mode 100644
index 00000000000..655939eaf9f
--- /dev/null
+++ b/components/hooks/useRecentSearches.ts
@@ -0,0 +1,128 @@
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+
+import { DEFAULT_VERSION } from './useVersion'
+
+export interface RecentSearch {
+ query: string
+ results: number
+ version: string
+ locale: string
+ date: Date
+}
+
+type RecentSearchesInfo = {
+ recentSearches: RecentSearch[]
+ addRecentSearch: (query: string, results: number) => void
+ clearRecentSearches: () => void
+}
+
+const STORAGE_KEY = 'recentsearches'
+
+function store(struct: RecentSearch[]) {
+ const storage = process.env.NODE_ENV === 'development' ? sessionStorage : localStorage
+ try {
+ storage.setItem(STORAGE_KEY, JSON.stringify(struct))
+ } catch (err) {
+ console.warn('Unable to storage in local storage', err)
+ }
+}
+
+function load(): RecentSearch[] {
+ const storage = process.env.NODE_ENV === 'development' ? sessionStorage : localStorage
+ try {
+ return JSON.parse(storage.getItem(STORAGE_KEY) || '[]')
+ } catch (err) {
+ console.warn('Unable to storage in local storage', err)
+ return []
+ }
+}
+
+export const useRecentSearches = (): RecentSearchesInfo => {
+ const router = useRouter()
+
+ const [recentSearches, setRecentSearches] = useState<RecentSearch[]>([])
+
+ useEffect(() => {
+ const fromStorage = load()
+ if (fromStorage.length > 0) {
+ setRecentSearches(fromStorage)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (recentSearches.length > 0) {
+ console.log('STORE IN STORAGE', recentSearches)
+ store(recentSearches)
+ }
+ }, [recentSearches])
+
+ function addRecentSearch(query: string, results: number) {
+ if (!results) return
+ const versionId = router.query.versionId || DEFAULT_VERSION
+
+ if (!versionId || typeof versionId !== 'string') {
+ console.log('no versionId', router.query)
+
+ return
+ }
+ const { locale } = router
+ if (!locale) return
+
+ // // If the most recently added one is the same and within the same time
+ // // do nothing.
+ // if (recentSearches.length > 0) {
+ // const lastSearch = recentSearches[0]
+ // if (
+ // lastSearch.query === query &&
+ // lastSearch.results === results &&
+ // lastSearch.version === versionId &&
+ // lastSearch.locale === locale
+ // ) {
+ // // console.log('New recent query not new', query)
+
+ // return
+ // }
+ // }
+
+ setRecentSearches((prevState) => {
+ const thisSearch = {
+ query,
+ results,
+ version: versionId,
+ locale,
+ date: new Date(),
+ }
+
+ const merged = [
+ thisSearch,
+ ...prevState.filter((previousSearch, i) => {
+ return !i && query.startsWith(previousSearch.query)
+ }),
+ ]
+ console.log(
+ 'MERGED',
+ merged.map((x) => x.query)
+ )
+
+ return merged.slice(0, 10)
+ })
+ }
+
+ function clearRecentSearches() {
+ console.log('CLEARING')
+
+ setRecentSearches([])
+ store([])
+ }
+ return {
+ recentSearches: recentSearches.filter((search) => {
+ return (
+ search.version === (router.query.versionId || DEFAULT_VERSION) &&
+ search.locale === router.locale
+ )
+ }),
+ addRecentSearch,
+ clearRecentSearches,
+ }
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment