Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-Select custom input inside MenuList

I'm trying to create a custom select using React-Select options. I would like to have my search not in the control box, but rather in the menu. I tried this:

import React from "react";
import Select, { components } from "react-select";
import { colourOptions, groupedOptions } from "./docs/data";


const MenuList = props => {
  return (
    <components.MenuList {...props}>
      <components.Input {...props} />;
      {props.selectProps.inputValue.length > 1 ? props.children : ""}
    </components.MenuList>
  );
};
export default () => (
  <Select
    defaultValue={colourOptions[1]}
    options={groupedOptions}
    components={{ MenuList }}
  />
);

Problem is I'm getting an error saying

Uncaught Invariant Violation: input is a void element tag and must neither have children nor use dangerouslySetInnerHTML

I'm guessing the react select's components.Input is rendering another div inside the input tag or something like that. Does anybody have an idea how this can be done maybe?

like image 701
Mumfordwiz Avatar asked Mar 01 '19 08:03

Mumfordwiz


2 Answers

You should inspire yourself with what's suggested in the documentation in the section Advanced here: https://react-select.com/advanced.

I have recreated a live example in CodeSandbox so you can see it in action and play with it. But the main idea is to embed the original Select element inside some controlled element and them edit the style of your Select to make it feel as one single MenuList.

class PopoutExample extends Component<*, State> {
  state = { isOpen: false, value: undefined };
  toggleOpen = () => {
    this.setState(state => ({ isOpen: !state.isOpen }));
  };
  onSelectChange = value => {
    this.toggleOpen();
    this.setState({ value });
  };
  render() {
    const { isOpen, value } = this.state;
    return (
      <Dropdown
        isOpen={isOpen}
        onClose={this.toggleOpen}
        target={
          <Button
            iconAfter={<ChevronDown />}
            onClick={this.toggleOpen}
            isSelected={isOpen}
          >
            {value ? `State: ${value.label}` : "Select a State"}
          </Button>
        }
      >
        <Select
          autoFocus
          backspaceRemovesValue={false}
          components={{ DropdownIndicator, IndicatorSeparator: null }}
          controlShouldRenderValue={false}
          hideSelectedOptions={false}
          isClearable={false}
          menuIsOpen
          onChange={this.onSelectChange}
          options={stateOptions}
          placeholder="Search..."
          styles={selectStyles}
          tabSelectsValue={false}
          value={value}
        />
      </Dropdown>
    );
  }
}

// styled components

const Menu = props => {
  const shadow = "hsla(218, 50%, 10%, 0.1)";
  return (
    <div
      css={{
        backgroundColor: "white",
        borderRadius: 4,
        boxShadow: `0 0 0 1px ${shadow}, 0 4px 11px ${shadow}`,
        marginTop: 8,
        position: "absolute",
        zIndex: 2
      }}
      {...props}
    />
  );
};
const Blanket = props => (
  <div
    css={{
      bottom: 0,
      left: 0,
      top: 0,
      right: 0,
      position: "fixed",
      zIndex: 1
    }}
    {...props}
  />
);
const Dropdown = ({ children, isOpen, target, onClose }) => (
  <div css={{ position: "relative" }}>
    {target}
    {isOpen ? <Menu>{children}</Menu> : null}
    {isOpen ? <Blanket onClick={onClose} /> : null}
  </div>
);
const Svg = p => (
  <svg
    width="24"
    height="24"
    viewBox="0 0 24 24"
    focusable="false"
    role="presentation"
    {...p}
  />
);
const DropdownIndicator = () => (
  <div css={{ color: colors.neutral20, height: 24, width: 32 }}>
    <Svg>
      <path
        d="M16.436 15.085l3.94 4.01a1 1 0 0 1-1.425 1.402l-3.938-4.006a7.5 7.5 0 1 1 1.423-1.406zM10.5 16a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11z"
        fill="currentColor"
        fillRule="evenodd"
      />
    </Svg>
  </div>
);
const ChevronDown = () => (
  <Svg style={{ marginRight: -6 }}>
    <path
      d="M8.292 10.293a1.009 1.009 0 0 0 0 1.419l2.939 2.965c.218.215.5.322.779.322s.556-.107.769-.322l2.93-2.955a1.01 1.01 0 0 0 0-1.419.987.987 0 0 0-1.406 0l-2.298 2.317-2.307-2.327a.99.99 0 0 0-1.406 0z"
      fill="currentColor"
      fillRule="evenodd"
    />
  </Svg>
);
like image 162
Laura Avatar answered Oct 08 '22 07:10

Laura


Input as a part of MenuList.

It is inspired by https://codesandbox.io/embed/m75wlyx3oy solution.

Default MenuList is exchanged with CustomMenuWithInput which in first place show options in dropdown. Options list is provided by { props.children }. Under last option comes input field.

onFocus={onMenuInputFocus} prevents from closing dropdown on each letter typed in input.


const CustomMenuWithInput = ({ selectProps: { onMenuInputFocus }, ...props }) => {
  const [value, setValue] = useState('')

  return (
    <div>
      { props.children }
      <input
        id='add-option'
        label='Add option'
        placeholder='option'
        autoCorrect='off'
        autoComplete='off'
        spellCheck='false'
        type='text'
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onMouseDown={(e) => {
          e.stopPropagation()
          e.target.focus()
        }}
        onFocus={onMenuInputFocus}
      />
    </div>
  )
}

const SelectView = ({ onChange }) => {
  const [inputValue, setInputValue] = useState('')
  const [isFocused, setIsFocused] = useState(false)

  return (
    <Select
      id=''
      label={'List'}
      options={list}
      onChange={opt => {
        onChange(opt.value, opt)
        setIsFocused(false)
      }}
      onInputChange={setInputValue}
      onMenuInputFocus={() => setIsFocused(true)}
      inputValue={inputValue}
      placeholder='Select...'
      components={{
        MenuList: CustomMenuWithInput,
      }}
      isSearchable
      {...{
        menuIsOpen: isFocused || undefined,
        isFocused: isFocused || undefined
      }}
    />
  )
}

like image 22
Aga Avatar answered Oct 08 '22 08:10

Aga