Spaces:
Build error
Build error
'use client'; | |
import * as SelectPrimitive from '@radix-ui/react-select'; | |
import { cva } from 'class-variance-authority'; | |
import * as React from 'react'; | |
import { | |
BasicField, | |
BasicFieldOptions, | |
extractBasicFieldProps, | |
} from '../basic-field'; | |
import { cn, ComponentAnatomy, defineStyleAnatomy } from '../core/styling'; | |
import { mergeRefs } from '../core/utils'; | |
import { | |
extractInputPartProps, | |
hiddenInputStyles, | |
InputAddon, | |
InputAnatomy, | |
InputContainer, | |
InputIcon, | |
InputStyling, | |
} from '../input'; | |
/* ------------------------------------------------------------------------------------------------- | |
* Anatomy | |
* -----------------------------------------------------------------------------------------------*/ | |
export const SelectAnatomy = defineStyleAnatomy({ | |
root: cva([ | |
'UI-Select__root', | |
'inline-flex items-center justify-between relative whitespace-nowrap truncate', | |
]), | |
chevronIcon: cva([ | |
'UI-Combobox__chevronIcon', | |
'ml-2 h-4 w-4 shrink-0 opacity-50', | |
]), | |
scrollButton: cva([ | |
'UI-Select__scrollButton', | |
'flex items-center justify-center h-[25px] bg-[--paper] text-base cursor-default', | |
]), | |
content: cva([ | |
'UI-Select__content', | |
'w-full overflow-hidden rounded-[--radius] shadow-md bg-[--paper] border leading-none z-50', | |
]), | |
viewport: cva(['UI-Select__viewport', 'p-1 z-10']), | |
item: cva([ | |
'UI-Select__item', | |
'text-base leading-none rounded-[--radius] flex items-center h-8 pr-2 pl-8 relative', | |
'select-none disabled:opacity-50 disabled:pointer-events-none', | |
'data-highlighted:outline-none data-highlighted:bg-[--subtle]', | |
'data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none', | |
]), | |
checkIcon: cva([ | |
'UI-Select__checkIcon', | |
'absolute left-2 w-4 inline-flex items-center justify-center', | |
]), | |
}); | |
/* ------------------------------------------------------------------------------------------------- | |
* Select | |
* -----------------------------------------------------------------------------------------------*/ | |
export type SelectOption = { | |
value: string; | |
label?: string; | |
disabled?: boolean; | |
}; | |
export type SelectProps = InputStyling & | |
BasicFieldOptions & | |
Omit<React.ComponentPropsWithoutRef<'button'>, 'value' | 'defaultValue'> & | |
ComponentAnatomy<typeof SelectAnatomy> & { | |
/** | |
* The options to display in the dropdown | |
*/ | |
options: SelectOption[] | undefined; | |
/** | |
* The placeholder text | |
*/ | |
placeholder?: string; | |
/** | |
* Direction of the text | |
*/ | |
dir?: 'ltr' | 'rtl'; | |
/** | |
* The selected value | |
*/ | |
value?: string | undefined; | |
/** | |
* Callback fired when the selected value changes | |
*/ | |
onValueChange?: (value: string) => void; | |
/** | |
* Callback fired when the dropdown opens or closes | |
*/ | |
onOpenChange?: (open: boolean) => void; | |
/** | |
* Default selected value when uncontrolled | |
*/ | |
defaultValue?: string; | |
/** | |
* Ref to the input element | |
*/ | |
inputRef?: React.Ref<HTMLSelectElement>; | |
}; | |
export const Select = React.forwardRef<HTMLButtonElement, SelectProps>( | |
(props, ref) => { | |
const [props1, basicFieldProps] = extractBasicFieldProps<SelectProps>( | |
props, | |
React.useId() | |
); | |
const [ | |
{ | |
size, | |
intent, | |
leftAddon, | |
leftIcon, | |
rightAddon, | |
rightIcon, | |
/**/ | |
className, | |
placeholder, | |
options, | |
chevronIconClass, | |
scrollButtonClass, | |
contentClass, | |
viewportClass, | |
checkIconClass, | |
itemClass, | |
/**/ | |
dir, | |
value: controlledValue, | |
onValueChange, | |
onOpenChange, | |
defaultValue, | |
inputRef, | |
...rest | |
}, | |
{ | |
inputContainerProps, | |
leftAddonProps, | |
leftIconProps, | |
rightAddonProps, | |
rightIconProps, | |
}, | |
] = extractInputPartProps<SelectProps>({ | |
...props1, | |
size: props1.size ?? 'md', | |
intent: props1.intent ?? 'basic', | |
leftAddon: props1.leftAddon, | |
leftIcon: props1.leftIcon, | |
rightAddon: props1.rightAddon, | |
rightIcon: props1.rightIcon, | |
}); | |
const isFirst = React.useRef(true); | |
const buttonRef = React.useRef<HTMLButtonElement>(null); | |
const [_value, _setValue] = React.useState<string | undefined>( | |
controlledValue ?? defaultValue | |
); | |
const handleOnValueChange = React.useCallback((value: string) => { | |
if (value === '__placeholder__') { | |
_setValue(''); | |
onValueChange?.(''); | |
return; | |
} | |
_setValue(value); | |
onValueChange?.(value); | |
}, []); | |
React.useEffect(() => { | |
if (!defaultValue || !isFirst.current) { | |
_setValue(controlledValue); | |
} | |
isFirst.current = false; | |
}, [controlledValue]); | |
return ( | |
<BasicField {...basicFieldProps}> | |
<InputContainer {...inputContainerProps}> | |
<InputAddon {...leftAddonProps} /> | |
<InputIcon {...leftIconProps} /> | |
<SelectPrimitive.Root | |
dir={dir} | |
value={_value} | |
onValueChange={handleOnValueChange} | |
onOpenChange={onOpenChange} | |
defaultValue={defaultValue} | |
> | |
<SelectPrimitive.Trigger | |
ref={mergeRefs([buttonRef, ref])} | |
id={basicFieldProps.id} | |
className={cn( | |
InputAnatomy.root({ | |
size, | |
intent, | |
hasError: !!basicFieldProps.error, | |
isDisabled: !!basicFieldProps.disabled, | |
isReadonly: !!basicFieldProps.readonly, | |
hasRightAddon: !!rightAddon, | |
hasRightIcon: !!rightIcon, | |
hasLeftAddon: !!leftAddon, | |
hasLeftIcon: !!leftIcon, | |
}), | |
SelectAnatomy.root(), | |
className | |
)} | |
aria-label={basicFieldProps.name || 'Select'} | |
{...rest} | |
> | |
<SelectPrimitive.Value placeholder={placeholder} /> | |
<SelectPrimitive.Icon className={cn(!!rightIcon && 'hidden')}> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className={cn(SelectAnatomy.chevronIcon(), chevronIconClass)} | |
> | |
<path d="m6 9 6 6 6-6" /> | |
</svg> | |
</SelectPrimitive.Icon> | |
</SelectPrimitive.Trigger> | |
<SelectPrimitive.Portal> | |
<SelectPrimitive.Content | |
className={cn(SelectAnatomy.content(), contentClass)} | |
> | |
<SelectPrimitive.ScrollUpButton | |
className={cn( | |
SelectAnatomy.scrollButton(), | |
scrollButtonClass | |
)} | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className={cn( | |
SelectAnatomy.chevronIcon(), | |
'rotate-180', | |
chevronIconClass | |
)} | |
> | |
<path d="m6 9 6 6 6-6" /> | |
</svg> | |
</SelectPrimitive.ScrollUpButton> | |
<SelectPrimitive.Viewport | |
className={cn(SelectAnatomy.viewport(), viewportClass)} | |
> | |
{!!placeholder && !basicFieldProps.required && ( | |
<SelectPrimitive.Item | |
className={cn(SelectAnatomy.item(), itemClass)} | |
value="__placeholder__" | |
> | |
<SelectPrimitive.ItemText className="flex-none whitespace-nowrap truncate"> | |
{placeholder} | |
</SelectPrimitive.ItemText> | |
</SelectPrimitive.Item> | |
)} | |
{options?.map((option) => ( | |
<SelectPrimitive.Item | |
key={option.value} | |
className={cn(SelectAnatomy.item(), itemClass)} | |
value={option.value} | |
disabled={option.disabled} | |
data-disabled={option.disabled} | |
> | |
<SelectPrimitive.ItemText className="flex-none whitespace-nowrap truncate"> | |
{option.label} | |
</SelectPrimitive.ItemText> | |
<SelectPrimitive.ItemIndicator asChild> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className={cn( | |
SelectAnatomy.checkIcon(), | |
checkIconClass | |
)} | |
> | |
<path d="M20 6 9 17l-5-5" /> | |
</svg> | |
</SelectPrimitive.ItemIndicator> | |
</SelectPrimitive.Item> | |
))} | |
</SelectPrimitive.Viewport> | |
<SelectPrimitive.ScrollDownButton | |
className={cn( | |
SelectAnatomy.scrollButton(), | |
scrollButtonClass | |
)} | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className={cn( | |
SelectAnatomy.chevronIcon(), | |
chevronIconClass | |
)} | |
> | |
<path d="m6 9 6 6 6-6" /> | |
</svg> | |
</SelectPrimitive.ScrollDownButton> | |
</SelectPrimitive.Content> | |
</SelectPrimitive.Portal> | |
</SelectPrimitive.Root> | |
<select | |
ref={inputRef} | |
name={basicFieldProps.name} | |
className={hiddenInputStyles} | |
aria-hidden="true" | |
required={basicFieldProps.required} | |
disabled={basicFieldProps.disabled} | |
value={_value} | |
tabIndex={-1} | |
onChange={() => {}} | |
onFocusCapture={() => buttonRef.current?.focus()} | |
> | |
<option value="" /> | |
{options?.map((option) => ( | |
<option | |
key={option.value} | |
value={option.value} | |
disabled={option.disabled} | |
/> | |
))} | |
</select> | |
<InputAddon {...rightAddonProps} /> | |
<InputIcon {...rightIconProps} /> | |
</InputContainer> | |
</BasicField> | |
); | |
} | |
); | |
Select.displayName = 'Select'; | |