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 { i as inheritAriaAttributes, e as renderHiddenInput } from './helpers.js';
6
+ import { c as checkInvalidState } from './validity.js';
6
7
  import { c as createColorClasses, h as hostContext } from './theme.js';
7
8
  import { b as getIonMode } from './ionic-global.js';
8
9
 
@@ -63,6 +64,10 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
63
64
  * submitting if the value is invalid.
64
65
  */
65
66
  this.required = false;
67
+ /**
68
+ * Track validation state for proper aria-live announcements.
69
+ */
70
+ this.isInvalid = false;
66
71
  /**
67
72
  * Sets the checked property and emits
68
73
  * the ionChange event. Use this to update the
@@ -109,16 +114,63 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
109
114
  ev.stopPropagation();
110
115
  };
111
116
  }
117
+ connectedCallback() {
118
+ const { el } = this;
119
+ // Watch for class changes to update validation state.
120
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
121
+ this.validationObserver = new MutationObserver(() => {
122
+ const newIsInvalid = checkInvalidState(el);
123
+ if (this.isInvalid !== newIsInvalid) {
124
+ this.isInvalid = newIsInvalid;
125
+ /**
126
+ * Screen readers tend to announce changes
127
+ * to `aria-describedby` when the attribute
128
+ * is changed during a blur event for a
129
+ * native form control.
130
+ * However, the announcement can be spotty
131
+ * when using a non-native form control
132
+ * and `forceUpdate()`.
133
+ * This is due to `forceUpdate()` internally
134
+ * rescheduling the DOM update to a lower
135
+ * priority queue regardless if it's called
136
+ * inside a Promise or not, thus causing
137
+ * the screen reader to potentially miss the
138
+ * change.
139
+ * By using a State variable inside a Promise,
140
+ * it guarantees a re-render immediately at
141
+ * a higher priority.
142
+ */
143
+ Promise.resolve().then(() => {
144
+ this.hintTextId = this.getHintTextId();
145
+ });
146
+ }
147
+ });
148
+ this.validationObserver.observe(el, {
149
+ attributes: true,
150
+ attributeFilter: ['class'],
151
+ });
152
+ }
153
+ // Always set initial state
154
+ this.isInvalid = checkInvalidState(el);
155
+ }
112
156
  componentWillLoad() {
113
157
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
158
+ this.hintTextId = this.getHintTextId();
159
+ }
160
+ disconnectedCallback() {
161
+ // Clean up validation observer to prevent memory leaks.
162
+ if (this.validationObserver) {
163
+ this.validationObserver.disconnect();
164
+ this.validationObserver = undefined;
165
+ }
114
166
  }
115
167
  /** @internal */
116
168
  async setFocus() {
117
169
  this.el.focus();
118
170
  }
119
- getHintTextID() {
120
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
121
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
171
+ getHintTextId() {
172
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
173
+ if (isInvalid && errorText) {
122
174
  return errorTextId;
123
175
  }
124
176
  if (helperText) {
@@ -131,7 +183,7 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
131
183
  * This element should only be rendered if hint text is set.
132
184
  */
133
185
  renderHintText() {
134
- const { helperText, errorText, helperTextId, errorTextId } = this;
186
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
135
187
  /**
136
188
  * undefined and empty string values should
137
189
  * be treated as not having helper/error text.
@@ -140,7 +192,7 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
140
192
  if (!hasHintText) {
141
193
  return;
142
194
  }
143
- 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)));
195
+ 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)));
144
196
  }
145
197
  render() {
146
198
  const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, alignment, required, } = this;
@@ -150,7 +202,7 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
150
202
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
151
203
  // The host element must have a checkbox role to ensure proper VoiceOver
152
204
  // support in Safari for accessibility.
153
- 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, {
205
+ 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, {
154
206
  [mode]: true,
155
207
  'in-item': hostContext('ion-item', el),
156
208
  'checkbox-checked': checked,
@@ -160,10 +212,10 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
160
212
  [`checkbox-justify-${justify}`]: justify !== undefined,
161
213
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
162
214
  [`checkbox-label-placement-${labelPlacement}`]: true,
163
- }) }, 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: {
215
+ }) }, 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: {
164
216
  'label-text-wrapper': true,
165
217
  'label-text-wrapper-hidden': !hasLabelContent,
166
- }, 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)))));
218
+ }, 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)))));
167
219
  }
168
220
  getSVGPath(mode, indeterminate) {
169
221
  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" }));
@@ -190,6 +242,8 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
190
242
  "justify": [1],
191
243
  "alignment": [1],
192
244
  "required": [4],
245
+ "isInvalid": [32],
246
+ "hintTextId": [32],
193
247
  "setFocus": [64]
194
248
  }]);
195
249
  let checkboxIds = 0;
@@ -790,6 +790,28 @@ const Datetime = /*@__PURE__*/ proxyCustomElement(class Datetime extends HTMLEle
790
790
  destroyKeyboardMO();
791
791
  }
792
792
  };
793
+ /**
794
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
795
+ * Fallback to ensure the datetime becomes ready even if
796
+ * IntersectionObserver never reports it as intersecting.
797
+ *
798
+ * This is primarily used in environments where the observer
799
+ * might not fire as expected, such as when running under
800
+ * synthetic tests that stub IntersectionObserver.
801
+ */
802
+ this.ensureReadyIfVisible = () => {
803
+ if (this.el.classList.contains('datetime-ready')) {
804
+ return;
805
+ }
806
+ const rect = this.el.getBoundingClientRect();
807
+ if (rect.width === 0 || rect.height === 0) {
808
+ return;
809
+ }
810
+ this.initializeListeners();
811
+ writeTask(() => {
812
+ this.el.classList.add('datetime-ready');
813
+ });
814
+ };
793
815
  this.processValue = (value) => {
794
816
  const hasValue = value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
795
817
  const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
@@ -1107,6 +1129,17 @@ const Datetime = /*@__PURE__*/ proxyCustomElement(class Datetime extends HTMLEle
1107
1129
  * triggering the `hiddenIO` observer below.
1108
1130
  */
1109
1131
  raf(() => visibleIO === null || visibleIO === void 0 ? void 0 : visibleIO.observe(intersectionTrackerRef));
1132
+ /**
1133
+ * TODO(FW-6931): Remove this fallback upon solving the root cause
1134
+ * Fallback: If IntersectionObserver never reports that the
1135
+ * datetime is visible but the host clearly has layout, ensure
1136
+ * we still initialize listeners and mark the component as ready.
1137
+ *
1138
+ * We schedule this after everything has had a chance to run.
1139
+ */
1140
+ setTimeout(() => {
1141
+ this.ensureReadyIfVisible();
1142
+ }, 100);
1110
1143
  /**
1111
1144
  * We need to clean up listeners when the datetime is hidden
1112
1145
  * in a popover/modal so that we can properly scroll containers
@@ -1862,7 +1895,7 @@ const Datetime = /*@__PURE__*/ proxyCustomElement(class Datetime extends HTMLEle
1862
1895
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1863
1896
  const hasWheelVariant = hasDatePresentation && preferWheel;
1864
1897
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1865
- return (h(Host, { key: '57492534800ea059a7c2bbd9f0059cc0b75ae8d2', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1898
+ return (h(Host, { key: 'efdbc0922670a841bc667ceac392cdc1dedffd01', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1866
1899
  [mode]: true,
1867
1900
  ['datetime-readonly']: readonly,
1868
1901
  ['datetime-disabled']: disabled,
@@ -1872,7 +1905,7 @@ const Datetime = /*@__PURE__*/ proxyCustomElement(class Datetime extends HTMLEle
1872
1905
  [`datetime-size-${size}`]: true,
1873
1906
  [`datetime-prefer-wheel`]: hasWheelVariant,
1874
1907
  [`datetime-grid`]: isGridStyle,
1875
- })) }, h("div", { key: '97dac5e5195635ac0bc5fb472b9d09e5c3c6bbc3', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1908
+ })) }, h("div", { key: '3f8bb75fcb0baff55182ef3aa1b535eacc58d81f', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1876
1909
  }
1877
1910
  get el() { return this; }
1878
1911
  static get watchers() { return {
@@ -2,7 +2,8 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, h, Host } from '@stencil/core/internal/client';
5
- import { c as createNotchController, a as checkInvalidState } from './validity.js';
5
+ import { c as createNotchController } from './notch-controller.js';
6
+ import { c as checkInvalidState } from './validity.js';
6
7
  import { l as debounceEvent, i as inheritAriaAttributes, d as inheritAttributes, c as componentOnReady } from './helpers.js';
7
8
  import { c as createSlotMutationController, g as getCounterText } from './input.utils.js';
8
9
  import { h as hostContext, c as createColorClasses } from './theme.js';
@@ -2,8 +2,9 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, Build, h, Host, forceUpdate } from '@stencil/core/internal/client';
5
- import { c as createNotchController, a as checkInvalidState } from './validity.js';
5
+ import { c as createNotchController } from './notch-controller.js';
6
6
  import { i as isOptionSelected, d as defineCustomElement$8, c as compareOptions } from './radio.js';
7
+ import { c as checkInvalidState } from './validity.js';
7
8
  import { d as inheritAttributes, e as renderHiddenInput, h as focusVisibleElement } from './helpers.js';
8
9
  import { p as printIonWarning } from './index4.js';
9
10
  import { c as popoverController, b as actionSheetController, a as alertController, m as modalController } from './overlays.js';
@@ -223,7 +224,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
223
224
  * a higher priority.
224
225
  */
225
226
  Promise.resolve().then(() => {
226
- this.hintTextID = this.getHintTextID();
227
+ this.hintTextId = this.getHintTextId();
227
228
  });
228
229
  }
229
230
  });
@@ -237,7 +238,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
237
238
  }
238
239
  componentWillLoad() {
239
240
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
240
- this.hintTextID = this.getHintTextID();
241
+ this.hintTextId = this.getHintTextId();
241
242
  }
242
243
  componentDidLoad() {
243
244
  /**
@@ -736,9 +737,9 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
736
737
  }
737
738
  renderListbox() {
738
739
  const { disabled, inputId, isExpanded, required } = this;
739
- 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) }));
740
+ 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) }));
740
741
  }
741
- getHintTextID() {
742
+ getHintTextId() {
742
743
  const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
743
744
  if (isInvalid && errorText) {
744
745
  return errorTextId;
@@ -860,7 +861,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
860
861
  "isExpanded": [32],
861
862
  "hasFocus": [32],
862
863
  "isInvalid": [32],
863
- "hintTextID": [32],
864
+ "hintTextId": [32],
864
865
  "open": [64]
865
866
  }, undefined, {
866
867
  "disabled": ["styleChanged"],
@@ -2,7 +2,8 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, writeTask, h, Host } from '@stencil/core/internal/client';
5
- import { c as createNotchController, a as checkInvalidState } from './validity.js';
5
+ import { c as createNotchController } from './notch-controller.js';
6
+ import { c as checkInvalidState } from './validity.js';
6
7
  import { l as debounceEvent, i as inheritAriaAttributes, d as inheritAttributes, c as componentOnReady } from './helpers.js';
7
8
  import { c as createSlotMutationController, g as getCounterText } from './input.utils.js';
8
9
  import { h as hostContext, c as createColorClasses } from './theme.js';
@@ -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 { i as inheritAriaAttributes, e as renderHiddenInput } from './helpers.js';
6
+ import { c as checkInvalidState } from './validity.js';
6
7
  import { d as hapticSelection } from './haptic.js';
7
8
  import { a as isPlatform, b as getIonMode } from './ionic-global.js';
8
9
  import { i as isRTL } from './dir.js';
@@ -33,6 +34,10 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
33
34
  this.inheritedAttributes = {};
34
35
  this.didLoad = false;
35
36
  this.activated = false;
37
+ /**
38
+ * Track validation state for proper aria-live announcements.
39
+ */
40
+ this.isInvalid = false;
36
41
  /**
37
42
  * The name of the control, which is submitted with the form data.
38
43
  */
@@ -146,15 +151,52 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
146
151
  });
147
152
  }
148
153
  async connectedCallback() {
154
+ const { didLoad, el } = this;
149
155
  /**
150
156
  * If we have not yet rendered
151
157
  * ion-toggle, then toggleTrack is not defined.
152
158
  * But if we are moving ion-toggle via appendChild,
153
159
  * then toggleTrack will be defined.
154
160
  */
155
- if (this.didLoad) {
161
+ if (didLoad) {
156
162
  this.setupGesture();
157
163
  }
164
+ // Watch for class changes to update validation state.
165
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
166
+ this.validationObserver = new MutationObserver(() => {
167
+ const newIsInvalid = checkInvalidState(el);
168
+ if (this.isInvalid !== newIsInvalid) {
169
+ this.isInvalid = newIsInvalid;
170
+ /**
171
+ * Screen readers tend to announce changes
172
+ * to `aria-describedby` when the attribute
173
+ * is changed during a blur event for a
174
+ * native form control.
175
+ * However, the announcement can be spotty
176
+ * when using a non-native form control
177
+ * and `forceUpdate()`.
178
+ * This is due to `forceUpdate()` internally
179
+ * rescheduling the DOM update to a lower
180
+ * priority queue regardless if it's called
181
+ * inside a Promise or not, thus causing
182
+ * the screen reader to potentially miss the
183
+ * change.
184
+ * By using a State variable inside a Promise,
185
+ * it guarantees a re-render immediately at
186
+ * a higher priority.
187
+ */
188
+ Promise.resolve().then(() => {
189
+ this.hintTextId = this.getHintTextId();
190
+ });
191
+ }
192
+ });
193
+ this.validationObserver.observe(el, {
194
+ attributes: true,
195
+ attributeFilter: ['class'],
196
+ });
197
+ }
198
+ // Always set initial state
199
+ this.isInvalid = checkInvalidState(el);
158
200
  }
159
201
  componentDidLoad() {
160
202
  this.setupGesture();
@@ -165,9 +207,15 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
165
207
  this.gesture.destroy();
166
208
  this.gesture = undefined;
167
209
  }
210
+ // Clean up validation observer to prevent memory leaks.
211
+ if (this.validationObserver) {
212
+ this.validationObserver.disconnect();
213
+ this.validationObserver = undefined;
214
+ }
168
215
  }
169
216
  componentWillLoad() {
170
217
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
218
+ this.hintTextId = this.getHintTextId();
171
219
  }
172
220
  onStart() {
173
221
  this.activated = true;
@@ -208,9 +256,9 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
208
256
  get hasLabel() {
209
257
  return this.el.textContent !== '';
210
258
  }
211
- getHintTextID() {
212
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
213
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
259
+ getHintTextId() {
260
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
261
+ if (isInvalid && errorText) {
214
262
  return errorTextId;
215
263
  }
216
264
  if (helperText) {
@@ -223,7 +271,7 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
223
271
  * This element should only be rendered if hint text is set.
224
272
  */
225
273
  renderHintText() {
226
- const { helperText, errorText, helperTextId, errorTextId } = this;
274
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
227
275
  /**
228
276
  * undefined and empty string values should
229
277
  * be treated as not having helper/error text.
@@ -232,15 +280,15 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
232
280
  if (!hasHintText) {
233
281
  return;
234
282
  }
235
- 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)));
283
+ 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)));
236
284
  }
237
285
  render() {
238
- const { activated, alignment, checked, color, disabled, el, errorTextId, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
286
+ const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
239
287
  const mode = getIonMode(this);
240
288
  const value = this.getValue();
241
289
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
242
290
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
243
- 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, {
291
+ 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, {
244
292
  [mode]: true,
245
293
  'in-item': hostContext('ion-item', el),
246
294
  'toggle-activated': activated,
@@ -250,10 +298,10 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
250
298
  [`toggle-alignment-${alignment}`]: alignment !== undefined,
251
299
  [`toggle-label-placement-${labelPlacement}`]: true,
252
300
  [`toggle-${rtl}`]: true,
253
- }) }, 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: {
301
+ }) }, 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: {
254
302
  'label-text-wrapper': true,
255
303
  'label-text-wrapper-hidden': !hasLabel,
256
- }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '8322b9d54dc7edeb4e16fefcde9f7ebca8d5c3e1' }), this.renderHintText()), h("div", { key: 'fe6984143db817a7b3020a3f57cf5418fc3dcc0e', class: "native-wrapper" }, this.renderToggleControl()))));
304
+ }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '7b162b7dd27199cca2a4c995276a18b9f8e44aaf' }), this.renderHintText()), h("div", { key: 'd13c34bd42fca01cc73ddb4ea7e471b33a282a3e', class: "native-wrapper" }, this.renderToggleControl()))));
257
305
  }
258
306
  get el() { return this; }
259
307
  static get watchers() { return {
@@ -276,7 +324,9 @@ const Toggle = /*@__PURE__*/ proxyCustomElement(class Toggle extends HTMLElement
276
324
  "justify": [1],
277
325
  "alignment": [1],
278
326
  "required": [4],
279
- "activated": [32]
327
+ "activated": [32],
328
+ "isInvalid": [32],
329
+ "hintTextId": [32]
280
330
  }, undefined, {
281
331
  "disabled": ["disabledChanged"]
282
332
  }]);
@@ -0,0 +1,153 @@
1
+ /*!
2
+ * (C) Ionic http://ionicframework.com - MIT License
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
+ export { createNotchController as c };