Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: added the option to have a search in the select component #131

Merged
merged 8 commits into from
Mar 11, 2024
17 changes: 17 additions & 0 deletions docs/src/stories/Select/Root.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ const meta: Meta<SelectRootProps> = {
},
type: { name: 'string', required: false },
},
enableSearch: {
control: 'boolean',
description: 'Habilkita o search dentro do select.',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
type: { name: 'boolean', required: false },
},
searchPlaceholder: {
control: 'text',
description: 'Define o placeholder do search dentro do componente.',
table: {
type: { summary: 'text' },
},
type: { name: 'string', required: false },
},
position: {
control: 'inline-radio',
description:
Expand Down
60 changes: 35 additions & 25 deletions package/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import LayoutDefault from './layout/Default'
import { Accordion } from './library'
import { Select } from './library'

import { zodResolver } from '@hookform/resolvers/zod'
import { Eraser } from 'phosphor-react'
import { useState } from 'react'
import { SetStateAction, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { z } from 'zod'

Expand All @@ -28,6 +28,12 @@ function App() {
reset()
setTest('')
}

const [selectedValue, setSelectedValue] = useState('')

const handleChange = (value: SetStateAction<string>) => {
setSelectedValue(value)
}
return (
<LayoutDefault
asChild
Expand All @@ -36,29 +42,33 @@ function App() {
>
<form onSubmit={handleSubmit(onSubmit)}>
{/* ================================= TEST AREA ================================= */}
<Accordion.Root type="single" collapsible>
{[...Array(3)].map((item, index) => (
<Accordion.Item
key={index}
value={index.toString()}
className="w-72"
>
<Accordion.Header>
<span>xxx</span>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content>
<span>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Deleniti odio tempore magni error, illo placeat minus
accusantium, veniam atque voluptate iusto rerum nemo
aspernatur obcaecati repellendus, mollitia beatae eos
assumenda.
</span>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>

<Select.Root
value={selectedValue}
onChange={handleChange}
placeholder="Selecione uma opção"
enableSearch={true}
searchPlaceholder="Digite aqui sua busca"
>
<Select.Item value="option1">Alice</Select.Item>
<Select.Item value="option2">Bob</Select.Item>
<Select.Item value="option3">Charlie</Select.Item>
<Select.Item value="option4">David</Select.Item>
<Select.Item value="option5">Emma</Select.Item>
<Select.Item value="option6">Frank</Select.Item>
<Select.Item value="option7">Grace</Select.Item>
<Select.Item value="option8">Harry</Select.Item>
<Select.Item value="option9">Ivy</Select.Item>
<Select.Item value="option10">Jack</Select.Item>
<Select.Item value="option11">Kate</Select.Item>
<Select.Item value="option12">Liam</Select.Item>
<Select.Item value="option13">Mia</Select.Item>
<Select.Item value="option14">Noah</Select.Item>
<Select.Item value="option15">Olivia</Select.Item>
<Select.Item value="option16">Peter</Select.Item>
<Select.Item value="option17">Quinn</Select.Item>
</Select.Root>

{/* ================================= TEST AREA ================================= */}
</form>
</LayoutDefault>
Expand Down
34 changes: 34 additions & 0 deletions package/src/__tests__/Select/Search.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import SearchInput from '@/src/library/Select/Search'

import { fireEvent, render } from '@testing-library/react'

describe('SearchInput tests', () => {
beforeEach(() => {})

it('Should render SearchInput with placeholder', () => {
const placeholder = 'Search here...'
const onChangeMock = jest.fn()

const { getByPlaceholderText } = render(
<SearchInput onChange={onChangeMock} searchPlaceholder={placeholder} />,
)

const inputElement = getByPlaceholderText(placeholder)
expect(inputElement).toBeInTheDocument()
})

it('Should call onChange handler with input value', () => {
const onChangeMock = jest.fn()

const { getByPlaceholderText } = render(
<SearchInput onChange={onChangeMock} searchPlaceholder="Search..." />,
)

const inputElement = getByPlaceholderText('Search...')
const inputValue = 'Test input value'

fireEvent.change(inputElement, { target: { value: inputValue } })

expect(onChangeMock).toHaveBeenCalledWith(inputValue)
})
})
50 changes: 48 additions & 2 deletions package/src/library/Select/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import SearchInput from './Search'

import { cn } from '@/src/utils/class-merge.helper'
import { Status, StatusClass } from '@types'

import * as RadixSelect from '@radix-ui/react-select'
import { CaretDown, CaretUp } from 'phosphor-react'
import { FC } from 'react'
import {
FC,
useState,
ReactNode,
Children,
isValidElement,
SetStateAction,
} from 'react'

const statusVariants: StatusClass = {
error: { root: 'border-error' },
Expand All @@ -15,25 +24,48 @@ export interface SelectRootProps extends RadixSelect.SelectProps {
align?: RadixSelect.SelectContentProps['align']
className?: string
placeholder?: string
searchPlaceholder?: string
portalContainer?: HTMLElement | null
position?: RadixSelect.SelectContentProps['position']
status?: Status
onChange?: (value: string) => void
enableSearch?: boolean
children: ReactNode
Ftarganski marked this conversation as resolved.
Show resolved Hide resolved
}

const SelectRoot: FC<SelectRootProps> = ({
align = 'center',
children,
className,
placeholder,
searchPlaceholder,
portalContainer,
position = 'popper',
status,
value,
onChange,
onValueChange,
enableSearch = false,
...rest
}) => {
const [searchText, setSearchText] = useState('')
const [isTyping, setIsTyping] = useState(false)

const filteredItems = Children.toArray(children).filter(
(child) =>
isValidElement(child) &&
child.props.children.toLowerCase().includes(searchText.toLowerCase()),
)

const handleSearchChange = (value: SetStateAction<string>) => {
setSearchText(value)
setIsTyping(true)
}

const handleItemClick = () => {
setIsTyping(false)
}

return (
<RadixSelect.Root
value={value}
Expand Down Expand Up @@ -68,6 +100,12 @@ const SelectRoot: FC<SelectRootProps> = ({
avoidCollisions
className="z-100 flex max-h-[--radix-select-content-available-height] min-w-56 max-w-[--radix-select-content-available-width] flex-col gap-2 rounded-lg border border-gray bg-gray-100 shadow"
>
{enableSearch && (
<SearchInput
searchPlaceholder={searchPlaceholder}
onChange={handleSearchChange}
/>
)}
<RadixSelect.ScrollUpButton className="flex items-center justify-center p-2">
<CaretUp />
</RadixSelect.ScrollUpButton>
Expand All @@ -81,7 +119,15 @@ const SelectRoot: FC<SelectRootProps> = ({
<RadixSelect.ItemText>{placeholder}</RadixSelect.ItemText>
</RadixSelect.Item>
)}
{children}
{filteredItems.map((item, index) => (
<RadixSelect.Item
key={index}
onSelect={handleItemClick}
value={isValidElement(item) ? item.props.value : item}
>
{item}
</RadixSelect.Item>
))}
</RadixSelect.Viewport>
<RadixSelect.ScrollDownButton className="flex items-center justify-center p-2">
<CaretDown />
Expand Down
39 changes: 39 additions & 0 deletions package/src/library/Select/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as RadixSelect from '@radix-ui/react-select'
import { MagnifyingGlass } from 'phosphor-react'
import { FC, useState, ChangeEvent } from 'react'

export interface SearchInputProps {
onChange: (value: string) => void
searchPlaceholder?: string
}

const SearchInput: FC<SearchInputProps> = ({ onChange, searchPlaceholder }) => {
const [searchText, setSearchText] = useState('')

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setSearchText(value)
onChange(value)
}

return (
<div className="flex items-center px-4 py-2">
<label htmlFor="search" className="sr-only">
{searchPlaceholder}
</label>
<RadixSelect.Icon>
<MagnifyingGlass />
</RadixSelect.Icon>
<input
id="search"
type="text"
placeholder={searchPlaceholder}
value={searchText}
onChange={handleInputChange}
className="px-4 py-2"
/>
</div>
)
}

export default SearchInput
3 changes: 3 additions & 0 deletions package/src/library/Select/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import SelectItem from './Item'
import SelectRoot from './Root'
import SearchInput from './Search'

export type { SelectRootProps } from './Root'
export type { SelectItemProps } from './Item'
export type { SearchInputProps } from './Search'

const Select = {
Root: SelectRoot,
Item: SelectItem,
Search: SearchInput,
}

export default Select