|
import React, { cloneElement, forwardRef } from "react"; |
|
import ReactSelect, { components as selectComponents } from "react-select"; |
|
import AsyncReactSelect from "react-select/async"; |
|
import CreatableReactSelect from "react-select/creatable"; |
|
import { |
|
Flex, |
|
Tag, |
|
TagCloseButton, |
|
TagLabel, |
|
Divider, |
|
CloseButton, |
|
Center, |
|
Box, |
|
Portal, |
|
StylesProvider, |
|
useMultiStyleConfig, |
|
useStyles, |
|
useTheme, |
|
useColorModeValue, |
|
useFormControl, |
|
createIcon |
|
} from "@chakra-ui/react"; |
|
|
|
// Taken from the @chakra-ui/icons package to prevent needing it as a dependency |
|
// https://github.com/chakra-ui/chakra-ui/blob/main/packages/icons/src/ChevronDown.tsx |
|
const ChevronDown = createIcon({ |
|
displayName: "ChevronDownIcon", |
|
d: "M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z" |
|
}); |
|
|
|
// Custom styles for components which do not have a chakra equivalent |
|
const chakraStyles = { |
|
// When disabled, react-select sets the pointer-state to none |
|
// which prevents the `not-allowed` cursor style from chakra |
|
// from getting applied to the Control |
|
container: (provided) => ({ |
|
...provided, |
|
pointerEvents: "auto" |
|
}), |
|
input: (provided) => ({ |
|
...provided, |
|
color: "inherit", |
|
lineHeight: 1 |
|
}), |
|
menu: (provided) => ({ |
|
...provided, |
|
boxShadow: "none" |
|
}), |
|
valueContainer: (provided, { selectProps: { size } }) => { |
|
const px = { |
|
sm: "0.75rem", |
|
md: "1rem", |
|
lg: "1rem" |
|
}; |
|
|
|
return { |
|
...provided, |
|
padding: `0.125rem ${px[size]}` |
|
}; |
|
}, |
|
loadingMessage: (provided, { selectProps: { size } }) => { |
|
const fontSizes = { |
|
sm: "0.875rem", |
|
md: "1rem", |
|
lg: "1.125rem" |
|
}; |
|
|
|
const paddings = { |
|
sm: "6px 9px", |
|
md: "8px 12px", |
|
lg: "10px 15px" |
|
}; |
|
|
|
return { |
|
...provided, |
|
fontSize: fontSizes[size], |
|
padding: paddings[size] |
|
}; |
|
}, |
|
// Add the chakra style for when a TagCloseButton has focus |
|
multiValueRemove: ( |
|
provided, |
|
{ isFocused, selectProps: { multiValueRemoveFocusStyle } } |
|
) => (isFocused ? multiValueRemoveFocusStyle : {}), |
|
control: () => ({}), |
|
menuList: () => ({}), |
|
option: () => ({}), |
|
multiValue: () => ({}), |
|
multiValueLabel: () => ({}), |
|
group: () => ({}) |
|
}; |
|
|
|
const chakraComponents = { |
|
// Control components |
|
Control: ({ |
|
children, |
|
innerRef, |
|
innerProps, |
|
isDisabled, |
|
isFocused, |
|
selectProps: { size, isInvalid } |
|
}) => { |
|
const inputStyles = useMultiStyleConfig("Input", { size }); |
|
|
|
const heights = { |
|
sm: 8, |
|
md: 10, |
|
lg: 12 |
|
}; |
|
|
|
return ( |
|
<StylesProvider value={inputStyles}> |
|
<Flex |
|
ref={innerRef} |
|
sx={{ |
|
...inputStyles.field, |
|
p: 0, |
|
overflow: "hidden", |
|
h: "auto", |
|
minH: heights[size] |
|
}} |
|
{...innerProps} |
|
data-focus={isFocused ? true : undefined} |
|
data-invalid={isInvalid ? true : undefined} |
|
data-disabled={isDisabled ? true : undefined} |
|
> |
|
{children} |
|
</Flex> |
|
</StylesProvider> |
|
); |
|
}, |
|
MultiValueContainer: ({ |
|
children, |
|
innerRef, |
|
innerProps, |
|
data, |
|
selectProps |
|
}) => ( |
|
<Tag |
|
ref={innerRef} |
|
{...innerProps} |
|
m="0.125rem" |
|
// react-select Fixed Options example: https://react-select.com/home#fixed-options |
|
variant={data.isFixed ? "solid" : "subtle"} |
|
colorScheme={data.colorScheme || selectProps.colorScheme} |
|
size={selectProps.size} |
|
> |
|
{children} |
|
</Tag> |
|
), |
|
MultiValueLabel: ({ children, innerRef, innerProps }) => ( |
|
<TagLabel ref={innerRef} {...innerProps}> |
|
{children} |
|
</TagLabel> |
|
), |
|
MultiValueRemove: ({ children, innerRef, innerProps, data: { isFixed } }) => { |
|
if (isFixed) { |
|
return null; |
|
} |
|
|
|
return ( |
|
<TagCloseButton ref={innerRef} {...innerProps} tabIndex={-1}> |
|
{children} |
|
</TagCloseButton> |
|
); |
|
}, |
|
IndicatorSeparator: ({ innerProps }) => ( |
|
<Divider {...innerProps} orientation="vertical" opacity="1" /> |
|
), |
|
ClearIndicator: ({ innerProps, selectProps: { size } }) => ( |
|
<CloseButton {...innerProps} size={size} mx={2} tabIndex={-1} /> |
|
), |
|
DropdownIndicator: ({ innerProps, selectProps: { size } }) => { |
|
const { addon } = useStyles(); |
|
|
|
const iconSizes = { |
|
sm: 4, |
|
md: 5, |
|
lg: 6 |
|
}; |
|
const iconSize = iconSizes[size]; |
|
|
|
return ( |
|
<Center |
|
{...innerProps} |
|
sx={{ |
|
...addon, |
|
h: "100%", |
|
borderRadius: 0, |
|
borderWidth: 0, |
|
cursor: "pointer" |
|
}} |
|
> |
|
<ChevronDown h={iconSize} w={iconSize} /> |
|
</Center> |
|
); |
|
}, |
|
// Menu components |
|
MenuPortal: ({ children }) => <Portal>{children}</Portal>, |
|
Menu: ({ children, ...props }) => { |
|
const menuStyles = useMultiStyleConfig("Menu"); |
|
return ( |
|
<selectComponents.Menu {...props}> |
|
<StylesProvider value={menuStyles}>{children}</StylesProvider> |
|
</selectComponents.Menu> |
|
); |
|
}, |
|
MenuList: ({ innerRef, children, maxHeight, selectProps: { size } }) => { |
|
const { list } = useStyles(); |
|
const chakraTheme = useTheme(); |
|
|
|
const borderRadii = { |
|
sm: chakraTheme.radii.sm, |
|
md: chakraTheme.radii.md, |
|
lg: chakraTheme.radii.md |
|
}; |
|
|
|
return ( |
|
<Box |
|
sx={{ |
|
...list, |
|
maxH: `${maxHeight}px`, |
|
overflowY: "auto", |
|
borderRadius: borderRadii[size] |
|
}} |
|
ref={innerRef} |
|
> |
|
{children} |
|
</Box> |
|
); |
|
}, |
|
GroupHeading: ({ innerProps, children }) => { |
|
const { groupTitle } = useStyles(); |
|
return ( |
|
<Box sx={groupTitle} {...innerProps}> |
|
{children} |
|
</Box> |
|
); |
|
}, |
|
Option: ({ |
|
innerRef, |
|
innerProps, |
|
children, |
|
isFocused, |
|
isDisabled, |
|
selectProps: { size } |
|
}) => { |
|
const { item } = useStyles(); |
|
return ( |
|
<Box |
|
role="button" |
|
sx={{ |
|
...item, |
|
w: "100%", |
|
textAlign: "start", |
|
bg: isFocused ? item._focus.bg : "transparent", |
|
fontSize: size, |
|
...(isDisabled && item._disabled) |
|
}} |
|
ref={innerRef} |
|
{...innerProps} |
|
{...(isDisabled && { disabled: true })} |
|
> |
|
{children} |
|
</Box> |
|
); |
|
} |
|
}; |
|
|
|
const ChakraReactSelect = ({ |
|
children, |
|
styles = {}, |
|
components = {}, |
|
theme = () => ({}), |
|
size = "md", |
|
colorScheme = "gray", |
|
isDisabled, |
|
isInvalid, |
|
...props |
|
}) => { |
|
const chakraTheme = useTheme(); |
|
|
|
// Combine the props passed into the component with the props |
|
// that can be set on a surrounding form control to get |
|
// the values of `isDisabled` and `isInvalid` |
|
const inputProps = useFormControl({ isDisabled, isInvalid }); |
|
|
|
// The chakra theme styles for TagCloseButton when focused |
|
const closeButtonFocus = |
|
chakraTheme.components.Tag.baseStyle.closeButton._focus; |
|
const multiValueRemoveFocusStyle = { |
|
background: closeButtonFocus.bg, |
|
boxShadow: chakraTheme.shadows[closeButtonFocus.boxShadow] |
|
}; |
|
|
|
// The chakra UI global placeholder color |
|
// https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/styles.ts#L13 |
|
const placeholderColor = useColorModeValue( |
|
chakraTheme.colors.gray[400], |
|
chakraTheme.colors.whiteAlpha[400] |
|
); |
|
|
|
// Ensure that the size used is one of the options, either `sm`, `md`, or `lg` |
|
let realSize = size; |
|
const sizeOptions = ["sm", "md", "lg"]; |
|
if (!sizeOptions.includes(size)) { |
|
realSize = "md"; |
|
} |
|
|
|
const select = cloneElement(children, { |
|
components: { |
|
...chakraComponents, |
|
...components |
|
}, |
|
styles: { |
|
...chakraStyles, |
|
...styles |
|
}, |
|
theme: (baseTheme) => { |
|
const propTheme = theme(baseTheme); |
|
|
|
return { |
|
...baseTheme, |
|
...propTheme, |
|
colors: { |
|
...baseTheme.colors, |
|
neutral50: placeholderColor, // placeholder text color |
|
neutral40: placeholderColor, // noOptionsMessage color |
|
...propTheme.colors |
|
}, |
|
spacing: { |
|
...baseTheme.spacing, |
|
...propTheme.spacing |
|
} |
|
}; |
|
}, |
|
colorScheme, |
|
size: realSize, |
|
multiValueRemoveFocusStyle, |
|
// isDisabled and isInvalid can be set on the component |
|
// or on a surrounding form control |
|
isDisabled: inputProps.disabled, |
|
isInvalid: !!inputProps["aria-invalid"], |
|
...props |
|
}); |
|
|
|
return select; |
|
}; |
|
|
|
const Select = forwardRef((props, ref) => ( |
|
<ChakraReactSelect {...props}> |
|
<ReactSelect ref={ref} /> |
|
</ChakraReactSelect> |
|
)); |
|
|
|
const AsyncSelect = forwardRef((props, ref) => ( |
|
<ChakraReactSelect {...props}> |
|
<AsyncReactSelect ref={ref} /> |
|
</ChakraReactSelect> |
|
)); |
|
|
|
const CreatableSelect = forwardRef((props, ref) => ( |
|
<ChakraReactSelect {...props}> |
|
<CreatableReactSelect ref={ref} /> |
|
</ChakraReactSelect> |
|
)); |
|
|
|
export { Select as default, AsyncSelect, CreatableSelect }; |
@valkn0t I would love to but I'm afraid I'm not the most familiar with TypeScript. I've made one component with it, and even then it was super basic. If you want to give it a shot and share here, or point me in the right direction I'd give it a try!