tldraw/apps/docs/components/Autocomplete.tsx
Mime Čuvalo 157d24db73
docs: rework search UI (#2723)
Reworks search to not be a page and instead to be inline dropdown.

<img width="763" alt="Screenshot 2024-02-05 at 13 22 58"
src="https://github.com/tldraw/tldraw/assets/469604/4e5a8076-62cd-44bb-b8e7-7f5ecdc4af24">


- rework search completely
- rm Search Results css
- uses Ariakit and add appropriate hooks / styling
- I couldn't use Radix unfortunately since they're still working on
adding a Combox: https://github.com/radix-ui/primitives/issues/1342
- I'm open to other suggestions but Ariakit plays nicely with Radix and
keeps things open to migrate to Radix in the future
- fixes bug with not scrolling to right place when having a direct link
- adds categories in the search results - examples / reference / learn
- and adds category icons. Let me know if there's a better policy for
adding new SVG icons cc @steveruizok

### Change Type

- [x] `minor` — New feature

### Test Plan

1. Test searches using normal method for each type (examples, docs,
refs)
2. Test searches using AI for each type (ditto)

### Release Notes

- Docs: rework the search to be an inline dropdown.
2024-02-05 14:32:50 +00:00

123 lines
2.9 KiB
TypeScript

import {
Combobox,
ComboboxCancel,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxItem,
ComboboxItemValue,
ComboboxPopover,
ComboboxProvider,
} from '@ariakit/react'
import { ComponentType, ForwardedRef, forwardRef, startTransition, useState } from 'react'
import './Autocomplete.css'
import { Icon } from './Icon'
import Spinner from './Spinner'
export type DropdownOption = {
label: string
value: string
group?: string
}
type AutocompleteProps = {
customUI?: React.ReactNode
groups?: string[]
groupsToIcon?: {
[key: string]: ComponentType<{
className?: string
}>
}
groupsToLabel?: { [key: string]: string }
isLoading: boolean
options: DropdownOption[]
onChange: (value: string) => void
onInputChange: (value: string) => void
}
const DEFAULT_GROUP = 'autocomplete-default'
const Autocomplete = forwardRef(function Autocomplete(
{
customUI,
groups = [DEFAULT_GROUP],
groupsToIcon,
groupsToLabel,
isLoading,
options,
onInputChange,
onChange,
}: AutocompleteProps,
ref: ForwardedRef<HTMLInputElement>
) {
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
const renderedGroups = groups.map((group) => {
const filteredOptions = options.filter(
({ group: optionGroup }) => optionGroup === group || group === DEFAULT_GROUP
)
if (filteredOptions.length === 0) return null
return (
<ComboboxGroup>
{groupsToLabel?.[group] && (
<ComboboxGroupLabel className="autocomplete__group">
{groupsToLabel?.[group]}
</ComboboxGroupLabel>
)}
{filteredOptions.map(({ label, value }) => {
const Icon = groupsToIcon?.[group]
return (
<ComboboxItem key={`${label}-${value}`} className="autocomplete__item" value={value}>
{Icon && <Icon className="autocomplete__item__icon" />}
<ComboboxItemValue value={label} />
</ComboboxItem>
)
})}
</ComboboxGroup>
)
})
return (
<ComboboxProvider<string>
defaultSelectedValue=""
open={open}
setOpen={setOpen}
resetValueOnHide
includesBaseElement={false}
setValue={(newValue) => {
startTransition(() => setValue(newValue))
onInputChange(newValue)
}}
setSelectedValue={(newValue) => onChange(newValue)}
>
<div className="autocomplete__wrapper">
{isLoading ? (
<Spinner className="autocomplete__icon" />
) : (
<Icon className="autocomplete__icon" icon="search" small />
)}
<Combobox
autoSelect
placeholder="Search…"
ref={ref}
className="autocomplete__input"
value={value}
/>
{value && <ComboboxCancel className="autocomplete__cancel" />}
{value && (
<ComboboxPopover className="autocomplete__popover">
{customUI}
{options.length === 0 && <span>No results found.</span>}
{options.length !== 0 && renderedGroups}
</ComboboxPopover>
)}
</div>
</ComboboxProvider>
)
})
export default Autocomplete