[Feature]: Allow SearchSelect icon to be a custom component
angelhodar opened this issue · comments
What problem does this feature solve?
Hey everyone, I hope to find all of you well! I have been testing new inputs and changes in tremor and I have a suggestion. Right now, I have added a SearchSelect
where I render a list of clients. Each client has a logo for their brand, but its an external image url, so I cant render a static component as it needs the url prop to be passed. To solve this, I had to do something like this in the SearchSelectInput
wrapper I am creating:
'use client'
import {
SearchSelect,
SearchSelectProps as TremorSearchSelectProps,
SearchSelectItem,
SearchSelectItemProps,
} from '@tremor/react'
import {
BaseFormInputProps,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from 'components/Primitives/Form'
import { Avatar, AvatarImage } from 'components/Primitives/Avatar'
interface SearchSelectOptionProps extends SearchSelectItemProps {
label: string
picture?: string | null
}
type SearchSelectProps = Omit<TremorSearchSelectProps, 'children'> &
BaseFormInputProps
export interface SearchSelectInputProps extends SearchSelectProps {
options: SearchSelectOptionProps[]
}
export default function SearchSelectInput(props: SearchSelectInputProps) {
const {
name,
control,
label,
description,
placeholder,
options,
onSearchValueChange,
enableClear = true,
} = props
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<SearchSelect
placeholder={placeholder}
defaultValue={field.value}
onValueChange={field.onChange}
onSearchValueChange={onSearchValueChange}
enableClear={enableClear}
>
{options.map((option) => (
<SearchSelectItem key={option.value} value={option.value}>
<div className="flex flex-row items-center">
{option.picture && (
<Avatar className="w-7 h-7 mr-4">
<AvatarImage src={option.picture} />
</Avatar>
)}
{option.label}
</div>
</SearchSelectItem>
))}
</SearchSelect>
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
}
Its working BUT the browser gives a warning because a div cant be a child of select option:
What does the proposed API look like?
To solve this, I have thought about changing the icon signature to accept a React.ReactNode too, and in the render just check if the icon is a function or not, so it would allow to pass custom components. It would be something like this:
import React from 'react'
import { Combobox } from '@headlessui/react'
import { makeClassName, tremorTwMerge } from 'lib'
const makeSearchSelectItemClassName = makeClassName('SearchSelectItem')
export interface SearchSelectItemProps
extends React.HTMLAttributes<HTMLLIElement> {
value: string
icon?: React.ElementType | React.ReactNode
}
const SearchSelectItem = React.forwardRef<HTMLLIElement, SearchSelectItemProps>(
(props, ref) => {
const { value, icon, className, children, ...other } = props
// Render the icon if it's a component type (function) or a valid React element
const renderIcon = () => {
if (typeof icon === 'function') {
const iconClassName = () =>
tremorTwMerge(
makeSearchSelectItemClassName('icon'),
'flex-none h-5 w-5 mr-3 text-tremor-content-subtle dark:text-dark-tremor-content-subtle'
)
const IconComponent = icon
return <IconComponent className={iconClassName()} />
} else if (React.isValidElement(icon)) {
return icon
}
return null
}
return (
<Combobox.Option
className={tremorTwMerge(
makeSearchSelectItemClassName('root'),
'flex justify-start items-center cursor-default text-tremor-default p-2.5 ui-active:bg-tremor-background-muted ui-active:text-tremor-content-strong ui-selected:text-tremor-content-strong ui-selected:bg-tremor-background-muted text-tremor-content-emphasis dark:ui-active:bg-dark-tremor-background-muted dark:ui-active:text-dark-tremor-content-strong dark:ui-selected:text-dark-tremor-content-strong dark:ui-selected:bg-dark-tremor-background-muted dark:text-dark-tremor-content-emphasis',
className
)}
ref={ref}
key={value}
value={value}
{...other}
>
{renderIcon()}
<span className="whitespace-nowrap truncate">{children ?? value}</span>
</Combobox.Option>
)
}
)
SearchSelectItem.displayName = 'SearchSelectItem'
export default SearchSelectItem
What do you think? Btw if you just want to keep this simple and allow this custom functionality in the new raw components you are creating, i totally understand it :)