106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { type Ref } from 'react';
|
|
import {
|
|
Button as AriaButton,
|
|
Select as AriaSelect,
|
|
type SelectProps as AriaSelectProps,
|
|
FieldError,
|
|
ListBox,
|
|
ListBoxItem,
|
|
Popover,
|
|
SelectValue,
|
|
} from 'react-aria-components';
|
|
|
|
import clsx from 'clsx';
|
|
import { tv } from 'tailwind-variants';
|
|
|
|
export type SelectOption = {
|
|
label: string;
|
|
value: string | number;
|
|
isDisabled?: boolean;
|
|
};
|
|
|
|
const selectStyle = tv({
|
|
base: 'flex items-center text-start gap-4 w-full bg-white text-sm text-dabeeo-black-34 border border-dabeeo-gray-be cursor-default px-3 h-9 min-w-[120px] outline-0',
|
|
variants: {
|
|
isDisabled: {
|
|
true: 'text-dabeeo-gray-99 bg-dabeeo-gray-eb border-dabeeo-gray-be cursor-not-allowed',
|
|
false:
|
|
'hover:text-dabeeo-black-34 hover:border-dabeeo-black-34 data-pressed:text-dabeeo-black-34 data-pressed:border-dabeeo-black-34',
|
|
},
|
|
isReadOnly: {
|
|
true: 'text-dabeeo-black-34 bg-dabeeo-yellow-secondary cursor-auto',
|
|
},
|
|
isFocused: {
|
|
true: 'border-dabeeo-black-34',
|
|
},
|
|
},
|
|
});
|
|
|
|
export interface SelectProps<T extends SelectOption>
|
|
extends Omit<AriaSelectProps<T>, 'selectionMode' | 'onChange' | 'children'> {
|
|
items?: Iterable<T>;
|
|
onChange?: (value: T['value']) => void;
|
|
maxHeight?: number;
|
|
placement?: 'bottom' | 'top';
|
|
ref?: Ref<HTMLButtonElement>;
|
|
isReadOnly?: boolean;
|
|
}
|
|
|
|
export const Select = <T extends SelectOption>(props: SelectProps<T>) => {
|
|
const { className, items, onChange, maxHeight, placement = 'bottom', ref, isReadOnly, ...restProps } = props;
|
|
|
|
return (
|
|
<AriaSelect
|
|
selectionMode="single"
|
|
aria-label="선택"
|
|
className={clsx('group/select', className)}
|
|
{...restProps}
|
|
onChange={(key) => {
|
|
onChange?.(key as T['value']);
|
|
}}
|
|
>
|
|
<AriaButton
|
|
ref={ref}
|
|
className={(renderProps) =>
|
|
selectStyle({ ...renderProps, isDisabled: props.isDisabled || isReadOnly, isReadOnly })}
|
|
{...{ isDisabled: props.isDisabled || isReadOnly }}
|
|
>
|
|
<SelectValue className="flex-1 text-sm data-placeholder:text-dabeeo-gray-99">
|
|
{({ selectedText, defaultChildren }) => selectedText || defaultChildren}
|
|
</SelectValue>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="10"
|
|
height="5"
|
|
viewBox="0 0 10 5"
|
|
fill="none"
|
|
className={clsx('group-data-open/select:rotate-180 transition', isReadOnly && 'hidden')}
|
|
>
|
|
<path d="M0 0H10L5 5L0 0Z" fill="#BEBEBE" />
|
|
</svg>
|
|
</AriaButton>
|
|
<FieldError className="mt-0.5 text-dabeeo-red text-xs" />
|
|
<Popover className="w-(--trigger-width)" offset={0.5} placement={placement}>
|
|
<ListBox
|
|
items={items}
|
|
style={{ maxHeight: maxHeight ? `${maxHeight}px` : 'inherit' }}
|
|
className={clsx(
|
|
'w-full outline-0 box-border border-l border-r border-dabeeo-gray-be max-h-[inherit] bg-white overflow-auto',
|
|
placement === 'top' ? 'border-t' : 'border-b'
|
|
)}
|
|
>
|
|
{(item) => (
|
|
<ListBoxItem
|
|
id={item.value}
|
|
className="outline-0 px-3 py-2 text-sm font-semibold text-dabeeo-black-34 data-selected:text-dabeeo-navy-main data-selected:bg-dabeeo-yellow-secondary data-selected:font-bold data-selected:hover:bg-dabeeo-gray-eb focus:bg-dabeeo-gray-eb hover:bg-dabeeo-gray-eb data-disabled:text-dabeeo-gray-be"
|
|
isDisabled={!!item.isDisabled}
|
|
>
|
|
{item.label}
|
|
</ListBoxItem>
|
|
)}
|
|
</ListBox>
|
|
</Popover>
|
|
</AriaSelect>
|
|
);
|
|
};
|