Skip to content

Commit

Permalink
ignore
Browse files Browse the repository at this point in the history
  • Loading branch information
johnpanos committed Aug 6, 2024
1 parent ec24292 commit 6d305c3
Showing 1 changed file with 163 additions and 87 deletions.
250 changes: 163 additions & 87 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,194 @@
// @ts-ignore
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, forwardRef, useEffect } from 'react';

Check failure on line 1 in src/components/Select/Select.tsx

View workflow job for this annotation

GitHub Actions / test

Module '"react-select"' has no exported member 'IndicatorProps'. Did you mean to use 'import IndicatorProps from "react-select"' instead?

Check failure on line 1 in src/components/Select/Select.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'CreateOptionProps'.

Check failure on line 1 in src/components/Select/Select.tsx

View workflow job for this annotation

GitHub Actions / test

Type '{ id?: string | undefined; placeholder?: ReactNode; tabIndex?: number | undefined; 'aria-errormessage'?: string | undefined; 'aria-invalid'?: boolean | "true" | "false" | "grammar" | "spelling" | undefined; ... 72 more ...; isValidNewOption: (({ label, value, options }: CreateOptionProps<...>) => boolean) | undefine...' is not assignable to type 'IntrinsicAttributes & Omit<Pick<Props<OptionType, boolean, GroupBase<OptionType>>, "className" | "id" | ... 29 more ... | "theme"> & InexactPartial<...> & InexactPartial<...>, StateManagedPropKeys> & ... 4 more ... & CreatableAdditionalProps<...>'.
import ReactSelect, {
components as ReactSelectComponents,
Props as ReactSelectProps,
OptionProps,
MultiValueProps,
IndicatorProps,
StylesConfig,
Props as SelectProps,
ActionMeta,
SingleValue,
MultiValue,
ValueContainerProps,
} from 'react-select';
import AsyncSelect from 'react-select/async';
import AsyncCreatableSelect from 'react-select/async-creatable';
import CreatableSelect from 'react-select/creatable';
import AsyncCreatableSelect from 'react-select/async-creatable';
import Icon from '../Icon/Icon';
import Badge from '../Badge/Badge';

export interface Option {
label: string;
value: any;
}
// Type definitions
type OptionType = { label: string; value: any; disabled?: boolean };

interface SelectProps extends Omit<ReactSelectProps<Option, boolean>, 'onChange'> {
options?: Option[];
defaultValue?: Option | Option[] | null;
value?: Option | Option[] | null;
onChange?: (value: Option | Option[] | null, action: ActionMeta<Option>) => void;
loadOptions?: (inputValue: string, callback: (options: Option[]) => void) => void;
interface CustomSelectProps extends Omit<SelectProps<OptionType, boolean>, 'onChange' | 'isMulti' | 'isDisabled'> {
onChange?: (value: any, action?: ActionMeta<OptionType>) => void;
arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode;
valueComponent?: React.ComponentType<MultiValueProps<OptionType>>;
optionComponent?: React.ComponentType<OptionProps<OptionType>>;
loadOptions?: (input: string, callback: (options: OptionType[]) => void) => void;
creatable?: boolean;
multi?: boolean;
name?: string;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
multi?: boolean;
disabled?: boolean;
isValidNewOption?: (inputValue: any) => boolean;
}

const Select: React.FC<SelectProps> = ({
options,
defaultValue,
value,
onChange,
loadOptions,
creatable,
multi,
name,
inputProps,
className,
...props
}) => {
const [internalValue, setInternalValue] = useState<Option | Option[] | null>(
value || defaultValue || null
// Utility functions
const getSelectArrow = (isOpen: boolean, arrowRenderer?: (props: { isOpen: boolean }) => React.ReactNode) =>
arrowRenderer ? arrowRenderer({ isOpen }) : <Icon name={`caret-${isOpen ? 'up' : 'down'}`} />;

const getCloseButton = () => (
<Icon name="xmark" className="ms-1" style={{ opacity: 0.5, fontSize: '.5rem' }} />
);

// Custom components
const CustomMultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
const { children, removeProps, ...badgeProps } = props;

return (
<Badge
color="light"
className="ms-1 fw-normal border d-inline-flex align-items-center text-start"
style={{ textTransform: 'none', whiteSpace: 'normal' }}
{...badgeProps}
>
{children}
<span {...removeProps}>
{getCloseButton()}
</span>
</Badge>
);
};

const CustomOption: React.FC<OptionProps<OptionType>> = (props) => {
const { children, isDisabled, isFocused, isSelected, innerProps, data } = props;

return (
<div
className={`
dropdown-item
${isSelected && !isFocused ? 'bg-light' : ''}
${isFocused ? 'bg-primary text-white' : ''}
${isDisabled || data.disabled ? 'disabled' : ''}
`.trim()}
{...innerProps}
aria-disabled={isDisabled || data.disabled}
>
{children}
</div>
);
const selectRef = useRef<any>(null);
const isControlled = value !== undefined;
};

const CustomArrow: React.FC<IndicatorProps<OptionType>> = ({ selectProps }) => {
const { menuIsOpen, arrowRenderer } = selectProps as CustomSelectProps;
return <>{getSelectArrow(!!menuIsOpen, arrowRenderer)}</>;
};

// Main Select component
const Select = forwardRef<any, CustomSelectProps>((props, ref) => {
const {
arrowRenderer,
className,
defaultValue,
inputProps,
valueComponent,
optionComponent,
loadOptions,
creatable,
onChange,
multi,
isValidNewOption,
value: propsValue,
options: propsOptions,
disabled,
...restProps
} = props;

const [value, setValue] = useState(propsValue || defaultValue);
const [options, setOptions] = useState(propsOptions || []);

useEffect(() => {
if (propsValue !== undefined) {
setValue(propsValue);
}
}, [propsValue]);

useEffect(() => {
if (isControlled) {
setInternalValue(value);
if (propsOptions) {
setOptions(propsOptions);
}
}, [value, isControlled]);

const handleChange = (
newValue: SingleValue<Option> | MultiValue<Option>,
action: ActionMeta<Option>
) => {
if (!isControlled) {
setInternalValue(newValue);
}, [propsOptions]);

const handleChange = (newValue: any, action: ActionMeta<OptionType>) => {
setValue(newValue);
if (onChange) {
// For multi-select, always pass an array
if (multi) {
onChange(newValue ? newValue : [], action);
} else {
onChange(newValue, action);
}
}
onChange?.(newValue, action);
};

let SelectComponent: any = ReactSelect;
if (loadOptions) {
SelectComponent = creatable ? AsyncCreatableSelect : AsyncSelect;
// Handle async options loading
const loadOptionsWrapper = loadOptions
? (inputValue: string) =>
new Promise<OptionType[]>((resolve) => {
loadOptions(inputValue, (result: any) => {
resolve(result.options || []);
});
})
: undefined;

// Determine which Select component to use
let SelectElement: typeof ReactSelect | typeof AsyncSelect | typeof CreatableSelect | typeof AsyncCreatableSelect = ReactSelect;
if (loadOptionsWrapper && creatable) {
SelectElement = AsyncCreatableSelect;
} else if (loadOptionsWrapper) {
SelectElement = AsyncSelect;
} else if (creatable) {
SelectComponent = CreatableSelect;
SelectElement = CreatableSelect;
}

const selectClassName = `Select ${multi ? 'Select--multi' : 'Select--single'} ${
loadOptions ? 'select-async' : ''
} ${className || ''}`.trim();

const CustomValueContainer = ({ children, ...props }: ValueContainerProps<Option, boolean>) => {
return (
<ReactSelectComponents.ValueContainer {...props}>
{children}
{name && <input type="hidden" name={name} value={props.getValue()[0]?.value || ''} />}
</ReactSelectComponents.ValueContainer>
);
// Custom styles
const customStyles: StylesConfig<OptionType, boolean> = {
control: (base) => ({
...base,
minHeight: '2.35rem',
}),
option: (base, state) => ({
...base,
backgroundColor: state.isDisabled ? '#f8f9fa' : base.backgroundColor,
color: state.isDisabled ? '#6c757d' : base.color,
cursor: state.isDisabled ? 'not-allowed' : 'default',
}),
};

const isValidNewOptionWrapper = isValidNewOption
? ({ label, value, options }: CreateOptionProps<OptionType>) => isValidNewOption({ label, value })
: undefined;

return (
<SelectComponent
ref={selectRef}
options={options}
value={internalValue}
onChange={handleChange}
loadOptions={loadOptions}
isMulti={multi}
className={selectClassName}
classNamePrefix="Select"
{...props}
<SelectElement
ref={ref}
className={`${className || ''} ${loadOptionsWrapper ? 'select-async' : ''}`.trim()}
components={{
...ReactSelectComponents,
Input: (inputComponentProps: any) => (
<ReactSelectComponents.Input
{...inputComponentProps}
{...inputProps}
name={inputProps?.name || name}
/>
),
ValueContainer: CustomValueContainer,
MultiValue: valueComponent || CustomMultiValue,
Option: optionComponent || CustomOption,
DropdownIndicator: CustomArrow,
}}
styles={customStyles}
inputProps={{ name: props.name, ...inputProps }}
isMulti={multi}
isDisabled={disabled}
loadOptions={loadOptionsWrapper}
onChange={handleChange}
value={value}
options={options}
isValidNewOption={isValidNewOptionWrapper}
isOptionDisabled={(option: OptionType) => !!option.disabled}
{...restProps}
/>
);
};
});

// For testing purposes
Select.Async = AsyncSelect;
Select.AsyncCreatable = AsyncCreatableSelect;
Select.Creatable = CreatableSelect;
Select.displayName = 'Select';

export default Select;

0 comments on commit 6d305c3

Please sign in to comment.