@ionic/core 8.7.7-nightly.20251015 → 8.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/components/button.js +3 -7
  2. package/components/header.js +41 -3
  3. package/components/ion-input.js +6 -14
  4. package/components/ion-select.js +58 -10
  5. package/components/ion-textarea.js +5 -13
  6. package/components/{notch-controller.js → validity.js} +14 -1
  7. package/dist/cjs/ion-app_8.cjs.entry.js +41 -3
  8. package/dist/cjs/ion-button_2.cjs.entry.js +3 -7
  9. package/dist/cjs/ion-input.cjs.entry.js +7 -15
  10. package/dist/cjs/ion-select_3.cjs.entry.js +56 -10
  11. package/dist/cjs/ion-textarea.cjs.entry.js +6 -14
  12. package/dist/cjs/ionic.cjs.js +1 -1
  13. package/dist/cjs/loader.cjs.js +1 -1
  14. package/dist/cjs/{notch-controller-Bzqhjm4f.js → validity-C8QoAYT2.js} +14 -0
  15. package/dist/collection/components/button/button.js +3 -7
  16. package/dist/collection/components/header/header.js +5 -4
  17. package/dist/collection/components/header/header.utils.js +37 -0
  18. package/dist/collection/components/input/input.js +6 -14
  19. package/dist/collection/components/select/select.js +59 -11
  20. package/dist/collection/components/textarea/textarea.js +5 -13
  21. package/dist/collection/utils/forms/index.js +1 -0
  22. package/dist/collection/utils/forms/validity.js +15 -0
  23. package/dist/docs.json +1 -1
  24. package/dist/esm/ion-app_8.entry.js +41 -3
  25. package/dist/esm/ion-button_2.entry.js +3 -7
  26. package/dist/esm/ion-input.entry.js +6 -14
  27. package/dist/esm/ion-select_3.entry.js +55 -9
  28. package/dist/esm/ion-textarea.entry.js +5 -13
  29. package/dist/esm/ionic.js +1 -1
  30. package/dist/esm/loader.js +1 -1
  31. package/dist/esm/{notch-controller-BwelN_JM.js → validity-B8oWougr.js} +14 -1
  32. package/dist/ionic/ionic.esm.js +1 -1
  33. package/dist/ionic/p-43ed1ef5.entry.js +4 -0
  34. package/dist/ionic/p-4c85d268.entry.js +4 -0
  35. package/dist/ionic/p-4cc26913.entry.js +4 -0
  36. package/dist/ionic/p-8bdfc8f6.entry.js +4 -0
  37. package/dist/ionic/{p-DCv9sLH2.js → p-DieJyvMP.js} +1 -1
  38. package/dist/ionic/p-f65f9308.entry.js +4 -0
  39. package/dist/types/components/header/header.utils.d.ts +10 -0
  40. package/dist/types/components/input/input.d.ts +0 -4
  41. package/dist/types/components/select/select.d.ts +6 -0
  42. package/dist/types/components/textarea/textarea.d.ts +0 -4
  43. package/dist/types/utils/forms/index.d.ts +1 -0
  44. package/dist/types/utils/forms/validity.d.ts +10 -0
  45. package/hydrate/index.js +86 -41
  46. package/hydrate/index.mjs +86 -41
  47. package/package.json +2 -2
  48. package/dist/ionic/p-1c8a476d.entry.js +0 -4
  49. package/dist/ionic/p-7647da93.entry.js +0 -4
  50. package/dist/ionic/p-785026d7.entry.js +0 -4
  51. package/dist/ionic/p-78c74a3e.entry.js +0 -4
  52. package/dist/ionic/p-913a7c1e.entry.js +0 -4
@@ -219,11 +219,7 @@ const Button = /*@__PURE__*/ proxyCustomElement(class Button extends HTMLElement
219
219
  target,
220
220
  };
221
221
  let fill = this.fill;
222
- /**
223
- * We check both undefined and null to
224
- * work around https://github.com/ionic-team/stencil/issues/3586.
225
- */
226
- if (fill == null) {
222
+ if (fill === undefined) {
227
223
  fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
228
224
  }
229
225
  /**
@@ -236,7 +232,7 @@ const Button = /*@__PURE__*/ proxyCustomElement(class Button extends HTMLElement
236
232
  {
237
233
  type !== 'button' && this.renderHiddenButton();
238
234
  }
239
- return (h(Host, { key: 'b105ad09215adb3ca2298acdadf0dc9154bbb9b0', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
235
+ return (h(Host, { key: 'ed82ea53705523f9afc5f1a9addff44cc6424f27', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
240
236
  [mode]: true,
241
237
  [buttonType]: true,
242
238
  [`${buttonType}-${expand}`]: expand !== undefined,
@@ -251,7 +247,7 @@ const Button = /*@__PURE__*/ proxyCustomElement(class Button extends HTMLElement
251
247
  'button-disabled': disabled,
252
248
  'ion-activatable': true,
253
249
  'ion-focusable': true,
254
- }) }, h(TagType, Object.assign({ key: '66b4e7112bcb9e41d5a723fbbadb0a3104f9ee1d' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '1439fc3da280221028dcf7ce8ec9dab273c4d4bb', class: "button-inner" }, h("slot", { key: 'd5269ae1afc87ec7b99746032f59cbae93720a9f', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '461c83e97aa246aa86d83e14f1e15a288d35041e', name: "start" }), h("slot", { key: '807170d47101f9f6a333dd4ff489c89284f306fe' }), h("slot", { key: 'e67f116dd0349a0d27893e4f3ff0ccef1d402f80', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: '273f0bd9645a36c1bfd18a5c2ab4f81e22b7b989', type: this.rippleType }))));
250
+ }) }, h(TagType, Object.assign({ key: 'fadec13053469dd0405bbbc61b70ced568aa4826' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '6bf0e5144fb1148002e88038522402b789689d2c', class: "button-inner" }, h("slot", { key: '25da0ca155cfa9e2754842c34f4fd09f576ac2d2', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '51414065bb11953ec9d818f8d9353589bc9072c5', name: "start" }), h("slot", { key: 'c9b5f8842aeabd20628df2f4600f1257ea913d8d' }), h("slot", { key: '478dd3671c7be1909fc84e672f0fa8dfe6082263', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: 'e1d55f85a55144d743f58a5914cd116cb065fa8c', type: this.rippleType }))));
255
251
  }
256
252
  get el() { return this; }
257
253
  static get watchers() { return {
@@ -8,6 +8,8 @@ import { h as hostContext } from './theme.js';
8
8
  import { b as getIonMode } from './ionic-global.js';
9
9
 
10
10
  const TRANSITION = 'all 0.2s ease-in-out';
11
+ const ROLE_NONE = 'none';
12
+ const ROLE_BANNER = 'banner';
11
13
  const cloneElement = (tagName) => {
12
14
  const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
13
15
  if (getCachedEl !== null) {
@@ -134,6 +136,7 @@ const setHeaderActive = (headerIndex, active = true) => {
134
136
  const toolbars = headerIndex.toolbars;
135
137
  const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
136
138
  if (active) {
139
+ headerEl.setAttribute('role', ROLE_BANNER);
137
140
  headerEl.classList.remove('header-collapse-condense-inactive');
138
141
  ionTitles.forEach((ionTitle) => {
139
142
  if (ionTitle) {
@@ -142,6 +145,16 @@ const setHeaderActive = (headerIndex, active = true) => {
142
145
  });
143
146
  }
144
147
  else {
148
+ /**
149
+ * There can only be one banner landmark per page.
150
+ * By default, all ion-headers have the banner role.
151
+ * This causes an accessibility issue when using a
152
+ * condensed header since there are two ion-headers
153
+ * on the page at once (active and inactive).
154
+ * To solve this, the role needs to be toggled
155
+ * based on which header is active.
156
+ */
157
+ headerEl.setAttribute('role', ROLE_NONE);
145
158
  headerEl.classList.add('header-collapse-condense-inactive');
146
159
  /**
147
160
  * The small title should only be accessed by screen readers
@@ -201,6 +214,30 @@ const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
201
214
  });
202
215
  });
203
216
  };
217
+ /**
218
+ * Get the role type for the ion-header.
219
+ *
220
+ * @param isInsideMenu If ion-header is inside ion-menu.
221
+ * @param isCondensed If ion-header has collapse="condense".
222
+ * @param mode The current mode.
223
+ * @returns 'none' if inside ion-menu or if condensed in md
224
+ * mode, otherwise 'banner'.
225
+ */
226
+ const getRoleType = (isInsideMenu, isCondensed, mode) => {
227
+ // If the header is inside a menu, it should not have the banner role.
228
+ if (isInsideMenu) {
229
+ return ROLE_NONE;
230
+ }
231
+ /**
232
+ * Only apply role="none" to `md` mode condensed headers
233
+ * since the large header is never shown.
234
+ */
235
+ if (isCondensed && mode === 'md') {
236
+ return ROLE_NONE;
237
+ }
238
+ // Default to banner role.
239
+ return ROLE_BANNER;
240
+ };
204
241
 
205
242
  const headerIosCss = "ion-header{display:block;position:relative;-ms-flex-order:-1;order:-1;width:100%;z-index:10}ion-header ion-toolbar:first-of-type{padding-top:var(--ion-safe-area-top, 0)}.header-ios ion-toolbar:last-of-type{--border-width:0 0 0.55px}@supports ((-webkit-backdrop-filter: blur(0)) or (backdrop-filter: blur(0))){.header-background{left:0;right:0;top:0;bottom:0;position:absolute;-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px)}.header-translucent-ios ion-toolbar{--opacity:.8}.header-collapse-condense-inactive .header-background{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}}.header-ios.ion-no-border ion-toolbar:last-of-type{--border-width:0}.header-collapse-fade ion-toolbar{--opacity-scale:inherit}.header-collapse-fade.header-transitioning ion-toolbar{--background:transparent;--border-style:none}.header-collapse-condense{z-index:9}.header-collapse-condense ion-toolbar{position:-webkit-sticky;position:sticky;top:0}.header-collapse-condense ion-toolbar:first-of-type{padding-top:0px;z-index:1}.header-collapse-condense ion-toolbar{z-index:0}.header-collapse-condense ion-toolbar:last-of-type{--border-width:0px}.header-collapse-condense ion-toolbar ion-searchbar{padding-top:0px;padding-bottom:13px}.header-collapse-main{--opacity-scale:1}.header-collapse-main ion-toolbar{--opacity-scale:inherit}.header-collapse-main ion-toolbar.in-toolbar ion-title,.header-collapse-main ion-toolbar.in-toolbar ion-buttons{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.header-collapse-condense ion-toolbar,.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--background:var(--ion-background-color, #fff)}.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--border-style:none;--opacity-scale:1}.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse{opacity:0;pointer-events:none}.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-buttons.buttons-collapse{visibility:hidden}ion-header.header-ios:not(.header-collapse-main):has(~ion-content ion-header.header-ios[collapse=condense],~ion-content ion-header.header-ios.header-collapse-condense){opacity:0}";
206
243
 
@@ -345,16 +382,17 @@ const Header = /*@__PURE__*/ proxyCustomElement(class Header extends HTMLElement
345
382
  const { translucent, inheritedAttributes } = this;
346
383
  const mode = getIonMode(this);
347
384
  const collapse = this.collapse || 'none';
385
+ const isCondensed = collapse === 'condense';
348
386
  // banner role must be at top level, so remove role if inside a menu
349
- const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner';
350
- return (h(Host, Object.assign({ key: 'b6cc27f0b08afc9fcc889683525da765d80ba672', role: roleType, class: {
387
+ const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode);
388
+ return (h(Host, Object.assign({ key: '863c4568cd7b8c0ec55109f193bbbaed68a1346e', role: roleType, class: {
351
389
  [mode]: true,
352
390
  // Used internally for styling
353
391
  [`header-${mode}`]: true,
354
392
  [`header-translucent`]: this.translucent,
355
393
  [`header-collapse-${collapse}`]: true,
356
394
  [`header-translucent-${mode}`]: this.translucent,
357
- } }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '395766d4dcee3398bc91960db21f922095292f14', class: "header-background" }), h("slot", { key: '09a67ece27b258ff1248805d43d92a49b2c6859a' })));
395
+ } }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '25c3bdce328b0b35607d154c8b8374679313d881', class: "header-background" }), h("slot", { key: 'b44fab0a9be7920b9650da26117c783e751e1702' })));
358
396
  }
359
397
  get el() { return this; }
360
398
  static get style() { return {
@@ -2,7 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, h, Host } from '@stencil/core/internal/client';
5
- import { c as createNotchController } from './notch-controller.js';
5
+ import { c as createNotchController, a as checkInvalidState } from './validity.js';
6
6
  import { l as debounceEvent, i as inheritAriaAttributes, d as inheritAttributes, c as componentOnReady } from './helpers.js';
7
7
  import { c as createSlotMutationController, g as getCounterText } from './input.utils.js';
8
8
  import { h as hostContext, c as createColorClasses } from './theme.js';
@@ -233,14 +233,6 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
233
233
  componentWillLoad() {
234
234
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
235
235
  }
236
- /**
237
- * Checks if the input is in an invalid state based on Ionic validation classes
238
- */
239
- checkInvalidState() {
240
- const hasIonTouched = this.el.classList.contains('ion-touched');
241
- const hasIonInvalid = this.el.classList.contains('ion-invalid');
242
- return hasIonTouched && hasIonInvalid;
243
- }
244
236
  connectedCallback() {
245
237
  const { el } = this;
246
238
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -248,7 +240,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
248
240
  // Watch for class changes to update validation state
249
241
  if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
250
242
  this.validationObserver = new MutationObserver(() => {
251
- const newIsInvalid = this.checkInvalidState();
243
+ const newIsInvalid = checkInvalidState(el);
252
244
  if (this.isInvalid !== newIsInvalid) {
253
245
  this.isInvalid = newIsInvalid;
254
246
  // Force a re-render to update aria-describedby immediately
@@ -261,7 +253,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
261
253
  });
262
254
  }
263
255
  // Always set initial state
264
- this.isInvalid = this.checkInvalidState();
256
+ this.isInvalid = checkInvalidState(el);
265
257
  this.debounceChanged();
266
258
  if (Build.isBrowser) {
267
259
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -525,7 +517,7 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
525
517
  * TODO(FW-5592): Remove hasStartEndSlots condition
526
518
  */
527
519
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
528
- return (h(Host, { key: '8a51f0300d5bc66392f9ab9a6fa0b5d388072a33', class: createColorClasses(this.color, {
520
+ return (h(Host, { key: '97b5308021064d9e7434ef2d3d96f27045c1b0c4', class: createColorClasses(this.color, {
529
521
  [mode]: true,
530
522
  'has-value': hasValue,
531
523
  'has-focus': hasFocus,
@@ -536,14 +528,14 @@ const Input = /*@__PURE__*/ proxyCustomElement(class Input extends HTMLElement {
536
528
  'in-item': inItem,
537
529
  'in-item-color': hostContext('ion-item.ion-color', this.el),
538
530
  'input-disabled': disabled,
539
- }) }, h("label", { key: '9f8cf88d7d0e27931b51bd9c67f048c7fc6f5703', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '7ad30bf9777774062a6ccf9a3ba804f251eef1bb', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: '8af0b0325d101df8eed7d24f2767d6ca4d307319', name: "start" }), h("input", Object.assign({ key: '1c53f7f9fa2567f3df19681cf4e7c21be382eae6', 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: 'b081d0e1ec1444b4c9cca145fc9cd2ad4a68b3da', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
531
+ }) }, h("label", { key: '353f68726ce180299bd9adc81e5ff7d26a48f54f', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '2034b4bad04fc157f3298a1805819216b6f439d0', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: '96bb5e30176b2bd76dfb75bfbf6c1c3d4403f4bb', name: "start" }), h("input", Object.assign({ key: '1a1d75b0e414a95c89d5a760757c33548d234aca', 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: '95f3df17b7691d9a2e7dcd4a51f16a94aa3ca36f', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
540
532
  /**
541
533
  * This prevents mobile browsers from
542
534
  * blurring the input when the clear
543
535
  * button is activated.
544
536
  */
545
537
  ev.preventDefault();
546
- }, onClick: this.clearTextInput }, h("ion-icon", { key: '01535299241c3635460c05646420acf62a1ff567', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '480f3eb58b08ae792866a5b9b4c068748c5567cc', name: "end" })), shouldRenderHighlight && h("div", { key: 'a8609cacee88e4a09f1cca65b6a47cb79a56f35e', class: "input-highlight" })), this.renderBottomContent()));
538
+ }, onClick: this.clearTextInput }, h("ion-icon", { key: '16b0af75eed50c8115fb5597f73b5fbf71c2530e', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: 'c48da0f8ddb3764ac43efa705bb4a6bb2d9cc2fd', name: "end" })), shouldRenderHighlight && h("div", { key: 'f15238481fc20de56ca7ecb6e350b3c024cc755e', class: "input-highlight" })), this.renderBottomContent()));
547
539
  }
548
540
  get el() { return this; }
549
541
  static get watchers() { return {
@@ -1,8 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { proxyCustomElement, HTMLElement, createEvent, h, Host, forceUpdate } from '@stencil/core/internal/client';
5
- import { c as createNotchController } from './notch-controller.js';
4
+ import { proxyCustomElement, HTMLElement, createEvent, Build, h, Host, forceUpdate } from '@stencil/core/internal/client';
5
+ import { c as createNotchController, a as checkInvalidState } from './validity.js';
6
6
  import { i as isOptionSelected, d as defineCustomElement$8, c as compareOptions } from './radio.js';
7
7
  import { d as inheritAttributes, e as renderHiddenInput, h as focusVisibleElement } from './helpers.js';
8
8
  import { p as printIonWarning } from './index4.js';
@@ -65,6 +65,10 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
65
65
  * is applied in both cases.
66
66
  */
67
67
  this.hasFocus = false;
68
+ /**
69
+ * Track validation state for proper aria-live announcements.
70
+ */
71
+ this.isInvalid = false;
68
72
  /**
69
73
  * The text to display on the cancel button.
70
74
  */
@@ -194,9 +198,46 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
194
198
  */
195
199
  forceUpdate(this);
196
200
  });
201
+ // Watch for class changes to update validation state.
202
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
203
+ this.validationObserver = new MutationObserver(() => {
204
+ const newIsInvalid = checkInvalidState(this.el);
205
+ if (this.isInvalid !== newIsInvalid) {
206
+ this.isInvalid = newIsInvalid;
207
+ /**
208
+ * Screen readers tend to announce changes
209
+ * to `aria-describedby` when the attribute
210
+ * is changed during a blur event for a
211
+ * native form control.
212
+ * However, the announcement can be spotty
213
+ * when using a non-native form control
214
+ * and `forceUpdate()`.
215
+ * This is due to `forceUpdate()` internally
216
+ * rescheduling the DOM update to a lower
217
+ * priority queue regardless if it's called
218
+ * inside a Promise or not, thus causing
219
+ * the screen reader to potentially miss the
220
+ * change.
221
+ * By using a State variable inside a Promise,
222
+ * it guarantees a re-render immediately at
223
+ * a higher priority.
224
+ */
225
+ Promise.resolve().then(() => {
226
+ this.hintTextID = this.getHintTextID();
227
+ });
228
+ }
229
+ });
230
+ this.validationObserver.observe(el, {
231
+ attributes: true,
232
+ attributeFilter: ['class'],
233
+ });
234
+ }
235
+ // Always set initial state
236
+ this.isInvalid = checkInvalidState(this.el);
197
237
  }
198
238
  componentWillLoad() {
199
239
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
240
+ this.hintTextID = this.getHintTextID();
200
241
  }
201
242
  componentDidLoad() {
202
243
  /**
@@ -220,6 +261,11 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
220
261
  this.notchController.destroy();
221
262
  this.notchController = undefined;
222
263
  }
264
+ // Clean up validation observer to prevent memory leaks.
265
+ if (this.validationObserver) {
266
+ this.validationObserver.disconnect();
267
+ this.validationObserver = undefined;
268
+ }
223
269
  }
224
270
  /**
225
271
  * Open the select overlay. The overlay is either an alert, action sheet, or popover,
@@ -690,11 +736,11 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
690
736
  }
691
737
  renderListbox() {
692
738
  const { disabled, inputId, isExpanded, required } = this;
693
- return (h("button", { disabled: disabled, id: inputId, "aria-label": this.ariaLabel, "aria-haspopup": "dialog", "aria-expanded": `${isExpanded}`, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, "aria-required": `${required}`, onFocus: this.onFocus, onBlur: this.onBlur, ref: (focusEl) => (this.focusEl = focusEl) }));
739
+ return (h("button", { disabled: disabled, id: inputId, "aria-label": this.ariaLabel, "aria-haspopup": "dialog", "aria-expanded": `${isExpanded}`, "aria-describedby": this.hintTextID, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-required": `${required}`, onFocus: this.onFocus, onBlur: this.onBlur, ref: (focusEl) => (this.focusEl = focusEl) }));
694
740
  }
695
741
  getHintTextID() {
696
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
697
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
742
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
743
+ if (isInvalid && errorText) {
698
744
  return errorTextId;
699
745
  }
700
746
  if (helperText) {
@@ -706,10 +752,10 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
706
752
  * Renders the helper text or error text values
707
753
  */
708
754
  renderHintText() {
709
- const { helperText, errorText, helperTextId, errorTextId } = this;
755
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
710
756
  return [
711
- h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText),
712
- h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText),
757
+ h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null),
758
+ h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null),
713
759
  ];
714
760
  }
715
761
  /**
@@ -757,7 +803,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
757
803
  * TODO(FW-5592): Remove hasStartEndSlots condition
758
804
  */
759
805
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
760
- return (h(Host, { key: 'c03fb65e8fc9f9aab295e07b282377d57d910519', onClick: this.onClick, class: createColorClasses(this.color, {
806
+ return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
761
807
  [mode]: true,
762
808
  'in-item': inItem,
763
809
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -775,7 +821,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
775
821
  [`select-justify-${justify}`]: justifyEnabled,
776
822
  [`select-shape-${shape}`]: shape !== undefined,
777
823
  [`select-label-placement-${labelPlacement}`]: true,
778
- }) }, h("label", { key: '0d0c8ec55269adcac625f2899a547f4e7f3e3741', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'f6dfc93c0e23cbe75a2947abde67d842db2dad78', class: "select-wrapper-inner" }, h("slot", { key: '957bfadf9f101f519091419a362d3abdc2be66f6', name: "start" }), h("div", { key: 'ca349202a484e7f2e884533fd330f0b136754f7d', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: 'f0e62a6533ff1c8f62bd2d27f60b23385c4fa9ed', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: 'fb840d46bafafb09898ebeebbe8c181906a3d8a2', class: "select-highlight" })), this.renderBottomContent()));
824
+ }) }, h("label", { key: '6005b34a0c50bc4d7653a4276bc232ecd02e083c', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'c7e07aa81ae856c057f16275dd058f37c5670a47', class: "select-wrapper-inner" }, h("slot", { key: '7fc2deefe0424404caacdbbd9e08ed43ba55d28a', name: "start" }), h("div", { key: '157d74ee717b1bc30b5f1c233a09b0c8456aa68e', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: 'ea66db304528b82bf9317730b6dce3db2612f235', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '786eb1530b7476f0615d4e7c0bf4e7e4dc66509c', class: "select-highlight" })), this.renderBottomContent()));
779
825
  }
780
826
  get el() { return this; }
781
827
  static get watchers() { return {
@@ -813,6 +859,8 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
813
859
  "required": [4],
814
860
  "isExpanded": [32],
815
861
  "hasFocus": [32],
862
+ "isInvalid": [32],
863
+ "hintTextID": [32],
816
864
  "open": [64]
817
865
  }, undefined, {
818
866
  "disabled": ["styleChanged"],
@@ -2,7 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, writeTask, h, Host } from '@stencil/core/internal/client';
5
- import { c as createNotchController } from './notch-controller.js';
5
+ import { c as createNotchController, a as checkInvalidState } from './validity.js';
6
6
  import { l as debounceEvent, i as inheritAriaAttributes, d as inheritAttributes, c as componentOnReady } from './helpers.js';
7
7
  import { c as createSlotMutationController, g as getCounterText } from './input.utils.js';
8
8
  import { h as hostContext, c as createColorClasses } from './theme.js';
@@ -192,14 +192,6 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
192
192
  this.el.click();
193
193
  }
194
194
  }
195
- /**
196
- * Checks if the textarea is in an invalid state based on Ionic validation classes
197
- */
198
- checkValidationState() {
199
- const hasIonTouched = this.el.classList.contains('ion-touched');
200
- const hasIonInvalid = this.el.classList.contains('ion-invalid');
201
- return hasIonTouched && hasIonInvalid;
202
- }
203
195
  connectedCallback() {
204
196
  const { el } = this;
205
197
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -207,7 +199,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
207
199
  // Watch for class changes to update validation state
208
200
  if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
209
201
  this.validationObserver = new MutationObserver(() => {
210
- const newIsInvalid = this.checkValidationState();
202
+ const newIsInvalid = checkInvalidState(this.el);
211
203
  if (this.isInvalid !== newIsInvalid) {
212
204
  this.isInvalid = newIsInvalid;
213
205
  // Force a re-render to update aria-describedby immediately
@@ -220,7 +212,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
220
212
  });
221
213
  }
222
214
  // Always set initial state
223
- this.isInvalid = this.checkValidationState();
215
+ this.isInvalid = checkInvalidState(this.el);
224
216
  this.debounceChanged();
225
217
  if (Build.isBrowser) {
226
218
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -484,7 +476,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
484
476
  * TODO(FW-5592): Remove hasStartEndSlots condition
485
477
  */
486
478
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
487
- return (h(Host, { key: '26b46666a92b3f652775bb1c46661f9a30392104', class: createColorClasses(this.color, {
479
+ return (h(Host, { key: 'a70a62d7aae3831a50acd74f60b930925ada1326', class: createColorClasses(this.color, {
488
480
  [mode]: true,
489
481
  'has-value': hasValue,
490
482
  'has-focus': hasFocus,
@@ -493,7 +485,7 @@ const Textarea = /*@__PURE__*/ proxyCustomElement(class Textarea extends HTMLEle
493
485
  [`textarea-shape-${shape}`]: shape !== undefined,
494
486
  [`textarea-label-placement-${labelPlacement}`]: true,
495
487
  'textarea-disabled': disabled,
496
- }) }, h("label", { key: '2649da816216959ebe1f34cafd9dedbac20ec3c2', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'dca98593efece1b044dbcda045fa70882d715cb2', class: "textarea-wrapper-inner" }, h("div", { key: '2019daf87fddca5ec0b2e336f0376fd9642bae1b', class: "start-slot-wrapper" }, h("slot", { key: '36c423c394a71d08261705b9d6729e756bf65924', name: "start" })), h("div", { key: '0c3ea34105c7eddfa4094371c5d288c50ed10db3', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: 'ce173b83b16aff43d293fa1edef9b66c6676227b', 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: '756e343cfd208bb5ad9ecf08d77cbb0a9606dc7b', class: "end-slot-wrapper" }, h("slot", { key: '0eb596814a037fa4634ff8c5bac0045540edfe21', name: "end" }))), shouldRenderHighlight && h("div", { key: 'df62f896eb6e0e2d1217aa487c198eb82a52bcb8', class: "textarea-highlight" })), this.renderBottomContent()));
488
+ }) }, h("label", { key: '8a2dd59a60f7469df84018eb0ede3a9ec3862703', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '1bfc368236e3da7a225a45118c27fbfc1fe5fa46', class: "textarea-wrapper-inner" }, h("div", { key: '215cbb2635ff52e31a8973376989b85e7245d40f', class: "start-slot-wrapper" }, h("slot", { key: '9f6b461cdee9d629deb695d2bea054ece2f32305', name: "start" })), h("div", { key: 'c1af35a2d5bc452bebe0b22a26d15ff52b4e9fc8', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: '69a69b3cf0932baafbe37e6e846f1a571608d3f2', 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: 'c053ea8b865d0e29763aed2e4939cc9c9e374c15', class: "end-slot-wrapper" }, h("slot", { key: '930aa641833b0df54b9ea10368fc2f46d5f491f6', name: "end" }))), shouldRenderHighlight && h("div", { key: '8d12597d15f5f429d80e8272ea99e64ed924e482', class: "textarea-highlight" })), this.renderBottomContent()));
497
489
  }
498
490
  get el() { return this; }
499
491
  static get watchers() { return {
@@ -150,4 +150,17 @@ const createNotchController = (el, getNotchSpacerEl, getLabelSlot) => {
150
150
  };
151
151
  };
152
152
 
153
- export { createNotchController as c };
153
+ /**
154
+ * Checks if the form element is in an invalid state based on
155
+ * Ionic validation classes.
156
+ *
157
+ * @param el The form element to check.
158
+ * @returns `true` if the element is invalid, `false` otherwise.
159
+ */
160
+ const checkInvalidState = (el) => {
161
+ const hasIonTouched = el.classList.contains('ion-touched');
162
+ const hasIonInvalid = el.classList.contains('ion-invalid');
163
+ return hasIonTouched && hasIonInvalid;
164
+ };
165
+
166
+ export { checkInvalidState as a, createNotchController as c };
@@ -710,6 +710,8 @@ Footer.style = {
710
710
  };
711
711
 
712
712
  const TRANSITION = 'all 0.2s ease-in-out';
713
+ const ROLE_NONE = 'none';
714
+ const ROLE_BANNER = 'banner';
713
715
  const cloneElement = (tagName) => {
714
716
  const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
715
717
  if (getCachedEl !== null) {
@@ -836,6 +838,7 @@ const setHeaderActive = (headerIndex, active = true) => {
836
838
  const toolbars = headerIndex.toolbars;
837
839
  const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
838
840
  if (active) {
841
+ headerEl.setAttribute('role', ROLE_BANNER);
839
842
  headerEl.classList.remove('header-collapse-condense-inactive');
840
843
  ionTitles.forEach((ionTitle) => {
841
844
  if (ionTitle) {
@@ -844,6 +847,16 @@ const setHeaderActive = (headerIndex, active = true) => {
844
847
  });
845
848
  }
846
849
  else {
850
+ /**
851
+ * There can only be one banner landmark per page.
852
+ * By default, all ion-headers have the banner role.
853
+ * This causes an accessibility issue when using a
854
+ * condensed header since there are two ion-headers
855
+ * on the page at once (active and inactive).
856
+ * To solve this, the role needs to be toggled
857
+ * based on which header is active.
858
+ */
859
+ headerEl.setAttribute('role', ROLE_NONE);
847
860
  headerEl.classList.add('header-collapse-condense-inactive');
848
861
  /**
849
862
  * The small title should only be accessed by screen readers
@@ -903,6 +916,30 @@ const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
903
916
  });
904
917
  });
905
918
  };
919
+ /**
920
+ * Get the role type for the ion-header.
921
+ *
922
+ * @param isInsideMenu If ion-header is inside ion-menu.
923
+ * @param isCondensed If ion-header has collapse="condense".
924
+ * @param mode The current mode.
925
+ * @returns 'none' if inside ion-menu or if condensed in md
926
+ * mode, otherwise 'banner'.
927
+ */
928
+ const getRoleType = (isInsideMenu, isCondensed, mode) => {
929
+ // If the header is inside a menu, it should not have the banner role.
930
+ if (isInsideMenu) {
931
+ return ROLE_NONE;
932
+ }
933
+ /**
934
+ * Only apply role="none" to `md` mode condensed headers
935
+ * since the large header is never shown.
936
+ */
937
+ if (isCondensed && mode === 'md') {
938
+ return ROLE_NONE;
939
+ }
940
+ // Default to banner role.
941
+ return ROLE_BANNER;
942
+ };
906
943
 
907
944
  const headerIosCss = "ion-header{display:block;position:relative;-ms-flex-order:-1;order:-1;width:100%;z-index:10}ion-header ion-toolbar:first-of-type{padding-top:var(--ion-safe-area-top, 0)}.header-ios ion-toolbar:last-of-type{--border-width:0 0 0.55px}@supports ((-webkit-backdrop-filter: blur(0)) or (backdrop-filter: blur(0))){.header-background{left:0;right:0;top:0;bottom:0;position:absolute;-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px)}.header-translucent-ios ion-toolbar{--opacity:.8}.header-collapse-condense-inactive .header-background{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}}.header-ios.ion-no-border ion-toolbar:last-of-type{--border-width:0}.header-collapse-fade ion-toolbar{--opacity-scale:inherit}.header-collapse-fade.header-transitioning ion-toolbar{--background:transparent;--border-style:none}.header-collapse-condense{z-index:9}.header-collapse-condense ion-toolbar{position:-webkit-sticky;position:sticky;top:0}.header-collapse-condense ion-toolbar:first-of-type{padding-top:0px;z-index:1}.header-collapse-condense ion-toolbar{z-index:0}.header-collapse-condense ion-toolbar:last-of-type{--border-width:0px}.header-collapse-condense ion-toolbar ion-searchbar{padding-top:0px;padding-bottom:13px}.header-collapse-main{--opacity-scale:1}.header-collapse-main ion-toolbar{--opacity-scale:inherit}.header-collapse-main ion-toolbar.in-toolbar ion-title,.header-collapse-main ion-toolbar.in-toolbar ion-buttons{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.header-collapse-condense ion-toolbar,.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--background:var(--ion-background-color, #fff)}.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--border-style:none;--opacity-scale:1}.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse{opacity:0;pointer-events:none}.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-buttons.buttons-collapse{visibility:hidden}ion-header.header-ios:not(.header-collapse-main):has(~ion-content ion-header.header-ios[collapse=condense],~ion-content ion-header.header-ios.header-collapse-condense){opacity:0}";
908
945
 
@@ -1044,16 +1081,17 @@ const Header = class {
1044
1081
  const { translucent, inheritedAttributes } = this;
1045
1082
  const mode = ionicGlobal.getIonMode(this);
1046
1083
  const collapse = this.collapse || 'none';
1084
+ const isCondensed = collapse === 'condense';
1047
1085
  // banner role must be at top level, so remove role if inside a menu
1048
- const roleType = theme.hostContext('ion-menu', this.el) ? 'none' : 'banner';
1049
- return (index.h(index.Host, Object.assign({ key: 'b6cc27f0b08afc9fcc889683525da765d80ba672', role: roleType, class: {
1086
+ const roleType = getRoleType(theme.hostContext('ion-menu', this.el), isCondensed, mode);
1087
+ return (index.h(index.Host, Object.assign({ key: '863c4568cd7b8c0ec55109f193bbbaed68a1346e', role: roleType, class: {
1050
1088
  [mode]: true,
1051
1089
  // Used internally for styling
1052
1090
  [`header-${mode}`]: true,
1053
1091
  [`header-translucent`]: this.translucent,
1054
1092
  [`header-collapse-${collapse}`]: true,
1055
1093
  [`header-translucent-${mode}`]: this.translucent,
1056
- } }, inheritedAttributes), mode === 'ios' && translucent && index.h("div", { key: '395766d4dcee3398bc91960db21f922095292f14', class: "header-background" }), index.h("slot", { key: '09a67ece27b258ff1248805d43d92a49b2c6859a' })));
1094
+ } }, inheritedAttributes), mode === 'ios' && translucent && index.h("div", { key: '25c3bdce328b0b35607d154c8b8374679313d881', class: "header-background" }), index.h("slot", { key: 'b44fab0a9be7920b9650da26117c783e751e1702' })));
1057
1095
  }
1058
1096
  get el() { return index.getElement(this); }
1059
1097
  };
@@ -215,11 +215,7 @@ const Button = class {
215
215
  target,
216
216
  };
217
217
  let fill = this.fill;
218
- /**
219
- * We check both undefined and null to
220
- * work around https://github.com/ionic-team/stencil/issues/3586.
221
- */
222
- if (fill == null) {
218
+ if (fill === undefined) {
223
219
  fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
224
220
  }
225
221
  /**
@@ -232,7 +228,7 @@ const Button = class {
232
228
  {
233
229
  type !== 'button' && this.renderHiddenButton();
234
230
  }
235
- return (index.h(index.Host, { key: 'b105ad09215adb3ca2298acdadf0dc9154bbb9b0', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: theme.createColorClasses(color, {
231
+ return (index.h(index.Host, { key: 'ed82ea53705523f9afc5f1a9addff44cc6424f27', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: theme.createColorClasses(color, {
236
232
  [mode]: true,
237
233
  [buttonType]: true,
238
234
  [`${buttonType}-${expand}`]: expand !== undefined,
@@ -247,7 +243,7 @@ const Button = class {
247
243
  'button-disabled': disabled,
248
244
  'ion-activatable': true,
249
245
  'ion-focusable': true,
250
- }) }, index.h(TagType, Object.assign({ key: '66b4e7112bcb9e41d5a723fbbadb0a3104f9ee1d' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), index.h("span", { key: '1439fc3da280221028dcf7ce8ec9dab273c4d4bb', class: "button-inner" }, index.h("slot", { key: 'd5269ae1afc87ec7b99746032f59cbae93720a9f', name: "icon-only", onSlotchange: this.slotChanged }), index.h("slot", { key: '461c83e97aa246aa86d83e14f1e15a288d35041e', name: "start" }), index.h("slot", { key: '807170d47101f9f6a333dd4ff489c89284f306fe' }), index.h("slot", { key: 'e67f116dd0349a0d27893e4f3ff0ccef1d402f80', name: "end" })), mode === 'md' && index.h("ion-ripple-effect", { key: '273f0bd9645a36c1bfd18a5c2ab4f81e22b7b989', type: this.rippleType }))));
246
+ }) }, index.h(TagType, Object.assign({ key: 'fadec13053469dd0405bbbc61b70ced568aa4826' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), index.h("span", { key: '6bf0e5144fb1148002e88038522402b789689d2c', class: "button-inner" }, index.h("slot", { key: '25da0ca155cfa9e2754842c34f4fd09f576ac2d2', name: "icon-only", onSlotchange: this.slotChanged }), index.h("slot", { key: '51414065bb11953ec9d818f8d9353589bc9072c5', name: "start" }), index.h("slot", { key: 'c9b5f8842aeabd20628df2f4600f1257ea913d8d' }), index.h("slot", { key: '478dd3671c7be1909fc84e672f0fa8dfe6082263', name: "end" })), mode === 'md' && index.h("ion-ripple-effect", { key: 'e1d55f85a55144d743f58a5914cd116cb065fa8c', type: this.rippleType }))));
251
247
  }
252
248
  get el() { return index.getElement(this); }
253
249
  static get watchers() { return {
@@ -4,7 +4,7 @@
4
4
  'use strict';
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
- var notchController = require('./notch-controller-Bzqhjm4f.js');
7
+ var validity = require('./validity-C8QoAYT2.js');
8
8
  var helpers = require('./helpers-DrTqNghc.js');
9
9
  var input_utils = require('./input.utils-B_QROI2g.js');
10
10
  var theme = require('./theme-CeDs6Hcv.js');
@@ -232,22 +232,14 @@ const Input = class {
232
232
  componentWillLoad() {
233
233
  this.inheritedAttributes = Object.assign(Object.assign({}, helpers.inheritAriaAttributes(this.el)), helpers.inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
234
234
  }
235
- /**
236
- * Checks if the input is in an invalid state based on Ionic validation classes
237
- */
238
- checkInvalidState() {
239
- const hasIonTouched = this.el.classList.contains('ion-touched');
240
- const hasIonInvalid = this.el.classList.contains('ion-invalid');
241
- return hasIonTouched && hasIonInvalid;
242
- }
243
235
  connectedCallback() {
244
236
  const { el } = this;
245
237
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
246
- this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
238
+ this.notchController = validity.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
247
239
  // Watch for class changes to update validation state
248
240
  if (typeof MutationObserver !== 'undefined') {
249
241
  this.validationObserver = new MutationObserver(() => {
250
- const newIsInvalid = this.checkInvalidState();
242
+ const newIsInvalid = validity.checkInvalidState(el);
251
243
  if (this.isInvalid !== newIsInvalid) {
252
244
  this.isInvalid = newIsInvalid;
253
245
  // Force a re-render to update aria-describedby immediately
@@ -260,7 +252,7 @@ const Input = class {
260
252
  });
261
253
  }
262
254
  // Always set initial state
263
- this.isInvalid = this.checkInvalidState();
255
+ this.isInvalid = validity.checkInvalidState(el);
264
256
  this.debounceChanged();
265
257
  {
266
258
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -524,7 +516,7 @@ const Input = class {
524
516
  * TODO(FW-5592): Remove hasStartEndSlots condition
525
517
  */
526
518
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
527
- return (index.h(index.Host, { key: '8a51f0300d5bc66392f9ab9a6fa0b5d388072a33', class: theme.createColorClasses(this.color, {
519
+ return (index.h(index.Host, { key: '97b5308021064d9e7434ef2d3d96f27045c1b0c4', class: theme.createColorClasses(this.color, {
528
520
  [mode]: true,
529
521
  'has-value': hasValue,
530
522
  'has-focus': hasFocus,
@@ -535,14 +527,14 @@ const Input = class {
535
527
  'in-item': inItem,
536
528
  'in-item-color': theme.hostContext('ion-item.ion-color', this.el),
537
529
  'input-disabled': disabled,
538
- }) }, index.h("label", { key: '9f8cf88d7d0e27931b51bd9c67f048c7fc6f5703', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '7ad30bf9777774062a6ccf9a3ba804f251eef1bb', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: '8af0b0325d101df8eed7d24f2767d6ca4d307319', name: "start" }), index.h("input", Object.assign({ key: '1c53f7f9fa2567f3df19681cf4e7c21be382eae6', 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: 'b081d0e1ec1444b4c9cca145fc9cd2ad4a68b3da', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
530
+ }) }, index.h("label", { key: '353f68726ce180299bd9adc81e5ff7d26a48f54f', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: '2034b4bad04fc157f3298a1805819216b6f439d0', class: "native-wrapper", onClick: this.onLabelClick }, index.h("slot", { key: '96bb5e30176b2bd76dfb75bfbf6c1c3d4403f4bb', name: "start" }), index.h("input", Object.assign({ key: '1a1d75b0e414a95c89d5a760757c33548d234aca', 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: '95f3df17b7691d9a2e7dcd4a51f16a94aa3ca36f', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
539
531
  /**
540
532
  * This prevents mobile browsers from
541
533
  * blurring the input when the clear
542
534
  * button is activated.
543
535
  */
544
536
  ev.preventDefault();
545
- }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '01535299241c3635460c05646420acf62a1ff567', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: '480f3eb58b08ae792866a5b9b4c068748c5567cc', name: "end" })), shouldRenderHighlight && index.h("div", { key: 'a8609cacee88e4a09f1cca65b6a47cb79a56f35e', class: "input-highlight" })), this.renderBottomContent()));
537
+ }, onClick: this.clearTextInput }, index.h("ion-icon", { key: '16b0af75eed50c8115fb5597f73b5fbf71c2530e', "aria-hidden": "true", icon: clearIconData }))), index.h("slot", { key: 'c48da0f8ddb3764ac43efa705bb4a6bb2d9cc2fd', name: "end" })), shouldRenderHighlight && index.h("div", { key: 'f15238481fc20de56ca7ecb6e350b3c024cc755e', class: "input-highlight" })), this.renderBottomContent()));
546
538
  }
547
539
  get el() { return index.getElement(this); }
548
540
  static get watchers() { return {