@etsoo/materialui 1.0.56 → 1.0.58

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.
@@ -51,3 +51,57 @@ it('Render SelectEx', async () => {
51
51
 
52
52
  expect(itemChangeCallback).toBeCalledTimes(2);
53
53
  });
54
+
55
+ it('Render multiple SelectEx', async () => {
56
+ // Arrange
57
+ type T = { id: number; name: string };
58
+ const options: T[] = [
59
+ { id: 1, name: 'Name 1' },
60
+ { id: 2, name: 'Name 2' },
61
+ { id: 3, name: 'Name 3' }
62
+ ];
63
+
64
+ const itemChangeCallback = jest.fn((option, userAction) => {
65
+ if (userAction) expect(option.id).toBe(3);
66
+ else expect(option.id).toBe(1);
67
+ });
68
+
69
+ // Render component
70
+ const { baseElement } = render(
71
+ <SelectEx<T>
72
+ options={options}
73
+ name="test"
74
+ onItemChange={itemChangeCallback}
75
+ value={[1, 2]}
76
+ multiple
77
+ search
78
+ labelField="name"
79
+ />
80
+ );
81
+
82
+ expect(itemChangeCallback).toBeCalled();
83
+
84
+ // Act, click to show the list
85
+ const button = screen.getByRole('button');
86
+ fireEvent.mouseDown(button); // Not click
87
+
88
+ // Get list item
89
+ const itemName1 = await findByText(baseElement, 'Name 1');
90
+ const checkbox1 = itemName1.closest('li')?.querySelector('input');
91
+
92
+ expect(checkbox1?.checked).toBeTruthy();
93
+
94
+ const itemName3 = await findByText(baseElement, 'Name 3');
95
+ expect(itemName3.nodeName).toBe('SPAN');
96
+
97
+ // Checkbox
98
+ const checkbox3 = itemName3.closest('li')?.querySelector('input');
99
+
100
+ act(() => {
101
+ checkbox3?.click();
102
+ });
103
+
104
+ expect(checkbox3?.checked).toBeTruthy();
105
+
106
+ expect(itemChangeCallback).toBeCalledTimes(2);
107
+ });
@@ -4,7 +4,7 @@ import { ChangeEventHandler } from 'react';
4
4
  /**
5
5
  * Autocomplete extended props
6
6
  */
7
- export declare type AutocompleteExtendedProps<T extends object, D extends DataTypes.Keys<T>> = Omit<AutocompleteProps<T, undefined, false, false>, 'renderInput' | 'options'> & {
7
+ export declare type AutocompleteExtendedProps<T extends object, D extends DataTypes.Keys<T>> = Omit<AutocompleteProps<T, undefined, false, false>, 'renderInput' | 'options' | 'multiple'> & {
8
8
  /**
9
9
  * Id field
10
10
  */
package/lib/SelectEx.js CHANGED
@@ -11,26 +11,30 @@ import { ReactUtils } from '@etsoo/react';
11
11
  * @returns Component
12
12
  */
13
13
  export function SelectEx(props) {
14
- var _a;
15
14
  // Destruct
16
15
  const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemChange, onItemClick, onLoadData, multiple = false, name, options, refresh, search = false, autoAddBlankItem = search, value, onChange, fullWidth, ...rest } = props;
17
16
  // Options state
18
17
  const [localOptions, setOptions] = React.useState([]);
19
- const isMounted = React.useRef(true);
18
+ const isMounted = React.useRef(false);
20
19
  const doItemChange = (options, value, userAction) => {
21
20
  if (onItemChange == null)
22
21
  return;
23
- if (value == null || value === '') {
24
- onItemChange(undefined, userAction);
25
- return;
22
+ let option;
23
+ if (multiple && Array.isArray(value)) {
24
+ option = options.find((option) => value.includes(option[idField]));
25
+ }
26
+ else if (value == null || value === '') {
27
+ option = undefined;
28
+ }
29
+ else {
30
+ option = options.find((option) => option[idField] === value);
26
31
  }
27
- const option = options.find((option) => option[idField] === value);
28
32
  onItemChange(option, userAction);
29
33
  };
30
34
  const setOptionsAdd = (options) => {
31
35
  setOptions(options);
32
- if (localValue != null && localValue !== '')
33
- doItemChange(options, localValue, false);
36
+ if (valueSource != null)
37
+ doItemChange(options, valueSource, false);
34
38
  };
35
39
  // When options change
36
40
  // [options] will cause infinite loop
@@ -41,57 +45,44 @@ export function SelectEx(props) {
41
45
  setOptionsAdd(options);
42
46
  }, [options, propertyWay]);
43
47
  // Local value
44
- const valueSource = (_a = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value) !== null && _a !== void 0 ? _a : '';
45
- let localValue;
46
- if (multiple) {
47
- if (Array.isArray(valueSource))
48
- localValue = valueSource;
49
- else
50
- localValue = [valueSource];
51
- }
52
- else {
53
- localValue = valueSource;
54
- }
48
+ const v = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value;
49
+ const valueSource = multiple
50
+ ? v
51
+ ? Array.isArray(v)
52
+ ? v
53
+ : [v]
54
+ : []
55
+ : v !== null && v !== void 0 ? v : '';
55
56
  // Value state
56
- const [valueState, setValueStateBase] = React.useState();
57
+ const [valueState, setValueStateBase] = React.useState(valueSource);
57
58
  const valueRef = React.useRef();
58
59
  const setValueState = (newValue) => {
59
60
  valueRef.current = newValue;
60
61
  setValueStateBase(newValue);
61
62
  };
62
63
  React.useEffect(() => {
63
- if (localValue != null)
64
- setValueState(localValue);
65
- }, [localValue]);
64
+ if (valueSource != null)
65
+ setValueState(valueSource);
66
+ }, [valueSource]);
66
67
  // Label id
67
68
  const labelId = `selectex-label-${name}`;
68
- // Item checked or not
69
- const itemChecked = (id) => {
70
- if (Array.isArray(valueState))
71
- return valueState.indexOf(id) !== -1;
72
- return valueState === id;
73
- };
74
- // Change handler
75
- const handleChange = (event) => {
76
- const value = event.target.value;
77
- if (multiple && !Array.isArray(value))
78
- return setItemValue([value]);
79
- else
80
- return setItemValue(value);
81
- };
82
69
  // Set item
83
70
  const setItemValue = (id) => {
84
71
  var _a;
85
72
  if (id != valueRef.current) {
73
+ // Difference
74
+ const diff = multiple
75
+ ? Utils.arrayDifferences(id, valueRef.current)
76
+ : id;
86
77
  setValueState(id);
87
78
  const input = (_a = divRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('input');
88
79
  if (input) {
89
80
  // Different value, trigger change event
90
81
  ReactUtils.triggerChange(input, id, false);
91
82
  }
92
- return true;
83
+ return diff;
93
84
  }
94
- return false;
85
+ return undefined;
95
86
  };
96
87
  // Get option id
97
88
  const getId = (option) => {
@@ -123,7 +114,7 @@ export function SelectEx(props) {
123
114
  // When value change
124
115
  React.useEffect(() => {
125
116
  refreshData();
126
- }, [localValue]);
117
+ }, [valueSource]);
127
118
  // When layout ready
128
119
  React.useEffect(() => {
129
120
  var _a;
@@ -134,6 +125,7 @@ export function SelectEx(props) {
134
125
  setValueState(multiple ? [] : '');
135
126
  };
136
127
  input === null || input === void 0 ? void 0 : input.addEventListener('change', inputChange);
128
+ isMounted.current = true;
137
129
  return () => {
138
130
  isMounted.current = false;
139
131
  input === null || input === void 0 ? void 0 : input.removeEventListener('change', inputChange);
@@ -145,17 +137,21 @@ export function SelectEx(props) {
145
137
  React.createElement(InputLabel, { id: labelId, shrink: search
146
138
  ? MUGlobal.searchFieldShrink
147
139
  : MUGlobal.inputFieldShrink }, label),
148
- React.createElement(Select, { ref: divRef, value: localOptions.some((option) => itemChecked(getId(option)))
149
- ? valueState !== null && valueState !== void 0 ? valueState : ''
150
- : '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
140
+ React.createElement(Select, { ref: divRef, value: multiple
141
+ ? valueState
142
+ : localOptions.some((o) => o[idField] === valueState)
143
+ ? valueState
144
+ : '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
151
145
  if (onChange) {
152
146
  onChange(event, child);
153
147
  // event.preventDefault() will block executing
154
148
  if (event.defaultPrevented)
155
149
  return;
156
150
  }
157
- if (handleChange(event)) {
158
- doItemChange(localOptions, event.target.value, true);
151
+ // Set item value
152
+ const diff = setItemValue(event.target.value);
153
+ if (diff != null) {
154
+ doItemChange(localOptions, diff, true);
159
155
  }
160
156
  }, renderValue: (selected) => {
161
157
  // The text shows up
@@ -181,7 +177,9 @@ export function SelectEx(props) {
181
177
  }, style: itemStyle == null
182
178
  ? undefined
183
179
  : itemStyle(option) },
184
- multiple && (React.createElement(Checkbox, { checked: itemChecked(id) })),
180
+ multiple && (React.createElement(Checkbox, { checked: Array.isArray(valueState)
181
+ ? valueState.includes(id)
182
+ : valueState === id })),
185
183
  React.createElement(ListItemText, { primary: label }),
186
184
  itemIconRenderer && (React.createElement(ListItemRightIcon, null, itemIconRenderer(option[idField])))));
187
185
  })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/materialui",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "description": "TypeScript Material-UI Implementation",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -54,7 +54,7 @@
54
54
  "@etsoo/appscript": "^1.3.28",
55
55
  "@etsoo/notificationbase": "^1.1.13",
56
56
  "@etsoo/react": "^1.6.23",
57
- "@etsoo/shared": "^1.1.70",
57
+ "@etsoo/shared": "^1.1.71",
58
58
  "@mui/icons-material": "^5.10.9",
59
59
  "@mui/material": "^5.10.12",
60
60
  "@types/pica": "^9.0.1",
@@ -10,7 +10,7 @@ export type AutocompleteExtendedProps<
10
10
  D extends DataTypes.Keys<T>
11
11
  > = Omit<
12
12
  AutocompleteProps<T, undefined, false, false>,
13
- 'renderInput' | 'options'
13
+ 'renderInput' | 'options' | 'multiple'
14
14
  > & {
15
15
  /**
16
16
  * Id field
package/src/SelectEx.tsx CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  MenuItem,
9
9
  OutlinedInput,
10
10
  Select,
11
- SelectChangeEvent,
12
11
  SelectProps,
13
12
  Stack
14
13
  } from '@mui/material';
@@ -143,7 +142,7 @@ export function SelectEx<
143
142
 
144
143
  // Options state
145
144
  const [localOptions, setOptions] = React.useState<readonly T[]>([]);
146
- const isMounted = React.useRef(true);
145
+ const isMounted = React.useRef(false);
147
146
 
148
147
  const doItemChange = (
149
148
  options: readonly T[],
@@ -151,18 +150,21 @@ export function SelectEx<
151
150
  userAction: boolean
152
151
  ) => {
153
152
  if (onItemChange == null) return;
154
- if (value == null || value === '') {
155
- onItemChange(undefined, userAction);
156
- return;
153
+
154
+ let option: T | undefined;
155
+ if (multiple && Array.isArray(value)) {
156
+ option = options.find((option) => value.includes(option[idField]));
157
+ } else if (value == null || value === '') {
158
+ option = undefined;
159
+ } else {
160
+ option = options.find((option) => option[idField] === value);
157
161
  }
158
- const option = options.find((option) => option[idField] === value);
159
162
  onItemChange(option, userAction);
160
163
  };
161
164
 
162
165
  const setOptionsAdd = (options: readonly T[]) => {
163
166
  setOptions(options);
164
- if (localValue != null && localValue !== '')
165
- doItemChange(options, localValue, false);
167
+ if (valueSource != null) doItemChange(options, valueSource, false);
166
168
  };
167
169
 
168
170
  // When options change
@@ -174,17 +176,18 @@ export function SelectEx<
174
176
  }, [options, propertyWay]);
175
177
 
176
178
  // Local value
177
- const valueSource = defaultValue ?? value ?? '';
178
- let localValue: unknown | unknown[];
179
- if (multiple) {
180
- if (Array.isArray(valueSource)) localValue = valueSource;
181
- else localValue = [valueSource];
182
- } else {
183
- localValue = valueSource;
184
- }
179
+ const v = defaultValue ?? value;
180
+ const valueSource = multiple
181
+ ? v
182
+ ? Array.isArray(v)
183
+ ? v
184
+ : [v]
185
+ : []
186
+ : v ?? '';
185
187
 
186
188
  // Value state
187
- const [valueState, setValueStateBase] = React.useState<unknown>();
189
+ const [valueState, setValueStateBase] =
190
+ React.useState<unknown>(valueSource);
188
191
  const valueRef = React.useRef<unknown>();
189
192
  const setValueState = (newValue: unknown) => {
190
193
  valueRef.current = newValue;
@@ -192,28 +195,23 @@ export function SelectEx<
192
195
  };
193
196
 
194
197
  React.useEffect(() => {
195
- if (localValue != null) setValueState(localValue);
196
- }, [localValue]);
198
+ if (valueSource != null) setValueState(valueSource);
199
+ }, [valueSource]);
197
200
 
198
201
  // Label id
199
202
  const labelId = `selectex-label-${name}`;
200
203
 
201
- // Item checked or not
202
- const itemChecked = (id: unknown) => {
203
- if (Array.isArray(valueState)) return valueState.indexOf(id) !== -1;
204
- return valueState === id;
205
- };
206
-
207
- // Change handler
208
- const handleChange = (event: SelectChangeEvent<unknown>) => {
209
- const value = event.target.value;
210
- if (multiple && !Array.isArray(value)) return setItemValue([value]);
211
- else return setItemValue(value);
212
- };
213
-
214
204
  // Set item
215
205
  const setItemValue = (id: unknown) => {
216
206
  if (id != valueRef.current) {
207
+ // Difference
208
+ const diff = multiple
209
+ ? Utils.arrayDifferences(
210
+ id as T[D][],
211
+ valueRef.current as T[D][]
212
+ )
213
+ : id;
214
+
217
215
  setValueState(id);
218
216
 
219
217
  const input = divRef.current?.querySelector('input');
@@ -221,9 +219,9 @@ export function SelectEx<
221
219
  // Different value, trigger change event
222
220
  ReactUtils.triggerChange(input, id as string, false);
223
221
  }
224
- return true;
222
+ return diff;
225
223
  }
226
- return false;
224
+ return undefined;
227
225
  };
228
226
 
229
227
  // Get option id
@@ -257,7 +255,7 @@ export function SelectEx<
257
255
  // When value change
258
256
  React.useEffect(() => {
259
257
  refreshData();
260
- }, [localValue]);
258
+ }, [valueSource]);
261
259
 
262
260
  // When layout ready
263
261
  React.useEffect(() => {
@@ -268,6 +266,8 @@ export function SelectEx<
268
266
  };
269
267
  input?.addEventListener('change', inputChange);
270
268
 
269
+ isMounted.current = true;
270
+
271
271
  return () => {
272
272
  isMounted.current = false;
273
273
  input?.removeEventListener('change', inputChange);
@@ -297,10 +297,12 @@ export function SelectEx<
297
297
  <Select
298
298
  ref={divRef}
299
299
  value={
300
- localOptions.some((option) =>
301
- itemChecked(getId(option))
302
- )
303
- ? valueState ?? ''
300
+ multiple
301
+ ? valueState
302
+ : localOptions.some(
303
+ (o) => o[idField] === valueState
304
+ )
305
+ ? valueState
304
306
  : ''
305
307
  }
306
308
  input={
@@ -321,12 +323,10 @@ export function SelectEx<
321
323
  if (event.defaultPrevented) return;
322
324
  }
323
325
 
324
- if (handleChange(event)) {
325
- doItemChange(
326
- localOptions,
327
- event.target.value,
328
- true
329
- );
326
+ // Set item value
327
+ const diff = setItemValue(event.target.value);
328
+ if (diff != null) {
329
+ doItemChange(localOptions, diff, true);
330
330
  }
331
331
  }}
332
332
  renderValue={(selected) => {
@@ -369,7 +369,13 @@ export function SelectEx<
369
369
  }
370
370
  >
371
371
  {multiple && (
372
- <Checkbox checked={itemChecked(id)} />
372
+ <Checkbox
373
+ checked={
374
+ Array.isArray(valueState)
375
+ ? valueState.includes(id)
376
+ : valueState === id
377
+ }
378
+ />
373
379
  )}
374
380
  <ListItemText primary={label} />
375
381
  {itemIconRenderer && (