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

[Feature]: Allow SearchSelect icon to be a custom component #1021

Open
angelhodar opened this issue Apr 25, 2024 · 0 comments
Open

[Feature]: Allow SearchSelect icon to be a custom component #1021

angelhodar opened this issue Apr 25, 2024 · 0 comments
Labels
Type: Feature New feature for existing component

Comments

@angelhodar
Copy link

angelhodar commented Apr 25, 2024

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:

image

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 :)

@severinlandolt severinlandolt added the Type: Feature New feature for existing component label May 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature New feature for existing component
Projects
None yet
Development

No branches or pull requests

2 participants