feat(FE-Storyless): add custom control component to SelectInput for adornment support

This commit is contained in:
rstubryan
2025-10-30 12:00:22 +07:00
parent a8fee20133
commit 2307035717
+57 -35
View File
@@ -9,6 +9,8 @@ import Select, {
InputActionMeta,
MultiValue,
SingleValue,
components as ReactSelectComponents,
ControlProps,
} from 'react-select';
import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated';
@@ -64,6 +66,33 @@ interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
const animatedComponents = makeAnimated();
const CustomControl = <
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>,
>(
props: ControlProps<Option, IsMulti, Group>
) => {
const { children } = props;
const customProps = props.selectProps as unknown as {
shouldShowAdornment?: boolean;
startAdornment?: ReactNode;
};
const shouldShowAdornment = customProps.shouldShowAdornment ?? false;
const startAdornment = customProps.startAdornment;
return (
<ReactSelectComponents.Control {...props}>
<div className='flex-1 px-4! py-1.5 gap-1 flex items-center'>
{shouldShowAdornment && startAdornment}
{children}
</div>
</ReactSelectComponents.Control>
);
};
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const {
label,
@@ -94,10 +123,18 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const [internalInputValue, setInternalInputValue] = useState('');
const [debouncedInputValue] = useDebounce(internalInputValue, delay);
const shouldShowAdornment = startAdornment && !internalInputValue;
const components = useMemo(() => {
const base = isAnimated ? animatedComponents : {};
return { ...base, IndicatorSeparator: () => null };
}, [isAnimated]);
const customComponents = { ...base, IndicatorSeparator: () => null };
if (startAdornment) {
customComponents.Control = CustomControl;
}
return customComponents;
}, [isAnimated, startAdornment]);
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') setInternalInputValue(val);
@@ -156,6 +193,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
menuIsOpen={openMenu}
inputValue={internalInputValue}
onInputChange={internalInputChangeHandler}
onMenuClose={() => setInternalInputValue('')}
isMulti={isMulti}
isDisabled={isDisabled}
isLoading={isLoading}
@@ -165,17 +203,19 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
placeholder={placeholder}
className={cn('w-full', className?.select)}
classNames={{
control: ({ isFocused, isDisabled }) =>
cn(
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
{
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'border-gray-300': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
}
),
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
...(!startAdornment && {
control: ({ isFocused, isDisabled }) =>
cn(
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
{
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'border-gray-300': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
}
),
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
}),
placeholder: () =>
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
singleValue: () =>
@@ -212,29 +252,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
components={{
...components,
...(optionComponent ? { Option: optionComponent } : {}),
...(startAdornment ? {
Control: ({ children, innerRef, innerProps, menuIsOpen, isFocused, isDisabled }) => (
<div
ref={innerRef}
{...innerProps}
className={cn(
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer! flex items-center',
{
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'border-gray-300': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
}
)}
>
<div className='flex-1 px-4! gap-1 flex items-center'>
{startAdornment}
{children}
</div>
</div>
),
} : {}),
}}
{...(startAdornment && {
shouldShowAdornment,
startAdornment,
})}
menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined
}