@zauru-sdk/components 2.3.0 → 2.4.0
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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [2.4.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.3.1...v2.4.0) (2025-07-07)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* build ([b22e345](https://github.com/intuitiva/zauru-typescript-sdk/commit/b22e345387b87bd77a43d45924875e806ac62aa3))
|
|
12
|
+
* getPurchaseOrderStringQuery ([ddac075](https://github.com/intuitiva/zauru-typescript-sdk/commit/ddac07589aa19f82ab009b9c027952a2f07412a4))
|
|
13
|
+
* remove lodash dependency ([5b1a6ce](https://github.com/intuitiva/zauru-typescript-sdk/commit/5b1a6ceba61db8cdb0b3ca98c1431c1d57ad7091))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [2.3.1](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.3.0...v2.3.1) (2025-06-27)
|
|
20
|
+
|
|
21
|
+
**Note:** Version bump only for package @zauru-sdk/components
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
# [2.3.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.2.0...v2.3.0) (2025-06-27)
|
|
7
28
|
|
|
8
29
|
**Note:** Version bump only for package @zauru-sdk/components
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
export const Container = (props) => {
|
|
3
3
|
const { title, description, children, className = "", rightContent } = props;
|
|
4
4
|
const titleInfo = (_jsxs(_Fragment, { children: [title && (_jsx("h3", { className: "text-3xl font-bold leading-8 text-gray-900", children: title })), description && (_jsx("p", { className: "mt-1 text-md text-gray-600", children: description }))] }));
|
|
5
|
-
return (_jsx(_Fragment, { children: _jsxs("div", { className: `mx-2 ${className}`, children: [rightContent && (_jsxs("div", { className: "flex justify-between items-
|
|
5
|
+
return (_jsx(_Fragment, { children: _jsxs("div", { className: `mx-2 ${className}`, children: [rightContent && (_jsxs("div", { className: "flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4", children: [_jsx("div", { className: "max-w-3xl", children: titleInfo }), _jsx("div", { className: "flex-shrink-0", children: rightContent })] })), !rightContent && titleInfo, _jsx("div", { className: "mt-5 space-y-5", children: children })] }) }));
|
|
6
6
|
};
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { IdeaIconSVG } from "@zauru-sdk/icons";
|
|
3
|
-
import { useEffect, useState, useRef } from "react";
|
|
3
|
+
import { useEffect, useState, useRef, useCallback, } from "react";
|
|
4
4
|
import { LoadingInputSkeleton } from "../../Skeletons/index.js";
|
|
5
5
|
import { useFormContext } from "react-hook-form";
|
|
6
|
+
// Simple comparison helper to check if two SelectFieldOption arrays contain the
|
|
7
|
+
// same values (order-insensitive). We only compare the `value` of each option
|
|
8
|
+
// because it uniquely identifies a SelectFieldOption.
|
|
9
|
+
const areOptionArraysEqual = (a = [], b = []) => {
|
|
10
|
+
if (a.length !== b.length)
|
|
11
|
+
return false;
|
|
12
|
+
const aValues = a.map((v) => v.value).sort();
|
|
13
|
+
const bValues = b.map((v) => v.value).sort();
|
|
14
|
+
return aValues.every((val, idx) => val === bValues[idx]);
|
|
15
|
+
};
|
|
6
16
|
export const SelectField = (props) => {
|
|
7
17
|
const { id, name, title, defaultValue, defaultValueMulti = [], helpText, hint, options, onChange, onChangeMulti, isClearable = false, disabled = false, readOnly = false, isMulti = false, loading = false, className = "", onInputChange, required = false, } = props;
|
|
8
18
|
const [value, setValue] = useState(defaultValue || null);
|
|
@@ -16,6 +26,20 @@ export const SelectField = (props) => {
|
|
|
16
26
|
const optionsRef = useRef(null);
|
|
17
27
|
const [isTabPressed, setIsTabPressed] = useState(false);
|
|
18
28
|
const [isSearching, setIsSearching] = useState(false);
|
|
29
|
+
// Use refs to store stable references to callbacks
|
|
30
|
+
const onChangeRef = useRef(onChange);
|
|
31
|
+
const onChangeMultiRef = useRef(onChangeMulti);
|
|
32
|
+
const onInputChangeRef = useRef(onInputChange);
|
|
33
|
+
// Update refs when callbacks change
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onChangeRef.current = onChange;
|
|
36
|
+
}, [onChange]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
onChangeMultiRef.current = onChangeMulti;
|
|
39
|
+
}, [onChangeMulti]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
onInputChangeRef.current = onInputChange;
|
|
42
|
+
}, [onInputChange]);
|
|
19
43
|
const { register: tempRegister, formState: { errors }, setValue: setFormValue, } = useFormContext() || { formState: {} };
|
|
20
44
|
const error = errors ? errors[props.name ?? "-1"] : undefined;
|
|
21
45
|
const register = tempRegister
|
|
@@ -28,6 +52,18 @@ export const SelectField = (props) => {
|
|
|
28
52
|
const bgColor = isReadOnly ? "bg-gray-200" : `bg-${color}-50`;
|
|
29
53
|
const textColor = isReadOnly ? "text-gray-500" : `text-${color}-900`;
|
|
30
54
|
const borderColor = isReadOnly ? "border-gray-300" : `border-${color}-200`;
|
|
55
|
+
// Sincronizar estado interno cuando cambien las props
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (defaultValue && (!value || value.value !== defaultValue.value)) {
|
|
58
|
+
setValue(defaultValue);
|
|
59
|
+
setInputValue(defaultValue.label);
|
|
60
|
+
}
|
|
61
|
+
}, [defaultValue]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!areOptionArraysEqual(defaultValueMulti, valueMulti)) {
|
|
64
|
+
setValueMulti(defaultValueMulti);
|
|
65
|
+
}
|
|
66
|
+
}, [defaultValueMulti]);
|
|
31
67
|
useEffect(() => {
|
|
32
68
|
setFilteredOptions(options);
|
|
33
69
|
}, [options]);
|
|
@@ -43,7 +79,7 @@ export const SelectField = (props) => {
|
|
|
43
79
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
44
80
|
};
|
|
45
81
|
}, []);
|
|
46
|
-
const handleSetSingleValue = (value) => {
|
|
82
|
+
const handleSetSingleValue = useCallback((value) => {
|
|
47
83
|
if (value) {
|
|
48
84
|
setValue(value);
|
|
49
85
|
setInputValue(value.label);
|
|
@@ -58,9 +94,9 @@ export const SelectField = (props) => {
|
|
|
58
94
|
setFormValue(name || "", "");
|
|
59
95
|
}
|
|
60
96
|
}
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
const handleAddMultiValue = (value) => {
|
|
97
|
+
onChangeRef.current && onChangeRef.current(value ?? null);
|
|
98
|
+
}, [name, setFormValue]);
|
|
99
|
+
const handleAddMultiValue = useCallback((value) => {
|
|
64
100
|
if (value) {
|
|
65
101
|
const existValue = options.some((v) => v.value === value.value);
|
|
66
102
|
const noEstaYaSeleccionado = !valueMulti.some((v) => v.value === value.value);
|
|
@@ -70,7 +106,7 @@ export const SelectField = (props) => {
|
|
|
70
106
|
if (setFormValue) {
|
|
71
107
|
setFormValue(name || "", newValue.map((v) => v.value));
|
|
72
108
|
}
|
|
73
|
-
|
|
109
|
+
onChangeMultiRef.current && onChangeMultiRef.current(newValue);
|
|
74
110
|
}
|
|
75
111
|
}
|
|
76
112
|
else {
|
|
@@ -79,28 +115,28 @@ export const SelectField = (props) => {
|
|
|
79
115
|
setFormValue(name || "", []);
|
|
80
116
|
}
|
|
81
117
|
setInputValue("");
|
|
82
|
-
|
|
118
|
+
onChangeMultiRef.current && onChangeMultiRef.current([]);
|
|
83
119
|
}
|
|
84
|
-
};
|
|
85
|
-
const handleRemoveMultiValue = (value) => {
|
|
120
|
+
}, [options, valueMulti, name, setFormValue]);
|
|
121
|
+
const handleRemoveMultiValue = useCallback((value) => {
|
|
86
122
|
const newValue = valueMulti.filter((v) => v.value !== value.value);
|
|
87
123
|
setValueMulti(newValue);
|
|
88
124
|
if (setFormValue) {
|
|
89
125
|
setFormValue(name || "", newValue.map((v) => v.value));
|
|
90
126
|
}
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
const handleInputChange = (e) => {
|
|
127
|
+
onChangeMultiRef.current && onChangeMultiRef.current(newValue);
|
|
128
|
+
}, [valueMulti, name, setFormValue]);
|
|
129
|
+
const handleInputChange = useCallback((e) => {
|
|
94
130
|
const newValue = e.target.value;
|
|
95
131
|
if (register) {
|
|
96
132
|
register.onChange(e);
|
|
97
133
|
}
|
|
98
134
|
setInputValue(newValue);
|
|
99
|
-
|
|
135
|
+
onInputChangeRef.current && onInputChangeRef.current(newValue);
|
|
100
136
|
setIsSearching(true);
|
|
101
137
|
setFilteredOptions(options.filter((option) => option.label.toLowerCase().includes(newValue.toLowerCase())));
|
|
102
|
-
};
|
|
103
|
-
const handleOptionClick = (option) => {
|
|
138
|
+
}, [register, options]);
|
|
139
|
+
const handleOptionClick = useCallback((option) => {
|
|
104
140
|
if (isMulti) {
|
|
105
141
|
handleAddMultiValue(option);
|
|
106
142
|
}
|
|
@@ -111,16 +147,16 @@ export const SelectField = (props) => {
|
|
|
111
147
|
setIsSearching(false);
|
|
112
148
|
setFilteredOptions(options);
|
|
113
149
|
setIsOpen(false);
|
|
114
|
-
};
|
|
115
|
-
const handleClear = () => {
|
|
150
|
+
}, [isMulti, handleAddMultiValue, handleSetSingleValue, options]);
|
|
151
|
+
const handleClear = useCallback(() => {
|
|
116
152
|
if (isMulti) {
|
|
117
153
|
handleAddMultiValue();
|
|
118
154
|
}
|
|
119
155
|
else {
|
|
120
156
|
handleSetSingleValue();
|
|
121
157
|
}
|
|
122
|
-
};
|
|
123
|
-
const handleBlur = () => {
|
|
158
|
+
}, [isMulti, handleAddMultiValue, handleSetSingleValue]);
|
|
159
|
+
const handleBlur = useCallback(() => {
|
|
124
160
|
setTimeout(() => {
|
|
125
161
|
if (isTabPressed && highlightedIndex >= 0) {
|
|
126
162
|
handleOptionClick(filteredOptions[highlightedIndex]);
|
|
@@ -133,8 +169,24 @@ export const SelectField = (props) => {
|
|
|
133
169
|
setIsSearching(false);
|
|
134
170
|
setIsOpen(false);
|
|
135
171
|
}, 200);
|
|
136
|
-
}
|
|
137
|
-
|
|
172
|
+
}, [
|
|
173
|
+
isTabPressed,
|
|
174
|
+
highlightedIndex,
|
|
175
|
+
filteredOptions,
|
|
176
|
+
isSearching,
|
|
177
|
+
handleOptionClick,
|
|
178
|
+
]);
|
|
179
|
+
const scrollToHighlightedOption = useCallback(() => {
|
|
180
|
+
if (optionsRef.current && optionsRef.current.children[highlightedIndex]) {
|
|
181
|
+
const highlightedOption = optionsRef.current.children[highlightedIndex];
|
|
182
|
+
highlightedOption.scrollIntoView({
|
|
183
|
+
block: "center",
|
|
184
|
+
inline: "center",
|
|
185
|
+
behavior: "smooth",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}, [highlightedIndex]);
|
|
189
|
+
const handleKeyDown = useCallback((e) => {
|
|
138
190
|
if (e.key === "Tab") {
|
|
139
191
|
setIsTabPressed(true);
|
|
140
192
|
}
|
|
@@ -152,24 +204,36 @@ export const SelectField = (props) => {
|
|
|
152
204
|
e.preventDefault();
|
|
153
205
|
handleOptionClick(filteredOptions[highlightedIndex]);
|
|
154
206
|
}
|
|
155
|
-
else if (e.key === "Backspace" &&
|
|
207
|
+
else if (e.key === "Backspace" &&
|
|
208
|
+
inputValue === "" &&
|
|
209
|
+
(value || valueMulti.length > 0)) {
|
|
210
|
+
// Solo borrar selecciones cuando el input esté vacío
|
|
156
211
|
e.preventDefault();
|
|
157
|
-
|
|
158
|
-
|
|
212
|
+
if (isMulti && valueMulti.length > 0) {
|
|
213
|
+
// En modo multi, borrar la última selección
|
|
214
|
+
const lastIndex = valueMulti.length - 1;
|
|
215
|
+
handleRemoveMultiValue(valueMulti[lastIndex]);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// En modo single, borrar la selección
|
|
219
|
+
handleClear();
|
|
220
|
+
}
|
|
159
221
|
setFilteredOptions(options);
|
|
160
222
|
setIsOpen(true);
|
|
161
223
|
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
224
|
+
}, [
|
|
225
|
+
filteredOptions,
|
|
226
|
+
scrollToHighlightedOption,
|
|
227
|
+
highlightedIndex,
|
|
228
|
+
handleOptionClick,
|
|
229
|
+
inputValue,
|
|
230
|
+
value,
|
|
231
|
+
valueMulti,
|
|
232
|
+
handleClear,
|
|
233
|
+
handleRemoveMultiValue,
|
|
234
|
+
isMulti,
|
|
235
|
+
options,
|
|
236
|
+
]);
|
|
173
237
|
if (loading) {
|
|
174
238
|
return (_jsxs(_Fragment, { children: [title && (_jsx("label", { htmlFor: error ? `${name}-error` : `${name}-success`, className: `block text-sm font-medium text-${color}-700 dark:text-${color}-500`, children: title })), _jsx(LoadingInputSkeleton, {}), helpText && (_jsx("p", { className: `mt-2 italic text-sm text-${color}-500 dark:text-${color}-400`, children: helpText }))] }));
|
|
175
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Componentes reutilizables en las WebApps de Zauru.",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"@rails/activestorage": "^8.0.200",
|
|
36
36
|
"@reduxjs/toolkit": "^2.2.1",
|
|
37
37
|
"@remix-run/react": "^2.8.1",
|
|
38
|
-
"@zauru-sdk/common": "^2.
|
|
39
|
-
"@zauru-sdk/hooks": "^2.
|
|
38
|
+
"@zauru-sdk/common": "^2.3.1",
|
|
39
|
+
"@zauru-sdk/hooks": "^2.3.1",
|
|
40
40
|
"@zauru-sdk/icons": "^2.0.188",
|
|
41
|
-
"@zauru-sdk/types": "^2.
|
|
42
|
-
"@zauru-sdk/utils": "^2.
|
|
41
|
+
"@zauru-sdk/types": "^2.3.1",
|
|
42
|
+
"@zauru-sdk/utils": "^2.4.0",
|
|
43
43
|
"browser-image-compression": "^2.0.2",
|
|
44
44
|
"framer-motion": "^11.7.0",
|
|
45
45
|
"jsonwebtoken": "^9.0.2",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"styled-components": "^5.3.5",
|
|
53
53
|
"zod": "^3.23.8"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "322aeeae8a6462b8d9877918a5c2f320d6fd9b87"
|
|
56
56
|
}
|
|
@@ -26,9 +26,9 @@ export const Container = (props: Props) => {
|
|
|
26
26
|
<>
|
|
27
27
|
<div className={`mx-2 ${className}`}>
|
|
28
28
|
{rightContent && (
|
|
29
|
-
<div className="flex justify-between items-
|
|
30
|
-
<div>{titleInfo}</div>
|
|
31
|
-
<div>{rightContent}</div>
|
|
29
|
+
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
|
|
30
|
+
<div className="max-w-3xl">{titleInfo}</div>
|
|
31
|
+
<div className="flex-shrink-0">{rightContent}</div>
|
|
32
32
|
</div>
|
|
33
33
|
)}
|
|
34
34
|
{!rightContent && titleInfo}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { IdeaIconSVG } from "@zauru-sdk/icons";
|
|
2
2
|
import { SelectFieldOption } from "@zauru-sdk/types";
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
useRef,
|
|
7
|
+
KeyboardEvent,
|
|
8
|
+
useCallback,
|
|
9
|
+
} from "react";
|
|
4
10
|
import { LoadingInputSkeleton } from "../../Skeletons/index.js";
|
|
5
11
|
import { useFormContext } from "react-hook-form";
|
|
6
12
|
|
|
@@ -26,6 +32,19 @@ type Props = {
|
|
|
26
32
|
required?: boolean;
|
|
27
33
|
};
|
|
28
34
|
|
|
35
|
+
// Simple comparison helper to check if two SelectFieldOption arrays contain the
|
|
36
|
+
// same values (order-insensitive). We only compare the `value` of each option
|
|
37
|
+
// because it uniquely identifies a SelectFieldOption.
|
|
38
|
+
const areOptionArraysEqual = (
|
|
39
|
+
a: SelectFieldOption[] = [],
|
|
40
|
+
b: SelectFieldOption[] = []
|
|
41
|
+
): boolean => {
|
|
42
|
+
if (a.length !== b.length) return false;
|
|
43
|
+
const aValues = a.map((v) => v.value).sort();
|
|
44
|
+
const bValues = b.map((v) => v.value).sort();
|
|
45
|
+
return aValues.every((val, idx) => val === bValues[idx]);
|
|
46
|
+
};
|
|
47
|
+
|
|
29
48
|
export const SelectField = (props: Props) => {
|
|
30
49
|
const {
|
|
31
50
|
id,
|
|
@@ -62,6 +81,25 @@ export const SelectField = (props: Props) => {
|
|
|
62
81
|
const optionsRef = useRef<HTMLUListElement>(null);
|
|
63
82
|
const [isTabPressed, setIsTabPressed] = useState<boolean>(false);
|
|
64
83
|
const [isSearching, setIsSearching] = useState<boolean>(false);
|
|
84
|
+
|
|
85
|
+
// Use refs to store stable references to callbacks
|
|
86
|
+
const onChangeRef = useRef(onChange);
|
|
87
|
+
const onChangeMultiRef = useRef(onChangeMulti);
|
|
88
|
+
const onInputChangeRef = useRef(onInputChange);
|
|
89
|
+
|
|
90
|
+
// Update refs when callbacks change
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
onChangeRef.current = onChange;
|
|
93
|
+
}, [onChange]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
onChangeMultiRef.current = onChangeMulti;
|
|
97
|
+
}, [onChangeMulti]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
onInputChangeRef.current = onInputChange;
|
|
101
|
+
}, [onInputChange]);
|
|
102
|
+
|
|
65
103
|
const {
|
|
66
104
|
register: tempRegister,
|
|
67
105
|
formState: { errors },
|
|
@@ -80,6 +118,20 @@ export const SelectField = (props: Props) => {
|
|
|
80
118
|
const textColor = isReadOnly ? "text-gray-500" : `text-${color}-900`;
|
|
81
119
|
const borderColor = isReadOnly ? "border-gray-300" : `border-${color}-200`;
|
|
82
120
|
|
|
121
|
+
// Sincronizar estado interno cuando cambien las props
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (defaultValue && (!value || value.value !== defaultValue.value)) {
|
|
124
|
+
setValue(defaultValue);
|
|
125
|
+
setInputValue(defaultValue.label);
|
|
126
|
+
}
|
|
127
|
+
}, [defaultValue]);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!areOptionArraysEqual(defaultValueMulti, valueMulti)) {
|
|
131
|
+
setValueMulti(defaultValueMulti);
|
|
132
|
+
}
|
|
133
|
+
}, [defaultValueMulti]);
|
|
134
|
+
|
|
83
135
|
useEffect(() => {
|
|
84
136
|
setFilteredOptions(options);
|
|
85
137
|
}, [options]);
|
|
@@ -100,98 +152,113 @@ export const SelectField = (props: Props) => {
|
|
|
100
152
|
};
|
|
101
153
|
}, []);
|
|
102
154
|
|
|
103
|
-
const handleSetSingleValue = (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
setFormValue
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
setFormValue
|
|
155
|
+
const handleSetSingleValue = useCallback(
|
|
156
|
+
(value?: SelectFieldOption) => {
|
|
157
|
+
if (value) {
|
|
158
|
+
setValue(value);
|
|
159
|
+
setInputValue(value.label);
|
|
160
|
+
if (setFormValue) {
|
|
161
|
+
setFormValue(name || "", value.value);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
setValue(null);
|
|
165
|
+
setInputValue("");
|
|
166
|
+
if (setFormValue) {
|
|
167
|
+
setFormValue(name || "", "");
|
|
168
|
+
}
|
|
115
169
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
170
|
+
onChangeRef.current && onChangeRef.current(value ?? null);
|
|
171
|
+
},
|
|
172
|
+
[name, setFormValue]
|
|
173
|
+
);
|
|
119
174
|
|
|
120
|
-
const handleAddMultiValue = (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
const handleAddMultiValue = useCallback(
|
|
176
|
+
(value?: SelectFieldOption) => {
|
|
177
|
+
if (value) {
|
|
178
|
+
const existValue = options.some((v) => v.value === value.value);
|
|
179
|
+
const noEstaYaSeleccionado = !valueMulti.some(
|
|
180
|
+
(v) => v.value === value.value
|
|
181
|
+
);
|
|
182
|
+
if (existValue && noEstaYaSeleccionado) {
|
|
183
|
+
const newValue = [...valueMulti, value];
|
|
184
|
+
setValueMulti(newValue);
|
|
185
|
+
if (setFormValue) {
|
|
186
|
+
setFormValue(
|
|
187
|
+
name || "",
|
|
188
|
+
newValue.map((v) => v.value)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
onChangeMultiRef.current && onChangeMultiRef.current(newValue);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
setValueMulti([]);
|
|
129
195
|
if (setFormValue) {
|
|
130
|
-
setFormValue(
|
|
131
|
-
name || "",
|
|
132
|
-
newValue.map((v) => v.value)
|
|
133
|
-
);
|
|
196
|
+
setFormValue(name || "", []);
|
|
134
197
|
}
|
|
135
|
-
|
|
198
|
+
setInputValue("");
|
|
199
|
+
onChangeMultiRef.current && onChangeMultiRef.current([]);
|
|
136
200
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
201
|
+
},
|
|
202
|
+
[options, valueMulti, name, setFormValue]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleRemoveMultiValue = useCallback(
|
|
206
|
+
(value: SelectFieldOption) => {
|
|
207
|
+
const newValue = valueMulti.filter((v) => v.value !== value.value);
|
|
208
|
+
setValueMulti(newValue);
|
|
139
209
|
if (setFormValue) {
|
|
140
|
-
setFormValue(
|
|
210
|
+
setFormValue(
|
|
211
|
+
name || "",
|
|
212
|
+
newValue.map((v) => v.value)
|
|
213
|
+
);
|
|
141
214
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
215
|
+
onChangeMultiRef.current && onChangeMultiRef.current(newValue);
|
|
216
|
+
},
|
|
217
|
+
[valueMulti, name, setFormValue]
|
|
218
|
+
);
|
|
146
219
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
220
|
+
const handleInputChange = useCallback(
|
|
221
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
222
|
+
const newValue = e.target.value;
|
|
223
|
+
if (register) {
|
|
224
|
+
register.onChange(e);
|
|
225
|
+
}
|
|
226
|
+
setInputValue(newValue);
|
|
227
|
+
onInputChangeRef.current && onInputChangeRef.current(newValue);
|
|
228
|
+
setIsSearching(true);
|
|
229
|
+
setFilteredOptions(
|
|
230
|
+
options.filter((option) =>
|
|
231
|
+
option.label.toLowerCase().includes(newValue.toLowerCase())
|
|
232
|
+
)
|
|
154
233
|
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
160
|
-
const newValue = e.target.value;
|
|
161
|
-
if (register) {
|
|
162
|
-
register.onChange(e);
|
|
163
|
-
}
|
|
164
|
-
setInputValue(newValue);
|
|
165
|
-
onInputChange && onInputChange(newValue);
|
|
166
|
-
setIsSearching(true);
|
|
167
|
-
setFilteredOptions(
|
|
168
|
-
options.filter((option) =>
|
|
169
|
-
option.label.toLowerCase().includes(newValue.toLowerCase())
|
|
170
|
-
)
|
|
171
|
-
);
|
|
172
|
-
};
|
|
234
|
+
},
|
|
235
|
+
[register, options]
|
|
236
|
+
);
|
|
173
237
|
|
|
174
|
-
const handleOptionClick = (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
238
|
+
const handleOptionClick = useCallback(
|
|
239
|
+
(option: SelectFieldOption) => {
|
|
240
|
+
if (isMulti) {
|
|
241
|
+
handleAddMultiValue(option);
|
|
242
|
+
} else {
|
|
243
|
+
handleSetSingleValue(option);
|
|
244
|
+
}
|
|
245
|
+
setHighlightedIndex(-1);
|
|
246
|
+
setIsSearching(false);
|
|
247
|
+
setFilteredOptions(options);
|
|
248
|
+
setIsOpen(false);
|
|
249
|
+
},
|
|
250
|
+
[isMulti, handleAddMultiValue, handleSetSingleValue, options]
|
|
251
|
+
);
|
|
185
252
|
|
|
186
|
-
const handleClear = () => {
|
|
253
|
+
const handleClear = useCallback(() => {
|
|
187
254
|
if (isMulti) {
|
|
188
255
|
handleAddMultiValue();
|
|
189
256
|
} else {
|
|
190
257
|
handleSetSingleValue();
|
|
191
258
|
}
|
|
192
|
-
};
|
|
259
|
+
}, [isMulti, handleAddMultiValue, handleSetSingleValue]);
|
|
193
260
|
|
|
194
|
-
const handleBlur = () => {
|
|
261
|
+
const handleBlur = useCallback(() => {
|
|
195
262
|
setTimeout(() => {
|
|
196
263
|
if (isTabPressed && highlightedIndex >= 0) {
|
|
197
264
|
handleOptionClick(filteredOptions[highlightedIndex]);
|
|
@@ -204,36 +271,15 @@ export const SelectField = (props: Props) => {
|
|
|
204
271
|
setIsSearching(false);
|
|
205
272
|
setIsOpen(false);
|
|
206
273
|
}, 200);
|
|
207
|
-
}
|
|
274
|
+
}, [
|
|
275
|
+
isTabPressed,
|
|
276
|
+
highlightedIndex,
|
|
277
|
+
filteredOptions,
|
|
278
|
+
isSearching,
|
|
279
|
+
handleOptionClick,
|
|
280
|
+
]);
|
|
208
281
|
|
|
209
|
-
const
|
|
210
|
-
if (e.key === "Tab") {
|
|
211
|
-
setIsTabPressed(true);
|
|
212
|
-
} else if (e.key === "ArrowDown") {
|
|
213
|
-
e.preventDefault();
|
|
214
|
-
setHighlightedIndex((prevIndex) =>
|
|
215
|
-
prevIndex < filteredOptions.length - 1 ? prevIndex + 1 : 0
|
|
216
|
-
);
|
|
217
|
-
scrollToHighlightedOption();
|
|
218
|
-
} else if (e.key === "ArrowUp") {
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
setHighlightedIndex((prevIndex) =>
|
|
221
|
-
prevIndex > 0 ? prevIndex - 1 : filteredOptions.length - 1
|
|
222
|
-
);
|
|
223
|
-
scrollToHighlightedOption();
|
|
224
|
-
} else if (e.key === "Enter" && highlightedIndex !== -1) {
|
|
225
|
-
e.preventDefault();
|
|
226
|
-
handleOptionClick(filteredOptions[highlightedIndex]);
|
|
227
|
-
} else if (e.key === "Backspace" && (value || valueMulti.length > 0)) {
|
|
228
|
-
e.preventDefault();
|
|
229
|
-
handleClear();
|
|
230
|
-
setInputValue("");
|
|
231
|
-
setFilteredOptions(options);
|
|
232
|
-
setIsOpen(true);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const scrollToHighlightedOption = () => {
|
|
282
|
+
const scrollToHighlightedOption = useCallback(() => {
|
|
237
283
|
if (optionsRef.current && optionsRef.current.children[highlightedIndex]) {
|
|
238
284
|
const highlightedOption = optionsRef.current.children[
|
|
239
285
|
highlightedIndex
|
|
@@ -244,7 +290,60 @@ export const SelectField = (props: Props) => {
|
|
|
244
290
|
behavior: "smooth",
|
|
245
291
|
});
|
|
246
292
|
}
|
|
247
|
-
};
|
|
293
|
+
}, [highlightedIndex]);
|
|
294
|
+
|
|
295
|
+
const handleKeyDown = useCallback(
|
|
296
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
297
|
+
if (e.key === "Tab") {
|
|
298
|
+
setIsTabPressed(true);
|
|
299
|
+
} else if (e.key === "ArrowDown") {
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
setHighlightedIndex((prevIndex) =>
|
|
302
|
+
prevIndex < filteredOptions.length - 1 ? prevIndex + 1 : 0
|
|
303
|
+
);
|
|
304
|
+
scrollToHighlightedOption();
|
|
305
|
+
} else if (e.key === "ArrowUp") {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
setHighlightedIndex((prevIndex) =>
|
|
308
|
+
prevIndex > 0 ? prevIndex - 1 : filteredOptions.length - 1
|
|
309
|
+
);
|
|
310
|
+
scrollToHighlightedOption();
|
|
311
|
+
} else if (e.key === "Enter" && highlightedIndex !== -1) {
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
handleOptionClick(filteredOptions[highlightedIndex]);
|
|
314
|
+
} else if (
|
|
315
|
+
e.key === "Backspace" &&
|
|
316
|
+
inputValue === "" &&
|
|
317
|
+
(value || valueMulti.length > 0)
|
|
318
|
+
) {
|
|
319
|
+
// Solo borrar selecciones cuando el input esté vacío
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
if (isMulti && valueMulti.length > 0) {
|
|
322
|
+
// En modo multi, borrar la última selección
|
|
323
|
+
const lastIndex = valueMulti.length - 1;
|
|
324
|
+
handleRemoveMultiValue(valueMulti[lastIndex]);
|
|
325
|
+
} else {
|
|
326
|
+
// En modo single, borrar la selección
|
|
327
|
+
handleClear();
|
|
328
|
+
}
|
|
329
|
+
setFilteredOptions(options);
|
|
330
|
+
setIsOpen(true);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
[
|
|
334
|
+
filteredOptions,
|
|
335
|
+
scrollToHighlightedOption,
|
|
336
|
+
highlightedIndex,
|
|
337
|
+
handleOptionClick,
|
|
338
|
+
inputValue,
|
|
339
|
+
value,
|
|
340
|
+
valueMulti,
|
|
341
|
+
handleClear,
|
|
342
|
+
handleRemoveMultiValue,
|
|
343
|
+
isMulti,
|
|
344
|
+
options,
|
|
345
|
+
]
|
|
346
|
+
);
|
|
248
347
|
|
|
249
348
|
if (loading) {
|
|
250
349
|
return (
|