javascriptreactjsreact-select

react-select wont close when clicking outside


I'm using react-select and have made a few customizations to it. I tried to modify the ValueContainer and the SelectContainer components which cause some issues. The dropdown wont close when I click outside of it after having selected a value. I'm wondering if anyone can see what I have done wrong here?

I'm assuming that the dropdown wont close because the onBlur event for the Input doesn't trigger. E.g the input doesn't regain its focus after i click a value with closeMenuOnSelect = true. I am also assuming that this is because react-select cant find the input for some reason because I've modified the structure. Might be missing some refs or something but I can't understand where to get them and where to put them. Anyone know?

Here is my custom Select component:

const ReactSelect: React.FC<ReactSelectProps> = ({
  backgroundColor,
  isSearchable = false,
  placeholder = '',
  size,
  grow,
  className,
  required,
  label,
  variant = 'underline',
  dataTestId,
  ...props
}) => {
  const filterConfig = {
    ignoreCase: true,
    ignoreAccents: true,
    matchFromStart: false,
    stringify: (option: any) => `${option.label}`,
    trim: true,
  };

  const [showIsRequired, setShowIsRequired] = useState(
    !props.defaultValue && required
  );

  const handleOnChange = (newValue: any, actionMeta: ActionMeta<any>) => {
    setShowIsRequired(!newValue && required);
    if (props.onChange) {
      props.onChange(newValue, actionMeta);
    }
  };

  const properties = {
    ...props,
    className: className,
    onChange: handleOnChange,
    isSearchable: isSearchable,
    styles: customStyles(size, backgroundColor, grow, showIsRequired, variant),
    menuPlacement: 'auto' as MenuPlacement,
    placeholder: placeholder,
    noOptionsMessage: () => t`Ingen elementer funnet`,
    loadingMessage: () => t`Laster...`,
    filterOption: createFilter(filterConfig),
    closeMenuOnSelect: !props.isMulti,
    hideSelectedOptions: false,
    components: {
      ValueContainer: (inputProps: ValueContainerProps) => (
        <ValueContainer required={required} label={label} {...inputProps} />
      ),
      Option,
      SelectContainer: (containerProps: ContainerProps) => (
        <SelectContainer dataTestId={dataTestId} {...containerProps} />
      ),
    },
  };

  if ('value' in props) {
    return <StateManagedSelect {...properties} />;
  }

  return <Select {...properties} />;
};

And here are my custom <SelectContainer> and <ValueContainer>:

const ValueContainer = ({ children, label, required, ...rest }: any) => {
  const labelFloatingStyle = {
    top: '0.30rem',
    left: '0.6rem',
    transform: 'translate(0, 0) scale(1)',
    fontSize: '0.75rem',
    color: 'hsl(236, 91%, 9%)',
    fontWeight: '500',
  };
  const labelStyle = {
    top: '50%',
    left: '0.75rem',
    transform: 'translate(0, -50%) scale(1)',
    fontSize: '1rem',
    color: 'hsl(0, 0%, 45%)',
  };
  const requiredDotStyles = {
    color: 'hsl(35, 100%, 43%)',
    fontSize: '1.5rem',
    display: rest.hasValue ? 'none' : 'inline',
  };
  const getLabelStyles = () =>
    rest.hasValue || rest.isFocused ? labelFloatingStyle : labelStyle;
  return (
    <components.ValueContainer {...rest}>
      {children}
      {label && (
        <label
          style={{
            position: 'absolute',
            transformOrigin: 'left bottom',
            transition: 'all 0.2s',
            display: 'flex',
            alignItems: 'center',
            ...getLabelStyles(),
          }}
        >
          {label} {required && <span style={requiredDotStyles}>*</span>}
        </label>
      )}
    </components.ValueContainer>
  );
};

const SelectContainer = ({ dataTestId, ...rest }: any) => (
  <div data-test-id={dataTestId}>
    <components.SelectContainer {...rest} />
  </div>
);

Solution

  • The problem was with how I sent in the custom components. You shouldn't pass the props there. I think that might ruin some of the important ReactSelect props like refs and such.

    Send the components in like this instead. Define your custom properties in this object as well:

      const properties = {
        ...props,
        className: className,
        onChange: handleOnChange,
        isSearchable: isSearchable,
        styles: customStyles(size, backgroundColor, grow, showIsRequired, variant),
        menuPlacement: 'auto' as MenuPlacement,
        placeholder: placeholder,
        noOptionsMessage: () => t`Ingen elementer funnet`,
        loadingMessage: () => t`Laster...`,
        filterOption: createFilter(filterConfig),
        closeMenuOnSelect: !props.isMulti,
        hideSelectedOptions: false,
        label: label,
        required: required,
        dataTestId: dataTestId,
        components: {
          ValueContainer,
          Option,
          SelectContainer,
        },
      };
    
      if ('value' in props) {
        return <StateManagedSelect {...properties} />;
      }
    
      return <Select {...properties} />;
    

    Then you can get these custom props in the custom component using props.selectProps like this:

    const ValueContainer = ({ children, ...rest }: any) => {
      const { label, required } = rest.selectProps; // HERE
      const labelFloatingStyle = {
        top: '0.20rem',
        left: '0.6rem',
        transform: 'translate(0, 0) scale(1)',
        fontSize: '0.75rem',
        color: 'hsl(236, 91%, 9%)',
        fontWeight: '500',
      };
      const labelStyle = {
        top: '50%',
        left: '0.75rem',
        transform: 'translate(0, -50%) scale(1)',
        fontSize: '1rem',
        color: 'hsl(0, 0%, 45%)',
      };
      const requiredDotStyles = {
        color: 'hsl(35, 100%, 43%)',
        fontSize: '1.5rem',
        display: rest.hasValue ? 'none' : 'inline',
      };
    
      const getLabelStyles = () =>
        rest.hasValue || rest.isFocused ? labelFloatingStyle : labelStyle;
    
      return (
        <components.ValueContainer {...rest}>
          {children}
          {label && (
            <label
              style={{
                position: 'absolute',
                transformOrigin: 'left bottom',
                transition: 'all 0.2s',
                display: 'flex',
                alignItems: 'center',
                cursor: 'pointer',
                ...getLabelStyles(),
              }}
            >
              {label} {required && <span style={requiredDotStyles}>*</span>}
            </label>
          )}
        </components.ValueContainer>
      );
    };