1<ListUpdateSelector
2 defaultSelectionStrings={[]}
3 allSelectionStrings={["selection1", "selection2", "selection3"]}
4 optionChangeHandler={(selectedValues) => {
5 someRef.current = selectedValues;
6 }}
7/>
1import CheckIcon from "@mui/icons-material/Check";
2import CloseIcon from "@mui/icons-material/Close";
3import { autocompleteClasses } from "@mui/material/Autocomplete";
4import { styled } from "@mui/material/styles";
5import useAutocomplete, {
6 AutocompleteGetTagProps,
7} from "@mui/material/useAutocomplete";
8import { CSSProperties, useEffect, useRef } from "react";
9
10const Root = styled("div")(
11 ({ theme }) => `
12 color: ${
13 theme.palette.mode === "dark" ? "rgba(255,255,255,0.65)" : "rgba(0,0,0,.85)"
14 };
15 font-size: 14px;
16`
17);
18
19const Label = styled("label")`
20 padding: 0 0 4px;
21 line-height: 1.5;
22 display: block;
23`;
24
25const InputWrapper = styled("div")(
26 ({ theme }) => `
27 border-radius: 4px;
28 flex-wrap: wrap;
29
30 &:hover {
31 border-color: ${theme.palette.mode === "dark" ? "#177ddc" : "#40a9ff"};
32 }
33
34 & div {
35 display: flex;
36 justify-content: space-between;
37 }
38
39 & input {
40 background-color: ${theme.palette.mode === "dark" ? "#141414" : "#fff"};
41 color: ${
42 theme.palette.mode === "dark"
43 ? "rgba(255,255,255,0.65)"
44 : "rgba(0,0,0,.85)"
45 };
46 height: 30px;
47 box-sizing: border-box;
48 padding: 4px 6px;
49 width: 1px;
50 border: 1px solid rgba(0, 0, 0, 0.1);
51 border-radius: 2px;
52 min-width: 30px;
53 flex-grow: 1;
54 margin: 0;
55 outline: 0;
56 width: 100%;
57 margin: 0px;
58 margin-top: 4px;
59 }
60`
61);
62
63interface TagProps extends ReturnType<AutocompleteGetTagProps> {
64 label: string;
65}
66
67function Tag(props: TagProps) {
68 const { label, onDelete, ...other } = props;
69 return (
70 <div {...other}>
71 <span>{label}</span>
72 <CloseIcon onClick={onDelete} />
73 </div>
74 );
75}
76
77const StyledTag = styled(Tag)<TagProps>(
78 ({ theme }) => `
79 display: flex;
80 align-items: center;
81 height: 24px;
82 margin: 2px 0px;
83 line-height: 22px;
84 background-color: ${
85 theme.palette.mode === "dark" ? "rgba(255,255,255,0.08)" : "#fafafa"
86 };
87 border: 1px solid ${theme.palette.mode === "dark" ? "#303030" : "#e8e8e8"};
88 border-radius: 2px;
89 box-sizing: content-box;
90 padding: 0 4px 0 10px;
91 outline: 0;
92 overflow: hidden;
93
94 &:focus {
95 border-color: ${theme.palette.mode === "dark" ? "#177ddc" : "#40a9ff"};
96 background-color: ${theme.palette.mode === "dark" ? "#003b57" : "#e6f7ff"};
97 }
98
99 & span {
100 overflow: hidden;
101 white-space: nowrap;
102 text-overflow: ellipsis;
103 }
104
105 & svg {
106 font-size: 12px;
107 cursor: pointer;
108 padding: 4px;
109 }
110`
111);
112
113const Listbox = styled("ul")(
114 ({ theme }) => `
115 width: 300px;
116 margin: 2px 0 0;
117 padding: 0;
118 position: absolute;
119 top: calc(100% + 5px);
120 list-style: none;
121 background-color: ${theme.palette.mode === "dark" ? "#141414" : "#fff"};
122 overflow: auto;
123 max-height: 250px;
124 border-radius: 4px;
125 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
126 z-index: 3;
127
128
129 & li {
130 padding: 5px 12px;
131 display: flex;
132
133 & span {
134 flex-grow: 1;
135 }
136
137 & svg {
138 color: transparent;
139 }
140 }
141
142 & li[aria-selected='true'] {
143 background-color: ${theme.palette.mode === "dark" ? "#2b2b2b" : "#fafafa"};
144 font-weight: 600;
145
146 & svg {
147 color: #1890ff;
148 }
149 }
150
151 & li.${autocompleteClasses.focused} {
152 background-color: ${theme.palette.mode === "dark" ? "#003b57" : "#e6f7ff"};
153 cursor: pointer;
154
155 & svg {
156 color: currentColor;
157 }
158 }
159`
160);
161
162export default function ListUpdateSelector({
163 defaultSelectionStrings,
164 allSelectionStrings,
165 optionChangeHandler,
166 style = {},
167 inputStyle = {},
168}: {
169 defaultSelectionStrings: string[];
170 allSelectionStrings: string[];
171 optionChangeHandler: (option: string[]) => void;
172 style?: CSSProperties;
173 inputStyle?: CSSProperties;
174}) {
175 const selections = allSelectionStrings;
176
177 const {
178 getRootProps,
179 getInputLabelProps,
180 getInputProps,
181 getTagProps,
182 getListboxProps,
183 getOptionProps,
184 groupedOptions,
185 value,
186 focused,
187 setAnchorEl,
188 } = useAutocomplete({
189 id: "selector-hook",
190 defaultValue: defaultSelectionStrings,
191 multiple: true,
192 options: selections,
193 getOptionLabel: (option) => option,
194 });
195
196 const optionChangeHandlerTakesEffect = useRef(false);
197
198 useEffect(() => {
199 // prevent handler is called on the first render.
200 if (optionChangeHandlerTakesEffect.current) {
201 optionChangeHandler(value);
202 } else {
203 optionChangeHandlerTakesEffect.current = true;
204 }
205 }, [value]);
206
207 return (
208 <Root>
209 <div
210 className="user-row-selector"
211 style={{ position: "relative", ...style }}
212 >
213 <div {...getRootProps()}>
214 <InputWrapper ref={setAnchorEl} className={focused ? "focused" : ""}>
215 {value.map((option: string, index: number) => (
216 <span title={option}>
217 <StyledTag label={option} {...getTagProps({ index })} />
218 </span>
219 ))}
220 <input {...getInputProps()} style={inputStyle} />
221 </InputWrapper>
222 </div>
223 {groupedOptions.length > 0 ? (
224 <Listbox {...getListboxProps()}>
225 {(groupedOptions as string[]).map((option, index) => (
226 <li {...getOptionProps({ option, index })}>
227 <span>{option}</span>
228 <CheckIcon fontSize="small" />
229 </li>
230 ))}
231 </Listbox>
232 ) : null}
233 </div>
234 </Root>
235 );
236}