@ionic/core 8.7.3-dev.11755190119.16c6a375 → 8.7.3-dev.11755195979.1ec83531

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.
@@ -43,6 +43,10 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
43
43
  * is applied in both cases.
44
44
  */
45
45
  this.hasFocus = false;
46
+ /**
47
+ * Track validation state for proper aria-live announcements
48
+ */
49
+ this.isInvalid = false;
46
50
  /**
47
51
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
48
52
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -227,10 +231,31 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
227
231
  componentWillLoad() {
228
232
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
229
233
  }
234
+ /**
235
+ * Checks if the input is in an invalid state based on validation classes
236
+ */
237
+ checkValidationState() {
238
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
239
+ }
230
240
  connectedCallback() {
231
241
  const { el } = this;
232
242
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
233
243
  this.notchController = createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
244
+ // Watch for class changes to update validation state
245
+ if (Build.isBrowser) {
246
+ this.validationObserver = new MutationObserver(() => {
247
+ const newIsInvalid = this.checkValidationState();
248
+ if (this.isInvalid !== newIsInvalid) {
249
+ this.isInvalid = newIsInvalid;
250
+ }
251
+ });
252
+ this.validationObserver.observe(el, {
253
+ attributes: true,
254
+ attributeFilter: ['class'],
255
+ });
256
+ // Set initial state
257
+ this.isInvalid = this.checkValidationState();
258
+ }
234
259
  this.debounceChanged();
235
260
  if (Build.isBrowser) {
236
261
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -267,6 +292,11 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
267
292
  this.notchController.destroy();
268
293
  this.notchController = undefined;
269
294
  }
295
+ // Clean up validation observer to prevent memory leaks
296
+ if (this.validationObserver) {
297
+ this.validationObserver.disconnect();
298
+ this.validationObserver = undefined;
299
+ }
270
300
  }
271
301
  /**
272
302
  * Sets focus on the native `input` in `ion-input`. Use this method instead of the global
@@ -368,15 +398,15 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
368
398
  * Renders the helper text or error text values
369
399
  */
370
400
  renderHintText() {
371
- const { helperText, errorText, helperTextId, errorTextId } = this;
401
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
372
402
  return [
373
403
  h("div", { id: helperTextId, class: "helper-text" }, helperText),
374
- h("div", { id: errorTextId, class: "error-text", "aria-live": "assertive", "aria-atomic": "true" }, errorText),
404
+ h("div", { id: errorTextId, class: "error-text", role: isInvalid && errorText ? 'alert' : undefined, "aria-live": isInvalid && errorText ? 'polite' : 'off', "aria-atomic": "true" }, isInvalid && errorText ? errorText : ''),
375
405
  ];
376
406
  }
377
407
  getHintTextID() {
378
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
379
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
408
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
409
+ if (isInvalid && errorText) {
380
410
  return errorTextId;
381
411
  }
382
412
  if (helperText) {
@@ -489,7 +519,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
489
519
  * TODO(FW-5592): Remove hasStartEndSlots condition
490
520
  */
491
521
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
492
- return (h(Host, { key: 'f00c1c1d3f57e98dd0c5a88c5bfef156a265e742', class: createColorClasses(this.color, {
522
+ return (h(Host, { key: 'fcb5d96c872ce63cb6252b6d7d302ab30e6ea83b', class: createColorClasses(this.color, {
493
523
  [mode]: true,
494
524
  'has-value': hasValue,
495
525
  'has-focus': hasFocus,
@@ -500,14 +530,14 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
500
530
  'in-item': inItem,
501
531
  'in-item-color': hostContext('ion-item.ion-color', this.el),
502
532
  'input-disabled': disabled,
503
- }) }, h("label", { key: '95de0c0153ddd94c39e15d779ea0e1b3a991a3d9', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '55d22ffcac1f8c07c5b86df1ae36ff39643143cd', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: '59341c66cace2c718219d1023bd9190dbd41b000', name: "start" }), h("input", Object.assign({ key: 'ab675664758bee4b2717c3914438c85b8b1ebb44', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (h("button", { key: 'fec4fa631b33151bc31ed9fc4f34970f72fd36f5', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
533
+ }) }, h("label", { key: '73bcaaf2527cbf295283b4581046fb9b361ce2dd', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '22dd6c6623a284e6a51910fa47b0a2ebdbd5971d', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: 'c02dac4336eb8e7570ed07cfa18fadca4bc6dd9d', name: "start" }), h("input", Object.assign({ key: '42fc6409e9ee51feed480a672bf074cfa6f74c8f', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (h("button", { key: 'b124a2b970542daa6fcb45ac5546a9fde22b45be', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
504
534
  /**
505
535
  * This prevents mobile browsers from
506
536
  * blurring the input when the clear
507
537
  * button is activated.
508
538
  */
509
539
  ev.preventDefault();
510
- }, onClick: this.clearTextInput }, h("ion-icon", { key: '47bc105137bfad30a5d3c05b11eda552bc643d9a', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '6d93afaf67d147dff45e17825d5d8c7a82d7be18', name: "end" })), shouldRenderHighlight && h("div", { key: '7ea437e065d66ba7d143e1c9ad72f4bcdb1e5f01', class: "input-highlight" })), this.renderBottomContent()));
540
+ }, onClick: this.clearTextInput }, h("ion-icon", { key: '243037c08825c2bf26c53bb9507f7cd73e56a4de', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '9faff25f968c089b7716a8afb405ba5afb86a1ac', name: "end" })), shouldRenderHighlight && h("div", { key: '54bf3da059a2bf30e4990ba34abb23fd3bf137b0', class: "input-highlight" })), this.renderBottomContent()));
511
541
  }
512
542
  get el() { return this; }
513
543
  static get watchers() { return {
@@ -556,6 +586,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
556
586
  "type": [1],
557
587
  "value": [1032],
558
588
  "hasFocus": [32],
589
+ "isInvalid": [32],
559
590
  "setFocus": [64],
560
591
  "getInputElement": [64]
561
592
  }, [[2, "click", "onClickCapture"]], {
@@ -40,6 +40,10 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
40
40
  * is applied in both cases.
41
41
  */
42
42
  this.hasFocus = false;
43
+ /**
44
+ * Track validation state for proper aria-live announcements
45
+ */
46
+ this.isInvalid = false;
43
47
  /**
44
48
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
45
49
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -186,10 +190,31 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
186
190
  this.el.click();
187
191
  }
188
192
  }
193
+ /**
194
+ * Checks if the textarea is in an invalid state based on validation classes
195
+ */
196
+ checkValidationState() {
197
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
198
+ }
189
199
  connectedCallback() {
190
200
  const { el } = this;
191
201
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
192
202
  this.notchController = createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
203
+ // Watch for class changes to update validation state
204
+ if (Build.isBrowser) {
205
+ this.validationObserver = new MutationObserver(() => {
206
+ const newIsInvalid = this.checkValidationState();
207
+ if (this.isInvalid !== newIsInvalid) {
208
+ this.isInvalid = newIsInvalid;
209
+ }
210
+ });
211
+ this.validationObserver.observe(el, {
212
+ attributes: true,
213
+ attributeFilter: ['class'],
214
+ });
215
+ // Set initial state
216
+ this.isInvalid = this.checkValidationState();
217
+ }
193
218
  this.debounceChanged();
194
219
  if (Build.isBrowser) {
195
220
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -211,6 +236,11 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
211
236
  this.notchController.destroy();
212
237
  this.notchController = undefined;
213
238
  }
239
+ // Clean up validation observer to prevent memory leaks
240
+ if (this.validationObserver) {
241
+ this.validationObserver.disconnect();
242
+ this.validationObserver = undefined;
243
+ }
214
244
  }
215
245
  componentWillLoad() {
216
246
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['data-form-type', 'title', 'tabindex', 'dir']));
@@ -381,15 +411,15 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
381
411
  * Renders the helper text or error text values
382
412
  */
383
413
  renderHintText() {
384
- const { helperText, errorText, helperTextId, errorTextId } = this;
414
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
385
415
  return [
386
416
  h("div", { id: helperTextId, class: "helper-text" }, helperText),
387
- h("div", { id: errorTextId, class: "error-text" }, errorText),
417
+ h("div", { id: errorTextId, class: "error-text", role: isInvalid && errorText ? 'alert' : undefined, "aria-live": isInvalid && errorText ? 'polite' : 'off', "aria-atomic": "true" }, isInvalid && errorText ? errorText : ''),
388
418
  ];
389
419
  }
390
420
  getHintTextID() {
391
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
392
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
421
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
422
+ if (isInvalid && errorText) {
393
423
  return errorTextId;
394
424
  }
395
425
  if (helperText) {
@@ -448,7 +478,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
448
478
  * TODO(FW-5592): Remove hasStartEndSlots condition
449
479
  */
450
480
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
451
- return (h(Host, { key: 'd9f2ede0107987fc42c99e310cd2336bad5a5755', class: createColorClasses(this.color, {
481
+ return (h(Host, { key: 'b67193cbdbd70844901e7f58346cb6021ea8ff9b', class: createColorClasses(this.color, {
452
482
  [mode]: true,
453
483
  'has-value': hasValue,
454
484
  'has-focus': hasFocus,
@@ -457,7 +487,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
457
487
  [`textarea-shape-${shape}`]: shape !== undefined,
458
488
  [`textarea-label-placement-${labelPlacement}`]: true,
459
489
  'textarea-disabled': disabled,
460
- }) }, h("label", { key: '9de598b95237462bb3bccffaefe83afbb43554b8', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'e33c426c6541d723ccc246bb404c03687726ff83', class: "textarea-wrapper-inner" }, h("div", { key: '521e11af9d54d281b0a2b1c25bcfc6f742c18296', class: "start-slot-wrapper" }, h("slot", { key: '515523f6ca3ce0e5dd08f3275c21a190fb1ca177', name: "start" })), h("div", { key: '916e01e00de8400ae00ef06bc1fb62d8be2eee08', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: '810271e6532d90e27dab1fcb26546113c1ce9cb0', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes), value)), h("div", { key: '80aca9ea9546dca9d38efd291a6b0be384bb6978', class: "end-slot-wrapper" }, h("slot", { key: '407fab16c66a9f4a542369bfecc0d9afa0065977', name: "end" }))), shouldRenderHighlight && h("div", { key: 'f00523a6698fac8a1996e04303487bef01d10f25', class: "textarea-highlight" })), this.renderBottomContent()));
490
+ }) }, h("label", { key: '11cb5c9eefd6c82f0d94a283f65e3bfea7cfd31f', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'f23c54b1d02b07ae54a729a1c871788b804031fe', class: "textarea-wrapper-inner" }, h("div", { key: 'd467679b2132c67307eacf29f0cb9cffe9fa3f70', class: "start-slot-wrapper" }, h("slot", { key: 'e75629aeb663218b24598803a400c0755bbc0958', name: "start" })), h("div", { key: 'dacfbeaee43984034a59375adcda329e5cf73de4', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: 'cee91118b3d2fa7f6b0660bab8384cd508e432b1', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes), value)), h("div", { key: '7802d173e3e338776ae2a095037191fdfe771dda', class: "end-slot-wrapper" }, h("slot", { key: '1b721a633799701fab2527b046c03ee23fa30c2e', name: "end" }))), shouldRenderHighlight && h("div", { key: 'ae2836d8d7ab9a1a6e70c3c17bbf99e30835c39d', class: "textarea-highlight" })), this.renderBottomContent()));
461
491
  }
462
492
  get el() { return this; }
463
493
  static get watchers() { return {
@@ -499,6 +529,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
499
529
  "labelPlacement": [1, "label-placement"],
500
530
  "shape": [1],
501
531
  "hasFocus": [32],
532
+ "isInvalid": [32],
502
533
  "setFocus": [64],
503
534
  "getInputElement": [64]
504
535
  }, [[2, "click", "onClickCapture"]], {
@@ -44,6 +44,10 @@ const Input = class {
44
44
  * is applied in both cases.
45
45
  */
46
46
  this.hasFocus = false;
47
+ /**
48
+ * Track validation state for proper aria-live announcements
49
+ */
50
+ this.isInvalid = false;
47
51
  /**
48
52
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
49
53
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -228,10 +232,31 @@ const Input = class {
228
232
  componentWillLoad() {
229
233
  this.inheritedAttributes = Object.assign(Object.assign({}, helpers.inheritAriaAttributes(this.el)), helpers.inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
230
234
  }
235
+ /**
236
+ * Checks if the input is in an invalid state based on validation classes
237
+ */
238
+ checkValidationState() {
239
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
240
+ }
231
241
  connectedCallback() {
232
242
  const { el } = this;
233
243
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
234
244
  this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
245
+ // Watch for class changes to update validation state
246
+ {
247
+ this.validationObserver = new MutationObserver(() => {
248
+ const newIsInvalid = this.checkValidationState();
249
+ if (this.isInvalid !== newIsInvalid) {
250
+ this.isInvalid = newIsInvalid;
251
+ }
252
+ });
253
+ this.validationObserver.observe(el, {
254
+ attributes: true,
255
+ attributeFilter: ['class'],
256
+ });
257
+ // Set initial state
258
+ this.isInvalid = this.checkValidationState();
259
+ }
235
260
  this.debounceChanged();
236
261
  {
237
262
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -268,6 +293,11 @@ const Input = class {
268
293
  this.notchController.destroy();
269
294
  this.notchController = undefined;
270
295
  }
296
+ // Clean up validation observer to prevent memory leaks
297
+ if (this.validationObserver) {
298
+ this.validationObserver.disconnect();
299
+ this.validationObserver = undefined;
300
+ }
271
301
  }
272
302
  /**
273
303
  * Sets focus on the native `input` in `ion-input`. Use this method instead of the global
@@ -369,15 +399,15 @@ const Input = class {
369
399
  * Renders the helper text or error text values
370
400
  */
371
401
  renderHintText() {
372
- const { helperText, errorText, helperTextId, errorTextId } = this;
402
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
373
403
  return [
374
404
  index.h("div", { id: helperTextId, class: "helper-text" }, helperText),
375
- index.h("div", { id: errorTextId, class: "error-text", "aria-live": "assertive", "aria-atomic": "true" }, errorText),
405
+ index.h("div", { id: errorTextId, class: "error-text", role: isInvalid && errorText ? 'alert' : undefined, "aria-live": isInvalid && errorText ? 'polite' : 'off', "aria-atomic": "true" }, isInvalid && errorText ? errorText : ''),
376
406
  ];
377
407
  }
378
408
  getHintTextID() {
379
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
380
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
409
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
410
+ if (isInvalid && errorText) {
381
411
  return errorTextId;
382
412
  }
383
413
  if (helperText) {
@@ -490,7 +520,7 @@ const Input = class {
490
520
  * TODO(FW-5592): Remove hasStartEndSlots condition
491
521
  */
492
522
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
493
- return (index.h(index.Host, { key: 'f00c1c1d3f57e98dd0c5a88c5bfef156a265e742', class: theme.createColorClasses(this.color, {
523
+ return (index.h(index.Host, { key: 'fcb5d96c872ce63cb6252b6d7d302ab30e6ea83b', class: theme.createColorClasses(this.color, {
494
524
  [mode]: true,
495
525
  'has-value': hasValue,
496
526
  'has-focus': hasFocus,
@@ -501,14 +531,14 @@ const Input = class {
501
531
  'in-item': inItem,
502
532
  'in-item-color': theme.hostContext('ion-item.ion-color', this.el),
503
533
  'input-disabled': disabled,
504
- }) }, index.h("label", { key: '95de0c0153ddd94c39e15d779ea0e1b3a991a3d9', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '55d22ffcac1f8c07c5b86df1ae36ff39643143cd', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: '59341c66cace2c718219d1023bd9190dbd41b000', name: "start" }), index.h("input", Object.assign({ key: 'ab675664758bee4b2717c3914438c85b8b1ebb44', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (index.h("button", { key: 'fec4fa631b33151bc31ed9fc4f34970f72fd36f5', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
534
+ }) }, index.h("label", { key: '73bcaaf2527cbf295283b4581046fb9b361ce2dd', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '22dd6c6623a284e6a51910fa47b0a2ebdbd5971d', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: 'c02dac4336eb8e7570ed07cfa18fadca4bc6dd9d', name: "start" }), index.h("input", Object.assign({ key: '42fc6409e9ee51feed480a672bf074cfa6f74c8f', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (index.h("button", { key: 'b124a2b970542daa6fcb45ac5546a9fde22b45be', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
505
535
  /**
506
536
  * This prevents mobile browsers from
507
537
  * blurring the input when the clear
508
538
  * button is activated.
509
539
  */
510
540
  ev.preventDefault();
511
- }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '47bc105137bfad30a5d3c05b11eda552bc643d9a', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: '6d93afaf67d147dff45e17825d5d8c7a82d7be18', name: "end" })), shouldRenderHighlight && index.h("div", { key: '7ea437e065d66ba7d143e1c9ad72f4bcdb1e5f01', class: "input-highlight" })), this.renderBottomContent()));
541
+ }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '243037c08825c2bf26c53bb9507f7cd73e56a4de', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: '9faff25f968c089b7716a8afb405ba5afb86a1ac', name: "end" })), shouldRenderHighlight && index.h("div", { key: '54bf3da059a2bf30e4990ba34abb23fd3bf137b0', class: "input-highlight" })), this.renderBottomContent()));
512
542
  }
513
543
  get el() { return index.getElement(this); }
514
544
  static get watchers() { return {
@@ -42,6 +42,10 @@ const Textarea = class {
42
42
  * is applied in both cases.
43
43
  */
44
44
  this.hasFocus = false;
45
+ /**
46
+ * Track validation state for proper aria-live announcements
47
+ */
48
+ this.isInvalid = false;
45
49
  /**
46
50
  * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
47
51
  * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
@@ -188,10 +192,31 @@ const Textarea = class {
188
192
  this.el.click();
189
193
  }
190
194
  }
195
+ /**
196
+ * Checks if the textarea is in an invalid state based on validation classes
197
+ */
198
+ checkValidationState() {
199
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
200
+ }
191
201
  connectedCallback() {
192
202
  const { el } = this;
193
203
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
194
204
  this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
205
+ // Watch for class changes to update validation state
206
+ {
207
+ this.validationObserver = new MutationObserver(() => {
208
+ const newIsInvalid = this.checkValidationState();
209
+ if (this.isInvalid !== newIsInvalid) {
210
+ this.isInvalid = newIsInvalid;
211
+ }
212
+ });
213
+ this.validationObserver.observe(el, {
214
+ attributes: true,
215
+ attributeFilter: ['class'],
216
+ });
217
+ // Set initial state
218
+ this.isInvalid = this.checkValidationState();
219
+ }
195
220
  this.debounceChanged();
196
221
  {
197
222
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -213,6 +238,11 @@ const Textarea = class {
213
238
  this.notchController.destroy();
214
239
  this.notchController = undefined;
215
240
  }
241
+ // Clean up validation observer to prevent memory leaks
242
+ if (this.validationObserver) {
243
+ this.validationObserver.disconnect();
244
+ this.validationObserver = undefined;
245
+ }
216
246
  }
217
247
  componentWillLoad() {
218
248
  this.inheritedAttributes = Object.assign(Object.assign({}, helpers.inheritAriaAttributes(this.el)), helpers.inheritAttributes(this.el, ['data-form-type', 'title', 'tabindex', 'dir']));
@@ -383,15 +413,15 @@ const Textarea = class {
383
413
  * Renders the helper text or error text values
384
414
  */
385
415
  renderHintText() {
386
- const { helperText, errorText, helperTextId, errorTextId } = this;
416
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
387
417
  return [
388
418
  index.h("div", { id: helperTextId, class: "helper-text" }, helperText),
389
- index.h("div", { id: errorTextId, class: "error-text" }, errorText),
419
+ index.h("div", { id: errorTextId, class: "error-text", role: isInvalid && errorText ? 'alert' : undefined, "aria-live": isInvalid && errorText ? 'polite' : 'off', "aria-atomic": "true" }, isInvalid && errorText ? errorText : ''),
390
420
  ];
391
421
  }
392
422
  getHintTextID() {
393
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
394
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
423
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
424
+ if (isInvalid && errorText) {
395
425
  return errorTextId;
396
426
  }
397
427
  if (helperText) {
@@ -450,7 +480,7 @@ const Textarea = class {
450
480
  * TODO(FW-5592): Remove hasStartEndSlots condition
451
481
  */
452
482
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
453
- return (index.h(index.Host, { key: 'd9f2ede0107987fc42c99e310cd2336bad5a5755', class: theme.createColorClasses(this.color, {
483
+ return (index.h(index.Host, { key: 'b67193cbdbd70844901e7f58346cb6021ea8ff9b', class: theme.createColorClasses(this.color, {
454
484
  [mode]: true,
455
485
  'has-value': hasValue,
456
486
  'has-focus': hasFocus,
@@ -459,7 +489,7 @@ const Textarea = class {
459
489
  [`textarea-shape-${shape}`]: shape !== undefined,
460
490
  [`textarea-label-placement-${labelPlacement}`]: true,
461
491
  'textarea-disabled': disabled,
462
- }) }, index.h("label", { key: '9de598b95237462bb3bccffaefe83afbb43554b8', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: 'e33c426c6541d723ccc246bb404c03687726ff83', class: "textarea-wrapper-inner" }, index.h("div", { key: '521e11af9d54d281b0a2b1c25bcfc6f742c18296', class: "start-slot-wrapper" }, index.h("slot", { key: '515523f6ca3ce0e5dd08f3275c21a190fb1ca177', name: "start" })), index.h("div", { key: '916e01e00de8400ae00ef06bc1fb62d8be2eee08', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, index.h("textarea", Object.assign({ key: '810271e6532d90e27dab1fcb26546113c1ce9cb0', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes), value)), index.h("div", { key: '80aca9ea9546dca9d38efd291a6b0be384bb6978', class: "end-slot-wrapper" }, index.h("slot", { key: '407fab16c66a9f4a542369bfecc0d9afa0065977', name: "end" }))), shouldRenderHighlight && index.h("div", { key: 'f00523a6698fac8a1996e04303487bef01d10f25', class: "textarea-highlight" })), this.renderBottomContent()));
492
+ }) }, index.h("label", { key: '11cb5c9eefd6c82f0d94a283f65e3bfea7cfd31f', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: 'f23c54b1d02b07ae54a729a1c871788b804031fe', class: "textarea-wrapper-inner" }, index.h("div", { key: 'd467679b2132c67307eacf29f0cb9cffe9fa3f70', class: "start-slot-wrapper" }, index.h("slot", { key: 'e75629aeb663218b24598803a400c0755bbc0958', name: "start" })), index.h("div", { key: 'dacfbeaee43984034a59375adcda329e5cf73de4', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, index.h("textarea", Object.assign({ key: 'cee91118b3d2fa7f6b0660bab8384cd508e432b1', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId }, this.inheritedAttributes), value)), index.h("div", { key: '7802d173e3e338776ae2a095037191fdfe771dda', class: "end-slot-wrapper" }, index.h("slot", { key: '1b721a633799701fab2527b046c03ee23fa30c2e', name: "end" }))), shouldRenderHighlight && index.h("div", { key: 'ae2836d8d7ab9a1a6e70c3c17bbf99e30835c39d', class: "textarea-highlight" })), this.renderBottomContent()));
463
493
  }
464
494
  get el() { return index.getElement(this); }
465
495
  static get watchers() { return {