ywana-core8 0.1.81 → 0.1.83
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/dist/index.cjs +100 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +155 -48
- package/dist/index.css.map +1 -1
- package/dist/index.modern.js +100 -71
- package/dist/index.modern.js.map +1 -1
- package/dist/index.umd.js +100 -71
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/html/list.css +27 -10
- package/src/html/list.example.js +336 -60
- package/src/html/list.js +56 -47
- package/src/html/table2.css +5 -5
- package/src/html/textfield2.css +109 -22
- package/src/html/textfield2.example.js +2 -2
- package/src/html/textfield2.js +28 -9
- package/src/widgets/login/LoginBox.css +14 -11
- package/src/widgets/login/LoginBox.js +1 -1
package/src/html/list.js
CHANGED
@@ -25,6 +25,7 @@ export const List = (props) => {
|
|
25
25
|
searchable = false,
|
26
26
|
searchPlaceholder = "Search...",
|
27
27
|
searchBy = ['line1', 'line2'],
|
28
|
+
searchPosition = 'top',
|
28
29
|
sortable = false,
|
29
30
|
sortBy,
|
30
31
|
sortDirection = 'asc',
|
@@ -32,6 +33,7 @@ export const List = (props) => {
|
|
32
33
|
multiSelect = false,
|
33
34
|
onMultiSelect,
|
34
35
|
dense = false,
|
36
|
+
outlined = false,
|
35
37
|
disabled = false,
|
36
38
|
animated = true,
|
37
39
|
virtualized = false,
|
@@ -79,7 +81,7 @@ export const List = (props) => {
|
|
79
81
|
}, [disabled, multiSelect, selected, onSelect, onMultiSelect])
|
80
82
|
|
81
83
|
// Handle search
|
82
|
-
const handleSearch = useCallback((
|
84
|
+
const handleSearch = useCallback((_, value) => {
|
83
85
|
setSearchTerm(value)
|
84
86
|
}, [])
|
85
87
|
|
@@ -123,10 +125,28 @@ export const List = (props) => {
|
|
123
125
|
if (onSort) onSort(newConfig)
|
124
126
|
}, [sortable, sortConfig, onSort])
|
125
127
|
|
128
|
+
// Search component JSX - memoized to prevent focus loss
|
129
|
+
const searchComponent = useMemo(() => (
|
130
|
+
searchable && (
|
131
|
+
<div className="list__search">
|
132
|
+
<TextField
|
133
|
+
id="list-search"
|
134
|
+
placeholder={searchPlaceholder}
|
135
|
+
value={searchTerm}
|
136
|
+
onChange={handleSearch}
|
137
|
+
icon="search"
|
138
|
+
outlined={true}
|
139
|
+
size="small"
|
140
|
+
/>
|
141
|
+
</div>
|
142
|
+
)
|
143
|
+
), [searchable, searchPlaceholder, searchTerm, handleSearch])
|
144
|
+
|
126
145
|
// Generate CSS classes
|
127
146
|
const cssClasses = [
|
128
147
|
'list',
|
129
148
|
dense && 'list--dense',
|
149
|
+
outlined && 'list--outlined',
|
130
150
|
disabled && 'list--disabled',
|
131
151
|
animated && 'list--animated',
|
132
152
|
loading && 'list--loading',
|
@@ -158,23 +178,12 @@ export const List = (props) => {
|
|
158
178
|
if (empty || sortedItems.length === 0) {
|
159
179
|
return (
|
160
180
|
<div className={cssClasses} style={style} {...ariaAttributes} {...restProps}>
|
161
|
-
{
|
162
|
-
<div className="list__search">
|
163
|
-
<TextField
|
164
|
-
id="list-search"
|
165
|
-
placeholder={searchPlaceholder}
|
166
|
-
value={searchTerm}
|
167
|
-
onChange={handleSearch}
|
168
|
-
icon="search"
|
169
|
-
outlined={true}
|
170
|
-
size="small"
|
171
|
-
/>
|
172
|
-
</div>
|
173
|
-
)}
|
181
|
+
{searchPosition === 'top' && searchComponent}
|
174
182
|
<div className="list__empty">
|
175
183
|
<Icon icon={emptyIcon} size="large" />
|
176
184
|
<Text>{emptyMessage}</Text>
|
177
185
|
</div>
|
186
|
+
{searchPosition === 'bottom' && searchComponent}
|
178
187
|
{children}
|
179
188
|
</div>
|
180
189
|
)
|
@@ -195,19 +204,7 @@ export const List = (props) => {
|
|
195
204
|
/>
|
196
205
|
) : (
|
197
206
|
<div className={cssClasses} style={style} ref={listRef} {...ariaAttributes} {...restProps}>
|
198
|
-
{
|
199
|
-
<div className="list__search">
|
200
|
-
<TextField
|
201
|
-
id="list-search"
|
202
|
-
placeholder={searchPlaceholder}
|
203
|
-
value={searchTerm}
|
204
|
-
onChange={handleSearch}
|
205
|
-
icon="search"
|
206
|
-
outlined={true}
|
207
|
-
size="small"
|
208
|
-
/>
|
209
|
-
</div>
|
210
|
-
)}
|
207
|
+
{searchPosition === 'top' && searchComponent}
|
211
208
|
|
212
209
|
{sortable && sortBy && (
|
213
210
|
<div className="list__sort">
|
@@ -244,6 +241,7 @@ export const List = (props) => {
|
|
244
241
|
/>
|
245
242
|
))}
|
246
243
|
</ul>
|
244
|
+
{searchPosition === 'bottom' && searchComponent}
|
247
245
|
{children}
|
248
246
|
</div>
|
249
247
|
)
|
@@ -264,6 +262,7 @@ const GroupedList = (props) => {
|
|
264
262
|
onSearch,
|
265
263
|
searchable = false,
|
266
264
|
searchPlaceholder = "Search...",
|
265
|
+
searchPosition = 'top',
|
267
266
|
multiSelect = false,
|
268
267
|
dense = false,
|
269
268
|
disabled = false,
|
@@ -302,21 +301,26 @@ const GroupedList = (props) => {
|
|
302
301
|
})
|
303
302
|
}, [])
|
304
303
|
|
304
|
+
// Search component for grouped list - memoized to prevent focus loss
|
305
|
+
const groupedSearchComponent = useMemo(() => (
|
306
|
+
searchable && (
|
307
|
+
<div className="list__search">
|
308
|
+
<TextField
|
309
|
+
id="grouped-list-search"
|
310
|
+
placeholder={searchPlaceholder}
|
311
|
+
value={searchTerm}
|
312
|
+
onChange={onSearch}
|
313
|
+
icon="search"
|
314
|
+
outlined={true}
|
315
|
+
size="small"
|
316
|
+
/>
|
317
|
+
</div>
|
318
|
+
)
|
319
|
+
), [searchable, searchPlaceholder, searchTerm, onSearch])
|
320
|
+
|
305
321
|
return (
|
306
322
|
<div className={`${cssClasses} grouped`} style={style} {...ariaAttributes} {...restProps}>
|
307
|
-
{
|
308
|
-
<div className="list__search">
|
309
|
-
<TextField
|
310
|
-
id="grouped-list-search"
|
311
|
-
placeholder={searchPlaceholder}
|
312
|
-
value={searchTerm}
|
313
|
-
onChange={onSearch}
|
314
|
-
icon="search"
|
315
|
-
outlined={true}
|
316
|
-
size="small"
|
317
|
-
/>
|
318
|
-
</div>
|
319
|
-
)}
|
323
|
+
{searchPosition === 'top' && groupedSearchComponent}
|
320
324
|
|
321
325
|
{groups.map(group => {
|
322
326
|
const isCollapsed = collapsedGroups.has(group.name)
|
@@ -373,6 +377,7 @@ const GroupedList = (props) => {
|
|
373
377
|
</Fragment>
|
374
378
|
)
|
375
379
|
})}
|
380
|
+
{searchPosition === 'bottom' && groupedSearchComponent}
|
376
381
|
{children}
|
377
382
|
</div>
|
378
383
|
)
|
@@ -509,19 +514,19 @@ const ListItem = ({
|
|
509
514
|
)}
|
510
515
|
</main>
|
511
516
|
|
512
|
-
{/* Meta information */}
|
513
|
-
{meta && (
|
514
|
-
<div className="list__item-meta">
|
515
|
-
{typeof meta === 'string' ? <Text size="small">{meta}</Text> : meta}
|
516
|
-
</div>
|
517
|
-
)}
|
518
|
-
|
519
517
|
{/* Actions */}
|
520
518
|
{actions && (
|
521
519
|
<div className="list__item-actions" role="toolbar">
|
522
520
|
{actions}
|
523
521
|
</div>
|
524
522
|
)}
|
523
|
+
|
524
|
+
{/* Meta information */}
|
525
|
+
{meta && (
|
526
|
+
<div className="list__item-meta">
|
527
|
+
{typeof meta === 'string' ? <Text size="small">{meta}</Text> : meta}
|
528
|
+
</div>
|
529
|
+
)}
|
525
530
|
</li>
|
526
531
|
)
|
527
532
|
}
|
@@ -569,6 +574,8 @@ List.propTypes = {
|
|
569
574
|
searchPlaceholder: PropTypes.string,
|
570
575
|
/** Properties to search by */
|
571
576
|
searchBy: PropTypes.arrayOf(PropTypes.string),
|
577
|
+
/** Search position */
|
578
|
+
searchPosition: PropTypes.oneOf(['top', 'bottom']),
|
572
579
|
/** Enable sorting */
|
573
580
|
sortable: PropTypes.bool,
|
574
581
|
/** Property to sort by */
|
@@ -583,6 +590,8 @@ List.propTypes = {
|
|
583
590
|
onMultiSelect: PropTypes.func,
|
584
591
|
/** Dense layout */
|
585
592
|
dense: PropTypes.bool,
|
593
|
+
/** Outlined variant with borders */
|
594
|
+
outlined: PropTypes.bool,
|
586
595
|
/** Disabled state */
|
587
596
|
disabled: PropTypes.bool,
|
588
597
|
/** Enable animations */
|
package/src/html/table2.css
CHANGED
@@ -1415,22 +1415,22 @@ body.datatable2-resizing {
|
|
1415
1415
|
}
|
1416
1416
|
|
1417
1417
|
.datatable2__filter-cell {
|
1418
|
-
padding:
|
1418
|
+
padding: .5rem;
|
1419
1419
|
}
|
1420
1420
|
|
1421
1421
|
.datatable2__filter-field {
|
1422
1422
|
width: 100% !important;
|
1423
1423
|
height: 32px !important;
|
1424
|
-
border: 1px solid var(--divider-color, #e0e0e0) !important;
|
1425
1424
|
border-radius: 4px !important;
|
1426
|
-
padding: 4px 8px !important;
|
1427
1425
|
font-size: 0.75rem !important;
|
1428
1426
|
background: white !important;
|
1427
|
+
padding-top: 0px !important;
|
1428
|
+
}
|
1429
|
+
|
1430
|
+
.datatable2__filter-field>input {
|
1429
1431
|
}
|
1430
1432
|
|
1431
1433
|
.datatable2__filter-field:focus {
|
1432
|
-
border-color: var(--primary-color, #2196f3) !important;
|
1433
|
-
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2) !important;
|
1434
1434
|
}
|
1435
1435
|
|
1436
1436
|
.datatable2__filter-actions-cell {
|
package/src/html/textfield2.css
CHANGED
@@ -97,11 +97,19 @@
|
|
97
97
|
.textfield2-clear,
|
98
98
|
.textfield2-password-toggle {
|
99
99
|
position: absolute;
|
100
|
-
top:
|
101
|
-
|
100
|
+
top: 50%;
|
101
|
+
transform: translateY(-50%);
|
102
|
+
right: 0.75rem; /* Más espacio desde el borde */
|
102
103
|
color: var(--text-color-light, #666);
|
103
104
|
cursor: pointer;
|
104
105
|
transition: color 0.2s ease;
|
106
|
+
z-index: 2;
|
107
|
+
}
|
108
|
+
|
109
|
+
/* Icons positioning for outlined variant */
|
110
|
+
.textfield2-outlined .textfield2-clear,
|
111
|
+
.textfield2-outlined .textfield2-password-toggle {
|
112
|
+
right: 0.75rem; /* Alineado con el padding del input */
|
105
113
|
}
|
106
114
|
|
107
115
|
.textfield2-clear:hover,
|
@@ -109,13 +117,28 @@
|
|
109
117
|
color: var(--text-color, #333);
|
110
118
|
}
|
111
119
|
|
120
|
+
/* For fields without labels, the centering still works with transform */
|
112
121
|
.textfield2.no-label .textfield2-clear,
|
113
122
|
.textfield2.no-label .textfield2-password-toggle {
|
114
|
-
top
|
123
|
+
/* Remove top override - let transform handle centering */
|
115
124
|
}
|
116
125
|
|
117
126
|
.textfield2-password-toggle {
|
118
|
-
right: 2.
|
127
|
+
right: 2.5rem; /* Más espacio para el clear button */
|
128
|
+
}
|
129
|
+
|
130
|
+
/* When both clear and password toggle are present */
|
131
|
+
.textfield2.textfield2-password .textfield2-clear {
|
132
|
+
right: 2.5rem;
|
133
|
+
}
|
134
|
+
|
135
|
+
/* Outlined variant - icon spacing */
|
136
|
+
.textfield2-outlined .textfield2-password-toggle {
|
137
|
+
right: 2.5rem;
|
138
|
+
}
|
139
|
+
|
140
|
+
.textfield2-outlined.textfield2-password .textfield2-clear {
|
141
|
+
right: 2.5rem;
|
119
142
|
}
|
120
143
|
|
121
144
|
.textfield2.textfield2-date .textfield2-clear,
|
@@ -161,6 +184,32 @@
|
|
161
184
|
transform: scale(0.9);
|
162
185
|
}
|
163
186
|
|
187
|
+
/* Outlined variant - label positioning when active */
|
188
|
+
.textfield2-outlined > input:focus ~ label,
|
189
|
+
.textfield2-outlined > input:valid ~ label,
|
190
|
+
.textfield2-outlined > input[value]:not([value=""]) ~ label,
|
191
|
+
.textfield2-outlined > textarea:focus ~ label,
|
192
|
+
.textfield2-outlined > textarea:valid ~ label,
|
193
|
+
.textfield2-outlined > textarea[value]:not([value=""]) ~ label,
|
194
|
+
.textfield2-outlined.focused > label,
|
195
|
+
.textfield2-outlined.has-placeholder > label {
|
196
|
+
top: -0.5rem; /* Flotando sobre el borde */
|
197
|
+
left: 0.75rem;
|
198
|
+
font-size: 0.875rem; /* Tamaño más legible */
|
199
|
+
color: var(--primary-color, #2196f3);
|
200
|
+
transform: none; /* Sin escala adicional */
|
201
|
+
}
|
202
|
+
|
203
|
+
/* Error state for outlined labels */
|
204
|
+
.textfield2-outlined.error > input:focus ~ label,
|
205
|
+
.textfield2-outlined.error > textarea:focus ~ label,
|
206
|
+
.textfield2-outlined.invalid > input:focus ~ label,
|
207
|
+
.textfield2-outlined.invalid > textarea:focus ~ label,
|
208
|
+
.textfield2-outlined.error.focused > label,
|
209
|
+
.textfield2-outlined.invalid.focused > label {
|
210
|
+
color: var(--error-color, #f44336);
|
211
|
+
}
|
212
|
+
|
164
213
|
.textfield2.error > input:focus ~ label,
|
165
214
|
.textfield2.error > textarea:focus ~ label,
|
166
215
|
.textfield2.invalid > input:focus ~ label,
|
@@ -228,12 +277,18 @@
|
|
228
277
|
display: flex;
|
229
278
|
align-items: center;
|
230
279
|
gap: 0.25rem;
|
231
|
-
margin-top: 0.
|
280
|
+
margin-top: 0.5rem; /* Más espacio desde el input */
|
232
281
|
font-size: 0.75rem;
|
233
282
|
line-height: 1.2;
|
234
283
|
min-height: 1rem;
|
235
284
|
}
|
236
285
|
|
286
|
+
/* Helper text for outlined variant */
|
287
|
+
.textfield2-outlined .textfield2-helper {
|
288
|
+
margin-top: 0.5rem;
|
289
|
+
margin-left: 0.75rem; /* Alineado con el input */
|
290
|
+
}
|
291
|
+
|
237
292
|
.textfield2-helper.helper {
|
238
293
|
color: var(--text-color-light, #666);
|
239
294
|
}
|
@@ -246,39 +301,71 @@
|
|
246
301
|
flex-shrink: 0;
|
247
302
|
}
|
248
303
|
|
249
|
-
/* Outlined variant */
|
304
|
+
/* Outlined variant - Container structure */
|
250
305
|
.textfield2-outlined {
|
251
|
-
|
252
|
-
|
253
|
-
padding: 0.5rem;
|
254
|
-
background-color: var(--paper-color, #fff);
|
306
|
+
position: relative;
|
307
|
+
margin-bottom: 1.5rem; /* Espacio para helper text */
|
255
308
|
}
|
256
309
|
|
310
|
+
/* Outlined input container - solo el input tiene borde */
|
257
311
|
.textfield2-outlined > input,
|
258
312
|
.textfield2-outlined > textarea {
|
259
|
-
|
260
|
-
|
313
|
+
width: 100%;
|
314
|
+
border: 1px solid var(--divider-color, #e0e0e0);
|
315
|
+
border-radius: 4px;
|
316
|
+
padding: 1rem 3rem 1rem 0.75rem; /* Espacio para iconos a la derecha */
|
317
|
+
background-color: var(--paper-color, #fff);
|
318
|
+
font-size: 1rem;
|
319
|
+
line-height: 1.5;
|
320
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
321
|
+
outline: none;
|
261
322
|
}
|
262
323
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
top: -0.5rem;
|
324
|
+
/* Adjust padding when no icons are present */
|
325
|
+
.textfield2-outlined:not(.has-icons) > input,
|
326
|
+
.textfield2-outlined:not(.has-icons) > textarea {
|
327
|
+
padding-right: 0.75rem;
|
268
328
|
}
|
269
329
|
|
270
|
-
|
330
|
+
/* Focus states for outlined inputs */
|
271
331
|
.textfield2-outlined > input:focus,
|
272
|
-
.textfield2-outlined > textarea:focus
|
332
|
+
.textfield2-outlined > textarea:focus,
|
333
|
+
.textfield2-outlined.focused > input,
|
334
|
+
.textfield2-outlined.focused > textarea {
|
273
335
|
border-color: var(--primary-color, #2196f3);
|
274
|
-
|
336
|
+
box-shadow: 0 0 0 1px var(--primary-color, #2196f3);
|
275
337
|
}
|
276
338
|
|
277
|
-
|
278
|
-
.textfield2-outlined.
|
339
|
+
/* Error states for outlined inputs */
|
340
|
+
.textfield2-outlined.error > input,
|
341
|
+
.textfield2-outlined.error > textarea,
|
342
|
+
.textfield2-outlined.invalid > input,
|
343
|
+
.textfield2-outlined.invalid > textarea {
|
279
344
|
border-color: var(--error-color, #f44336);
|
280
345
|
}
|
281
346
|
|
347
|
+
.textfield2-outlined.error > input:focus,
|
348
|
+
.textfield2-outlined.error > textarea:focus,
|
349
|
+
.textfield2-outlined.invalid > input:focus,
|
350
|
+
.textfield2-outlined.invalid > textarea:focus {
|
351
|
+
box-shadow: 0 0 0 1px var(--error-color, #f44336);
|
352
|
+
}
|
353
|
+
|
354
|
+
/* Outlined label positioning */
|
355
|
+
.textfield2-outlined > label {
|
356
|
+
position: absolute;
|
357
|
+
left: 0.75rem;
|
358
|
+
top: 1rem; /* Centrado en el input */
|
359
|
+
background-color: var(--paper-color, #fff);
|
360
|
+
padding: 0 0.25rem;
|
361
|
+
font-size: 1rem;
|
362
|
+
color: var(--text-color-light, #666);
|
363
|
+
transition: all 0.2s ease;
|
364
|
+
pointer-events: none;
|
365
|
+
z-index: 1;
|
366
|
+
}
|
367
|
+
|
368
|
+
/* Hide the focus bar for outlined variant */
|
282
369
|
.textfield2-outlined .textfield2-bar {
|
283
370
|
display: none;
|
284
371
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import React, { useState } from 'react'
|
2
|
-
import { TextField2, TextArea2, PasswordField2, DropDown2, DateRange2 } from '
|
3
|
-
import { ExampleLayout, ExampleSection, ExampleSubsection, CodeSnippet } from './ExampleLayout'
|
2
|
+
import { TextField2, TextArea2, PasswordField2, DropDown2, DateRange2 } from './textfield2'
|
3
|
+
import { ExampleLayout, ExampleSection, ExampleSubsection, CodeSnippet } from './ExampleLayout'
|
4
4
|
|
5
5
|
/**
|
6
6
|
* Ejemplos de uso de los componentes TextField2 mejorados
|
package/src/html/textfield2.js
CHANGED
@@ -51,6 +51,7 @@ export const TextField2 = (props) => {
|
|
51
51
|
const [isFocused, setIsFocused] = useState(false)
|
52
52
|
const [internalError, setInternalError] = useState('')
|
53
53
|
const [isValid, setIsValid] = useState(true)
|
54
|
+
const [hasBeenTouched, setHasBeenTouched] = useState(false)
|
54
55
|
const inputRef = useRef(null)
|
55
56
|
const debounceRef = useRef(null)
|
56
57
|
|
@@ -59,8 +60,15 @@ export const TextField2 = (props) => {
|
|
59
60
|
console.warn('TextField2 component: id prop is required')
|
60
61
|
}
|
61
62
|
|
62
|
-
// Validate value and set error states
|
63
|
+
// Validate value and set error states - only after user interaction
|
63
64
|
useEffect(() => {
|
65
|
+
// Don't validate required fields until user has interacted with the field
|
66
|
+
if (!hasBeenTouched && required && !value) {
|
67
|
+
setIsValid(true)
|
68
|
+
setInternalError('')
|
69
|
+
return
|
70
|
+
}
|
71
|
+
|
64
72
|
if (validation && value !== undefined) {
|
65
73
|
const validationResult = validation(value)
|
66
74
|
const valid = typeof validationResult === 'boolean' ? validationResult : validationResult.valid
|
@@ -72,27 +80,32 @@ export const TextField2 = (props) => {
|
|
72
80
|
if (onValidation) {
|
73
81
|
onValidation(id, valid, errorMessage)
|
74
82
|
}
|
75
|
-
} else if (required && !value) {
|
83
|
+
} else if (required && !value && hasBeenTouched) {
|
76
84
|
setIsValid(false)
|
77
85
|
setInternalError('This field is required')
|
78
86
|
} else {
|
79
87
|
setIsValid(true)
|
80
88
|
setInternalError('')
|
81
89
|
}
|
82
|
-
}, [value, required, id]) //
|
90
|
+
}, [value, required, id, hasBeenTouched]) // Added hasBeenTouched to dependencies
|
83
91
|
|
84
92
|
// Handle input changes with debouncing
|
85
93
|
const handleChange = useCallback((event) => {
|
86
94
|
if (disabled || readOnly) return
|
87
|
-
|
95
|
+
|
88
96
|
event.stopPropagation()
|
89
97
|
const newValue = event.target.value
|
90
|
-
|
98
|
+
|
99
|
+
// Mark field as touched on first change
|
100
|
+
if (!hasBeenTouched) {
|
101
|
+
setHasBeenTouched(true)
|
102
|
+
}
|
103
|
+
|
91
104
|
// Clear previous debounce
|
92
105
|
if (debounceRef.current) {
|
93
106
|
clearTimeout(debounceRef.current)
|
94
107
|
}
|
95
|
-
|
108
|
+
|
96
109
|
if (debounceMs > 0) {
|
97
110
|
debounceRef.current = setTimeout(() => {
|
98
111
|
if (onChange) onChange(id, newValue, event)
|
@@ -100,7 +113,7 @@ export const TextField2 = (props) => {
|
|
100
113
|
} else {
|
101
114
|
if (onChange) onChange(id, newValue, event)
|
102
115
|
}
|
103
|
-
}, [disabled, readOnly, id, onChange, debounceMs])
|
116
|
+
}, [disabled, readOnly, id, onChange, debounceMs, hasBeenTouched])
|
104
117
|
|
105
118
|
// Handle key press events
|
106
119
|
const handleKeyPress = useCallback((event) => {
|
@@ -131,10 +144,16 @@ export const TextField2 = (props) => {
|
|
131
144
|
// Handle blur events
|
132
145
|
const handleBlur = useCallback((event) => {
|
133
146
|
if (disabled) return
|
134
|
-
|
147
|
+
|
135
148
|
setIsFocused(false)
|
149
|
+
|
150
|
+
// Mark field as touched on blur if it hasn't been touched yet
|
151
|
+
if (!hasBeenTouched) {
|
152
|
+
setHasBeenTouched(true)
|
153
|
+
}
|
154
|
+
|
136
155
|
if (onBlur) onBlur(event)
|
137
|
-
}, [disabled, onBlur])
|
156
|
+
}, [disabled, onBlur, hasBeenTouched])
|
138
157
|
|
139
158
|
// Handle clear action
|
140
159
|
const handleClear = useCallback(() => {
|
@@ -2,7 +2,7 @@
|
|
2
2
|
background-color: var(--paper-color);
|
3
3
|
}
|
4
4
|
|
5
|
-
.login-box
|
5
|
+
.login-box>main {
|
6
6
|
padding: 1rem 1rem 0 1rem;
|
7
7
|
display: flex;
|
8
8
|
flex-direction: column;
|
@@ -34,24 +34,26 @@
|
|
34
34
|
}
|
35
35
|
|
36
36
|
@keyframes rotation-counterclock {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
from {
|
38
|
+
transform: rotate(359deg);
|
39
|
+
}
|
40
|
+
|
41
|
+
to {
|
42
|
+
transform: rotate(0deg);
|
43
43
|
}
|
44
|
+
}
|
44
45
|
|
45
46
|
@keyframes rotation {
|
46
47
|
from {
|
47
48
|
transform: rotate(0deg);
|
48
49
|
}
|
50
|
+
|
49
51
|
to {
|
50
52
|
transform: rotate(359deg);
|
51
53
|
}
|
52
54
|
}
|
53
55
|
|
54
|
-
.login-box
|
56
|
+
.login-box>footer {
|
55
57
|
grid-area: footer;
|
56
58
|
padding: 1rem;
|
57
59
|
display: flex;
|
@@ -63,6 +65,7 @@
|
|
63
65
|
from {
|
64
66
|
opacity: 0;
|
65
67
|
}
|
68
|
+
|
66
69
|
to {
|
67
70
|
opacity: 1;
|
68
71
|
}
|
@@ -74,7 +77,7 @@
|
|
74
77
|
}
|
75
78
|
|
76
79
|
.login-box .forgot-button {
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
+
text-align: right;
|
81
|
+
font-weight: 500;
|
82
|
+
max-width: 15rem;
|
80
83
|
}
|