@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-center", children: [_jsx("div", { children: titleInfo }), _jsx("div", { children: rightContent })] })), !rightContent && titleInfo, _jsx("div", { className: "mt-5 space-y-5", children: children })] }) }));
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
- onChange && onChange(value ?? null);
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
- onChangeMulti && onChangeMulti(newValue);
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
- onChangeMulti && onChangeMulti([]);
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
- onChangeMulti && onChangeMulti(newValue);
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
- onInputChange && onInputChange(newValue);
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
- const handleKeyDown = (e) => {
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" && (value || valueMulti.length > 0)) {
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
- handleClear();
158
- setInputValue("");
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
- const scrollToHighlightedOption = () => {
164
- if (optionsRef.current && optionsRef.current.children[highlightedIndex]) {
165
- const highlightedOption = optionsRef.current.children[highlightedIndex];
166
- highlightedOption.scrollIntoView({
167
- block: "center",
168
- inline: "center",
169
- behavior: "smooth",
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.0",
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.2.0",
39
- "@zauru-sdk/hooks": "^2.2.0",
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.2.0",
42
- "@zauru-sdk/utils": "^2.3.0",
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": "c0a2ddf3c3c2b5ba8da9e7c7a3a40043a64688d3"
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-center">
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, { useEffect, useState, useRef, KeyboardEvent } from "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 = (value?: SelectFieldOption) => {
104
- if (value) {
105
- setValue(value);
106
- setInputValue(value.label);
107
- if (setFormValue) {
108
- setFormValue(name || "", value.value);
109
- }
110
- } else {
111
- setValue(null);
112
- setInputValue("");
113
- if (setFormValue) {
114
- setFormValue(name || "", "");
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
- onChange && onChange(value ?? null);
118
- };
170
+ onChangeRef.current && onChangeRef.current(value ?? null);
171
+ },
172
+ [name, setFormValue]
173
+ );
119
174
 
120
- const handleAddMultiValue = (value?: SelectFieldOption) => {
121
- if (value) {
122
- const existValue = options.some((v) => v.value === value.value);
123
- const noEstaYaSeleccionado = !valueMulti.some(
124
- (v) => v.value === value.value
125
- );
126
- if (existValue && noEstaYaSeleccionado) {
127
- const newValue = [...valueMulti, value];
128
- setValueMulti(newValue);
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
- onChangeMulti && onChangeMulti(newValue);
198
+ setInputValue("");
199
+ onChangeMultiRef.current && onChangeMultiRef.current([]);
136
200
  }
137
- } else {
138
- setValueMulti([]);
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(name || "", []);
210
+ setFormValue(
211
+ name || "",
212
+ newValue.map((v) => v.value)
213
+ );
141
214
  }
142
- setInputValue("");
143
- onChangeMulti && onChangeMulti([]);
144
- }
145
- };
215
+ onChangeMultiRef.current && onChangeMultiRef.current(newValue);
216
+ },
217
+ [valueMulti, name, setFormValue]
218
+ );
146
219
 
147
- const handleRemoveMultiValue = (value: SelectFieldOption) => {
148
- const newValue = valueMulti.filter((v) => v.value !== value.value);
149
- setValueMulti(newValue);
150
- if (setFormValue) {
151
- setFormValue(
152
- name || "",
153
- newValue.map((v) => v.value)
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
- onChangeMulti && onChangeMulti(newValue);
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 = (option: SelectFieldOption) => {
175
- if (isMulti) {
176
- handleAddMultiValue(option);
177
- } else {
178
- handleSetSingleValue(option);
179
- }
180
- setHighlightedIndex(-1);
181
- setIsSearching(false);
182
- setFilteredOptions(options);
183
- setIsOpen(false);
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 handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
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 (