|
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