voyager-ionic-core 8.7.6 → 8.7.9

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 (251) hide show
  1. package/components/button.js +3 -7
  2. package/components/checkbox.js +4 -7
  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-input.js +6 -14
  7. package/components/ion-select.js +58 -10
  8. package/components/ion-textarea.js +5 -13
  9. package/components/ion-toggle.js +4 -7
  10. package/components/{notch-controller.js → validity.js} +14 -1
  11. package/dist/cjs/{index-CD5Rjp23.js → index-094mMFB-.js} +76 -5
  12. package/dist/cjs/index.cjs.js +3 -3
  13. package/dist/cjs/ion-accordion_2.cjs.entry.js +91 -13
  14. package/dist/cjs/ion-app_8.cjs.entry.js +43 -5
  15. package/dist/cjs/ion-button_2.cjs.entry.js +3 -7
  16. package/dist/cjs/ion-checkbox.cjs.entry.js +4 -7
  17. package/dist/cjs/ion-input.cjs.entry.js +7 -15
  18. package/dist/cjs/ion-modal.cjs.entry.js +1 -1
  19. package/dist/cjs/ion-nav_2.cjs.entry.js +1 -1
  20. package/dist/cjs/ion-popover.cjs.entry.js +1 -1
  21. package/dist/cjs/ion-select_3.cjs.entry.js +56 -10
  22. package/dist/cjs/ion-textarea.cjs.entry.js +6 -14
  23. package/dist/cjs/ion-toggle.cjs.entry.js +4 -7
  24. package/dist/cjs/ionic.cjs.js +1 -1
  25. package/dist/cjs/{ios.transition-j9CclgEW.js → ios.transition-BOt_uW73.js} +1 -1
  26. package/dist/cjs/loader.cjs.js +1 -1
  27. package/dist/cjs/{md.transition-CwFyRSfv.js → md.transition-Dt968VXB.js} +1 -1
  28. package/dist/cjs/{notch-controller-Bzqhjm4f.js → validity-C8QoAYT2.js} +14 -0
  29. package/dist/collection/components/accordion/accordion.js +93 -14
  30. package/dist/collection/components/button/button.js +3 -7
  31. package/dist/collection/components/checkbox/checkbox.js +4 -7
  32. package/dist/collection/components/header/header.ios.css +27 -1
  33. package/dist/collection/components/header/header.js +5 -4
  34. package/dist/collection/components/header/header.utils.js +37 -0
  35. package/dist/collection/components/input/input.js +6 -14
  36. package/dist/collection/components/select/select.js +59 -11
  37. package/dist/collection/components/textarea/textarea.js +5 -13
  38. package/dist/collection/components/toggle/toggle.js +4 -7
  39. package/dist/collection/utils/forms/index.js +1 -0
  40. package/dist/collection/utils/forms/validity.js +15 -0
  41. package/dist/collection/utils/test/playwright/page/utils/spy-on-event.js +32 -0
  42. package/dist/collection/utils/transition/index.js +74 -3
  43. package/dist/docs.json +1 -1
  44. package/dist/esm/{index-D6G2seR8.js → index-r2D9DEro.js} +76 -5
  45. package/dist/esm/index.js +3 -3
  46. package/dist/esm/ion-accordion_2.entry.js +91 -13
  47. package/dist/esm/ion-app_8.entry.js +43 -5
  48. package/dist/esm/ion-button_2.entry.js +3 -7
  49. package/dist/esm/ion-checkbox.entry.js +4 -7
  50. package/dist/esm/ion-input.entry.js +6 -14
  51. package/dist/esm/ion-modal.entry.js +1 -1
  52. package/dist/esm/ion-nav_2.entry.js +1 -1
  53. package/dist/esm/ion-popover.entry.js +1 -1
  54. package/dist/esm/ion-select_3.entry.js +55 -9
  55. package/dist/esm/ion-textarea.entry.js +5 -13
  56. package/dist/esm/ion-toggle.entry.js +4 -7
  57. package/dist/esm/ionic.js +1 -1
  58. package/dist/esm/{ios.transition-Bpq9ixwv.js → ios.transition-BDzw0_Hm.js} +1 -1
  59. package/dist/esm/loader.js +1 -1
  60. package/dist/esm/{md.transition-zOA0oanq.js → md.transition-BzDYi3qq.js} +1 -1
  61. package/dist/esm/{notch-controller-BwelN_JM.js → validity-B8oWougr.js} +14 -1
  62. package/dist/ionic/index.esm.js +1 -1
  63. package/dist/ionic/ionic.esm.js +1 -1
  64. package/dist/ionic/p-43ed1ef5.entry.js +4 -0
  65. package/dist/ionic/p-4cc26913.entry.js +4 -0
  66. package/dist/ionic/{p-323421af.entry.js → p-5a39a99a.entry.js} +1 -1
  67. package/dist/ionic/p-5fb517e4.entry.js +4 -0
  68. package/dist/ionic/p-8bdfc8f6.entry.js +4 -0
  69. package/dist/ionic/{p-9a36e2e7.entry.js → p-95bddd49.entry.js} +1 -1
  70. package/dist/ionic/{p-DPhQmGJN.js → p-C7hRNDhM.js} +1 -1
  71. package/dist/ionic/p-DUt5fQmA.js +4 -0
  72. package/dist/ionic/{p-9R1XyICs.js → p-DZRJwG4S.js} +1 -1
  73. package/dist/ionic/{p-DCv9sLH2.js → p-DieJyvMP.js} +1 -1
  74. package/dist/ionic/p-d0a2a1ab.entry.js +4 -0
  75. package/dist/ionic/p-dc2e126d.entry.js +4 -0
  76. package/dist/ionic/{p-de7b5fa3.entry.js → p-e16b69e1.entry.js} +1 -1
  77. package/dist/ionic/p-f65f9308.entry.js +4 -0
  78. package/dist/ionic/p-fc278823.entry.js +4 -0
  79. package/dist/ionic/svg/checkbox-outline.svg +1 -0
  80. package/dist/ionic/svg/checkbox-sharp.svg +1 -0
  81. package/dist/ionic/svg/checkbox.svg +1 -0
  82. package/dist/ionic/svg/checkmark-circle-outline.svg +1 -0
  83. package/dist/ionic/svg/checkmark-circle-sharp.svg +1 -0
  84. package/dist/ionic/svg/checkmark-circle.svg +1 -0
  85. package/dist/ionic/svg/checkmark-done-circle-outline.svg +1 -0
  86. package/dist/ionic/svg/checkmark-done-circle-sharp.svg +1 -0
  87. package/dist/ionic/svg/checkmark-done-circle.svg +1 -0
  88. package/dist/ionic/svg/checkmark-done-outline.svg +1 -0
  89. package/dist/ionic/svg/checkmark-done-sharp.svg +1 -0
  90. package/dist/ionic/svg/checkmark-done.svg +1 -0
  91. package/dist/ionic/svg/checkmark-outline.svg +1 -0
  92. package/dist/ionic/svg/checkmark-sharp.svg +1 -0
  93. package/dist/ionic/svg/checkmark.svg +1 -0
  94. package/dist/ionic/svg/chevron-back-circle-outline.svg +1 -0
  95. package/dist/ionic/svg/chevron-back-circle-sharp.svg +1 -0
  96. package/dist/ionic/svg/chevron-back-circle.svg +1 -0
  97. package/dist/ionic/svg/chevron-back-outline.svg +1 -0
  98. package/dist/ionic/svg/chevron-back-sharp.svg +1 -0
  99. package/dist/ionic/svg/chevron-back.svg +1 -0
  100. package/dist/ionic/svg/chevron-collapse-outline.svg +1 -0
  101. package/dist/ionic/svg/chevron-collapse-sharp.svg +1 -0
  102. package/dist/ionic/svg/chevron-collapse.svg +1 -0
  103. package/dist/ionic/svg/chevron-down-circle-outline.svg +1 -0
  104. package/dist/ionic/svg/chevron-down-circle-sharp.svg +1 -0
  105. package/dist/ionic/svg/chevron-down-circle.svg +1 -0
  106. package/dist/ionic/svg/chevron-down-outline.svg +1 -0
  107. package/dist/ionic/svg/chevron-down-sharp.svg +1 -0
  108. package/dist/ionic/svg/chevron-down.svg +1 -0
  109. package/dist/ionic/svg/chevron-expand-outline.svg +1 -0
  110. package/dist/ionic/svg/chevron-expand-sharp.svg +1 -0
  111. package/dist/ionic/svg/chevron-expand.svg +1 -0
  112. package/dist/ionic/svg/chevron-forward-circle-outline.svg +1 -0
  113. package/dist/ionic/svg/chevron-forward-circle-sharp.svg +1 -0
  114. package/dist/ionic/svg/chevron-forward-circle.svg +1 -0
  115. package/dist/ionic/svg/chevron-forward-outline.svg +1 -0
  116. package/dist/ionic/svg/chevron-forward-sharp.svg +1 -0
  117. package/dist/ionic/svg/chevron-forward.svg +1 -0
  118. package/dist/ionic/svg/chevron-up-circle-outline.svg +1 -0
  119. package/dist/ionic/svg/chevron-up-circle-sharp.svg +1 -0
  120. package/dist/ionic/svg/chevron-up-circle.svg +1 -0
  121. package/dist/ionic/svg/chevron-up-outline.svg +1 -0
  122. package/dist/ionic/svg/chevron-up-sharp.svg +1 -0
  123. package/dist/ionic/svg/chevron-up.svg +1 -0
  124. package/dist/ionic/svg/clipboard-outline.svg +1 -0
  125. package/dist/ionic/svg/clipboard-sharp.svg +1 -0
  126. package/dist/ionic/svg/clipboard.svg +1 -0
  127. package/dist/ionic/svg/close-circle-outline.svg +1 -0
  128. package/dist/ionic/svg/close-circle-sharp.svg +1 -0
  129. package/dist/ionic/svg/close-circle.svg +1 -0
  130. package/dist/ionic/svg/close-outline.svg +1 -0
  131. package/dist/ionic/svg/close-sharp.svg +1 -0
  132. package/dist/ionic/svg/close.svg +1 -0
  133. package/dist/ionic/svg/cloud-circle-outline.svg +1 -0
  134. package/dist/ionic/svg/cloud-circle-sharp.svg +1 -0
  135. package/dist/ionic/svg/cloud-circle.svg +1 -0
  136. package/dist/ionic/svg/cloud-done-outline.svg +1 -0
  137. package/dist/ionic/svg/cloud-done-sharp.svg +1 -0
  138. package/dist/ionic/svg/cloud-done.svg +1 -0
  139. package/dist/ionic/svg/cloud-download-outline.svg +1 -0
  140. package/dist/ionic/svg/cloud-download-sharp.svg +1 -0
  141. package/dist/ionic/svg/cloud-download.svg +1 -0
  142. package/dist/ionic/svg/cloud-offline-outline.svg +1 -0
  143. package/dist/ionic/svg/cloud-offline-sharp.svg +1 -0
  144. package/dist/ionic/svg/cloud-offline.svg +1 -0
  145. package/dist/ionic/svg/cloud-outline.svg +1 -0
  146. package/dist/ionic/svg/cloud-sharp.svg +1 -0
  147. package/dist/ionic/svg/cloud-upload-outline.svg +1 -0
  148. package/dist/ionic/svg/cloud-upload-sharp.svg +1 -0
  149. package/dist/ionic/svg/cloud-upload.svg +1 -0
  150. package/dist/ionic/svg/cloud.svg +1 -0
  151. package/dist/ionic/svg/cloudy-night-outline.svg +1 -0
  152. package/dist/ionic/svg/cloudy-night-sharp.svg +1 -0
  153. package/dist/ionic/svg/cloudy-night.svg +1 -0
  154. package/dist/ionic/svg/cloudy-outline.svg +1 -0
  155. package/dist/ionic/svg/cloudy-sharp.svg +1 -0
  156. package/dist/ionic/svg/cloudy.svg +1 -0
  157. package/dist/ionic/svg/code-download-outline.svg +1 -0
  158. package/dist/ionic/svg/code-download-sharp.svg +1 -0
  159. package/dist/ionic/svg/code-download.svg +1 -0
  160. package/dist/ionic/svg/code-outline.svg +1 -0
  161. package/dist/ionic/svg/code-sharp.svg +1 -0
  162. package/dist/ionic/svg/code-slash-outline.svg +1 -0
  163. package/dist/ionic/svg/code-slash-sharp.svg +1 -0
  164. package/dist/ionic/svg/code-slash.svg +1 -0
  165. package/dist/ionic/svg/code-working-outline.svg +1 -0
  166. package/dist/ionic/svg/code-working-sharp.svg +1 -0
  167. package/dist/ionic/svg/code-working.svg +1 -0
  168. package/dist/ionic/svg/code.svg +1 -0
  169. package/dist/ionic/svg/cog-outline.svg +1 -0
  170. package/dist/ionic/svg/cog-sharp.svg +1 -0
  171. package/dist/ionic/svg/cog.svg +1 -0
  172. package/dist/ionic/svg/color-fill-outline.svg +1 -0
  173. package/dist/ionic/svg/color-fill-sharp.svg +1 -0
  174. package/dist/ionic/svg/color-fill.svg +1 -0
  175. package/dist/ionic/svg/color-filter-outline.svg +1 -0
  176. package/dist/ionic/svg/color-filter-sharp.svg +1 -0
  177. package/dist/ionic/svg/color-filter.svg +1 -0
  178. package/dist/ionic/svg/color-palette-outline.svg +1 -0
  179. package/dist/ionic/svg/color-palette-sharp.svg +1 -0
  180. package/dist/ionic/svg/color-palette.svg +1 -0
  181. package/dist/ionic/svg/color-wand-outline.svg +1 -0
  182. package/dist/ionic/svg/color-wand-sharp.svg +1 -0
  183. package/dist/ionic/svg/color-wand.svg +1 -0
  184. package/dist/ionic/svg/compass-outline.svg +1 -0
  185. package/dist/ionic/svg/compass-sharp.svg +1 -0
  186. package/dist/ionic/svg/compass.svg +1 -0
  187. package/dist/ionic/svg/construct-outline.svg +1 -0
  188. package/dist/ionic/svg/construct-sharp.svg +1 -0
  189. package/dist/ionic/svg/construct.svg +1 -0
  190. package/dist/ionic/svg/contract-outline.svg +1 -0
  191. package/dist/ionic/svg/contract-sharp.svg +1 -0
  192. package/dist/ionic/svg/contract.svg +1 -0
  193. package/dist/ionic/svg/contrast-outline.svg +1 -0
  194. package/dist/ionic/svg/contrast-sharp.svg +1 -0
  195. package/dist/ionic/svg/contrast.svg +1 -0
  196. package/dist/ionic/svg/copy-outline.svg +1 -0
  197. package/dist/ionic/svg/copy-sharp.svg +1 -0
  198. package/dist/ionic/svg/copy.svg +1 -0
  199. package/dist/ionic/svg/create-outline.svg +1 -0
  200. package/dist/ionic/svg/create-sharp.svg +1 -0
  201. package/dist/ionic/svg/create.svg +1 -0
  202. package/dist/ionic/svg/crop-outline.svg +1 -0
  203. package/dist/ionic/svg/crop-sharp.svg +1 -0
  204. package/dist/ionic/svg/crop.svg +1 -0
  205. package/dist/ionic/svg/cube-outline.svg +1 -0
  206. package/dist/ionic/svg/cube-sharp.svg +1 -0
  207. package/dist/ionic/svg/cube.svg +1 -0
  208. package/dist/ionic/svg/cut-outline.svg +1 -0
  209. package/dist/ionic/svg/cut-sharp.svg +1 -0
  210. package/dist/ionic/svg/cut.svg +1 -0
  211. package/dist/ionic/svg/desktop-outline.svg +1 -0
  212. package/dist/ionic/svg/desktop-sharp.svg +1 -0
  213. package/dist/ionic/svg/desktop.svg +1 -0
  214. package/dist/ionic/svg/diamond-outline.svg +1 -0
  215. package/dist/ionic/svg/diamond-sharp.svg +1 -0
  216. package/dist/ionic/svg/diamond.svg +1 -0
  217. package/dist/ionic/svg/dice-outline.svg +1 -0
  218. package/dist/ionic/svg/dice-sharp.svg +1 -0
  219. package/dist/ionic/svg/dice.svg +1 -0
  220. package/dist/ionic/svg/disc-outline.svg +1 -0
  221. package/dist/ionic/svg/disc-sharp.svg +1 -0
  222. package/dist/ionic/svg/disc.svg +1 -0
  223. package/dist/ionic/svg/document-attach-outline.svg +1 -0
  224. package/dist/ionic/svg/document-attach-sharp.svg +1 -0
  225. package/dist/ionic/svg/document-attach.svg +1 -0
  226. package/dist/ionic/svg/document-lock-outline.svg +1 -0
  227. package/dist/ionic/svg/document-lock-sharp.svg +1 -0
  228. package/dist/ionic/svg/document-lock.svg +1 -0
  229. package/dist/ionic/svg/document-outline.svg +1 -0
  230. package/dist/types/components/accordion/accordion.d.ts +18 -1
  231. package/dist/types/components/checkbox/checkbox.d.ts +0 -1
  232. package/dist/types/components/header/header.utils.d.ts +10 -0
  233. package/dist/types/components/input/input.d.ts +0 -4
  234. package/dist/types/components/select/select.d.ts +6 -0
  235. package/dist/types/components/textarea/textarea.d.ts +0 -4
  236. package/dist/types/components/toggle/toggle.d.ts +0 -1
  237. package/dist/types/utils/forms/index.d.ts +1 -0
  238. package/dist/types/utils/forms/validity.d.ts +10 -0
  239. package/dist/types/utils/transition/index.d.ts +9 -0
  240. package/hydrate/index.js +262 -73
  241. package/hydrate/index.mjs +262 -73
  242. package/package.json +3 -3
  243. package/dist/ionic/p-1c8a476d.entry.js +0 -4
  244. package/dist/ionic/p-3355a2ff.entry.js +0 -4
  245. package/dist/ionic/p-62e50f80.entry.js +0 -4
  246. package/dist/ionic/p-785026d7.entry.js +0 -4
  247. package/dist/ionic/p-78c74a3e.entry.js +0 -4
  248. package/dist/ionic/p-83fc84e7.entry.js +0 -4
  249. package/dist/ionic/p-913a7c1e.entry.js +0 -4
  250. package/dist/ionic/p-CMhMiYSX.js +0 -4
  251. package/dist/ionic/p-c17c0a01.entry.js +0 -4
@@ -21,10 +21,57 @@ import { getIonMode } from "../../global/ionic-global";
21
21
  */
22
22
  export class Accordion {
23
23
  constructor() {
24
- this.updateListener = () => this.updateState(false);
24
+ this.accordionGroupUpdateHandler = () => {
25
+ /**
26
+ * Determine if this update will cause an actual state change.
27
+ * We only want to mark as "interacted" if the state is changing.
28
+ */
29
+ const accordionGroup = this.accordionGroupEl;
30
+ if (accordionGroup) {
31
+ const value = accordionGroup.value;
32
+ const accordionValue = this.value;
33
+ const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
34
+ const isExpanded = this.state === 4 /* AccordionState.Expanded */ || this.state === 8 /* AccordionState.Expanding */;
35
+ const stateWillChange = shouldExpand !== isExpanded;
36
+ /**
37
+ * Only mark as interacted if:
38
+ * 1. This is not the first update we've received with a defined value
39
+ * 2. The state is actually changing (prevents redundant updates from enabling animations)
40
+ */
41
+ if (this.hasReceivedFirstUpdate && stateWillChange) {
42
+ this.hasInteracted = true;
43
+ }
44
+ /**
45
+ * Only count this as the first update if the group value is defined.
46
+ * This prevents the initial undefined value from the group's componentDidLoad
47
+ * from being treated as the first real update.
48
+ */
49
+ if (value !== undefined) {
50
+ this.hasReceivedFirstUpdate = true;
51
+ }
52
+ }
53
+ this.updateState();
54
+ };
25
55
  this.state = 1 /* AccordionState.Collapsed */;
26
56
  this.isNext = false;
27
57
  this.isPrevious = false;
58
+ /**
59
+ * Tracks whether a user-initiated interaction has occurred.
60
+ * Animations are disabled until the first interaction happens.
61
+ * This prevents the accordion from animating when it's programmatically
62
+ * set to an expanded or collapsed state on initial load.
63
+ */
64
+ this.hasInteracted = false;
65
+ /**
66
+ * Tracks if this accordion has ever been expanded.
67
+ * Used to prevent the first expansion from animating.
68
+ */
69
+ this.hasEverBeenExpanded = false;
70
+ /**
71
+ * Tracks if this accordion has received its first update from the group.
72
+ * Used to distinguish initial programmatic sets from user interactions.
73
+ */
74
+ this.hasReceivedFirstUpdate = false;
28
75
  /**
29
76
  * The value of the accordion. Defaults to an autogenerated
30
77
  * value.
@@ -129,10 +176,15 @@ export class Accordion {
129
176
  iconEl.setAttribute('aria-hidden', 'true');
130
177
  ionItem.appendChild(iconEl);
131
178
  };
132
- this.expandAccordion = (initialUpdate = false) => {
179
+ this.expandAccordion = () => {
133
180
  const { contentEl, contentElWrapper } = this;
134
- if (initialUpdate || contentEl === undefined || contentElWrapper === undefined) {
181
+ /**
182
+ * If the content elements aren't available yet, just set the state.
183
+ * This happens on initial render before the DOM is ready.
184
+ */
185
+ if (contentEl === undefined || contentElWrapper === undefined) {
135
186
  this.state = 4 /* AccordionState.Expanded */;
187
+ this.hasEverBeenExpanded = true;
136
188
  return;
137
189
  }
138
190
  if (this.state === 4 /* AccordionState.Expanded */) {
@@ -141,6 +193,11 @@ export class Accordion {
141
193
  if (this.currentRaf !== undefined) {
142
194
  cancelAnimationFrame(this.currentRaf);
143
195
  }
196
+ /**
197
+ * Mark that this accordion has been expanded at least once.
198
+ * This allows subsequent expansions to animate.
199
+ */
200
+ this.hasEverBeenExpanded = true;
144
201
  if (this.shouldAnimate()) {
145
202
  raf(() => {
146
203
  this.state = 8 /* AccordionState.Expanding */;
@@ -158,9 +215,13 @@ export class Accordion {
158
215
  this.state = 4 /* AccordionState.Expanded */;
159
216
  }
160
217
  };
161
- this.collapseAccordion = (initialUpdate = false) => {
218
+ this.collapseAccordion = () => {
162
219
  const { contentEl } = this;
163
- if (initialUpdate || contentEl === undefined) {
220
+ /**
221
+ * If the content element isn't available yet, just set the state.
222
+ * This happens on initial render before the DOM is ready.
223
+ */
224
+ if (contentEl === undefined) {
164
225
  this.state = 1 /* AccordionState.Collapsed */;
165
226
  return;
166
227
  }
@@ -195,6 +256,18 @@ export class Accordion {
195
256
  * of what is set in the config.
196
257
  */
197
258
  this.shouldAnimate = () => {
259
+ /**
260
+ * Don't animate until after the first user interaction.
261
+ * This prevents animations on initial load when accordions
262
+ * start in an expanded or collapsed state programmatically.
263
+ *
264
+ * Additionally, don't animate the very first expansion even if
265
+ * hasInteracted is true. This handles edge cases like React StrictMode
266
+ * where effects run twice and might incorrectly mark as interacted.
267
+ */
268
+ if (!this.hasInteracted || !this.hasEverBeenExpanded) {
269
+ return false;
270
+ }
198
271
  if (typeof window === 'undefined') {
199
272
  return false;
200
273
  }
@@ -211,7 +284,7 @@ export class Accordion {
211
284
  }
212
285
  return true;
213
286
  };
214
- this.updateState = async (initialUpdate = false) => {
287
+ this.updateState = async () => {
215
288
  const accordionGroup = this.accordionGroupEl;
216
289
  const accordionValue = this.value;
217
290
  if (!accordionGroup) {
@@ -220,11 +293,11 @@ export class Accordion {
220
293
  const value = accordionGroup.value;
221
294
  const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
222
295
  if (shouldExpand) {
223
- this.expandAccordion(initialUpdate);
296
+ this.expandAccordion();
224
297
  this.isNext = this.isPrevious = false;
225
298
  }
226
299
  else {
227
- this.collapseAccordion(initialUpdate);
300
+ this.collapseAccordion();
228
301
  /**
229
302
  * When using popout or inset,
230
303
  * the collapsed accordion items
@@ -272,14 +345,14 @@ export class Accordion {
272
345
  var _a;
273
346
  const accordionGroupEl = (this.accordionGroupEl = (_a = this.el) === null || _a === void 0 ? void 0 : _a.closest('ion-accordion-group'));
274
347
  if (accordionGroupEl) {
275
- this.updateState(true);
276
- addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
348
+ this.updateState();
349
+ addEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
277
350
  }
278
351
  }
279
352
  disconnectedCallback() {
280
353
  const accordionGroupEl = this.accordionGroupEl;
281
354
  if (accordionGroupEl) {
282
- removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
355
+ removeEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
283
356
  }
284
357
  }
285
358
  componentDidLoad() {
@@ -303,6 +376,11 @@ export class Accordion {
303
376
  const { accordionGroupEl, disabled, readonly, value, state } = this;
304
377
  if (disabled || readonly)
305
378
  return;
379
+ /**
380
+ * Mark that the user has interacted with the accordion.
381
+ * This enables animations for all future state changes.
382
+ */
383
+ this.hasInteracted = true;
306
384
  if (accordionGroupEl) {
307
385
  /**
308
386
  * Because the accordion group may or may
@@ -323,7 +401,7 @@ export class Accordion {
323
401
  const headerPart = expanded ? 'header expanded' : 'header';
324
402
  const contentPart = expanded ? 'content expanded' : 'content';
325
403
  this.setAria(expanded);
326
- return (h(Host, { key: '073e1d02c18dcbc20c68648426e87c14750c031d', class: {
404
+ return (h(Host, { key: '9c90bce01eff7e5774a19f69c872f3761d66cf3c', class: {
327
405
  [mode]: true,
328
406
  'accordion-expanding': this.state === 8 /* AccordionState.Expanding */,
329
407
  'accordion-expanded': this.state === 4 /* AccordionState.Expanded */,
@@ -334,7 +412,7 @@ export class Accordion {
334
412
  'accordion-disabled': disabled,
335
413
  'accordion-readonly': readonly,
336
414
  'accordion-animated': this.shouldAnimate(),
337
- } }, h("div", { key: '9b4cf326de8bb6b4033992903c0c1bfd7eea9bcc', onClick: () => this.toggleExpanded(), id: "header", part: headerPart, "aria-controls": "content", ref: (headerEl) => (this.headerEl = headerEl) }, h("slot", { key: '464c32a37f64655eacf4218284214f5f30b14a1e', name: "header" })), h("div", { key: '8bb52e6a62d7de0106b253201a89a32e79d9a594', id: "content", part: contentPart, role: "region", "aria-labelledby": "header", ref: (contentEl) => (this.contentEl = contentEl) }, h("div", { key: '1d9dfd952ad493754aaeea7a8f625b33c2dd90a0', id: "content-wrapper", ref: (contentElWrapper) => (this.contentElWrapper = contentElWrapper) }, h("slot", { key: '970dfbc55a612d739d0ca3b7b1a08e5c96d0c479', name: "content" })))));
415
+ } }, h("div", { key: 'cab40d5bcf3c93fd78e70b6d3906a541e725837d', onClick: () => this.toggleExpanded(), id: "header", part: headerPart, "aria-controls": "content", ref: (headerEl) => (this.headerEl = headerEl) }, h("slot", { key: '672bc7fb3f9e18076b41e20fc9eaeab7cafcf3a2', name: "header" })), h("div", { key: 'fd777ca5b4ab04aa4f44c339d58c8cd987c52bcb', id: "content", part: contentPart, role: "region", "aria-labelledby": "header", ref: (contentEl) => (this.contentEl = contentEl) }, h("div", { key: '0aad70a71e2cd2c16b2e98fa0bdd40421d95fe16', id: "content-wrapper", ref: (contentElWrapper) => (this.contentElWrapper = contentElWrapper) }, h("slot", { key: 'd630e10ac7c56b4dbf943b523f26759b83aead55', name: "content" })))));
338
416
  }
339
417
  static get is() { return "ion-accordion"; }
340
418
  static get encapsulation() { return "shadow"; }
@@ -459,7 +537,8 @@ export class Accordion {
459
537
  return {
460
538
  "state": {},
461
539
  "isNext": {},
462
- "isPrevious": {}
540
+ "isPrevious": {},
541
+ "hasInteracted": {}
463
542
  };
464
543
  }
465
544
  static get elementRef() { return "el"; }
@@ -216,11 +216,7 @@ export class Button {
216
216
  target,
217
217
  };
218
218
  let fill = this.fill;
219
- /**
220
- * We check both undefined and null to
221
- * work around https://github.com/ionic-team/stencil/issues/3586.
222
- */
223
- if (fill == null) {
219
+ if (fill === undefined) {
224
220
  fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
225
221
  }
226
222
  /**
@@ -233,7 +229,7 @@ export class Button {
233
229
  {
234
230
  type !== 'button' && this.renderHiddenButton();
235
231
  }
236
- return (h(Host, { key: 'b105ad09215adb3ca2298acdadf0dc9154bbb9b0', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
232
+ return (h(Host, { key: 'ed82ea53705523f9afc5f1a9addff44cc6424f27', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
237
233
  [mode]: true,
238
234
  [buttonType]: true,
239
235
  [`${buttonType}-${expand}`]: expand !== undefined,
@@ -248,7 +244,7 @@ export class Button {
248
244
  'button-disabled': disabled,
249
245
  'ion-activatable': true,
250
246
  'ion-focusable': true,
251
- }) }, h(TagType, Object.assign({ key: '66b4e7112bcb9e41d5a723fbbadb0a3104f9ee1d' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '1439fc3da280221028dcf7ce8ec9dab273c4d4bb', class: "button-inner" }, h("slot", { key: 'd5269ae1afc87ec7b99746032f59cbae93720a9f', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '461c83e97aa246aa86d83e14f1e15a288d35041e', name: "start" }), h("slot", { key: '807170d47101f9f6a333dd4ff489c89284f306fe' }), h("slot", { key: 'e67f116dd0349a0d27893e4f3ff0ccef1d402f80', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: '273f0bd9645a36c1bfd18a5c2ab4f81e22b7b989', type: this.rippleType }))));
247
+ }) }, h(TagType, Object.assign({ key: 'fadec13053469dd0405bbbc61b70ced568aa4826' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '6bf0e5144fb1148002e88038522402b789689d2c', class: "button-inner" }, h("slot", { key: '25da0ca155cfa9e2754842c34f4fd09f576ac2d2', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '51414065bb11953ec9d818f8d9353589bc9072c5', name: "start" }), h("slot", { key: 'c9b5f8842aeabd20628df2f4600f1257ea913d8d' }), h("slot", { key: '478dd3671c7be1909fc84e672f0fa8dfe6082263', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: 'e1d55f85a55144d743f58a5914cd116cb065fa8c', type: this.rippleType }))));
252
248
  }
253
249
  static get is() { return "ion-button"; }
254
250
  static get encapsulation() { return "shadow"; }
@@ -77,7 +77,6 @@ export class Checkbox {
77
77
  };
78
78
  this.toggleChecked = (ev) => {
79
79
  ev.preventDefault();
80
- this.setFocus();
81
80
  this.setChecked(!this.checked);
82
81
  this.indeterminate = false;
83
82
  };
@@ -114,9 +113,7 @@ export class Checkbox {
114
113
  }
115
114
  /** @internal */
116
115
  async setFocus() {
117
- if (this.focusEl) {
118
- this.focusEl.focus();
119
- }
116
+ this.el.focus();
120
117
  }
121
118
  getHintTextID() {
122
119
  const { el, helperText, errorText, helperTextId, errorTextId } = this;
@@ -152,7 +149,7 @@ export class Checkbox {
152
149
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
153
150
  // The host element must have a checkbox role to ensure proper VoiceOver
154
151
  // support in Safari for accessibility.
155
- return (h(Host, { key: '26cbe7220e555107200e9b5deeae754aa534a80b', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, "aria-labelledby": hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, class: createColorClasses(color, {
152
+ return (h(Host, { key: 'ee2e02d28f9d15a1ec746609f7e9559444f621e5', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, "aria-labelledby": hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses(color, {
156
153
  [mode]: true,
157
154
  'in-item': hostContext('ion-item', el),
158
155
  'checkbox-checked': checked,
@@ -162,10 +159,10 @@ export class Checkbox {
162
159
  [`checkbox-justify-${justify}`]: justify !== undefined,
163
160
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
164
161
  [`checkbox-label-placement-${labelPlacement}`]: true,
165
- }), onClick: this.onClick }, h("label", { key: 'f025cec5ff08e8be4487b9cc0324616ca5dfae2a', class: "checkbox-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: 'dc53f7e4e240dc2e18556e6350df2b5c3169f553', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, onFocus: () => this.onFocus(), onBlur: () => this.onBlur(), ref: (focusEl) => (this.focusEl = focusEl), required: required }, inheritedAttributes)), h("div", { key: 'a625e9b50c3b617de8bbbfd624d772454fecaf2d', class: {
162
+ }) }, h("label", { key: '84d4c33da0348dc65ad36fb0fafd48be366dcf3b', class: "checkbox-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '427db69a3ab8a17aa0867519c90f585b8930406b', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), h("div", { key: '9dda7024b3a4f1ee55351f783f9a10f9b4ad0d12', class: {
166
163
  'label-text-wrapper': true,
167
164
  'label-text-wrapper-hidden': !hasLabelContent,
168
- }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '87d1a90691327945f4343406706e4ab27f453844' }), this.renderHintText()), h("div", { key: 'b57fed8cdecee4df1ef0d57f157267ee77fac653', class: "native-wrapper" }, h("svg", { key: '13a8aac044d46dc99e3b60a1a643785511f216ac', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
165
+ }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: 'f9d1d545ffd4164b650808241b51ea1bedc6a42c' }), this.renderHintText()), h("div", { key: 'a96d61ac324864228f14caa0e9f2c0d15418882e', class: "native-wrapper" }, h("svg", { key: '64ff3e4d87e190601811ef64323edec18d510cd1', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
169
166
  }
170
167
  getSVGPath(mode, indeterminate) {
171
168
  let path = indeterminate ? (h("path", { d: "M6 12L18 12", part: "mark" })) : (h("path", { d: "M5.9,12.5l3.8,3.8l8.8-8.8", part: "mark" }));
@@ -152,6 +152,15 @@ ion-header ion-toolbar:first-of-type {
152
152
  --opacity-scale: inherit;
153
153
  }
154
154
 
155
+ /**
156
+ * Override styles applied during the page transition to prevent
157
+ * header flickering.
158
+ */
159
+ .header-collapse-fade.header-transitioning ion-toolbar {
160
+ --background: transparent;
161
+ --border-style: none;
162
+ }
163
+
155
164
  .header-collapse-condense {
156
165
  z-index: 9;
157
166
  }
@@ -175,7 +184,6 @@ ion-header ion-toolbar:first-of-type {
175
184
  * since it needs to blend in with the header above it.
176
185
  */
177
186
  .header-collapse-condense ion-toolbar {
178
- --background: var(--ion-background-color, #fff);
179
187
  z-index: 0;
180
188
  }
181
189
 
@@ -201,6 +209,24 @@ ion-header ion-toolbar:first-of-type {
201
209
  transition: all 0.2s ease-in-out;
202
210
  }
203
211
 
212
+ /**
213
+ * Large title toolbar should just use the content background
214
+ * since it needs to blend in with the header above it.
215
+ */
216
+ .header-collapse-condense ion-toolbar,
217
+ .header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
218
+ --background: var(--ion-background-color, #fff);
219
+ }
220
+
221
+ /**
222
+ * Override styles applied during the page transition to prevent
223
+ * header flickering.
224
+ */
225
+ .header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
226
+ --border-style: none;
227
+ --opacity-scale: 1;
228
+ }
229
+
204
230
  .header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
205
231
  .header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
206
232
  opacity: 0;
@@ -6,7 +6,7 @@ import { findIonContent, getScrollElement, printIonContentErrorMsg } from "../..
6
6
  import { inheritAriaAttributes } from "../../utils/helpers";
7
7
  import { hostContext } from "../../utils/theme";
8
8
  import { getIonMode } from "../../global/ionic-global";
9
- import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, } from "./header.utils";
9
+ import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, getRoleType, } from "./header.utils";
10
10
  /**
11
11
  * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
12
12
  */
@@ -145,16 +145,17 @@ export class Header {
145
145
  const { translucent, inheritedAttributes } = this;
146
146
  const mode = getIonMode(this);
147
147
  const collapse = this.collapse || 'none';
148
+ const isCondensed = collapse === 'condense';
148
149
  // banner role must be at top level, so remove role if inside a menu
149
- const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner';
150
- return (h(Host, Object.assign({ key: 'b6cc27f0b08afc9fcc889683525da765d80ba672', role: roleType, class: {
150
+ const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode);
151
+ return (h(Host, Object.assign({ key: '863c4568cd7b8c0ec55109f193bbbaed68a1346e', role: roleType, class: {
151
152
  [mode]: true,
152
153
  // Used internally for styling
153
154
  [`header-${mode}`]: true,
154
155
  [`header-translucent`]: this.translucent,
155
156
  [`header-collapse-${collapse}`]: true,
156
157
  [`header-translucent-${mode}`]: this.translucent,
157
- } }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '395766d4dcee3398bc91960db21f922095292f14', class: "header-background" }), h("slot", { key: '09a67ece27b258ff1248805d43d92a49b2c6859a' })));
158
+ } }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '25c3bdce328b0b35607d154c8b8374679313d881', class: "header-background" }), h("slot", { key: 'b44fab0a9be7920b9650da26117c783e751e1702' })));
158
159
  }
159
160
  static get is() { return "ion-header"; }
160
161
  static get originalStyleUrls() {
@@ -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() {