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
@@ -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"; }
@@ -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 { inheritAriaAttributes, renderHiddenInput } from "../../utils/helpers";
6
7
  import { createColorClasses, hostContext } from "../../utils/theme";
7
8
  import { getIonMode } from "../../global/ionic-global";
@@ -62,6 +63,10 @@ export class Checkbox {
62
63
  * submitting if the value is invalid.
63
64
  */
64
65
  this.required = false;
66
+ /**
67
+ * Track validation state for proper aria-live announcements.
68
+ */
69
+ this.isInvalid = false;
65
70
  /**
66
71
  * Sets the checked property and emits
67
72
  * the ionChange event. Use this to update the
@@ -77,7 +82,6 @@ export class Checkbox {
77
82
  };
78
83
  this.toggleChecked = (ev) => {
79
84
  ev.preventDefault();
80
- this.setFocus();
81
85
  this.setChecked(!this.checked);
82
86
  this.indeterminate = false;
83
87
  };
@@ -109,18 +113,63 @@ export class Checkbox {
109
113
  ev.stopPropagation();
110
114
  };
111
115
  }
116
+ connectedCallback() {
117
+ const { el } = this;
118
+ // Watch for class changes to update validation state.
119
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
120
+ this.validationObserver = new MutationObserver(() => {
121
+ const newIsInvalid = checkInvalidState(el);
122
+ if (this.isInvalid !== newIsInvalid) {
123
+ this.isInvalid = newIsInvalid;
124
+ /**
125
+ * Screen readers tend to announce changes
126
+ * to `aria-describedby` when the attribute
127
+ * is changed during a blur event for a
128
+ * native form control.
129
+ * However, the announcement can be spotty
130
+ * when using a non-native form control
131
+ * and `forceUpdate()`.
132
+ * This is due to `forceUpdate()` internally
133
+ * rescheduling the DOM update to a lower
134
+ * priority queue regardless if it's called
135
+ * inside a Promise or not, thus causing
136
+ * the screen reader to potentially miss the
137
+ * change.
138
+ * By using a State variable inside a Promise,
139
+ * it guarantees a re-render immediately at
140
+ * a higher priority.
141
+ */
142
+ Promise.resolve().then(() => {
143
+ this.hintTextId = this.getHintTextId();
144
+ });
145
+ }
146
+ });
147
+ this.validationObserver.observe(el, {
148
+ attributes: true,
149
+ attributeFilter: ['class'],
150
+ });
151
+ }
152
+ // Always set initial state
153
+ this.isInvalid = checkInvalidState(el);
154
+ }
112
155
  componentWillLoad() {
113
156
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
157
+ this.hintTextId = this.getHintTextId();
158
+ }
159
+ disconnectedCallback() {
160
+ // Clean up validation observer to prevent memory leaks.
161
+ if (this.validationObserver) {
162
+ this.validationObserver.disconnect();
163
+ this.validationObserver = undefined;
164
+ }
114
165
  }
115
166
  /** @internal */
116
167
  async setFocus() {
117
- if (this.focusEl) {
118
- this.focusEl.focus();
119
- }
168
+ this.el.focus();
120
169
  }
121
- getHintTextID() {
122
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
123
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
170
+ getHintTextId() {
171
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
172
+ if (isInvalid && errorText) {
124
173
  return errorTextId;
125
174
  }
126
175
  if (helperText) {
@@ -133,7 +182,7 @@ export class Checkbox {
133
182
  * This element should only be rendered if hint text is set.
134
183
  */
135
184
  renderHintText() {
136
- const { helperText, errorText, helperTextId, errorTextId } = this;
185
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
137
186
  /**
138
187
  * undefined and empty string values should
139
188
  * be treated as not having helper/error text.
@@ -142,7 +191,7 @@ export class Checkbox {
142
191
  if (!hasHintText) {
143
192
  return;
144
193
  }
145
- return (h("div", { class: "checkbox-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)));
194
+ return (h("div", { class: "checkbox-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)));
146
195
  }
147
196
  render() {
148
197
  const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, alignment, required, } = this;
@@ -152,7 +201,7 @@ export class Checkbox {
152
201
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
153
202
  // The host element must have a checkbox role to ensure proper VoiceOver
154
203
  // 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, {
204
+ return (h(Host, { key: 'ae0fbd4b21accbac132e6b85c513512ad9179394', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": hasLabelContent ? this.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, onClick: this.onClick, class: createColorClasses(color, {
156
205
  [mode]: true,
157
206
  'in-item': hostContext('ion-item', el),
158
207
  'checkbox-checked': checked,
@@ -162,10 +211,10 @@ export class Checkbox {
162
211
  [`checkbox-justify-${justify}`]: justify !== undefined,
163
212
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
164
213
  [`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: {
214
+ }) }, h("label", { key: '7a3d7f3c27dde514f2dbf2e34f4629fad33ec3bf', class: "checkbox-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '4130d77ddf034271fecccda14e101a5a809921b6', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), h("div", { key: '5daa74f4e62b0947e37764762524001ee42609d9', class: {
166
215
  'label-text-wrapper': true,
167
216
  '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)))));
217
+ }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '23ff66138f8c3a2f56f39113fc842d54b2f7952a' }), this.renderHintText()), h("div", { key: 'ab914d9623c19fc46821d5e62db92f1192ebbe7e', class: "native-wrapper" }, h("svg", { key: '66e3f4f5dcaa9756fb0e9452299954f9ed3dcb7b', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
169
218
  }
170
219
  getSVGPath(mode, indeterminate) {
171
220
  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" }));
@@ -433,6 +482,12 @@ export class Checkbox {
433
482
  }
434
483
  };
435
484
  }
485
+ static get states() {
486
+ return {
487
+ "isInvalid": {},
488
+ "hintTextId": {}
489
+ };
490
+ }
436
491
  static get events() {
437
492
  return [{
438
493
  "method": "ionChange",
@@ -585,6 +585,28 @@ export class Datetime {
585
585
  destroyKeyboardMO();
586
586
  }
587
587
  };
588
+ /**
589
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
590
+ * Fallback to ensure the datetime becomes ready even if
591
+ * IntersectionObserver never reports it as intersecting.
592
+ *
593
+ * This is primarily used in environments where the observer
594
+ * might not fire as expected, such as when running under
595
+ * synthetic tests that stub IntersectionObserver.
596
+ */
597
+ this.ensureReadyIfVisible = () => {
598
+ if (this.el.classList.contains('datetime-ready')) {
599
+ return;
600
+ }
601
+ const rect = this.el.getBoundingClientRect();
602
+ if (rect.width === 0 || rect.height === 0) {
603
+ return;
604
+ }
605
+ this.initializeListeners();
606
+ writeTask(() => {
607
+ this.el.classList.add('datetime-ready');
608
+ });
609
+ };
588
610
  this.processValue = (value) => {
589
611
  const hasValue = value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
590
612
  const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
@@ -902,6 +924,17 @@ export class Datetime {
902
924
  * triggering the `hiddenIO` observer below.
903
925
  */
904
926
  raf(() => visibleIO === null || visibleIO === void 0 ? void 0 : visibleIO.observe(intersectionTrackerRef));
927
+ /**
928
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
929
+ * Fallback: If IntersectionObserver never reports that the
930
+ * datetime is visible but the host clearly has layout, ensure
931
+ * we still initialize listeners and mark the component as ready.
932
+ *
933
+ * We schedule this after everything has had a chance to run.
934
+ */
935
+ setTimeout(() => {
936
+ this.ensureReadyIfVisible();
937
+ }, 100);
905
938
  /**
906
939
  * We need to clean up listeners when the datetime is hidden
907
940
  * in a popover/modal so that we can properly scroll containers
@@ -1657,7 +1690,7 @@ export class Datetime {
1657
1690
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1658
1691
  const hasWheelVariant = hasDatePresentation && preferWheel;
1659
1692
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1660
- return (h(Host, { key: '57492534800ea059a7c2bbd9f0059cc0b75ae8d2', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1693
+ return (h(Host, { key: 'efdbc0922670a841bc667ceac392cdc1dedffd01', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1661
1694
  [mode]: true,
1662
1695
  ['datetime-readonly']: readonly,
1663
1696
  ['datetime-disabled']: disabled,
@@ -1667,7 +1700,7 @@ export class Datetime {
1667
1700
  [`datetime-size-${size}`]: true,
1668
1701
  [`datetime-prefer-wheel`]: hasWheelVariant,
1669
1702
  [`datetime-grid`]: isGridStyle,
1670
- })) }, h("div", { key: '97dac5e5195635ac0bc5fb472b9d09e5c3c6bbc3', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1703
+ })) }, h("div", { key: '3f8bb75fcb0baff55182ef3aa1b535eacc58d81f', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1671
1704
  }
1672
1705
  static get is() { return "ion-datetime"; }
1673
1706
  static get encapsulation() { return "shadow"; }
@@ -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
+ };