neogestify-ui-components 1.2.9 → 1.2.10
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/package.json
CHANGED
|
@@ -30,7 +30,7 @@ export const Select: FC<SelectProps> = ({
|
|
|
30
30
|
const selectId = id || `select-${Math.random().toString(36).substring(2, 9)}`;
|
|
31
31
|
|
|
32
32
|
const getVariantClasses = () => {
|
|
33
|
-
const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors
|
|
33
|
+
const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors';
|
|
34
34
|
|
|
35
35
|
if (variant === 'small') {
|
|
36
36
|
return `${baseClasses} px-2.5 py-1.5 text-sm border-gray-300 dark:border-gray-600`;
|
|
@@ -39,6 +39,10 @@ export const Select: FC<SelectProps> = ({
|
|
|
39
39
|
return `${baseClasses} px-3 py-2 border-gray-300 dark:border-gray-600 ${error ? 'border-red-300 dark:border-red-600 focus:ring-red-500' : ''}`;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
const getOptionClasses = (option: Option) => {
|
|
43
|
+
return `bg-white dark:bg-gray-800 text-gray-900 dark:text-white py-2 ${option.disabled ? 'opacity-50 cursor-not-allowed' : ''}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
42
46
|
const combinedClassName = `${getVariantClasses()} ${className}`.trim();
|
|
43
47
|
|
|
44
48
|
return (
|
|
@@ -52,7 +56,7 @@ export const Select: FC<SelectProps> = ({
|
|
|
52
56
|
)}
|
|
53
57
|
<select id={selectId} className={combinedClassName} {...props}>
|
|
54
58
|
{placeholder && placeholder.trim() && (
|
|
55
|
-
<option value="" disabled>
|
|
59
|
+
<option value="" disabled className="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 py-2">
|
|
56
60
|
{placeholder}
|
|
57
61
|
</option>
|
|
58
62
|
)}
|
|
@@ -62,6 +66,7 @@ export const Select: FC<SelectProps> = ({
|
|
|
62
66
|
value={option.value}
|
|
63
67
|
disabled={option.disabled}
|
|
64
68
|
selected={option.selected}
|
|
69
|
+
className={getOptionClasses(option)}
|
|
65
70
|
>
|
|
66
71
|
{option.label}
|
|
67
72
|
</option>
|
|
@@ -3,7 +3,6 @@ export { Input } from './Input';
|
|
|
3
3
|
export { TextArea } from './TextArea';
|
|
4
4
|
export { Form } from './Form';
|
|
5
5
|
export { Select } from './Select';
|
|
6
|
-
export { CustomSelect } from './CustomSelect';
|
|
7
6
|
export { Table } from './Table';
|
|
8
7
|
export { Modal, type ModalRef } from './Modal';
|
|
9
8
|
export { Loading } from './Loading';
|
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { type FC, type ReactNode, useState, useRef, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface Option {
|
|
4
|
-
value: string | number;
|
|
5
|
-
label: string;
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
selected?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface CustomSelectProps {
|
|
11
|
-
options: Option[];
|
|
12
|
-
placeholder?: string;
|
|
13
|
-
variant?: 'default' | 'small';
|
|
14
|
-
error?: boolean;
|
|
15
|
-
helperText?: string;
|
|
16
|
-
label?: string | ReactNode;
|
|
17
|
-
className?: string;
|
|
18
|
-
id?: string;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
value?: string | number;
|
|
21
|
-
defaultValue?: string | number;
|
|
22
|
-
onChange?: (value: string | number) => void;
|
|
23
|
-
name?: string;
|
|
24
|
-
required?: boolean;
|
|
25
|
-
// Styling props for customization
|
|
26
|
-
optionClassName?: string;
|
|
27
|
-
optionSelectedClassName?: string;
|
|
28
|
-
optionHoverClassName?: string;
|
|
29
|
-
optionDisabledClassName?: string;
|
|
30
|
-
dropdownClassName?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const CustomSelect: FC<CustomSelectProps> = ({
|
|
34
|
-
options,
|
|
35
|
-
placeholder,
|
|
36
|
-
variant = 'default',
|
|
37
|
-
error = false,
|
|
38
|
-
helperText,
|
|
39
|
-
label,
|
|
40
|
-
className = '',
|
|
41
|
-
id,
|
|
42
|
-
disabled = false,
|
|
43
|
-
value: controlledValue,
|
|
44
|
-
defaultValue,
|
|
45
|
-
onChange,
|
|
46
|
-
name,
|
|
47
|
-
required,
|
|
48
|
-
optionClassName = '',
|
|
49
|
-
optionSelectedClassName = '',
|
|
50
|
-
optionHoverClassName = '',
|
|
51
|
-
optionDisabledClassName = '',
|
|
52
|
-
dropdownClassName = '',
|
|
53
|
-
}) => {
|
|
54
|
-
const selectId = id || `custom-select-${Math.random().toString(36).substring(2, 9)}`;
|
|
55
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
56
|
-
const [selectedValue, setSelectedValue] = useState<string | number | undefined>(
|
|
57
|
-
controlledValue ?? defaultValue ?? options.find(opt => opt.selected)?.value
|
|
58
|
-
);
|
|
59
|
-
const [hoveredIndex, setHoveredIndex] = useState<number>(-1);
|
|
60
|
-
const [searchTerm, setSearchTerm] = useState('');
|
|
61
|
-
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
|
62
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
63
|
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
64
|
-
|
|
65
|
-
const isControlled = controlledValue !== undefined;
|
|
66
|
-
const currentValue = isControlled ? controlledValue : selectedValue;
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (isControlled) {
|
|
70
|
-
setSelectedValue(controlledValue);
|
|
71
|
-
}
|
|
72
|
-
}, [controlledValue, isControlled]);
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
76
|
-
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
77
|
-
setIsOpen(false);
|
|
78
|
-
setSearchTerm('');
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
if (isOpen) {
|
|
83
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
84
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
85
|
-
}
|
|
86
|
-
}, [isOpen]);
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (isOpen && hoveredIndex >= 0 && dropdownRef.current) {
|
|
90
|
-
const hoveredElement = dropdownRef.current.children[hoveredIndex] as HTMLElement;
|
|
91
|
-
if (hoveredElement) {
|
|
92
|
-
hoveredElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, [hoveredIndex, isOpen]);
|
|
96
|
-
|
|
97
|
-
const selectedOption = options.find(opt => opt.value === currentValue);
|
|
98
|
-
|
|
99
|
-
const handleSelect = (option: Option) => {
|
|
100
|
-
if (option.disabled) return;
|
|
101
|
-
|
|
102
|
-
if (!isControlled) {
|
|
103
|
-
setSelectedValue(option.value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
onChange?.(option.value);
|
|
107
|
-
setIsOpen(false);
|
|
108
|
-
setSearchTerm('');
|
|
109
|
-
setHoveredIndex(-1);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
113
|
-
if (disabled) return;
|
|
114
|
-
|
|
115
|
-
switch (e.key) {
|
|
116
|
-
case 'Enter':
|
|
117
|
-
case ' ':
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
if (!isOpen) {
|
|
120
|
-
setIsOpen(true);
|
|
121
|
-
const selectedIdx = options.findIndex(opt => opt.value === currentValue);
|
|
122
|
-
setHoveredIndex(selectedIdx >= 0 ? selectedIdx : 0);
|
|
123
|
-
} else if (hoveredIndex >= 0) {
|
|
124
|
-
handleSelect(options[hoveredIndex]);
|
|
125
|
-
}
|
|
126
|
-
break;
|
|
127
|
-
|
|
128
|
-
case 'ArrowDown':
|
|
129
|
-
e.preventDefault();
|
|
130
|
-
if (!isOpen) {
|
|
131
|
-
setIsOpen(true);
|
|
132
|
-
const selectedIdx = options.findIndex(opt => opt.value === currentValue);
|
|
133
|
-
setHoveredIndex(selectedIdx >= 0 ? selectedIdx : 0);
|
|
134
|
-
} else {
|
|
135
|
-
setHoveredIndex(prev => {
|
|
136
|
-
let next = prev + 1;
|
|
137
|
-
while (next < options.length && options[next].disabled) {
|
|
138
|
-
next++;
|
|
139
|
-
}
|
|
140
|
-
return next < options.length ? next : prev;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
|
|
145
|
-
case 'ArrowUp':
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
if (isOpen) {
|
|
148
|
-
setHoveredIndex(prev => {
|
|
149
|
-
let next = prev - 1;
|
|
150
|
-
while (next >= 0 && options[next].disabled) {
|
|
151
|
-
next--;
|
|
152
|
-
}
|
|
153
|
-
return next >= 0 ? next : prev;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
|
|
158
|
-
case 'Escape':
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
setIsOpen(false);
|
|
161
|
-
setSearchTerm('');
|
|
162
|
-
break;
|
|
163
|
-
|
|
164
|
-
case 'Tab':
|
|
165
|
-
if (isOpen) {
|
|
166
|
-
setIsOpen(false);
|
|
167
|
-
setSearchTerm('');
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
|
|
171
|
-
default:
|
|
172
|
-
// Type-ahead search
|
|
173
|
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
174
|
-
e.preventDefault();
|
|
175
|
-
const newSearchTerm = searchTerm + e.key.toLowerCase();
|
|
176
|
-
setSearchTerm(newSearchTerm);
|
|
177
|
-
|
|
178
|
-
const matchIndex = options.findIndex(opt =>
|
|
179
|
-
!opt.disabled && opt.label.toLowerCase().startsWith(newSearchTerm)
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
if (matchIndex >= 0) {
|
|
183
|
-
setHoveredIndex(matchIndex);
|
|
184
|
-
if (!isOpen) {
|
|
185
|
-
handleSelect(options[matchIndex]);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (searchTimeout) clearTimeout(searchTimeout);
|
|
190
|
-
const timeout = setTimeout(() => setSearchTerm(''), 1000);
|
|
191
|
-
setSearchTimeout(timeout);
|
|
192
|
-
}
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const getButtonClasses = () => {
|
|
198
|
-
const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors';
|
|
199
|
-
|
|
200
|
-
if (variant === 'small') {
|
|
201
|
-
return `${baseClasses} px-2.5 py-1.5 text-sm border-gray-300 dark:border-gray-600`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return `${baseClasses} px-3 py-2 border-gray-300 dark:border-gray-600 ${
|
|
205
|
-
error ? 'border-red-300 dark:border-red-600 focus:ring-red-500' : ''
|
|
206
|
-
}`;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const getOptionClasses = (option: Option, index: number) => {
|
|
210
|
-
const isSelected = option.value === currentValue;
|
|
211
|
-
const isHovered = index === hoveredIndex;
|
|
212
|
-
|
|
213
|
-
let classes = 'px-3 py-2 cursor-pointer transition-colors';
|
|
214
|
-
|
|
215
|
-
if (option.disabled) {
|
|
216
|
-
classes += ' opacity-50 cursor-not-allowed bg-gray-100 dark:bg-gray-800';
|
|
217
|
-
if (optionDisabledClassName) {
|
|
218
|
-
classes += ` ${optionDisabledClassName}`;
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
if (isSelected) {
|
|
222
|
-
classes += ' bg-indigo-50 dark:bg-indigo-900/50 text-indigo-900 dark:text-indigo-100 font-medium';
|
|
223
|
-
if (optionSelectedClassName) {
|
|
224
|
-
classes += ` ${optionSelectedClassName}`;
|
|
225
|
-
}
|
|
226
|
-
} else if (isHovered) {
|
|
227
|
-
classes += ' bg-gray-100 dark:bg-gray-600';
|
|
228
|
-
if (optionHoverClassName) {
|
|
229
|
-
classes += ` ${optionHoverClassName}`;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
classes += ' text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-600';
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (optionClassName) {
|
|
237
|
-
classes += ` ${optionClassName}`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return classes;
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const combinedButtonClassName = `${getButtonClasses()} ${className}`.trim();
|
|
244
|
-
const combinedDropdownClassName = `absolute z-50 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto ${dropdownClassName}`.trim();
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<div className="space-y-1 w-full">
|
|
248
|
-
{label && typeof label === 'string' ? (
|
|
249
|
-
<label htmlFor={selectId} className="block text-xs font-normal text-gray-700 dark:text-gray-300">
|
|
250
|
-
{label}
|
|
251
|
-
{required && <span className="text-red-500 ml-1">*</span>}
|
|
252
|
-
</label>
|
|
253
|
-
) : (
|
|
254
|
-
label
|
|
255
|
-
)}
|
|
256
|
-
|
|
257
|
-
<div ref={containerRef} className="relative w-full">
|
|
258
|
-
<button
|
|
259
|
-
id={selectId}
|
|
260
|
-
type="button"
|
|
261
|
-
role="combobox"
|
|
262
|
-
aria-expanded={isOpen}
|
|
263
|
-
aria-haspopup="listbox"
|
|
264
|
-
aria-labelledby={label ? `${selectId}-label` : undefined}
|
|
265
|
-
aria-required={required}
|
|
266
|
-
disabled={disabled}
|
|
267
|
-
className={combinedButtonClassName}
|
|
268
|
-
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
269
|
-
onKeyDown={handleKeyDown}
|
|
270
|
-
>
|
|
271
|
-
<span className={selectedOption ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400'}>
|
|
272
|
-
{selectedOption ? selectedOption.label : placeholder || 'Seleccionar...'}
|
|
273
|
-
</span>
|
|
274
|
-
<svg
|
|
275
|
-
className={`w-5 h-5 transition-transform text-gray-400 ${isOpen ? 'transform rotate-180' : ''}`}
|
|
276
|
-
fill="none"
|
|
277
|
-
stroke="currentColor"
|
|
278
|
-
viewBox="0 0 24 24"
|
|
279
|
-
>
|
|
280
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
281
|
-
</svg>
|
|
282
|
-
</button>
|
|
283
|
-
|
|
284
|
-
{isOpen && (
|
|
285
|
-
<div
|
|
286
|
-
ref={dropdownRef}
|
|
287
|
-
role="listbox"
|
|
288
|
-
aria-labelledby={label ? `${selectId}-label` : undefined}
|
|
289
|
-
className={combinedDropdownClassName}
|
|
290
|
-
>
|
|
291
|
-
{options.map((option, index) => (
|
|
292
|
-
<div
|
|
293
|
-
key={option.value}
|
|
294
|
-
role="option"
|
|
295
|
-
aria-selected={option.value === currentValue}
|
|
296
|
-
aria-disabled={option.disabled}
|
|
297
|
-
className={getOptionClasses(option, index)}
|
|
298
|
-
onClick={() => handleSelect(option)}
|
|
299
|
-
onMouseEnter={() => !option.disabled && setHoveredIndex(index)}
|
|
300
|
-
>
|
|
301
|
-
{option.label}
|
|
302
|
-
</div>
|
|
303
|
-
))}
|
|
304
|
-
</div>
|
|
305
|
-
)}
|
|
306
|
-
|
|
307
|
-
{/* Hidden input for form submission */}
|
|
308
|
-
{name && (
|
|
309
|
-
<input
|
|
310
|
-
type="hidden"
|
|
311
|
-
name={name}
|
|
312
|
-
value={currentValue ?? ''}
|
|
313
|
-
/>
|
|
314
|
-
)}
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
{helperText && (
|
|
318
|
-
<p className={`mt-1 text-sm ${error ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400'}`}>
|
|
319
|
-
{helperText}
|
|
320
|
-
</p>
|
|
321
|
-
)}
|
|
322
|
-
</div>
|
|
323
|
-
);
|
|
324
|
-
};
|