Files
DABEEO-DETECTION-APPLICATION/web-app/app/shared/components/select/Select.tsx

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>
);
};