Skip to content

Commit 6d305c3

Browse files
committed
ignore
1 parent ec24292 commit 6d305c3

1 file changed

Lines changed: 163 additions & 87 deletions

File tree

src/components/Select/Select.tsx

Lines changed: 163 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,194 @@
1-
// @ts-ignore
2-
import React, { useState, useRef, useEffect } from 'react';
1+
import React, { useState, forwardRef, useEffect } from 'react';
32
import ReactSelect, {
4-
components as ReactSelectComponents,
5-
Props as ReactSelectProps,
3+
OptionProps,
4+
MultiValueProps,
5+
IndicatorProps,
6+
StylesConfig,
7+
Props as SelectProps,
68
ActionMeta,
7-
SingleValue,
8-
MultiValue,
9-
ValueContainerProps,
109
} from 'react-select';
1110
import AsyncSelect from 'react-select/async';
12-
import AsyncCreatableSelect from 'react-select/async-creatable';
1311
import CreatableSelect from 'react-select/creatable';
12+
import AsyncCreatableSelect from 'react-select/async-creatable';
13+
import Icon from '../Icon/Icon';
14+
import Badge from '../Badge/Badge';
1415

15-
export interface Option {
16-
label: string;
17-
value: any;
18-
}
16+
// Type definitions
17+
type OptionType = { label: string; value: any; disabled?: boolean };
1918

20-
interface SelectProps extends Omit<ReactSelectProps<Option, boolean>, 'onChange'> {
21-
options?: Option[];
22-
defaultValue?: Option | Option[] | null;
23-
value?: Option | Option[] | null;
24-
onChange?: (value: Option | Option[] | null, action: ActionMeta<Option>) => void;
25-
loadOptions?: (inputValue: string, callback: (options: Option[]) => void) => void;
19+
interface CustomSelectProps extends Omit<SelectProps<OptionType, boolean>, 'onChange' | 'isMulti' | 'isDisabled'> {
20+
onChange?: (value: any, action?: ActionMeta<OptionType>) => void;
21+
arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode;
22+
valueComponent?: React.ComponentType<MultiValueProps<OptionType>>;
23+
optionComponent?: React.ComponentType<OptionProps<OptionType>>;
24+
loadOptions?: (input: string, callback: (options: OptionType[]) => void) => void;
2625
creatable?: boolean;
27-
multi?: boolean;
28-
name?: string;
2926
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
27+
multi?: boolean;
28+
disabled?: boolean;
29+
isValidNewOption?: (inputValue: any) => boolean;
3030
}
3131

32-
const Select: React.FC<SelectProps> = ({
33-
options,
34-
defaultValue,
35-
value,
36-
onChange,
37-
loadOptions,
38-
creatable,
39-
multi,
40-
name,
41-
inputProps,
42-
className,
43-
...props
44-
}) => {
45-
const [internalValue, setInternalValue] = useState<Option | Option[] | null>(
46-
value || defaultValue || null
32+
// Utility functions
33+
const getSelectArrow = (isOpen: boolean, arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode) =>
34+
arrowRenderer ? arrowRenderer({ isOpen }) : <Icon name={`caret-${isOpen ? 'up' : 'down'}`} />;
35+
36+
const getCloseButton = () => (
37+
<Icon name="xmark" className="ms-1" style={{ opacity: 0.5, fontSize: '.5rem' }} />
38+
);
39+
40+
// Custom components
41+
const CustomMultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
42+
const { children, removeProps, ...badgeProps } = props;
43+
44+
return (
45+
<Badge
46+
color="light"
47+
className="ms-1 fw-normal border d-inline-flex align-items-center text-start"
48+
style={{ textTransform: 'none', whiteSpace: 'normal' }}
49+
{...badgeProps}
50+
>
51+
{children}
52+
<span {...removeProps}>
53+
{getCloseButton()}
54+
</span>
55+
</Badge>
56+
);
57+
};
58+
59+
const CustomOption: React.FC<OptionProps<OptionType>> = (props) => {
60+
const { children, isDisabled, isFocused, isSelected, innerProps, data } = props;
61+
62+
return (
63+
<div
64+
className={`
65+
dropdown-item
66+
${isSelected && !isFocused ? 'bg-light' : ''}
67+
${isFocused ? 'bg-primary text-white' : ''}
68+
${isDisabled || data.disabled ? 'disabled' : ''}
69+
`.trim()}
70+
{...innerProps}
71+
aria-disabled={isDisabled || data.disabled}
72+
>
73+
{children}
74+
</div>
4775
);
48-
const selectRef = useRef<any>(null);
49-
const isControlled = value !== undefined;
76+
};
77+
78+
const CustomArrow: React.FC<IndicatorProps<OptionType>> = ({ selectProps }) => {
79+
const { menuIsOpen, arrowRenderer } = selectProps as CustomSelectProps;
80+
return <>{getSelectArrow(!!menuIsOpen, arrowRenderer)}</>;
81+
};
82+
83+
// Main Select component
84+
const Select = forwardRef<any, CustomSelectProps>((props, ref) => {
85+
const {
86+
arrowRenderer,
87+
className,
88+
defaultValue,
89+
inputProps,
90+
valueComponent,
91+
optionComponent,
92+
loadOptions,
93+
creatable,
94+
onChange,
95+
multi,
96+
isValidNewOption,
97+
value: propsValue,
98+
options: propsOptions,
99+
disabled,
100+
...restProps
101+
} = props;
102+
103+
const [value, setValue] = useState(propsValue || defaultValue);
104+
const [options, setOptions] = useState(propsOptions || []);
105+
106+
useEffect(() => {
107+
if (propsValue !== undefined) {
108+
setValue(propsValue);
109+
}
110+
}, [propsValue]);
50111

51112
useEffect(() => {
52-
if (isControlled) {
53-
setInternalValue(value);
113+
if (propsOptions) {
114+
setOptions(propsOptions);
54115
}
55-
}, [value, isControlled]);
56-
57-
const handleChange = (
58-
newValue: SingleValue<Option> | MultiValue<Option>,
59-
action: ActionMeta<Option>
60-
) => {
61-
if (!isControlled) {
62-
setInternalValue(newValue);
116+
}, [propsOptions]);
117+
118+
const handleChange = (newValue: any, action: ActionMeta<OptionType>) => {
119+
setValue(newValue);
120+
if (onChange) {
121+
// For multi-select, always pass an array
122+
if (multi) {
123+
onChange(newValue ? newValue : [], action);
124+
} else {
125+
onChange(newValue, action);
126+
}
63127
}
64-
onChange?.(newValue, action);
65128
};
66129

67-
let SelectComponent: any = ReactSelect;
68-
if (loadOptions) {
69-
SelectComponent = creatable ? AsyncCreatableSelect : AsyncSelect;
130+
// Handle async options loading
131+
const loadOptionsWrapper = loadOptions
132+
? (inputValue: string) =>
133+
new Promise<OptionType[]>((resolve) => {
134+
loadOptions(inputValue, (result: any) => {
135+
resolve(result.options || []);
136+
});
137+
})
138+
: undefined;
139+
140+
// Determine which Select component to use
141+
let SelectElement: typeof ReactSelect | typeof AsyncSelect | typeof CreatableSelect | typeof AsyncCreatableSelect = ReactSelect;
142+
if (loadOptionsWrapper && creatable) {
143+
SelectElement = AsyncCreatableSelect;
144+
} else if (loadOptionsWrapper) {
145+
SelectElement = AsyncSelect;
70146
} else if (creatable) {
71-
SelectComponent = CreatableSelect;
147+
SelectElement = CreatableSelect;
72148
}
73149

74-
const selectClassName = `Select ${multi ? 'Select--multi' : 'Select--single'} ${
75-
loadOptions ? 'select-async' : ''
76-
} ${className || ''}`.trim();
77-
78-
const CustomValueContainer = ({ children, ...props }: ValueContainerProps<Option, boolean>) => {
79-
return (
80-
<ReactSelectComponents.ValueContainer {...props}>
81-
{children}
82-
{name && <input type="hidden" name={name} value={props.getValue()[0]?.value || ''} />}
83-
</ReactSelectComponents.ValueContainer>
84-
);
150+
// Custom styles
151+
const customStyles: StylesConfig<OptionType, boolean> = {
152+
control: (base) => ({
153+
...base,
154+
minHeight: '2.35rem',
155+
}),
156+
option: (base, state) => ({
157+
...base,
158+
backgroundColor: state.isDisabled ? '#f8f9fa' : base.backgroundColor,
159+
color: state.isDisabled ? '#6c757d' : base.color,
160+
cursor: state.isDisabled ? 'not-allowed' : 'default',
161+
}),
85162
};
86163

164+
const isValidNewOptionWrapper = isValidNewOption
165+
? ({ label, value, options }: CreateOptionProps<OptionType>) => isValidNewOption({ label, value })
166+
: undefined;
167+
87168
return (
88-
<SelectComponent
89-
ref={selectRef}
90-
options={options}
91-
value={internalValue}
92-
onChange={handleChange}
93-
loadOptions={loadOptions}
94-
isMulti={multi}
95-
className={selectClassName}
96-
classNamePrefix="Select"
97-
{...props}
169+
<SelectElement
170+
ref={ref}
171+
className={`${className || ''} ${loadOptionsWrapper ? 'select-async' : ''}`.trim()}
98172
components={{
99-
...ReactSelectComponents,
100-
Input: (inputComponentProps: any) => (
101-
<ReactSelectComponents.Input
102-
{...inputComponentProps}
103-
{...inputProps}
104-
name={inputProps?.name || name}
105-
/>
106-
),
107-
ValueContainer: CustomValueContainer,
173+
MultiValue: valueComponent || CustomMultiValue,
174+
Option: optionComponent || CustomOption,
175+
DropdownIndicator: CustomArrow,
108176
}}
177+
styles={customStyles}
178+
inputProps={{ name: props.name, ...inputProps }}
179+
isMulti={multi}
180+
isDisabled={disabled}
181+
loadOptions={loadOptionsWrapper}
182+
onChange={handleChange}
183+
value={value}
184+
options={options}
185+
isValidNewOption={isValidNewOptionWrapper}
186+
isOptionDisabled={(option: OptionType) => !!option.disabled}
187+
{...restProps}
109188
/>
110189
);
111-
};
190+
});
112191

113-
// For testing purposes
114-
Select.Async = AsyncSelect;
115-
Select.AsyncCreatable = AsyncCreatableSelect;
116-
Select.Creatable = CreatableSelect;
192+
Select.displayName = 'Select';
117193

118194
export default Select;

0 commit comments

Comments
 (0)