@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.
- package/components/ion-input.js +51 -5
- package/components/ion-textarea.js +50 -4
- package/dist/cjs/ion-input.cjs.entry.js +50 -5
- package/dist/cjs/ion-textarea.cjs.entry.js +49 -4
- package/dist/cjs/ionic.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/input/input.js +52 -6
- package/dist/collection/components/textarea/textarea.js +51 -5
- package/dist/docs.json +1 -1
- package/dist/esm/ion-input.entry.js +50 -5
- package/dist/esm/ion-textarea.entry.js +49 -4
- package/dist/esm/ionic.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-08493d32.entry.js +4 -0
- package/dist/ionic/p-a30b23cf.entry.js +4 -0
- package/dist/types/components/input/input.d.ts +9 -0
- package/dist/types/components/textarea/textarea.d.ts +9 -0
- package/hydrate/index.js +69 -9
- package/hydrate/index.mjs +69 -9
- package/package.json +1 -1
- package/dist/ionic/p-1488b7cc.entry.js +0 -4
- package/dist/ionic/p-c5210d3e.entry.js +0 -4
package/components/ion-input.js
CHANGED
|
@@ -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 {
|
|
379
|
-
if (
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 {
|
|
392
|
-
if (
|
|
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: '
|
|
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: '
|
|
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 {
|
|
380
|
-
if (
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 {
|
|
394
|
-
if (
|
|
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: '
|
|
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: '
|
|
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 {
|