@ionic/core 7.3.2-dev.11693321763.15a54694 → 7.3.2-dev.11693507138.1f9ed625

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 (181) hide show
  1. package/components/checkbox.js +6 -3
  2. package/components/data.js +31 -7
  3. package/components/framework-delegate.js +2 -21
  4. package/components/ion-datetime-button.js +3 -0
  5. package/components/ion-datetime.js +106 -81
  6. package/components/ion-modal.js +1 -1
  7. package/components/ion-range.js +2 -2
  8. package/components/ion-toggle.js +6 -3
  9. package/components/popover.js +1 -1
  10. package/components/radio.js +6 -3
  11. package/dist/cjs/{data-967b9c15.js → data-db832785.js} +31 -7
  12. package/dist/cjs/{framework-delegate-c0873a6f.js → framework-delegate-e8174951.js} +2 -21
  13. package/dist/cjs/index.cjs.js +2 -2
  14. package/dist/cjs/ion-action-sheet.cjs.entry.js +2 -2
  15. package/dist/cjs/ion-alert.cjs.entry.js +2 -2
  16. package/dist/cjs/ion-app_8.cjs.entry.js +1 -1
  17. package/dist/cjs/ion-checkbox.cjs.entry.js +5 -3
  18. package/dist/cjs/ion-datetime-button.cjs.entry.js +4 -1
  19. package/dist/cjs/ion-datetime_3.cjs.entry.js +108 -84
  20. package/dist/cjs/ion-loading.cjs.entry.js +2 -2
  21. package/dist/cjs/ion-menu_3.cjs.entry.js +2 -2
  22. package/dist/cjs/ion-modal.cjs.entry.js +3 -3
  23. package/dist/cjs/ion-nav_2.cjs.entry.js +1 -1
  24. package/dist/cjs/ion-popover.cjs.entry.js +3 -3
  25. package/dist/cjs/ion-radio_2.cjs.entry.js +5 -3
  26. package/dist/cjs/ion-range.cjs.entry.js +2 -2
  27. package/dist/cjs/ion-select_3.cjs.entry.js +2 -2
  28. package/dist/cjs/ion-tab_2.cjs.entry.js +1 -1
  29. package/dist/cjs/ion-toast.cjs.entry.js +2 -2
  30. package/dist/cjs/ion-toggle.cjs.entry.js +5 -3
  31. package/dist/cjs/ionic.cjs.js +1 -1
  32. package/dist/cjs/loader.cjs.js +1 -1
  33. package/dist/cjs/{overlays-5c1706ed.js → overlays-0ea06fcb.js} +1 -1
  34. package/dist/collection/components/checkbox/checkbox.ios.css +35 -0
  35. package/dist/collection/components/checkbox/checkbox.js +24 -4
  36. package/dist/collection/components/checkbox/checkbox.md.css +35 -0
  37. package/dist/collection/components/checkbox/test/item/checkbox.e2e.js +13 -0
  38. package/dist/collection/components/checkbox/test/label/checkbox.e2e.js +16 -0
  39. package/dist/collection/components/datetime/datetime.js +107 -84
  40. package/dist/collection/components/datetime/test/prefer-wheel/datetime.e2e.js +15 -0
  41. package/dist/collection/components/datetime/test/set-value/datetime.e2e.js +9 -12
  42. package/dist/collection/components/datetime/utils/data.js +14 -6
  43. package/dist/collection/components/datetime/utils/parse.js +18 -1
  44. package/dist/collection/components/datetime-button/datetime-button.js +3 -0
  45. package/dist/collection/components/modal/modal.js +1 -1
  46. package/dist/collection/components/popover/popover.js +1 -1
  47. package/dist/collection/components/radio/radio.ios.css +35 -0
  48. package/dist/collection/components/radio/radio.js +24 -4
  49. package/dist/collection/components/radio/radio.md.css +35 -0
  50. package/dist/collection/components/radio/test/item/radio.e2e.js +15 -0
  51. package/dist/collection/components/radio/test/label-placement/radio.e2e.js +20 -0
  52. package/dist/collection/components/range/range.ios.css +41 -1
  53. package/dist/collection/components/range/range.js +3 -3
  54. package/dist/collection/components/range/range.md.css +41 -1
  55. package/dist/collection/components/range/test/label/range.e2e.js +48 -0
  56. package/dist/collection/components/toggle/test/item/toggle.e2e.js +15 -0
  57. package/dist/collection/components/toggle/test/label/toggle.e2e.js +16 -0
  58. package/dist/collection/components/toggle/toggle.ios.css +35 -1
  59. package/dist/collection/components/toggle/toggle.js +24 -4
  60. package/dist/collection/components/toggle/toggle.md.css +35 -1
  61. package/dist/collection/utils/framework-delegate.js +2 -21
  62. package/dist/docs.json +115 -18
  63. package/dist/esm/{data-4b448e3a.js → data-009dbf15.js} +31 -7
  64. package/dist/esm/{framework-delegate-aa433dea.js → framework-delegate-2b76c681.js} +2 -21
  65. package/dist/esm/index.js +2 -2
  66. package/dist/esm/ion-action-sheet.entry.js +2 -2
  67. package/dist/esm/ion-alert.entry.js +2 -2
  68. package/dist/esm/ion-app_8.entry.js +1 -1
  69. package/dist/esm/ion-checkbox.entry.js +5 -3
  70. package/dist/esm/ion-datetime-button.entry.js +4 -1
  71. package/dist/esm/ion-datetime_3.entry.js +108 -84
  72. package/dist/esm/ion-loading.entry.js +2 -2
  73. package/dist/esm/ion-menu_3.entry.js +2 -2
  74. package/dist/esm/ion-modal.entry.js +3 -3
  75. package/dist/esm/ion-nav_2.entry.js +1 -1
  76. package/dist/esm/ion-popover.entry.js +3 -3
  77. package/dist/esm/ion-radio_2.entry.js +5 -3
  78. package/dist/esm/ion-range.entry.js +2 -2
  79. package/dist/esm/ion-select_3.entry.js +2 -2
  80. package/dist/esm/ion-tab_2.entry.js +1 -1
  81. package/dist/esm/ion-toast.entry.js +2 -2
  82. package/dist/esm/ion-toggle.entry.js +5 -3
  83. package/dist/esm/ionic.js +1 -1
  84. package/dist/esm/loader.js +1 -1
  85. package/dist/esm/{overlays-e1dadfe6.js → overlays-c3387ec0.js} +1 -1
  86. package/dist/esm-es5/data-009dbf15.js +4 -0
  87. package/dist/esm-es5/framework-delegate-2b76c681.js +4 -0
  88. package/dist/esm-es5/index.js +1 -1
  89. package/dist/esm-es5/ion-action-sheet.entry.js +1 -1
  90. package/dist/esm-es5/ion-alert.entry.js +1 -1
  91. package/dist/esm-es5/ion-app_8.entry.js +1 -1
  92. package/dist/esm-es5/ion-checkbox.entry.js +1 -1
  93. package/dist/esm-es5/ion-datetime-button.entry.js +1 -1
  94. package/dist/esm-es5/ion-datetime_3.entry.js +1 -1
  95. package/dist/esm-es5/ion-loading.entry.js +1 -1
  96. package/dist/esm-es5/ion-menu_3.entry.js +1 -1
  97. package/dist/esm-es5/ion-modal.entry.js +1 -1
  98. package/dist/esm-es5/ion-nav_2.entry.js +1 -1
  99. package/dist/esm-es5/ion-popover.entry.js +1 -1
  100. package/dist/esm-es5/ion-radio_2.entry.js +1 -1
  101. package/dist/esm-es5/ion-range.entry.js +1 -1
  102. package/dist/esm-es5/ion-select_3.entry.js +1 -1
  103. package/dist/esm-es5/ion-tab_2.entry.js +1 -1
  104. package/dist/esm-es5/ion-toast.entry.js +1 -1
  105. package/dist/esm-es5/ion-toggle.entry.js +1 -1
  106. package/dist/esm-es5/ionic.js +1 -1
  107. package/dist/esm-es5/loader.js +1 -1
  108. package/dist/esm-es5/{overlays-e1dadfe6.js → overlays-c3387ec0.js} +1 -1
  109. package/dist/html.html-data.json +52 -4
  110. package/dist/ionic/index.esm.js +1 -1
  111. package/dist/ionic/ionic.esm.js +1 -1
  112. package/dist/ionic/{p-84a95033.system.entry.js → p-06f1f560.system.entry.js} +1 -1
  113. package/dist/ionic/p-1f4adf06.system.entry.js +4 -0
  114. package/dist/ionic/{p-c759a704.entry.js → p-20087c90.entry.js} +1 -1
  115. package/dist/ionic/{p-98ebc376.entry.js → p-229df0d8.entry.js} +1 -1
  116. package/dist/ionic/{p-51669f1e.system.entry.js → p-2936ec7f.system.entry.js} +1 -1
  117. package/dist/ionic/{p-e44233f5.entry.js → p-29b3e70a.entry.js} +1 -1
  118. package/dist/ionic/{p-2bff71d5.system.entry.js → p-2bc49e5d.system.entry.js} +1 -1
  119. package/dist/ionic/{p-f2c667c9.system.entry.js → p-2da3e015.system.entry.js} +1 -1
  120. package/dist/ionic/p-2f8bea16.entry.js +4 -0
  121. package/dist/ionic/p-40138199.entry.js +4 -0
  122. package/dist/ionic/{p-7c9dd24d.entry.js → p-423d5a4d.entry.js} +1 -1
  123. package/dist/ionic/p-44ef7224.system.js +4 -0
  124. package/dist/ionic/p-4ca1ce6b.system.entry.js +4 -0
  125. package/dist/ionic/{p-f7faa32d.entry.js → p-5c96dc4e.entry.js} +1 -1
  126. package/dist/ionic/{p-582dc7c1.js → p-5ef91692.js} +1 -1
  127. package/dist/ionic/{p-f9708a54.entry.js → p-61769039.entry.js} +1 -1
  128. package/dist/ionic/{p-d3b8aebf.entry.js → p-67b891af.entry.js} +1 -1
  129. package/dist/ionic/{p-d1d3b740.system.entry.js → p-6b9bf14c.system.entry.js} +1 -1
  130. package/dist/ionic/p-6da27ab8.system.entry.js +4 -0
  131. package/dist/ionic/p-721933ff.system.js +1 -1
  132. package/dist/ionic/{p-f789b7fa.entry.js → p-74723f41.entry.js} +1 -1
  133. package/dist/ionic/{p-28f5715a.system.entry.js → p-74bd26b8.system.entry.js} +1 -1
  134. package/dist/ionic/p-7cfd253c.js +4 -0
  135. package/dist/ionic/{p-45ee211b.system.entry.js → p-8074bdd7.system.entry.js} +1 -1
  136. package/dist/ionic/{p-b32742f3.system.js → p-81b46714.system.js} +1 -1
  137. package/dist/ionic/p-86c5c320.entry.js +4 -0
  138. package/dist/ionic/{p-613d4042.system.js → p-8d9475b5.system.js} +1 -1
  139. package/dist/ionic/{p-d693e765.system.entry.js → p-8fa43732.system.entry.js} +1 -1
  140. package/dist/ionic/p-90ec5785.system.entry.js +4 -0
  141. package/dist/ionic/{p-091cd81e.entry.js → p-958bde76.entry.js} +1 -1
  142. package/dist/ionic/{p-b3a58769.entry.js → p-9b3c3564.entry.js} +1 -1
  143. package/dist/ionic/p-af986299.system.entry.js +4 -0
  144. package/dist/ionic/p-b321c999.entry.js +4 -0
  145. package/dist/ionic/p-b34c1ae1.js +4 -0
  146. package/dist/ionic/{p-5c17f888.entry.js → p-c0d1e5a1.entry.js} +1 -1
  147. package/dist/ionic/{p-dab7745f.system.entry.js → p-ccea9adc.system.entry.js} +1 -1
  148. package/dist/ionic/{p-0b68c41d.system.entry.js → p-d1356034.system.entry.js} +1 -1
  149. package/dist/ionic/{p-d9eb4632.system.entry.js → p-d5db0834.system.entry.js} +1 -1
  150. package/dist/ionic/{p-79e3f0b6.entry.js → p-d8538837.entry.js} +1 -1
  151. package/dist/ionic/p-d8a0267b.system.entry.js +4 -0
  152. package/dist/ionic/{p-8843b046.system.js → p-db46fc66.system.js} +1 -1
  153. package/dist/ionic/p-f5c19164.entry.js +4 -0
  154. package/dist/types/components/checkbox/checkbox.d.ts +8 -1
  155. package/dist/types/components/datetime/datetime.d.ts +17 -10
  156. package/dist/types/components/datetime/utils/data.d.ts +1 -1
  157. package/dist/types/components/datetime/utils/parse.d.ts +3 -3
  158. package/dist/types/components/radio/radio.d.ts +8 -1
  159. package/dist/types/components/range/range.d.ts +2 -1
  160. package/dist/types/components/toast/toast-interface.d.ts +3 -0
  161. package/dist/types/components/toggle/toggle.d.ts +8 -1
  162. package/dist/types/components.d.ts +40 -16
  163. package/hydrate/index.js +164 -122
  164. package/package.json +1 -1
  165. package/dist/collection/components/popover/test/async/popover.e2e.js +0 -20
  166. package/dist/esm-es5/data-4b448e3a.js +0 -4
  167. package/dist/esm-es5/framework-delegate-aa433dea.js +0 -4
  168. package/dist/ionic/p-0053fe81.system.entry.js +0 -4
  169. package/dist/ionic/p-2cdcd6f2.entry.js +0 -4
  170. package/dist/ionic/p-3565c8a0.entry.js +0 -4
  171. package/dist/ionic/p-5b88f780.entry.js +0 -4
  172. package/dist/ionic/p-601fd53c.js +0 -4
  173. package/dist/ionic/p-98b618ce.system.entry.js +0 -4
  174. package/dist/ionic/p-b1e8f045.entry.js +0 -4
  175. package/dist/ionic/p-b287ab05.js +0 -4
  176. package/dist/ionic/p-b3cfc6e1.system.entry.js +0 -4
  177. package/dist/ionic/p-b54a1fcb.system.entry.js +0 -4
  178. package/dist/ionic/p-cc8910e7.system.js +0 -4
  179. package/dist/ionic/p-e53b8af4.system.entry.js +0 -4
  180. package/dist/ionic/p-f78e60a5.entry.js +0 -4
  181. package/dist/ionic/p-fd42994b.system.entry.js +0 -4
@@ -55,6 +55,7 @@ export class Checkbox {
55
55
  this.value = 'on';
56
56
  this.labelPlacement = 'start';
57
57
  this.justify = 'space-between';
58
+ this.alignment = 'center';
58
59
  this.legacy = undefined;
59
60
  }
60
61
  // TODO(FW-3100): remove this
@@ -92,7 +93,7 @@ export class Checkbox {
92
93
  return legacyFormController.hasLegacyControl() ? this.renderLegacyCheckbox() : this.renderCheckbox();
93
94
  }
94
95
  renderCheckbox() {
95
- const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, } = this;
96
+ const { color, checked, disabled, el, getSVGPath, indeterminate, inheritedAttributes, inputId, justify, labelPlacement, name, value, alignment, } = this;
96
97
  const mode = getIonMode(this);
97
98
  const path = getSVGPath(mode, indeterminate);
98
99
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
@@ -104,6 +105,7 @@ export class Checkbox {
104
105
  'checkbox-indeterminate': indeterminate,
105
106
  interactive: true,
106
107
  [`checkbox-justify-${justify}`]: true,
108
+ [`checkbox-alignment-${alignment}`]: true,
107
109
  [`checkbox-label-placement-${labelPlacement}`]: true,
108
110
  }) }, h("label", { class: "checkbox-wrapper" }, h("input", Object.assign({ type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, onFocus: () => this.onFocus(), onBlur: () => this.onBlur(), ref: (focusEl) => (this.focusEl = focusEl) }, inheritedAttributes)), h("div", { class: {
109
111
  'label-text-wrapper': true,
@@ -280,15 +282,15 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
280
282
  "type": "string",
281
283
  "mutable": false,
282
284
  "complexType": {
283
- "original": "'start' | 'end' | 'fixed'",
284
- "resolved": "\"end\" | \"fixed\" | \"start\"",
285
+ "original": "'start' | 'end' | 'fixed' | 'stacked'",
286
+ "resolved": "\"end\" | \"fixed\" | \"stacked\" | \"start\"",
285
287
  "references": {}
286
288
  },
287
289
  "required": false,
288
290
  "optional": false,
289
291
  "docs": {
290
292
  "tags": [],
291
- "text": "Where to place the label relative to the checkbox.\n`\"start\"`: The label will appear to the left of the checkbox in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the checkbox in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\")."
293
+ "text": "Where to place the label relative to the checkbox.\n`\"start\"`: The label will appear to the left of the checkbox in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the checkbox in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\").\n`\"stacked\"`: The label will appear above the checkbox regardless of the direction. The alignment of the label can be controlled with the `alignment` property."
292
294
  },
293
295
  "attribute": "label-placement",
294
296
  "reflect": false,
@@ -312,6 +314,24 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
312
314
  "reflect": false,
313
315
  "defaultValue": "'space-between'"
314
316
  },
317
+ "alignment": {
318
+ "type": "string",
319
+ "mutable": false,
320
+ "complexType": {
321
+ "original": "'start' | 'center'",
322
+ "resolved": "\"center\" | \"start\"",
323
+ "references": {}
324
+ },
325
+ "required": false,
326
+ "optional": false,
327
+ "docs": {
328
+ "tags": [],
329
+ "text": "How to control the alignment of the checkbox and label on the cross axis.\n`\"start\"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.\n`\"center\"`: The label and control will appear at the center of the cross axis in both LTR and RTL."
330
+ },
331
+ "attribute": "alignment",
332
+ "reflect": false,
333
+ "defaultValue": "'center'"
334
+ },
315
335
  "legacy": {
316
336
  "type": "boolean",
317
337
  "mutable": false,
@@ -124,6 +124,15 @@
124
124
  margin-bottom: 10px;
125
125
  }
126
126
 
127
+ :host(.in-item.checkbox-label-placement-stacked) .label-text-wrapper {
128
+ margin-top: 10px;
129
+ margin-bottom: 16px;
130
+ }
131
+
132
+ :host(.in-item.checkbox-label-placement-stacked) .native-wrapper {
133
+ margin-bottom: 10px;
134
+ }
135
+
127
136
  /**
128
137
  * If no label text is placed into the slot
129
138
  * then the element should be hidden otherwise
@@ -198,6 +207,14 @@ input {
198
207
  justify-content: end;
199
208
  }
200
209
 
210
+ :host(.checkbox-alignment-start) .checkbox-wrapper {
211
+ align-items: start;
212
+ }
213
+
214
+ :host(.checkbox-alignment-center) .checkbox-wrapper {
215
+ align-items: center;
216
+ }
217
+
201
218
  /**
202
219
  * Label is on the left of the checkbox in LTR and
203
220
  * on the right in RTL.
@@ -261,6 +278,24 @@ input {
261
278
  max-width: 200px;
262
279
  }
263
280
 
281
+ /**
282
+ * Label is on top of the checkbox.
283
+ */
284
+ :host(.checkbox-label-placement-stacked) .checkbox-wrapper {
285
+ flex-direction: column;
286
+ }
287
+
288
+ :host(.checkbox-label-placement-stacked) .label-text-wrapper {
289
+ /**
290
+ * The margin between the label and
291
+ * the checkbox should be on the bottom
292
+ * when the label sits at the top.
293
+ */
294
+ margin-left: 0;
295
+ margin-right: 0;
296
+ margin-bottom: 16px;
297
+ }
298
+
264
299
  :host(.checkbox-checked) .checkbox-icon,
265
300
  :host(.checkbox-indeterminate) .checkbox-icon {
266
301
  border-color: var(--border-color-checked);
@@ -58,4 +58,17 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
58
58
  expect(await list.screenshot()).toMatchSnapshot(screenshot(`checkbox-long-label-in-item`));
59
59
  });
60
60
  });
61
+ test.describe(title('checkbox: stacked label in item'), () => {
62
+ test('should render margins correctly when using stacked label in item', async ({ page }) => {
63
+ await page.setContent(`
64
+ <ion-list>
65
+ <ion-item>
66
+ <ion-checkbox label-placement="stacked">Enable Notifications</ion-checkbox>
67
+ </ion-item>
68
+ </ion-list>
69
+ `, config);
70
+ const list = page.locator('ion-list');
71
+ await expect(list).toHaveScreenshot(screenshot(`checkbox-stacked-label-in-item`));
72
+ });
73
+ });
61
74
  });
@@ -91,5 +91,21 @@ configs().forEach(({ title, screenshot, config }) => {
91
91
  expect(await checkbox.screenshot()).toMatchSnapshot(screenshot(`checkbox-label-fixed-justify-space-between`));
92
92
  });
93
93
  });
94
+ test.describe('checkbox: stacked placement', () => {
95
+ test('should align the label to the start of the container in the stacked position', async ({ page }) => {
96
+ await page.setContent(`
97
+ <ion-checkbox label-placement="stacked" alignment="start" style="width: 200px">This is a long label</ion-checkbox>
98
+ `, config);
99
+ const checkbox = page.locator('ion-checkbox');
100
+ await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-label-stacked-align-start`));
101
+ });
102
+ test('should align the label to the center of the container in the stacked position', async ({ page }) => {
103
+ await page.setContent(`
104
+ <ion-checkbox label-placement="stacked" alignment="center" style="width: 200px">This is a long label</ion-checkbox>
105
+ `, config);
106
+ const checkbox = page.locator('ion-checkbox');
107
+ await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-label-stacked-align-center`));
108
+ });
109
+ });
94
110
  });
95
111
  });
@@ -38,11 +38,6 @@ export class Datetime {
38
38
  constructor() {
39
39
  this.inputId = `ion-dt-${datetimeIds++}`;
40
40
  this.prevPresentation = null;
41
- /**
42
- * Duplicate reference to `activeParts` that does not trigger a re-render of the component.
43
- * Allows caching an instance of the `activeParts` in between render cycles.
44
- */
45
- this.activePartsClone = [];
46
41
  this.warnIfIncorrectValueUsage = () => {
47
42
  const { multiple, value } = this;
48
43
  if (!multiple && Array.isArray(value)) {
@@ -70,9 +65,9 @@ export class Datetime {
70
65
  * data. This should be used when rendering an
71
66
  * interface in an environment where the `value`
72
67
  * may not be set. This function works
73
- * by returning the first selected date in
74
- * "activePartsClone" and then falling back to
75
- * defaultParts if no active date is selected.
68
+ * by returning the first selected date and then
69
+ * falling back to defaultParts if no active date
70
+ * is selected.
76
71
  */
77
72
  this.getActivePartsWithFallback = () => {
78
73
  var _a;
@@ -80,8 +75,8 @@ export class Datetime {
80
75
  return (_a = this.getActivePart()) !== null && _a !== void 0 ? _a : defaultParts;
81
76
  };
82
77
  this.getActivePart = () => {
83
- const { activePartsClone } = this;
84
- return Array.isArray(activePartsClone) ? activePartsClone[0] : activePartsClone;
78
+ const { activeParts } = this;
79
+ return Array.isArray(activeParts) ? activeParts[0] : activeParts;
85
80
  };
86
81
  this.closeParentOverlay = () => {
87
82
  const popoverOrModal = this.el.closest('ion-modal, ion-popover');
@@ -93,7 +88,7 @@ export class Datetime {
93
88
  this.workingParts = Object.assign({}, parts);
94
89
  };
95
90
  this.setActiveParts = (parts, removeDate = false) => {
96
- const { multiple, minParts, maxParts, activePartsClone } = this;
91
+ const { multiple, minParts, maxParts, activeParts } = this;
97
92
  /**
98
93
  * When setting the active parts, it is possible
99
94
  * to set invalid data. For example,
@@ -107,16 +102,7 @@ export class Datetime {
107
102
  const validatedParts = validateParts(parts, minParts, maxParts);
108
103
  this.setWorkingParts(validatedParts);
109
104
  if (multiple) {
110
- /**
111
- * We read from activePartsClone here because valueChanged() only updates that,
112
- * so it's the more reliable source of truth. If we read from activeParts, then
113
- * if you click July 1, manually set the value to July 2, and then click July 3,
114
- * the new value would be [July 1, July 3], ignoring the value set.
115
- *
116
- * We can then pass the new value to activeParts (rather than activePartsClone)
117
- * since the clone will be updated automatically by activePartsChanged().
118
- */
119
- const activePartsArray = Array.isArray(activePartsClone) ? activePartsClone : [activePartsClone];
105
+ const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
120
106
  if (removeDate) {
121
107
  this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
122
108
  }
@@ -333,6 +319,19 @@ export class Datetime {
333
319
  const monthBox = month.getBoundingClientRect();
334
320
  if (Math.abs(monthBox.x - box.x) > 2)
335
321
  return;
322
+ /**
323
+ * If we're force-rendering a month, assume we've
324
+ * scrolled to that and return it.
325
+ *
326
+ * If forceRenderDate is ever used in a context where the
327
+ * forced month is not immediately auto-scrolled to, this
328
+ * should be updated to also check whether `month` has the
329
+ * same month and year as the forced date.
330
+ */
331
+ const { forceRenderDate } = this;
332
+ if (forceRenderDate !== undefined) {
333
+ return { month: forceRenderDate.month, year: forceRenderDate.year, day: forceRenderDate.day };
334
+ }
336
335
  /**
337
336
  * From here, we can determine if the start
338
337
  * month or the end month was scrolled into view.
@@ -387,6 +386,9 @@ export class Datetime {
387
386
  this.setWorkingParts(Object.assign(Object.assign({}, this.workingParts), { month, day: day, year }));
388
387
  calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
389
388
  calendarBodyRef.style.removeProperty('overflow');
389
+ if (this.resolveForceDateScrolling) {
390
+ this.resolveForceDateScrolling();
391
+ }
390
392
  });
391
393
  };
392
394
  /**
@@ -441,10 +443,17 @@ export class Datetime {
441
443
  }
442
444
  };
443
445
  this.processValue = (value) => {
444
- const hasValue = value !== null && value !== undefined;
446
+ const hasValue = value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
445
447
  const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
446
- const { minParts, maxParts } = this;
448
+ const { minParts, maxParts, workingParts, el } = this;
447
449
  this.warnIfIncorrectValueUsage();
450
+ /**
451
+ * Return early if the value wasn't parsed correctly, such as
452
+ * if an improperly formatted date string was provided.
453
+ */
454
+ if (!valueToProcess) {
455
+ return;
456
+ }
448
457
  /**
449
458
  * Datetime should only warn of out of bounds values
450
459
  * if set by the user. If the `value` is undefined,
@@ -462,16 +471,9 @@ export class Datetime {
462
471
  * that the values don't necessarily have to be in order.
463
472
  */
464
473
  const singleValue = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess;
465
- const { month, day, year, hour, minute } = clampDate(singleValue, minParts, maxParts);
474
+ const targetValue = clampDate(singleValue, minParts, maxParts);
475
+ const { month, day, year, hour, minute } = targetValue;
466
476
  const ampm = parseAmPm(hour);
467
- this.setWorkingParts({
468
- month,
469
- day,
470
- year,
471
- hour,
472
- minute,
473
- ampm,
474
- });
475
477
  /**
476
478
  * Since `activeParts` indicates a value that
477
479
  * been explicitly selected either by the
@@ -501,6 +503,62 @@ export class Datetime {
501
503
  */
502
504
  this.activeParts = [];
503
505
  }
506
+ /**
507
+ * Only animate if:
508
+ * 1. We're using grid style (wheel style pickers should just jump to new value)
509
+ * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to)
510
+ * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example)
511
+ * 4. The month/year picker is not open (since you wouldn't see the animation anyway)
512
+ */
513
+ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
514
+ const bodyIsVisible = el.classList.contains('datetime-ready');
515
+ const { isGridStyle, showMonthAndYear } = this;
516
+ if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
517
+ this.animateToDate(targetValue);
518
+ }
519
+ else {
520
+ /**
521
+ * We only need to do this if we didn't just animate to a new month,
522
+ * since that calls prevMonth/nextMonth which calls setWorkingParts for us.
523
+ */
524
+ this.setWorkingParts({
525
+ month,
526
+ day,
527
+ year,
528
+ hour,
529
+ minute,
530
+ ampm,
531
+ });
532
+ }
533
+ };
534
+ this.animateToDate = async (targetValue) => {
535
+ const { workingParts } = this;
536
+ /**
537
+ * Tell other render functions that we need to force the
538
+ * target month to appear in place of the actual next/prev month.
539
+ * Because this is a State variable, a rerender will be triggered
540
+ * automatically, updating the rendered months.
541
+ */
542
+ this.forceRenderDate = targetValue;
543
+ /**
544
+ * Flag that we've started scrolling to the forced date.
545
+ * The resolve function will be called by the datetime's
546
+ * scroll listener when it's done updating everything.
547
+ * This is a replacement for making prev/nextMonth async,
548
+ * since the logic we're waiting on is in a listener.
549
+ */
550
+ const forceDateScrollingPromise = new Promise((resolve) => {
551
+ this.resolveForceDateScrolling = resolve;
552
+ });
553
+ /**
554
+ * Animate smoothly to the forced month. This will also update
555
+ * workingParts and correct the surrounding months for us.
556
+ */
557
+ const targetMonthIsBefore = isBefore(targetValue, workingParts);
558
+ targetMonthIsBefore ? this.prevMonth() : this.nextMonth();
559
+ await forceDateScrollingPromise;
560
+ this.resolveForceDateScrolling = undefined;
561
+ this.forceRenderDate = undefined;
504
562
  };
505
563
  this.onFocus = () => {
506
564
  this.ionFocus.emit();
@@ -556,6 +614,7 @@ export class Datetime {
556
614
  ampm: 'pm',
557
615
  };
558
616
  this.isTimePopoverOpen = false;
617
+ this.forceRenderDate = undefined;
559
618
  this.color = 'primary';
560
619
  this.name = this.inputId;
561
620
  this.disabled = false;
@@ -595,6 +654,11 @@ export class Datetime {
595
654
  maxChanged() {
596
655
  this.processMaxParts();
597
656
  }
657
+ get isGridStyle() {
658
+ const { presentation, preferWheel } = this;
659
+ const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
660
+ return hasDatePresentation && !preferWheel;
661
+ }
598
662
  yearValuesChanged() {
599
663
  this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
600
664
  }
@@ -610,51 +674,13 @@ export class Datetime {
610
674
  minuteValuesChanged() {
611
675
  this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
612
676
  }
613
- activePartsChanged() {
614
- this.activePartsClone = this.activeParts;
615
- }
616
677
  /**
617
678
  * Update the datetime value when the value changes
618
679
  */
619
- valueChanged() {
620
- const { value, minParts, maxParts, workingParts } = this;
680
+ async valueChanged() {
681
+ const { value } = this;
621
682
  if (this.hasValue()) {
622
- this.warnIfIncorrectValueUsage();
623
- /**
624
- * Clones the value of the `activeParts` to the private clone, to update
625
- * the date display on the current render cycle without causing another render.
626
- *
627
- * This allows us to update the current value's date/time display without
628
- * refocusing or shifting the user's display (leaves the user in place).
629
- */
630
- const valueDateParts = parseDate(value);
631
- if (valueDateParts) {
632
- warnIfValueOutOfBounds(valueDateParts, minParts, maxParts);
633
- if (Array.isArray(valueDateParts)) {
634
- this.activePartsClone = [...valueDateParts];
635
- }
636
- else {
637
- const { month, day, year, hour, minute } = valueDateParts;
638
- const ampm = hour != null ? (hour >= 12 ? 'pm' : 'am') : undefined;
639
- this.activePartsClone = Object.assign(Object.assign({}, this.activeParts), { month,
640
- day,
641
- year,
642
- hour,
643
- minute,
644
- ampm });
645
- /**
646
- * The working parts am/pm value must be updated when the value changes, to
647
- * ensure the time picker hour column values are generated correctly.
648
- *
649
- * Note that we don't need to do this if valueDateParts is an array, since
650
- * multiple="true" does not apply to time pickers.
651
- */
652
- this.setWorkingParts(Object.assign(Object.assign({}, workingParts), { ampm }));
653
- }
654
- }
655
- else {
656
- printIonWarning(`Unable to parse date string: ${value}. Please provide a valid ISO 8601 datetime string.`);
657
- }
683
+ this.processValue(value);
658
684
  }
659
685
  this.emitStyle();
660
686
  this.ionValueChange.emit({ value });
@@ -857,8 +883,6 @@ export class Datetime {
857
883
  printIonWarning('The highlightedDates property is not supported with preferWheel="true".', el);
858
884
  }
859
885
  }
860
- this.processMinParts();
861
- this.processMaxParts();
862
886
  const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
863
887
  const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
864
888
  const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
@@ -866,6 +890,8 @@ export class Datetime {
866
890
  const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues));
867
891
  const todayParts = (this.todayParts = parseDate(getToday()));
868
892
  this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
893
+ this.processMinParts();
894
+ this.processMaxParts();
869
895
  this.processValue(this.value);
870
896
  this.emitStyle();
871
897
  }
@@ -1250,7 +1276,7 @@ export class Datetime {
1250
1276
  const { el, highlightedDates, isDateEnabled, multiple } = this;
1251
1277
  const referenceParts = { month, day, year };
1252
1278
  const isCalendarPadding = day === null;
1253
- const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(this.locale, referenceParts, this.activePartsClone, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
1279
+ const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(this.locale, referenceParts, this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
1254
1280
  const dateIsoString = convertDataToISO(referenceParts);
1255
1281
  let isCalDayDisabled = isCalMonthDisabled || disabled;
1256
1282
  if (!isCalDayDisabled && isDateEnabled !== undefined) {
@@ -1307,7 +1333,7 @@ export class Datetime {
1307
1333
  }))));
1308
1334
  }
1309
1335
  renderCalendarBody() {
1310
- return (h("div", { class: "calendar-body ion-focusable", ref: (el) => (this.calendarBodyRef = el), tabindex: "0" }, generateMonths(this.workingParts).map(({ month, year }) => {
1336
+ return (h("div", { class: "calendar-body ion-focusable", ref: (el) => (this.calendarBodyRef = el), tabindex: "0" }, generateMonths(this.workingParts, this.forceRenderDate).map(({ month, year }) => {
1311
1337
  return this.renderMonth(month, year);
1312
1338
  })));
1313
1339
  }
@@ -1462,14 +1488,13 @@ export class Datetime {
1462
1488
  }
1463
1489
  }
1464
1490
  render() {
1465
- const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size } = this;
1491
+ const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size, isGridStyle, } = this;
1466
1492
  const mode = getIonMode(this);
1467
1493
  const isMonthAndYearPresentation = presentation === 'year' || presentation === 'month' || presentation === 'month-year';
1468
1494
  const shouldShowMonthAndYear = showMonthAndYear || isMonthAndYearPresentation;
1469
1495
  const monthYearPickerOpen = showMonthAndYear && !isMonthAndYearPresentation;
1470
1496
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1471
1497
  const hasWheelVariant = hasDatePresentation && preferWheel;
1472
- const hasGrid = hasDatePresentation && !preferWheel;
1473
1498
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1474
1499
  return (h(Host, { "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1475
1500
  [mode]: true,
@@ -1480,7 +1505,7 @@ export class Datetime {
1480
1505
  [`datetime-presentation-${presentation}`]: true,
1481
1506
  [`datetime-size-${size}`]: true,
1482
1507
  [`datetime-prefer-wheel`]: hasWheelVariant,
1483
- [`datetime-grid`]: hasGrid,
1508
+ [`datetime-grid`]: isGridStyle,
1484
1509
  })) }, this.renderDatetime(mode)));
1485
1510
  }
1486
1511
  static get is() { return "ion-datetime"; }
@@ -2039,7 +2064,8 @@ export class Datetime {
2039
2064
  "showMonthAndYear": {},
2040
2065
  "activeParts": {},
2041
2066
  "workingParts": {},
2042
- "isTimePopoverOpen": {}
2067
+ "isTimePopoverOpen": {},
2068
+ "forceRenderDate": {}
2043
2069
  };
2044
2070
  }
2045
2071
  static get events() {
@@ -2267,9 +2293,6 @@ export class Datetime {
2267
2293
  }, {
2268
2294
  "propName": "minuteValues",
2269
2295
  "methodName": "minuteValuesChanged"
2270
- }, {
2271
- "propName": "activeParts",
2272
- "methodName": "activePartsChanged"
2273
2296
  }, {
2274
2297
  "propName": "value",
2275
2298
  "methodName": "valueChanged"
@@ -184,6 +184,21 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
184
184
  await yearValues.nth(10).click();
185
185
  await ionChange.next();
186
186
  });
187
+ test('should jump to selected date when programmatically updating value', async ({ page }) => {
188
+ await page.setContent(`
189
+ <ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2019-05-30"></ion-datetime>
190
+ `, config);
191
+ await page.waitForSelector('.datetime-ready');
192
+ const datetime = page.locator('ion-datetime');
193
+ await datetime.evaluate((el) => (el.value = '2021-05-25T12:40:00.000Z'));
194
+ await page.waitForChanges();
195
+ const selectedMonth = datetime.locator('.month-column .picker-item-active');
196
+ const selectedDay = datetime.locator('.day-column .picker-item-active');
197
+ const selectedYear = datetime.locator('.year-column .picker-item-active');
198
+ await expect(selectedMonth).toHaveText(/May/);
199
+ await expect(selectedDay).toHaveText(/25/);
200
+ await expect(selectedYear).toHaveText(/2021/);
201
+ });
187
202
  test.describe('datetime: date wheel localization', () => {
188
203
  test('should correctly localize the date data', async ({ page }) => {
189
204
  await page.setContent(`
@@ -30,24 +30,21 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
30
30
  await page.waitForSelector('.datetime-ready');
31
31
  const datetime = page.locator('ion-datetime');
32
32
  const activeDayButton = page.locator('.calendar-day-active');
33
- const monthYearButton = page.locator('.calendar-month-year');
34
- const monthColumn = page.locator('.month-column');
35
- const ionChange = await page.spyOnEvent('ionChange');
36
33
  await datetime.evaluate((el) => (el.value = '2021-10-05'));
37
- // Open month/year picker
38
- await monthYearButton.click();
39
- await page.waitForChanges();
40
- // Select October 2021
41
- // The year will automatically switch to 2021 when selecting 10
42
- await monthColumn.locator('.picker-item[data-value="10"]').click();
43
- await ionChange.next();
44
- // Close month/year picker
45
- await monthYearButton.click();
46
34
  await page.waitForChanges();
47
35
  // Check that correct day is highlighted
48
36
  await expect(activeDayButton).toHaveAttribute('data-day', '5');
49
37
  await expect(activeDayButton).toHaveAttribute('data-month', '10');
50
38
  await expect(activeDayButton).toHaveAttribute('data-year', '2021');
51
39
  });
40
+ test('should scroll to new month when value is initially set and then updated', async ({ page }) => {
41
+ await page.goto('/src/components/datetime/test/set-value', config);
42
+ await page.waitForSelector('.datetime-ready');
43
+ const datetime = page.locator('ion-datetime');
44
+ await datetime.evaluate((el) => (el.value = '2021-05-25T12:40:00.000Z'));
45
+ await page.waitForChanges();
46
+ const calendarHeader = datetime.locator('.calendar-month-year');
47
+ await expect(calendarHeader).toHaveText(/May 2021/);
48
+ });
52
49
  });
53
50
  });
@@ -220,12 +220,20 @@ export const generateTime = (refParts, hourCycle = 'h12', minParts, maxParts, ho
220
220
  * Given DatetimeParts, generate the previous,
221
221
  * current, and and next months.
222
222
  */
223
- export const generateMonths = (refParts) => {
224
- return [
225
- getPreviousMonth(refParts),
226
- { month: refParts.month, year: refParts.year, day: refParts.day },
227
- getNextMonth(refParts),
228
- ];
223
+ export const generateMonths = (refParts, forcedDate) => {
224
+ const current = { month: refParts.month, year: refParts.year, day: refParts.day };
225
+ /**
226
+ * If we're forcing a month to appear, and it's different from the current month,
227
+ * ensure it appears by replacing the next or previous month as appropriate.
228
+ */
229
+ if (forcedDate !== undefined && (refParts.month !== forcedDate.month || refParts.year !== forcedDate.year)) {
230
+ const forced = { month: forcedDate.month, year: forcedDate.year, day: forcedDate.day };
231
+ const forcedMonthIsBefore = isBefore(forced, current);
232
+ return forcedMonthIsBefore
233
+ ? [forced, current, getNextMonth(refParts)]
234
+ : [getPreviousMonth(refParts), current, forced];
235
+ }
236
+ return [getPreviousMonth(refParts), current, getNextMonth(refParts)];
229
237
  };
230
238
  export const getMonthColumnData = (locale, refParts, minParts, maxParts, monthValues, formatOptions = {
231
239
  month: 'long',
@@ -1,6 +1,7 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
+ import { printIonWarning } from "../../../utils/logging/index";
4
5
  import { isAfter, isBefore } from "./comparison";
5
6
  import { getNumDaysInMonth } from "./helpers";
6
7
  const ISO_8601_REGEXP =
@@ -47,7 +48,22 @@ export const getPartsFromCalendarDay = (el) => {
47
48
  };
48
49
  export function parseDate(val) {
49
50
  if (Array.isArray(val)) {
50
- return val.map((valStr) => parseDate(valStr));
51
+ const parsedArray = [];
52
+ for (const valStr of val) {
53
+ const parsedVal = parseDate(valStr);
54
+ /**
55
+ * If any of the values weren't parsed correctly, consider
56
+ * the entire batch incorrect. This simplifies the type
57
+ * signatures by having "undefined" be a general error case
58
+ * instead of returning (Datetime | undefined)[], which is
59
+ * harder for TS to perform type narrowing on.
60
+ */
61
+ if (!parsedVal) {
62
+ return undefined;
63
+ }
64
+ parsedArray.push(parsedVal);
65
+ }
66
+ return parsedArray;
51
67
  }
52
68
  // manually parse IS0 cuz Date.parse cannot be trusted
53
69
  // ISO 8601 format: 1994-12-15T13:47:20Z
@@ -67,6 +83,7 @@ export function parseDate(val) {
67
83
  }
68
84
  if (parse === null) {
69
85
  // wasn't able to parse the ISO datetime
86
+ printIonWarning(`Unable to parse date string: ${val}. Please provide a valid ISO 8601 datetime string.`);
70
87
  return undefined;
71
88
  }
72
89
  // ensure all the parse values exist with at least 0
@@ -54,6 +54,9 @@ export class DatetimeButton {
54
54
  * to today's date and time if no value is set.
55
55
  */
56
56
  const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
57
+ if (!parsedDatetimes) {
58
+ return;
59
+ }
57
60
  /**
58
61
  * If developers incorrectly use multiple="true"
59
62
  * with non "date" datetimes, then just select
@@ -217,8 +217,8 @@ export class Modal {
217
217
  */
218
218
  this.currentBreakpoint = this.initialBreakpoint;
219
219
  const { inline, delegate } = this.getDelegate(true);
220
- this.ionMount.emit();
221
220
  this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
221
+ this.ionMount.emit();
222
222
  /**
223
223
  * When using the lazy loaded build of Stencil, we need to wait
224
224
  * for every Stencil component instance to be ready before presenting