@etsoo/materialui 1.0.55 → 1.0.57

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,50 @@ 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 itemName3 = await findByText(baseElement, 'Name 3');
90
+ expect(itemName3.nodeName).toBe('SPAN');
91
+
92
+ // Checkbox
93
+ const checkbox = itemName3.closest('li')?.querySelector('input');
94
+
95
+ act(() => {
96
+ checkbox?.click();
97
+ });
98
+
99
+ expect(itemChangeCallback).toBeCalledTimes(2);
100
+ });
@@ -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
  */
@@ -0,0 +1,56 @@
1
+ import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
2
+ import { ListItemButtonProps, ListProps } from '@mui/material';
3
+ import React from 'react';
4
+ declare type QueryData = {
5
+ title?: string;
6
+ };
7
+ /**
8
+ * List chooser button props
9
+ */
10
+ export interface ListChooserButtonProps<T extends object, D extends DataTypes.Keys<T>> {
11
+ (id: T[D]): ListItemButtonProps;
12
+ }
13
+ /**
14
+ * List chooser props
15
+ */
16
+ export declare type ListChooserProps<T extends object, D extends DataTypes.Keys<T>, Q extends object> = ListProps & {
17
+ /**
18
+ * Condition renderer
19
+ */
20
+ conditionRenderer?: (rq: Partial<Q>, delayed: DelayedExecutorType) => React.ReactNode;
21
+ /**
22
+ * List item renderer
23
+ */
24
+ itemRenderer?: (data: T, props: ListChooserButtonProps<T, D>) => React.ReactNode;
25
+ /**
26
+ * Label field
27
+ */
28
+ labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
29
+ /**
30
+ * Id field
31
+ */
32
+ idField?: D;
33
+ /**
34
+ * Load data callback
35
+ */
36
+ loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
37
+ /**
38
+ * Multiple selected
39
+ */
40
+ multiple?: boolean;
41
+ /**
42
+ * Item onchange callback
43
+ */
44
+ onItemChange: (items: T[], ids: T[D][]) => void;
45
+ /**
46
+ * Title
47
+ */
48
+ title: string;
49
+ };
50
+ /**
51
+ * List chooser
52
+ * @param props Props
53
+ * @returns Component
54
+ */
55
+ export declare function ListChooser<T extends object, D extends DataTypes.Keys<T> = IdDefaultType<T>, Q extends object = QueryData>(props: ListChooserProps<T, D, Q>): JSX.Element;
56
+ export {};
@@ -0,0 +1,79 @@
1
+ import { useDelayedExecutor } from '@etsoo/react';
2
+ import { List, ListItem, ListItemButton, ListItemText, TextField } from '@mui/material';
3
+ import React from 'react';
4
+ import { VBox } from './FlexBox';
5
+ /**
6
+ * List chooser
7
+ * @param props Props
8
+ * @returns Component
9
+ */
10
+ export function ListChooser(props) {
11
+ var _a;
12
+ // Selected ids state
13
+ const [selectedIds, setSelectedIds] = React.useState([]);
14
+ const selectProps = (id) => ({
15
+ selected: selectedIds.includes(id),
16
+ onClick: () => {
17
+ if (multiple) {
18
+ const index = selectedIds.indexOf(id);
19
+ if (index === -1)
20
+ selectedIds.push(id);
21
+ else
22
+ selectedIds.splice(index, 1);
23
+ setSelectedIds([...selectedIds]);
24
+ }
25
+ else {
26
+ setSelectedIds([id]);
27
+ }
28
+ }
29
+ });
30
+ // Destruct
31
+ const { conditionRenderer = (rq, delayed) => (React.createElement(TextField, { autoFocus: true, margin: "dense", name: "title", label: title, fullWidth: true, variant: "standard", inputProps: { maxLength: 128 }, onChange: (event) => {
32
+ Reflect.set(rq, 'title', event.target.value);
33
+ delayed.call();
34
+ } })), itemRenderer = (item, selectProps) => {
35
+ const id = item[idField];
36
+ const label = typeof labelField === 'function'
37
+ ? labelField(item)
38
+ : Reflect.get(item, labelField);
39
+ return (React.createElement(ListItem, { disableGutters: true, key: `${id}` },
40
+ React.createElement(ListItemButton, { ...selectProps(id) },
41
+ React.createElement(ListItemText, { primary: label }))));
42
+ }, idField = 'id', labelField = 'label', loadData, multiple = false, onItemChange, title, ...rest } = props;
43
+ // Default minimum height
44
+ (_a = rest.sx) !== null && _a !== void 0 ? _a : (rest.sx = { minHeight: '220px' });
45
+ // State
46
+ const [items, setItems] = React.useState([]);
47
+ // Query request data
48
+ const mounted = React.useRef(false);
49
+ const rq = React.useRef({});
50
+ // Delayed execution
51
+ const delayed = useDelayedExecutor(async () => {
52
+ const result = await loadData(rq.current);
53
+ if (result == null || !mounted.current)
54
+ return;
55
+ if (!multiple &&
56
+ selectedIds.length > 0 &&
57
+ !result.some((item) => selectedIds.includes(item[idField]))) {
58
+ setSelectedIds([]);
59
+ }
60
+ setItems(result);
61
+ }, 480);
62
+ React.useEffect(() => {
63
+ if (!mounted.current)
64
+ return;
65
+ onItemChange(items.filter((item) => selectedIds.includes(item[idField])), selectedIds);
66
+ }, [selectedIds]);
67
+ React.useEffect(() => {
68
+ mounted.current = true;
69
+ delayed.call(0);
70
+ return () => {
71
+ mounted.current = false;
72
+ delayed.clear();
73
+ };
74
+ }, [delayed]);
75
+ // Layout
76
+ return (React.createElement(VBox, null,
77
+ conditionRenderer(rq.current, delayed),
78
+ React.createElement(List, { disablePadding: true, dense: true, ...rest }, items.map((item) => itemRenderer(item, selectProps)))));
79
+ }
@@ -63,6 +63,10 @@ export declare type OptionGroupProps<T extends object, D extends DataTypes.Keys<
63
63
  * Display group of elements in a compact row
64
64
  */
65
65
  row?: boolean;
66
+ /**
67
+ * Item size
68
+ */
69
+ itemSize?: 'small' | 'medium';
66
70
  };
67
71
  /**
68
72
  * OptionGroup
@@ -8,7 +8,7 @@ import React from 'react';
8
8
  */
9
9
  export function OptionGroup(props) {
10
10
  // Destruct
11
- const { getOptionLabel, defaultValue, idField = 'id', label, labelField = 'label', multiple = false, mRef, name, onValueChange, options, readOnly, row, size, ...rest } = props;
11
+ const { getOptionLabel, defaultValue, idField = 'id', label, labelField = 'label', multiple = false, mRef, name, onValueChange, options, readOnly, row, itemSize, ...rest } = props;
12
12
  // Get option value
13
13
  // D type should be the source id type
14
14
  const getOptionValue = (option) => {
@@ -45,7 +45,7 @@ export function OptionGroup(props) {
45
45
  // Value
46
46
  const ov = getOptionValue(option);
47
47
  // Control
48
- const control = multiple ? (React.createElement(Checkbox, { name: name, readOnly: readOnly, size: size, checked: itemChecked(option), disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), onChange: (event) => {
48
+ const control = multiple ? (React.createElement(Checkbox, { name: name, readOnly: readOnly, size: itemSize, checked: itemChecked(option), disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), onChange: (event) => {
49
49
  if (firstOptionValue == null)
50
50
  return;
51
51
  const typeValue = Utils.parseString(event.target.value, firstOptionValue);
@@ -64,7 +64,7 @@ export function OptionGroup(props) {
64
64
  if (onValueChange)
65
65
  onValueChange(changedValues);
66
66
  setValues(changedValues);
67
- } })) : (React.createElement(Radio, { disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), size: size, readOnly: readOnly }));
67
+ } })) : (React.createElement(Radio, { disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), size: itemSize, readOnly: readOnly }));
68
68
  // Label
69
69
  const label = getOptionLabel == null
70
70
  ? `${option[labelField]}`
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,28 +45,25 @@ 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
69
  // Item checked or not
@@ -71,27 +72,23 @@ export function SelectEx(props) {
71
72
  return valueState.indexOf(id) !== -1;
72
73
  return valueState === id;
73
74
  };
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
75
  // Set item
83
76
  const setItemValue = (id) => {
84
77
  var _a;
85
78
  if (id != valueRef.current) {
79
+ // Difference
80
+ const diff = multiple
81
+ ? Utils.arrayDifferences(id, valueRef.current)
82
+ : id;
86
83
  setValueState(id);
87
84
  const input = (_a = divRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('input');
88
85
  if (input) {
89
86
  // Different value, trigger change event
90
87
  ReactUtils.triggerChange(input, id, false);
91
88
  }
92
- return true;
89
+ return diff;
93
90
  }
94
- return false;
91
+ return undefined;
95
92
  };
96
93
  // Get option id
97
94
  const getId = (option) => {
@@ -123,7 +120,7 @@ export function SelectEx(props) {
123
120
  // When value change
124
121
  React.useEffect(() => {
125
122
  refreshData();
126
- }, [localValue]);
123
+ }, [valueSource]);
127
124
  // When layout ready
128
125
  React.useEffect(() => {
129
126
  var _a;
@@ -134,6 +131,7 @@ export function SelectEx(props) {
134
131
  setValueState(multiple ? [] : '');
135
132
  };
136
133
  input === null || input === void 0 ? void 0 : input.addEventListener('change', inputChange);
134
+ isMounted.current = true;
137
135
  return () => {
138
136
  isMounted.current = false;
139
137
  input === null || input === void 0 ? void 0 : input.removeEventListener('change', inputChange);
@@ -145,17 +143,21 @@ export function SelectEx(props) {
145
143
  React.createElement(InputLabel, { id: labelId, shrink: search
146
144
  ? MUGlobal.searchFieldShrink
147
145
  : 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) => {
146
+ React.createElement(Select, { ref: divRef, value: multiple
147
+ ? valueState
148
+ : localOptions.some((o) => o[idField] === valueState)
149
+ ? valueState
150
+ : '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
151
151
  if (onChange) {
152
152
  onChange(event, child);
153
153
  // event.preventDefault() will block executing
154
154
  if (event.defaultPrevented)
155
155
  return;
156
156
  }
157
- if (handleChange(event)) {
158
- doItemChange(localOptions, event.target.value, true);
157
+ // Set item value
158
+ const diff = setItemValue(event.target.value);
159
+ if (diff != null) {
160
+ doItemChange(localOptions, diff, true);
159
161
  }
160
162
  }, renderValue: (selected) => {
161
163
  // The text shows up
package/lib/index.d.ts CHANGED
@@ -44,6 +44,7 @@ export * from './HiSelector';
44
44
  export * from './IconButtonLink';
45
45
  export * from './InputField';
46
46
  export * from './ItemList';
47
+ export * from './ListChooser';
47
48
  export * from './ListItemRightIcon';
48
49
  export * from './ListMoreDisplay';
49
50
  export * from './LoadingButton';
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ export * from './HiSelector';
44
44
  export * from './IconButtonLink';
45
45
  export * from './InputField';
46
46
  export * from './ItemList';
47
+ export * from './ListChooser';
47
48
  export * from './ListItemRightIcon';
48
49
  export * from './ListMoreDisplay';
49
50
  export * from './LoadingButton';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/materialui",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "TypeScript Material-UI Implementation",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -48,20 +48,20 @@
48
48
  "dependencies": {
49
49
  "@dnd-kit/core": "^6.0.5",
50
50
  "@dnd-kit/sortable": "^7.0.1",
51
- "@emotion/css": "^11.10.0",
52
- "@emotion/react": "^11.10.4",
53
- "@emotion/styled": "^11.10.4",
54
- "@etsoo/appscript": "^1.3.23",
51
+ "@emotion/css": "^11.10.5",
52
+ "@emotion/react": "^11.10.5",
53
+ "@emotion/styled": "^11.10.5",
54
+ "@etsoo/appscript": "^1.3.28",
55
55
  "@etsoo/notificationbase": "^1.1.13",
56
- "@etsoo/react": "^1.6.22",
57
- "@etsoo/shared": "^1.1.67",
56
+ "@etsoo/react": "^1.6.23",
57
+ "@etsoo/shared": "^1.1.71",
58
58
  "@mui/icons-material": "^5.10.9",
59
- "@mui/material": "^5.10.11",
59
+ "@mui/material": "^5.10.12",
60
60
  "@types/pica": "^9.0.1",
61
61
  "@types/pulltorefreshjs": "^0.1.5",
62
- "@types/react": "^18.0.23",
62
+ "@types/react": "^18.0.24",
63
63
  "@types/react-avatar-editor": "^13.0.0",
64
- "@types/react-dom": "^18.0.7",
64
+ "@types/react-dom": "^18.0.8",
65
65
  "@types/react-input-mask": "^3.0.1",
66
66
  "@types/react-window": "^1.8.5",
67
67
  "pica": "^9.0.1",
@@ -71,20 +71,20 @@
71
71
  "react-dom": "^18.2.0",
72
72
  "react-draggable": "^4.4.5",
73
73
  "react-imask": "^6.4.3",
74
- "react-router-dom": "^6.4.2",
75
- "react-window": "^1.8.7"
74
+ "react-router-dom": "^6.4.3",
75
+ "react-window": "^1.8.8"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@babel/cli": "^7.19.3",
79
79
  "@babel/core": "^7.19.6",
80
80
  "@babel/plugin-transform-runtime": "^7.19.6",
81
81
  "@babel/preset-env": "^7.19.4",
82
- "@babel/runtime-corejs3": "^7.19.6",
82
+ "@babel/runtime-corejs3": "^7.20.1",
83
83
  "@testing-library/jest-dom": "^5.16.5",
84
84
  "@testing-library/react": "^13.4.0",
85
- "@types/jest": "^29.2.0",
86
- "@typescript-eslint/eslint-plugin": "^5.41.0",
87
- "@typescript-eslint/parser": "^5.41.0",
85
+ "@types/jest": "^29.2.1",
86
+ "@typescript-eslint/eslint-plugin": "^5.42.0",
87
+ "@typescript-eslint/parser": "^5.42.0",
88
88
  "eslint": "^8.26.0",
89
89
  "eslint-config-airbnb-base": "^15.0.0",
90
90
  "eslint-plugin-import": "^2.26.0",
@@ -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
@@ -0,0 +1,204 @@
1
+ import { useDelayedExecutor } from '@etsoo/react';
2
+ import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
3
+ import {
4
+ List,
5
+ ListItem,
6
+ ListItemButton,
7
+ ListItemButtonProps,
8
+ ListItemText,
9
+ ListProps,
10
+ TextField
11
+ } from '@mui/material';
12
+ import React from 'react';
13
+ import { VBox } from './FlexBox';
14
+
15
+ type QueryData = {
16
+ title?: string;
17
+ };
18
+
19
+ /**
20
+ * List chooser button props
21
+ */
22
+ export interface ListChooserButtonProps<
23
+ T extends object,
24
+ D extends DataTypes.Keys<T>
25
+ > {
26
+ (id: T[D]): ListItemButtonProps;
27
+ }
28
+
29
+ /**
30
+ * List chooser props
31
+ */
32
+ export type ListChooserProps<
33
+ T extends object,
34
+ D extends DataTypes.Keys<T>,
35
+ Q extends object
36
+ > = ListProps & {
37
+ /**
38
+ * Condition renderer
39
+ */
40
+ conditionRenderer?: (
41
+ rq: Partial<Q>,
42
+ delayed: DelayedExecutorType
43
+ ) => React.ReactNode;
44
+
45
+ /**
46
+ * List item renderer
47
+ */
48
+ itemRenderer?: (
49
+ data: T,
50
+ props: ListChooserButtonProps<T, D>
51
+ ) => React.ReactNode;
52
+
53
+ /**
54
+ * Label field
55
+ */
56
+ labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
57
+
58
+ /**
59
+ * Id field
60
+ */
61
+ idField?: D;
62
+
63
+ /**
64
+ * Load data callback
65
+ */
66
+ loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
67
+
68
+ /**
69
+ * Multiple selected
70
+ */
71
+ multiple?: boolean;
72
+
73
+ /**
74
+ * Item onchange callback
75
+ */
76
+ onItemChange: (items: T[], ids: T[D][]) => void;
77
+
78
+ /**
79
+ * Title
80
+ */
81
+ title: string;
82
+ };
83
+
84
+ /**
85
+ * List chooser
86
+ * @param props Props
87
+ * @returns Component
88
+ */
89
+ export function ListChooser<
90
+ T extends object,
91
+ D extends DataTypes.Keys<T> = IdDefaultType<T>,
92
+ Q extends object = QueryData
93
+ >(props: ListChooserProps<T, D, Q>) {
94
+ // Selected ids state
95
+ const [selectedIds, setSelectedIds] = React.useState<T[D][]>([]);
96
+
97
+ const selectProps: ListChooserButtonProps<T, D> = (id: T[D]) => ({
98
+ selected: selectedIds.includes(id),
99
+ onClick: () => {
100
+ if (multiple) {
101
+ const index = selectedIds.indexOf(id);
102
+ if (index === -1) selectedIds.push(id);
103
+ else selectedIds.splice(index, 1);
104
+ setSelectedIds([...selectedIds]);
105
+ } else {
106
+ setSelectedIds([id]);
107
+ }
108
+ }
109
+ });
110
+
111
+ // Destruct
112
+ const {
113
+ conditionRenderer = (rq: Partial<Q>, delayed: DelayedExecutorType) => (
114
+ <TextField
115
+ autoFocus
116
+ margin="dense"
117
+ name="title"
118
+ label={title}
119
+ fullWidth
120
+ variant="standard"
121
+ inputProps={{ maxLength: 128 }}
122
+ onChange={(event) => {
123
+ Reflect.set(rq, 'title', event.target.value);
124
+ delayed.call();
125
+ }}
126
+ />
127
+ ),
128
+ itemRenderer = (item, selectProps) => {
129
+ const id = item[idField];
130
+ const label =
131
+ typeof labelField === 'function'
132
+ ? labelField(item)
133
+ : Reflect.get(item, labelField);
134
+
135
+ return (
136
+ <ListItem disableGutters key={`${id}`}>
137
+ <ListItemButton {...selectProps(id)}>
138
+ <ListItemText primary={label} />
139
+ </ListItemButton>
140
+ </ListItem>
141
+ );
142
+ },
143
+ idField = 'id' as D,
144
+ labelField = 'label',
145
+ loadData,
146
+ multiple = false,
147
+ onItemChange,
148
+ title,
149
+ ...rest
150
+ } = props;
151
+
152
+ // Default minimum height
153
+ rest.sx ??= { minHeight: '220px' };
154
+
155
+ // State
156
+ const [items, setItems] = React.useState<T[]>([]);
157
+
158
+ // Query request data
159
+ const mounted = React.useRef<boolean>(false);
160
+ const rq = React.useRef<Partial<Q>>({});
161
+
162
+ // Delayed execution
163
+ const delayed = useDelayedExecutor(async () => {
164
+ const result = await loadData(rq.current);
165
+ if (result == null || !mounted.current) return;
166
+
167
+ if (
168
+ !multiple &&
169
+ selectedIds.length > 0 &&
170
+ !result.some((item) => selectedIds.includes(item[idField]))
171
+ ) {
172
+ setSelectedIds([]);
173
+ }
174
+
175
+ setItems(result);
176
+ }, 480);
177
+
178
+ React.useEffect(() => {
179
+ if (!mounted.current) return;
180
+ onItemChange(
181
+ items.filter((item) => selectedIds.includes(item[idField])),
182
+ selectedIds
183
+ );
184
+ }, [selectedIds]);
185
+
186
+ React.useEffect(() => {
187
+ mounted.current = true;
188
+ delayed.call(0);
189
+ return () => {
190
+ mounted.current = false;
191
+ delayed.clear();
192
+ };
193
+ }, [delayed]);
194
+
195
+ // Layout
196
+ return (
197
+ <VBox>
198
+ {conditionRenderer(rq.current, delayed)}
199
+ <List disablePadding dense {...rest}>
200
+ {items.map((item) => itemRenderer(item, selectProps))}
201
+ </List>
202
+ </VBox>
203
+ );
204
+ }
@@ -95,6 +95,11 @@ export type OptionGroupProps<
95
95
  * Display group of elements in a compact row
96
96
  */
97
97
  row?: boolean;
98
+
99
+ /**
100
+ * Item size
101
+ */
102
+ itemSize?: 'small' | 'medium';
98
103
  };
99
104
 
100
105
  /**
@@ -121,7 +126,7 @@ export function OptionGroup<
121
126
  options,
122
127
  readOnly,
123
128
  row,
124
- size,
129
+ itemSize,
125
130
  ...rest
126
131
  } = props;
127
132
 
@@ -173,7 +178,7 @@ export function OptionGroup<
173
178
  <Checkbox
174
179
  name={name}
175
180
  readOnly={readOnly}
176
- size={size}
181
+ size={itemSize}
177
182
  checked={itemChecked(option)}
178
183
  disabled={disabledIds?.includes(ov)}
179
184
  onChange={(event) => {
@@ -203,7 +208,7 @@ export function OptionGroup<
203
208
  ) : (
204
209
  <Radio
205
210
  disabled={disabledIds?.includes(ov)}
206
- size={size}
211
+ size={itemSize}
207
212
  readOnly={readOnly}
208
213
  />
209
214
  );
package/src/SelectEx.tsx CHANGED
@@ -143,7 +143,7 @@ export function SelectEx<
143
143
 
144
144
  // Options state
145
145
  const [localOptions, setOptions] = React.useState<readonly T[]>([]);
146
- const isMounted = React.useRef(true);
146
+ const isMounted = React.useRef(false);
147
147
 
148
148
  const doItemChange = (
149
149
  options: readonly T[],
@@ -151,18 +151,21 @@ export function SelectEx<
151
151
  userAction: boolean
152
152
  ) => {
153
153
  if (onItemChange == null) return;
154
- if (value == null || value === '') {
155
- onItemChange(undefined, userAction);
156
- return;
154
+
155
+ let option: T | undefined;
156
+ if (multiple && Array.isArray(value)) {
157
+ option = options.find((option) => value.includes(option[idField]));
158
+ } else if (value == null || value === '') {
159
+ option = undefined;
160
+ } else {
161
+ option = options.find((option) => option[idField] === value);
157
162
  }
158
- const option = options.find((option) => option[idField] === value);
159
163
  onItemChange(option, userAction);
160
164
  };
161
165
 
162
166
  const setOptionsAdd = (options: readonly T[]) => {
163
167
  setOptions(options);
164
- if (localValue != null && localValue !== '')
165
- doItemChange(options, localValue, false);
168
+ if (valueSource != null) doItemChange(options, valueSource, false);
166
169
  };
167
170
 
168
171
  // When options change
@@ -174,17 +177,18 @@ export function SelectEx<
174
177
  }, [options, propertyWay]);
175
178
 
176
179
  // 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
- }
180
+ const v = defaultValue ?? value;
181
+ const valueSource = multiple
182
+ ? v
183
+ ? Array.isArray(v)
184
+ ? v
185
+ : [v]
186
+ : []
187
+ : v ?? '';
185
188
 
186
189
  // Value state
187
- const [valueState, setValueStateBase] = React.useState<unknown>();
190
+ const [valueState, setValueStateBase] =
191
+ React.useState<unknown>(valueSource);
188
192
  const valueRef = React.useRef<unknown>();
189
193
  const setValueState = (newValue: unknown) => {
190
194
  valueRef.current = newValue;
@@ -192,8 +196,8 @@ export function SelectEx<
192
196
  };
193
197
 
194
198
  React.useEffect(() => {
195
- if (localValue != null) setValueState(localValue);
196
- }, [localValue]);
199
+ if (valueSource != null) setValueState(valueSource);
200
+ }, [valueSource]);
197
201
 
198
202
  // Label id
199
203
  const labelId = `selectex-label-${name}`;
@@ -204,16 +208,17 @@ export function SelectEx<
204
208
  return valueState === id;
205
209
  };
206
210
 
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
211
  // Set item
215
212
  const setItemValue = (id: unknown) => {
216
213
  if (id != valueRef.current) {
214
+ // Difference
215
+ const diff = multiple
216
+ ? Utils.arrayDifferences(
217
+ id as T[D][],
218
+ valueRef.current as T[D][]
219
+ )
220
+ : id;
221
+
217
222
  setValueState(id);
218
223
 
219
224
  const input = divRef.current?.querySelector('input');
@@ -221,9 +226,9 @@ export function SelectEx<
221
226
  // Different value, trigger change event
222
227
  ReactUtils.triggerChange(input, id as string, false);
223
228
  }
224
- return true;
229
+ return diff;
225
230
  }
226
- return false;
231
+ return undefined;
227
232
  };
228
233
 
229
234
  // Get option id
@@ -257,7 +262,7 @@ export function SelectEx<
257
262
  // When value change
258
263
  React.useEffect(() => {
259
264
  refreshData();
260
- }, [localValue]);
265
+ }, [valueSource]);
261
266
 
262
267
  // When layout ready
263
268
  React.useEffect(() => {
@@ -268,6 +273,8 @@ export function SelectEx<
268
273
  };
269
274
  input?.addEventListener('change', inputChange);
270
275
 
276
+ isMounted.current = true;
277
+
271
278
  return () => {
272
279
  isMounted.current = false;
273
280
  input?.removeEventListener('change', inputChange);
@@ -297,10 +304,12 @@ export function SelectEx<
297
304
  <Select
298
305
  ref={divRef}
299
306
  value={
300
- localOptions.some((option) =>
301
- itemChecked(getId(option))
302
- )
303
- ? valueState ?? ''
307
+ multiple
308
+ ? valueState
309
+ : localOptions.some(
310
+ (o) => o[idField] === valueState
311
+ )
312
+ ? valueState
304
313
  : ''
305
314
  }
306
315
  input={
@@ -321,12 +330,10 @@ export function SelectEx<
321
330
  if (event.defaultPrevented) return;
322
331
  }
323
332
 
324
- if (handleChange(event)) {
325
- doItemChange(
326
- localOptions,
327
- event.target.value,
328
- true
329
- );
333
+ // Set item value
334
+ const diff = setItemValue(event.target.value);
335
+ if (diff != null) {
336
+ doItemChange(localOptions, diff, true);
330
337
  }
331
338
  }}
332
339
  renderValue={(selected) => {
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export * from './HiSelector';
47
47
  export * from './IconButtonLink';
48
48
  export * from './InputField';
49
49
  export * from './ItemList';
50
+ export * from './ListChooser';
50
51
  export * from './ListItemRightIcon';
51
52
  export * from './ListMoreDisplay';
52
53
  export * from './LoadingButton';