| | |
| | | 'use client' |
| | | import type { FC } from 'react' |
| | | import React, { useEffect, useState } from 'react' |
| | | import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' |
| | | import React, { Fragment, useEffect, useState } from 'react' |
| | | import { Combobox, Listbox, Transition } from '@headlessui/react' |
| | | import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' |
| | | import Badge from '../badge/index' |
| | | import { RiCheckLine } from '@remixicon/react' |
| | | import { useTranslation } from 'react-i18next' |
| | | import classNames from '@/utils/classnames' |
| | |
| | | export type ISelectProps = { |
| | | className?: string |
| | | wrapperClassName?: string |
| | | renderTrigger?: (value: Item | null) => React.JSX.Element | null |
| | | renderTrigger?: (value: Item | null) => JSX.Element | null |
| | | items?: Item[] |
| | | defaultValue?: number | string |
| | | disabled?: boolean |
| | |
| | | disabled = false, |
| | | onSelect, |
| | | allowSearch = true, |
| | | bgClassName = 'bg-components-input-bg-normal', |
| | | bgClassName = 'bg-gray-100', |
| | | overlayClassName, |
| | | optionClassName, |
| | | renderOption, |
| | |
| | | } |
| | | }}> |
| | | <div className={classNames('relative')}> |
| | | <div className='group text-text-secondary'> |
| | | <div className='group text-gray-800'> |
| | | {allowSearch |
| | | ? <ComboboxInput |
| | | className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`} |
| | | ? <Combobox.Input |
| | | className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`} |
| | | onChange={(event) => { |
| | | if (!disabled) |
| | | setQuery(event.target.value) |
| | | }} |
| | | displayValue={(item: Item) => item?.name} |
| | | /> |
| | | : <ComboboxButton onClick={ |
| | | : <Combobox.Button onClick={ |
| | | () => { |
| | | if (!disabled) |
| | | setOpen(!open) |
| | | } |
| | | } className={classNames(`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover group-hover:bg-state-base-hover`, optionClassName)}> |
| | | <div className='w-0 grow truncate text-left' title={selectedItem?.name}>{selectedItem?.name}</div> |
| | | </ComboboxButton>} |
| | | <ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" onClick={ |
| | | } className={classNames(`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`, optionClassName)}> |
| | | <div className='w-0 grow text-left truncate' title={selectedItem?.name}>{selectedItem?.name}</div> |
| | | </Combobox.Button>} |
| | | <Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={ |
| | | () => { |
| | | if (!disabled) |
| | | setOpen(!open) |
| | | } |
| | | }> |
| | | {open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />} |
| | | </ComboboxButton> |
| | | </Combobox.Button> |
| | | </div> |
| | | |
| | | {(filteredItems.length > 0 && open) && ( |
| | | <ComboboxOptions className={`absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm ${overlayClassName}`}> |
| | | <Combobox.Options className={`absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm ${overlayClassName}`}> |
| | | {filteredItems.map((item: Item) => ( |
| | | <ComboboxOption |
| | | <Combobox.Option |
| | | key={item.value} |
| | | value={item} |
| | | className={({ active }: { active: boolean }) => |
| | | classNames( |
| | | 'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', |
| | | active ? 'bg-state-base-hover' : '', |
| | | 'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700', |
| | | active ? 'bg-gray-100' : '', |
| | | optionClassName, |
| | | ) |
| | | } |
| | |
| | | {selected && ( |
| | | <span |
| | | className={classNames( |
| | | 'absolute inset-y-0 right-0 flex items-center pr-4 text-text-secondary', |
| | | 'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700', |
| | | )} |
| | | > |
| | | <RiCheckLine className="h-4 w-4" aria-hidden="true" /> |
| | |
| | | )} |
| | | </> |
| | | )} |
| | | </ComboboxOption> |
| | | </Combobox.Option> |
| | | ))} |
| | | </ComboboxOptions> |
| | | </Combobox.Options> |
| | | )} |
| | | </div> |
| | | </Combobox > |
| | |
| | | }} |
| | | > |
| | | <div className={classNames('group/simple-select relative h-9', wrapperClassName)}> |
| | | {renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>} |
| | | {renderTrigger && <Listbox.Button className='w-full'>{renderTrigger(selectedItem)}</Listbox.Button>} |
| | | {!renderTrigger && ( |
| | | <ListboxButton className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> |
| | | <Listbox.Button className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> |
| | | <span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> |
| | | <span className="absolute inset-y-0 right-0 flex items-center pr-2"> |
| | | {(selectedItem && !notClearable) |
| | |
| | | setSelectedItem(null) |
| | | onSelect({ name: '', value: '' }) |
| | | }} |
| | | className="h-4 w-4 cursor-pointer text-text-quaternary" |
| | | className="h-4 w-4 text-text-quaternary cursor-pointer" |
| | | aria-hidden="false" |
| | | /> |
| | | ) |
| | |
| | | /> |
| | | )} |
| | | </span> |
| | | </ListboxButton> |
| | | </Listbox.Button> |
| | | )} |
| | | |
| | | {!disabled && ( |
| | | <ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> |
| | | {items.map((item: Item) => ( |
| | | <ListboxOption |
| | | key={item.value} |
| | | className={ |
| | | classNames( |
| | | 'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', |
| | | optionClassName, |
| | | ) |
| | | } |
| | | value={item} |
| | | disabled={disabled} |
| | | > |
| | | {({ /* active, */ selected }) => ( |
| | | <> |
| | | {renderOption |
| | | ? renderOption({ item, selected }) |
| | | : (<> |
| | | <span className={classNames('block', selected && 'font-normal')}>{item.name}</span> |
| | | {selected && !hideChecked && ( |
| | | <span |
| | | className={classNames( |
| | | 'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent', |
| | | )} |
| | | > |
| | | <RiCheckLine className="h-4 w-4" aria-hidden="true" /> |
| | | </span> |
| | | )} |
| | | </>)} |
| | | </> |
| | | )} |
| | | </ListboxOption> |
| | | ))} |
| | | </ListboxOptions> |
| | | <Transition |
| | | as={Fragment} |
| | | leave="transition ease-in duration-100" |
| | | leaveFrom="opacity-100" |
| | | leaveTo="opacity-0" |
| | | > |
| | | |
| | | <Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> |
| | | {items.map((item: Item) => ( |
| | | <Listbox.Option |
| | | key={item.value} |
| | | className={({ active }) => |
| | | classNames( |
| | | 'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', |
| | | optionClassName, |
| | | ) |
| | | } |
| | | value={item} |
| | | disabled={disabled} |
| | | > |
| | | {({ /* active, */ selected }) => ( |
| | | <> |
| | | {renderOption |
| | | ? renderOption({ item, selected }) |
| | | : (<> |
| | | <span className={classNames('block', selected && 'font-normal')}>{item.name}</span> |
| | | {selected && !hideChecked && ( |
| | | <span |
| | | className={classNames( |
| | | 'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent', |
| | | )} |
| | | > |
| | | <RiCheckLine className="h-4 w-4" aria-hidden="true" /> |
| | | </span> |
| | | )} |
| | | </>)} |
| | | </> |
| | | )} |
| | | </Listbox.Option> |
| | | ))} |
| | | </Listbox.Options> |
| | | </Transition> |
| | | )} |
| | | </div> |
| | | </Listbox> |
| | |
| | | onSelect: (value: Item) => void |
| | | items: Item[] |
| | | placeholder?: string |
| | | installedValue?: string | number |
| | | renderTrigger?: (value?: Item) => React.JSX.Element | null |
| | | renderTrigger?: (value?: Item) => JSX.Element | null |
| | | triggerClassName?: string |
| | | triggerClassNameFn?: (open: boolean) => string |
| | | popupClassName?: string |
| | |
| | | onSelect, |
| | | items, |
| | | placeholder, |
| | | installedValue, |
| | | renderTrigger, |
| | | triggerClassName, |
| | | triggerClassNameFn, |
| | |
| | | const { t } = useTranslation() |
| | | const [open, setOpen] = useState(false) |
| | | const localPlaceholder = placeholder || t('common.placeholder.select') |
| | | const selectedItem = value ? items.find(item => item.value === value) : undefined |
| | | const selectedItem = items.find(item => item.value === value) |
| | | |
| | | return ( |
| | | <PortalToFollowElem |
| | |
| | | : ( |
| | | <div |
| | | className={classNames(` |
| | | group flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-components-input-bg-normal hover:bg-state-base-hover-alt text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'} |
| | | flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-gray-100 text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'} |
| | | `, triggerClassName, triggerClassNameFn?.(open))} |
| | | title={selectedItem?.name} |
| | | > |
| | | <span |
| | | className={` |
| | | grow truncate |
| | | ${!selectedItem?.name && 'text-components-input-text-placeholder'} |
| | | ${!selectedItem?.name && 'text-gray-400'} |
| | | `} |
| | | > |
| | | {selectedItem?.name ?? localPlaceholder} |
| | | </span> |
| | | <div className='mx-0.5'>{installedValue && selectedItem && selectedItem.value !== installedValue && <Badge>{installedValue} {'->'} {selectedItem.value} </Badge>}</div> |
| | | <ChevronDownIcon className='h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary' /> |
| | | <ChevronDownIcon className='shrink-0 h-4 w-4 text-gray-400' /> |
| | | </div> |
| | | )} |
| | | |
| | | </PortalToFollowElemTrigger> |
| | | <PortalToFollowElemContent className={`z-20 ${popupClassName}`}> |
| | | <div |
| | | className={classNames('px-1 py-1 max-h-60 overflow-auto rounded-md text-base shadow-lg border-components-panel-border bg-components-panel-bg border-[0.5px] focus:outline-none sm:text-sm', popupInnerClassName)} |
| | | className={classNames('px-1 py-1 max-h-60 overflow-auto rounded-md bg-white text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm', popupInnerClassName)} |
| | | > |
| | | {items.map((item: Item) => ( |
| | | <div |
| | | key={item.value} |
| | | className={` |
| | | flex h-9 cursor-pointer items-center justify-between rounded-lg px-2.5 text-text-secondary hover:bg-state-base-hover |
| | | ${item.value === value && 'bg-state-base-hover'} |
| | | flex items-center justify-between px-2.5 h-9 cursor-pointer rounded-lg hover:bg-gray-100 text-gray-700 |
| | | ${item.value === value && 'bg-gray-100'} |
| | | `} |
| | | title={item.name} |
| | | onClick={() => { |
| | |
| | | className='w-0 grow truncate' |
| | | title={item.name} |
| | | > |
| | | <span className='truncate'>{item.name}</span> |
| | | {item.value === installedValue && ( |
| | | <Badge uppercase={true} className='ml-1 shrink-0'>INSTALLED</Badge> |
| | | )} |
| | | {item.name} |
| | | </span> |
| | | {!hideChecked && item.value === value && ( |
| | | <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' /> |
| | | <RiCheckLine className='shrink-0 h-4 w-4 text-text-accent' /> |
| | | )} |
| | | </div> |
| | | ))} |