voyager-ionic-core 8.7.6 → 8.7.11

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 (266) hide show
  1. package/components/button.js +3 -7
  2. package/components/checkbox.js +64 -13
  3. package/components/header.js +42 -4
  4. package/components/index2.js +74 -3
  5. package/components/ion-accordion.js +93 -14
  6. package/components/ion-datetime.js +35 -2
  7. package/components/ion-input.js +6 -13
  8. package/components/ion-select.js +59 -10
  9. package/components/ion-textarea.js +5 -12
  10. package/components/ion-toggle.js +63 -16
  11. package/components/radio-group.js +60 -7
  12. package/components/validity.js +17 -0
  13. package/dist/cjs/{index-CD5Rjp23.js → index-094mMFB-.js} +76 -5
  14. package/dist/cjs/index.cjs.js +3 -3
  15. package/dist/cjs/ion-accordion_2.cjs.entry.js +91 -13
  16. package/dist/cjs/ion-app_8.cjs.entry.js +43 -5
  17. package/dist/cjs/ion-button_2.cjs.entry.js +3 -7
  18. package/dist/cjs/ion-checkbox.cjs.entry.js +61 -12
  19. package/dist/cjs/ion-datetime_3.cjs.entry.js +35 -2
  20. package/dist/cjs/ion-input.cjs.entry.js +6 -13
  21. package/dist/cjs/ion-modal.cjs.entry.js +1 -1
  22. package/dist/cjs/ion-nav_2.cjs.entry.js +1 -1
  23. package/dist/cjs/ion-popover.cjs.entry.js +1 -1
  24. package/dist/cjs/ion-radio_2.cjs.entry.js +57 -6
  25. package/dist/cjs/ion-select_3.cjs.entry.js +56 -9
  26. package/dist/cjs/ion-textarea.cjs.entry.js +5 -12
  27. package/dist/cjs/ion-toggle.cjs.entry.js +59 -14
  28. package/dist/cjs/ionic.cjs.js +1 -1
  29. package/dist/cjs/{ios.transition-j9CclgEW.js → ios.transition-BOt_uW73.js} +1 -1
  30. package/dist/cjs/loader.cjs.js +1 -1
  31. package/dist/cjs/{md.transition-CwFyRSfv.js → md.transition-Dt968VXB.js} +1 -1
  32. package/dist/cjs/validity-BpS37YFM.js +19 -0
  33. package/dist/collection/components/accordion/accordion.js +93 -14
  34. package/dist/collection/components/button/button.js +3 -7
  35. package/dist/collection/components/checkbox/checkbox.js +68 -13
  36. package/dist/collection/components/datetime/datetime.js +35 -2
  37. package/dist/collection/components/header/header.ios.css +27 -1
  38. package/dist/collection/components/header/header.js +5 -4
  39. package/dist/collection/components/header/header.utils.js +37 -0
  40. package/dist/collection/components/input/input.js +6 -14
  41. package/dist/collection/components/radio-group/radio-group.js +64 -7
  42. package/dist/collection/components/select/select.js +60 -12
  43. package/dist/collection/components/textarea/textarea.js +5 -13
  44. package/dist/collection/components/toggle/toggle.js +63 -16
  45. package/dist/collection/utils/forms/index.js +1 -0
  46. package/dist/collection/utils/forms/validity.js +15 -0
  47. package/dist/collection/utils/test/playwright/page/utils/set-content.js +7 -0
  48. package/dist/collection/utils/test/playwright/page/utils/spy-on-event.js +32 -0
  49. package/dist/collection/utils/transition/index.js +74 -3
  50. package/dist/docs.json +1 -1
  51. package/dist/esm/{index-D6G2seR8.js → index-r2D9DEro.js} +76 -5
  52. package/dist/esm/index.js +3 -3
  53. package/dist/esm/ion-accordion_2.entry.js +91 -13
  54. package/dist/esm/ion-app_8.entry.js +43 -5
  55. package/dist/esm/ion-button_2.entry.js +3 -7
  56. package/dist/esm/ion-checkbox.entry.js +61 -12
  57. package/dist/esm/ion-datetime_3.entry.js +35 -2
  58. package/dist/esm/ion-input.entry.js +6 -13
  59. package/dist/esm/ion-modal.entry.js +1 -1
  60. package/dist/esm/ion-nav_2.entry.js +1 -1
  61. package/dist/esm/ion-popover.entry.js +1 -1
  62. package/dist/esm/ion-radio_2.entry.js +57 -6
  63. package/dist/esm/ion-select_3.entry.js +56 -9
  64. package/dist/esm/ion-textarea.entry.js +5 -12
  65. package/dist/esm/ion-toggle.entry.js +59 -14
  66. package/dist/esm/ionic.js +1 -1
  67. package/dist/esm/{ios.transition-Bpq9ixwv.js → ios.transition-BDzw0_Hm.js} +1 -1
  68. package/dist/esm/loader.js +1 -1
  69. package/dist/esm/{md.transition-zOA0oanq.js → md.transition-BzDYi3qq.js} +1 -1
  70. package/dist/esm/validity-DJztqcrH.js +17 -0
  71. package/dist/ionic/index.esm.js +1 -1
  72. package/dist/ionic/ionic.esm.js +1 -1
  73. package/dist/ionic/p-40c261a3.entry.js +4 -0
  74. package/dist/ionic/p-43ed1ef5.entry.js +4 -0
  75. package/dist/ionic/p-4e41ea20.entry.js +4 -0
  76. package/dist/ionic/{p-323421af.entry.js → p-5a39a99a.entry.js} +1 -1
  77. package/dist/ionic/p-5fb517e4.entry.js +4 -0
  78. package/dist/ionic/p-7380261c.entry.js +4 -0
  79. package/dist/ionic/{p-9a36e2e7.entry.js → p-95bddd49.entry.js} +1 -1
  80. package/dist/ionic/{p-DPhQmGJN.js → p-C7hRNDhM.js} +1 -1
  81. package/dist/ionic/p-DJztqcrH.js +4 -0
  82. package/dist/ionic/p-DUt5fQmA.js +4 -0
  83. package/dist/ionic/{p-9R1XyICs.js → p-DZRJwG4S.js} +1 -1
  84. package/dist/ionic/p-c19f63d0.entry.js +4 -0
  85. package/dist/ionic/p-cb93126d.entry.js +4 -0
  86. package/dist/ionic/p-d0a2a1ab.entry.js +4 -0
  87. package/dist/ionic/p-d1f54e28.entry.js +4 -0
  88. package/dist/ionic/p-d3014190.entry.js +4 -0
  89. package/dist/ionic/{p-de7b5fa3.entry.js → p-e16b69e1.entry.js} +1 -1
  90. package/dist/ionic/svg/checkbox-outline.svg +1 -0
  91. package/dist/ionic/svg/checkbox-sharp.svg +1 -0
  92. package/dist/ionic/svg/checkbox.svg +1 -0
  93. package/dist/ionic/svg/checkmark-circle-outline.svg +1 -0
  94. package/dist/ionic/svg/checkmark-circle-sharp.svg +1 -0
  95. package/dist/ionic/svg/checkmark-circle.svg +1 -0
  96. package/dist/ionic/svg/checkmark-done-circle-outline.svg +1 -0
  97. package/dist/ionic/svg/checkmark-done-circle-sharp.svg +1 -0
  98. package/dist/ionic/svg/checkmark-done-circle.svg +1 -0
  99. package/dist/ionic/svg/checkmark-done-outline.svg +1 -0
  100. package/dist/ionic/svg/checkmark-done-sharp.svg +1 -0
  101. package/dist/ionic/svg/checkmark-done.svg +1 -0
  102. package/dist/ionic/svg/checkmark-outline.svg +1 -0
  103. package/dist/ionic/svg/checkmark-sharp.svg +1 -0
  104. package/dist/ionic/svg/checkmark.svg +1 -0
  105. package/dist/ionic/svg/chevron-back-circle-outline.svg +1 -0
  106. package/dist/ionic/svg/chevron-back-circle-sharp.svg +1 -0
  107. package/dist/ionic/svg/chevron-back-circle.svg +1 -0
  108. package/dist/ionic/svg/chevron-back-outline.svg +1 -0
  109. package/dist/ionic/svg/chevron-back-sharp.svg +1 -0
  110. package/dist/ionic/svg/chevron-back.svg +1 -0
  111. package/dist/ionic/svg/chevron-collapse-outline.svg +1 -0
  112. package/dist/ionic/svg/chevron-collapse-sharp.svg +1 -0
  113. package/dist/ionic/svg/chevron-collapse.svg +1 -0
  114. package/dist/ionic/svg/chevron-down-circle-outline.svg +1 -0
  115. package/dist/ionic/svg/chevron-down-circle-sharp.svg +1 -0
  116. package/dist/ionic/svg/chevron-down-circle.svg +1 -0
  117. package/dist/ionic/svg/chevron-down-outline.svg +1 -0
  118. package/dist/ionic/svg/chevron-down-sharp.svg +1 -0
  119. package/dist/ionic/svg/chevron-down.svg +1 -0
  120. package/dist/ionic/svg/chevron-expand-outline.svg +1 -0
  121. package/dist/ionic/svg/chevron-expand-sharp.svg +1 -0
  122. package/dist/ionic/svg/chevron-expand.svg +1 -0
  123. package/dist/ionic/svg/chevron-forward-circle-outline.svg +1 -0
  124. package/dist/ionic/svg/chevron-forward-circle-sharp.svg +1 -0
  125. package/dist/ionic/svg/chevron-forward-circle.svg +1 -0
  126. package/dist/ionic/svg/chevron-forward-outline.svg +1 -0
  127. package/dist/ionic/svg/chevron-forward-sharp.svg +1 -0
  128. package/dist/ionic/svg/chevron-forward.svg +1 -0
  129. package/dist/ionic/svg/chevron-up-circle-outline.svg +1 -0
  130. package/dist/ionic/svg/chevron-up-circle-sharp.svg +1 -0
  131. package/dist/ionic/svg/chevron-up-circle.svg +1 -0
  132. package/dist/ionic/svg/chevron-up-outline.svg +1 -0
  133. package/dist/ionic/svg/chevron-up-sharp.svg +1 -0
  134. package/dist/ionic/svg/chevron-up.svg +1 -0
  135. package/dist/ionic/svg/clipboard-outline.svg +1 -0
  136. package/dist/ionic/svg/clipboard-sharp.svg +1 -0
  137. package/dist/ionic/svg/clipboard.svg +1 -0
  138. package/dist/ionic/svg/close-circle-outline.svg +1 -0
  139. package/dist/ionic/svg/close-circle-sharp.svg +1 -0
  140. package/dist/ionic/svg/close-circle.svg +1 -0
  141. package/dist/ionic/svg/close-outline.svg +1 -0
  142. package/dist/ionic/svg/close-sharp.svg +1 -0
  143. package/dist/ionic/svg/close.svg +1 -0
  144. package/dist/ionic/svg/cloud-circle-outline.svg +1 -0
  145. package/dist/ionic/svg/cloud-circle-sharp.svg +1 -0
  146. package/dist/ionic/svg/cloud-circle.svg +1 -0
  147. package/dist/ionic/svg/cloud-done-outline.svg +1 -0
  148. package/dist/ionic/svg/cloud-done-sharp.svg +1 -0
  149. package/dist/ionic/svg/cloud-done.svg +1 -0
  150. package/dist/ionic/svg/cloud-download-outline.svg +1 -0
  151. package/dist/ionic/svg/cloud-download-sharp.svg +1 -0
  152. package/dist/ionic/svg/cloud-download.svg +1 -0
  153. package/dist/ionic/svg/cloud-offline-outline.svg +1 -0
  154. package/dist/ionic/svg/cloud-offline-sharp.svg +1 -0
  155. package/dist/ionic/svg/cloud-offline.svg +1 -0
  156. package/dist/ionic/svg/cloud-outline.svg +1 -0
  157. package/dist/ionic/svg/cloud-sharp.svg +1 -0
  158. package/dist/ionic/svg/cloud-upload-outline.svg +1 -0
  159. package/dist/ionic/svg/cloud-upload-sharp.svg +1 -0
  160. package/dist/ionic/svg/cloud-upload.svg +1 -0
  161. package/dist/ionic/svg/cloud.svg +1 -0
  162. package/dist/ionic/svg/cloudy-night-outline.svg +1 -0
  163. package/dist/ionic/svg/cloudy-night-sharp.svg +1 -0
  164. package/dist/ionic/svg/cloudy-night.svg +1 -0
  165. package/dist/ionic/svg/cloudy-outline.svg +1 -0
  166. package/dist/ionic/svg/cloudy-sharp.svg +1 -0
  167. package/dist/ionic/svg/cloudy.svg +1 -0
  168. package/dist/ionic/svg/code-download-outline.svg +1 -0
  169. package/dist/ionic/svg/code-download-sharp.svg +1 -0
  170. package/dist/ionic/svg/code-download.svg +1 -0
  171. package/dist/ionic/svg/code-outline.svg +1 -0
  172. package/dist/ionic/svg/code-sharp.svg +1 -0
  173. package/dist/ionic/svg/code-slash-outline.svg +1 -0
  174. package/dist/ionic/svg/code-slash-sharp.svg +1 -0
  175. package/dist/ionic/svg/code-slash.svg +1 -0
  176. package/dist/ionic/svg/code-working-outline.svg +1 -0
  177. package/dist/ionic/svg/code-working-sharp.svg +1 -0
  178. package/dist/ionic/svg/code-working.svg +1 -0
  179. package/dist/ionic/svg/code.svg +1 -0
  180. package/dist/ionic/svg/cog-outline.svg +1 -0
  181. package/dist/ionic/svg/cog-sharp.svg +1 -0
  182. package/dist/ionic/svg/cog.svg +1 -0
  183. package/dist/ionic/svg/color-fill-outline.svg +1 -0
  184. package/dist/ionic/svg/color-fill-sharp.svg +1 -0
  185. package/dist/ionic/svg/color-fill.svg +1 -0
  186. package/dist/ionic/svg/color-filter-outline.svg +1 -0
  187. package/dist/ionic/svg/color-filter-sharp.svg +1 -0
  188. package/dist/ionic/svg/color-filter.svg +1 -0
  189. package/dist/ionic/svg/color-palette-outline.svg +1 -0
  190. package/dist/ionic/svg/color-palette-sharp.svg +1 -0
  191. package/dist/ionic/svg/color-palette.svg +1 -0
  192. package/dist/ionic/svg/color-wand-outline.svg +1 -0
  193. package/dist/ionic/svg/color-wand-sharp.svg +1 -0
  194. package/dist/ionic/svg/color-wand.svg +1 -0
  195. package/dist/ionic/svg/compass-outline.svg +1 -0
  196. package/dist/ionic/svg/compass-sharp.svg +1 -0
  197. package/dist/ionic/svg/compass.svg +1 -0
  198. package/dist/ionic/svg/construct-outline.svg +1 -0
  199. package/dist/ionic/svg/construct-sharp.svg +1 -0
  200. package/dist/ionic/svg/construct.svg +1 -0
  201. package/dist/ionic/svg/contract-outline.svg +1 -0
  202. package/dist/ionic/svg/contract-sharp.svg +1 -0
  203. package/dist/ionic/svg/contract.svg +1 -0
  204. package/dist/ionic/svg/contrast-outline.svg +1 -0
  205. package/dist/ionic/svg/contrast-sharp.svg +1 -0
  206. package/dist/ionic/svg/contrast.svg +1 -0
  207. package/dist/ionic/svg/copy-outline.svg +1 -0
  208. package/dist/ionic/svg/copy-sharp.svg +1 -0
  209. package/dist/ionic/svg/copy.svg +1 -0
  210. package/dist/ionic/svg/create-outline.svg +1 -0
  211. package/dist/ionic/svg/create-sharp.svg +1 -0
  212. package/dist/ionic/svg/create.svg +1 -0
  213. package/dist/ionic/svg/crop-outline.svg +1 -0
  214. package/dist/ionic/svg/crop-sharp.svg +1 -0
  215. package/dist/ionic/svg/crop.svg +1 -0
  216. package/dist/ionic/svg/cube-outline.svg +1 -0
  217. package/dist/ionic/svg/cube-sharp.svg +1 -0
  218. package/dist/ionic/svg/cube.svg +1 -0
  219. package/dist/ionic/svg/cut-outline.svg +1 -0
  220. package/dist/ionic/svg/cut-sharp.svg +1 -0
  221. package/dist/ionic/svg/cut.svg +1 -0
  222. package/dist/ionic/svg/desktop-outline.svg +1 -0
  223. package/dist/ionic/svg/desktop-sharp.svg +1 -0
  224. package/dist/ionic/svg/desktop.svg +1 -0
  225. package/dist/ionic/svg/diamond-outline.svg +1 -0
  226. package/dist/ionic/svg/diamond-sharp.svg +1 -0
  227. package/dist/ionic/svg/diamond.svg +1 -0
  228. package/dist/ionic/svg/dice-outline.svg +1 -0
  229. package/dist/ionic/svg/dice-sharp.svg +1 -0
  230. package/dist/ionic/svg/dice.svg +1 -0
  231. package/dist/ionic/svg/disc-outline.svg +1 -0
  232. package/dist/ionic/svg/disc-sharp.svg +1 -0
  233. package/dist/ionic/svg/disc.svg +1 -0
  234. package/dist/ionic/svg/document-attach-outline.svg +1 -0
  235. package/dist/ionic/svg/document-attach-sharp.svg +1 -0
  236. package/dist/ionic/svg/document-attach.svg +1 -0
  237. package/dist/ionic/svg/document-lock-outline.svg +1 -0
  238. package/dist/ionic/svg/document-lock-sharp.svg +1 -0
  239. package/dist/ionic/svg/document-lock.svg +1 -0
  240. package/dist/ionic/svg/document-outline.svg +1 -0
  241. package/dist/types/components/accordion/accordion.d.ts +18 -1
  242. package/dist/types/components/checkbox/checkbox.d.ts +9 -2
  243. package/dist/types/components/datetime/datetime.d.ts +10 -0
  244. package/dist/types/components/header/header.utils.d.ts +10 -0
  245. package/dist/types/components/input/input.d.ts +0 -4
  246. package/dist/types/components/radio-group/radio-group.d.ts +9 -1
  247. package/dist/types/components/select/select.d.ts +7 -1
  248. package/dist/types/components/textarea/textarea.d.ts +0 -4
  249. package/dist/types/components/toggle/toggle.d.ts +7 -2
  250. package/dist/types/utils/forms/index.d.ts +1 -0
  251. package/dist/types/utils/forms/validity.d.ts +10 -0
  252. package/dist/types/utils/transition/index.d.ts +9 -0
  253. package/hydrate/index.js +687 -413
  254. package/hydrate/index.mjs +687 -413
  255. package/package.json +4 -4
  256. package/dist/ionic/p-1c8a476d.entry.js +0 -4
  257. package/dist/ionic/p-3355a2ff.entry.js +0 -4
  258. package/dist/ionic/p-4efea47a.entry.js +0 -4
  259. package/dist/ionic/p-62e50f80.entry.js +0 -4
  260. package/dist/ionic/p-785026d7.entry.js +0 -4
  261. package/dist/ionic/p-78c74a3e.entry.js +0 -4
  262. package/dist/ionic/p-7bcfc421.entry.js +0 -4
  263. package/dist/ionic/p-83fc84e7.entry.js +0 -4
  264. package/dist/ionic/p-913a7c1e.entry.js +0 -4
  265. package/dist/ionic/p-CMhMiYSX.js +0 -4
  266. package/dist/ionic/p-c17c0a01.entry.js +0 -4
@@ -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,7 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { Host, h } from "@stencil/core";
4
+ import { Build, Host, h } from "@stencil/core";
5
+ import { checkInvalidState } from "../../utils/forms/index";
5
6
  import { renderHiddenInput } from "../../utils/helpers";
6
7
  import { getIonMode } from "../../global/ionic-global";
7
8
  export class RadioGroup {
@@ -10,6 +11,10 @@ export class RadioGroup {
10
11
  this.helperTextId = `${this.inputId}-helper-text`;
11
12
  this.errorTextId = `${this.inputId}-error-text`;
12
13
  this.labelId = `${this.inputId}-lbl`;
14
+ /**
15
+ * Track validation state for proper aria-live announcements.
16
+ */
17
+ this.isInvalid = false;
13
18
  /**
14
19
  * If `true`, the radios can be deselected.
15
20
  */
@@ -91,6 +96,52 @@ export class RadioGroup {
91
96
  this.labelId = label.id = this.name + '-lbl';
92
97
  }
93
98
  }
99
+ // Watch for class changes to update validation state.
100
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
101
+ this.validationObserver = new MutationObserver(() => {
102
+ const newIsInvalid = checkInvalidState(this.el);
103
+ if (this.isInvalid !== newIsInvalid) {
104
+ this.isInvalid = newIsInvalid;
105
+ /**
106
+ * Screen readers tend to announce changes
107
+ * to `aria-describedby` when the attribute
108
+ * is changed during a blur event for a
109
+ * native form control.
110
+ * However, the announcement can be spotty
111
+ * when using a non-native form control
112
+ * and `forceUpdate()`.
113
+ * This is due to `forceUpdate()` internally
114
+ * rescheduling the DOM update to a lower
115
+ * priority queue regardless if it's called
116
+ * inside a Promise or not, thus causing
117
+ * the screen reader to potentially miss the
118
+ * change.
119
+ * By using a State variable inside a Promise,
120
+ * it guarantees a re-render immediately at
121
+ * a higher priority.
122
+ */
123
+ Promise.resolve().then(() => {
124
+ this.hintTextId = this.getHintTextId();
125
+ });
126
+ }
127
+ });
128
+ this.validationObserver.observe(this.el, {
129
+ attributes: true,
130
+ attributeFilter: ['class'],
131
+ });
132
+ }
133
+ // Always set initial state
134
+ this.isInvalid = checkInvalidState(this.el);
135
+ }
136
+ componentWillLoad() {
137
+ this.hintTextId = this.getHintTextId();
138
+ }
139
+ disconnectedCallback() {
140
+ // Clean up validation observer to prevent memory leaks.
141
+ if (this.validationObserver) {
142
+ this.validationObserver.disconnect();
143
+ this.validationObserver = undefined;
144
+ }
94
145
  }
95
146
  getRadios() {
96
147
  return Array.from(this.el.querySelectorAll('ion-radio'));
@@ -166,16 +217,16 @@ export class RadioGroup {
166
217
  * Renders the helper text or error text values
167
218
  */
168
219
  renderHintText() {
169
- const { helperText, errorText, helperTextId, errorTextId } = this;
220
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
170
221
  const hasHintText = !!helperText || !!errorText;
171
222
  if (!hasHintText) {
172
223
  return;
173
224
  }
174
- return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text" }, errorText)));
225
+ return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", role: "alert" }, isInvalid ? errorText : null)));
175
226
  }
176
- getHintTextID() {
177
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
178
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
227
+ getHintTextId() {
228
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
229
+ if (isInvalid && errorText) {
179
230
  return errorTextId;
180
231
  }
181
232
  if (helperText) {
@@ -187,7 +238,7 @@ export class RadioGroup {
187
238
  const { label, labelId, el, name, value } = this;
188
239
  const mode = getIonMode(this);
189
240
  renderHiddenInput(true, el, name, value, false);
190
- return (h(Host, { key: '81b8ebc96b2f383c36717f290d2959cc921ad6e8', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '45b09efc10776b889a8f372cba80d25a3fc849da', class: "radio-group-wrapper" }, h("slot", { key: '58714934542c2fdd7396de160364f3f06b32e8f8' }))));
241
+ return (h(Host, { key: 'db593b3ed511e9395e3c7bfd91b787328692cd6d', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '85045b45a0100a45f3b9a35d1c5a25ec63d525c4', class: "radio-group-wrapper" }, h("slot", { key: '53dacb87ce62398e78771fb2efaf839ab922d946' }))));
191
242
  }
192
243
  static get is() { return "ion-radio-group"; }
193
244
  static get originalStyleUrls() {
@@ -328,6 +379,12 @@ export class RadioGroup {
328
379
  }
329
380
  };
330
381
  }
382
+ static get states() {
383
+ return {
384
+ "isInvalid": {},
385
+ "hintTextId": {}
386
+ };
387
+ }
331
388
  static get events() {
332
389
  return [{
333
390
  "method": "ionChange",
@@ -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
- getHintTextID() {
721
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
722
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
766
+ getHintTextId() {
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"; }
@@ -1,7 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { Host, h } from "@stencil/core";
4
+ import { Build, Host, h } from "@stencil/core";
5
+ import { checkInvalidState } from "../../utils/forms/index";
5
6
  import { renderHiddenInput, inheritAriaAttributes } from "../../utils/helpers";
6
7
  import { hapticSelection } from "../../utils/native/haptic";
7
8
  import { isPlatform } from "../../utils/platform";
@@ -32,6 +33,10 @@ export class Toggle {
32
33
  this.inheritedAttributes = {};
33
34
  this.didLoad = false;
34
35
  this.activated = false;
36
+ /**
37
+ * Track validation state for proper aria-live announcements.
38
+ */
39
+ this.isInvalid = false;
35
40
  /**
36
41
  * The name of the control, which is submitted with the form data.
37
42
  */
@@ -139,22 +144,58 @@ export class Toggle {
139
144
  const { checked, value } = this;
140
145
  const isNowChecked = !checked;
141
146
  this.checked = isNowChecked;
142
- this.setFocus();
143
147
  this.ionChange.emit({
144
148
  checked: isNowChecked,
145
149
  value,
146
150
  });
147
151
  }
148
152
  async connectedCallback() {
153
+ const { didLoad, el } = this;
149
154
  /**
150
155
  * If we have not yet rendered
151
156
  * ion-toggle, then toggleTrack is not defined.
152
157
  * But if we are moving ion-toggle via appendChild,
153
158
  * then toggleTrack will be defined.
154
159
  */
155
- if (this.didLoad) {
160
+ if (didLoad) {
156
161
  this.setupGesture();
157
162
  }
163
+ // Watch for class changes to update validation state.
164
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
165
+ this.validationObserver = new MutationObserver(() => {
166
+ const newIsInvalid = checkInvalidState(el);
167
+ if (this.isInvalid !== newIsInvalid) {
168
+ this.isInvalid = newIsInvalid;
169
+ /**
170
+ * Screen readers tend to announce changes
171
+ * to `aria-describedby` when the attribute
172
+ * is changed during a blur event for a
173
+ * native form control.
174
+ * However, the announcement can be spotty
175
+ * when using a non-native form control
176
+ * and `forceUpdate()`.
177
+ * This is due to `forceUpdate()` internally
178
+ * rescheduling the DOM update to a lower
179
+ * priority queue regardless if it's called
180
+ * inside a Promise or not, thus causing
181
+ * the screen reader to potentially miss the
182
+ * change.
183
+ * By using a State variable inside a Promise,
184
+ * it guarantees a re-render immediately at
185
+ * a higher priority.
186
+ */
187
+ Promise.resolve().then(() => {
188
+ this.hintTextId = this.getHintTextId();
189
+ });
190
+ }
191
+ });
192
+ this.validationObserver.observe(el, {
193
+ attributes: true,
194
+ attributeFilter: ['class'],
195
+ });
196
+ }
197
+ // Always set initial state
198
+ this.isInvalid = checkInvalidState(el);
158
199
  }
159
200
  componentDidLoad() {
160
201
  this.setupGesture();
@@ -165,9 +206,15 @@ export class Toggle {
165
206
  this.gesture.destroy();
166
207
  this.gesture = undefined;
167
208
  }
209
+ // Clean up validation observer to prevent memory leaks.
210
+ if (this.validationObserver) {
211
+ this.validationObserver.disconnect();
212
+ this.validationObserver = undefined;
213
+ }
168
214
  }
169
215
  componentWillLoad() {
170
216
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
217
+ this.hintTextId = this.getHintTextId();
171
218
  }
172
219
  onStart() {
173
220
  this.activated = true;
@@ -190,9 +237,7 @@ export class Toggle {
190
237
  return this.value || '';
191
238
  }
192
239
  setFocus() {
193
- if (this.focusEl) {
194
- this.focusEl.focus();
195
- }
240
+ this.el.focus();
196
241
  }
197
242
  renderOnOffSwitchLabels(mode, checked) {
198
243
  const icon = this.getSwitchLabelIcon(mode, checked);
@@ -210,9 +255,9 @@ export class Toggle {
210
255
  get hasLabel() {
211
256
  return this.el.textContent !== '';
212
257
  }
213
- getHintTextID() {
214
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
215
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
258
+ getHintTextId() {
259
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
260
+ if (isInvalid && errorText) {
216
261
  return errorTextId;
217
262
  }
218
263
  if (helperText) {
@@ -225,7 +270,7 @@ export class Toggle {
225
270
  * This element should only be rendered if hint text is set.
226
271
  */
227
272
  renderHintText() {
228
- const { helperText, errorText, helperTextId, errorTextId } = this;
273
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
229
274
  /**
230
275
  * undefined and empty string values should
231
276
  * be treated as not having helper/error text.
@@ -234,15 +279,15 @@ export class Toggle {
234
279
  if (!hasHintText) {
235
280
  return;
236
281
  }
237
- return (h("div", { class: "toggle-bottom" }, h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText)));
282
+ return (h("div", { class: "toggle-bottom" }, h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null)));
238
283
  }
239
284
  render() {
240
- const { activated, alignment, checked, color, disabled, el, errorTextId, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
285
+ const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
241
286
  const mode = getIonMode(this);
242
287
  const value = this.getValue();
243
288
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
244
289
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
245
- return (h(Host, { key: '21037ea2e8326f58c84becadde475f007f931924', role: "switch", "aria-checked": `${checked}`, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === errorTextId, onClick: this.onClick, "aria-labelledby": hasLabel ? inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, class: createColorClasses(color, {
290
+ return (h(Host, { key: 'f569148edd89ee041a4719ffc4733c16b05229bd', role: "switch", "aria-checked": `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, "aria-labelledby": hasLabel ? inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, class: createColorClasses(color, {
246
291
  [mode]: true,
247
292
  'in-item': hostContext('ion-item', el),
248
293
  'toggle-activated': activated,
@@ -252,10 +297,10 @@ export class Toggle {
252
297
  [`toggle-alignment-${alignment}`]: alignment !== undefined,
253
298
  [`toggle-label-placement-${labelPlacement}`]: true,
254
299
  [`toggle-${rtl}`]: true,
255
- }) }, h("label", { key: '4d153679d118d01286f6633d1c19558a97745ff6', class: "toggle-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '0dfcd4df15b8d41bec5ff5f8912503afbb7bec53', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, onFocus: () => this.onFocus(), onBlur: () => this.onBlur(), ref: (focusEl) => (this.focusEl = focusEl), required: required }, inheritedAttributes)), h("div", { key: 'ffed3a07ba2ab70e5b232e6041bc3b6b34be8331', class: {
300
+ }) }, h("label", { key: '3027f2ac4be6de422a14486d847fbee77f615db1', class: "toggle-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '4b0304c9e879e432b80184b4e5de37d55c11b436', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), h("div", { key: '8ef265ec942e7f01ff31cbb202ed146c6bf94e02', class: {
256
301
  'label-text-wrapper': true,
257
302
  'label-text-wrapper-hidden': !hasLabel,
258
- }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: 'd88e1e3dcdd8293f6b61f237cd7a0511dcbce300' }), this.renderHintText()), h("div", { key: '0e924225f5f0caf3c88738acb6c557bd8c1b68f6', class: "native-wrapper" }, this.renderToggleControl()))));
303
+ }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '7b162b7dd27199cca2a4c995276a18b9f8e44aaf' }), this.renderHintText()), h("div", { key: 'd13c34bd42fca01cc73ddb4ea7e471b33a282a3e', class: "native-wrapper" }, this.renderToggleControl()))));
259
304
  }
260
305
  static get is() { return "ion-toggle"; }
261
306
  static get encapsulation() { return "shadow"; }
@@ -518,7 +563,9 @@ export class Toggle {
518
563
  }
519
564
  static get states() {
520
565
  return {
521
- "activated": {}
566
+ "activated": {},
567
+ "isInvalid": {},
568
+ "hintTextId": {}
522
569
  };
523
570
  }
524
571
  static get events() {
@@ -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
+ };
@@ -95,6 +95,13 @@ export const setContent = async (page, html, testInfo, options) => {
95
95
  route.continue();
96
96
  }
97
97
  });
98
+ /**
99
+ * URL query parameters cause the custom Playwright `page.route`
100
+ * interceptor to fail, which is necessary to inject the test HTML.
101
+ *
102
+ * To avoid this, the final navigation URL is kept simple by using
103
+ * hash params to ensure the route interceptor always works.
104
+ */
98
105
  await page.goto(`${baseUrl}#`, options);
99
106
  }
100
107
  };
@@ -3,6 +3,38 @@
3
3
  */
4
4
  import { addE2EListener, EventSpy } from "../event-spy";
5
5
  export const spyOnEvent = async (page, eventName) => {
6
+ /**
7
+ * Tabbing out of the page boundary can lead to unreliable `ionBlur events,
8
+ * particularly in Firefox.
9
+ *
10
+ * This occurs because Playwright may incorrectly maintain focus state on the
11
+ * last element, even after a Tab press attempts to shift focus outside the
12
+ * viewport. To reliably trigger the necessary blur event, add a visually
13
+ * hidden, focusable element at the end of the page to receive focus instead of
14
+ * the browser.
15
+ *
16
+ * Playwright issue reference:
17
+ * https://github.com/microsoft/playwright/issues/32269
18
+ */
19
+ if (eventName === 'ionBlur') {
20
+ const hiddenInput = await page.$('#hidden-input-for-ion-blur');
21
+ if (!hiddenInput) {
22
+ await page.evaluate(() => {
23
+ const input = document.createElement('input');
24
+ input.id = 'hidden-input-for-ion-blur';
25
+ input.style.position = 'absolute';
26
+ input.style.opacity = '0';
27
+ input.style.height = '0';
28
+ input.style.width = '0';
29
+ input.style.pointerEvents = 'none';
30
+ document.body.appendChild(input);
31
+ // Clean up the element when the page is unloaded.
32
+ window.addEventListener('unload', () => {
33
+ input.remove();
34
+ });
35
+ });
36
+ }
37
+ }
6
38
  const spy = new EventSpy(eventName);
7
39
  const handle = await page.evaluateHandle(() => window);
8
40
  await addE2EListener(page, handle, eventName, (ev) => spy.push(ev));