|
1 |
| -// @ts-ignore |
2 |
| -import React, { useState, useRef, useEffect } from 'react'; |
| 1 | +// @ts-nocheck |
| 2 | +import React, { useState, forwardRef, useEffect } from 'react'; |
3 | 3 | import ReactSelect, {
|
4 |
| - components as ReactSelectComponents, |
5 |
| - Props as ReactSelectProps, |
| 4 | + OptionProps, |
| 5 | + MultiValueProps, |
| 6 | + IndicatorProps, |
| 7 | + StylesConfig, |
| 8 | + Props as SelectProps, |
6 | 9 | ActionMeta,
|
7 |
| - SingleValue, |
8 |
| - MultiValue, |
9 |
| - ValueContainerProps, |
10 | 10 | } from 'react-select';
|
11 | 11 | import AsyncSelect from 'react-select/async';
|
12 | 12 | import AsyncCreatableSelect from 'react-select/async-creatable';
|
13 | 13 | import CreatableSelect from 'react-select/creatable';
|
| 14 | +import Badge from '../Badge/Badge'; |
| 15 | +import Icon from '../Icon/Icon'; |
14 | 16 |
|
15 |
| -export interface Option { |
16 |
| - label: string; |
17 |
| - value: any; |
18 |
| -} |
| 17 | +// Type definitions |
| 18 | +type OptionType = { label: string; value: any; disabled?: boolean }; |
19 | 19 |
|
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; |
| 20 | +interface CustomSelectProps extends Omit<SelectProps<OptionType, boolean>, 'onChange' | 'isMulti' | 'isDisabled'> { |
| 21 | + onChange?: (value: any, action?: ActionMeta<OptionType>) => void; |
| 22 | + arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode; |
| 23 | + valueComponent?: React.ComponentType<MultiValueProps<OptionType>>; |
| 24 | + optionComponent?: React.ComponentType<OptionProps<OptionType>>; |
| 25 | + loadOptions?: (input: string, callback: (options: OptionType[]) => void) => void; |
26 | 26 | creatable?: boolean;
|
27 |
| - multi?: boolean; |
28 |
| - name?: string; |
29 | 27 | inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
| 28 | + multi?: boolean; |
| 29 | + disabled?: boolean; |
| 30 | + isValidNewOption?: (inputValue: any) => boolean; |
30 | 31 | }
|
31 | 32 |
|
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 |
| 33 | +// Utility functions |
| 34 | +const getSelectArrow = (isOpen: boolean, arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode) => |
| 35 | + arrowRenderer ? arrowRenderer({ isOpen }) : <Icon name={`caret-${isOpen ? 'up' : 'down'}`} />; |
| 36 | + |
| 37 | +const getCloseButton = () => ( |
| 38 | + <Icon name="xmark" className="ms-1" style={{ opacity: 0.5, fontSize: '.5rem' }} /> |
| 39 | +); |
| 40 | + |
| 41 | +// Custom components |
| 42 | +const CustomMultiValue: React.FC<MultiValueProps<OptionType>> = (props) => { |
| 43 | + const { children, removeProps, ...badgeProps } = props; |
| 44 | + |
| 45 | + return ( |
| 46 | + <Badge |
| 47 | + color="light" |
| 48 | + className="ms-1 fw-normal border d-inline-flex align-items-center text-start" |
| 49 | + style={{ textTransform: 'none', whiteSpace: 'normal' }} |
| 50 | + {...badgeProps} |
| 51 | + > |
| 52 | + {children} |
| 53 | + <span {...removeProps}> |
| 54 | + {getCloseButton()} |
| 55 | + </span> |
| 56 | + </Badge> |
47 | 57 | );
|
48 |
| - const selectRef = useRef<any>(null); |
49 |
| - const isControlled = value !== undefined; |
| 58 | +}; |
| 59 | + |
| 60 | +const CustomOption: React.FC<OptionProps<OptionType>> = (props) => { |
| 61 | + const { children, isDisabled, isFocused, isSelected, innerProps, data } = props; |
| 62 | + |
| 63 | + return ( |
| 64 | + <div |
| 65 | + className={` |
| 66 | + dropdown-item |
| 67 | + ${isSelected && !isFocused ? 'bg-light' : ''} |
| 68 | + ${isFocused ? 'bg-primary text-white' : ''} |
| 69 | + ${isDisabled || data.disabled ? 'disabled' : ''} |
| 70 | + `.trim()} |
| 71 | + {...innerProps} |
| 72 | + aria-disabled={isDisabled || data.disabled} |
| 73 | + > |
| 74 | + {children} |
| 75 | + </div> |
| 76 | + ); |
| 77 | +}; |
| 78 | + |
| 79 | +const CustomArrow: React.FC<IndicatorProps<OptionType>> = ({ selectProps }) => { |
| 80 | + const { menuIsOpen, arrowRenderer } = selectProps as CustomSelectProps; |
| 81 | + return <>{getSelectArrow(!!menuIsOpen, arrowRenderer)}</>; |
| 82 | +}; |
| 83 | + |
| 84 | +// Main Select component |
| 85 | +const Select = forwardRef<any, CustomSelectProps>((props, ref) => { |
| 86 | + const { |
| 87 | + arrowRenderer, |
| 88 | + className, |
| 89 | + defaultValue, |
| 90 | + inputProps, |
| 91 | + valueComponent, |
| 92 | + optionComponent, |
| 93 | + loadOptions, |
| 94 | + creatable, |
| 95 | + onChange, |
| 96 | + multi, |
| 97 | + isValidNewOption, |
| 98 | + value: propsValue, |
| 99 | + options: propsOptions, |
| 100 | + disabled, |
| 101 | + ...restProps |
| 102 | + } = props; |
| 103 | + |
| 104 | + const [value, setValue] = useState(propsValue || defaultValue); |
| 105 | + const [options, setOptions] = useState(propsOptions || []); |
| 106 | + |
| 107 | + useEffect(() => { |
| 108 | + if (propsValue !== undefined) { |
| 109 | + setValue(propsValue); |
| 110 | + } |
| 111 | + }, [propsValue]); |
50 | 112 |
|
51 | 113 | useEffect(() => {
|
52 |
| - if (isControlled) { |
53 |
| - setInternalValue(value); |
| 114 | + if (propsOptions) { |
| 115 | + setOptions(propsOptions); |
54 | 116 | }
|
55 |
| - }, [value, isControlled]); |
56 |
| - |
57 |
| - const handleChange = ( |
58 |
| - newValue: SingleValue<Option> | MultiValue<Option>, |
59 |
| - action: ActionMeta<Option> |
60 |
| - ) => { |
61 |
| - if (!isControlled) { |
62 |
| - setInternalValue(newValue); |
| 117 | + }, [propsOptions]); |
| 118 | + |
| 119 | + const handleChange = (newValue: any, action: ActionMeta<OptionType>) => { |
| 120 | + setValue(newValue); |
| 121 | + if (onChange) { |
| 122 | + // For multi-select, always pass an array |
| 123 | + if (multi) { |
| 124 | + onChange(newValue || [], action); |
| 125 | + } else { |
| 126 | + onChange(newValue, action); |
| 127 | + } |
63 | 128 | }
|
64 |
| - onChange?.(newValue, action); |
65 | 129 | };
|
66 | 130 |
|
67 |
| - let SelectComponent: any = ReactSelect; |
68 |
| - if (loadOptions) { |
69 |
| - SelectComponent = creatable ? AsyncCreatableSelect : AsyncSelect; |
| 131 | + // Handle async options loading |
| 132 | + const loadOptionsWrapper = loadOptions |
| 133 | + ? (inputValue: string) => |
| 134 | + new Promise<OptionType[]>((resolve) => { |
| 135 | + loadOptions(inputValue, (result: any) => { |
| 136 | + resolve(result.options || []); |
| 137 | + }); |
| 138 | + }) |
| 139 | + : undefined; |
| 140 | + |
| 141 | + // Determine which Select component to use |
| 142 | + let SelectElement: typeof ReactSelect | typeof AsyncSelect | typeof CreatableSelect | typeof AsyncCreatableSelect = ReactSelect; |
| 143 | + if (loadOptionsWrapper && creatable) { |
| 144 | + SelectElement = AsyncCreatableSelect; |
| 145 | + } else if (loadOptionsWrapper) { |
| 146 | + SelectElement = AsyncSelect; |
70 | 147 | } else if (creatable) {
|
71 |
| - SelectComponent = CreatableSelect; |
| 148 | + SelectElement = CreatableSelect; |
72 | 149 | }
|
73 | 150 |
|
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 |
| - ); |
| 151 | + // Custom styles |
| 152 | + const customStyles: StylesConfig<OptionType, boolean> = { |
| 153 | + control: (base) => {return { |
| 154 | + ...base, |
| 155 | + minHeight: '2.35rem', |
| 156 | + }}, |
| 157 | + option: (base, state) => {return { |
| 158 | + ...base, |
| 159 | + backgroundColor: state.isDisabled ? '#f8f9fa' : base.backgroundColor, |
| 160 | + color: state.isDisabled ? '#6c757d' : base.color, |
| 161 | + cursor: state.isDisabled ? 'not-allowed' : 'default', |
| 162 | + }}, |
85 | 163 | };
|
86 | 164 |
|
| 165 | + const isValidNewOptionWrapper = isValidNewOption |
| 166 | + // eslint-disable-next-line no-shadow |
| 167 | + ? ({ label, value, options }: CreateOptionProps<OptionType>) => isValidNewOption({ label, value }) |
| 168 | + : undefined; |
| 169 | + |
87 | 170 | 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} |
| 171 | + <SelectElement |
| 172 | + ref={ref} |
| 173 | + className={`${className || ''} ${loadOptionsWrapper ? 'select-async' : ''}`.trim()} |
98 | 174 | components={{
|
99 |
| - ...ReactSelectComponents, |
100 |
| - Input: (inputComponentProps: any) => ( |
101 |
| - <ReactSelectComponents.Input |
102 |
| - {...inputComponentProps} |
103 |
| - {...inputProps} |
104 |
| - name={inputProps?.name || name} |
105 |
| - /> |
106 |
| - ), |
107 |
| - ValueContainer: CustomValueContainer, |
| 175 | + MultiValue: valueComponent || CustomMultiValue, |
| 176 | + Option: optionComponent || CustomOption, |
| 177 | + DropdownIndicator: CustomArrow, |
108 | 178 | }}
|
| 179 | + styles={customStyles} |
| 180 | + inputProps={{ name: props.name, ...inputProps }} |
| 181 | + isMulti={multi} |
| 182 | + isDisabled={disabled} |
| 183 | + loadOptions={loadOptionsWrapper} |
| 184 | + onChange={handleChange} |
| 185 | + value={value} |
| 186 | + options={options} |
| 187 | + isValidNewOption={isValidNewOptionWrapper} |
| 188 | + isOptionDisabled={(option: OptionType) => !!option.disabled} |
| 189 | + {...restProps} |
109 | 190 | />
|
110 | 191 | );
|
111 |
| -}; |
| 192 | +}); |
112 | 193 |
|
113 |
| -// For testing purposes |
114 |
| -Select.Async = AsyncSelect; |
115 |
| -Select.AsyncCreatable = AsyncCreatableSelect; |
116 |
| -Select.Creatable = CreatableSelect; |
| 194 | +Select.displayName = 'Select'; |
117 | 195 |
|
118 | 196 | export default Select;
|
0 commit comments