@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,155 +1,6 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { w as win } from './index9.js';
5
- import { r as raf } from './helpers.js';
6
-
7
- /**
8
- * A utility to calculate the size of an outline notch
9
- * width relative to the content passed. This is used in
10
- * components such as `ion-select` with `fill="outline"`
11
- * where we need to pass slotted HTML content. This is not
12
- * needed when rendering plaintext content because we can
13
- * render the plaintext again hidden with `opacity: 0` inside
14
- * of the notch. As a result we can rely on the intrinsic size
15
- * of the element to correctly compute the notch width. We
16
- * cannot do this with slotted content because we cannot project
17
- * it into 2 places at once.
18
- *
19
- * @internal
20
- * @param el: The host element
21
- * @param getNotchSpacerEl: A function that returns a reference to the notch spacer element inside of the component template.
22
- * @param getLabelSlot: A function that returns a reference to the slotted content.
23
- */
24
- const createNotchController = (el, getNotchSpacerEl, getLabelSlot) => {
25
- let notchVisibilityIO;
26
- const needsExplicitNotchWidth = () => {
27
- const notchSpacerEl = getNotchSpacerEl();
28
- if (
29
- /**
30
- * If the notch is not being used
31
- * then we do not need to set the notch width.
32
- */
33
- notchSpacerEl === undefined ||
34
- /**
35
- * If either the label property is being
36
- * used or the label slot is not defined,
37
- * then we do not need to estimate the notch width.
38
- */
39
- el.label !== undefined ||
40
- getLabelSlot() === null) {
41
- return false;
42
- }
43
- return true;
44
- };
45
- const calculateNotchWidth = () => {
46
- if (needsExplicitNotchWidth()) {
47
- /**
48
- * Run this the frame after
49
- * the browser has re-painted the host element.
50
- * Otherwise, the label element may have a width
51
- * of 0 and the IntersectionObserver will be used.
52
- */
53
- raf(() => {
54
- setNotchWidth();
55
- });
56
- }
57
- };
58
- /**
59
- * When using a label prop we can render
60
- * the label value inside of the notch and
61
- * let the browser calculate the size of the notch.
62
- * However, we cannot render the label slot in multiple
63
- * places so we need to manually calculate the notch dimension
64
- * based on the size of the slotted content.
65
- *
66
- * This function should only be used to set the notch width
67
- * on slotted label content. The notch width for label prop
68
- * content is automatically calculated based on the
69
- * intrinsic size of the label text.
70
- */
71
- const setNotchWidth = () => {
72
- const notchSpacerEl = getNotchSpacerEl();
73
- if (notchSpacerEl === undefined) {
74
- return;
75
- }
76
- if (!needsExplicitNotchWidth()) {
77
- notchSpacerEl.style.removeProperty('width');
78
- return;
79
- }
80
- const width = getLabelSlot().scrollWidth;
81
- if (
82
- /**
83
- * If the computed width of the label is 0
84
- * and notchSpacerEl's offsetParent is null
85
- * then that means the element is hidden.
86
- * As a result, we need to wait for the element
87
- * to become visible before setting the notch width.
88
- *
89
- * We do not check el.offsetParent because
90
- * that can be null if the host element has
91
- * position: fixed applied to it.
92
- * notchSpacerEl does not have position: fixed.
93
- */
94
- width === 0 &&
95
- notchSpacerEl.offsetParent === null &&
96
- win !== undefined &&
97
- 'IntersectionObserver' in win) {
98
- /**
99
- * If there is an IO already attached
100
- * then that will update the notch
101
- * once the element becomes visible.
102
- * As a result, there is no need to create
103
- * another one.
104
- */
105
- if (notchVisibilityIO !== undefined) {
106
- return;
107
- }
108
- const io = (notchVisibilityIO = new IntersectionObserver((ev) => {
109
- /**
110
- * If the element is visible then we
111
- * can try setting the notch width again.
112
- */
113
- if (ev[0].intersectionRatio === 1) {
114
- setNotchWidth();
115
- io.disconnect();
116
- notchVisibilityIO = undefined;
117
- }
118
- },
119
- /**
120
- * Set the root to be the host element
121
- * This causes the IO callback
122
- * to be fired in WebKit as soon as the element
123
- * is visible. If we used the default root value
124
- * then WebKit would only fire the IO callback
125
- * after any animations (such as a modal transition)
126
- * finished, and there would potentially be a flicker.
127
- */
128
- { threshold: 0.01, root: el }));
129
- io.observe(notchSpacerEl);
130
- return;
131
- }
132
- /**
133
- * If the element is visible then we can set the notch width.
134
- * The notch is only visible when the label is scaled,
135
- * which is why we multiply the width by 0.75 as this is
136
- * the same amount the label element is scaled by in the host CSS.
137
- * (See $form-control-label-stacked-scale in ionic.globals.scss).
138
- */
139
- notchSpacerEl.style.setProperty('width', `${width * 0.75}px`);
140
- };
141
- const destroy = () => {
142
- if (notchVisibilityIO) {
143
- notchVisibilityIO.disconnect();
144
- notchVisibilityIO = undefined;
145
- }
146
- };
147
- return {
148
- calculateNotchWidth,
149
- destroy,
150
- };
151
- };
152
-
153
4
  /**
154
5
  * Checks if the form element is in an invalid state based on
155
6
  * Ionic validation classes.
@@ -163,4 +14,4 @@ const checkInvalidState = (el) => {
163
14
  return hasIonTouched && hasIonInvalid;
164
15
  };
165
16
 
166
- export { checkInvalidState as a, createNotchController as c };
17
+ export { checkInvalidState as c };
@@ -5,6 +5,7 @@
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
7
  var helpers = require('./helpers-DrTqNghc.js');
8
+ var validity = require('./validity-BpS37YFM.js');
8
9
  var theme = require('./theme-CeDs6Hcv.js');
9
10
  var ionicGlobal = require('./ionic-global-HMVqOFGO.js');
10
11
 
@@ -61,6 +62,10 @@ const Checkbox = class {
61
62
  * submitting if the value is invalid.
62
63
  */
63
64
  this.required = false;
65
+ /**
66
+ * Track validation state for proper aria-live announcements.
67
+ */
68
+ this.isInvalid = false;
64
69
  /**
65
70
  * Sets the checked property and emits
66
71
  * the ionChange event. Use this to update the
@@ -107,16 +112,63 @@ const Checkbox = class {
107
112
  ev.stopPropagation();
108
113
  };
109
114
  }
115
+ connectedCallback() {
116
+ const { el } = this;
117
+ // Watch for class changes to update validation state.
118
+ if (typeof MutationObserver !== 'undefined') {
119
+ this.validationObserver = new MutationObserver(() => {
120
+ const newIsInvalid = validity.checkInvalidState(el);
121
+ if (this.isInvalid !== newIsInvalid) {
122
+ this.isInvalid = newIsInvalid;
123
+ /**
124
+ * Screen readers tend to announce changes
125
+ * to `aria-describedby` when the attribute
126
+ * is changed during a blur event for a
127
+ * native form control.
128
+ * However, the announcement can be spotty
129
+ * when using a non-native form control
130
+ * and `forceUpdate()`.
131
+ * This is due to `forceUpdate()` internally
132
+ * rescheduling the DOM update to a lower
133
+ * priority queue regardless if it's called
134
+ * inside a Promise or not, thus causing
135
+ * the screen reader to potentially miss the
136
+ * change.
137
+ * By using a State variable inside a Promise,
138
+ * it guarantees a re-render immediately at
139
+ * a higher priority.
140
+ */
141
+ Promise.resolve().then(() => {
142
+ this.hintTextId = this.getHintTextId();
143
+ });
144
+ }
145
+ });
146
+ this.validationObserver.observe(el, {
147
+ attributes: true,
148
+ attributeFilter: ['class'],
149
+ });
150
+ }
151
+ // Always set initial state
152
+ this.isInvalid = validity.checkInvalidState(el);
153
+ }
110
154
  componentWillLoad() {
111
155
  this.inheritedAttributes = Object.assign({}, helpers.inheritAriaAttributes(this.el));
156
+ this.hintTextId = this.getHintTextId();
157
+ }
158
+ disconnectedCallback() {
159
+ // Clean up validation observer to prevent memory leaks.
160
+ if (this.validationObserver) {
161
+ this.validationObserver.disconnect();
162
+ this.validationObserver = undefined;
163
+ }
112
164
  }
113
165
  /** @internal */
114
166
  async setFocus() {
115
167
  this.el.focus();
116
168
  }
117
- getHintTextID() {
118
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
119
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
169
+ getHintTextId() {
170
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
171
+ if (isInvalid && errorText) {
120
172
  return errorTextId;
121
173
  }
122
174
  if (helperText) {
@@ -129,7 +181,7 @@ const Checkbox = class {
129
181
  * This element should only be rendered if hint text is set.
130
182
  */
131
183
  renderHintText() {
132
- const { helperText, errorText, helperTextId, errorTextId } = this;
184
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
133
185
  /**
134
186
  * undefined and empty string values should
135
187
  * be treated as not having helper/error text.
@@ -138,7 +190,7 @@ const Checkbox = class {
138
190
  if (!hasHintText) {
139
191
  return;
140
192
  }
141
- return (index.h("div", { class: "checkbox-bottom" }, index.h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText), index.h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText)));
193
+ return (index.h("div", { class: "checkbox-bottom" }, index.h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), index.h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null)));
142
194
  }
143
195
  render() {
144
196
  const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, alignment, required, } = this;
@@ -148,7 +200,7 @@ const Checkbox = class {
148
200
  helpers.renderHiddenInput(true, el, name, checked ? value : '', disabled);
149
201
  // The host element must have a checkbox role to ensure proper VoiceOver
150
202
  // support in Safari for accessibility.
151
- return (index.h(index.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: theme.createColorClasses(color, {
203
+ return (index.h(index.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: theme.createColorClasses(color, {
152
204
  [mode]: true,
153
205
  'in-item': theme.hostContext('ion-item', el),
154
206
  'checkbox-checked': checked,
@@ -158,10 +210,10 @@ const Checkbox = class {
158
210
  [`checkbox-justify-${justify}`]: justify !== undefined,
159
211
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
160
212
  [`checkbox-label-placement-${labelPlacement}`]: true,
161
- }) }, index.h("label", { key: '84d4c33da0348dc65ad36fb0fafd48be366dcf3b', class: "checkbox-wrapper", htmlFor: inputId }, index.h("input", Object.assign({ key: '427db69a3ab8a17aa0867519c90f585b8930406b', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), index.h("div", { key: '9dda7024b3a4f1ee55351f783f9a10f9b4ad0d12', class: {
213
+ }) }, index.h("label", { key: '7a3d7f3c27dde514f2dbf2e34f4629fad33ec3bf', class: "checkbox-wrapper", htmlFor: inputId }, index.h("input", Object.assign({ key: '4130d77ddf034271fecccda14e101a5a809921b6', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), index.h("div", { key: '5daa74f4e62b0947e37764762524001ee42609d9', class: {
162
214
  'label-text-wrapper': true,
163
215
  'label-text-wrapper-hidden': !hasLabelContent,
164
- }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, index.h("slot", { key: 'f9d1d545ffd4164b650808241b51ea1bedc6a42c' }), this.renderHintText()), index.h("div", { key: 'a96d61ac324864228f14caa0e9f2c0d15418882e', class: "native-wrapper" }, index.h("svg", { key: '64ff3e4d87e190601811ef64323edec18d510cd1', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
216
+ }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, index.h("slot", { key: '23ff66138f8c3a2f56f39113fc842d54b2f7952a' }), this.renderHintText()), index.h("div", { key: 'ab914d9623c19fc46821d5e62db92f1192ebbe7e', class: "native-wrapper" }, index.h("svg", { key: '66e3f4f5dcaa9756fb0e9452299954f9ed3dcb7b', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
165
217
  }
166
218
  getSVGPath(mode, indeterminate) {
167
219
  let path = indeterminate ? (index.h("path", { d: "M6 12L18 12", part: "mark" })) : (index.h("path", { d: "M5.9,12.5l3.8,3.8l8.8-8.8", part: "mark" }));
@@ -4,7 +4,8 @@
4
4
  'use strict';
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
- var validity = require('./validity-C8QoAYT2.js');
7
+ var notchController = require('./notch-controller-Bzqhjm4f.js');
8
+ var validity = require('./validity-BpS37YFM.js');
8
9
  var helpers = require('./helpers-DrTqNghc.js');
9
10
  var input_utils = require('./input.utils-B_QROI2g.js');
10
11
  var theme = require('./theme-CeDs6Hcv.js');
@@ -235,7 +236,7 @@ const Input = class {
235
236
  connectedCallback() {
236
237
  const { el } = this;
237
238
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
238
- this.notchController = validity.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
239
+ this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
239
240
  // Watch for class changes to update validation state
240
241
  if (typeof MutationObserver !== 'undefined') {
241
242
  this.validationObserver = new MutationObserver(() => {
@@ -8,6 +8,7 @@ var helpers = require('./helpers-DrTqNghc.js');
8
8
  var compareWithUtils = require('./compare-with-utils-DSicavqM.js');
9
9
  var theme = require('./theme-CeDs6Hcv.js');
10
10
  var ionicGlobal = require('./ionic-global-HMVqOFGO.js');
11
+ var validity = require('./validity-BpS37YFM.js');
11
12
 
12
13
  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}";
13
14
 
@@ -177,6 +178,10 @@ const RadioGroup = class {
177
178
  this.helperTextId = `${this.inputId}-helper-text`;
178
179
  this.errorTextId = `${this.inputId}-error-text`;
179
180
  this.labelId = `${this.inputId}-lbl`;
181
+ /**
182
+ * Track validation state for proper aria-live announcements.
183
+ */
184
+ this.isInvalid = false;
180
185
  /**
181
186
  * If `true`, the radios can be deselected.
182
187
  */
@@ -258,6 +263,52 @@ const RadioGroup = class {
258
263
  this.labelId = label.id = this.name + '-lbl';
259
264
  }
260
265
  }
266
+ // Watch for class changes to update validation state.
267
+ if (typeof MutationObserver !== 'undefined') {
268
+ this.validationObserver = new MutationObserver(() => {
269
+ const newIsInvalid = validity.checkInvalidState(this.el);
270
+ if (this.isInvalid !== newIsInvalid) {
271
+ this.isInvalid = newIsInvalid;
272
+ /**
273
+ * Screen readers tend to announce changes
274
+ * to `aria-describedby` when the attribute
275
+ * is changed during a blur event for a
276
+ * native form control.
277
+ * However, the announcement can be spotty
278
+ * when using a non-native form control
279
+ * and `forceUpdate()`.
280
+ * This is due to `forceUpdate()` internally
281
+ * rescheduling the DOM update to a lower
282
+ * priority queue regardless if it's called
283
+ * inside a Promise or not, thus causing
284
+ * the screen reader to potentially miss the
285
+ * change.
286
+ * By using a State variable inside a Promise,
287
+ * it guarantees a re-render immediately at
288
+ * a higher priority.
289
+ */
290
+ Promise.resolve().then(() => {
291
+ this.hintTextId = this.getHintTextId();
292
+ });
293
+ }
294
+ });
295
+ this.validationObserver.observe(this.el, {
296
+ attributes: true,
297
+ attributeFilter: ['class'],
298
+ });
299
+ }
300
+ // Always set initial state
301
+ this.isInvalid = validity.checkInvalidState(this.el);
302
+ }
303
+ componentWillLoad() {
304
+ this.hintTextId = this.getHintTextId();
305
+ }
306
+ disconnectedCallback() {
307
+ // Clean up validation observer to prevent memory leaks.
308
+ if (this.validationObserver) {
309
+ this.validationObserver.disconnect();
310
+ this.validationObserver = undefined;
311
+ }
261
312
  }
262
313
  getRadios() {
263
314
  return Array.from(this.el.querySelectorAll('ion-radio'));
@@ -333,16 +384,16 @@ const RadioGroup = class {
333
384
  * Renders the helper text or error text values
334
385
  */
335
386
  renderHintText() {
336
- const { helperText, errorText, helperTextId, errorTextId } = this;
387
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
337
388
  const hasHintText = !!helperText || !!errorText;
338
389
  if (!hasHintText) {
339
390
  return;
340
391
  }
341
- return (index.h("div", { class: "radio-group-top" }, index.h("div", { id: helperTextId, class: "helper-text" }, helperText), index.h("div", { id: errorTextId, class: "error-text" }, errorText)));
392
+ return (index.h("div", { class: "radio-group-top" }, index.h("div", { id: helperTextId, class: "helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), index.h("div", { id: errorTextId, class: "error-text", role: "alert" }, isInvalid ? errorText : null)));
342
393
  }
343
- getHintTextID() {
344
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
345
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
394
+ getHintTextId() {
395
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
396
+ if (isInvalid && errorText) {
346
397
  return errorTextId;
347
398
  }
348
399
  if (helperText) {
@@ -354,7 +405,7 @@ const RadioGroup = class {
354
405
  const { label, labelId, el, name, value } = this;
355
406
  const mode = ionicGlobal.getIonMode(this);
356
407
  helpers.renderHiddenInput(true, el, name, value, false);
357
- return (index.h(index.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(), index.h("div", { key: '45b09efc10776b889a8f372cba80d25a3fc849da', class: "radio-group-wrapper" }, index.h("slot", { key: '58714934542c2fdd7396de160364f3f06b32e8f8' }))));
408
+ return (index.h(index.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(), index.h("div", { key: '85045b45a0100a45f3b9a35d1c5a25ec63d525c4', class: "radio-group-wrapper" }, index.h("slot", { key: '53dacb87ce62398e78771fb2efaf839ab922d946' }))));
358
409
  }
359
410
  get el() { return index.getElement(this); }
360
411
  static get watchers() { return {
@@ -4,8 +4,9 @@
4
4
  'use strict';
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
- var validity = require('./validity-C8QoAYT2.js');
7
+ var notchController = require('./notch-controller-Bzqhjm4f.js');
8
8
  var compareWithUtils = require('./compare-with-utils-DSicavqM.js');
9
+ var validity = require('./validity-BpS37YFM.js');
9
10
  var helpers = require('./helpers-DrTqNghc.js');
10
11
  var overlays = require('./overlays-DxIZwUXI.js');
11
12
  var dir = require('./dir-Cn0z1rJH.js');
@@ -165,7 +166,7 @@ const Select = class {
165
166
  }
166
167
  async connectedCallback() {
167
168
  const { el } = this;
168
- this.notchController = validity.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
169
+ this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
169
170
  this.updateOverlayOptions();
170
171
  this.emitStyle();
171
172
  this.mutationO = watchOptions.watchForOptions(this.el, 'ion-select-option', async () => {
@@ -203,7 +204,7 @@ const Select = class {
203
204
  * a higher priority.
204
205
  */
205
206
  Promise.resolve().then(() => {
206
- this.hintTextID = this.getHintTextID();
207
+ this.hintTextId = this.getHintTextId();
207
208
  });
208
209
  }
209
210
  });
@@ -217,7 +218,7 @@ const Select = class {
217
218
  }
218
219
  componentWillLoad() {
219
220
  this.inheritedAttributes = helpers.inheritAttributes(this.el, ['aria-label']);
220
- this.hintTextID = this.getHintTextID();
221
+ this.hintTextId = this.getHintTextId();
221
222
  }
222
223
  componentDidLoad() {
223
224
  /**
@@ -716,9 +717,9 @@ const Select = class {
716
717
  }
717
718
  renderListbox() {
718
719
  const { disabled, inputId, isExpanded, required } = this;
719
- return (index.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) }));
720
+ return (index.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) }));
720
721
  }
721
- getHintTextID() {
722
+ getHintTextId() {
722
723
  const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
723
724
  if (isInvalid && errorText) {
724
725
  return errorTextId;
@@ -4,7 +4,8 @@
4
4
  'use strict';
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
- var validity = require('./validity-C8QoAYT2.js');
7
+ var notchController = require('./notch-controller-Bzqhjm4f.js');
8
+ var validity = require('./validity-BpS37YFM.js');
8
9
  var helpers = require('./helpers-DrTqNghc.js');
9
10
  var input_utils = require('./input.utils-B_QROI2g.js');
10
11
  var theme = require('./theme-CeDs6Hcv.js');
@@ -195,7 +196,7 @@ const Textarea = class {
195
196
  connectedCallback() {
196
197
  const { el } = this;
197
198
  this.slotMutationController = input_utils.createSlotMutationController(el, ['label', 'start', 'end'], () => index.forceUpdate(this));
198
- this.notchController = validity.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
199
+ this.notchController = notchController.createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
199
200
  // Watch for class changes to update validation state
200
201
  if (typeof MutationObserver !== 'undefined') {
201
202
  this.validationObserver = new MutationObserver(() => {
@@ -5,6 +5,7 @@
5
5
 
6
6
  var index = require('./index-D6Wc6v08.js');
7
7
  var helpers = require('./helpers-DrTqNghc.js');
8
+ var validity = require('./validity-BpS37YFM.js');
8
9
  var haptic = require('./haptic-ClPPQ_PS.js');
9
10
  var ionicGlobal = require('./ionic-global-HMVqOFGO.js');
10
11
  var dir = require('./dir-Cn0z1rJH.js');
@@ -31,6 +32,10 @@ const Toggle = class {
31
32
  this.inheritedAttributes = {};
32
33
  this.didLoad = false;
33
34
  this.activated = false;
35
+ /**
36
+ * Track validation state for proper aria-live announcements.
37
+ */
38
+ this.isInvalid = false;
34
39
  /**
35
40
  * The name of the control, which is submitted with the form data.
36
41
  */
@@ -144,15 +149,52 @@ const Toggle = class {
144
149
  });
145
150
  }
146
151
  async connectedCallback() {
152
+ const { didLoad, el } = this;
147
153
  /**
148
154
  * If we have not yet rendered
149
155
  * ion-toggle, then toggleTrack is not defined.
150
156
  * But if we are moving ion-toggle via appendChild,
151
157
  * then toggleTrack will be defined.
152
158
  */
153
- if (this.didLoad) {
159
+ if (didLoad) {
154
160
  this.setupGesture();
155
161
  }
162
+ // Watch for class changes to update validation state.
163
+ if (typeof MutationObserver !== 'undefined') {
164
+ this.validationObserver = new MutationObserver(() => {
165
+ const newIsInvalid = validity.checkInvalidState(el);
166
+ if (this.isInvalid !== newIsInvalid) {
167
+ this.isInvalid = newIsInvalid;
168
+ /**
169
+ * Screen readers tend to announce changes
170
+ * to `aria-describedby` when the attribute
171
+ * is changed during a blur event for a
172
+ * native form control.
173
+ * However, the announcement can be spotty
174
+ * when using a non-native form control
175
+ * and `forceUpdate()`.
176
+ * This is due to `forceUpdate()` internally
177
+ * rescheduling the DOM update to a lower
178
+ * priority queue regardless if it's called
179
+ * inside a Promise or not, thus causing
180
+ * the screen reader to potentially miss the
181
+ * change.
182
+ * By using a State variable inside a Promise,
183
+ * it guarantees a re-render immediately at
184
+ * a higher priority.
185
+ */
186
+ Promise.resolve().then(() => {
187
+ this.hintTextId = this.getHintTextId();
188
+ });
189
+ }
190
+ });
191
+ this.validationObserver.observe(el, {
192
+ attributes: true,
193
+ attributeFilter: ['class'],
194
+ });
195
+ }
196
+ // Always set initial state
197
+ this.isInvalid = validity.checkInvalidState(el);
156
198
  }
157
199
  componentDidLoad() {
158
200
  this.setupGesture();
@@ -163,9 +205,15 @@ const Toggle = class {
163
205
  this.gesture.destroy();
164
206
  this.gesture = undefined;
165
207
  }
208
+ // Clean up validation observer to prevent memory leaks.
209
+ if (this.validationObserver) {
210
+ this.validationObserver.disconnect();
211
+ this.validationObserver = undefined;
212
+ }
166
213
  }
167
214
  componentWillLoad() {
168
215
  this.inheritedAttributes = Object.assign({}, helpers.inheritAriaAttributes(this.el));
216
+ this.hintTextId = this.getHintTextId();
169
217
  }
170
218
  onStart() {
171
219
  this.activated = true;
@@ -206,9 +254,9 @@ const Toggle = class {
206
254
  get hasLabel() {
207
255
  return this.el.textContent !== '';
208
256
  }
209
- getHintTextID() {
210
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
211
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
257
+ getHintTextId() {
258
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
259
+ if (isInvalid && errorText) {
212
260
  return errorTextId;
213
261
  }
214
262
  if (helperText) {
@@ -221,7 +269,7 @@ const Toggle = class {
221
269
  * This element should only be rendered if hint text is set.
222
270
  */
223
271
  renderHintText() {
224
- const { helperText, errorText, helperTextId, errorTextId } = this;
272
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
225
273
  /**
226
274
  * undefined and empty string values should
227
275
  * be treated as not having helper/error text.
@@ -230,15 +278,15 @@ const Toggle = class {
230
278
  if (!hasHintText) {
231
279
  return;
232
280
  }
233
- return (index.h("div", { class: "toggle-bottom" }, index.h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText), index.h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText)));
281
+ return (index.h("div", { class: "toggle-bottom" }, index.h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), index.h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null)));
234
282
  }
235
283
  render() {
236
- const { activated, alignment, checked, color, disabled, el, errorTextId, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
284
+ const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
237
285
  const mode = ionicGlobal.getIonMode(this);
238
286
  const value = this.getValue();
239
287
  const rtl = dir.isRTL(el) ? 'rtl' : 'ltr';
240
288
  helpers.renderHiddenInput(true, el, name, checked ? value : '', disabled);
241
- return (index.h(index.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: theme.createColorClasses(color, {
289
+ return (index.h(index.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: theme.createColorClasses(color, {
242
290
  [mode]: true,
243
291
  'in-item': theme.hostContext('ion-item', el),
244
292
  'toggle-activated': activated,
@@ -248,10 +296,10 @@ const Toggle = class {
248
296
  [`toggle-alignment-${alignment}`]: alignment !== undefined,
249
297
  [`toggle-label-placement-${labelPlacement}`]: true,
250
298
  [`toggle-${rtl}`]: true,
251
- }) }, index.h("label", { key: '673625b62a2c909e95dccb642c91312967a6cd1c', class: "toggle-wrapper", htmlFor: inputId }, index.h("input", Object.assign({ key: '7dc3f357b4708116663970047765da9f8f845bf0', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), index.h("div", { key: '8f1c6a182031e8cbc6727e5f4ac0e00ad4247447', class: {
299
+ }) }, index.h("label", { key: '3027f2ac4be6de422a14486d847fbee77f615db1', class: "toggle-wrapper", htmlFor: inputId }, index.h("input", Object.assign({ key: '4b0304c9e879e432b80184b4e5de37d55c11b436', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), index.h("div", { key: '8ef265ec942e7f01ff31cbb202ed146c6bf94e02', class: {
252
300
  'label-text-wrapper': true,
253
301
  'label-text-wrapper-hidden': !hasLabel,
254
- }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, index.h("slot", { key: '8322b9d54dc7edeb4e16fefcde9f7ebca8d5c3e1' }), this.renderHintText()), index.h("div", { key: 'fe6984143db817a7b3020a3f57cf5418fc3dcc0e', class: "native-wrapper" }, this.renderToggleControl()))));
302
+ }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, index.h("slot", { key: '7b162b7dd27199cca2a4c995276a18b9f8e44aaf' }), this.renderHintText()), index.h("div", { key: 'd13c34bd42fca01cc73ddb4ea7e471b33a282a3e', class: "native-wrapper" }, this.renderToggleControl()))));
255
303
  }
256
304
  get el() { return index.getElement(this); }
257
305
  static get watchers() { return {