@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.
Files changed (63) hide show
  1. package/custom-elements.json +181 -1
  2. package/dist/browser/moni-ui.iife.min.js +289 -274
  3. package/dist/browser/moni-ui.iife.min.js.br +0 -0
  4. package/dist/browser/moni-ui.iife.min.js.gz +0 -0
  5. package/dist/browser/moni-ui.iife.min.js.map +1 -1
  6. package/dist/browser/moni-ui.min.js +1462 -1372
  7. package/dist/browser/moni-ui.min.js.br +0 -0
  8. package/dist/browser/moni-ui.min.js.gz +0 -0
  9. package/dist/browser/moni-ui.min.js.map +1 -1
  10. package/dist/cdn/components/moni-color-field.js +1 -0
  11. package/dist/cdn/components/moni-select.js +3 -3
  12. package/dist/cdn/components/moni-text-field.js +12 -2
  13. package/dist/cdn/components/moni-textarea.js +6 -2
  14. package/dist/cdn/importmap.json +98 -98
  15. package/dist/components/moni-color-field.d.ts.map +1 -1
  16. package/dist/components/moni-color-field.js +1 -0
  17. package/dist/components/moni-select.d.ts +28 -0
  18. package/dist/components/moni-select.d.ts.map +1 -1
  19. package/dist/components/moni-select.js +48 -0
  20. package/dist/components/moni-text-field.d.ts +45 -2
  21. package/dist/components/moni-text-field.d.ts.map +1 -1
  22. package/dist/components/moni-text-field.js +113 -1
  23. package/dist/components/moni-textarea.d.ts +27 -0
  24. package/dist/components/moni-textarea.d.ts.map +1 -1
  25. package/dist/components/moni-textarea.js +61 -0
  26. package/docs/api.json +178 -5
  27. package/docs/assets/moni-context-menu/placement--top.png +0 -0
  28. package/docs/assets/moni-select/required--false.png +0 -0
  29. package/docs/assets/moni-select/required--true.png +0 -0
  30. package/docs/assets/moni-text-field/autocomplete--default.png +0 -0
  31. package/docs/assets/moni-text-field/inputmode--default.png +0 -0
  32. package/docs/assets/moni-text-field/max--default.png +0 -0
  33. package/docs/assets/moni-text-field/maxlength--default.png +0 -0
  34. package/docs/assets/moni-text-field/min--default.png +0 -0
  35. package/docs/assets/moni-text-field/minlength--default.png +0 -0
  36. package/docs/assets/moni-text-field/pattern--default.png +0 -0
  37. package/docs/assets/moni-text-field/readonly--false.png +0 -0
  38. package/docs/assets/moni-text-field/readonly--true.png +0 -0
  39. package/docs/assets/moni-text-field/required--false.png +0 -0
  40. package/docs/assets/moni-text-field/required--true.png +0 -0
  41. package/docs/assets/moni-text-field/step--default.png +0 -0
  42. package/docs/assets/moni-text-field/type--color.png +0 -0
  43. package/docs/assets/moni-text-field/type--date.png +0 -0
  44. package/docs/assets/moni-text-field/type--datetime-local.png +0 -0
  45. package/docs/assets/moni-text-field/type--month.png +0 -0
  46. package/docs/assets/moni-text-field/type--time.png +0 -0
  47. package/docs/assets/moni-text-field/type--week.png +0 -0
  48. package/docs/assets/moni-textarea/autocomplete--default.png +0 -0
  49. package/docs/assets/moni-textarea/minlength--default.png +0 -0
  50. package/docs/assets/moni-textarea/readonly--false.png +0 -0
  51. package/docs/assets/moni-textarea/readonly--true.png +0 -0
  52. package/docs/assets/moni-textarea/required--false.png +0 -0
  53. package/docs/assets/moni-textarea/required--true.png +0 -0
  54. package/docs/components/select.md +15 -1
  55. package/docs/components/text-field.md +89 -1
  56. package/docs/components/textarea.md +34 -1
  57. package/package.json +1 -1
  58. package/src/components/moni-color-field.ts +1 -0
  59. package/src/components/moni-select.test.ts +34 -0
  60. package/src/components/moni-select.ts +54 -0
  61. package/src/components/moni-text-field.test.ts +78 -0
  62. package/src/components/moni-text-field.ts +109 -2
  63. 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
  ![moni-text-field — type=search](../assets/moni-text-field/type--search.png)
218
230
 
231
+ ![moni-text-field — type=date](../assets/moni-text-field/type--date.png)
232
+
233
+ ![moni-text-field — type=time](../assets/moni-text-field/type--time.png)
234
+
235
+ ![moni-text-field — type=datetime-local](../assets/moni-text-field/type--datetime-local.png)
236
+
237
+ ![moni-text-field — type=month](../assets/moni-text-field/type--month.png)
238
+
239
+ ![moni-text-field — type=week](../assets/moni-text-field/type--week.png)
240
+
241
+ ![moni-text-field — type=color](../assets/moni-text-field/type--color.png)
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
  ![moni-text-field — placeholder=default](../assets/moni-text-field/placeholder--default.png)
303
327
 
328
+ ### `required`
329
+
330
+ Marca el campo como obligatorio y lo integra en la validación del formulario.
331
+
332
+ ![moni-text-field — required=false](../assets/moni-text-field/required--false.png)
333
+
334
+ ![moni-text-field — required=true](../assets/moni-text-field/required--true.png)
335
+
336
+ ### `readonly`
337
+
338
+ Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`.
339
+
340
+ ![moni-text-field — readonly=false](../assets/moni-text-field/readonly--false.png)
341
+
342
+ ![moni-text-field — readonly=true](../assets/moni-text-field/readonly--true.png)
343
+
344
+ ### `autocomplete`
345
+
346
+ Pista de autocompletado del navegador, por ejemplo `email` o `current-password`.
347
+
348
+ ![moni-text-field — autocomplete=default](../assets/moni-text-field/autocomplete--default.png)
349
+
350
+ ### `inputmode`
351
+
352
+ Teclado virtual a mostrar en móvil, por ejemplo `numeric` o `tel`.
353
+
354
+ ![moni-text-field — inputmode=default](../assets/moni-text-field/inputmode--default.png)
355
+
356
+ ### `maxlength`
357
+
358
+ Máximo de caracteres aceptados.
359
+
360
+ ![moni-text-field — maxlength=default](../assets/moni-text-field/maxlength--default.png)
361
+
362
+ ### `minlength`
363
+
364
+ Mínimo de caracteres aceptados.
365
+
366
+ ![moni-text-field — minlength=default](../assets/moni-text-field/minlength--default.png)
367
+
368
+ ### `min`
369
+
370
+ Valor mínimo para `number` y los tipos de fecha y hora.
371
+
372
+ ![moni-text-field — min=default](../assets/moni-text-field/min--default.png)
373
+
374
+ ### `max`
375
+
376
+ Valor máximo para `number` y los tipos de fecha y hora.
377
+
378
+ ![moni-text-field — max=default](../assets/moni-text-field/max--default.png)
379
+
380
+ ### `step`
381
+
382
+ Incremento para `number` y los tipos de fecha y hora.
383
+
384
+ ![moni-text-field — step=default](../assets/moni-text-field/step--default.png)
385
+
386
+ ### `pattern`
387
+
388
+ Expresión regular que debe cumplir el valor.
389
+
390
+ ![moni-text-field — pattern=default](../assets/moni-text-field/pattern--default.png)
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
- No expone métodos públicos adicionales.
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
  ![moni-textarea — maxlength=default](../assets/moni-textarea/maxlength--default.png)
234
239
 
240
+ ### `required`
241
+
242
+ Marca el campo como obligatorio y lo integra en la validación del formulario.
243
+
244
+ ![moni-textarea — required=false](../assets/moni-textarea/required--false.png)
245
+
246
+ ![moni-textarea — required=true](../assets/moni-textarea/required--true.png)
247
+
248
+ ### `readonly`
249
+
250
+ Impide editar el valor sin sacarlo del envío ni atenuarlo como `disabled`.
251
+
252
+ ![moni-textarea — readonly=false](../assets/moni-textarea/readonly--false.png)
253
+
254
+ ![moni-textarea — readonly=true](../assets/moni-textarea/readonly--true.png)
255
+
256
+ ### `minlength`
257
+
258
+ Mínimo de caracteres aceptados.
259
+
260
+ ![moni-textarea — minlength=default](../assets/moni-textarea/minlength--default.png)
261
+
262
+ ### `autocomplete`
263
+
264
+ Pista de autocompletado del navegador.
265
+
266
+ ![moni-textarea — autocomplete=default](../assets/moni-textarea/autocomplete--default.png)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moni-labs/moni-ui",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system unificado para Moni — Material Design 3 Expressive, mobile-first.",
@@ -266,6 +266,7 @@ export class MoniColorField extends MoniElement {
266
266
  .value=${this.value}
267
267
  ?disabled=${this.disabled}
268
268
  tabindex="-1"
269
+ aria-label=${ifDefined(this.label || undefined)}
269
270
  @input=${this._onColorInput}
270
271
  @change=${this._onColorChange}
271
272
  />
@@ -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' = 'text';
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' : ''}