monday-ui-react-core
Version:
Official monday.com UI resources for application development in React.js
446 lines (413 loc) • 12.5 kB
JSX
/* eslint-disable react/require-default-props,react/forbid-prop-types */
import React, { useCallback, useMemo, useState } from "react";
import Select, { components } from "react-select";
import AsyncSelect from "react-select/async";
import NOOP from "lodash/noop";
import { WindowedMenuList } from "react-windowed-select";
import PropTypes from "prop-types";
import cx from "classnames";
import MenuComponent from "./components/menu/menu";
import DropdownIndicatorComponent from "./components/DropdownIndicator/DropdownIndicator";
import OptionComponent from "./components/option/option";
import SingleValueComponent from "./components/singleValue/singleValue";
import ClearIndicatorComponent from "./components/ClearIndicator/ClearIndicator";
import ValueContainer from "./components/ValueContainer/ValueContainer";
import { defaultCustomStyles, ADD_AUTO_HEIGHT_COMPONENTS } from "./DropdownConstants";
import { SIZES } from "../../constants/sizes";
import generateBaseStyles, { customTheme } from "./Dropdown.styles";
import "./Dropdown.scss";
const Dropdown = ({
className,
placeholder,
disabled,
onMenuOpen,
onMenuClose,
onFocus,
onBlur,
onChange: customOnChange,
searchable,
options,
defaultValue,
value: customValue,
noOptionsMessage,
openMenuOnFocus,
openMenuOnClick,
clearable,
OptionRenderer,
optionRenderer,
ValueRenderer,
valueRenderer,
menuRenderer,
rtl,
size,
asyncOptions,
cacheOptions,
defaultOptions,
isVirtualized,
menuPortalTarget,
extraStyles,
menuIsOpen,
tabIndex,
id,
autoFocus,
multi = false,
multiline = false,
onOptionRemove: customOnOptionRemove,
onOptionSelect,
onClear
}) => {
const [selected, setSelected] = useState(defaultValue || []);
const [isDialogShown, setIsDialogShown] = useState(false);
const finalOptionRenderer = optionRenderer || OptionRenderer;
const finalValueRenderer = valueRenderer || ValueRenderer;
const isControlled = !!customValue;
const selectedOptions = customValue ?? selected;
const selectedOptionsMap = useMemo(
() =>
Array.isArray(selectedOptions)
? selectedOptions.reduce((acc, option) => ({ ...acc, [option.value]: option }), {})
: {},
[selectedOptions]
);
const value = multi ? selectedOptions : customValue;
const styles = useMemo(() => {
// We first want to get the default stylized groups (e.g. "container", "menu").
const baseStyles = generateBaseStyles({
size,
rtl
});
// Then we want to run the consumer's root-level custom styles with our "base" override groups.
const customStyles = extraStyles(baseStyles);
// Lastly, we create a style groups object that makes sure we run each custom group with our basic overrides.
const mergedStyles = Object.entries(customStyles).reduce((accumulator, [stylesGroup, stylesFn]) => {
return {
...accumulator,
[stylesGroup]: (defaultStyles, state) => {
const provided = baseStyles[stylesGroup] ? baseStyles[stylesGroup](defaultStyles, state) : defaultStyles;
return stylesFn(provided, state);
}
};
}, {});
if (multi) {
if (multiline) {
ADD_AUTO_HEIGHT_COMPONENTS.forEach(component => {
const original = mergedStyles[component];
mergedStyles[component] = (provided, state) => ({
...original(provided, state),
height: "auto"
});
});
}
const originalValueContainer = mergedStyles.valueContainer;
mergedStyles.valueContainer = (provided, state) => ({
...originalValueContainer(provided, state),
paddingLeft: 6
});
}
return mergedStyles;
}, [size, rtl, extraStyles, multi, multiline]);
const Menu = useCallback(props => <MenuComponent {...props} Renderer={menuRenderer} />, [menuRenderer]);
const DropdownIndicator = useCallback(props => <DropdownIndicatorComponent {...props} size={size} />, [size]);
const Option = useCallback(props => <OptionComponent {...props} Renderer={finalOptionRenderer} />, [
finalOptionRenderer
]);
const Input = useCallback(props => <components.Input {...props} aria-label="Dropdown input" />, []);
const SingleValue = useCallback(props => <SingleValueComponent {...props} Renderer={finalValueRenderer} />, [
finalValueRenderer
]);
const ClearIndicator = useCallback(props => <ClearIndicatorComponent {...props} size={size} />, [size]);
const onOptionRemove = useMemo(
() =>
customOnOptionRemove
? (optionValue, e) => customOnOptionRemove(selectedOptionsMap[optionValue], e)
: function(optionValue, e) {
setSelected(selected.filter(option => option.value !== optionValue));
e.stopPropagation();
},
[customOnOptionRemove, selected, selectedOptionsMap]
);
const valueContainerRenderer = useCallback(
props => (
<components.ValueContainer {...props}>
<ValueContainer
selectedOptions={selectedOptions}
onSelectedDelete={onOptionRemove}
setIsDialogShown={setIsDialogShown}
isDialogShown={isDialogShown}
isMultiline={multiline}
{...props}
/>
</components.ValueContainer>
),
[selectedOptions, onOptionRemove, isDialogShown, multiline]
);
const onChange = (option, event) => {
if (customOnChange) {
customOnChange(option, event);
}
switch (event.action) {
case "select-option": {
const selectedOption = multi ? event.option : option;
if (onOptionSelect) {
onOptionSelect(selectedOption);
}
if (!isControlled) {
setSelected([...selected, selectedOption]);
}
break;
}
case "clear":
if (onClear) {
onClear();
}
if (!isControlled) {
setSelected([]);
}
break;
}
};
const DropDownComponent = asyncOptions ? AsyncSelect : Select;
const asyncAdditions = {
...(asyncOptions && {
loadOptions: asyncOptions,
cacheOptions,
...(defaultOptions && { defaultOptions })
})
};
const additions = {
...(!asyncOptions && { options }),
...(multi && {
isMulti: true,
filterOption: option => !selectedOptionsMap[option.value]
})
};
return (
<DropDownComponent
className={cx("dropdown-wrapper", className)}
components={{
DropdownIndicator,
Menu,
ClearIndicator,
Input,
...(finalOptionRenderer && { Option }),
...(finalValueRenderer && { SingleValue }),
...(multi && {
MultiValue: NOOP, // We need it for react-select to behave nice with "multi"
ValueContainer: valueContainerRenderer
}),
...(isVirtualized && { MenuList: WindowedMenuList })
}}
size={size}
noOptionsMessage={noOptionsMessage}
placeholder={placeholder}
isDisabled={disabled}
isClearable={clearable}
isSearchable={searchable}
defaultValue={defaultValue}
value={value}
onMenuOpen={onMenuOpen}
onMenuClose={onMenuClose}
onFocus={onFocus}
onBlur={onBlur}
onChange={onChange}
openMenuOnFocus={openMenuOnFocus}
openMenuOnClick={openMenuOnClick}
isRtl={rtl}
styles={styles}
theme={customTheme}
menuPortalTarget={menuPortalTarget}
menuIsOpen={menuIsOpen}
tabIndex={tabIndex}
id={id}
autoFocus={autoFocus}
{...asyncAdditions}
{...additions}
/>
);
};
Dropdown.size = SIZES;
Dropdown.defaultProps = {
className: "",
placeholder: "",
onMenuOpen: NOOP,
onMenuClose: NOOP,
onKeyDown: NOOP,
onFocus: NOOP,
onBlur: NOOP,
onChange: NOOP,
searchable: true,
options: [],
noOptionsMessage: NOOP,
clearable: true,
size: SIZES.MEDIUM,
extraStyles: defaultCustomStyles,
tabIndex: "0",
id: undefined,
autoFocus: false
};
Dropdown.propTypes = {
/**
* Custom style
*/
className: PropTypes.string,
/**
* Placeholder to show when no value was selected
*/
placeholder: PropTypes.string,
/**
* If set to true, dropdown will be disabled
*/
disabled: PropTypes.bool,
/**
* Called when menu is opened
*/
onMenuOpen: PropTypes.func,
/**
* Called when menu is closed
*/
onMenuClose: PropTypes.func,
/**
* Called when key is pressed in the dropdown
*/
onKeyDown: PropTypes.func,
/**
* Called when focused
*/
onFocus: PropTypes.func,
/**
* Called when blurred
*/
onBlur: PropTypes.func,
/**
* Called when selected value has changed
*/
onChange: PropTypes.func,
/**
* If true, search in options will be enabled
*/
searchable: PropTypes.bool,
/**
* The dropdown options
*/
options: PropTypes.arrayOf(PropTypes.object),
/**
* Text to display when there are no options
*/
noOptionsMessage: PropTypes.func,
/**
* If set to true, the menu will open when focused
*/
openMenuOnFocus: PropTypes.bool,
/**
* If set to true, the menu will open when clicked
*/
openMenuOnClick: PropTypes.bool,
/**
* If set to true, clear button will be added
*/
clearable: PropTypes.bool,
/**
* custom option render function
*/
optionRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* custom value render function
*/
valueRenderer: PropTypes.func,
/**
* custom menu render function
*/
menuRenderer: PropTypes.func,
/**
* If set to true, the dropdown will be in Right to Left mode
*/
rtl: PropTypes.bool,
/**
* Set default selected value
*/
defaultValue: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})
),
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})
]),
/**
* The component's value.
* When passed, makes this a [controlled](https://reactjs.org/docs/forms.html#controlled-components) component.
*/
value: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})
),
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})
]),
/**
* Select menu size from `Dropdown.size` - Dropdown.size.LARGE | Dropdown.size.MEDIUM | Dropdown.size.SMALL
*/
size: PropTypes.string,
/**
* If provided Dropdown will work in async mode. Can be either promise or callback
*/
asyncOptions: PropTypes.oneOfType([
PropTypes.func, // callback
PropTypes.shape({
then: PropTypes.func.isRequired,
catch: PropTypes.func.isRequired
}) // Promise
]),
/**
* If set to true, fetched async options will be cached
*/
cacheOptions: PropTypes.bool,
/**
* If set, `asyncOptions` will be invoked with its value on mount and the resolved results will be loaded
*/
defaultOptions: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.object)]),
/**
* If set to true, the menu will use virtualization. Virtualized async works only with
*/
isVirtualized: PropTypes.bool,
/**
* Whether the menu should use a portal, and where it should attach
*/
menuPortalTarget: PropTypes.oneOfType([PropTypes.element, PropTypes.object]),
/**
* Custom function to override existing styles (similar to `react-select`'s `style` prop), for example: `base => ({...base, color: 'red'})`, where `base` is the component's default styles
*/
extraStyles: PropTypes.func,
/**
* Tab index for keyboard navigation purposes
*/
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* ID for the select container
*/
id: PropTypes.string,
/**
* focusAuto when component mount
*/
autoFocus: PropTypes.bool,
/**
* If set to true, the dropdown will be in multi-select mode.
* When in multi-select mode, the selected value will be an array,
* and it will be displayed as our [`<Chips>`](/?path=/docs/components-chips--sandbox) component.
*/
multi: PropTypes.bool,
/**
* If set to true together with `multi`, it will make the dropdown expand to multiple lines when new values are selected.
*/
multiline: PropTypes.bool
};
export default Dropdown;