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, InputActionMeta,
MultiValue, MultiValue,
SingleValue, SingleValue,
components as ReactSelectComponents,
ControlProps,
} from 'react-select'; } from 'react-select';
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
@@ -64,6 +66,33 @@ interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
const animatedComponents = makeAnimated(); 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 SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const { const {
label, label,
@@ -94,10 +123,18 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const [internalInputValue, setInternalInputValue] = useState(''); const [internalInputValue, setInternalInputValue] = useState('');
const [debouncedInputValue] = useDebounce(internalInputValue, delay); const [debouncedInputValue] = useDebounce(internalInputValue, delay);
const shouldShowAdornment = startAdornment && !internalInputValue;
const components = useMemo(() => { const components = useMemo(() => {
const base = isAnimated ? animatedComponents : {}; const base = isAnimated ? animatedComponents : {};
return { ...base, IndicatorSeparator: () => null }; const customComponents = { ...base, IndicatorSeparator: () => null };
}, [isAnimated]);
if (startAdornment) {
customComponents.Control = CustomControl;
}
return customComponents;
}, [isAnimated, startAdornment]);
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => { const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') setInternalInputValue(val); if (meta.action === 'input-change') setInternalInputValue(val);
@@ -156,6 +193,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
menuIsOpen={openMenu} menuIsOpen={openMenu}
inputValue={internalInputValue} inputValue={internalInputValue}
onInputChange={internalInputChangeHandler} onInputChange={internalInputChangeHandler}
onMenuClose={() => setInternalInputValue('')}
isMulti={isMulti} isMulti={isMulti}
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoading} isLoading={isLoading}
@@ -165,17 +203,19 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
placeholder={placeholder} placeholder={placeholder}
className={cn('w-full', className?.select)} className={cn('w-full', className?.select)}
classNames={{ classNames={{
control: ({ isFocused, isDisabled }) => ...(!startAdornment && {
cn( control: ({ isFocused, isDisabled }) =>
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!', 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-red-500! ring-2 ring-red-200': isError,
'border-gray-300': !isError && !isFocused, 'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled, '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'), ),
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
}),
placeholder: () => placeholder: () =>
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }), cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
singleValue: () => singleValue: () =>
@@ -212,29 +252,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
components={{ components={{
...components, ...components,
...(optionComponent ? { Option: optionComponent } : {}), ...(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={ menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined typeof document !== 'undefined' ? document.body : undefined
} }