Skip to content

Commit

Permalink
fix(web): SearchBox update for search in /s/syslumenn (#5767)
Browse files Browse the repository at this point in the history
* Search all on icon click

* changing SearchBox to use AsyncSearch

* smá tweak

* debounce and a few minor tweaks

* format

* Show loading icon while typing, enabled english search and improved keyboard navigation (tab, enter and arrow keys) (#6160)

Co-authored-by: Rúnar Vestmann <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 6, 2022
1 parent 34fe46f commit e147674
Showing 1 changed file with 189 additions and 113 deletions.
302 changes: 189 additions & 113 deletions apps/web/components/Organization/SearchBox/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useState } from 'react'
import {
AsyncSearchInput,
Box,
Button,
AsyncSearch,
Text,
Link,
Button,
AsyncSearchOption,
} from '@island.is/island-ui/core'
import { useQuery } from '@apollo/client'
import { useLazyQuery } from '@apollo/client'
import {
Query,
QueryGetArticlesArgs,
Expand All @@ -15,6 +15,11 @@ import {
import { GET_ORGANIZATION_SERVICES_QUERY } from '@island.is/web/screens/queries'
import { LinkType, useLinkResolver } from '@island.is/web/hooks/useLinkResolver'
import { useRouter } from 'next/router'
import { useDebounce } from 'react-use'

interface AsyncSearchOptionWithIsArticleField extends AsyncSearchOption {
isArticle: boolean
}

interface SearchBoxProps {
organizationPage: Query['getOrganizationPage']
Expand All @@ -30,135 +35,206 @@ export const SearchBox = ({
searchAllText,
}: SearchBoxProps) => {
const { linkResolver } = useLinkResolver()
const Router = useRouter()
const router = useRouter()

const [value, setValue] = useState('')
const [options, setOptions] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [waitingForNextPageToLoad, setWaitingForNextPageToLoad] = useState(
false,
)

const { data, loading } = useQuery<Query, QueryGetArticlesArgs>(
const [fetch, { data, loading }] = useLazyQuery<Query, QueryGetArticlesArgs>(
GET_ORGANIZATION_SERVICES_QUERY,
{
variables: {
input: {
lang: 'is',
organization: organizationPage.slug,
size: 500,
sort: SortField.Popular,
},
},
},
)

const items = useMemo(
() =>
data?.getArticles
.map((o) => ({
type: 'article',
label: o.title,
value: o.slug,
}))
.concat(
...organizationPage.menuLinks.map((x) => [
{
type: 'url',
label: x.primaryLink.text,
value: x.primaryLink.url,
useDebounce(
() => {
if (value) {
fetch({
variables: {
input: {
lang: router.asPath.includes('/en/') ? 'en' : 'is',
organization: organizationPage.slug,
size: 500,
sort: SortField.Popular,
},
...x.childrenLinks.map((y) => ({
type: 'url',
label: y.text,
value: y.url,
})),
]),
) ?? [],
[data],
},
})
}
setIsLoading(false)
},
300,
[value],
)

const [value, setValue] = useState('')
const [options, setOptions] = useState([])
const items = data?.getArticles?.map((item, index) => ({
label: item.title,
value: item.slug,
isArticle: true,
component: ({ active }) => {
return (
<Box
key={`article-${item.id ?? ''}-${index}`}
cursor="pointer"
outline="none"
paddingX={2}
paddingY={1}
role="button"
background={active ? 'blue100' : 'white'}
onClick={() => {
setOptions([])
}}
>
<Text as="span">{item.title}</Text>
</Box>
)
},
}))

const clearAll = () => {
setIsLoading(false)
setOptions([])
}

const handleOptionSelect = (
selectedItem: AsyncSearchOptionWithIsArticleField,
value: string,
) => {
setOptions([])
if (!value) return
setWaitingForNextPageToLoad(true)

const [hasFocus, setHasFocus] = useState(false)
let pathname: string
let searchAll = false

useEffect(() => {
if (selectedItem && selectedItem?.isArticle) {
const newValue = selectedItem.value
pathname = linkResolver('Article' as LinkType, [newValue]).href
setValue(selectedItem.label)
} else {
pathname = linkResolver('search').href
searchAll = true
}

router
.push({
pathname,
query: searchAll ? { q: value } : {},
})
.then(() => window.scrollTo(0, 0))
}

const updateOptions = () => {
const newOpts = items
.filter(
(item) =>
value && item.label.toLowerCase().includes(value.toLowerCase()),
)
.slice(0, 5)
? items
.filter(
(item) =>
value && item.label.toLowerCase().includes(value.toLowerCase()),
)
.slice(0, 5)
: []

if (!value) {
setOptions([])
clearAll()
}

setOptions(newOpts)
}, [value])

const onBlur = () => {
setTimeout(() => {
setHasFocus(false)
}, 100)
}
return (
<Box marginTop={3}>
<AsyncSearchInput
rootProps={{
'aria-controls': '-menu',
}}
hasFocus={hasFocus}
menuProps={{
comp: 'div',
}}
buttonProps={{}}
inputProps={{
inputSize: 'medium',
onFocus: () => setHasFocus(true),
onBlur,
placeholder,
value,
onChange: (e) => setValue(e.target.value),
}}
>
{!!value && (
<>
<Box padding={2} onClick={(e) => e.stopPropagation()}>
{options?.map((x) => (
<Box paddingY={1}>
<Link
key={x.value}
href={
x.type === 'url'
? x.value
: linkResolver('Article' as LinkType, [x.value]).href
}
underline="normal"
>
<Text key={x.value} as="span">
{x.label}
</Text>
</Link>
</Box>
))}
{!options?.length && (
<Box paddingY={1}>
<Text as="span">{noResultsText}</Text>
</Box>
)}
<Box paddingY={2}>
setOptions(
newOpts.length
? newOpts.concat({
label: value,
value: '',
isArticle: false,
component: ({ active }) => (
<Box
padding={2}
background={active ? 'blue100' : 'white'}
cursor="pointer"
>
<Button
type="button"
variant="text"
onClick={() =>
Router.push({
pathname: linkResolver('search').href,
query: { q: value },
})
}
onClick={() => {
setWaitingForNextPageToLoad(true)
router
.push({
pathname: linkResolver('search').href,
query: { q: value },
})
.then(() => window.scrollTo(0, 0))
}}
>
{searchAllText}
</Button>
</Box>
</Box>
</>
)}
</AsyncSearchInput>
),
})
: [
{
label: value,
value: '',
isArticle: false,
component: () => (
<Box padding={2} disabled>
<Text as="span">{noResultsText}</Text>
<Box cursor="pointer">
<Button
type="button"
variant="text"
onClick={() => {
setWaitingForNextPageToLoad(true)
router
.push({
pathname: linkResolver('search').href,
query: { q: value },
})
.then(() => window.scrollTo(0, 0))
}}
>
{searchAllText}
</Button>
</Box>
</Box>
),
},
],
)
}

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(updateOptions, [value, loading, data])

const busy = loading || isLoading || waitingForNextPageToLoad

return (
<Box marginTop={3}>
<AsyncSearch
size={'medium'}
colored={false}
key="island-organization"
placeholder={placeholder}
options={options}
loading={busy}
initialInputValue={''}
inputValue={value}
onInputValueChange={(newValue) => {
setIsLoading(newValue !== value)
setValue(newValue ?? '')
}}
closeMenuOnSubmit
onSubmit={(value, selectedOption) => {
handleOptionSelect(
selectedOption as AsyncSearchOptionWithIsArticleField,
value,
)
}}
onChange={(i, option) => {
handleOptionSelect(
option.selectedItem as AsyncSearchOptionWithIsArticleField,
value,
)
}}
/>
</Box>
)
}

0 comments on commit e147674

Please sign in to comment.