😎
Material-UIでSearchableSelectを作る
概要
- タイトル通りですが、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