😎

Material-UIでSearchableSelectを作る

2023/03/01に公開

概要

  • タイトル通りですが、Material-UIでSearchableなSelectコンポーネントを実装したので記事にしてみました
  • ついでにClearableも実装しています
  • 下記2点ができてないです(T_T)
    • 選択時にTextFieldにFocusがかかるようにする
    • 特定条件でのwarningの除去
  • 各バージョン
{
    "typescript": "^4.9.5"
    "react": "^18.2.0"
    "@mui/material": "^5.11.10"
}

詳細

  • 呼び出し元
  <SearchableSelect
    label="都道府県"
    setValue={setPrefectures}
    options={prefecturesList}
  />
  • SearchableSelectコンポーネント
import React, { useMemo, useState } from "react";
import {
  Box,
  FormControl,
  Select as MuiSelect,
  MenuItem,
  InputLabel,
  ListSubheader,
  TextField,
  InputAdornment,
  type SelectChangeEvent,
  IconButton,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import ClearIcon from "@mui/icons-material/Clear";

interface ISelectObject {
  value: string;
  label: string;
}

interface IProps {
  label: string;
  options: ISelectObject[];
  setValue: React.Dispatch<React.SetStateAction<string>>;
  isClearable?: boolean;
}

const containsText = (text: string, searchText: string): boolean =>
  text.toLowerCase().includes(searchText.toLowerCase());

const SearchableSelect: React.FC<IProps> = (props) => {
  // HACK: TextFieldにFocusがかかるようにする(autoFocusつけたり色々試したができなかった)
  // HACK: 何か選択済みで検索をかけるとwarningが表示される
  //       material-uiが選択済みの状態でoptionがfilterされるのを考慮していないため

  const { label, options, setValue, isClearable = true } = props;
  const [displayValue, setDisplayValue] = useState("");
  const [searchText, setSearchText] = useState("");

  const displayedOptions = useMemo(
    () => options.filter((option) => containsText(option.label, searchText)),
    [searchText, options]
  );

  const handleChange = (event: SelectChangeEvent<string>): void => {
    const selectedObject = JSON.parse(event.target.value) as ISelectObject;
    setValue(selectedObject.value);
    setDisplayValue(selectedObject.label);
  };

  const handleClearClick = (): void => {
    setValue("");
    setDisplayValue("");
  };

  return (
    <Box sx={{ m: 2 }}>
      <FormControl fullWidth>
        <InputLabel id="searchable-select-label">{label}</InputLabel>
        <MuiSelect
          id="searchable-select-id"
          labelId="searchable-select-label"
          label={label}
          onChange={handleChange}
          onClose={() => {
            setSearchText("");
          }}
          defaultValue=""
          renderValue={() => displayValue}
          endAdornment={
            isClearable ? (
              <IconButton
                sx={{
                  display: displayValue === "" ? "none" : "inline-flex",
                  marginRight: "10px",
                }}
                onClick={handleClearClick}
              >
                <ClearIcon />
              </IconButton>
            ) : (
              <></>
            )
          }
        >
          <ListSubheader>
            <TextField
              fullWidth
              size="small"
              placeholder="検索"
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <SearchIcon />
                  </InputAdornment>
                ),
              }}
              onChange={(e) => {
                setSearchText(e.target.value);
              }}
              onKeyDown={(e) => {
                if (e.key !== "Escape") {
                  // Prevents autoselecting item while typing (default Select behaviour)
                  e.stopPropagation();
                }
              }}
            />
          </ListSubheader>

          {displayedOptions.map((option, index) => (
            <MenuItem key={index} value={JSON.stringify(option)}>
              {option.label}
            </MenuItem>
          ))}
        </MuiSelect>
      </FormControl>
    </Box>
  );
};

export { SearchableSelect };

まとめ

  • 本家本元になかったので自前で実装しました
  • Material-UIはコンポーネントの組み合わせで色々できるのがいいですね!

Discussion