@etsoo/materialui 1.1.26 → 1.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ComboBox.d.ts +2 -2
- package/lib/ComboBox.js +19 -16
- package/lib/Tiplist.d.ts +3 -3
- package/lib/Tiplist.js +19 -20
- package/package.json +6 -6
- package/src/ComboBox.tsx +267 -260
- package/src/Tiplist.tsx +267 -264
package/src/ComboBox.tsx
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from
|
|
2
|
+
DataTypes,
|
|
3
|
+
IdDefaultType,
|
|
4
|
+
Keyboard,
|
|
5
|
+
LabelDefaultType,
|
|
6
|
+
ListType
|
|
7
|
+
} from "@etsoo/shared";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from
|
|
13
|
-
import React from
|
|
14
|
-
import { Utils as SharedUtils } from
|
|
15
|
-
import { ReactUtils } from
|
|
9
|
+
Autocomplete,
|
|
10
|
+
AutocompleteRenderInputParams,
|
|
11
|
+
Checkbox
|
|
12
|
+
} from "@mui/material";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { Utils as SharedUtils } from "@etsoo/shared";
|
|
15
|
+
import { ReactUtils } from "@etsoo/react";
|
|
16
16
|
|
|
17
|
-
import CheckBoxOutlineBlankIcon from
|
|
18
|
-
import CheckBoxIcon from
|
|
19
|
-
import { AutocompleteExtendedProps } from
|
|
20
|
-
import { SearchField } from
|
|
21
|
-
import { InputField } from
|
|
17
|
+
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
|
|
18
|
+
import CheckBoxIcon from "@mui/icons-material/CheckBox";
|
|
19
|
+
import { AutocompleteExtendedProps } from "./AutocompleteExtendedProps";
|
|
20
|
+
import { SearchField } from "./SearchField";
|
|
21
|
+
import { InputField } from "./InputField";
|
|
22
|
+
import { globalApp } from "./app/ReactApp";
|
|
22
23
|
|
|
23
24
|
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
|
24
25
|
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
|
@@ -27,49 +28,49 @@ const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
|
|
27
28
|
* ComboBox props
|
|
28
29
|
*/
|
|
29
30
|
export type ComboBoxProps<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
T extends object,
|
|
32
|
+
D extends DataTypes.Keys<T>,
|
|
33
|
+
L extends DataTypes.Keys<T, string>
|
|
33
34
|
> = AutocompleteExtendedProps<T, D> & {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Auto add blank item
|
|
37
|
+
*/
|
|
38
|
+
autoAddBlankItem?: boolean;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Data readonly
|
|
42
|
+
*/
|
|
43
|
+
dataReadonly?: boolean;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Label field
|
|
47
|
+
*/
|
|
48
|
+
labelField?: L;
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Load data callback
|
|
52
|
+
*/
|
|
53
|
+
loadData?: () => PromiseLike<T[] | null | undefined>;
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Multiple
|
|
57
|
+
*/
|
|
58
|
+
multiple?: boolean;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* On load data handler
|
|
62
|
+
*/
|
|
63
|
+
onLoadData?: (options: T[]) => void;
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Array of options.
|
|
67
|
+
*/
|
|
68
|
+
options?: ReadonlyArray<T>;
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Id values
|
|
72
|
+
*/
|
|
73
|
+
idValues?: T[D][];
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -78,233 +79,239 @@ export type ComboBoxProps<
|
|
|
78
79
|
* @returns Component
|
|
79
80
|
*/
|
|
80
81
|
export function ComboBox<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
T extends object = ListType,
|
|
83
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>,
|
|
84
|
+
L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
|
|
84
85
|
>(props: ComboBoxProps<T, D, L>) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
search = false,
|
|
88
|
-
autoAddBlankItem = search,
|
|
89
|
-
idField = 'id' as D,
|
|
90
|
-
idValue,
|
|
91
|
-
idValues,
|
|
92
|
-
inputError,
|
|
93
|
-
inputHelperText,
|
|
94
|
-
inputMargin,
|
|
95
|
-
inputOnChange,
|
|
96
|
-
inputRequired,
|
|
97
|
-
inputVariant,
|
|
98
|
-
defaultValue,
|
|
99
|
-
label,
|
|
100
|
-
labelField = 'label' as L,
|
|
101
|
-
loadData,
|
|
102
|
-
multiple = false,
|
|
103
|
-
onLoadData,
|
|
104
|
-
name,
|
|
105
|
-
inputAutoComplete = 'new-password', // disable autocomplete and autofill, 'off' does not work
|
|
106
|
-
options,
|
|
107
|
-
dataReadonly = true,
|
|
108
|
-
readOnly,
|
|
109
|
-
onChange,
|
|
110
|
-
openOnFocus = true,
|
|
111
|
-
value,
|
|
112
|
-
disableCloseOnSelect = multiple,
|
|
113
|
-
renderOption = multiple
|
|
114
|
-
? (props, option, { selected }) => (
|
|
115
|
-
<li {...props}>
|
|
116
|
-
<>
|
|
117
|
-
<Checkbox
|
|
118
|
-
icon={icon}
|
|
119
|
-
checkedIcon={checkedIcon}
|
|
120
|
-
style={{ marginRight: 8 }}
|
|
121
|
-
checked={selected}
|
|
122
|
-
/>
|
|
123
|
-
{option[labelField]}
|
|
124
|
-
</>
|
|
125
|
-
</li>
|
|
126
|
-
)
|
|
127
|
-
: undefined,
|
|
128
|
-
getOptionLabel = (option: T) => `${option[labelField]}`,
|
|
129
|
-
sx = { minWidth: '150px' },
|
|
130
|
-
...rest
|
|
131
|
-
} = props;
|
|
86
|
+
// Labels
|
|
87
|
+
const labels = globalApp?.getLabels("noOptions", "loading");
|
|
132
88
|
|
|
133
|
-
|
|
134
|
-
|
|
89
|
+
// Destruct
|
|
90
|
+
const {
|
|
91
|
+
search = false,
|
|
92
|
+
autoAddBlankItem = search,
|
|
93
|
+
idField = "id" as D,
|
|
94
|
+
idValue,
|
|
95
|
+
idValues,
|
|
96
|
+
inputError,
|
|
97
|
+
inputHelperText,
|
|
98
|
+
inputMargin,
|
|
99
|
+
inputOnChange,
|
|
100
|
+
inputRequired,
|
|
101
|
+
inputVariant,
|
|
102
|
+
defaultValue,
|
|
103
|
+
label,
|
|
104
|
+
labelField = "label" as L,
|
|
105
|
+
loadData,
|
|
106
|
+
multiple = false,
|
|
107
|
+
onLoadData,
|
|
108
|
+
name,
|
|
109
|
+
inputAutoComplete = "new-password", // disable autocomplete and autofill, 'off' does not work
|
|
110
|
+
options,
|
|
111
|
+
dataReadonly = true,
|
|
112
|
+
readOnly,
|
|
113
|
+
onChange,
|
|
114
|
+
openOnFocus = true,
|
|
115
|
+
value,
|
|
116
|
+
disableCloseOnSelect = multiple,
|
|
117
|
+
renderOption = multiple
|
|
118
|
+
? (props, option, { selected }) => (
|
|
119
|
+
<li {...props}>
|
|
120
|
+
<>
|
|
121
|
+
<Checkbox
|
|
122
|
+
icon={icon}
|
|
123
|
+
checkedIcon={checkedIcon}
|
|
124
|
+
style={{ marginRight: 8 }}
|
|
125
|
+
checked={selected}
|
|
126
|
+
/>
|
|
127
|
+
{option[labelField]}
|
|
128
|
+
</>
|
|
129
|
+
</li>
|
|
130
|
+
)
|
|
131
|
+
: undefined,
|
|
132
|
+
getOptionLabel = (option: T) => `${option[labelField]}`,
|
|
133
|
+
sx = { minWidth: "150px" },
|
|
134
|
+
noOptionsText = labels?.noOptions,
|
|
135
|
+
loadingText = labels?.loading,
|
|
136
|
+
...rest
|
|
137
|
+
} = props;
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const isMounted = React.useRef(true);
|
|
139
|
+
// Value input ref
|
|
140
|
+
const inputRef = React.createRef<HTMLInputElement>();
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
React.useEffect(() => {
|
|
144
|
-
if (propertyWay && options != null) setOptions(options);
|
|
145
|
-
}, [options, propertyWay]);
|
|
142
|
+
// Options state
|
|
143
|
+
const [localOptions, setOptions] = React.useState(options ?? []);
|
|
144
|
+
const isMounted = React.useRef(true);
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: idValues != null
|
|
154
|
-
? localOptions.filter((o) => idValues?.includes(o[idField]))
|
|
155
|
-
: defaultValue ?? value;
|
|
156
|
-
} else {
|
|
157
|
-
localValue =
|
|
158
|
-
idValue != null
|
|
159
|
-
? localOptions.find((o) => o[idField] === idValue)
|
|
160
|
-
: idValues != null
|
|
161
|
-
? localOptions.filter((o) => idValues?.includes(o[idField]))
|
|
162
|
-
: defaultValue ?? value;
|
|
163
|
-
}
|
|
146
|
+
// When options change
|
|
147
|
+
// [options] will cause infinite loop
|
|
148
|
+
const propertyWay = loadData == null;
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
if (propertyWay && options != null) setOptions(options);
|
|
151
|
+
}, [options, propertyWay]);
|
|
164
152
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
153
|
+
// Local default value
|
|
154
|
+
let localValue: T | T[] | null | undefined;
|
|
155
|
+
if (multiple) {
|
|
156
|
+
localValue =
|
|
157
|
+
idValue != null
|
|
158
|
+
? localOptions.filter((o) => o[idField] === idValue)
|
|
159
|
+
: idValues != null
|
|
160
|
+
? localOptions.filter((o) => idValues?.includes(o[idField]))
|
|
161
|
+
: defaultValue ?? value;
|
|
162
|
+
} else {
|
|
163
|
+
localValue =
|
|
164
|
+
idValue != null
|
|
165
|
+
? localOptions.find((o) => o[idField] === idValue)
|
|
166
|
+
: idValues != null
|
|
167
|
+
? localOptions.filter((o) => idValues?.includes(o[idField]))
|
|
168
|
+
: defaultValue ?? value;
|
|
169
|
+
}
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
// State
|
|
172
|
+
// null for controlled
|
|
173
|
+
const [stateValue, setStateValue] = React.useState<T | T[] | null>(null);
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Object.assign(params, { readOnly });
|
|
175
|
+
React.useEffect(() => {
|
|
176
|
+
if (localValue != null) setStateValue(localValue);
|
|
177
|
+
}, [localValue]);
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
// Add readOnly
|
|
180
|
+
const addReadOnly = (params: AutocompleteRenderInputParams) => {
|
|
181
|
+
if (readOnly != null) {
|
|
182
|
+
Object.assign(params, { readOnly });
|
|
183
|
+
|
|
184
|
+
if (readOnly) {
|
|
185
|
+
Object.assign(params.inputProps, { "data-reset": true });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
+
if (dataReadonly) {
|
|
190
|
+
params.inputProps.onKeyDown = (event) => {
|
|
191
|
+
if (Keyboard.isTypingContent(event.key)) {
|
|
192
|
+
event.preventDefault();
|
|
189
193
|
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
// https://stackoverflow.com/questions/15738259/disabling-chrome-autofill
|
|
198
|
+
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html
|
|
199
|
+
Object.assign(params.inputProps, { autoComplete: inputAutoComplete });
|
|
194
200
|
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
return params;
|
|
202
|
+
};
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
const getValue = (value: T | T[] | null): string => {
|
|
205
|
+
if (value == null) return "";
|
|
206
|
+
if (Array.isArray(value))
|
|
207
|
+
return value.map((item) => item[idField]).join(",");
|
|
208
|
+
return `${value[idField]}`;
|
|
209
|
+
};
|
|
204
210
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
const setInputValue = (value: T | T[] | null) => {
|
|
212
|
+
// Set state
|
|
213
|
+
setStateValue(value);
|
|
208
214
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
// Input value
|
|
216
|
+
const input = inputRef.current;
|
|
217
|
+
if (input) {
|
|
218
|
+
// Update value
|
|
219
|
+
const newValue = getValue(value);
|
|
214
220
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
if (newValue !== input.value) {
|
|
222
|
+
// Different value, trigger change event
|
|
223
|
+
ReactUtils.triggerChange(input, newValue, false);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
221
227
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
if (propertyWay || loadData == null) return;
|
|
230
|
+
loadData().then((result) => {
|
|
231
|
+
if (result == null || !isMounted.current) return;
|
|
232
|
+
if (onLoadData) onLoadData(result);
|
|
233
|
+
if (autoAddBlankItem) {
|
|
234
|
+
SharedUtils.addBlankItem(result, idField, labelField);
|
|
235
|
+
}
|
|
236
|
+
setOptions(result);
|
|
237
|
+
});
|
|
238
|
+
}, [
|
|
239
|
+
propertyWay,
|
|
240
|
+
loadData,
|
|
241
|
+
onLoadData,
|
|
242
|
+
autoAddBlankItem,
|
|
243
|
+
idField,
|
|
244
|
+
labelField
|
|
245
|
+
]);
|
|
240
246
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
React.useEffect(() => {
|
|
248
|
+
return () => {
|
|
249
|
+
isMounted.current = false;
|
|
250
|
+
};
|
|
251
|
+
}, []);
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
253
|
+
// Layout
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
<input
|
|
257
|
+
ref={inputRef}
|
|
258
|
+
data-reset="true"
|
|
259
|
+
type="text"
|
|
260
|
+
style={{ display: "none" }}
|
|
261
|
+
name={name}
|
|
262
|
+
value={getValue(stateValue)}
|
|
263
|
+
readOnly
|
|
264
|
+
onChange={inputOnChange}
|
|
265
|
+
/>
|
|
266
|
+
{/* Previous input will reset first with "disableClearable = false", next input trigger change works */}
|
|
267
|
+
<Autocomplete<T, boolean | undefined, false, false>
|
|
268
|
+
value={multiple ? stateValue ?? [] : stateValue}
|
|
269
|
+
multiple={multiple}
|
|
270
|
+
disableCloseOnSelect={disableCloseOnSelect}
|
|
271
|
+
getOptionLabel={getOptionLabel}
|
|
272
|
+
isOptionEqualToValue={(option: T, value: T) =>
|
|
273
|
+
option[idField] === value[idField]
|
|
274
|
+
}
|
|
275
|
+
onChange={(event, value, reason, details) => {
|
|
276
|
+
// Set value
|
|
277
|
+
setInputValue(value);
|
|
272
278
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
helperText={inputHelperText}
|
|
301
|
-
/>
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
options={localOptions}
|
|
305
|
-
renderOption={renderOption}
|
|
306
|
-
{...rest}
|
|
279
|
+
// Custom
|
|
280
|
+
if (onChange != null) onChange(event, value, reason, details);
|
|
281
|
+
}}
|
|
282
|
+
openOnFocus={openOnFocus}
|
|
283
|
+
sx={sx}
|
|
284
|
+
renderInput={(params) =>
|
|
285
|
+
search ? (
|
|
286
|
+
<SearchField
|
|
287
|
+
{...addReadOnly(params)}
|
|
288
|
+
label={label}
|
|
289
|
+
name={name + "Input"}
|
|
290
|
+
margin={inputMargin}
|
|
291
|
+
variant={inputVariant}
|
|
292
|
+
required={inputRequired}
|
|
293
|
+
error={inputError}
|
|
294
|
+
helperText={inputHelperText}
|
|
295
|
+
/>
|
|
296
|
+
) : (
|
|
297
|
+
<InputField
|
|
298
|
+
{...addReadOnly(params)}
|
|
299
|
+
label={label}
|
|
300
|
+
name={name + "Input"}
|
|
301
|
+
margin={inputMargin}
|
|
302
|
+
variant={inputVariant}
|
|
303
|
+
required={inputRequired}
|
|
304
|
+
error={inputError}
|
|
305
|
+
helperText={inputHelperText}
|
|
307
306
|
/>
|
|
308
|
-
|
|
309
|
-
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
options={localOptions}
|
|
310
|
+
renderOption={renderOption}
|
|
311
|
+
noOptionsText={noOptionsText}
|
|
312
|
+
loadingText={loadingText}
|
|
313
|
+
{...rest}
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
310
317
|
}
|