@ionic/core 8.7.10-nightly.20251111 → 8.7.10-nightly.20251119

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 (56) hide show
  1. package/components/checkbox.js +63 -9
  2. package/components/ion-input.js +2 -1
  3. package/components/ion-select.js +7 -6
  4. package/components/ion-textarea.js +2 -1
  5. package/components/ion-toggle.js +62 -12
  6. package/components/notch-controller.js +153 -0
  7. package/components/radio-group.js +60 -7
  8. package/components/validity.js +1 -150
  9. package/dist/cjs/ion-checkbox.cjs.entry.js +60 -8
  10. package/dist/cjs/ion-input.cjs.entry.js +3 -2
  11. package/dist/cjs/ion-radio_2.cjs.entry.js +57 -6
  12. package/dist/cjs/ion-select_3.cjs.entry.js +7 -6
  13. package/dist/cjs/ion-textarea.cjs.entry.js +3 -2
  14. package/dist/cjs/ion-toggle.cjs.entry.js +58 -10
  15. package/dist/cjs/ionic.cjs.js +1 -1
  16. package/dist/cjs/loader.cjs.js +1 -1
  17. package/dist/cjs/{validity-C8QoAYT2.js → notch-controller-Bzqhjm4f.js} +0 -14
  18. package/dist/cjs/validity-BpS37YFM.js +19 -0
  19. package/dist/collection/components/checkbox/checkbox.js +67 -9
  20. package/dist/collection/components/radio-group/radio-group.js +64 -7
  21. package/dist/collection/components/select/select.js +5 -5
  22. package/dist/collection/components/toggle/toggle.js +62 -12
  23. package/dist/docs.json +1 -1
  24. package/dist/esm/ion-checkbox.entry.js +60 -8
  25. package/dist/esm/ion-input.entry.js +2 -1
  26. package/dist/esm/ion-radio_2.entry.js +57 -6
  27. package/dist/esm/ion-select_3.entry.js +6 -5
  28. package/dist/esm/ion-textarea.entry.js +2 -1
  29. package/dist/esm/ion-toggle.entry.js +58 -10
  30. package/dist/esm/ionic.js +1 -1
  31. package/dist/esm/loader.js +1 -1
  32. package/dist/esm/{validity-B8oWougr.js → notch-controller-BwelN_JM.js} +1 -14
  33. package/dist/esm/validity-DJztqcrH.js +17 -0
  34. package/dist/ionic/ionic.esm.js +1 -1
  35. package/dist/ionic/p-40c261a3.entry.js +4 -0
  36. package/dist/ionic/p-4e41ea20.entry.js +4 -0
  37. package/dist/ionic/p-7380261c.entry.js +4 -0
  38. package/dist/ionic/{p-DieJyvMP.js → p-DCv9sLH2.js} +1 -1
  39. package/dist/ionic/p-DJztqcrH.js +4 -0
  40. package/dist/ionic/p-c19f63d0.entry.js +4 -0
  41. package/dist/ionic/p-d1f54e28.entry.js +4 -0
  42. package/dist/ionic/p-d3014190.entry.js +4 -0
  43. package/dist/types/components/checkbox/checkbox.d.ts +9 -1
  44. package/dist/types/components/radio-group/radio-group.d.ts +9 -1
  45. package/dist/types/components/select/select.d.ts +2 -2
  46. package/dist/types/components/toggle/toggle.d.ts +7 -1
  47. package/dist/types/utils/forms/validity.d.ts +1 -1
  48. package/hydrate/index.js +277 -225
  49. package/hydrate/index.mjs +277 -225
  50. package/package.json +2 -2
  51. package/dist/ionic/p-4cc26913.entry.js +0 -4
  52. package/dist/ionic/p-7bcfc421.entry.js +0 -4
  53. package/dist/ionic/p-8bdfc8f6.entry.js +0 -4
  54. package/dist/ionic/p-dc2e126d.entry.js +0 -4
  55. package/dist/ionic/p-f65f9308.entry.js +0 -4
  56. package/dist/ionic/p-fc278823.entry.js +0 -4
@@ -1,7 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { Host, h } from "@stencil/core";
4
+ import { Build, Host, h } from "@stencil/core";
5
+ import { checkInvalidState } from "../../utils/forms/index";
5
6
  import { renderHiddenInput } from "../../utils/helpers";
6
7
  import { getIonMode } from "../../global/ionic-global";
7
8
  export class RadioGroup {
@@ -10,6 +11,10 @@ export class RadioGroup {
10
11
  this.helperTextId = `${this.inputId}-helper-text`;
11
12
  this.errorTextId = `${this.inputId}-error-text`;
12
13
  this.labelId = `${this.inputId}-lbl`;
14
+ /**
15
+ * Track validation state for proper aria-live announcements.
16
+ */
17
+ this.isInvalid = false;
13
18
  /**
14
19
  * If `true`, the radios can be deselected.
15
20
  */
@@ -91,6 +96,52 @@ export class RadioGroup {
91
96
  this.labelId = label.id = this.name + '-lbl';
92
97
  }
93
98
  }
99
+ // Watch for class changes to update validation state.
100
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
101
+ this.validationObserver = new MutationObserver(() => {
102
+ const newIsInvalid = checkInvalidState(this.el);
103
+ if (this.isInvalid !== newIsInvalid) {
104
+ this.isInvalid = newIsInvalid;
105
+ /**
106
+ * Screen readers tend to announce changes
107
+ * to `aria-describedby` when the attribute
108
+ * is changed during a blur event for a
109
+ * native form control.
110
+ * However, the announcement can be spotty
111
+ * when using a non-native form control
112
+ * and `forceUpdate()`.
113
+ * This is due to `forceUpdate()` internally
114
+ * rescheduling the DOM update to a lower
115
+ * priority queue regardless if it's called
116
+ * inside a Promise or not, thus causing
117
+ * the screen reader to potentially miss the
118
+ * change.
119
+ * By using a State variable inside a Promise,
120
+ * it guarantees a re-render immediately at
121
+ * a higher priority.
122
+ */
123
+ Promise.resolve().then(() => {
124
+ this.hintTextId = this.getHintTextId();
125
+ });
126
+ }
127
+ });
128
+ this.validationObserver.observe(this.el, {
129
+ attributes: true,
130
+ attributeFilter: ['class'],
131
+ });
132
+ }
133
+ // Always set initial state
134
+ this.isInvalid = checkInvalidState(this.el);
135
+ }
136
+ componentWillLoad() {
137
+ this.hintTextId = this.getHintTextId();
138
+ }
139
+ disconnectedCallback() {
140
+ // Clean up validation observer to prevent memory leaks.
141
+ if (this.validationObserver) {
142
+ this.validationObserver.disconnect();
143
+ this.validationObserver = undefined;
144
+ }
94
145
  }
95
146
  getRadios() {
96
147
  return Array.from(this.el.querySelectorAll('ion-radio'));
@@ -166,16 +217,16 @@ export class RadioGroup {
166
217
  * Renders the helper text or error text values
167
218
  */
168
219
  renderHintText() {
169
- const { helperText, errorText, helperTextId, errorTextId } = this;
220
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
170
221
  const hasHintText = !!helperText || !!errorText;
171
222
  if (!hasHintText) {
172
223
  return;
173
224
  }
174
- return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text" }, errorText)));
225
+ return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", role: "alert" }, isInvalid ? errorText : null)));
175
226
  }
176
- getHintTextID() {
177
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
178
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
227
+ getHintTextId() {
228
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
229
+ if (isInvalid && errorText) {
179
230
  return errorTextId;
180
231
  }
181
232
  if (helperText) {
@@ -187,7 +238,7 @@ export class RadioGroup {
187
238
  const { label, labelId, el, name, value } = this;
188
239
  const mode = getIonMode(this);
189
240
  renderHiddenInput(true, el, name, value, false);
190
- return (h(Host, { key: '81b8ebc96b2f383c36717f290d2959cc921ad6e8', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '45b09efc10776b889a8f372cba80d25a3fc849da', class: "radio-group-wrapper" }, h("slot", { key: '58714934542c2fdd7396de160364f3f06b32e8f8' }))));
241
+ return (h(Host, { key: 'db593b3ed511e9395e3c7bfd91b787328692cd6d', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '85045b45a0100a45f3b9a35d1c5a25ec63d525c4', class: "radio-group-wrapper" }, h("slot", { key: '53dacb87ce62398e78771fb2efaf839ab922d946' }))));
191
242
  }
192
243
  static get is() { return "ion-radio-group"; }
193
244
  static get originalStyleUrls() {
@@ -328,6 +379,12 @@ export class RadioGroup {
328
379
  }
329
380
  };
330
381
  }
382
+ static get states() {
383
+ return {
384
+ "isInvalid": {},
385
+ "hintTextId": {}
386
+ };
387
+ }
331
388
  static get events() {
332
389
  return [{
333
390
  "method": "ionChange",
@@ -202,7 +202,7 @@ export class Select {
202
202
  * a higher priority.
203
203
  */
204
204
  Promise.resolve().then(() => {
205
- this.hintTextID = this.getHintTextID();
205
+ this.hintTextId = this.getHintTextId();
206
206
  });
207
207
  }
208
208
  });
@@ -216,7 +216,7 @@ export class Select {
216
216
  }
217
217
  componentWillLoad() {
218
218
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
219
- this.hintTextID = this.getHintTextID();
219
+ this.hintTextId = this.getHintTextId();
220
220
  }
221
221
  componentDidLoad() {
222
222
  /**
@@ -761,9 +761,9 @@ export class Select {
761
761
  }
762
762
  renderListbox() {
763
763
  const { disabled, inputId, isExpanded, required } = this;
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) }));
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) }));
765
765
  }
766
- getHintTextID() {
766
+ getHintTextId() {
767
767
  const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
768
768
  if (isInvalid && errorText) {
769
769
  return errorTextId;
@@ -1316,7 +1316,7 @@ export class Select {
1316
1316
  "isExpanded": {},
1317
1317
  "hasFocus": {},
1318
1318
  "isInvalid": {},
1319
- "hintTextID": {}
1319
+ "hintTextId": {}
1320
1320
  };
1321
1321
  }
1322
1322
  static get events() {
@@ -1,7 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { Host, h } from "@stencil/core";
4
+ import { Build, Host, h } from "@stencil/core";
5
+ import { checkInvalidState } from "../../utils/forms/index";
5
6
  import { renderHiddenInput, inheritAriaAttributes } from "../../utils/helpers";
6
7
  import { hapticSelection } from "../../utils/native/haptic";
7
8
  import { isPlatform } from "../../utils/platform";
@@ -32,6 +33,10 @@ export class Toggle {
32
33
  this.inheritedAttributes = {};
33
34
  this.didLoad = false;
34
35
  this.activated = false;
36
+ /**
37
+ * Track validation state for proper aria-live announcements.
38
+ */
39
+ this.isInvalid = false;
35
40
  /**
36
41
  * The name of the control, which is submitted with the form data.
37
42
  */
@@ -145,15 +150,52 @@ export class Toggle {
145
150
  });
146
151
  }
147
152
  async connectedCallback() {
153
+ const { didLoad, el } = this;
148
154
  /**
149
155
  * If we have not yet rendered
150
156
  * ion-toggle, then toggleTrack is not defined.
151
157
  * But if we are moving ion-toggle via appendChild,
152
158
  * then toggleTrack will be defined.
153
159
  */
154
- if (this.didLoad) {
160
+ if (didLoad) {
155
161
  this.setupGesture();
156
162
  }
163
+ // Watch for class changes to update validation state.
164
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
165
+ this.validationObserver = new MutationObserver(() => {
166
+ const newIsInvalid = checkInvalidState(el);
167
+ if (this.isInvalid !== newIsInvalid) {
168
+ this.isInvalid = newIsInvalid;
169
+ /**
170
+ * Screen readers tend to announce changes
171
+ * to `aria-describedby` when the attribute
172
+ * is changed during a blur event for a
173
+ * native form control.
174
+ * However, the announcement can be spotty
175
+ * when using a non-native form control
176
+ * and `forceUpdate()`.
177
+ * This is due to `forceUpdate()` internally
178
+ * rescheduling the DOM update to a lower
179
+ * priority queue regardless if it's called
180
+ * inside a Promise or not, thus causing
181
+ * the screen reader to potentially miss the
182
+ * change.
183
+ * By using a State variable inside a Promise,
184
+ * it guarantees a re-render immediately at
185
+ * a higher priority.
186
+ */
187
+ Promise.resolve().then(() => {
188
+ this.hintTextId = this.getHintTextId();
189
+ });
190
+ }
191
+ });
192
+ this.validationObserver.observe(el, {
193
+ attributes: true,
194
+ attributeFilter: ['class'],
195
+ });
196
+ }
197
+ // Always set initial state
198
+ this.isInvalid = checkInvalidState(el);
157
199
  }
158
200
  componentDidLoad() {
159
201
  this.setupGesture();
@@ -164,9 +206,15 @@ export class Toggle {
164
206
  this.gesture.destroy();
165
207
  this.gesture = undefined;
166
208
  }
209
+ // Clean up validation observer to prevent memory leaks.
210
+ if (this.validationObserver) {
211
+ this.validationObserver.disconnect();
212
+ this.validationObserver = undefined;
213
+ }
167
214
  }
168
215
  componentWillLoad() {
169
216
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
217
+ this.hintTextId = this.getHintTextId();
170
218
  }
171
219
  onStart() {
172
220
  this.activated = true;
@@ -207,9 +255,9 @@ export class Toggle {
207
255
  get hasLabel() {
208
256
  return this.el.textContent !== '';
209
257
  }
210
- getHintTextID() {
211
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
212
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
258
+ getHintTextId() {
259
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
260
+ if (isInvalid && errorText) {
213
261
  return errorTextId;
214
262
  }
215
263
  if (helperText) {
@@ -222,7 +270,7 @@ export class Toggle {
222
270
  * This element should only be rendered if hint text is set.
223
271
  */
224
272
  renderHintText() {
225
- const { helperText, errorText, helperTextId, errorTextId } = this;
273
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
226
274
  /**
227
275
  * undefined and empty string values should
228
276
  * be treated as not having helper/error text.
@@ -231,15 +279,15 @@ export class Toggle {
231
279
  if (!hasHintText) {
232
280
  return;
233
281
  }
234
- return (h("div", { class: "toggle-bottom" }, h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText)));
282
+ return (h("div", { class: "toggle-bottom" }, h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null)));
235
283
  }
236
284
  render() {
237
- const { activated, alignment, checked, color, disabled, el, errorTextId, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
285
+ const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
238
286
  const mode = getIonMode(this);
239
287
  const value = this.getValue();
240
288
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
241
289
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
242
- return (h(Host, { key: '17bbbc8d229868e5c872b2bc5a3faf579780c5e0', role: "switch", "aria-checked": `${checked}`, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === errorTextId, onClick: this.onClick, "aria-labelledby": hasLabel ? inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, class: createColorClasses(color, {
290
+ return (h(Host, { key: 'f569148edd89ee041a4719ffc4733c16b05229bd', role: "switch", "aria-checked": `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, "aria-labelledby": hasLabel ? inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, class: createColorClasses(color, {
243
291
  [mode]: true,
244
292
  'in-item': hostContext('ion-item', el),
245
293
  'toggle-activated': activated,
@@ -249,10 +297,10 @@ export class Toggle {
249
297
  [`toggle-alignment-${alignment}`]: alignment !== undefined,
250
298
  [`toggle-label-placement-${labelPlacement}`]: true,
251
299
  [`toggle-${rtl}`]: true,
252
- }) }, h("label", { key: '673625b62a2c909e95dccb642c91312967a6cd1c', class: "toggle-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '7dc3f357b4708116663970047765da9f8f845bf0', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), h("div", { key: '8f1c6a182031e8cbc6727e5f4ac0e00ad4247447', class: {
300
+ }) }, h("label", { key: '3027f2ac4be6de422a14486d847fbee77f615db1', class: "toggle-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '4b0304c9e879e432b80184b4e5de37d55c11b436', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), h("div", { key: '8ef265ec942e7f01ff31cbb202ed146c6bf94e02', class: {
253
301
  'label-text-wrapper': true,
254
302
  'label-text-wrapper-hidden': !hasLabel,
255
- }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '8322b9d54dc7edeb4e16fefcde9f7ebca8d5c3e1' }), this.renderHintText()), h("div", { key: 'fe6984143db817a7b3020a3f57cf5418fc3dcc0e', class: "native-wrapper" }, this.renderToggleControl()))));
303
+ }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '7b162b7dd27199cca2a4c995276a18b9f8e44aaf' }), this.renderHintText()), h("div", { key: 'd13c34bd42fca01cc73ddb4ea7e471b33a282a3e', class: "native-wrapper" }, this.renderToggleControl()))));
256
304
  }
257
305
  static get is() { return "ion-toggle"; }
258
306
  static get encapsulation() { return "shadow"; }
@@ -515,7 +563,9 @@ export class Toggle {
515
563
  }
516
564
  static get states() {
517
565
  return {
518
- "activated": {}
566
+ "activated": {},
567
+ "isInvalid": {},
568
+ "hintTextId": {}
519
569
  };
520
570
  }
521
571
  static get events() {
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2025-11-11T06:11:07",
2
+ "timestamp": "2025-11-19T06:08:37",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.38.0",
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { r as registerInstance, c as createEvent, h, d as Host, g as getElement } from './index-C8IsBmNU.js';
5
5
  import { i as inheritAriaAttributes, a as renderHiddenInput } from './helpers-DEn3pfjm.js';
6
+ import { c as checkInvalidState } from './validity-DJztqcrH.js';
6
7
  import { c as createColorClasses, h as hostContext } from './theme-DiVJyqlX.js';
7
8
  import { b as getIonMode } from './ionic-global-CDrldh-5.js';
8
9
 
@@ -59,6 +60,10 @@ const Checkbox = class {
59
60
  * submitting if the value is invalid.
60
61
  */
61
62
  this.required = false;
63
+ /**
64
+ * Track validation state for proper aria-live announcements.
65
+ */
66
+ this.isInvalid = false;
62
67
  /**
63
68
  * Sets the checked property and emits
64
69
  * the ionChange event. Use this to update the
@@ -105,16 +110,63 @@ const Checkbox = class {
105
110
  ev.stopPropagation();
106
111
  };
107
112
  }
113
+ connectedCallback() {
114
+ const { el } = this;
115
+ // Watch for class changes to update validation state.
116
+ if (typeof MutationObserver !== 'undefined') {
117
+ this.validationObserver = new MutationObserver(() => {
118
+ const newIsInvalid = checkInvalidState(el);
119
+ if (this.isInvalid !== newIsInvalid) {
120
+ this.isInvalid = newIsInvalid;
121
+ /**
122
+ * Screen readers tend to announce changes
123
+ * to `aria-describedby` when the attribute
124
+ * is changed during a blur event for a
125
+ * native form control.
126
+ * However, the announcement can be spotty
127
+ * when using a non-native form control
128
+ * and `forceUpdate()`.
129
+ * This is due to `forceUpdate()` internally
130
+ * rescheduling the DOM update to a lower
131
+ * priority queue regardless if it's called
132
+ * inside a Promise or not, thus causing
133
+ * the screen reader to potentially miss the
134
+ * change.
135
+ * By using a State variable inside a Promise,
136
+ * it guarantees a re-render immediately at
137
+ * a higher priority.
138
+ */
139
+ Promise.resolve().then(() => {
140
+ this.hintTextId = this.getHintTextId();
141
+ });
142
+ }
143
+ });
144
+ this.validationObserver.observe(el, {
145
+ attributes: true,
146
+ attributeFilter: ['class'],
147
+ });
148
+ }
149
+ // Always set initial state
150
+ this.isInvalid = checkInvalidState(el);
151
+ }
108
152
  componentWillLoad() {
109
153
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
154
+ this.hintTextId = this.getHintTextId();
155
+ }
156
+ disconnectedCallback() {
157
+ // Clean up validation observer to prevent memory leaks.
158
+ if (this.validationObserver) {
159
+ this.validationObserver.disconnect();
160
+ this.validationObserver = undefined;
161
+ }
110
162
  }
111
163
  /** @internal */
112
164
  async setFocus() {
113
165
  this.el.focus();
114
166
  }
115
- getHintTextID() {
116
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
117
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
167
+ getHintTextId() {
168
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
169
+ if (isInvalid && errorText) {
118
170
  return errorTextId;
119
171
  }
120
172
  if (helperText) {
@@ -127,7 +179,7 @@ const Checkbox = class {
127
179
  * This element should only be rendered if hint text is set.
128
180
  */
129
181
  renderHintText() {
130
- const { helperText, errorText, helperTextId, errorTextId } = this;
182
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
131
183
  /**
132
184
  * undefined and empty string values should
133
185
  * be treated as not having helper/error text.
@@ -136,7 +188,7 @@ const Checkbox = class {
136
188
  if (!hasHintText) {
137
189
  return;
138
190
  }
139
- 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)));
191
+ 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)));
140
192
  }
141
193
  render() {
142
194
  const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, alignment, required, } = this;
@@ -146,7 +198,7 @@ const Checkbox = class {
146
198
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
147
199
  // The host element must have a checkbox role to ensure proper VoiceOver
148
200
  // support in Safari for accessibility.
149
- 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, {
201
+ 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, {
150
202
  [mode]: true,
151
203
  'in-item': hostContext('ion-item', el),
152
204
  'checkbox-checked': checked,
@@ -156,10 +208,10 @@ const Checkbox = class {
156
208
  [`checkbox-justify-${justify}`]: justify !== undefined,
157
209
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
158
210
  [`checkbox-label-placement-${labelPlacement}`]: true,
159
- }) }, 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: {
211
+ }) }, 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: {
160
212
  'label-text-wrapper': true,
161
213
  'label-text-wrapper-hidden': !hasLabelContent,
162
- }, 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)))));
214
+ }, 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)))));
163
215
  }
164
216
  getSVGPath(mode, indeterminate) {
165
217
  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" }));
@@ -2,7 +2,8 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { r as registerInstance, c as createEvent, i as forceUpdate, h, d as Host, g as getElement } from './index-C8IsBmNU.js';
5
- import { c as createNotchController, a as checkInvalidState } from './validity-B8oWougr.js';
5
+ import { c as createNotchController } from './notch-controller-BwelN_JM.js';
6
+ import { c as checkInvalidState } from './validity-DJztqcrH.js';
6
7
  import { d as debounceEvent, i as inheritAriaAttributes, b as inheritAttributes, c as componentOnReady } from './helpers-DEn3pfjm.js';
7
8
  import { c as createSlotMutationController, g as getCounterText } from './input.utils-DrvTa8gz.js';
8
9
  import { h as hostContext, c as createColorClasses } from './theme-DiVJyqlX.js';
@@ -6,6 +6,7 @@ import { f as addEventListener, m as removeEventListener, a as renderHiddenInput
6
6
  import { i as isOptionSelected } from './compare-with-utils-sObYyvOy.js';
7
7
  import { h as hostContext, c as createColorClasses } from './theme-DiVJyqlX.js';
8
8
  import { b as getIonMode } from './ionic-global-CDrldh-5.js';
9
+ import { c as checkInvalidState } from './validity-DJztqcrH.js';
9
10
 
10
11
  const radioIosCss = ":host{--inner-border-radius:50%;display:inline-block;position:relative;max-width:100%;min-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:2;-webkit-box-sizing:border-box;box-sizing:border-box}:host(.radio-disabled){pointer-events:none}.radio-icon{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:100%;height:100%;contain:layout size style}.radio-icon,.radio-inner{-webkit-box-sizing:border-box;box-sizing:border-box}input{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;clip:rect(0 0 0 0);opacity:0;overflow:hidden;-webkit-appearance:none;-moz-appearance:none}:host(:focus){outline:none}:host(.in-item){-ms-flex:1 1 0px;flex:1 1 0;width:100%;height:100%}:host([slot=start]),:host([slot=end]){-ms-flex:initial;flex:initial;width:auto}.radio-wrapper{display:-ms-flexbox;display:flex;position:relative;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;height:inherit;min-height:inherit;cursor:inherit}.label-text-wrapper{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}:host(.in-item) .label-text-wrapper{margin-top:10px;margin-bottom:10px}:host(.in-item.radio-label-placement-stacked) .label-text-wrapper{margin-top:10px;margin-bottom:16px}:host(.in-item.radio-label-placement-stacked) .native-wrapper{margin-bottom:10px}.label-text-wrapper-hidden{display:none}.native-wrapper{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}:host(.radio-justify-space-between) .radio-wrapper{-ms-flex-pack:justify;justify-content:space-between}:host(.radio-justify-start) .radio-wrapper{-ms-flex-pack:start;justify-content:start}:host(.radio-justify-end) .radio-wrapper{-ms-flex-pack:end;justify-content:end}:host(.radio-alignment-start) .radio-wrapper{-ms-flex-align:start;align-items:start}:host(.radio-alignment-center) .radio-wrapper{-ms-flex-align:center;align-items:center}:host(.radio-justify-space-between),:host(.radio-justify-start),:host(.radio-justify-end),:host(.radio-alignment-start),:host(.radio-alignment-center){display:block}:host(.radio-label-placement-start) .radio-wrapper{-ms-flex-direction:row;flex-direction:row}:host(.radio-label-placement-start) .label-text-wrapper{-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:16px;margin-inline-end:16px}:host(.radio-label-placement-end) .radio-wrapper{-ms-flex-direction:row-reverse;flex-direction:row-reverse}:host(.radio-label-placement-end) .label-text-wrapper{-webkit-margin-start:16px;margin-inline-start:16px;-webkit-margin-end:0;margin-inline-end:0}:host(.radio-label-placement-fixed) .label-text-wrapper{-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:16px;margin-inline-end:16px}:host(.radio-label-placement-fixed) .label-text-wrapper{-ms-flex:0 0 100px;flex:0 0 100px;width:100px;min-width:100px}:host(.radio-label-placement-stacked) .radio-wrapper{-ms-flex-direction:column;flex-direction:column}:host(.radio-label-placement-stacked) .label-text-wrapper{-webkit-transform:scale(0.75);transform:scale(0.75);margin-left:0;margin-right:0;margin-bottom:16px;max-width:calc(100% / 0.75)}:host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper{-webkit-transform-origin:left top;transform-origin:left top}:host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper,:host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-start .label-text-wrapper{-webkit-transform-origin:right top;transform-origin:right top}@supports selector(:dir(rtl)){:host(.radio-label-placement-stacked.radio-alignment-start:dir(rtl)) .label-text-wrapper{-webkit-transform-origin:right top;transform-origin:right top}}:host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper{-webkit-transform-origin:center top;transform-origin:center top}:host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper,:host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-center .label-text-wrapper{-webkit-transform-origin:calc(100% - center) top;transform-origin:calc(100% - center) top}@supports selector(:dir(rtl)){:host(.radio-label-placement-stacked.radio-alignment-center:dir(rtl)) .label-text-wrapper{-webkit-transform-origin:calc(100% - center) top;transform-origin:calc(100% - center) top}}:host{--color-checked:var(--ion-color-primary, #0054e9)}:host(.ion-color.radio-checked) .radio-inner{border-color:var(--ion-color-base)}.item-radio.item-ios ion-label{-webkit-margin-start:0;margin-inline-start:0}.radio-inner{width:33%;height:50%}:host(.radio-checked) .radio-inner{-webkit-transform:rotate(45deg);transform:rotate(45deg);border-width:0.125rem;border-top-width:0;border-left-width:0;border-style:solid;border-color:var(--color-checked)}:host(.radio-disabled){opacity:0.3}:host(.ion-focused) .radio-icon::after{border-radius:var(--inner-border-radius);top:-8px;display:block;position:absolute;width:36px;height:36px;background:var(--ion-color-primary-tint, #1a65eb);content:\"\";opacity:0.2}:host(.ion-focused) .radio-icon::after{inset-inline-start:-9px}.native-wrapper .radio-icon{width:0.9375rem;height:1.5rem}";
11
12
 
@@ -175,6 +176,10 @@ const RadioGroup = class {
175
176
  this.helperTextId = `${this.inputId}-helper-text`;
176
177
  this.errorTextId = `${this.inputId}-error-text`;
177
178
  this.labelId = `${this.inputId}-lbl`;
179
+ /**
180
+ * Track validation state for proper aria-live announcements.
181
+ */
182
+ this.isInvalid = false;
178
183
  /**
179
184
  * If `true`, the radios can be deselected.
180
185
  */
@@ -256,6 +261,52 @@ const RadioGroup = class {
256
261
  this.labelId = label.id = this.name + '-lbl';
257
262
  }
258
263
  }
264
+ // Watch for class changes to update validation state.
265
+ if (typeof MutationObserver !== 'undefined') {
266
+ this.validationObserver = new MutationObserver(() => {
267
+ const newIsInvalid = checkInvalidState(this.el);
268
+ if (this.isInvalid !== newIsInvalid) {
269
+ this.isInvalid = newIsInvalid;
270
+ /**
271
+ * Screen readers tend to announce changes
272
+ * to `aria-describedby` when the attribute
273
+ * is changed during a blur event for a
274
+ * native form control.
275
+ * However, the announcement can be spotty
276
+ * when using a non-native form control
277
+ * and `forceUpdate()`.
278
+ * This is due to `forceUpdate()` internally
279
+ * rescheduling the DOM update to a lower
280
+ * priority queue regardless if it's called
281
+ * inside a Promise or not, thus causing
282
+ * the screen reader to potentially miss the
283
+ * change.
284
+ * By using a State variable inside a Promise,
285
+ * it guarantees a re-render immediately at
286
+ * a higher priority.
287
+ */
288
+ Promise.resolve().then(() => {
289
+ this.hintTextId = this.getHintTextId();
290
+ });
291
+ }
292
+ });
293
+ this.validationObserver.observe(this.el, {
294
+ attributes: true,
295
+ attributeFilter: ['class'],
296
+ });
297
+ }
298
+ // Always set initial state
299
+ this.isInvalid = checkInvalidState(this.el);
300
+ }
301
+ componentWillLoad() {
302
+ this.hintTextId = this.getHintTextId();
303
+ }
304
+ disconnectedCallback() {
305
+ // Clean up validation observer to prevent memory leaks.
306
+ if (this.validationObserver) {
307
+ this.validationObserver.disconnect();
308
+ this.validationObserver = undefined;
309
+ }
259
310
  }
260
311
  getRadios() {
261
312
  return Array.from(this.el.querySelectorAll('ion-radio'));
@@ -331,16 +382,16 @@ const RadioGroup = class {
331
382
  * Renders the helper text or error text values
332
383
  */
333
384
  renderHintText() {
334
- const { helperText, errorText, helperTextId, errorTextId } = this;
385
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
335
386
  const hasHintText = !!helperText || !!errorText;
336
387
  if (!hasHintText) {
337
388
  return;
338
389
  }
339
- return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text" }, errorText)));
390
+ return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", role: "alert" }, isInvalid ? errorText : null)));
340
391
  }
341
- getHintTextID() {
342
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
343
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
392
+ getHintTextId() {
393
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
394
+ if (isInvalid && errorText) {
344
395
  return errorTextId;
345
396
  }
346
397
  if (helperText) {
@@ -352,7 +403,7 @@ const RadioGroup = class {
352
403
  const { label, labelId, el, name, value } = this;
353
404
  const mode = getIonMode(this);
354
405
  renderHiddenInput(true, el, name, value, false);
355
- return (h(Host, { key: '81b8ebc96b2f383c36717f290d2959cc921ad6e8', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '45b09efc10776b889a8f372cba80d25a3fc849da', class: "radio-group-wrapper" }, h("slot", { key: '58714934542c2fdd7396de160364f3f06b32e8f8' }))));
406
+ return (h(Host, { key: 'db593b3ed511e9395e3c7bfd91b787328692cd6d', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '85045b45a0100a45f3b9a35d1c5a25ec63d525c4', class: "radio-group-wrapper" }, h("slot", { key: '53dacb87ce62398e78771fb2efaf839ab922d946' }))));
356
407
  }
357
408
  get el() { return getElement(this); }
358
409
  static get watchers() { return {
@@ -2,8 +2,9 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { r as registerInstance, c as createEvent, f as printIonWarning, h, d as Host, g as getElement, i as forceUpdate } from './index-C8IsBmNU.js';
5
- import { c as createNotchController, a as checkInvalidState } from './validity-B8oWougr.js';
5
+ import { c as createNotchController } from './notch-controller-BwelN_JM.js';
6
6
  import { i as isOptionSelected, c as compareOptions } from './compare-with-utils-sObYyvOy.js';
7
+ import { c as checkInvalidState } from './validity-DJztqcrH.js';
7
8
  import { b as inheritAttributes, a as renderHiddenInput, n as focusVisibleElement } from './helpers-DEn3pfjm.js';
8
9
  import { c as popoverController, b as actionSheetController, a as alertController, m as modalController, s as safeCall } from './overlays-BymNv-BL.js';
9
10
  import { i as isRTL } from './dir-C53feagD.js';
@@ -201,7 +202,7 @@ const Select = class {
201
202
  * a higher priority.
202
203
  */
203
204
  Promise.resolve().then(() => {
204
- this.hintTextID = this.getHintTextID();
205
+ this.hintTextId = this.getHintTextId();
205
206
  });
206
207
  }
207
208
  });
@@ -215,7 +216,7 @@ const Select = class {
215
216
  }
216
217
  componentWillLoad() {
217
218
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
218
- this.hintTextID = this.getHintTextID();
219
+ this.hintTextId = this.getHintTextId();
219
220
  }
220
221
  componentDidLoad() {
221
222
  /**
@@ -714,9 +715,9 @@ const Select = class {
714
715
  }
715
716
  renderListbox() {
716
717
  const { disabled, inputId, isExpanded, required } = this;
717
- 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) }));
718
+ 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) }));
718
719
  }
719
- getHintTextID() {
720
+ getHintTextId() {
720
721
  const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
721
722
  if (isInvalid && errorText) {
722
723
  return errorTextId;
@@ -2,7 +2,8 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { r as registerInstance, c as createEvent, i as forceUpdate, w as writeTask, h, d as Host, g as getElement } from './index-C8IsBmNU.js';
5
- import { c as createNotchController, a as checkInvalidState } from './validity-B8oWougr.js';
5
+ import { c as createNotchController } from './notch-controller-BwelN_JM.js';
6
+ import { c as checkInvalidState } from './validity-DJztqcrH.js';
6
7
  import { d as debounceEvent, i as inheritAriaAttributes, b as inheritAttributes, c as componentOnReady } from './helpers-DEn3pfjm.js';
7
8
  import { c as createSlotMutationController, g as getCounterText } from './input.utils-DrvTa8gz.js';
8
9
  import { h as hostContext, c as createColorClasses } from './theme-DiVJyqlX.js';