/** * This is a new component built off react-select 5.4 * meant to replace Dropdown.jsx built off react-select 1.3 * * See storybook component for current functionality * * Prototyped on UserForm.tsx but added and tested the following: * Options: text, disabled, option helptext, option tooltip * Other: label text, dropdown help text, dropdown error */ import classnames from "classnames"; import React from "react"; import Select, { StylesConfig, DropdownIndicatorProps, OptionProps, components, PropsValue, SingleValue, } from "react-select-5"; import { COLORS } from "styles/var/colors"; import { PADDING } from "styles/var/padding"; import FormField from "components/forms/FormField"; import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper"; import Icon from "components/Icon"; import { IconNames } from "components/icons"; const getOptionBackgroundColor = (state: any) => { return state.isSelected || state.isFocused ? COLORS["ui-vibrant-blue-10"] : "transparent"; }; export interface CustomOptionType { label: string; value: string; tooltipContent?: string; helpText?: string; isDisabled?: boolean; iconName?: IconNames; } export interface IDropdownWrapper { options: CustomOptionType[]; value?: PropsValue | string; onChange: (newValue: SingleValue) => void; name: string; className?: string; labelClassname?: string; error?: string; label?: JSX.Element | string; helpText?: JSX.Element | string; isSearchable?: boolean; isDisabled?: boolean; iconName?: IconNames; placeholder?: string; /** E.g. scroll to view dropdown menu in a scrollable parent container */ onMenuOpen?: () => void; /** Table filter dropdowns have filter icon and height: 40px */ tableFilter?: boolean; } const baseClass = "dropdown-wrapper"; const DropdownWrapper = ({ options, value, onChange, name, className, labelClassname, error, label, helpText, isSearchable = false, isDisabled = false, iconName, placeholder, onMenuOpen, tableFilter = false, }: IDropdownWrapper) => { const wrapperClassNames = classnames(baseClass, className, { [`${baseClass}__table-filter`]: tableFilter, }); const handleChange = (newValue: SingleValue) => { onChange(newValue); }; // Ability to handle value of type string or CustomOptionType const getCurrentValue = () => { if (typeof value === "string") { return options.find((option) => option.value === value) || null; } return value; }; interface CustomOptionProps extends Omit, "data"> { data: CustomOptionType; } const CustomOption = (props: CustomOptionProps) => { const { data, ...rest } = props; const optionContent = (
{data.label} {data.helpText && ( {data.helpText} )}
); return ( {data.tooltipContent ? ( {optionContent} ) : ( optionContent )} ); }; const CustomDropdownIndicator = ( props: DropdownIndicatorProps ) => { const { isFocused, selectProps } = props; const color = isFocused || selectProps.menuIsOpen ? "core-fleet-blue" : "core-fleet-black"; return ( ); }; const ValueContainer = ({ children, ...props }: any) => { const iconToDisplay = iconName || (tableFilter ? "filter" : null); return ( components.ValueContainer && ( {!!children && iconToDisplay && ( )} {children} ) ); }; const customStyles: StylesConfig = { container: (provided) => ({ ...provided, width: "100%", height: "40px", }), control: (provided, state) => ({ ...provided, display: "flex", flexDirection: "row", width: "100%", backgroundColor: COLORS["ui-off-white"], paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px) paddingRight: "8px", cursor: "pointer", boxShadow: "none", borderRadius: "4px", borderColor: state.isFocused ? COLORS["core-fleet-blue"] : COLORS["ui-fleet-black-10"], "&:hover": { boxShadow: "none", borderColor: COLORS["core-fleet-blue"], ".dropdown-wrapper__single-value": { color: COLORS["core-vibrant-blue-over"], }, ".dropdown-wrapper__indicator path": { stroke: COLORS["core-vibrant-blue-over"], }, }, // When tabbing // Relies on --is-focused for styling as &:focus-visible cannot be applied "&.dropdown-wrapper__control--is-focused": { ".dropdown-wrapper__single-value": { color: COLORS["core-vibrant-blue-over"], }, ".dropdown-wrapper__indicator path": { stroke: COLORS["core-vibrant-blue-over"], }, }, ...(state.isDisabled && { ".dropdown-wrapper__single-value": { color: COLORS["ui-fleet-black-50"], }, ".dropdown-wrapper__indicator path": { stroke: COLORS["ui-fleet-black-50"], }, }), "&:active": { ".dropdown-wrapper__single-value": { color: COLORS["core-vibrant-blue-down"], }, ".dropdown-wrapper__indicator path": { stroke: COLORS["core-vibrant-blue-down"], }, }, ...(state.menuIsOpen && { ".dropdown-wrapper__indicator svg": { transform: "rotate(180deg)", transition: "transform 0.25s ease", }, }), }), singleValue: (provided) => ({ ...provided, fontSize: "16px", margin: 0, padding: 0, }), dropdownIndicator: (provided) => ({ ...provided, display: "flex", padding: "2px", svg: { transition: "transform 0.25s ease", }, }), menu: (provided) => ({ ...provided, boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)", borderRadius: "4px", zIndex: 6, overflow: "hidden", border: 0, marginTop: 0, maxHeight: "none", position: "absolute", left: "0", animation: "fade-in 150ms ease-out", }), menuList: (provided) => ({ ...provided, padding: PADDING["pad-small"], maxHeight: "none", }), valueContainer: (provided) => ({ ...provided, padding: 0, display: "flex", gap: PADDING["pad-small"], }), option: (provided, state) => ({ ...provided, padding: "10px 8px", fontSize: "14px", backgroundColor: getOptionBackgroundColor(state), color: COLORS["core-fleet-black"], "&:hover": { backgroundColor: state.isDisabled ? "transparent" : COLORS["ui-vibrant-blue-10"], }, "&:active": { backgroundColor: state.isDisabled ? "transparent" : COLORS["ui-vibrant-blue-10"], }, ...(state.isDisabled && { color: COLORS["ui-fleet-black-50"], fontStyle: "italic", cursor: "not-allowed", }), // Styles for custom option ".dropdown-wrapper__option": { display: "flex", flexDirection: "column", gap: "8px", width: "100%", }, ".dropdown-wrapper__help-text": { fontSize: "12px", whiteSpace: "normal", color: COLORS["ui-fleet-black-50"], fontStyle: "italic", }, }), menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections noOptionsMessage: (provided) => ({ ...provided, textAlign: "left", fontSize: "14px", padding: "10px 8px", }), }; const renderLabel = () => { const labelWrapperClasses = classnames( `${baseClass}__label`, labelClassname, { [`${baseClass}__label--error`]: !!error, [`${baseClass}__label--disabled`]: isDisabled, } ); if (!label) { return ""; } return ( ); }; return ( classNamePrefix="react-select" isSearchable={isSearchable} styles={customStyles} options={options} components={{ Option: CustomOption, DropdownIndicator: CustomDropdownIndicator, IndicatorSeparator: () => null, ValueContainer, }} value={getCurrentValue()} onChange={handleChange} isDisabled={isDisabled} noOptionsMessage={() => "No results found"} tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility placeholder={placeholder} onMenuOpen={onMenuOpen} /> ); }; export default DropdownWrapper;