-
-
Save ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 to your computer and use it in GitHub Desktop.
/* ----------- simple-select.js ----------- */ | |
import * as React from 'react'; | |
import Select from 'react-select'; | |
import type { Props } from 'react-select'; | |
import { defaultClassNames, defaultStyles } from './helper'; | |
import { | |
ClearIndicator, | |
DropdownIndicator, | |
MultiValueRemove, | |
Option | |
} from './components'; | |
const SimpleSelect = React.forwardRef((props: Props, ref) => { | |
const { | |
value, | |
onChange, | |
options = [], | |
styles = defaultStyles, | |
classNames = defaultClassNames, | |
components = {}, | |
...rest | |
} = props; | |
return ( | |
<Select | |
ref={ref} | |
value={value} | |
onChange={onChange} | |
options={options} | |
unstyled | |
components={{ | |
DropdownIndicator, | |
ClearIndicator, | |
MultiValueRemove, | |
Option, | |
...components | |
}} | |
styles={styles} | |
classNames={classNames} | |
{...rest} | |
/> | |
); | |
}); | |
export default SimpleSelect; | |
/* ----------- helper.js ----------- */ | |
import { cn } from 'lib/utils'; | |
/** | |
* styles that aligns with shadcn/ui | |
*/ | |
const controlStyles = { | |
base: 'flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer', | |
focus: 'outline-none ring-1 ring-ring', | |
disabled: 'cursor-not-allowed opacity-50' | |
}; | |
const placeholderStyles = 'text-sm text-muted-foreground'; | |
const valueContainerStyles = 'gap-1'; | |
const multiValueStyles = | |
'inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'; | |
const indicatorsContainerStyles = 'gap-1'; | |
const clearIndicatorStyles = 'p-1 rounded-md'; | |
const indicatorSeparatorStyles = 'bg-border'; | |
const dropdownIndicatorStyles = 'p-1 rounded-md'; | |
const menuStyles = | |
'p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground'; | |
const groupHeadingStyles = | |
'py-2 px-1 text-secondary-foreground text-sm font-semibold'; | |
const optionStyles = { | |
base: 'hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans', | |
focus: 'active:bg-accent/90 bg-accent text-accent-foreground', | |
disabled: 'pointer-events-none opacity-50', | |
selected: '' | |
}; | |
const noOptionsMessageStyles = | |
'text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm'; | |
const loadingIndicatorStyles = | |
'flex items-center justify-center h-4 w-4 opacity-50'; | |
const loadingMessageStyles = 'text-accent-foreground p-2 bg-accent'; | |
/** | |
* This factory method is used to build custom classNames configuration | |
*/ | |
export const createClassNames = (classNames) => { | |
return { | |
clearIndicator: (state) => | |
cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)), | |
container: (state) => cn(classNames?.container?.(state)), | |
control: (state) => | |
cn( | |
controlStyles.base, | |
state.isDisabled && controlStyles.disabled, | |
state.isFocused && controlStyles.focus, | |
classNames?.control?.(state) | |
), | |
dropdownIndicator: (state) => | |
cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)), | |
group: (state) => cn(classNames?.group?.(state)), | |
groupHeading: (state) => | |
cn(groupHeadingStyles, classNames?.groupHeading?.(state)), | |
indicatorsContainer: (state) => | |
cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)), | |
indicatorSeparator: (state) => | |
cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)), | |
input: (state) => cn(classNames?.input?.(state)), | |
loadingIndicator: (state) => | |
cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)), | |
loadingMessage: (state) => | |
cn(loadingMessageStyles, classNames?.loadingMessage?.(state)), | |
menu: (state) => cn(menuStyles, classNames?.menu?.(state)), | |
menuList: (state) => cn(classNames?.menuList?.(state)), | |
menuPortal: (state) => cn(classNames?.menuPortal?.(state)), | |
multiValue: (state) => | |
cn(multiValueStyles, classNames?.multiValue?.(state)), | |
multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)), | |
multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)), | |
noOptionsMessage: (state) => | |
cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)), | |
option: (state) => | |
cn( | |
optionStyles.base, | |
state.isFocused && optionStyles.focus, | |
state.isDisabled && optionStyles.disabled, | |
state.isSelected && optionStyles.selected, | |
classNames?.option?.(state) | |
), | |
placeholder: (state) => | |
cn(placeholderStyles, classNames?.placeholder?.(state)), | |
singleValue: (state) => cn(classNames?.singleValue?.(state)), | |
valueContainer: (state) => | |
cn(valueContainerStyles, classNames?.valueContainer?.(state)) | |
}; | |
}; | |
export const defaultClassNames = createClassNames({}); | |
export const defaultStyles = { | |
input: (base) => ({ | |
...base, | |
'input:focus': { | |
boxShadow: 'none' | |
} | |
}), | |
multiValueLabel: (base) => ({ | |
...base, | |
whiteSpace: 'normal', | |
overflow: 'visible' | |
}), | |
control: (base) => ({ | |
...base, | |
transition: 'none' | |
// minHeight: '2.25rem', // we used !min-h-9 instead | |
}), | |
menuList: (base) => ({ | |
...base, | |
'::-webkit-scrollbar': { | |
background: 'transparent' | |
}, | |
'::-webkit-scrollbar-track': { | |
background: 'transparent' | |
}, | |
'::-webkit-scrollbar-thumb': { | |
background: 'hsl(var(--border))' | |
}, | |
'::-webkit-scrollbar-thumb:hover': { | |
background: 'transparent' | |
} | |
}) | |
}; | |
/* ----------- components.jsx ----------- */ | |
import * as React from 'react'; | |
import type { | |
ClearIndicatorProps, | |
DropdownIndicatorProps, | |
MultiValueRemoveProps, | |
OptionProps | |
} from 'react-select'; | |
import { components } from 'react-select'; | |
import { | |
CaretSortIcon, | |
CheckIcon, | |
Cross2Icon as CloseIcon | |
} from '@radix-ui/react-icons'; | |
export const DropdownIndicator = (props: DropdownIndicatorProps) => { | |
return ( | |
<components.DropdownIndicator {...props}> | |
<CaretSortIcon className={'h-4 w-4 opacity-50'} /> | |
</components.DropdownIndicator> | |
); | |
}; | |
export const ClearIndicator = (props: ClearIndicatorProps) => { | |
return ( | |
<components.ClearIndicator {...props}> | |
<CloseIcon className={'h-3.5 w-3.5 opacity-50'} /> | |
</components.ClearIndicator> | |
); | |
}; | |
export const MultiValueRemove = (props: MultiValueRemoveProps) => { | |
return ( | |
<components.MultiValueRemove {...props}> | |
<CloseIcon className={'h-3 w-3 opacity-50'} /> | |
</components.MultiValueRemove> | |
); | |
}; | |
export const Option = (props: OptionProps) => { | |
return ( | |
<components.Option {...props}> | |
<div className="flex items-center justify-between"> | |
<div>{props.data.label}</div> | |
{props.isSelected && <CheckIcon />} | |
</div> | |
</components.Option> | |
); | |
}; | |
/* ----------- async-select.jsx ----------- */ | |
import * as React from 'react'; | |
import Async from 'react-select/async'; | |
import type { Props } from 'react-select/async'; | |
import { defaultClassNames, defaultStyles } from './helper'; | |
import { | |
ClearIndicator, | |
DropdownIndicator, | |
MultiValueRemove, | |
Option | |
} from './components'; | |
const AsyncSelect = React.forwardRef((props: Props, ref) => { | |
const { | |
value, | |
onChange, | |
styles = defaultStyles, | |
classNames = defaultClassNames, | |
components = {}, | |
...rest | |
} = props; | |
return ( | |
<Async | |
ref={ref} | |
value={value} | |
onChange={onChange} | |
unstyled | |
components={{ | |
DropdownIndicator, | |
ClearIndicator, | |
MultiValueRemove, | |
Option, | |
...components | |
}} | |
styles={styles} | |
classNames={classNames} | |
{...rest} | |
/> | |
); | |
}); | |
export default AsyncSelect; | |
/* ----------- hooks.jsx ----------- */ | |
/** | |
* This hook could be added to your select component if needed: | |
* const formatters = useFormatters() | |
* <Select | |
* // other props | |
* {...formatters} | |
* /> | |
*/ | |
export const useFormatters = () => { | |
// useful for CreatableSelect | |
const formatCreateLabel = (label) => ( | |
<span className={'text-sm'}> | |
Add | |
<span className={'font-semibold'}>{` "${label}"`}</span> | |
</span> | |
); | |
// useful for GroupedOptions | |
const formatGroupLabel = (data) => ( | |
<div className={'flex justify-between items-center'}> | |
<span>{data.label}</span> | |
<span | |
className={ | |
'rounded-md text-xs font-normal text-secondary-foreground bg-secondary shadow-sm px-1' | |
} | |
> | |
{data.options.length} | |
</span> | |
</div> | |
); | |
return { | |
formatCreateLabel, | |
formatGroupLabel | |
}; | |
}; |
how to use it? any example?
The usage is similar to any react-select
component.
For example, the AsyncSelect
component provides custom styling options and allows further customization, but its usage is exactly the same as the Async
component in react-select
:
import React from 'react';
import AsyncSelect from 'components/ui/async-select'; // instead of 'react-select/async'
import { ColourOption, colourOptions } from '../data';
const filterColors = (inputValue: string) => {
return colourOptions.filter((i) =>
i.label.toLowerCase().includes(inputValue.toLowerCase())
);
};
const loadOptions = (
inputValue: string,
callback: (options: ColourOption[]) => void
) => {
setTimeout(() => {
callback(filterColors(inputValue));
}, 1000);
};
export default () => (
<AsyncSelect cacheOptions loadOptions={loadOptions} defaultOptions />
);
thanks.. it would be nice if you could add typescript
thanks.. it would be nice if you could add typescript
the types are also inferred from react-select
:
import type { Props } from 'react-select';
// import type { Props } from 'react-select/async'; // for AsyncSelect
and it's working in my project (I use flow
)
![Screenshot 2024-03-27 at 12 13 28](https://private-user-images.githubusercontent.com/48165230/317257846-4fd4554d-4564-484c-bc9e-bc3a02b759a3.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg4MzAwNDMsIm5iZiI6MTczODgyOTc0MywicGF0aCI6Ii80ODE2NTIzMC8zMTcyNTc4NDYtNGZkNDU1NGQtNDU2NC00ODRjLWJjOWUtYmMzYTAyYjc1OWEzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA2VDA4MTU0M1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTVlMGUzYTE4NGFjNjhiNjlkOTEwZjU1YTE5ZjYxN2E2MTRkNDgyYzI0YmRkNmNjOWVlYmQxZjg1MWMyMmRlMzkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.dBPoXi0VtiICdLeXvlWi6-noP3EJT-ckd12FclkxAvE)
but if it didn't work for u in typescript u can refer to react-select with typescript
I recently migrated from flow
to TypeScript
and I'm using the following:
const CreatableSelect = React.forwardRef<
React.ElementRef<typeof Creatable>,
React.ComponentPropsWithoutRef<typeof Creatable>
>((props, ref) => {
For anyone who is looking for a complete typed code.
Note: I added React Window
I would also like to thank the creator for publishing such a useful code 🙏
// ~~~ helper.ts
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import { cn } from "./utils";
import {
type ClassNamesConfig,
type GroupBase,
type StylesConfig,
} from "react-select";
/**
* styles that aligns with shadcn/ui
*/
const controlStyles = {
base: "flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer",
focus: "outline-none ring-1 ring-ring",
disabled: "cursor-not-allowed opacity-50",
};
const placeholderStyles = "text-sm text-muted-foreground";
const valueContainerStyles = "gap-1";
const multiValueStyles =
"inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2";
const indicatorsContainerStyles = "gap-1";
const clearIndicatorStyles = "p-1 rounded-md";
const indicatorSeparatorStyles = "bg-border";
const dropdownIndicatorStyles = "p-1 rounded-md";
const menuStyles =
"p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground";
const groupHeadingStyles =
"py-2 px-1 text-secondary-foreground text-sm font-semibold";
const optionStyles = {
base: "hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans",
focus: "active:bg-accent/90 bg-accent text-accent-foreground",
disabled: "pointer-events-none opacity-50",
selected: "",
};
const noOptionsMessageStyles =
"text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm";
const loadingIndicatorStyles =
"flex items-center justify-center h-4 w-4 opacity-50";
const loadingMessageStyles = "text-accent-foreground p-2 bg-accent";
/**
* This factory method is used to build custom classNames configuration
*/
export const createClassNames = (
classNames: ClassNamesConfig,
): ClassNamesConfig => {
return {
clearIndicator: (state) =>
cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)),
container: (state) => cn(classNames?.container?.(state)),
control: (state) =>
cn(
controlStyles.base,
state.isDisabled && controlStyles.disabled,
state.isFocused && controlStyles.focus,
classNames?.control?.(state),
),
dropdownIndicator: (state) =>
cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)),
group: (state) => cn(classNames?.group?.(state)),
groupHeading: (state) =>
cn(groupHeadingStyles, classNames?.groupHeading?.(state)),
indicatorsContainer: (state) =>
cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)),
indicatorSeparator: (state) =>
cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)),
input: (state) => cn(classNames?.input?.(state)),
loadingIndicator: (state) =>
cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)),
loadingMessage: (state) =>
cn(loadingMessageStyles, classNames?.loadingMessage?.(state)),
menu: (state) => cn(menuStyles, classNames?.menu?.(state)),
menuList: (state) => cn(classNames?.menuList?.(state)),
menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
multiValue: (state) =>
cn(multiValueStyles, classNames?.multiValue?.(state)),
multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
noOptionsMessage: (state) =>
cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)),
option: (state) =>
cn(
optionStyles.base,
state.isFocused && optionStyles.focus,
state.isDisabled && optionStyles.disabled,
state.isSelected && optionStyles.selected,
classNames?.option?.(state),
),
placeholder: (state) =>
cn(placeholderStyles, classNames?.placeholder?.(state)),
singleValue: (state) => cn(classNames?.singleValue?.(state)),
valueContainer: (state) =>
cn(valueContainerStyles, classNames?.valueContainer?.(state)),
};
};
export const defaultClassNames = createClassNames({});
export const defaultStyles: StylesConfig<
unknown,
boolean,
GroupBase<unknown>
> = {
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none",
},
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: "normal",
overflow: "visible",
}),
control: (base) => ({
...base,
transition: "none",
// minHeight: '2.25rem', // we used !min-h-9 instead
}),
menuList: (base) => ({
...base,
"::-webkit-scrollbar": {
background: "transparent",
},
"::-webkit-scrollbar-track": {
background: "transparent",
},
"::-webkit-scrollbar-thumb": {
background: "hsl(var(--border))",
},
"::-webkit-scrollbar-thumb:hover": {
background: "transparent",
},
}),
};
// ~~~ ReactSelectCustomComponents.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import type {
ClearIndicatorProps,
DropdownIndicatorProps,
MenuListProps,
MenuProps,
MultiValueRemoveProps,
OptionProps,
} from "react-select";
import { components } from "react-select";
import {
CaretSortIcon,
CheckIcon,
Cross2Icon as CloseIcon,
} from "@radix-ui/react-icons";
import { FixedSizeList as List } from "react-window";
export const DropdownIndicator = (props: DropdownIndicatorProps) => {
return (
<components.DropdownIndicator {...props}>
<CaretSortIcon className={"h-4 w-4 opacity-50"} />
</components.DropdownIndicator>
);
};
export const ClearIndicator = (props: ClearIndicatorProps) => {
return (
<components.ClearIndicator {...props}>
<CloseIcon className={"h-3.5 w-3.5 opacity-50"} />
</components.ClearIndicator>
);
};
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<CloseIcon className={"h-3 w-3 opacity-50"} />
</components.MultiValueRemove>
);
};
export const Option = (props: OptionProps) => {
return (
<components.Option {...props}>
<div className="flex items-center justify-between">
{/* TODO: Figure out the type */}
<div>{(props.data as { label: string }).label}</div>
{props.isSelected && <CheckIcon />}
</div>
</components.Option>
);
};
// Using Menu and MenuList fixes the scrolling behavior
export const Menu = (props: MenuProps) => {
return <components.Menu {...props}>{props.children}</components.Menu>;
};
export const MenuList = (props: MenuListProps) => {
const { children, maxHeight } = props;
const childrenArray = React.Children.toArray(children);
const calculateHeight = () => {
// When using children it resizes correctly
const totalHeight = childrenArray.length * 35; // Adjust item height if different
return totalHeight < maxHeight ? totalHeight : maxHeight;
};
const height = calculateHeight();
// Ensure childrenArray has length. Even when childrenArray is empty there is one element left
if (!childrenArray || childrenArray.length - 1 === 0) {
return <components.MenuList {...props} />;
}
return (
<List
height={height}
itemCount={childrenArray.length}
itemSize={35} // Adjust item height if different
width="100%"
>
{({ index, style }) => <div style={style}>{childrenArray[index]}</div>}
</List>
);
};
// ~~~ Select.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import SelectComponent from "react-select";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
ClearIndicator,
DropdownIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
} from "./ReactSelectCustomComponents";
const Select = React.forwardRef<
React.ElementRef<typeof SelectComponent>,
React.ComponentPropsWithoutRef<typeof SelectComponent>
>((props: Props, ref) => {
const {
value,
onChange,
options = [],
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
const id = React.useId();
return (
<SelectComponent
instanceId={id}
ref={ref}
value={value}
onChange={onChange}
options={options}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
...components,
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
Select.displayName = "Select";
export default Select;
// ~~~ Creatable.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import CreatableSelect from "react-select/creatable";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
ClearIndicator,
DropdownIndicator,
Menu,
MenuList,
MultiValueRemove,
Option,
} from "./ReactSelectCustomComponents";
const Creatable = React.forwardRef<
React.ElementRef<typeof CreatableSelect>,
React.ComponentPropsWithoutRef<typeof CreatableSelect>
>((props: Props, ref) => {
const {
value,
onChange,
options = [],
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
const id = React.useId();
return (
<CreatableSelect
instanceId={id}
ref={ref}
value={value}
onChange={onChange}
options={options}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
...components,
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
Creatable.displayName = "Creatable";
export default Creatable;
// ~~~ useFormatters.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import { type CreatableAdditionalProps } from "node_modules/react-select/dist/declarations/src/useCreatable";
import { type GroupBase } from "react-select";
/**
* This hook could be added to your select component if needed:
* const formatters = useFormatters()
* <Select
* // other props
* {...formatters}
* />
*/
export const useFormatters = () => {
// useful for CreatableSelect
const formatCreateLabel: CreatableAdditionalProps<
unknown,
GroupBase<unknown>
>["formatCreateLabel"] = (label) => (
<span className={"text-sm"}>
Add
<span className={"font-semibold"}>{` "${label}"`}</span>
</span>
);
// useful for GroupedOptions
const formatGroupLabel: (group: GroupBase<unknown>) => React.ReactNode = (
data,
) => (
<div className={"flex items-center justify-between"}>
<span>{data.label}</span>
<span
className={
"rounded-md bg-secondary px-1 text-xs font-normal text-secondary-foreground shadow-sm"
}
>
{data.options.length}
</span>
</div>
);
return {
formatCreateLabel,
formatGroupLabel,
};
};
For anyone who is looking for a complete typed code. Note: I added React Window I would also like to thank the creator for publishing such a useful code 🙏
// ~~~ helper.ts // SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 import { cn } from "./utils"; import { type ClassNamesConfig, type GroupBase, type StylesConfig, } from "react-select"; /** * styles that aligns with shadcn/ui */ const controlStyles = { base: "flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer", focus: "outline-none ring-1 ring-ring", disabled: "cursor-not-allowed opacity-50", }; const placeholderStyles = "text-sm text-muted-foreground"; const valueContainerStyles = "gap-1"; const multiValueStyles = "inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"; const indicatorsContainerStyles = "gap-1"; const clearIndicatorStyles = "p-1 rounded-md"; const indicatorSeparatorStyles = "bg-border"; const dropdownIndicatorStyles = "p-1 rounded-md"; const menuStyles = "p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground"; const groupHeadingStyles = "py-2 px-1 text-secondary-foreground text-sm font-semibold"; const optionStyles = { base: "hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans", focus: "active:bg-accent/90 bg-accent text-accent-foreground", disabled: "pointer-events-none opacity-50", selected: "", }; const noOptionsMessageStyles = "text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm"; const loadingIndicatorStyles = "flex items-center justify-center h-4 w-4 opacity-50"; const loadingMessageStyles = "text-accent-foreground p-2 bg-accent"; /** * This factory method is used to build custom classNames configuration */ export const createClassNames = ( classNames: ClassNamesConfig, ): ClassNamesConfig => { return { clearIndicator: (state) => cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)), container: (state) => cn(classNames?.container?.(state)), control: (state) => cn( controlStyles.base, state.isDisabled && controlStyles.disabled, state.isFocused && controlStyles.focus, classNames?.control?.(state), ), dropdownIndicator: (state) => cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)), group: (state) => cn(classNames?.group?.(state)), groupHeading: (state) => cn(groupHeadingStyles, classNames?.groupHeading?.(state)), indicatorsContainer: (state) => cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)), indicatorSeparator: (state) => cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)), input: (state) => cn(classNames?.input?.(state)), loadingIndicator: (state) => cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)), loadingMessage: (state) => cn(loadingMessageStyles, classNames?.loadingMessage?.(state)), menu: (state) => cn(menuStyles, classNames?.menu?.(state)), menuList: (state) => cn(classNames?.menuList?.(state)), menuPortal: (state) => cn(classNames?.menuPortal?.(state)), multiValue: (state) => cn(multiValueStyles, classNames?.multiValue?.(state)), multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)), multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)), noOptionsMessage: (state) => cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)), option: (state) => cn( optionStyles.base, state.isFocused && optionStyles.focus, state.isDisabled && optionStyles.disabled, state.isSelected && optionStyles.selected, classNames?.option?.(state), ), placeholder: (state) => cn(placeholderStyles, classNames?.placeholder?.(state)), singleValue: (state) => cn(classNames?.singleValue?.(state)), valueContainer: (state) => cn(valueContainerStyles, classNames?.valueContainer?.(state)), }; }; export const defaultClassNames = createClassNames({}); export const defaultStyles: StylesConfig< unknown, boolean, GroupBase<unknown> > = { input: (base) => ({ ...base, "input:focus": { boxShadow: "none", }, }), multiValueLabel: (base) => ({ ...base, whiteSpace: "normal", overflow: "visible", }), control: (base) => ({ ...base, transition: "none", // minHeight: '2.25rem', // we used !min-h-9 instead }), menuList: (base) => ({ ...base, "::-webkit-scrollbar": { background: "transparent", }, "::-webkit-scrollbar-track": { background: "transparent", }, "::-webkit-scrollbar-thumb": { background: "hsl(var(--border))", }, "::-webkit-scrollbar-thumb:hover": { background: "transparent", }, }), }; // ~~~ ReactSelectCustomComponents.tsx // SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 import * as React from "react"; import type { ClearIndicatorProps, DropdownIndicatorProps, MenuListProps, MenuProps, MultiValueRemoveProps, OptionProps, } from "react-select"; import { components } from "react-select"; import { CaretSortIcon, CheckIcon, Cross2Icon as CloseIcon, } from "@radix-ui/react-icons"; import { FixedSizeList as List } from "react-window"; export const DropdownIndicator = (props: DropdownIndicatorProps) => { return ( <components.DropdownIndicator {...props}> <CaretSortIcon className={"h-4 w-4 opacity-50"} /> </components.DropdownIndicator> ); }; export const ClearIndicator = (props: ClearIndicatorProps) => { return ( <components.ClearIndicator {...props}> <CloseIcon className={"h-3.5 w-3.5 opacity-50"} /> </components.ClearIndicator> ); }; export const MultiValueRemove = (props: MultiValueRemoveProps) => { return ( <components.MultiValueRemove {...props}> <CloseIcon className={"h-3 w-3 opacity-50"} /> </components.MultiValueRemove> ); }; export const Option = (props: OptionProps) => { return ( <components.Option {...props}> <div className="flex items-center justify-between"> {/* TODO: Figure out the type */} <div>{(props.data as { label: string }).label}</div> {props.isSelected && <CheckIcon />} </div> </components.Option> ); }; // Using Menu and MenuList fixes the scrolling behavior export const Menu = (props: MenuProps) => { return <components.Menu {...props}>{props.children}</components.Menu>; }; export const MenuList = (props: MenuListProps) => { const { children, maxHeight } = props; const childrenArray = React.Children.toArray(children); const calculateHeight = () => { // When using children it resizes correctly const totalHeight = childrenArray.length * 35; // Adjust item height if different return totalHeight < maxHeight ? totalHeight : maxHeight; }; const height = calculateHeight(); // Ensure childrenArray has length. Even when childrenArray is empty there is one element left if (!childrenArray || childrenArray.length - 1 === 0) { return <components.MenuList {...props} />; } return ( <List height={height} itemCount={childrenArray.length} itemSize={35} // Adjust item height if different width="100%" > {({ index, style }) => <div style={style}>{childrenArray[index]}</div>} </List> ); }; // ~~~ Select.tsx // SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 import * as React from "react"; import SelectComponent from "react-select"; import type { Props } from "react-select"; import { defaultClassNames, defaultStyles } from "~/lib/helper"; import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option, Menu, MenuList, } from "./ReactSelectCustomComponents"; const Select = React.forwardRef< React.ElementRef<typeof SelectComponent>, React.ComponentPropsWithoutRef<typeof SelectComponent> >((props: Props, ref) => { const { value, onChange, options = [], styles = defaultStyles, classNames = defaultClassNames, components = {}, ...rest } = props; const id = React.useId(); return ( <SelectComponent instanceId={id} ref={ref} value={value} onChange={onChange} options={options} unstyled components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option, Menu, MenuList, ...components, }} styles={styles} classNames={classNames} {...rest} /> ); }); Select.displayName = "Select"; export default Select; // ~~~ Creatable.tsx // SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 import * as React from "react"; import CreatableSelect from "react-select/creatable"; import type { Props } from "react-select"; import { defaultClassNames, defaultStyles } from "~/lib/helper"; import { ClearIndicator, DropdownIndicator, Menu, MenuList, MultiValueRemove, Option, } from "./ReactSelectCustomComponents"; const Creatable = React.forwardRef< React.ElementRef<typeof CreatableSelect>, React.ComponentPropsWithoutRef<typeof CreatableSelect> >((props: Props, ref) => { const { value, onChange, options = [], styles = defaultStyles, classNames = defaultClassNames, components = {}, ...rest } = props; const id = React.useId(); return ( <CreatableSelect instanceId={id} ref={ref} value={value} onChange={onChange} options={options} unstyled components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option, Menu, MenuList, ...components, }} styles={styles} classNames={classNames} {...rest} /> ); }); Creatable.displayName = "Creatable"; export default Creatable; // ~~~ useFormatters.tsx // SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 import { type CreatableAdditionalProps } from "node_modules/react-select/dist/declarations/src/useCreatable"; import { type GroupBase } from "react-select"; /** * This hook could be added to your select component if needed: * const formatters = useFormatters() * <Select * // other props * {...formatters} * /> */ export const useFormatters = () => { // useful for CreatableSelect const formatCreateLabel: CreatableAdditionalProps< unknown, GroupBase<unknown> >["formatCreateLabel"] = (label) => ( <span className={"text-sm"}> Add <span className={"font-semibold"}>{` "${label}"`}</span> </span> ); // useful for GroupedOptions const formatGroupLabel: (group: GroupBase<unknown>) => React.ReactNode = ( data, ) => ( <div className={"flex items-center justify-between"}> <span>{data.label}</span> <span className={ "rounded-md bg-secondary px-1 text-xs font-normal text-secondary-foreground shadow-sm" } > {data.options.length} </span> </div> ); return { formatCreateLabel, formatGroupLabel, }; };
@H7ioo this is super helpful 🙏 Do you also have the async-select component typed by any chance? I get an error for the Props
attribute. Thanks for sharing this with the community
@feliche93.
It is actually the same. You have to update the import only.
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import AsyncSelectComponent from "react-select/async";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
ClearIndicator,
DropdownIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
} from "./ReactSelectCustomComponents";
const AsyncSelect = React.forwardRef<
React.ElementRef<typeof AsyncSelectComponent>,
React.ComponentPropsWithoutRef<typeof AsyncSelectComponent>
>((props: Props, ref) => {
const {
value,
onChange,
options = [],
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
const id = React.useId();
return (
<AsyncSelectComponent
className="HELLO"
instanceId={id}
ref={ref}
value={value}
onChange={onChange}
options={options}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
...components,
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
AsyncSelect.displayName = "Async Select";
export default AsyncSelect;
Thanks a lot for this code snippet. however I think that there is an issue with typescript here.
`const options: { value: string; label: string }[] = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
];
<Select
options={options}
onChange={(value) => console.log(value)}
/>`
In this case value's type is unknown and not the one from options. Any idea how we can solve this ?
You're welcome.
If you import Select from "react-select" you would have the same type.
The issue is from react-select itself.
I found a way to resolve the issue but you have to update all the components.
Select component receives a generic Option which is what you are looking for.
/** The value of the select; reflected by the selected option */
value: PropsValue<Option>;
To make this work you should update the Select component to receive a generic "OptionType" which set for default to {label: string, value: string}
.
Then you need to update the SelectComponent generic to have that OptionType like this SelectComponent<OptionType>
const Select = React.forwardRef<
React.ElementRef<typeof SelectComponent<OptionType>>,
React.ComponentPropsWithoutRef<typeof SelectComponent<OptionType>>
>((props: Props<OptionType>, ref) => {
const {
value, // This is OptionType
onChange,
options = [],
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
const id = React.useId();
return (
<SelectComponent
instanceId={id}
ref={ref}
value={value} // Now this is OptionType
onChange={onChange}
options={options}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
...components,
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
This will cause errors because the other components don't have the same Option type
<SelectComponent
...
components={{
DropdownIndicator, // Those components need to be updated to have the OptionType generic
...
}}
...
/>
☝️ Don't forget you have to wrap the forwardRef component so you can receive generic.
Those solutions could help: https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
export const DropdownIndicator = <OptionType extends object = {label:string, value: string}>(props: DropdownIndicatorProps<OptionType>) => {
return (
<components.DropdownIndicator {...props}>
<CaretSortIcon className={"h-4 w-4 opacity-50"} />
</components.DropdownIndicator>
);
};
And so on to resolve all the errors.
I hope this was clear and helpful.
@H7ioo Hey, thanks a lot for the feedback, I had found a solution similar but yours looks more clean, I'll try it out soon.
For anyone trying to make this component super generic with typescript, here's what I came up with the help of @H7ioo, thanks a lot
import React, { ReactElement, Ref } from 'react';
import SelectComponent, {
components,
ClassNamesConfig,
DropdownIndicatorProps,
GroupBase,
StylesConfig,
MultiValueRemoveProps,
ClearIndicatorProps,
OptionProps,
MenuProps,
MenuListProps,
Props,
SelectInstance,
createFilter,
} from 'react-select';
import { FixedSizeList as List } from 'react-window';
import { cn } from '@/lib/utils';
import { Check, ChevronDown, X } from 'lucide-react';
/** select option type */
export type OptionType = { label: string; value: string | number };
/**
* styles that aligns with shadcn/ui
*/
const selectStyles = {
controlStyles: {
base: 'flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer',
focus: 'outline-none ring-1 ring-ring',
disabled: 'cursor-not-allowed opacity-50',
},
placeholderStyles: 'text-muted-foreground text-sm ml-1 font-medium',
valueContainerStyles: 'gap-1',
multiValueStyles:
'inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
indicatorsContainerStyles: 'gap-1',
clearIndicatorStyles: 'p-1 rounded-md',
indicatorSeparatorStyles: 'bg-muted',
dropdownIndicatorStyles: 'p-1 rounded-md',
menu: 'mt-1.5 p-1.5 border border-input bg-background text-sm rounded-lg',
menuList: 'morel-scrollbar',
groupHeadingStyles:
'py-2 px-1 text-secondary-foreground text-sm font-semibold',
optionStyles: {
base: 'hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans',
focus: 'active:bg-accent/90 bg-accent text-accent-foreground',
disabled: 'pointer-events-none opacity-50',
selected: '',
},
noOptionsMessageStyles:
'text-muted-foreground py-4 text-center text-sm border border-border rounded-sm',
label: 'text-muted-foreground text-sm',
loadingIndicatorStyles: 'flex items-center justify-center h-4 w-4 opacity-50',
loadingMessageStyles: 'text-accent-foreground p-2 bg-accent',
};
/**
* This factory method is used to build custom classNames configuration
*/
export const createClassNames = (
classNames: ClassNamesConfig<OptionType, boolean, GroupBase<OptionType>>
): ClassNamesConfig<OptionType, boolean, GroupBase<OptionType>> => {
return {
clearIndicator: (state) =>
cn(
selectStyles.clearIndicatorStyles,
classNames?.clearIndicator?.(state)
),
container: (state) => cn(classNames?.container?.(state)),
control: (state) =>
cn(
selectStyles.controlStyles.base,
state.isDisabled && selectStyles.controlStyles.disabled,
state.isFocused && selectStyles.controlStyles.focus,
classNames?.control?.(state)
),
dropdownIndicator: (state) =>
cn(
selectStyles.dropdownIndicatorStyles,
classNames?.dropdownIndicator?.(state)
),
group: (state) => cn(classNames?.group?.(state)),
groupHeading: (state) =>
cn(selectStyles.groupHeadingStyles, classNames?.groupHeading?.(state)),
indicatorsContainer: (state) =>
cn(
selectStyles.indicatorsContainerStyles,
classNames?.indicatorsContainer?.(state)
),
indicatorSeparator: (state) =>
cn(
selectStyles.indicatorSeparatorStyles,
classNames?.indicatorSeparator?.(state)
),
input: (state) => cn(classNames?.input?.(state)),
loadingIndicator: (state) =>
cn(
selectStyles.loadingIndicatorStyles,
classNames?.loadingIndicator?.(state)
),
loadingMessage: (state) =>
cn(
selectStyles.loadingMessageStyles,
classNames?.loadingMessage?.(state)
),
menu: (state) => cn(selectStyles.menu, classNames?.menu?.(state)),
menuList: (state) => cn(classNames?.menuList?.(state)),
menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
multiValue: (state) =>
cn(selectStyles.multiValueStyles, classNames?.multiValue?.(state)),
multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
noOptionsMessage: (state) =>
cn(
selectStyles.noOptionsMessageStyles,
classNames?.noOptionsMessage?.(state)
),
option: (state) =>
cn(
selectStyles.optionStyles.base,
state.isFocused && selectStyles.optionStyles.focus,
state.isDisabled && selectStyles.optionStyles.disabled,
state.isSelected && selectStyles.optionStyles.selected,
classNames?.option?.(state)
),
placeholder: (state) =>
cn(selectStyles.placeholderStyles, classNames?.placeholder?.(state)),
singleValue: (state) => cn(classNames?.singleValue?.(state)),
valueContainer: (state) =>
cn(
selectStyles.valueContainerStyles,
classNames?.valueContainer?.(state)
),
};
};
export const defaultClassNames = createClassNames({});
export const defaultStyles: StylesConfig<
OptionType,
boolean,
GroupBase<OptionType>
> = {
input: (base) => ({
...base,
'input:focus': {
boxShadow: 'none',
},
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: 'normal',
overflow: 'visible',
}),
control: (base) => ({
...base,
transition: 'none',
// minHeight: '2.25rem', // we used !min-h-9 instead
}),
menuList: (base) => ({
...base,
'::-webkit-scrollbar': {
background: 'transparent',
},
'::-webkit-scrollbar-track': {
background: 'transparent',
},
'::-webkit-scrollbar-thumb': {
background: 'hsl(var(--border))',
},
'::-webkit-scrollbar-thumb:hover': {
background: 'transparent',
},
}),
};
/**
* React select custom components
*/
export const DropdownIndicator = (
props: DropdownIndicatorProps<OptionType>
) => {
return (
<components.DropdownIndicator {...props}>
<ChevronDown className='h-4 w-4 opacity-50' />
</components.DropdownIndicator>
);
};
export const ClearIndicator = (props: ClearIndicatorProps<OptionType>) => {
return (
<components.ClearIndicator {...props}>
<X className='h-4 w-4 opacity-50' />
</components.ClearIndicator>
);
};
export const MultiValueRemove = (props: MultiValueRemoveProps<OptionType>) => {
return (
<components.MultiValueRemove {...props}>
<X className='h-3.5 w-3.5 opacity-50' />
</components.MultiValueRemove>
);
};
export const Option = (props: OptionProps<OptionType>) => {
return (
<components.Option {...props}>
<div className='flex items-center justify-between'>
<div>{props.label}</div>
{props.isSelected && <Check className='h-4 w-4 opacity-50' />}
</div>
</components.Option>
);
};
// Using Menu and MenuList fixes the scrolling behavior
export const Menu = (props: MenuProps<OptionType>) => {
return <components.Menu {...props}>{props.children}</components.Menu>;
};
export const MenuList = (props: MenuListProps<OptionType>) => {
const { children, maxHeight } = props;
const childrenArray = React.Children.toArray(children);
const calculateHeight = () => {
// When using children it resizes correctly
const totalHeight = childrenArray.length * 35; // Adjust item height if different
return totalHeight < maxHeight ? totalHeight : maxHeight;
};
const height = calculateHeight();
// Ensure childrenArray has length. Even when childrenArray is empty there is one element left
if (!childrenArray || childrenArray.length - 1 === 0) {
return <components.MenuList {...props} />;
}
return (
<List
height={height}
itemCount={childrenArray.length}
itemSize={35} // Adjust item height if different
width='100%'
>
{({ index, style }) => <div style={style}>{childrenArray[index]}</div>}
</List>
);
};
const BaseSelect = <IsMulti extends boolean = false>(
props: Props<OptionType, IsMulti> & { isMulti?: IsMulti },
ref: React.Ref<SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>>
) => {
const {
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
const instanceId = React.useId();
return (
<SelectComponent<OptionType, IsMulti, GroupBase<OptionType>>
ref={ref}
instanceId={instanceId}
unstyled
filterOption={createFilter({
matchFrom: 'any',
stringify: (option) => option.label,
})}
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
Menu,
MenuList,
...components,
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
};
export default React.forwardRef(BaseSelect) as <
IsMulti extends boolean = false,
>(
p: Props<OptionType, IsMulti> & {
ref?: Ref<
React.LegacyRef<
SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>
>
>;
isMulti?: IsMulti;
}
) => ReactElement;
@H7ioo How can I use achieve multi select with the Select component?
@H7ioo How can I use achieve multi select with the Select component?
check the official website for all the use cases, the multi select
example is in the first page
![Screenshot 2024-10-14 at 15 21 16](https://private-user-images.githubusercontent.com/48165230/376279970-2a3eb4b6-5b1f-4993-841c-d8421196a6c7.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg4MzAwNDMsIm5iZiI6MTczODgyOTc0MywicGF0aCI6Ii80ODE2NTIzMC8zNzYyNzk5NzAtMmEzZWI0YjYtNWIxZi00OTkzLTg0MWMtZDg0MjExOTZhNmM3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA2VDA4MTU0M1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWM1ZjM4MTgwNTUxOTIzMmM5YzI2YzhmOWI3ZDYxNmJiMjFlMTIwZGU3ODU5ZTBmY2M3YTFjMTQ5YmI3NzAxM2YmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.8JkpC3M6dFPci7fl-yXOsH6dy9v7R2zpCQQT9GefSJ4)
this gist
proposition only adds the styling part, react-select components work as usual
Small react19 update to the default export based on to @fitimbytyqi's work (replacing deprecated React.LegacyRef
with RefAttributes<T>['ref']
)
export default React.forwardRef(BaseSelect) as <
IsMulti extends boolean = false,
>(
p: Props<OptionType, IsMulti> & {
ref?: React.RefAttributes<
SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>
>['ref']
isMulti?: IsMulti
},
) => ReactElement
how to use it? any example?