'Search input as option in Material UI Select component

I need to make Select component with Search input as first option in dropdown list. Something like this: enter image description here

The main problem is that Search component acts as normal from input. I don't know how to make it focusable. Thank you.



Solution 1:[1]

Problem Explanation

This happens because MenuItem is a direct child of the Select component. It acts in the same way as described in Selected menu documentation. You can see that when Menu (or in our case Select) is opened, focus is automatically put onto the selected MenuItem. That's why TextField loses focus in the first place (even if we attach an autoFocus attribute to it).

Ok, so we can obviously simply hack this using the useRef hook on a TextField and then call the focus() method when Select opens. Something like this:

var inputRef = useRef(undefined);
...
<Select onAnimationEnd={() -> inputRef.current.focus()} ... >
    ...
    <TextField ref={inputRef} ... />
    ...
</Select>

Well not so fast, there's a catch!

Here's where things break: You open your select and focus shifts to the input field as expected. Then you insert search text that would filter out the currently selected value. Now if you remove text with backspace until the currently selected item appears again, you would see that focus would automatically be placed from input field to the currently selected option. It might seem like a viable solution at first but you can see where UX suffers and we don't get expected stable behavior that we want.

Solution

I find your question a bit vague without any code and proper explanation, but I still find it useful since I was searching for the exact same solution as you: Searchable Select MUI Component with good UX. So please note that this is my assumption of what you wanted as a working solution.

Here's a CodeSandbox with my working solution. All the important bits are explained in code comments and here:

  • The most important change is adding MenuProps={{ autoFocus: false }} attribute to the Select component and autoFocus attribute to the TextField component.
  • I've put the search TextField into a ListSubheader so that it doesn't act as a selectable item in the menu. i.e. we can click the search input without triggering any selection.
  • I've added a custom renderValue function. This prevents rendering empty string in Select's value field if search text would exclude the currently selected option.
  • I've disabled event propagation on TextField with the onKeyDown function. This prevents auto selecting item while typing (which is default Select behavior)
  • [BONUS] You can easily extend this component with multi selection support, which was what I ended up making for my project, but I'll not include the details here since it's out of the scope of your question.

Solution 2:[2]

The MUI Autocomplete component might be just what you need [https://mui.com/components/autocomplete/](MUI Autocomplete)

Solution 3:[3]

You can place <ListSubheader><TextField> these two components inside <Select> component something below.

output:

enter image description here

app.js

import React, { useState, useMemo } from "react";
import Checkbox from "@material-ui/core/Checkbox";
import InputLabel from "@material-ui/core/InputLabel";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import ListSubheader from "@material-ui/core/ListSubheader";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";

import { MenuProps, useStyles, options } from "./utils";

const containsText = (text, searchText) =>
  text.toLowerCase().indexOf(searchText.toLowerCase()) > -1;

function App() {
  const classes = useStyles();
  const [selected, setSelected] = useState([]);
  const [searchText, setSearchText] = useState("");
  const isAllSelected =
    options.length > 0 && selected?.length === options.length;
  const displayedOptions = useMemo(
    () => options.filter((option) => containsText(option, searchText)),
    [searchText]
  );
  const handleChange = (event) => {
    console.log("vals", event.target);
    const value = event.target.value;
    if (value[value.length - 1] === "all") {
      setSelected(selected?.length === options.length ? [] : options);
      return;
    }
    setSelected(value);
    console.log("values", selected);
  };

  return (
    <FormControl className={classes.formControl}>
      <InputLabel id="mutiple-select-label">Multiple Select</InputLabel>
      <Select
        labelId="mutiple-select-label"
        multiple
        variant="outlined"
        value={selected || []}
        onChange={handleChange}
        renderValue={(selected) => selected}
        MenuProps={MenuProps}
        onClose={() => setSearchText("")}
      >
        <ListSubheader>
          <TextField
            size="small"
            // Autofocus on textfield
            autoFocus
            placeholder="Type to search..."
            fullWidth
            InputProps={{
              startAdornment: <InputAdornment position="start"></InputAdornment>
            }}
            onChange={(e) => setSearchText(e.target.value)}
            onKeyDown={(e) => {
              if (e.key !== "Escape") {
                // Prevents autoselecting item while typing (default Select behaviour)
                e.stopPropagation();
              }
            }}
          />
        </ListSubheader>
        <MenuItem
          value="all"
          classes={{
            root: isAllSelected ? classes.selectedAll : ""
          }}
        >
          <ListItemIcon>
            <Checkbox
              classes={{ indeterminate: classes.indeterminateColor }}
              checked={isAllSelected}
              indeterminate={
                selected?.length > 0 && selected.length < options.length
              }
            />
          </ListItemIcon>
          <ListItemText
            classes={{ primary: classes.selectAllText }}
            primary="Select All"
          />
        </MenuItem>
        {displayedOptions.map((option) => (
          <MenuItem key={option.id} value={option}>
            <ListItemIcon>
              <Checkbox checked={selected?.includes(option)} />
            </ListItemIcon>
            <ListItemText primary={option.title}>{option}</ListItemText>
          </MenuItem>
        ))}
      </Select>

      <p>{selected}</p>
    </FormControl>
  );
}

export default App;

util.js

import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles((theme) => ({
  formControl: {
    margin: theme.spacing(1),
    width: 300
  },
  indeterminateColor: {
    color: "#f50057"
  },
  selectAllText: {
    fontWeight: 500
  },
  selectedAll: {
    backgroundColor: "rgba(0, 0, 0, 0.08)",
    "&:hover": {
      backgroundColor: "rgba(0, 0, 0, 0.08)"
    }
  }
}));

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250
    }
  },
  getContentAnchorEl: null,
  anchorOrigin: {
    vertical: "bottom",
    horizontal: "center"
  },
  transformOrigin: {
    vertical: "top",
    horizontal: "center"
  },
  variant: "menu"
};

const options = [
  "Oliver Hansen",
  "Van Henry",
  "April Tucker",
  "Ralph Hubbard",
  "Omar Alexander",
  "Carlos Abbott",
  "Miriam Wagner",
  "Bradley Wilkerson",
  "Virginia Andrews",
  "Kelly Snyder"
];

export { useStyles, MenuProps, options };

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Greg--
Solution 3 KARTHIKEYAN.A