|
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 }; |
@tommerkle1 I had already been working on a way to add the
colorScheme
prop to the select itself, but I went ahead and added it to the options as well. If you take the updated gist and add the keycolorScheme
to any of the options you want to change (from your custom palette or the official chakra palette) it should be reflected when the options are selected.You can also add the
colorScheme
prop to the select component as a whole to change all of the selected options' colors.If you want to see how I did it, look at the
MultiValueContainer
custom component to see where I'm grabbing the color scheme from.I updated my example here: https://codesandbox.io/s/chakra-ui-react-select-648uv?file=/chakra-react-select.js