@ionic/core 8.7.7-nightly.20251014 → 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 (78) hide show
  1. package/components/button.js +3 -7
  2. package/components/header.js +42 -4
  3. package/components/index2.js +74 -3
  4. package/components/ion-input.js +6 -14
  5. package/components/ion-select.js +58 -10
  6. package/components/ion-textarea.js +5 -13
  7. package/components/{notch-controller.js → validity.js} +14 -1
  8. package/dist/cjs/{index-CD5Rjp23.js → index-094mMFB-.js} +76 -5
  9. package/dist/cjs/index.cjs.js +3 -3
  10. package/dist/cjs/ion-app_8.cjs.entry.js +43 -5
  11. package/dist/cjs/ion-button_2.cjs.entry.js +3 -7
  12. package/dist/cjs/ion-input.cjs.entry.js +7 -15
  13. package/dist/cjs/ion-modal.cjs.entry.js +1 -1
  14. package/dist/cjs/ion-nav_2.cjs.entry.js +1 -1
  15. package/dist/cjs/ion-popover.cjs.entry.js +1 -1
  16. package/dist/cjs/ion-select_3.cjs.entry.js +56 -10
  17. package/dist/cjs/ion-textarea.cjs.entry.js +6 -14
  18. package/dist/cjs/ionic.cjs.js +1 -1
  19. package/dist/cjs/{ios.transition-j9CclgEW.js → ios.transition-BOt_uW73.js} +1 -1
  20. package/dist/cjs/loader.cjs.js +1 -1
  21. package/dist/cjs/{md.transition-CwFyRSfv.js → md.transition-Dt968VXB.js} +1 -1
  22. package/dist/cjs/{notch-controller-Bzqhjm4f.js → validity-C8QoAYT2.js} +14 -0
  23. package/dist/collection/components/button/button.js +3 -7
  24. package/dist/collection/components/header/header.ios.css +27 -1
  25. package/dist/collection/components/header/header.js +5 -4
  26. package/dist/collection/components/header/header.utils.js +37 -0
  27. package/dist/collection/components/input/input.js +6 -14
  28. package/dist/collection/components/select/select.js +59 -11
  29. package/dist/collection/components/textarea/textarea.js +5 -13
  30. package/dist/collection/utils/forms/index.js +1 -0
  31. package/dist/collection/utils/forms/validity.js +15 -0
  32. package/dist/collection/utils/transition/index.js +74 -3
  33. package/dist/docs.json +1 -1
  34. package/dist/esm/{index-D6G2seR8.js → index-r2D9DEro.js} +76 -5
  35. package/dist/esm/index.js +3 -3
  36. package/dist/esm/ion-app_8.entry.js +43 -5
  37. package/dist/esm/ion-button_2.entry.js +3 -7
  38. package/dist/esm/ion-input.entry.js +6 -14
  39. package/dist/esm/ion-modal.entry.js +1 -1
  40. package/dist/esm/ion-nav_2.entry.js +1 -1
  41. package/dist/esm/ion-popover.entry.js +1 -1
  42. package/dist/esm/ion-select_3.entry.js +55 -9
  43. package/dist/esm/ion-textarea.entry.js +5 -13
  44. package/dist/esm/ionic.js +1 -1
  45. package/dist/esm/{ios.transition-Bpq9ixwv.js → ios.transition-BDzw0_Hm.js} +1 -1
  46. package/dist/esm/loader.js +1 -1
  47. package/dist/esm/{md.transition-zOA0oanq.js → md.transition-BzDYi3qq.js} +1 -1
  48. package/dist/esm/{notch-controller-BwelN_JM.js → validity-B8oWougr.js} +14 -1
  49. package/dist/ionic/index.esm.js +1 -1
  50. package/dist/ionic/ionic.esm.js +1 -1
  51. package/dist/ionic/p-43ed1ef5.entry.js +4 -0
  52. package/dist/ionic/p-4c85d268.entry.js +4 -0
  53. package/dist/ionic/p-4cc26913.entry.js +4 -0
  54. package/dist/ionic/p-8bdfc8f6.entry.js +4 -0
  55. package/dist/ionic/{p-DPhQmGJN.js → p-C7hRNDhM.js} +1 -1
  56. package/dist/ionic/p-DUt5fQmA.js +4 -0
  57. package/dist/ionic/{p-9R1XyICs.js → p-DZRJwG4S.js} +1 -1
  58. package/dist/ionic/{p-DCv9sLH2.js → p-DieJyvMP.js} +1 -1
  59. package/dist/ionic/{p-c59314fd.entry.js → p-a80f1b04.entry.js} +1 -1
  60. package/dist/ionic/{p-c85c40ee.entry.js → p-dbbe606a.entry.js} +1 -1
  61. package/dist/ionic/{p-de7b5fa3.entry.js → p-e16b69e1.entry.js} +1 -1
  62. package/dist/ionic/p-f65f9308.entry.js +4 -0
  63. package/dist/types/components/header/header.utils.d.ts +10 -0
  64. package/dist/types/components/input/input.d.ts +0 -4
  65. package/dist/types/components/select/select.d.ts +6 -0
  66. package/dist/types/components/textarea/textarea.d.ts +0 -4
  67. package/dist/types/utils/forms/index.d.ts +1 -0
  68. package/dist/types/utils/forms/validity.d.ts +10 -0
  69. package/dist/types/utils/transition/index.d.ts +9 -0
  70. package/hydrate/index.js +161 -45
  71. package/hydrate/index.mjs +161 -45
  72. package/package.json +2 -2
  73. package/dist/ionic/p-1c8a476d.entry.js +0 -4
  74. package/dist/ionic/p-49f0149c.entry.js +0 -4
  75. package/dist/ionic/p-785026d7.entry.js +0 -4
  76. package/dist/ionic/p-78c74a3e.entry.js +0 -4
  77. package/dist/ionic/p-913a7c1e.entry.js +0 -4
  78. package/dist/ionic/p-CMhMiYSX.js +0 -4
@@ -4,6 +4,8 @@
4
4
  import { readTask, writeTask } from "@stencil/core";
5
5
  import { clamp } from "../../utils/helpers";
6
6
  const TRANSITION = 'all 0.2s ease-in-out';
7
+ const ROLE_NONE = 'none';
8
+ const ROLE_BANNER = 'banner';
7
9
  export const cloneElement = (tagName) => {
8
10
  const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
9
11
  if (getCachedEl !== null) {
@@ -130,6 +132,7 @@ export const setHeaderActive = (headerIndex, active = true) => {
130
132
  const toolbars = headerIndex.toolbars;
131
133
  const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
132
134
  if (active) {
135
+ headerEl.setAttribute('role', ROLE_BANNER);
133
136
  headerEl.classList.remove('header-collapse-condense-inactive');
134
137
  ionTitles.forEach((ionTitle) => {
135
138
  if (ionTitle) {
@@ -138,6 +141,16 @@ export const setHeaderActive = (headerIndex, active = true) => {
138
141
  });
139
142
  }
140
143
  else {
144
+ /**
145
+ * There can only be one banner landmark per page.
146
+ * By default, all ion-headers have the banner role.
147
+ * This causes an accessibility issue when using a
148
+ * condensed header since there are two ion-headers
149
+ * on the page at once (active and inactive).
150
+ * To solve this, the role needs to be toggled
151
+ * based on which header is active.
152
+ */
153
+ headerEl.setAttribute('role', ROLE_NONE);
141
154
  headerEl.classList.add('header-collapse-condense-inactive');
142
155
  /**
143
156
  * The small title should only be accessed by screen readers
@@ -197,3 +210,27 @@ export const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
197
210
  });
198
211
  });
199
212
  };
213
+ /**
214
+ * Get the role type for the ion-header.
215
+ *
216
+ * @param isInsideMenu If ion-header is inside ion-menu.
217
+ * @param isCondensed If ion-header has collapse="condense".
218
+ * @param mode The current mode.
219
+ * @returns 'none' if inside ion-menu or if condensed in md
220
+ * mode, otherwise 'banner'.
221
+ */
222
+ export const getRoleType = (isInsideMenu, isCondensed, mode) => {
223
+ // If the header is inside a menu, it should not have the banner role.
224
+ if (isInsideMenu) {
225
+ return ROLE_NONE;
226
+ }
227
+ /**
228
+ * Only apply role="none" to `md` mode condensed headers
229
+ * since the large header is never shown.
230
+ */
231
+ if (isCondensed && mode === 'md') {
232
+ return ROLE_NONE;
233
+ }
234
+ // Default to banner role.
235
+ return ROLE_BANNER;
236
+ };
@@ -2,7 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { Build, Host, forceUpdate, h, } from "@stencil/core";
5
- import { createNotchController } from "../../utils/forms/index";
5
+ import { createNotchController, checkInvalidState } from "../../utils/forms/index";
6
6
  import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from "../../utils/helpers";
7
7
  import { createSlotMutationController } from "../../utils/slot-mutation-controller";
8
8
  import { createColorClasses, hostContext } from "../../utils/theme";
@@ -227,14 +227,6 @@ export class Input {
227
227
  componentWillLoad() {
228
228
  this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
229
229
  }
230
- /**
231
- * Checks if the input is in an invalid state based on Ionic validation classes
232
- */
233
- checkInvalidState() {
234
- const hasIonTouched = this.el.classList.contains('ion-touched');
235
- const hasIonInvalid = this.el.classList.contains('ion-invalid');
236
- return hasIonTouched && hasIonInvalid;
237
- }
238
230
  connectedCallback() {
239
231
  const { el } = this;
240
232
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -242,7 +234,7 @@ export class Input {
242
234
  // Watch for class changes to update validation state
243
235
  if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
244
236
  this.validationObserver = new MutationObserver(() => {
245
- const newIsInvalid = this.checkInvalidState();
237
+ const newIsInvalid = checkInvalidState(el);
246
238
  if (this.isInvalid !== newIsInvalid) {
247
239
  this.isInvalid = newIsInvalid;
248
240
  // Force a re-render to update aria-describedby immediately
@@ -255,7 +247,7 @@ export class Input {
255
247
  });
256
248
  }
257
249
  // Always set initial state
258
- this.isInvalid = this.checkInvalidState();
250
+ this.isInvalid = checkInvalidState(el);
259
251
  this.debounceChanged();
260
252
  if (Build.isBrowser) {
261
253
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -519,7 +511,7 @@ export class Input {
519
511
  * TODO(FW-5592): Remove hasStartEndSlots condition
520
512
  */
521
513
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
522
- return (h(Host, { key: '8a51f0300d5bc66392f9ab9a6fa0b5d388072a33', class: createColorClasses(this.color, {
514
+ return (h(Host, { key: '97b5308021064d9e7434ef2d3d96f27045c1b0c4', class: createColorClasses(this.color, {
523
515
  [mode]: true,
524
516
  'has-value': hasValue,
525
517
  'has-focus': hasFocus,
@@ -530,14 +522,14 @@ export class Input {
530
522
  'in-item': inItem,
531
523
  'in-item-color': hostContext('ion-item.ion-color', this.el),
532
524
  'input-disabled': disabled,
533
- }) }, 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) => {
525
+ }) }, 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) => {
534
526
  /**
535
527
  * This prevents mobile browsers from
536
528
  * blurring the input when the clear
537
529
  * button is activated.
538
530
  */
539
531
  ev.preventDefault();
540
- }, 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()));
532
+ }, 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()));
541
533
  }
542
534
  static get is() { return "ion-input"; }
543
535
  static get encapsulation() { return "scoped"; }
@@ -1,8 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { Host, h, forceUpdate } from "@stencil/core";
5
- import { compareOptions, createNotchController, isOptionSelected } from "../../utils/forms/index";
4
+ import { Build, Host, h, forceUpdate } from "@stencil/core";
5
+ import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from "../../utils/forms/index";
6
6
  import { focusVisibleElement, renderHiddenInput, inheritAttributes } from "../../utils/helpers";
7
7
  import { printIonWarning } from "../../utils/logging/index";
8
8
  import { actionSheetController, alertController, popoverController, modalController } from "../../utils/overlays";
@@ -44,6 +44,10 @@ export class Select {
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
  * The text to display on the cancel button.
49
53
  */
@@ -173,9 +177,46 @@ export class Select {
173
177
  */
174
178
  forceUpdate(this);
175
179
  });
180
+ // Watch for class changes to update validation state.
181
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
182
+ this.validationObserver = new MutationObserver(() => {
183
+ const newIsInvalid = checkInvalidState(this.el);
184
+ if (this.isInvalid !== newIsInvalid) {
185
+ this.isInvalid = newIsInvalid;
186
+ /**
187
+ * Screen readers tend to announce changes
188
+ * to `aria-describedby` when the attribute
189
+ * is changed during a blur event for a
190
+ * native form control.
191
+ * However, the announcement can be spotty
192
+ * when using a non-native form control
193
+ * and `forceUpdate()`.
194
+ * This is due to `forceUpdate()` internally
195
+ * rescheduling the DOM update to a lower
196
+ * priority queue regardless if it's called
197
+ * inside a Promise or not, thus causing
198
+ * the screen reader to potentially miss the
199
+ * change.
200
+ * By using a State variable inside a Promise,
201
+ * it guarantees a re-render immediately at
202
+ * a higher priority.
203
+ */
204
+ Promise.resolve().then(() => {
205
+ this.hintTextID = this.getHintTextID();
206
+ });
207
+ }
208
+ });
209
+ this.validationObserver.observe(el, {
210
+ attributes: true,
211
+ attributeFilter: ['class'],
212
+ });
213
+ }
214
+ // Always set initial state
215
+ this.isInvalid = checkInvalidState(this.el);
176
216
  }
177
217
  componentWillLoad() {
178
218
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
219
+ this.hintTextID = this.getHintTextID();
179
220
  }
180
221
  componentDidLoad() {
181
222
  /**
@@ -199,6 +240,11 @@ export class Select {
199
240
  this.notchController.destroy();
200
241
  this.notchController = undefined;
201
242
  }
243
+ // Clean up validation observer to prevent memory leaks.
244
+ if (this.validationObserver) {
245
+ this.validationObserver.disconnect();
246
+ this.validationObserver = undefined;
247
+ }
202
248
  }
203
249
  /**
204
250
  * Open the select overlay. The overlay is either an alert, action sheet, or popover,
@@ -715,11 +761,11 @@ export class Select {
715
761
  }
716
762
  renderListbox() {
717
763
  const { disabled, inputId, isExpanded, required } = this;
718
- 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) }));
764
+ 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) }));
719
765
  }
720
766
  getHintTextID() {
721
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
722
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
767
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
768
+ if (isInvalid && errorText) {
723
769
  return errorTextId;
724
770
  }
725
771
  if (helperText) {
@@ -731,10 +777,10 @@ export class Select {
731
777
  * Renders the helper text or error text values
732
778
  */
733
779
  renderHintText() {
734
- const { helperText, errorText, helperTextId, errorTextId } = this;
780
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
735
781
  return [
736
- h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText),
737
- h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText),
782
+ h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null),
783
+ h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null),
738
784
  ];
739
785
  }
740
786
  /**
@@ -782,7 +828,7 @@ export class Select {
782
828
  * TODO(FW-5592): Remove hasStartEndSlots condition
783
829
  */
784
830
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
785
- return (h(Host, { key: 'c03fb65e8fc9f9aab295e07b282377d57d910519', onClick: this.onClick, class: createColorClasses(this.color, {
831
+ return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
786
832
  [mode]: true,
787
833
  'in-item': inItem,
788
834
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -800,7 +846,7 @@ export class Select {
800
846
  [`select-justify-${justify}`]: justifyEnabled,
801
847
  [`select-shape-${shape}`]: shape !== undefined,
802
848
  [`select-label-placement-${labelPlacement}`]: true,
803
- }) }, 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()));
849
+ }) }, 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()));
804
850
  }
805
851
  static get is() { return "ion-select"; }
806
852
  static get encapsulation() { return "shadow"; }
@@ -1268,7 +1314,9 @@ export class Select {
1268
1314
  static get states() {
1269
1315
  return {
1270
1316
  "isExpanded": {},
1271
- "hasFocus": {}
1317
+ "hasFocus": {},
1318
+ "isInvalid": {},
1319
+ "hintTextID": {}
1272
1320
  };
1273
1321
  }
1274
1322
  static get events() {
@@ -2,7 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { Build, Host, forceUpdate, h, writeTask, } from "@stencil/core";
5
- import { createNotchController } from "../../utils/forms/index";
5
+ import { createNotchController, checkInvalidState } from "../../utils/forms/index";
6
6
  import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from "../../utils/helpers";
7
7
  import { createSlotMutationController } from "../../utils/slot-mutation-controller";
8
8
  import { createColorClasses, hostContext } from "../../utils/theme";
@@ -187,14 +187,6 @@ export class Textarea {
187
187
  this.el.click();
188
188
  }
189
189
  }
190
- /**
191
- * Checks if the textarea is in an invalid state based on Ionic validation classes
192
- */
193
- checkValidationState() {
194
- const hasIonTouched = this.el.classList.contains('ion-touched');
195
- const hasIonInvalid = this.el.classList.contains('ion-invalid');
196
- return hasIonTouched && hasIonInvalid;
197
- }
198
190
  connectedCallback() {
199
191
  const { el } = this;
200
192
  this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -202,7 +194,7 @@ export class Textarea {
202
194
  // Watch for class changes to update validation state
203
195
  if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
204
196
  this.validationObserver = new MutationObserver(() => {
205
- const newIsInvalid = this.checkValidationState();
197
+ const newIsInvalid = checkInvalidState(this.el);
206
198
  if (this.isInvalid !== newIsInvalid) {
207
199
  this.isInvalid = newIsInvalid;
208
200
  // Force a re-render to update aria-describedby immediately
@@ -215,7 +207,7 @@ export class Textarea {
215
207
  });
216
208
  }
217
209
  // Always set initial state
218
- this.isInvalid = this.checkValidationState();
210
+ this.isInvalid = checkInvalidState(this.el);
219
211
  this.debounceChanged();
220
212
  if (Build.isBrowser) {
221
213
  document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
@@ -479,7 +471,7 @@ export class Textarea {
479
471
  * TODO(FW-5592): Remove hasStartEndSlots condition
480
472
  */
481
473
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
482
- return (h(Host, { key: '26b46666a92b3f652775bb1c46661f9a30392104', class: createColorClasses(this.color, {
474
+ return (h(Host, { key: 'a70a62d7aae3831a50acd74f60b930925ada1326', class: createColorClasses(this.color, {
483
475
  [mode]: true,
484
476
  'has-value': hasValue,
485
477
  'has-focus': hasFocus,
@@ -488,7 +480,7 @@ export class Textarea {
488
480
  [`textarea-shape-${shape}`]: shape !== undefined,
489
481
  [`textarea-label-placement-${labelPlacement}`]: true,
490
482
  'textarea-disabled': disabled,
491
- }) }, 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()));
483
+ }) }, 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()));
492
484
  }
493
485
  static get is() { return "ion-textarea"; }
494
486
  static get encapsulation() { return "scoped"; }
@@ -3,3 +3,4 @@
3
3
  */
4
4
  export * from './notch-controller';
5
5
  export * from './compare-with-utils';
6
+ export * from './validity';
@@ -0,0 +1,15 @@
1
+ /*!
2
+ * (C) Ionic http://ionicframework.com - MIT License
3
+ */
4
+ /**
5
+ * Checks if the form element is in an invalid state based on
6
+ * Ionic validation classes.
7
+ *
8
+ * @param el The form element to check.
9
+ * @returns `true` if the element is invalid, `false` otherwise.
10
+ */
11
+ export const checkInvalidState = (el) => {
12
+ const hasIonTouched = el.classList.contains('ion-touched');
13
+ const hasIonInvalid = el.classList.contains('ion-invalid');
14
+ return hasIonTouched && hasIonInvalid;
15
+ };
@@ -10,11 +10,22 @@ const iosTransitionAnimation = () => import('./ios.transition');
10
10
  const mdTransitionAnimation = () => import('./md.transition');
11
11
  const focusController = createFocusController();
12
12
  // TODO(FW-2832): types
13
+ /**
14
+ * Executes the main page transition.
15
+ * It also manages the lifecycle of header visibility (if any)
16
+ * to prevent visual flickering in iOS. The flickering only
17
+ * occurs for a condensed header that is placed above the content.
18
+ *
19
+ * @param opts Options for the transition.
20
+ * @returns A promise that resolves when the transition is complete.
21
+ */
13
22
  export const transition = (opts) => {
14
23
  return new Promise((resolve, reject) => {
15
24
  writeTask(() => {
16
- beforeTransition(opts);
17
- runTransition(opts).then((result) => {
25
+ const transitioningInactiveHeader = getIosIonHeader(opts);
26
+ beforeTransition(opts, transitioningInactiveHeader);
27
+ runTransition(opts)
28
+ .then((result) => {
18
29
  if (result.animation) {
19
30
  result.animation.destroy();
20
31
  }
@@ -23,15 +34,21 @@ export const transition = (opts) => {
23
34
  }, (error) => {
24
35
  afterTransition(opts);
25
36
  reject(error);
37
+ })
38
+ .finally(() => {
39
+ // Ensure that the header is restored to its original state.
40
+ setHeaderTransitionClass(transitioningInactiveHeader, false);
26
41
  });
27
42
  });
28
43
  });
29
44
  };
30
- const beforeTransition = (opts) => {
45
+ const beforeTransition = (opts, transitioningInactiveHeader) => {
31
46
  const enteringEl = opts.enteringEl;
32
47
  const leavingEl = opts.leavingEl;
33
48
  focusController.saveViewFocus(leavingEl);
34
49
  setZIndex(enteringEl, leavingEl, opts.direction);
50
+ // Prevent flickering of the header by adding a class.
51
+ setHeaderTransitionClass(transitioningInactiveHeader, true);
35
52
  if (opts.showGoBack) {
36
53
  enteringEl.classList.add('can-go-back');
37
54
  }
@@ -220,6 +237,39 @@ const setZIndex = (enteringEl, leavingEl, direction) => {
220
237
  leavingEl.style.zIndex = '100';
221
238
  }
222
239
  };
240
+ /**
241
+ * Add a class to ensure that the header (if any)
242
+ * does not flicker during the transition. By adding the
243
+ * transitioning class, we ensure that the header has
244
+ * the necessary styles to prevent the following flickers:
245
+ * 1. When entering a page with a condensed header, the
246
+ * header should never be visible. However,
247
+ * it briefly renders the background color while
248
+ * the transition is occurring.
249
+ * 2. When leaving a page with a condensed header, the
250
+ * header has an opacity of 0 and the pages
251
+ * have a z-index which causes the entering page to
252
+ * briefly show it's content underneath the leaving page.
253
+ * 3. When entering a page or leaving a page with a fade
254
+ * header, the header should not have a background color.
255
+ * However, it briefly shows the background color while
256
+ * the transition is occurring.
257
+ *
258
+ * @param header The header element to modify.
259
+ * @param isTransitioning Whether the transition is occurring.
260
+ */
261
+ const setHeaderTransitionClass = (header, isTransitioning) => {
262
+ if (!header) {
263
+ return;
264
+ }
265
+ const transitionClass = 'header-transitioning';
266
+ if (isTransitioning) {
267
+ header.classList.add(transitionClass);
268
+ }
269
+ else {
270
+ header.classList.remove(transitionClass);
271
+ }
272
+ };
223
273
  export const getIonPageElement = (element) => {
224
274
  if (element.classList.contains('ion-page')) {
225
275
  return element;
@@ -231,3 +281,24 @@ export const getIonPageElement = (element) => {
231
281
  // idk, return the original element so at least something animates and we don't have a null pointer
232
282
  return element;
233
283
  };
284
+ /**
285
+ * Retrieves the ion-header element from a page based on the
286
+ * direction of the transition.
287
+ *
288
+ * @param opts Options for the transition.
289
+ * @returns The ion-header element or null if not found or not in 'ios' mode.
290
+ */
291
+ const getIosIonHeader = (opts) => {
292
+ const enteringEl = opts.enteringEl;
293
+ const leavingEl = opts.leavingEl;
294
+ const direction = opts.direction;
295
+ const mode = opts.mode;
296
+ if (mode !== 'ios') {
297
+ return null;
298
+ }
299
+ const element = direction === 'back' ? leavingEl : enteringEl;
300
+ if (!element) {
301
+ return null;
302
+ }
303
+ return element.querySelector('ion-header');
304
+ };
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2025-10-14T06:11:22",
2
+ "timestamp": "2025-10-15T18:56:08",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.38.0",
@@ -118,15 +118,26 @@ const createFocusController = () => {
118
118
  };
119
119
  const LAST_FOCUS = 'ion-last-focus';
120
120
 
121
- const iosTransitionAnimation = () => import('./ios.transition-Bpq9ixwv.js');
122
- const mdTransitionAnimation = () => import('./md.transition-zOA0oanq.js');
121
+ const iosTransitionAnimation = () => import('./ios.transition-BDzw0_Hm.js');
122
+ const mdTransitionAnimation = () => import('./md.transition-BzDYi3qq.js');
123
123
  const focusController = createFocusController();
124
124
  // TODO(FW-2832): types
125
+ /**
126
+ * Executes the main page transition.
127
+ * It also manages the lifecycle of header visibility (if any)
128
+ * to prevent visual flickering in iOS. The flickering only
129
+ * occurs for a condensed header that is placed above the content.
130
+ *
131
+ * @param opts Options for the transition.
132
+ * @returns A promise that resolves when the transition is complete.
133
+ */
125
134
  const transition = (opts) => {
126
135
  return new Promise((resolve, reject) => {
127
136
  writeTask(() => {
128
- beforeTransition(opts);
129
- runTransition(opts).then((result) => {
137
+ const transitioningInactiveHeader = getIosIonHeader(opts);
138
+ beforeTransition(opts, transitioningInactiveHeader);
139
+ runTransition(opts)
140
+ .then((result) => {
130
141
  if (result.animation) {
131
142
  result.animation.destroy();
132
143
  }
@@ -135,15 +146,21 @@ const transition = (opts) => {
135
146
  }, (error) => {
136
147
  afterTransition(opts);
137
148
  reject(error);
149
+ })
150
+ .finally(() => {
151
+ // Ensure that the header is restored to its original state.
152
+ setHeaderTransitionClass(transitioningInactiveHeader, false);
138
153
  });
139
154
  });
140
155
  });
141
156
  };
142
- const beforeTransition = (opts) => {
157
+ const beforeTransition = (opts, transitioningInactiveHeader) => {
143
158
  const enteringEl = opts.enteringEl;
144
159
  const leavingEl = opts.leavingEl;
145
160
  focusController.saveViewFocus(leavingEl);
146
161
  setZIndex(enteringEl, leavingEl, opts.direction);
162
+ // Prevent flickering of the header by adding a class.
163
+ setHeaderTransitionClass(transitioningInactiveHeader, true);
147
164
  if (opts.showGoBack) {
148
165
  enteringEl.classList.add('can-go-back');
149
166
  }
@@ -332,6 +349,39 @@ const setZIndex = (enteringEl, leavingEl, direction) => {
332
349
  leavingEl.style.zIndex = '100';
333
350
  }
334
351
  };
352
+ /**
353
+ * Add a class to ensure that the header (if any)
354
+ * does not flicker during the transition. By adding the
355
+ * transitioning class, we ensure that the header has
356
+ * the necessary styles to prevent the following flickers:
357
+ * 1. When entering a page with a condensed header, the
358
+ * header should never be visible. However,
359
+ * it briefly renders the background color while
360
+ * the transition is occurring.
361
+ * 2. When leaving a page with a condensed header, the
362
+ * header has an opacity of 0 and the pages
363
+ * have a z-index which causes the entering page to
364
+ * briefly show it's content underneath the leaving page.
365
+ * 3. When entering a page or leaving a page with a fade
366
+ * header, the header should not have a background color.
367
+ * However, it briefly shows the background color while
368
+ * the transition is occurring.
369
+ *
370
+ * @param header The header element to modify.
371
+ * @param isTransitioning Whether the transition is occurring.
372
+ */
373
+ const setHeaderTransitionClass = (header, isTransitioning) => {
374
+ if (!header) {
375
+ return;
376
+ }
377
+ const transitionClass = 'header-transitioning';
378
+ if (isTransitioning) {
379
+ header.classList.add(transitionClass);
380
+ }
381
+ else {
382
+ header.classList.remove(transitionClass);
383
+ }
384
+ };
335
385
  const getIonPageElement = (element) => {
336
386
  if (element.classList.contains('ion-page')) {
337
387
  return element;
@@ -343,5 +393,26 @@ const getIonPageElement = (element) => {
343
393
  // idk, return the original element so at least something animates and we don't have a null pointer
344
394
  return element;
345
395
  };
396
+ /**
397
+ * Retrieves the ion-header element from a page based on the
398
+ * direction of the transition.
399
+ *
400
+ * @param opts Options for the transition.
401
+ * @returns The ion-header element or null if not found or not in 'ios' mode.
402
+ */
403
+ const getIosIonHeader = (opts) => {
404
+ const enteringEl = opts.enteringEl;
405
+ const leavingEl = opts.leavingEl;
406
+ const direction = opts.direction;
407
+ const mode = opts.mode;
408
+ if (mode !== 'ios') {
409
+ return null;
410
+ }
411
+ const element = direction === 'back' ? leavingEl : enteringEl;
412
+ if (!element) {
413
+ return null;
414
+ }
415
+ return element.querySelector('ion-header');
416
+ };
346
417
 
347
418
  export { LIFECYCLE_WILL_ENTER as L, LIFECYCLE_DID_ENTER as a, LIFECYCLE_WILL_LEAVE as b, LIFECYCLE_DID_LEAVE as c, LIFECYCLE_WILL_UNLOAD as d, deepReady as e, getIonPageElement as g, lifecycle as l, setPageHidden as s, transition as t, waitForMount as w };
package/dist/esm/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  export { c as createAnimation } from './animation-Dt8bGnA-.js';
5
- export { a as LIFECYCLE_DID_ENTER, c as LIFECYCLE_DID_LEAVE, L as LIFECYCLE_WILL_ENTER, b as LIFECYCLE_WILL_LEAVE, d as LIFECYCLE_WILL_UNLOAD, g as getIonPageElement } from './index-D6G2seR8.js';
6
- export { iosTransitionAnimation } from './ios.transition-Bpq9ixwv.js';
7
- export { mdTransitionAnimation } from './md.transition-zOA0oanq.js';
5
+ export { a as LIFECYCLE_DID_ENTER, c as LIFECYCLE_DID_LEAVE, L as LIFECYCLE_WILL_ENTER, b as LIFECYCLE_WILL_LEAVE, d as LIFECYCLE_WILL_UNLOAD, g as getIonPageElement } from './index-r2D9DEro.js';
6
+ export { iosTransitionAnimation } from './ios.transition-BDzw0_Hm.js';
7
+ export { mdTransitionAnimation } from './md.transition-BzDYi3qq.js';
8
8
  export { g as getTimeGivenProgression } from './cubic-bezier-hHmYLOfE.js';
9
9
  export { createGesture } from './index-CfgBF1SE.js';
10
10
  export { g as getPlatforms, i as initialize, a as isPlatform } from './ionic-global-CDrldh-5.js';