brunner56's picture
implement app
0bfe2e3
'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';