@moni-labs/moni-ui 0.4.6 → 0.4.7
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/custom-elements.json +181 -1
- package/dist/browser/moni-ui.iife.min.js +289 -274
- package/dist/browser/moni-ui.iife.min.js.br +0 -0
- package/dist/browser/moni-ui.iife.min.js.gz +0 -0
- package/dist/browser/moni-ui.iife.min.js.map +1 -1
- package/dist/browser/moni-ui.min.js +1462 -1372
- package/dist/browser/moni-ui.min.js.br +0 -0
- package/dist/browser/moni-ui.min.js.gz +0 -0
- package/dist/browser/moni-ui.min.js.map +1 -1
- package/dist/cdn/components/moni-color-field.js +1 -0
- package/dist/cdn/components/moni-select.js +3 -3
- package/dist/cdn/components/moni-text-field.js +12 -2
- package/dist/cdn/components/moni-textarea.js +6 -2
- package/dist/cdn/importmap.json +98 -98
- package/dist/components/moni-color-field.d.ts.map +1 -1
- package/dist/components/moni-color-field.js +1 -0
- package/dist/components/moni-select.d.ts +28 -0
- package/dist/components/moni-select.d.ts.map +1 -1
- package/dist/components/moni-select.js +48 -0
- package/dist/components/moni-text-field.d.ts +45 -2
- package/dist/components/moni-text-field.d.ts.map +1 -1
- package/dist/components/moni-text-field.js +113 -1
- package/dist/components/moni-textarea.d.ts +27 -0
- package/dist/components/moni-textarea.d.ts.map +1 -1
- package/dist/components/moni-textarea.js +61 -0
- package/docs/api.json +178 -5
- package/docs/assets/moni-context-menu/placement--top.png +0 -0
- package/docs/assets/moni-select/required--false.png +0 -0
- package/docs/assets/moni-select/required--true.png +0 -0
- package/docs/assets/moni-text-field/autocomplete--default.png +0 -0
- package/docs/assets/moni-text-field/inputmode--default.png +0 -0
- package/docs/assets/moni-text-field/max--default.png +0 -0
- package/docs/assets/moni-text-field/maxlength--default.png +0 -0
- package/docs/assets/moni-text-field/min--default.png +0 -0
- package/docs/assets/moni-text-field/minlength--default.png +0 -0
- package/docs/assets/moni-text-field/pattern--default.png +0 -0
- package/docs/assets/moni-text-field/readonly--false.png +0 -0
- package/docs/assets/moni-text-field/readonly--true.png +0 -0
- package/docs/assets/moni-text-field/required--false.png +0 -0
- package/docs/assets/moni-text-field/required--true.png +0 -0
- package/docs/assets/moni-text-field/step--default.png +0 -0
- package/docs/assets/moni-text-field/type--color.png +0 -0
- package/docs/assets/moni-text-field/type--date.png +0 -0
- package/docs/assets/moni-text-field/type--datetime-local.png +0 -0
- package/docs/assets/moni-text-field/type--month.png +0 -0
- package/docs/assets/moni-text-field/type--time.png +0 -0
- package/docs/assets/moni-text-field/type--week.png +0 -0
- package/docs/assets/moni-textarea/autocomplete--default.png +0 -0
- package/docs/assets/moni-textarea/minlength--default.png +0 -0
- package/docs/assets/moni-textarea/readonly--false.png +0 -0
- package/docs/assets/moni-textarea/readonly--true.png +0 -0
- package/docs/assets/moni-textarea/required--false.png +0 -0
- package/docs/assets/moni-textarea/required--true.png +0 -0
- package/docs/components/select.md +15 -1
- package/docs/components/text-field.md +89 -1
- package/docs/components/textarea.md +34 -1
- package/package.json +1 -1
- package/src/components/moni-color-field.ts +1 -0
- package/src/components/moni-select.test.ts +34 -0
- package/src/components/moni-select.ts +54 -0
- package/src/components/moni-text-field.test.ts +78 -0
- package/src/components/moni-text-field.ts +109 -2
- package/src/components/moni-textarea.ts +62 -0
|
@@ -92,7 +92,7 @@ Las utilities aplicadas directamente al tag afectan su caja anfitriona. No uses
|
|
|
92
92
|
| `variant` | `variant` | `'filled' \| 'outlined' \| 'underlined'` | `'filled'` | Variante visual del campo de texto. |
|
|
93
93
|
| `size` | `size` | `'small' \| 'medium' \| 'large' \| 'extra'` | `'medium'` | Define las dimensiones del campo de texto. |
|
|
94
94
|
| `shape` | `shape` | `'round' \| 'no-round'` | `'no-round'` | Forma del radio del borde (border-radius) del campo. |
|
|
95
|
-
| `type` | `type` | `'text' \| 'password' \| 'email' \| 'number' \| 'tel' \| 'url' \| 'search'` | `'text'` | El tipo de input HTML nativo. |
|
|
95
|
+
| `type` | `type` | `'text' \| 'password' \| 'email' \| 'number' \| 'tel' \| 'url' \| 'search' \| 'date' \| 'time' \| 'datetime-local' \| 'month' \| 'week' \| 'color'` | `'text'` | El tipo de input HTML nativo. |
|
|
96
96
|
| `icon` | `icon` | `string` | `''` | Nombre del icono inicial (leading) (Material Symbols). |
|
|
97
97
|
| `trailing-icon` | `trailingIcon` | `string` | `''` | Nombre del icono final (trailing) (Material Symbols). |
|
|
98
98
|
| `prefix` | `prefix` | `string` | `''` | Prefijo de texto corto mostrado antes del valor del input. |
|
|
@@ -106,6 +106,16 @@ Las utilities aplicadas directamente al tag afectan su caja anfitriona. No uses
|
|
|
106
106
|
| `disabled` | `disabled` | `boolean` | `false` | Deshabilita el campo de texto. |
|
|
107
107
|
| `value` | `value` | `string` | `''` | El valor actual del input. |
|
|
108
108
|
| `placeholder` | `placeholder` | `string` | `''` | Texto de marcador de posición (placeholder) mostrado cuando el input está vacío y la etiqueta es flotante. |
|
|
109
|
+
| `required` | `required` | `boolean` | `false` | Marca el campo como obligatorio y lo integra en la validación del formulario. |
|
|
110
|
+
| `readonly` | `readonly` | `boolean` | `false` | Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`. |
|
|
111
|
+
| `autocomplete` | `autocomplete` | `string` | `''` | Pista de autocompletado del navegador, por ejemplo `email` o `current-password`. |
|
|
112
|
+
| `inputmode` | `inputmode` | `string` | `''` | Teclado virtual a mostrar en móvil, por ejemplo `numeric` o `tel`. |
|
|
113
|
+
| `maxlength` | `maxlength` | `number \| null` | `null` | Máximo de caracteres aceptados. |
|
|
114
|
+
| `minlength` | `minlength` | `number \| null` | `null` | Mínimo de caracteres aceptados. |
|
|
115
|
+
| `min` | `min` | `string` | `''` | Valor mínimo para `number` y los tipos de fecha y hora. |
|
|
116
|
+
| `max` | `max` | `string` | `''` | Valor máximo para `number` y los tipos de fecha y hora. |
|
|
117
|
+
| `step` | `step` | `string` | `''` | Incremento para `number` y los tipos de fecha y hora. |
|
|
118
|
+
| `pattern` | `pattern` | `string` | `''` | Expresión regular que debe cumplir el valor. |
|
|
109
119
|
| `mask` | `mask` | `string` | `''` | Patrón Inputmask. Admite opcionales `[]`, grupos `()`, alternadores `\|`, cuantificadores `{n,m}` y los tokens `9`, `a`, `*` y `K` (RUT). |
|
|
110
120
|
| `mask-alias` | `maskAlias` | `string` | `''` | Alias integrado de Inputmask, por ejemplo `email`, `datetime`, `numeric`, `currency` o `ip`. |
|
|
111
121
|
| `mask-options` | `maskOptions` | `InputmaskOptions` | `{}` | Opciones avanzadas de Inputmask. También acepta JSON mediante el atributo `mask-options`. |
|
|
@@ -131,6 +141,8 @@ Este componente no declara slots públicos.
|
|
|
131
141
|
- `formatWithMask(value: string, metadata = false): string |` — Método público formatWithMask.
|
|
132
142
|
- `setMaskedValue(value: string): void` — Método público setMaskedValue.
|
|
133
143
|
- `removeMask(): void` — Método público removeMask.
|
|
144
|
+
- `checkValidity(): boolean` — Método público checkValidity.
|
|
145
|
+
- `reportValidity(): boolean` — Método público reportValidity.
|
|
134
146
|
|
|
135
147
|
## CSS Parts
|
|
136
148
|
|
|
@@ -216,6 +228,18 @@ El tipo de input HTML nativo.
|
|
|
216
228
|
|
|
217
229
|

|
|
218
230
|
|
|
231
|
+

|
|
232
|
+
|
|
233
|
+

|
|
234
|
+
|
|
235
|
+

|
|
236
|
+
|
|
237
|
+

|
|
238
|
+
|
|
239
|
+

|
|
240
|
+
|
|
241
|
+

|
|
242
|
+
|
|
219
243
|
### `icon`
|
|
220
244
|
|
|
221
245
|
Nombre del icono inicial (leading) (Material Symbols).
|
|
@@ -301,6 +325,70 @@ Texto de marcador de posición (placeholder) mostrado cuando el input está vac
|
|
|
301
325
|
|
|
302
326
|

|
|
303
327
|
|
|
328
|
+
### `required`
|
|
329
|
+
|
|
330
|
+
Marca el campo como obligatorio y lo integra en la validación del formulario.
|
|
331
|
+
|
|
332
|
+

|
|
333
|
+
|
|
334
|
+

|
|
335
|
+
|
|
336
|
+
### `readonly`
|
|
337
|
+
|
|
338
|
+
Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`.
|
|
339
|
+
|
|
340
|
+

|
|
341
|
+
|
|
342
|
+

|
|
343
|
+
|
|
344
|
+
### `autocomplete`
|
|
345
|
+
|
|
346
|
+
Pista de autocompletado del navegador, por ejemplo `email` o `current-password`.
|
|
347
|
+
|
|
348
|
+

|
|
349
|
+
|
|
350
|
+
### `inputmode`
|
|
351
|
+
|
|
352
|
+
Teclado virtual a mostrar en móvil, por ejemplo `numeric` o `tel`.
|
|
353
|
+
|
|
354
|
+

|
|
355
|
+
|
|
356
|
+
### `maxlength`
|
|
357
|
+
|
|
358
|
+
Máximo de caracteres aceptados.
|
|
359
|
+
|
|
360
|
+

|
|
361
|
+
|
|
362
|
+
### `minlength`
|
|
363
|
+
|
|
364
|
+
Mínimo de caracteres aceptados.
|
|
365
|
+
|
|
366
|
+

|
|
367
|
+
|
|
368
|
+
### `min`
|
|
369
|
+
|
|
370
|
+
Valor mínimo para `number` y los tipos de fecha y hora.
|
|
371
|
+
|
|
372
|
+

|
|
373
|
+
|
|
374
|
+
### `max`
|
|
375
|
+
|
|
376
|
+
Valor máximo para `number` y los tipos de fecha y hora.
|
|
377
|
+
|
|
378
|
+

|
|
379
|
+
|
|
380
|
+
### `step`
|
|
381
|
+
|
|
382
|
+
Incremento para `number` y los tipos de fecha y hora.
|
|
383
|
+
|
|
384
|
+

|
|
385
|
+
|
|
386
|
+
### `pattern`
|
|
387
|
+
|
|
388
|
+
Expresión regular que debe cumplir el valor.
|
|
389
|
+
|
|
390
|
+

|
|
391
|
+
|
|
304
392
|
### `mask`
|
|
305
393
|
|
|
306
394
|
Patrón Inputmask. Admite opcionales `[]`, grupos `()`, alternadores `|`,
|
|
@@ -97,6 +97,10 @@ Las utilities aplicadas directamente al tag afectan su caja anfitriona. No uses
|
|
|
97
97
|
| `autosize` | `autosize` | `boolean` | `false` | Hace que el textarea crezca y se reduzca según su contenido. |
|
|
98
98
|
| `max-rows` | `maxRows` | `number` | `0` | Número máximo de filas visibles cuando `autosize` está activo. Cero no impone límite. |
|
|
99
99
|
| `maxlength` | `maxlength` | `number \| null` | `null` | Número máximo de caracteres permitidos en el área de texto. También habilita la visualización del contador de caracteres a menos que `noCounter` sea true. |
|
|
100
|
+
| `required` | `required` | `boolean` | `false` | Marca el campo como obligatorio y lo integra en la validación del formulario. |
|
|
101
|
+
| `readonly` | `readonly` | `boolean` | `false` | Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`. |
|
|
102
|
+
| `minlength` | `minlength` | `number \| null` | `null` | Mínimo de caracteres aceptados. |
|
|
103
|
+
| `autocomplete` | `autocomplete` | `string` | `''` | Pista de autocompletado del navegador. |
|
|
100
104
|
| `no-counter` | `noCounter` | `boolean` | `false` | Oculta la visualización del contador de caracteres cuando se establece `maxlength`. |
|
|
101
105
|
| `loading` | `loading` | `boolean` | `false` | Si es true, muestra un indicador de carga (progreso circular) al final. |
|
|
102
106
|
| `disabled` | `disabled` | `boolean` | `false` | Deshabilita el área de texto. |
|
|
@@ -116,7 +120,8 @@ No declara eventos propios.
|
|
|
116
120
|
|
|
117
121
|
## Métodos públicos
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
- `checkValidity(): boolean` — Método público checkValidity.
|
|
124
|
+
- `reportValidity(): boolean` — Método público reportValidity.
|
|
120
125
|
|
|
121
126
|
## CSS Parts
|
|
122
127
|
|
|
@@ -232,6 +237,34 @@ También habilita la visualización del contador de caracteres a menos que `noCo
|
|
|
232
237
|
|
|
233
238
|

|
|
234
239
|
|
|
240
|
+
### `required`
|
|
241
|
+
|
|
242
|
+
Marca el campo como obligatorio y lo integra en la validación del formulario.
|
|
243
|
+
|
|
244
|
+

|
|
245
|
+
|
|
246
|
+

|
|
247
|
+
|
|
248
|
+
### `readonly`
|
|
249
|
+
|
|
250
|
+
Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`.
|
|
251
|
+
|
|
252
|
+

|
|
253
|
+
|
|
254
|
+

|
|
255
|
+
|
|
256
|
+
### `minlength`
|
|
257
|
+
|
|
258
|
+
Mínimo de caracteres aceptados.
|
|
259
|
+
|
|
260
|
+

|
|
261
|
+
|
|
262
|
+
### `autocomplete`
|
|
263
|
+
|
|
264
|
+
Pista de autocompletado del navegador.
|
|
265
|
+
|
|
266
|
+

|
|
267
|
+
|
|
235
268
|
### `no-counter`
|
|
236
269
|
|
|
237
270
|
Oculta la visualización del contador de caracteres cuando se establece `maxlength`.
|
package/package.json
CHANGED
|
@@ -422,3 +422,37 @@ describe('moni-select · nombre accesible', () => {
|
|
|
422
422
|
el.remove();
|
|
423
423
|
});
|
|
424
424
|
})
|
|
425
|
+
|
|
426
|
+
describe('moni-select · required', () => {
|
|
427
|
+
/*
|
|
428
|
+
* Su input visible es readonly y por tanto queda fuera de la validación de
|
|
429
|
+
* restricciones, así que `required` se declara en el host.
|
|
430
|
+
*/
|
|
431
|
+
it('declara valueMissing mientras no haya valor', async () => {
|
|
432
|
+
// Igual que en text-field: jsdom no expone checkValidity() aquí.
|
|
433
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
434
|
+
const originalAttach = HTMLElement.prototype.attachInternals;
|
|
435
|
+
HTMLElement.prototype.attachInternals = function spy(this: HTMLElement) {
|
|
436
|
+
const internals = originalAttach.call(this);
|
|
437
|
+
const setValidity = internals.setValidity.bind(internals);
|
|
438
|
+
internals.setValidity = ((flags, message, anchor) => {
|
|
439
|
+
calls.push({ valueMissing: flags?.valueMissing === true });
|
|
440
|
+
return setValidity(flags, message, anchor);
|
|
441
|
+
}) as typeof internals.setValidity;
|
|
442
|
+
return internals;
|
|
443
|
+
};
|
|
444
|
+
const el = document.createElement('moni-select') as MoniSelect;
|
|
445
|
+
HTMLElement.prototype.attachInternals = originalAttach;
|
|
446
|
+
|
|
447
|
+
el.required = true;
|
|
448
|
+
document.body.appendChild(el);
|
|
449
|
+
await el.updateComplete;
|
|
450
|
+
expect(calls.some((c) => c.valueMissing === true)).toBe(true);
|
|
451
|
+
|
|
452
|
+
calls.length = 0;
|
|
453
|
+
el.value = 'uno';
|
|
454
|
+
await el.updateComplete;
|
|
455
|
+
expect(calls.at(-1)).toEqual({ valueMissing: false });
|
|
456
|
+
el.remove();
|
|
457
|
+
});
|
|
458
|
+
})
|
|
@@ -158,6 +158,15 @@ export class MoniSelect extends MoniElement {
|
|
|
158
158
|
*/
|
|
159
159
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Obliga a elegir una opción antes de enviar el formulario.
|
|
163
|
+
*
|
|
164
|
+
* El `<input>` visible es de sólo lectura, así que la validación nativa no
|
|
165
|
+
* se aplica sobre él: la restricción se declara en el host con
|
|
166
|
+
* `ElementInternals.setValidity`, que es lo que mira el formulario.
|
|
167
|
+
*/
|
|
168
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
169
|
+
|
|
161
170
|
/**
|
|
162
171
|
* Si es verdadero, muestra un indicador de carga (progreso lineal).
|
|
163
172
|
* @type {boolean}
|
|
@@ -281,6 +290,50 @@ export class MoniSelect extends MoniElement {
|
|
|
281
290
|
|
|
282
291
|
@query('slot') private _slot!: HTMLSlotElement;
|
|
283
292
|
@query('input') private _input!: HTMLInputElement;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Declara la validez del host según `required`.
|
|
296
|
+
*
|
|
297
|
+
* No se puede delegar en el `<input>` interno porque es `readonly`, y un
|
|
298
|
+
* campo de sólo lectura queda excluido de la validación de restricciones. El
|
|
299
|
+
* ancla sigue siendo ese input para que el navegador tenga dónde mostrar el
|
|
300
|
+
* mensaje y a dónde llevar el foco.
|
|
301
|
+
*/
|
|
302
|
+
/**
|
|
303
|
+
* API de validación equivalente a la de un control nativo.
|
|
304
|
+
*
|
|
305
|
+
* Los elementos form-associated participan en la validación del formulario,
|
|
306
|
+
* pero no reciben estos miembros automáticamente: hay que exponerlos para que
|
|
307
|
+
* un `moni-select` se pueda interrogar igual que un `<input>`.
|
|
308
|
+
*/
|
|
309
|
+
get validity(): ValidityState {
|
|
310
|
+
return this._internals.validity;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get validationMessage(): string {
|
|
314
|
+
return this._internals.validationMessage;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
checkValidity(): boolean {
|
|
318
|
+
return this._internals.checkValidity();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
reportValidity(): boolean {
|
|
322
|
+
return this._internals.reportValidity();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _syncValidity() {
|
|
326
|
+
if (!this._internals?.setValidity) return;
|
|
327
|
+
if (this.required && !this.value) {
|
|
328
|
+
this._internals.setValidity(
|
|
329
|
+
{ valueMissing: true },
|
|
330
|
+
'Selecciona una opción.',
|
|
331
|
+
this._input ?? undefined
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
this._internals.setValidity({});
|
|
336
|
+
}
|
|
284
337
|
@query('.dropdown-menu') private _menu?: HTMLElement;
|
|
285
338
|
|
|
286
339
|
static override styles = [
|
|
@@ -664,6 +717,7 @@ export class MoniSelect extends MoniElement {
|
|
|
664
717
|
if (changedProperties.has('value')) {
|
|
665
718
|
this._internals?.setFormValue?.(this.value);
|
|
666
719
|
}
|
|
720
|
+
this._syncValidity();
|
|
667
721
|
if (
|
|
668
722
|
changedProperties.has('_open') ||
|
|
669
723
|
changedProperties.has('_drilldownPath') ||
|
|
@@ -254,3 +254,81 @@ describe('moni-text-field · nombre accesible', () => {
|
|
|
254
254
|
el.remove();
|
|
255
255
|
});
|
|
256
256
|
})
|
|
257
|
+
|
|
258
|
+
describe('moni-text-field · restricciones nativas', () => {
|
|
259
|
+
/*
|
|
260
|
+
* El componente sólo reenviaba type/placeholder/disabled/value/name, así que
|
|
261
|
+
* cualquier campo con `required` o `autocomplete` tenía que renunciar a
|
|
262
|
+
* moni-ui y usar un input nativo.
|
|
263
|
+
*/
|
|
264
|
+
it('reenvía las restricciones al input interno', async () => {
|
|
265
|
+
const el = document.createElement('moni-text-field') as HTMLElement & {
|
|
266
|
+
updateComplete: Promise<unknown>;
|
|
267
|
+
[key: string]: unknown;
|
|
268
|
+
};
|
|
269
|
+
Object.assign(el, {
|
|
270
|
+
label: 'Correo',
|
|
271
|
+
type: 'email',
|
|
272
|
+
required: true,
|
|
273
|
+
autocomplete: 'email',
|
|
274
|
+
maxlength: 40,
|
|
275
|
+
minlength: 5,
|
|
276
|
+
pattern: '.+@.+',
|
|
277
|
+
inputmode: 'email'
|
|
278
|
+
});
|
|
279
|
+
document.body.appendChild(el);
|
|
280
|
+
await el.updateComplete;
|
|
281
|
+
|
|
282
|
+
const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
|
|
283
|
+
expect(input.required).toBe(true);
|
|
284
|
+
expect(input.getAttribute('autocomplete')).toBe('email');
|
|
285
|
+
expect(input.maxLength).toBe(40);
|
|
286
|
+
expect(input.minLength).toBe(5);
|
|
287
|
+
expect(input.getAttribute('pattern')).toBe('.+@.+');
|
|
288
|
+
expect(input.getAttribute('inputmode')).toBe('email');
|
|
289
|
+
el.remove();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('acepta los tipos de fecha y hora', async () => {
|
|
293
|
+
const el = document.createElement('moni-text-field') as HTMLElement & {
|
|
294
|
+
type: string;
|
|
295
|
+
updateComplete: Promise<unknown>;
|
|
296
|
+
};
|
|
297
|
+
el.type = 'date';
|
|
298
|
+
document.body.appendChild(el);
|
|
299
|
+
await el.updateComplete;
|
|
300
|
+
expect(el.shadowRoot?.querySelector('input')?.getAttribute('type')).toBe('date');
|
|
301
|
+
el.remove();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('publica la validez en el host para que el formulario la vea', async () => {
|
|
305
|
+
// jsdom no implementa checkValidity() en custom elements form-associated,
|
|
306
|
+
// así que se observa la llamada a setValidity. El bloqueo real del envío
|
|
307
|
+
// se verifica en navegador.
|
|
308
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
309
|
+
const originalAttach = HTMLElement.prototype.attachInternals;
|
|
310
|
+
HTMLElement.prototype.attachInternals = function spy(this: HTMLElement) {
|
|
311
|
+
const internals = originalAttach.call(this);
|
|
312
|
+
const setValidity = internals.setValidity.bind(internals);
|
|
313
|
+
internals.setValidity = ((flags, message, anchor) => {
|
|
314
|
+
// ValidityState expone sus flags como getters del prototipo, así que un
|
|
315
|
+
// spread devolvería {}: hay que leerlas uña a uña.
|
|
316
|
+
calls.push({ valueMissing: flags?.valueMissing === true });
|
|
317
|
+
return setValidity(flags, message, anchor);
|
|
318
|
+
}) as typeof internals.setValidity;
|
|
319
|
+
return internals;
|
|
320
|
+
};
|
|
321
|
+
const el = document.createElement('moni-text-field') as HTMLElement & {
|
|
322
|
+
required: boolean;
|
|
323
|
+
updateComplete: Promise<unknown>;
|
|
324
|
+
};
|
|
325
|
+
HTMLElement.prototype.attachInternals = originalAttach;
|
|
326
|
+
|
|
327
|
+
el.required = true;
|
|
328
|
+
document.body.appendChild(el);
|
|
329
|
+
await el.updateComplete;
|
|
330
|
+
|
|
331
|
+
expect(calls.some((c) => c.valueMissing === true)).toBe(true);
|
|
332
|
+
el.remove();
|
|
333
|
+
});
|
|
334
|
+
})
|
|
@@ -114,7 +114,7 @@ export class MoniTextField extends MoniElement {
|
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* El tipo de input HTML nativo.
|
|
117
|
-
* @type {'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search'}
|
|
117
|
+
* @type {'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local' | 'month' | 'week' | 'color'}
|
|
118
118
|
* @default 'text'
|
|
119
119
|
*/
|
|
120
120
|
@property({ reflect: true })
|
|
@@ -125,7 +125,13 @@ export class MoniTextField extends MoniElement {
|
|
|
125
125
|
| 'number'
|
|
126
126
|
| 'tel'
|
|
127
127
|
| 'url'
|
|
128
|
-
| 'search'
|
|
128
|
+
| 'search'
|
|
129
|
+
| 'date'
|
|
130
|
+
| 'time'
|
|
131
|
+
| 'datetime-local'
|
|
132
|
+
| 'month'
|
|
133
|
+
| 'week'
|
|
134
|
+
| 'color' = 'text';
|
|
129
135
|
|
|
130
136
|
/**
|
|
131
137
|
* Nombre del icono inicial (leading) (Material Symbols).
|
|
@@ -202,6 +208,50 @@ export class MoniTextField extends MoniElement {
|
|
|
202
208
|
*/
|
|
203
209
|
@property({ reflect: true }) placeholder = '';
|
|
204
210
|
|
|
211
|
+
/*
|
|
212
|
+
* Restricciones nativas.
|
|
213
|
+
*
|
|
214
|
+
* Antes ninguna de estas llegaba al `<input>` interno: el componente sólo
|
|
215
|
+
* reenviaba `type`, `placeholder`, `disabled`, `value` y `name`. Eso dejaba
|
|
216
|
+
* fuera el autocompletado de contraseñas, el teclado correcto en móvil, el
|
|
217
|
+
* límite de caracteres y toda la validación de restricciones, así que cada
|
|
218
|
+
* consumidor que necesitaba una de ellas tenía que renunciar al componente y
|
|
219
|
+
* usar un input nativo.
|
|
220
|
+
*
|
|
221
|
+
* Se reflejan al host además de reenviarse para que `[required]` y compañía
|
|
222
|
+
* puedan usarse como selectores CSS desde fuera.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/** Marca el campo como obligatorio y lo integra en la validación del formulario. */
|
|
226
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
227
|
+
|
|
228
|
+
/** Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`. */
|
|
229
|
+
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
230
|
+
|
|
231
|
+
/** Pista de autocompletado del navegador, por ejemplo `email` o `current-password`. */
|
|
232
|
+
@property({ reflect: true }) autocomplete = '';
|
|
233
|
+
|
|
234
|
+
/** Teclado virtual a mostrar en móvil, por ejemplo `numeric` o `tel`. */
|
|
235
|
+
@property({ reflect: true }) inputmode = '';
|
|
236
|
+
|
|
237
|
+
/** Máximo de caracteres aceptados. */
|
|
238
|
+
@property({ type: Number, reflect: true }) maxlength: number | null = null;
|
|
239
|
+
|
|
240
|
+
/** Mínimo de caracteres aceptados. */
|
|
241
|
+
@property({ type: Number, reflect: true }) minlength: number | null = null;
|
|
242
|
+
|
|
243
|
+
/** Valor mínimo para `number` y los tipos de fecha y hora. */
|
|
244
|
+
@property({ reflect: true }) min = '';
|
|
245
|
+
|
|
246
|
+
/** Valor máximo para `number` y los tipos de fecha y hora. */
|
|
247
|
+
@property({ reflect: true }) max = '';
|
|
248
|
+
|
|
249
|
+
/** Incremento para `number` y los tipos de fecha y hora. */
|
|
250
|
+
@property({ reflect: true }) step = '';
|
|
251
|
+
|
|
252
|
+
/** Expresión regular que debe cumplir el valor. */
|
|
253
|
+
@property({ reflect: true }) pattern = '';
|
|
254
|
+
|
|
205
255
|
/**
|
|
206
256
|
* Patrón Inputmask. Admite opcionales `[]`, grupos `()`, alternadores `|`,
|
|
207
257
|
* cuantificadores `{n,m}` y los tokens `9`, `a`, `*` y `K` (RUT).
|
|
@@ -334,11 +384,58 @@ export class MoniTextField extends MoniElement {
|
|
|
334
384
|
}
|
|
335
385
|
if (changed.has('disabled')) this._input.disabled = this.disabled;
|
|
336
386
|
}
|
|
387
|
+
this._syncValidity();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Copia la validez del `<input>` interno al host.
|
|
392
|
+
*
|
|
393
|
+
* Reenviar `required` o `pattern` no basta: el navegador valida el
|
|
394
|
+
* formulario mirando los controles asociados a él, y el que aquí participa
|
|
395
|
+
* es el host, no el input del Shadow DOM. Sin este puente el campo se
|
|
396
|
+
* pintaría como obligatorio pero el envío no se detendría.
|
|
397
|
+
*
|
|
398
|
+
* El tercer argumento ancla el globo de validación al input real, para que
|
|
399
|
+
* el navegador tenga dónde mostrarlo y a dónde mover el foco.
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* API de validación equivalente a la de un control nativo.
|
|
403
|
+
*
|
|
404
|
+
* Los elementos form-associated participan en la validación del formulario,
|
|
405
|
+
* pero no reciben estos miembros automáticamente: hay que exponerlos para que
|
|
406
|
+
* un `moni-text-field` se pueda interrogar igual que un `<input>`.
|
|
407
|
+
*/
|
|
408
|
+
get validity(): ValidityState {
|
|
409
|
+
return this._internals.validity;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
get validationMessage(): string {
|
|
413
|
+
return this._internals.validationMessage;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
checkValidity(): boolean {
|
|
417
|
+
return this._internals.checkValidity();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
reportValidity(): boolean {
|
|
421
|
+
return this._internals.reportValidity();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private _syncValidity() {
|
|
425
|
+
if (!this._input || !this._internals?.setValidity) return;
|
|
426
|
+
this._internals.setValidity(
|
|
427
|
+
this._input.validity,
|
|
428
|
+
this._input.validationMessage,
|
|
429
|
+
this._input
|
|
430
|
+
);
|
|
337
431
|
}
|
|
338
432
|
|
|
339
433
|
private _handleInput(e: Event) {
|
|
340
434
|
const target = e.target as HTMLInputElement;
|
|
341
435
|
this.value = target.value;
|
|
436
|
+
// Al teclear, `value` no siempre cambia de referencia (máscaras), así que
|
|
437
|
+
// la validez se refresca aquí y no sólo desde `updated()`.
|
|
438
|
+
this._syncValidity();
|
|
342
439
|
emitMoniEvent(this, 'moni-input', {
|
|
343
440
|
detail: { value: this.value, unmaskedValue: this.unmaskedValue, originalEvent: e }
|
|
344
441
|
});
|
|
@@ -499,6 +596,16 @@ export class MoniTextField extends MoniElement {
|
|
|
499
596
|
type=${this.type}
|
|
500
597
|
placeholder=${placeholder}
|
|
501
598
|
?disabled=${this.disabled}
|
|
599
|
+
?required=${this.required}
|
|
600
|
+
?readonly=${this.readonly}
|
|
601
|
+
autocomplete=${ifDefined(this.autocomplete || undefined)}
|
|
602
|
+
inputmode=${ifDefined(this.inputmode || undefined)}
|
|
603
|
+
maxlength=${ifDefined(this.maxlength ?? undefined)}
|
|
604
|
+
minlength=${ifDefined(this.minlength ?? undefined)}
|
|
605
|
+
min=${ifDefined(this.min || undefined)}
|
|
606
|
+
max=${ifDefined(this.max || undefined)}
|
|
607
|
+
step=${ifDefined(this.step || undefined)}
|
|
608
|
+
pattern=${ifDefined(this.pattern || undefined)}
|
|
502
609
|
.value=${this.value}
|
|
503
610
|
name=${ifDefined(this.name || undefined)}
|
|
504
611
|
class=${isActive ? 'active' : ''}
|
|
@@ -148,6 +148,24 @@ export class MoniTextarea extends MoniElement {
|
|
|
148
148
|
*/
|
|
149
149
|
@property({ type: Number, reflect: true }) maxlength: number | null = null;
|
|
150
150
|
|
|
151
|
+
/*
|
|
152
|
+
* Restricciones nativas. Antes sólo `maxlength` llegaba al `<textarea>`
|
|
153
|
+
* interno, así que un campo obligatorio o de sólo lectura obligaba a
|
|
154
|
+
* abandonar el componente y usar un control nativo.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/** Marca el campo como obligatorio y lo integra en la validación del formulario. */
|
|
158
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
159
|
+
|
|
160
|
+
/** Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`. */
|
|
161
|
+
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
162
|
+
|
|
163
|
+
/** Mínimo de caracteres aceptados. */
|
|
164
|
+
@property({ type: Number, reflect: true }) minlength: number | null = null;
|
|
165
|
+
|
|
166
|
+
/** Pista de autocompletado del navegador. */
|
|
167
|
+
@property({ reflect: true }) autocomplete = '';
|
|
168
|
+
|
|
151
169
|
/**
|
|
152
170
|
* Oculta la visualización del contador de caracteres cuando se establece `maxlength`.
|
|
153
171
|
* @type {boolean}
|
|
@@ -266,6 +284,46 @@ export class MoniTextarea extends MoniElement {
|
|
|
266
284
|
changed.has('maxRows')
|
|
267
285
|
) this._resizeTextarea();
|
|
268
286
|
}
|
|
287
|
+
this._syncValidity();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Copia la validez del `<textarea>` interno al host.
|
|
292
|
+
*
|
|
293
|
+
* El navegador valida mirando los controles asociados al formulario, y el
|
|
294
|
+
* que participa es el host, no el control del Shadow DOM: sin este puente
|
|
295
|
+
* `required` se pintaría pero no detendría el envío.
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* API de validación equivalente a la de un control nativo.
|
|
299
|
+
*
|
|
300
|
+
* Los elementos form-associated participan en la validación del formulario,
|
|
301
|
+
* pero no reciben estos miembros automáticamente: hay que exponerlos para que
|
|
302
|
+
* un `moni-textarea` se pueda interrogar igual que un `<input>`.
|
|
303
|
+
*/
|
|
304
|
+
get validity(): ValidityState {
|
|
305
|
+
return this._internals.validity;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get validationMessage(): string {
|
|
309
|
+
return this._internals.validationMessage;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
checkValidity(): boolean {
|
|
313
|
+
return this._internals.checkValidity();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reportValidity(): boolean {
|
|
317
|
+
return this._internals.reportValidity();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private _syncValidity() {
|
|
321
|
+
if (!this._input || !this._internals?.setValidity) return;
|
|
322
|
+
this._internals.setValidity(
|
|
323
|
+
this._input.validity,
|
|
324
|
+
this._input.validationMessage,
|
|
325
|
+
this._input
|
|
326
|
+
);
|
|
269
327
|
}
|
|
270
328
|
|
|
271
329
|
static override styles = [sharedStyles, fieldStyles, css`
|
|
@@ -361,8 +419,12 @@ export class MoniTextarea extends MoniElement {
|
|
|
361
419
|
part="input"
|
|
362
420
|
rows=${this.rows}
|
|
363
421
|
maxlength=${ifDefined(this.maxlength ?? undefined)}
|
|
422
|
+
minlength=${ifDefined(this.minlength ?? undefined)}
|
|
364
423
|
placeholder=${placeholder}
|
|
365
424
|
?disabled=${this.disabled}
|
|
425
|
+
?required=${this.required}
|
|
426
|
+
?readonly=${this.readonly}
|
|
427
|
+
autocomplete=${ifDefined(this.autocomplete || undefined)}
|
|
366
428
|
.value=${this.value}
|
|
367
429
|
name=${ifDefined(this.name || undefined)}
|
|
368
430
|
class=${isActive ? 'active' : ''}
|