voyager-ionic-core 8.7.9 → 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 (64) hide show
  1. package/components/checkbox.js +63 -9
  2. package/components/ion-datetime.js +35 -2
  3. package/components/ion-input.js +2 -1
  4. package/components/ion-select.js +7 -6
  5. package/components/ion-textarea.js +2 -1
  6. package/components/ion-toggle.js +62 -12
  7. package/components/notch-controller.js +153 -0
  8. package/components/radio-group.js +60 -7
  9. package/components/validity.js +1 -150
  10. package/dist/cjs/ion-checkbox.cjs.entry.js +60 -8
  11. package/dist/cjs/ion-datetime_3.cjs.entry.js +35 -2
  12. package/dist/cjs/ion-input.cjs.entry.js +3 -2
  13. package/dist/cjs/ion-radio_2.cjs.entry.js +57 -6
  14. package/dist/cjs/ion-select_3.cjs.entry.js +7 -6
  15. package/dist/cjs/ion-textarea.cjs.entry.js +3 -2
  16. package/dist/cjs/ion-toggle.cjs.entry.js +58 -10
  17. package/dist/cjs/ionic.cjs.js +1 -1
  18. package/dist/cjs/loader.cjs.js +1 -1
  19. package/dist/cjs/{validity-C8QoAYT2.js → notch-controller-Bzqhjm4f.js} +0 -14
  20. package/dist/cjs/validity-BpS37YFM.js +19 -0
  21. package/dist/collection/components/checkbox/checkbox.js +67 -9
  22. package/dist/collection/components/datetime/datetime.js +35 -2
  23. package/dist/collection/components/radio-group/radio-group.js +64 -7
  24. package/dist/collection/components/select/select.js +5 -5
  25. package/dist/collection/components/toggle/toggle.js +62 -12
  26. package/dist/collection/utils/test/playwright/page/utils/set-content.js +7 -0
  27. package/dist/docs.json +1 -1
  28. package/dist/esm/ion-checkbox.entry.js +60 -8
  29. package/dist/esm/ion-datetime_3.entry.js +35 -2
  30. package/dist/esm/ion-input.entry.js +2 -1
  31. package/dist/esm/ion-radio_2.entry.js +57 -6
  32. package/dist/esm/ion-select_3.entry.js +6 -5
  33. package/dist/esm/ion-textarea.entry.js +2 -1
  34. package/dist/esm/ion-toggle.entry.js +58 -10
  35. package/dist/esm/ionic.js +1 -1
  36. package/dist/esm/loader.js +1 -1
  37. package/dist/esm/{validity-B8oWougr.js → notch-controller-BwelN_JM.js} +1 -14
  38. package/dist/esm/validity-DJztqcrH.js +17 -0
  39. package/dist/ionic/ionic.esm.js +1 -1
  40. package/dist/ionic/p-40c261a3.entry.js +4 -0
  41. package/dist/ionic/p-4e41ea20.entry.js +4 -0
  42. package/dist/ionic/p-7380261c.entry.js +4 -0
  43. package/dist/ionic/{p-DieJyvMP.js → p-DCv9sLH2.js} +1 -1
  44. package/dist/ionic/p-DJztqcrH.js +4 -0
  45. package/dist/ionic/p-c19f63d0.entry.js +4 -0
  46. package/dist/ionic/p-cb93126d.entry.js +4 -0
  47. package/dist/ionic/p-d1f54e28.entry.js +4 -0
  48. package/dist/ionic/p-d3014190.entry.js +4 -0
  49. package/dist/types/components/checkbox/checkbox.d.ts +9 -1
  50. package/dist/types/components/datetime/datetime.d.ts +10 -0
  51. package/dist/types/components/radio-group/radio-group.d.ts +9 -1
  52. package/dist/types/components/select/select.d.ts +2 -2
  53. package/dist/types/components/toggle/toggle.d.ts +7 -1
  54. package/dist/types/utils/forms/validity.d.ts +1 -1
  55. package/hydrate/index.js +312 -227
  56. package/hydrate/index.mjs +312 -227
  57. package/package.json +2 -2
  58. package/dist/ionic/p-4cc26913.entry.js +0 -4
  59. package/dist/ionic/p-4efea47a.entry.js +0 -4
  60. package/dist/ionic/p-7bcfc421.entry.js +0 -4
  61. package/dist/ionic/p-8bdfc8f6.entry.js +0 -4
  62. package/dist/ionic/p-dc2e126d.entry.js +0 -4
  63. package/dist/ionic/p-f65f9308.entry.js +0 -4
  64. package/dist/ionic/p-fc278823.entry.js +0 -4
@@ -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" }));
@@ -784,6 +784,28 @@ const Datetime = class {
784
784
  destroyKeyboardMO();
785
785
  }
786
786
  };
787
+ /**
788
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
789
+ * Fallback to ensure the datetime becomes ready even if
790
+ * IntersectionObserver never reports it as intersecting.
791
+ *
792
+ * This is primarily used in environments where the observer
793
+ * might not fire as expected, such as when running under
794
+ * synthetic tests that stub IntersectionObserver.
795
+ */
796
+ this.ensureReadyIfVisible = () => {
797
+ if (this.el.classList.contains('datetime-ready')) {
798
+ return;
799
+ }
800
+ const rect = this.el.getBoundingClientRect();
801
+ if (rect.width === 0 || rect.height === 0) {
802
+ return;
803
+ }
804
+ this.initializeListeners();
805
+ writeTask(() => {
806
+ this.el.classList.add('datetime-ready');
807
+ });
808
+ };
787
809
  this.processValue = (value) => {
788
810
  const hasValue = value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
789
811
  const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
@@ -1101,6 +1123,17 @@ const Datetime = class {
1101
1123
  * triggering the `hiddenIO` observer below.
1102
1124
  */
1103
1125
  raf(() => visibleIO === null || visibleIO === void 0 ? void 0 : visibleIO.observe(intersectionTrackerRef));
1126
+ /**
1127
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
1128
+ * Fallback: If IntersectionObserver never reports that the
1129
+ * datetime is visible but the host clearly has layout, ensure
1130
+ * we still initialize listeners and mark the component as ready.
1131
+ *
1132
+ * We schedule this after everything has had a chance to run.
1133
+ */
1134
+ setTimeout(() => {
1135
+ this.ensureReadyIfVisible();
1136
+ }, 100);
1104
1137
  /**
1105
1138
  * We need to clean up listeners when the datetime is hidden
1106
1139
  * in a popover/modal so that we can properly scroll containers
@@ -1856,7 +1889,7 @@ const Datetime = class {
1856
1889
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1857
1890
  const hasWheelVariant = hasDatePresentation && preferWheel;
1858
1891
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1859
- return (h(Host, { key: '57492534800ea059a7c2bbd9f0059cc0b75ae8d2', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1892
+ return (h(Host, { key: 'efdbc0922670a841bc667ceac392cdc1dedffd01', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1860
1893
  [mode]: true,
1861
1894
  ['datetime-readonly']: readonly,
1862
1895
  ['datetime-disabled']: disabled,
@@ -1866,7 +1899,7 @@ const Datetime = class {
1866
1899
  [`datetime-size-${size}`]: true,
1867
1900
  [`datetime-prefer-wheel`]: hasWheelVariant,
1868
1901
  [`datetime-grid`]: isGridStyle,
1869
- })) }, h("div", { key: '97dac5e5195635ac0bc5fb472b9d09e5c3c6bbc3', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1902
+ })) }, h("div", { key: '3f8bb75fcb0baff55182ef3aa1b535eacc58d81f', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1870
1903
  }
1871
1904
  get el() { return getElement(this); }
1872
1905
  static get watchers() { return {
@@ -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';
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { r as registerInstance, c as createEvent, e as config, 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 hapticSelection } from './haptic-DzAMWJuk.js';
7
8
  import { a as isPlatform, b as getIonMode } from './ionic-global-CDrldh-5.js';
8
9
  import { i as isRTL } from './dir-C53feagD.js';
@@ -29,6 +30,10 @@ const Toggle = class {
29
30
  this.inheritedAttributes = {};
30
31
  this.didLoad = false;
31
32
  this.activated = false;
33
+ /**
34
+ * Track validation state for proper aria-live announcements.
35
+ */
36
+ this.isInvalid = false;
32
37
  /**
33
38
  * The name of the control, which is submitted with the form data.
34
39
  */
@@ -142,15 +147,52 @@ const Toggle = class {
142
147
  });
143
148
  }
144
149
  async connectedCallback() {
150
+ const { didLoad, el } = this;
145
151
  /**
146
152
  * If we have not yet rendered
147
153
  * ion-toggle, then toggleTrack is not defined.
148
154
  * But if we are moving ion-toggle via appendChild,
149
155
  * then toggleTrack will be defined.
150
156
  */
151
- if (this.didLoad) {
157
+ if (didLoad) {
152
158
  this.setupGesture();
153
159
  }
160
+ // Watch for class changes to update validation state.
161
+ if (typeof MutationObserver !== 'undefined') {
162
+ this.validationObserver = new MutationObserver(() => {
163
+ const newIsInvalid = checkInvalidState(el);
164
+ if (this.isInvalid !== newIsInvalid) {
165
+ this.isInvalid = newIsInvalid;
166
+ /**
167
+ * Screen readers tend to announce changes
168
+ * to `aria-describedby` when the attribute
169
+ * is changed during a blur event for a
170
+ * native form control.
171
+ * However, the announcement can be spotty
172
+ * when using a non-native form control
173
+ * and `forceUpdate()`.
174
+ * This is due to `forceUpdate()` internally
175
+ * rescheduling the DOM update to a lower
176
+ * priority queue regardless if it's called
177
+ * inside a Promise or not, thus causing
178
+ * the screen reader to potentially miss the
179
+ * change.
180
+ * By using a State variable inside a Promise,
181
+ * it guarantees a re-render immediately at
182
+ * a higher priority.
183
+ */
184
+ Promise.resolve().then(() => {
185
+ this.hintTextId = this.getHintTextId();
186
+ });
187
+ }
188
+ });
189
+ this.validationObserver.observe(el, {
190
+ attributes: true,
191
+ attributeFilter: ['class'],
192
+ });
193
+ }
194
+ // Always set initial state
195
+ this.isInvalid = checkInvalidState(el);
154
196
  }
155
197
  componentDidLoad() {
156
198
  this.setupGesture();
@@ -161,9 +203,15 @@ const Toggle = class {
161
203
  this.gesture.destroy();
162
204
  this.gesture = undefined;
163
205
  }
206
+ // Clean up validation observer to prevent memory leaks.
207
+ if (this.validationObserver) {
208
+ this.validationObserver.disconnect();
209
+ this.validationObserver = undefined;
210
+ }
164
211
  }
165
212
  componentWillLoad() {
166
213
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
214
+ this.hintTextId = this.getHintTextId();
167
215
  }
168
216
  onStart() {
169
217
  this.activated = true;
@@ -204,9 +252,9 @@ const Toggle = class {
204
252
  get hasLabel() {
205
253
  return this.el.textContent !== '';
206
254
  }
207
- getHintTextID() {
208
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
209
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
255
+ getHintTextId() {
256
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
257
+ if (isInvalid && errorText) {
210
258
  return errorTextId;
211
259
  }
212
260
  if (helperText) {
@@ -219,7 +267,7 @@ const Toggle = class {
219
267
  * This element should only be rendered if hint text is set.
220
268
  */
221
269
  renderHintText() {
222
- const { helperText, errorText, helperTextId, errorTextId } = this;
270
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
223
271
  /**
224
272
  * undefined and empty string values should
225
273
  * be treated as not having helper/error text.
@@ -228,15 +276,15 @@ const Toggle = class {
228
276
  if (!hasHintText) {
229
277
  return;
230
278
  }
231
- 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)));
279
+ 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)));
232
280
  }
233
281
  render() {
234
- const { activated, alignment, checked, color, disabled, el, errorTextId, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
282
+ const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
235
283
  const mode = getIonMode(this);
236
284
  const value = this.getValue();
237
285
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
238
286
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
239
- 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, {
287
+ 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, {
240
288
  [mode]: true,
241
289
  'in-item': hostContext('ion-item', el),
242
290
  'toggle-activated': activated,
@@ -246,10 +294,10 @@ const Toggle = class {
246
294
  [`toggle-alignment-${alignment}`]: alignment !== undefined,
247
295
  [`toggle-label-placement-${labelPlacement}`]: true,
248
296
  [`toggle-${rtl}`]: true,
249
- }) }, 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: {
297
+ }) }, 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: {
250
298
  'label-text-wrapper': true,
251
299
  'label-text-wrapper-hidden': !hasLabel,
252
- }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '8322b9d54dc7edeb4e16fefcde9f7ebca8d5c3e1' }), this.renderHintText()), h("div", { key: 'fe6984143db817a7b3020a3f57cf5418fc3dcc0e', class: "native-wrapper" }, this.renderToggleControl()))));
300
+ }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '7b162b7dd27199cca2a4c995276a18b9f8e44aaf' }), this.renderHintText()), h("div", { key: 'd13c34bd42fca01cc73ddb4ea7e471b33a282a3e', class: "native-wrapper" }, this.renderToggleControl()))));
253
301
  }
254
302
  get el() { return getElement(this); }
255
303
  static get watchers() { return {