tp-react-elements-dev 1.13.2 → 1.13.3

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.
@@ -1,19 +1,153 @@
1
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { Autocomplete, TextField, Checkbox } from '@mui/material';
3
- import { useMemo } from 'react';
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { Checkbox, Autocomplete, TextField, Box, Tooltip, Chip } from '@mui/material';
3
+ import { useState, useRef, useMemo, useCallback } from 'react';
4
4
  import { useWatch, Controller } from 'react-hook-form';
5
5
  import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
6
6
  import 'dayjs';
7
7
  import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
8
8
 
9
+ //region old code
10
+ // const MultiSelectAutocomplete = ({
11
+ // props,
12
+ // variant,
13
+ // }: {
14
+ // props: FormRenderProps;
15
+ // variant: VariantProps;
16
+ // }) => {
17
+ // const isError = !!props.errors?.[props.item.name];
18
+ // const watchedValue = useWatch({ control: props.control, name: props.item.name });
19
+ // const normalizeToStringArray = (value: unknown): string[] => {
20
+ // if (Array.isArray(value)) {
21
+ // return value.map((v) => String(v));
22
+ // }
23
+ // if (typeof value === 'string') {
24
+ // return value
25
+ // .split(',')
26
+ // .map((v) => v.trim())
27
+ // .filter((v) => v.length > 0);
28
+ // }
29
+ // if (value == null) return [];
30
+ // return [String(value)];
31
+ // };
32
+ // const normalizedSelectedValues = normalizeToStringArray(watchedValue);
33
+ // const isShowBorderError = isError && normalizedSelectedValues.length === 0;
34
+ // const options: OptionItem[] = (props.item.options || []) as OptionItem[];
35
+ // const selectedOptions: OptionItem[] = useMemo(() => {
36
+ // const valueSet = new Set(normalizedSelectedValues.map((v) => String(v)));
37
+ // return options.filter((opt) => valueSet.has(String(opt.value)));
38
+ // }, [normalizedSelectedValues, options]);
39
+ // return (
40
+ // <Controller
41
+ // control={props.control}
42
+ // name={props.item.name}
43
+ // key={props.item.name}
44
+ // render={({ field }) => (
45
+ // <>
46
+ // {renderLabel(variant, props)}
47
+ // <Autocomplete
48
+ // {...field}
49
+ // multiple
50
+ // disableCloseOnSelect
51
+ // id={`${props.item.name}-autocomplete-multi`}
52
+ // options={options}
53
+ // value={selectedOptions}
54
+ // getOptionLabel={(option) => String(option.label)}
55
+ // isOptionEqualToValue={(option, value) => String(option.value) === String(value.value)}
56
+ // onChange={(_, newValue) => {
57
+ // const joined = (newValue || []).map((o: OptionItem) => String(o.value)).join(',');
58
+ // props.setValue(props.item.name, joined);
59
+ // props?.item?.onChangeFn && props?.item?.onChangeFn(joined);
60
+ // props?.clearErrors && props?.clearErrors(props?.item?.name);
61
+ // }}
62
+ // onBlur={(e: any) => {
63
+ // props?.item?.onBlurFn && props?.item?.onBlurFn(e);
64
+ // }}
65
+ // size="small"
66
+ // renderOption={(renderProps, option, { selected }) => (
67
+ // <li {...renderProps} style={{ fontSize: '11px' }}>
68
+ // <Checkbox
69
+ // checked={selected}
70
+ // size="small"
71
+ // sx={{ mr: 1, p: '2px' }}
72
+ // />
73
+ // {option.label}
74
+ // </li>
75
+ // )}
76
+ // renderTags={(tagValue) => (
77
+ // tagValue.length ? <span>{tagValue.map((o: OptionItem) => String(o.label)).join(' , ')}</span> : null
78
+ // )}
79
+ // sx={{
80
+ // '& .MuiAutocomplete-input': {
81
+ // padding: '0px 0px 0px 5px !important',
82
+ // },
83
+ // '& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root,.css-8edmr2-MuiFormLabel-root-MuiInputLabel-root': {
84
+ // top: '-3px',
85
+ // },
86
+ // '& .MuiAutocomplete-option': {
87
+ // fontSize: '11px',
88
+ // zIndex: 2000,
89
+ // },
90
+ // ...props.item.sx,
91
+ // }}
92
+ // ListboxProps={{
93
+ // onScroll: (event: React.SyntheticEvent) => {
94
+ // const listboxNode = event.currentTarget as HTMLElement;
95
+ // if (listboxNode.scrollTop + listboxNode.clientHeight === listboxNode.scrollHeight) {
96
+ // // Infinite scroll hook can be wired here
97
+ // }
98
+ // },
99
+ // }}
100
+ // disabled={props.item.disable}
101
+ // slotProps={{
102
+ // popper: {
103
+ // sx: {
104
+ // zIndex: 1401,
105
+ // },
106
+ // },
107
+ // listbox: {
108
+ // sx: {
109
+ // fontSize: '11px',
110
+ // },
111
+ // },
112
+ // }}
113
+ // renderInput={(params) => (
114
+ // <TextField
115
+ // {...params}
116
+ // placeholder={props.item.placeholder}
117
+ // error={isShowBorderError}
118
+ // label={
119
+ // variant !== 'standard'
120
+ // ? `${props.item.label}${props.item.required ? ' *' : ''}`
121
+ // : ''
122
+ // }
123
+ // inputProps={{
124
+ // ...params.inputProps,
125
+ // 'aria-labelledby': `${props.item.name}-label`,
126
+ // 'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
127
+ // 'aria-required': props.item.required ? true : undefined,
128
+ // }}
129
+ // />
130
+ // )}
131
+ // />
132
+ // <FormBottomField {...props} />
133
+ // </>
134
+ // )}
135
+ // />
136
+ // );
137
+ // };
138
+ //endregion
9
139
  const MultiSelectAutocomplete = ({ props, variant, }) => {
10
140
  const isError = !!props.errors?.[props.item.name];
11
141
  const watchedValue = useWatch({ control: props.control, name: props.item.name });
142
+ const [open, setOpen] = useState(false);
143
+ const tagsContainerRef = useRef(null);
12
144
  const normalizeToStringArray = (value) => {
13
145
  if (Array.isArray(value)) {
146
+ // already array of primitives/strings
14
147
  return value.map((v) => String(v));
15
148
  }
16
149
  if (typeof value === 'string') {
150
+ // if backend provided CSV string, convert to array
17
151
  return value
18
152
  .split(',')
19
153
  .map((v) => v.trim())
@@ -21,26 +155,52 @@ const MultiSelectAutocomplete = ({ props, variant, }) => {
21
155
  }
22
156
  if (value == null)
23
157
  return [];
158
+ // for any other types, coerce to single-element array
24
159
  return [String(value)];
25
160
  };
26
161
  const normalizedSelectedValues = normalizeToStringArray(watchedValue);
27
162
  const isShowBorderError = isError && normalizedSelectedValues.length === 0;
28
- const options = (props.item.options || []);
163
+ const options = useMemo(() => (props.item.options || []), [props.item.options]);
29
164
  const selectedOptions = useMemo(() => {
30
165
  const valueSet = new Set(normalizedSelectedValues.map((v) => String(v)));
31
166
  return options.filter((opt) => valueSet.has(String(opt.value)));
32
167
  }, [normalizedSelectedValues, options]);
33
- return (jsx(Controller, { control: props.control, name: props.item.name, render: ({ field }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ...field, multiple: true, disableCloseOnSelect: true, id: `${props.item.name}-autocomplete-multi`, options: options, value: selectedOptions, getOptionLabel: (option) => String(option.label), isOptionEqualToValue: (option, value) => String(option.value) === String(value.value), onChange: (_, newValue) => {
34
- const joined = (newValue || []).map((o) => String(o.value)).join(',');
35
- props.setValue(props.item.name, joined);
36
- props?.item?.onChangeFn && props?.item?.onChangeFn(joined);
37
- props?.clearErrors && props?.clearErrors(props?.item?.name);
38
- }, onBlur: (e) => {
39
- props?.item?.onBlurFn && props?.item?.onBlurFn(e);
40
- }, size: "small", renderOption: (renderProps, option, { selected }) => (jsxs("li", { ...renderProps, style: { fontSize: '11px' }, children: [jsx(Checkbox, { checked: selected, size: "small", sx: { mr: 1, p: '2px' } }), option.label] })), renderTags: (tagValue) => (tagValue.length ? jsx("span", { children: tagValue.map((o) => String(o.label)).join(' , ') }) : null), sx: {
168
+ // Derived overflow flag without extra renders
169
+ const isOverflowing = (selectedOptions || []).length > 2;
170
+ // Stable callbacks to avoid re-renders
171
+ const getOptionLabelCb = useCallback((option) => String(option.label), []);
172
+ const isOptionEqualCb = useCallback((option, value) => String(option.value) === String(value.value), []);
173
+ const renderOptionCb = useCallback((renderProps, option, state) => (jsxs("li", { ...renderProps, style: { fontSize: '11px' }, children: [jsx(Checkbox, { checked: state.selected, size: "small", sx: { mr: 1, p: '2px' } }), option.label] })), []);
174
+ const chipSx = useMemo(() => ({ mr: 0.5, height: 20, fontSize: '10px', borderRadius: '10px', '& .MuiChip-label': { px: 0.75, py: 0 } }), []);
175
+ const selectedTooltip = useMemo(() => selectedOptions.map(o => String(o.label)).join(', '), [selectedOptions]);
176
+ const handleChange = useCallback((_, newValue) => {
177
+ const values = Array.isArray(newValue)
178
+ ? newValue.map((o) => String(o.value))
179
+ : [];
180
+ // Minimize work during selection for snappier UX
181
+ props.setValue(props.item.name, values, { shouldValidate: false, shouldDirty: true });
182
+ props?.item?.onChangeFn && props?.item?.onChangeFn(values.join(','));
183
+ props?.clearErrors && props?.clearErrors(props?.item?.name);
184
+ }, [props]);
185
+ const handleBlur = useCallback((e) => {
186
+ props?.item?.onBlurFn && props?.item?.onBlurFn(e);
187
+ }, [props]);
188
+ return (jsx(Controller, { control: props.control, name: props.item.name, render: ({ field: { ref, onBlur: fieldOnBlur } }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ref: ref, open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), multiple: true, disableCloseOnSelect: true, id: `${props.item.name}-autocomplete-multi`, options: options, value: selectedOptions, getOptionLabel: getOptionLabelCb, isOptionEqualToValue: isOptionEqualCb, onChange: handleChange, onBlur: (e) => {
189
+ fieldOnBlur();
190
+ handleBlur(e);
191
+ }, size: "small", disablePortal: false, autoHighlight: true, selectOnFocus: true, clearOnBlur: false, renderTags: (tagValue, getTagProps) => (jsx(Box, { ref: tagsContainerRef, sx: {
192
+ '& :hover': {
193
+ cursor: 'pointer',
194
+ },
195
+ display: 'flex', flexWrap: 'nowrap', overflow: 'hidden', alignItems: 'center'
196
+ }, children: isOverflowing ? (jsx(Tooltip, { title: selectedTooltip, placement: "top", children: jsx(Chip, { size: "small", label: `${tagValue.length} selected`, onClick: () => setOpen(true), clickable: true, sx: chipSx }) })) : (tagValue.map((option, index) => (jsx(Tooltip, { title: String(option?.label ?? option), placement: "top", children: jsx(Chip, { ...getTagProps({ index }), size: "small", label: String(option?.label ?? option), sx: chipSx }) }, String(option?.value ?? option?.label ?? index))))) })), renderOption: renderOptionCb, sx: {
41
197
  '& .MuiAutocomplete-input': {
42
198
  padding: '0px 0px 0px 5px !important',
43
199
  },
200
+ '& .MuiAutocomplete-inputRoot': {
201
+ flexWrap: 'nowrap', // keep search beside chip
202
+ alignItems: 'center'
203
+ },
44
204
  '& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root,.css-8edmr2-MuiFormLabel-root-MuiInputLabel-root': {
45
205
  top: '-3px',
46
206
  },
@@ -57,7 +217,7 @@ const MultiSelectAutocomplete = ({ props, variant, }) => {
57
217
  }, disabled: props.item.disable, slotProps: {
58
218
  popper: {
59
219
  sx: {
60
- zIndex: 1401,
220
+ zIndex: 20000,
61
221
  },
62
222
  },
63
223
  listbox: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements-dev",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",