|
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 }; |
@kotoyama sorry for the late response, I didn't see your comment! You mentioned
isInvalid
and that got me thinking that it is something I should implement. My wrapper now accepts theisInvalid
prop on both the<Select />
element itself, or a surrounding<FormControl />
element instead, should you choose to implement it that way.While I was at it, I also added the option to pass
isDisabled
to a surrounding<FormControl />
in order to disable it!If you look at my revision (should be the third to most recent) you can see what I changed to make that work!
Also, you said custom props, and I'm not sure if there was anything else you wanted to add, but you can pass any props you want into the main
<Select />
component, and if you want to use it on one of the specific sub-components you can grab it from the propselectProps
.See line 101 for an example: https://gist.github.com/csandman/c687a9fb4275112f281ab9a5701457e4#file-chakra-react-select-js-L101
Let me know if you have any other questions and I'll try to respond faster this time!