@ionic/core 8.7.3 → 8.7.4-dev.11755800369.16c1f61b

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"`.
@@ -128,6 +132,19 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
128
132
  }
129
133
  this.didInputClearOnEdit = false;
130
134
  this.ionBlur.emit(ev);
135
+ /**
136
+ * Check validation state after blur to handle framework-managed classes.
137
+ * Frameworks like Angular update classes asynchronously, often using
138
+ * requestAnimationFrame or promises. Using setTimeout ensures we check
139
+ * after all microtasks and animation frames have completed.
140
+ */
141
+ setTimeout(() => {
142
+ const newIsInvalid = this.checkValidationState();
143
+ if (this.isInvalid !== newIsInvalid) {
144
+ this.isInvalid = newIsInvalid;
145
+ forceUpdate(this);
146
+ }
147
+ }, 100);
131
148
  };
132
149
  this.onFocus = (ev) => {
133
150
  this.hasFocus = true;
@@ -227,10 +244,33 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
227
244
  componentWillLoad() {
228
245
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
229
246
  }
247
+ /**
248
+ * Checks if the input is in an invalid state based on validation classes
249
+ */
250
+ checkValidationState() {
251
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
252
+ }
230
253
  connectedCallback() {
231
254
  const { el } = this;
232
255
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
233
256
  this.notchController = createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
257
+ // Watch for class changes to update validation state
258
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
259
+ this.validationObserver = new MutationObserver(() => {
260
+ const newIsInvalid = this.checkValidationState();
261
+ if (this.isInvalid !== newIsInvalid) {
262
+ this.isInvalid = newIsInvalid;
263
+ // Force a re-render to update aria-describedby immediately
264
+ forceUpdate(this);
265
+ }
266
+ });
267
+ this.validationObserver.observe(el, {
268
+ attributes: true,
269
+ attributeFilter: ['class'],
270
+ });
271
+ }
272
+ // Always set initial state
273
+ this.isInvalid = this.checkValidationState();
234
274
  this.debounceChanged();
235
275
  if (Build.isBrowser) {
236
276
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -267,6 +307,11 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
267
307
  this.notchController.destroy();
268
308
  this.notchController = undefined;
269
309
  }
310
+ // Clean up validation observer to prevent memory leaks
311
+ if (this.validationObserver) {
312
+ this.validationObserver.disconnect();
313
+ this.validationObserver = undefined;
314
+ }
270
315
  }
271
316
  /**
272
317
  * Sets focus on the native `input` in `ion-input`. Use this method instead of the global
@@ -375,8 +420,8 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
375
420
  ];
376
421
  }
377
422
  getHintTextID() {
378
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
379
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
423
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
424
+ if (isInvalid && errorText) {
380
425
  return errorTextId;
381
426
  }
382
427
  if (helperText) {
@@ -489,7 +534,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
489
534
  * TODO(FW-5592): Remove hasStartEndSlots condition
490
535
  */
491
536
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
492
- return (h(Host, { key: '41b2526627e7d2773a80f011b123284203a71ca0', class: createColorClasses(this.color, {
537
+ return (h(Host, { key: 'af93f93b33492571bd61d6b67414f16821132138', class: createColorClasses(this.color, {
493
538
  [mode]: true,
494
539
  'has-value': hasValue,
495
540
  'has-focus': hasFocus,
@@ -500,14 +545,14 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
500
545
  'in-item': inItem,
501
546
  'in-item-color': hostContext('ion-item.ion-color', this.el),
502
547
  'input-disabled': disabled,
503
- }) }, h("label", { key: '9ab078363e32528102b441ad1791d83f86fdcbdc', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'e34b594980ec62e4c618e827fadf7669a39ad0d8', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: '12dc04ead5502e9e5736240e918bf9331bf7b5d9', name: "start" }), h("input", Object.assign({ key: 'df356eb4ced23109b2c0242f36dc043aba8782d6', 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: 'f79f68cabcd4ea99419331174a377827db0c0741', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
548
+ }) }, h("label", { key: '3d52da6c568fc5d60833e759ba78981f95ad78d5', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '11adb0df91d332a5e1d5c86af88ffbe18ad185a3', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: 'dee0e60628bc5f849c10bae0fed4a4089769266b', name: "start" }), h("input", Object.assign({ key: 'c72e0da90c882a0233b2e27249d5b326eb89f820', 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.isInvalid ? 'true' : undefined }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (h("button", { key: '7aec0dff5eff8dccbcb735301a0ac02f9d58b4f9', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
504
549
  /**
505
550
  * This prevents mobile browsers from
506
551
  * blurring the input when the clear
507
552
  * button is activated.
508
553
  */
509
554
  ev.preventDefault();
510
- }, onClick: this.clearTextInput }, h("ion-icon", { key: '237ec07ec2e10f08818a332bb596578c2c49f770', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '1f0a3624aa3e8dc3c307a6762230ab698768a5e5', name: "end" })), shouldRenderHighlight && h("div", { key: '8a8cbb82695a722a0010b53dd0b1f1f97534a20b', class: "input-highlight" })), this.renderBottomContent()));
555
+ }, onClick: this.clearTextInput }, h("ion-icon", { key: '79e0789ae92590c4cea9923e4370710ba31a4c38', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '3d8d737c32ad1755954aadf14ce956f400873203', name: "end" })), shouldRenderHighlight && h("div", { key: '6d214bf9b8e4058116fd154224e93bbf33c6cb58', class: "input-highlight" })), this.renderBottomContent()));
511
556
  }
512
557
  get el() { return this; }
513
558
  static get watchers() { return {
@@ -556,6 +601,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
556
601
  "type": [1],
557
602
  "value": [1032],
558
603
  "hasFocus": [32],
604
+ "isInvalid": [32],
559
605
  "setFocus": [64],
560
606
  "getInputElement": [64]
561
607
  }, [[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"`.
@@ -129,6 +133,19 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
129
133
  }
130
134
  this.didTextareaClearOnEdit = false;
131
135
  this.ionBlur.emit(ev);
136
+ /**
137
+ * Check validation state after blur to handle framework-managed classes.
138
+ * Frameworks like Angular update classes asynchronously, often using
139
+ * requestAnimationFrame or promises. Using setTimeout ensures we check
140
+ * after all microtasks and animation frames have completed.
141
+ */
142
+ setTimeout(() => {
143
+ const newIsInvalid = this.checkValidationState();
144
+ if (this.isInvalid !== newIsInvalid) {
145
+ this.isInvalid = newIsInvalid;
146
+ forceUpdate(this);
147
+ }
148
+ }, 100);
132
149
  };
133
150
  this.onKeyDown = (ev) => {
134
151
  this.checkClearOnEdit(ev);
@@ -186,10 +203,33 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
186
203
  this.el.click();
187
204
  }
188
205
  }
206
+ /**
207
+ * Checks if the textarea is in an invalid state based on validation classes
208
+ */
209
+ checkValidationState() {
210
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
211
+ }
189
212
  connectedCallback() {
190
213
  const { el } = this;
191
214
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
192
215
  this.notchController = createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
216
+ // Watch for class changes to update validation state
217
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
218
+ this.validationObserver = new MutationObserver(() => {
219
+ const newIsInvalid = this.checkValidationState();
220
+ if (this.isInvalid !== newIsInvalid) {
221
+ this.isInvalid = newIsInvalid;
222
+ // Force a re-render to update aria-describedby immediately
223
+ forceUpdate(this);
224
+ }
225
+ });
226
+ this.validationObserver.observe(el, {
227
+ attributes: true,
228
+ attributeFilter: ['class'],
229
+ });
230
+ }
231
+ // Always set initial state
232
+ this.isInvalid = this.checkValidationState();
193
233
  this.debounceChanged();
194
234
  if (Build.isBrowser) {
195
235
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -211,6 +251,11 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
211
251
  this.notchController.destroy();
212
252
  this.notchController = undefined;
213
253
  }
254
+ // Clean up validation observer to prevent memory leaks
255
+ if (this.validationObserver) {
256
+ this.validationObserver.disconnect();
257
+ this.validationObserver = undefined;
258
+ }
214
259
  }
215
260
  componentWillLoad() {
216
261
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['data-form-type', 'title', 'tabindex', 'dir']));
@@ -388,8 +433,8 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
388
433
  ];
389
434
  }
390
435
  getHintTextID() {
391
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
392
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
436
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
437
+ if (isInvalid && errorText) {
393
438
  return errorTextId;
394
439
  }
395
440
  if (helperText) {
@@ -448,7 +493,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
448
493
  * TODO(FW-5592): Remove hasStartEndSlots condition
449
494
  */
450
495
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
451
- return (h(Host, { key: 'd9f2ede0107987fc42c99e310cd2336bad5a5755', class: createColorClasses(this.color, {
496
+ return (h(Host, { key: '075cd9dad6f4f026e421b63e4565e092b3ea0a2a', class: createColorClasses(this.color, {
452
497
  [mode]: true,
453
498
  'has-value': hasValue,
454
499
  'has-focus': hasFocus,
@@ -457,7 +502,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
457
502
  [`textarea-shape-${shape}`]: shape !== undefined,
458
503
  [`textarea-label-placement-${labelPlacement}`]: true,
459
504
  '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()));
505
+ }) }, h("label", { key: 'acb810df87a8156e5f431d65ddba287831acfa97', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '6ee9e8b9dfd562a0a23f3cc4c07c7bad4d168d56', class: "textarea-wrapper-inner" }, h("div", { key: '68d1b9205ad427a2c6de6767a7eb74901fb4d508', class: "start-slot-wrapper" }, h("slot", { key: 'a3b407c79a73cba5cafb6f987d8018573a8c5993', name: "start" })), h("div", { key: '6de732e25024cee7b1da4eb923b8fa1c3b967223', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: 'e90c595b4d0d8b1f3d2ef8cc1f9ac76ccdbc741d', 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.isInvalid ? 'true' : undefined }, this.inheritedAttributes), value)), h("div", { key: 'dcb70f9b4c3b3123ca05225c3396bb65762fb12c', class: "end-slot-wrapper" }, h("slot", { key: '2b8dd7c492b60424512a7f36ba75306697875da4', name: "end" }))), shouldRenderHighlight && h("div", { key: 'f76c2c046a5a2cdcd3fa9df7a9922f429fc6dd79', class: "textarea-highlight" })), this.renderBottomContent()));
461
506
  }
462
507
  get el() { return this; }
463
508
  static get watchers() { return {
@@ -499,6 +544,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
499
544
  "labelPlacement": [1, "label-placement"],
500
545
  "shape": [1],
501
546
  "hasFocus": [32],
547
+ "isInvalid": [32],
502
548
  "setFocus": [64],
503
549
  "getInputElement": [64]
504
550
  }, [[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"`.
@@ -129,6 +133,19 @@ const Input = class {
129
133
  }
130
134
  this.didInputClearOnEdit = false;
131
135
  this.ionBlur.emit(ev);
136
+ /**
137
+ * Check validation state after blur to handle framework-managed classes.
138
+ * Frameworks like Angular update classes asynchronously, often using
139
+ * requestAnimationFrame or promises. Using setTimeout ensures we check
140
+ * after all microtasks and animation frames have completed.
141
+ */
142
+ setTimeout(() => {
143
+ const newIsInvalid = this.checkValidationState();
144
+ if (this.isInvalid !== newIsInvalid) {
145
+ this.isInvalid = newIsInvalid;
146
+ index.forceUpdate(this);
147
+ }
148
+ }, 100);
132
149
  };
133
150
  this.onFocus = (ev) => {
134
151
  this.hasFocus = true;
@@ -228,10 +245,33 @@ const Input = class {
228
245
  componentWillLoad() {
229
246
  this.inheritedAttributes = Object.assign(Object.assign({}, helpers.inheritAriaAttributes(this.el)), helpers.inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
230
247
  }
248
+ /**
249
+ * Checks if the input is in an invalid state based on validation classes
250
+ */
251
+ checkValidationState() {
252
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
253
+ }
231
254
  connectedCallback() {
232
255
  const { el } = this;
233
256
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
234
257
  this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
258
+ // Watch for class changes to update validation state
259
+ if (typeof MutationObserver !== 'undefined') {
260
+ this.validationObserver = new MutationObserver(() => {
261
+ const newIsInvalid = this.checkValidationState();
262
+ if (this.isInvalid !== newIsInvalid) {
263
+ this.isInvalid = newIsInvalid;
264
+ // Force a re-render to update aria-describedby immediately
265
+ index.forceUpdate(this);
266
+ }
267
+ });
268
+ this.validationObserver.observe(el, {
269
+ attributes: true,
270
+ attributeFilter: ['class'],
271
+ });
272
+ }
273
+ // Always set initial state
274
+ this.isInvalid = this.checkValidationState();
235
275
  this.debounceChanged();
236
276
  {
237
277
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -268,6 +308,11 @@ const Input = class {
268
308
  this.notchController.destroy();
269
309
  this.notchController = undefined;
270
310
  }
311
+ // Clean up validation observer to prevent memory leaks
312
+ if (this.validationObserver) {
313
+ this.validationObserver.disconnect();
314
+ this.validationObserver = undefined;
315
+ }
271
316
  }
272
317
  /**
273
318
  * Sets focus on the native `input` in `ion-input`. Use this method instead of the global
@@ -376,8 +421,8 @@ const Input = class {
376
421
  ];
377
422
  }
378
423
  getHintTextID() {
379
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
380
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
424
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
425
+ if (isInvalid && errorText) {
381
426
  return errorTextId;
382
427
  }
383
428
  if (helperText) {
@@ -490,7 +535,7 @@ const Input = class {
490
535
  * TODO(FW-5592): Remove hasStartEndSlots condition
491
536
  */
492
537
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
493
- return (index.h(index.Host, { key: '41b2526627e7d2773a80f011b123284203a71ca0', class: theme.createColorClasses(this.color, {
538
+ return (index.h(index.Host, { key: 'af93f93b33492571bd61d6b67414f16821132138', class: theme.createColorClasses(this.color, {
494
539
  [mode]: true,
495
540
  'has-value': hasValue,
496
541
  'has-focus': hasFocus,
@@ -501,14 +546,14 @@ const Input = class {
501
546
  'in-item': inItem,
502
547
  'in-item-color': theme.hostContext('ion-item.ion-color', this.el),
503
548
  'input-disabled': disabled,
504
- }) }, index.h("label", { key: '9ab078363e32528102b441ad1791d83f86fdcbdc', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: 'e34b594980ec62e4c618e827fadf7669a39ad0d8', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: '12dc04ead5502e9e5736240e918bf9331bf7b5d9', name: "start" }), index.h("input", Object.assign({ key: 'df356eb4ced23109b2c0242f36dc043aba8782d6', 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: 'f79f68cabcd4ea99419331174a377827db0c0741', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
549
+ }) }, index.h("label", { key: '3d52da6c568fc5d60833e759ba78981f95ad78d5', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '11adb0df91d332a5e1d5c86af88ffbe18ad185a3', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: 'dee0e60628bc5f849c10bae0fed4a4089769266b', name: "start" }), index.h("input", Object.assign({ key: 'c72e0da90c882a0233b2e27249d5b326eb89f820', 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.isInvalid ? 'true' : undefined }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (index.h("button", { key: '7aec0dff5eff8dccbcb735301a0ac02f9d58b4f9', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
505
550
  /**
506
551
  * This prevents mobile browsers from
507
552
  * blurring the input when the clear
508
553
  * button is activated.
509
554
  */
510
555
  ev.preventDefault();
511
- }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '237ec07ec2e10f08818a332bb596578c2c49f770', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: '1f0a3624aa3e8dc3c307a6762230ab698768a5e5', name: "end" })), shouldRenderHighlight && index.h("div", { key: '8a8cbb82695a722a0010b53dd0b1f1f97534a20b', class: "input-highlight" })), this.renderBottomContent()));
556
+ }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '79e0789ae92590c4cea9923e4370710ba31a4c38', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: '3d8d737c32ad1755954aadf14ce956f400873203', name: "end" })), shouldRenderHighlight && index.h("div", { key: '6d214bf9b8e4058116fd154224e93bbf33c6cb58', class: "input-highlight" })), this.renderBottomContent()));
512
557
  }
513
558
  get el() { return index.getElement(this); }
514
559
  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"`.
@@ -131,6 +135,19 @@ const Textarea = class {
131
135
  }
132
136
  this.didTextareaClearOnEdit = false;
133
137
  this.ionBlur.emit(ev);
138
+ /**
139
+ * Check validation state after blur to handle framework-managed classes.
140
+ * Frameworks like Angular update classes asynchronously, often using
141
+ * requestAnimationFrame or promises. Using setTimeout ensures we check
142
+ * after all microtasks and animation frames have completed.
143
+ */
144
+ setTimeout(() => {
145
+ const newIsInvalid = this.checkValidationState();
146
+ if (this.isInvalid !== newIsInvalid) {
147
+ this.isInvalid = newIsInvalid;
148
+ index.forceUpdate(this);
149
+ }
150
+ }, 100);
134
151
  };
135
152
  this.onKeyDown = (ev) => {
136
153
  this.checkClearOnEdit(ev);
@@ -188,10 +205,33 @@ const Textarea = class {
188
205
  this.el.click();
189
206
  }
190
207
  }
208
+ /**
209
+ * Checks if the textarea is in an invalid state based on validation classes
210
+ */
211
+ checkValidationState() {
212
+ return this.el.classList.contains('ion-touched') && this.el.classList.contains('ion-invalid');
213
+ }
191
214
  connectedCallback() {
192
215
  const { el } = this;
193
216
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
194
217
  this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
218
+ // Watch for class changes to update validation state
219
+ if (typeof MutationObserver !== 'undefined') {
220
+ this.validationObserver = new MutationObserver(() => {
221
+ const newIsInvalid = this.checkValidationState();
222
+ if (this.isInvalid !== newIsInvalid) {
223
+ this.isInvalid = newIsInvalid;
224
+ // Force a re-render to update aria-describedby immediately
225
+ index.forceUpdate(this);
226
+ }
227
+ });
228
+ this.validationObserver.observe(el, {
229
+ attributes: true,
230
+ attributeFilter: ['class'],
231
+ });
232
+ }
233
+ // Always set initial state
234
+ this.isInvalid = this.checkValidationState();
195
235
  this.debounceChanged();
196
236
  {
197
237
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -213,6 +253,11 @@ const Textarea = class {
213
253
  this.notchController.destroy();
214
254
  this.notchController = undefined;
215
255
  }
256
+ // Clean up validation observer to prevent memory leaks
257
+ if (this.validationObserver) {
258
+ this.validationObserver.disconnect();
259
+ this.validationObserver = undefined;
260
+ }
216
261
  }
217
262
  componentWillLoad() {
218
263
  this.inheritedAttributes = Object.assign(Object.assign({}, helpers.inheritAriaAttributes(this.el)), helpers.inheritAttributes(this.el, ['data-form-type', 'title', 'tabindex', 'dir']));
@@ -390,8 +435,8 @@ const Textarea = class {
390
435
  ];
391
436
  }
392
437
  getHintTextID() {
393
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
394
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
438
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
439
+ if (isInvalid && errorText) {
395
440
  return errorTextId;
396
441
  }
397
442
  if (helperText) {
@@ -450,7 +495,7 @@ const Textarea = class {
450
495
  * TODO(FW-5592): Remove hasStartEndSlots condition
451
496
  */
452
497
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
453
- return (index.h(index.Host, { key: 'd9f2ede0107987fc42c99e310cd2336bad5a5755', class: theme.createColorClasses(this.color, {
498
+ return (index.h(index.Host, { key: '075cd9dad6f4f026e421b63e4565e092b3ea0a2a', class: theme.createColorClasses(this.color, {
454
499
  [mode]: true,
455
500
  'has-value': hasValue,
456
501
  'has-focus': hasFocus,
@@ -459,7 +504,7 @@ const Textarea = class {
459
504
  [`textarea-shape-${shape}`]: shape !== undefined,
460
505
  [`textarea-label-placement-${labelPlacement}`]: true,
461
506
  '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()));
507
+ }) }, index.h("label", { key: 'acb810df87a8156e5f431d65ddba287831acfa97', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '6ee9e8b9dfd562a0a23f3cc4c07c7bad4d168d56', class: "textarea-wrapper-inner" }, index.h("div", { key: '68d1b9205ad427a2c6de6767a7eb74901fb4d508', class: "start-slot-wrapper" }, index.h("slot", { key: 'a3b407c79a73cba5cafb6f987d8018573a8c5993', name: "start" })), index.h("div", { key: '6de732e25024cee7b1da4eb923b8fa1c3b967223', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, index.h("textarea", Object.assign({ key: 'e90c595b4d0d8b1f3d2ef8cc1f9ac76ccdbc741d', 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.isInvalid ? 'true' : undefined }, this.inheritedAttributes), value)), index.h("div", { key: 'dcb70f9b4c3b3123ca05225c3396bb65762fb12c', class: "end-slot-wrapper" }, index.h("slot", { key: '2b8dd7c492b60424512a7f36ba75306697875da4', name: "end" }))), shouldRenderHighlight && index.h("div", { key: 'f76c2c046a5a2cdcd3fa9df7a9922f429fc6dd79', class: "textarea-highlight" })), this.renderBottomContent()));
463
508
  }
464
509
  get el() { return index.getElement(this); }
465
510
  static get watchers() { return {