-
-
Save bbovenzi/76a28701b7933420655925eefaa03dd5 to your computer and use it in GitHub Desktop.
A Chakra UI wrapper for react-select, made to be used as a multi-select which Chakra does not currently have (with Typescript support)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-underscore-dangle */ | |
import React from 'react'; | |
import Select, { components as selectComponents, Props as SelectProps } from 'react-select'; | |
import { | |
Flex, | |
Tag, | |
TagCloseButton, | |
TagLabel, | |
Divider, | |
CloseButton, | |
Center, | |
Box, | |
Portal, | |
StylesProvider, | |
useMultiStyleConfig, | |
useStyles, | |
useTheme, | |
useColorModeValue, | |
RecursiveCSSObject, | |
CSSWithMultiValues, | |
} from '@chakra-ui/react'; | |
import { ChevronDownIcon } from '@chakra-ui/icons'; | |
const MultiSelect = ({ | |
name = '', | |
styles = {}, | |
components = {}, | |
...props | |
}: SelectProps): JSX.Element => { | |
const chakraTheme = useTheme(); | |
const placeholderColor = useColorModeValue( | |
chakraTheme.colors.gray[400], | |
chakraTheme.colors.whiteAlpha[400], | |
); | |
const chakraStyles = { | |
input: (provided: Record<string, any>) => ({ | |
...provided, | |
color: 'inherit', | |
lineHeight: 1, | |
}), | |
menu: (provided: Record<string, any>) => ({ | |
...provided, | |
boxShadow: 'none', | |
}), | |
valueContainer: (provided: Record<string, any>) => ({ | |
...provided, | |
padding: '0.125rem 1rem', | |
}), | |
}; | |
return ( | |
<Select | |
name={name} | |
components={{ | |
...{ | |
Control: ({ | |
children, innerRef, innerProps, isDisabled, isFocused, | |
}) => { | |
const inputStyles = useMultiStyleConfig('Input', {}); | |
return ( | |
<StylesProvider value={inputStyles}> | |
<Flex | |
ref={innerRef} | |
sx={{ | |
...inputStyles.field, | |
p: 0, | |
overflow: 'hidden', | |
h: 'auto', | |
minH: 10, | |
}} | |
{...innerProps} | |
{...(isFocused && { 'data-focus': true })} | |
{...(isDisabled && { disabled: true })} | |
> | |
{children} | |
</Flex> | |
</StylesProvider> | |
); | |
}, | |
MultiValueContainer: ({ | |
children, | |
innerRef, | |
innerProps, | |
data: { isFixed }, | |
}) => ( | |
<Tag | |
ref={innerRef} | |
{...innerProps} | |
m="0.125rem" | |
variant={isFixed ? 'solid' : 'subtle'} | |
> | |
{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}> | |
{children} | |
</TagCloseButton> | |
); | |
}, | |
IndicatorSeparator: ({ innerProps }) => ( | |
<Divider | |
{...innerProps} | |
orientation="vertical" | |
opacity="1" | |
/> | |
), | |
ClearIndicator: ({ innerProps }) => ( | |
<CloseButton {...innerProps} size="sm" mx={2} /> | |
), | |
DropdownIndicator: ({ innerProps }) => { | |
const { addon } = useStyles(); | |
return ( | |
<Center | |
{...innerProps} | |
sx={{ | |
...addon, | |
h: '100%', | |
p: 0, | |
borderRadius: 0, | |
borderWidth: 0, | |
cursor: 'pointer', | |
}} | |
> | |
<ChevronDownIcon h={5} w={5} /> | |
</Center> | |
); | |
}, | |
// Menu components | |
MenuPortal: ({ children, ...portalProps }) => ( | |
<Portal {...portalProps}> | |
{children} | |
</Portal> | |
), | |
Menu: ({ children, ...menuProps }) => { | |
const menuStyles = useMultiStyleConfig('Menu', {}); | |
return ( | |
<selectComponents.Menu {...menuProps}> | |
<StylesProvider value={menuStyles}>{children}</StylesProvider> | |
</selectComponents.Menu> | |
); | |
}, | |
MenuList: ({ | |
innerRef, children, maxHeight, | |
}) => { | |
const { list } = useStyles(); | |
return ( | |
<Box | |
sx={{ | |
...list, | |
maxH: `${maxHeight}px`, | |
overflowY: 'auto', | |
}} | |
ref={innerRef} | |
> | |
{children} | |
</Box> | |
); | |
}, | |
GroupHeading: ({ innerProps, children }) => { | |
const { groupTitle } = useStyles(); | |
return ( | |
<Box sx={groupTitle} {...innerProps}> | |
{children} | |
</Box> | |
); | |
}, | |
Option: ({ | |
innerRef, innerProps, children, isFocused, isDisabled, | |
}) => { | |
const { item } = useStyles(); | |
interface ItemProps extends CSSWithMultiValues { | |
_disabled: CSSWithMultiValues, | |
_focus: CSSWithMultiValues, | |
} | |
return ( | |
<Box | |
sx={{ | |
...item, | |
w: '100%', | |
textAlign: 'left', | |
cursor: 'pointer', | |
bg: isFocused ? (item as RecursiveCSSObject<ItemProps>)._focus.bg : 'transparent', | |
...(isDisabled && (item as RecursiveCSSObject<ItemProps>)._disabled), | |
}} | |
ref={innerRef} | |
{...innerProps} | |
{...(isDisabled && { disabled: true })} | |
> | |
{children} | |
</Box> | |
); | |
}, | |
}, | |
...components, | |
}} | |
styles={{ | |
...chakraStyles, | |
...styles, | |
}} | |
theme={(baseTheme) => ({ | |
...baseTheme, | |
borderRadius: chakraTheme.radii.md, | |
colors: { | |
...baseTheme.colors, | |
neutral50: placeholderColor, // placeholder text color | |
neutral40: placeholderColor, // noOptionsMessage color | |
}, | |
})} | |
{...props} | |
/> | |
); | |
}; | |
export default MultiSelect; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment