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
@@ -1,8 +1,9 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
4
+ import { proxyCustomElement, HTMLElement, createEvent, Build, h, Host } from '@stencil/core/internal/client';
5
5
  import { e as renderHiddenInput } from './helpers.js';
6
+ import { c as checkInvalidState } from './validity.js';
6
7
  import { b as getIonMode } from './ionic-global.js';
7
8
 
8
9
  const radioGroupIosCss = "ion-radio-group{vertical-align:top}.radio-group-wrapper{display:inline}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
@@ -21,6 +22,10 @@ const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTM
21
22
  this.helperTextId = `${this.inputId}-helper-text`;
22
23
  this.errorTextId = `${this.inputId}-error-text`;
23
24
  this.labelId = `${this.inputId}-lbl`;
25
+ /**
26
+ * Track validation state for proper aria-live announcements.
27
+ */
28
+ this.isInvalid = false;
24
29
  /**
25
30
  * If `true`, the radios can be deselected.
26
31
  */
@@ -102,6 +107,52 @@ const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTM
102
107
  this.labelId = label.id = this.name + '-lbl';
103
108
  }
104
109
  }
110
+ // Watch for class changes to update validation state.
111
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
112
+ this.validationObserver = new MutationObserver(() => {
113
+ const newIsInvalid = checkInvalidState(this.el);
114
+ if (this.isInvalid !== newIsInvalid) {
115
+ this.isInvalid = newIsInvalid;
116
+ /**
117
+ * Screen readers tend to announce changes
118
+ * to `aria-describedby` when the attribute
119
+ * is changed during a blur event for a
120
+ * native form control.
121
+ * However, the announcement can be spotty
122
+ * when using a non-native form control
123
+ * and `forceUpdate()`.
124
+ * This is due to `forceUpdate()` internally
125
+ * rescheduling the DOM update to a lower
126
+ * priority queue regardless if it's called
127
+ * inside a Promise or not, thus causing
128
+ * the screen reader to potentially miss the
129
+ * change.
130
+ * By using a State variable inside a Promise,
131
+ * it guarantees a re-render immediately at
132
+ * a higher priority.
133
+ */
134
+ Promise.resolve().then(() => {
135
+ this.hintTextId = this.getHintTextId();
136
+ });
137
+ }
138
+ });
139
+ this.validationObserver.observe(this.el, {
140
+ attributes: true,
141
+ attributeFilter: ['class'],
142
+ });
143
+ }
144
+ // Always set initial state
145
+ this.isInvalid = checkInvalidState(this.el);
146
+ }
147
+ componentWillLoad() {
148
+ this.hintTextId = this.getHintTextId();
149
+ }
150
+ disconnectedCallback() {
151
+ // Clean up validation observer to prevent memory leaks.
152
+ if (this.validationObserver) {
153
+ this.validationObserver.disconnect();
154
+ this.validationObserver = undefined;
155
+ }
105
156
  }
106
157
  getRadios() {
107
158
  return Array.from(this.el.querySelectorAll('ion-radio'));
@@ -177,16 +228,16 @@ const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTM
177
228
  * Renders the helper text or error text values
178
229
  */
179
230
  renderHintText() {
180
- const { helperText, errorText, helperTextId, errorTextId } = this;
231
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
181
232
  const hasHintText = !!helperText || !!errorText;
182
233
  if (!hasHintText) {
183
234
  return;
184
235
  }
185
- return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text" }, errorText)));
236
+ 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)));
186
237
  }
187
- getHintTextID() {
188
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
189
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
238
+ getHintTextId() {
239
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
240
+ if (isInvalid && errorText) {
190
241
  return errorTextId;
191
242
  }
192
243
  if (helperText) {
@@ -198,7 +249,7 @@ const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTM
198
249
  const { label, labelId, el, name, value } = this;
199
250
  const mode = getIonMode(this);
200
251
  renderHiddenInput(true, el, name, value, false);
201
- 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' }))));
252
+ 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' }))));
202
253
  }
203
254
  get el() { return this; }
204
255
  static get watchers() { return {
@@ -215,6 +266,8 @@ const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTM
215
266
  "value": [1032],
216
267
  "helperText": [1, "helper-text"],
217
268
  "errorText": [1, "error-text"],
269
+ "isInvalid": [32],
270
+ "hintTextId": [32],
218
271
  "setFocus": [64]
219
272
  }, [[4, "keydown", "onKeydown"]], {
220
273
  "value": ["valueChanged"]
@@ -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" }));
@@ -786,6 +786,28 @@ const Datetime = class {
786
786
  destroyKeyboardMO();
787
787
  }
788
788
  };
789
+ /**
790
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
791
+ * Fallback to ensure the datetime becomes ready even if
792
+ * IntersectionObserver never reports it as intersecting.
793
+ *
794
+ * This is primarily used in environments where the observer
795
+ * might not fire as expected, such as when running under
796
+ * synthetic tests that stub IntersectionObserver.
797
+ */
798
+ this.ensureReadyIfVisible = () => {
799
+ if (this.el.classList.contains('datetime-ready')) {
800
+ return;
801
+ }
802
+ const rect = this.el.getBoundingClientRect();
803
+ if (rect.width === 0 || rect.height === 0) {
804
+ return;
805
+ }
806
+ this.initializeListeners();
807
+ index.writeTask(() => {
808
+ this.el.classList.add('datetime-ready');
809
+ });
810
+ };
789
811
  this.processValue = (value) => {
790
812
  const hasValue = value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
791
813
  const valueToProcess = hasValue ? data.parseDate(value) : this.defaultParts;
@@ -1103,6 +1125,17 @@ const Datetime = class {
1103
1125
  * triggering the `hiddenIO` observer below.
1104
1126
  */
1105
1127
  helpers.raf(() => visibleIO === null || visibleIO === void 0 ? void 0 : visibleIO.observe(intersectionTrackerRef));
1128
+ /**
1129
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
1130
+ * Fallback: If IntersectionObserver never reports that the
1131
+ * datetime is visible but the host clearly has layout, ensure
1132
+ * we still initialize listeners and mark the component as ready.
1133
+ *
1134
+ * We schedule this after everything has had a chance to run.
1135
+ */
1136
+ setTimeout(() => {
1137
+ this.ensureReadyIfVisible();
1138
+ }, 100);
1106
1139
  /**
1107
1140
  * We need to clean up listeners when the datetime is hidden
1108
1141
  * in a popover/modal so that we can properly scroll containers
@@ -1858,7 +1891,7 @@ const Datetime = class {
1858
1891
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1859
1892
  const hasWheelVariant = hasDatePresentation && preferWheel;
1860
1893
  helpers.renderHiddenInput(true, el, name, data.formatValue(value), disabled);
1861
- return (index.h(index.Host, { key: '57492534800ea059a7c2bbd9f0059cc0b75ae8d2', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, theme.createColorClasses(color, {
1894
+ return (index.h(index.Host, { key: 'efdbc0922670a841bc667ceac392cdc1dedffd01', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, theme.createColorClasses(color, {
1862
1895
  [mode]: true,
1863
1896
  ['datetime-readonly']: readonly,
1864
1897
  ['datetime-disabled']: disabled,
@@ -1868,7 +1901,7 @@ const Datetime = class {
1868
1901
  [`datetime-size-${size}`]: true,
1869
1902
  [`datetime-prefer-wheel`]: hasWheelVariant,
1870
1903
  [`datetime-grid`]: isGridStyle,
1871
- })) }, index.h("div", { key: '97dac5e5195635ac0bc5fb472b9d09e5c3c6bbc3', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1904
+ })) }, index.h("div", { key: '3f8bb75fcb0baff55182ef3aa1b535eacc58d81f', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1872
1905
  }
1873
1906
  get el() { return index.getElement(this); }
1874
1907
  static get watchers() { return {
@@ -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;