@ionic/core 9.0.11-dev.11793726772.1e883321 → 9.0.11-dev.11793727726.19b906b5

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 (89) hide show
  1. package/components/ion-action-sheet.js +1 -1
  2. package/components/ion-alert.js +1 -1
  3. package/components/ion-content.js +1 -1
  4. package/components/ion-datetime-button.js +1 -1
  5. package/components/ion-datetime.js +1 -1
  6. package/components/ion-modal.js +1 -1
  7. package/components/ion-popover.js +1 -1
  8. package/components/ion-select-modal.js +1 -1
  9. package/components/ion-select.js +1 -1
  10. package/components/ion-toast.js +1 -1
  11. package/components/{p-DtZimz8H.js → p-1HReh-CB.js} +1 -1
  12. package/components/{p-BCQSEpww.js → p-B7ExaLVL.js} +1 -1
  13. package/components/{p-CseHeZbU.js → p-BCYVFy50.js} +1 -1
  14. package/components/p-ByE0slik.js +4 -0
  15. package/components/p-DgNvIX46.js +4 -0
  16. package/components/p-DmyvpOqm.js +4 -0
  17. package/components/p-mG85e2ek.js +4 -0
  18. package/css/ionic/bundle.ionic.css +1 -1
  19. package/css/ionic/bundle.ionic.css.map +1 -1
  20. package/css/ionic/core.ionic.css +1 -1
  21. package/css/ionic/core.ionic.css.map +1 -1
  22. package/css/ionic/global.bundle.ionic.css.map +1 -1
  23. package/css/ionic/link.ionic.css.map +1 -1
  24. package/css/ionic/typography.ionic.css.map +1 -1
  25. package/dist/cjs/ion-action-sheet.cjs.entry.js +1 -1
  26. package/dist/cjs/ion-alert.cjs.entry.js +1 -1
  27. package/dist/cjs/ion-app_8.cjs.entry.js +5 -13
  28. package/dist/cjs/ion-datetime-button.cjs.entry.js +33 -6
  29. package/dist/cjs/ion-datetime.cjs.entry.js +353 -36
  30. package/dist/cjs/ion-modal.cjs.entry.js +1 -1
  31. package/dist/cjs/ion-popover.cjs.entry.js +1 -1
  32. package/dist/cjs/ion-toast.cjs.entry.js +1 -1
  33. package/dist/cjs/ionic.cjs.js +1 -1
  34. package/dist/cjs/loader.cjs.js +1 -1
  35. package/dist/collection/components/action-sheet/action-sheet.ionic.css +1 -1
  36. package/dist/collection/components/alert/alert.ionic.css +1 -1
  37. package/dist/collection/components/content/{content.ios.css → content.css} +41 -65
  38. package/dist/collection/components/content/content.js +5 -9
  39. package/dist/collection/components/datetime/datetime.ionic.css +179 -5
  40. package/dist/collection/components/datetime/datetime.ios.css +134 -2
  41. package/dist/collection/components/datetime/datetime.js +406 -33
  42. package/dist/collection/components/datetime/datetime.md.css +120 -0
  43. package/dist/collection/components/datetime/utils/state.js +18 -1
  44. package/dist/collection/components/datetime-button/datetime-button.js +34 -6
  45. package/dist/collection/components/modal/modal.ionic.css +2 -2
  46. package/dist/collection/components/popover/popover.ionic.css +1 -2
  47. package/dist/collection/components/toast/toast.ionic.css +1 -1
  48. package/dist/docs.json +160 -127
  49. package/dist/esm/{format-NT7iHsbA.js → format-DUHrIiJq.js} +1 -1
  50. package/dist/esm/ion-action-sheet.entry.js +1 -1
  51. package/dist/esm/ion-alert.entry.js +1 -1
  52. package/dist/esm/ion-app_8.entry.js +5 -13
  53. package/dist/esm/ion-datetime-button.entry.js +34 -7
  54. package/dist/esm/ion-datetime.entry.js +354 -37
  55. package/dist/esm/ion-modal.entry.js +1 -1
  56. package/dist/esm/ion-popover.entry.js +1 -1
  57. package/dist/esm/ion-toast.entry.js +1 -1
  58. package/dist/esm/ionic.js +1 -1
  59. package/dist/esm/loader.js +1 -1
  60. package/dist/html.html-data.json +20 -0
  61. package/dist/ionic/ionic.esm.js +1 -1
  62. package/dist/ionic/{p-b076511a.entry.js → p-3674a7de.entry.js} +1 -1
  63. package/dist/ionic/p-56d77e95.entry.js +4 -0
  64. package/dist/ionic/{p-3ddd32cc.entry.js → p-5a024b02.entry.js} +1 -1
  65. package/dist/ionic/p-9cce1ed5.entry.js +4 -0
  66. package/dist/ionic/{p-BtccSO5J.js → p-CaOPF8Xt.js} +1 -1
  67. package/dist/ionic/p-b30b468d.entry.js +4 -0
  68. package/dist/ionic/p-ceca04fb.entry.js +4 -0
  69. package/dist/ionic/p-d7da66b1.entry.js +4 -0
  70. package/dist/ionic/p-f02c29cd.entry.js +4 -0
  71. package/dist/types/components/datetime/datetime-interface.d.ts +4 -0
  72. package/dist/types/components/datetime/datetime.d.ts +71 -2
  73. package/dist/types/components/datetime/utils/state.d.ts +5 -2
  74. package/dist/types/components.d.ts +33 -0
  75. package/hydrate/index.js +405 -61
  76. package/hydrate/index.mjs +405 -61
  77. package/package.json +2 -2
  78. package/components/p-Da8LCPwo.js +0 -4
  79. package/components/p-DibGn4gb.js +0 -4
  80. package/components/p-NZGZhPF5.js +0 -4
  81. package/components/p-PFwuhbqg.js +0 -4
  82. package/dist/collection/components/content/content.ionic.css +0 -352
  83. package/dist/collection/components/content/content.md.css +0 -347
  84. package/dist/ionic/p-0743a26a.entry.js +0 -4
  85. package/dist/ionic/p-1d87b153.entry.js +0 -4
  86. package/dist/ionic/p-39e3be74.entry.js +0 -4
  87. package/dist/ionic/p-a1555032.entry.js +0 -4
  88. package/dist/ionic/p-ad46d2fd.entry.js +0 -4
  89. package/dist/ionic/p-dbf53341.entry.js +0 -4
@@ -50,6 +50,11 @@ import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from "./ut
50
50
  * @part calendar-day today - The calendar day that contains the current day.
51
51
  * @part calendar-day disabled - The calendar day that is disabled.
52
52
  *
53
+ * @part range-start - The calendar day button that is the start of a range selection.
54
+ * @part range-end - The calendar day button that is the end of a range selection.
55
+ * @part range-selection - The calendar day button that is part of a range selection (includes start and end).
56
+ * @part range-track - The track that connects the start and end days of a range selection.
57
+ *
53
58
  * @part datetime-header - The datetime header contains the content for the `title` slot and the selected date.
54
59
  * @part datetime-title - The element that contains the `title` slot content.
55
60
  * @part datetime-selected-date - The element that contains the selected date.
@@ -57,6 +62,11 @@ import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from "./ut
57
62
  export class Datetime {
58
63
  constructor() {
59
64
  this.inputId = `ion-dt-${datetimeIds++}`;
65
+ /**
66
+ * Set when the user completes a range interactively, so processValue
67
+ * can skip navigation and keep the calendar on the current month.
68
+ */
69
+ this.rangeCompletedInteractively = false;
60
70
  /**
61
71
  * Set when the user selects a value, cleared when it's confirmed
62
72
  * or `processValue` runs (`reset()`, or `value` being set to a
@@ -130,8 +140,22 @@ export class Datetime {
130
140
  /**
131
141
  * If `true`, multiple dates can be selected at once. Only
132
142
  * applies to `presentation="date"` and `preferWheel="false"`.
143
+ *
144
+ * @deprecated Use `selectionMode="multiple"` instead.
133
145
  */
134
146
  this.multiple = false;
147
+ /**
148
+ * The text shown in the datetime header when the start of the range
149
+ * is empty, also used by screen readers to announce the start date.
150
+ * Only applies when `selectionMode="range"`.
151
+ */
152
+ this.startDateLabel = 'Start date';
153
+ /**
154
+ * The text shown in the datetime header when the end of the range
155
+ * is empty, also used by screen readers to announce the end date.
156
+ * Only applies when `selectionMode="range"`.
157
+ */
158
+ this.endDateLabel = 'End date';
135
159
  /**
136
160
  * If `true`, a header will be shown above the calendar
137
161
  * picker. This will include both the slotted title, and
@@ -184,8 +208,8 @@ export class Datetime {
184
208
  */
185
209
  this.preferWheel = false;
186
210
  this.warnIfIncorrectValueUsage = () => {
187
- const { multiple, value } = this;
188
- if (!multiple && Array.isArray(value)) {
211
+ const { value } = this;
212
+ if (!this.isMultipleMode && !this.isRangeMode && Array.isArray(value)) {
189
213
  /**
190
214
  * We do some processing on the `value` array so
191
215
  * that it looks more like an array when logged to
@@ -194,7 +218,13 @@ export class Datetime {
194
218
  * Default toString() behavior: a,b
195
219
  * Custom behavior: ['a', 'b']
196
220
  */
197
- printIonWarning(`[ion-datetime] - An array of values was passed, but multiple is "false". This is incorrect usage and may result in unexpected behaviors. To dismiss this warning, pass a string to the "value" property when multiple="false".
221
+ printIonWarning(`[ion-datetime] - An array of values was passed, but neither selectionMode="multiple" nor selectionMode="range" is set. This is incorrect usage and may result in unexpected behaviors.
222
+
223
+ Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
224
+ `, this.el);
225
+ }
226
+ if (this.isRangeMode && Array.isArray(value) && value.length > 2) {
227
+ printIonWarning(`[ion-datetime] - More than 2 values were passed with selectionMode="range". Only the first and last values will be used.
198
228
 
199
229
  Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
200
230
  `, this.el);
@@ -220,6 +250,9 @@ export class Datetime {
220
250
  };
221
251
  this.getActivePart = () => {
222
252
  const { activeParts } = this;
253
+ if (this.isRangeParts(activeParts)) {
254
+ return activeParts.start;
255
+ }
223
256
  return Array.isArray(activeParts) ? activeParts[0] : activeParts;
224
257
  };
225
258
  this.closeParentOverlay = (role) => {
@@ -234,6 +267,33 @@ export class Datetime {
234
267
  ...parts,
235
268
  };
236
269
  };
270
+ /**
271
+ * Checks whether the span from start to end contains any disabled day.
272
+ * Used by range mode to prevent selecting across disabled dates.
273
+ */
274
+ this.rangeContainsDisabledDate = (start, end) => {
275
+ const { minParts, maxParts, parsedDayValues, parsedMonthValues, parsedYearValues, isDateEnabled } = this;
276
+ let cursor = getNextDay(start);
277
+ while (isBefore(cursor, end)) {
278
+ if (isDayDisabled(cursor, minParts, maxParts, parsedDayValues) ||
279
+ (parsedMonthValues !== undefined && !parsedMonthValues.includes(cursor.month)) ||
280
+ (parsedYearValues !== undefined && !parsedYearValues.includes(cursor.year))) {
281
+ return true;
282
+ }
283
+ if (isDateEnabled) {
284
+ try {
285
+ if (!isDateEnabled(convertDataToISO(cursor))) {
286
+ return true;
287
+ }
288
+ }
289
+ catch {
290
+ // Swallow user callback exceptions during validation
291
+ }
292
+ }
293
+ cursor = getNextDay(cursor);
294
+ }
295
+ return false;
296
+ };
237
297
  this.setActiveParts = (parts, removeDate = false) => {
238
298
  /** if the datetime component is in readonly mode,
239
299
  * allow browsing of the calendar without changing
@@ -243,7 +303,7 @@ export class Datetime {
243
303
  return;
244
304
  }
245
305
  this.hasUnconfirmedSelection = true;
246
- const { multiple, minParts, maxParts, activeParts } = this;
306
+ const { minParts, maxParts, activeParts } = this;
247
307
  /**
248
308
  * When setting the active parts, it is possible
249
309
  * to set invalid data. For example,
@@ -256,7 +316,10 @@ export class Datetime {
256
316
  */
257
317
  const validatedParts = validateParts(parts, minParts, maxParts);
258
318
  this.setWorkingParts(validatedParts);
259
- if (multiple) {
319
+ if (this.isRangeMode) {
320
+ this.setActivePartsRange(validatedParts);
321
+ }
322
+ else if (this.isMultipleMode) {
260
323
  const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
261
324
  if (removeDate) {
262
325
  this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
@@ -276,6 +339,26 @@ export class Datetime {
276
339
  }
277
340
  this.confirm();
278
341
  };
342
+ /**
343
+ * Handles range selection logic when a day is clicked.
344
+ *
345
+ * Creating a new range: first click sets start, second sets end.
346
+ * Modifying an existing range:
347
+ * - Before range → new start
348
+ * - Within range → new end
349
+ * - After range → new end
350
+ * If the resulting span contains a disabled date, start a new range instead.
351
+ */
352
+ this.setActivePartsRange = (parts) => {
353
+ const current = this.rangeParts;
354
+ if (!current || isBefore(parts, current.start) || this.rangeContainsDisabledDate(current.start, parts)) {
355
+ this.activeParts = { start: parts };
356
+ }
357
+ else {
358
+ this.rangeCompletedInteractively = true;
359
+ this.activeParts = { start: current.start, end: parts };
360
+ }
361
+ };
279
362
  this.initializeKeyboardListeners = () => {
280
363
  const calendarBodyRef = this.calendarBodyRef;
281
364
  if (!calendarBodyRef) {
@@ -687,6 +770,59 @@ export class Datetime {
687
770
  if (hasValue) {
688
771
  warnIfValueOutOfBounds(valueToProcess, minParts, maxParts);
689
772
  }
773
+ /**
774
+ * Range mode: convert the value array into DatetimeRangeParts.
775
+ */
776
+ if (this.isRangeMode) {
777
+ /**
778
+ * The calendar navigates to the start of the range, or to the default
779
+ * date when there is nothing selected, so that an empty range mode
780
+ * opens on the same month as the other selection modes.
781
+ */
782
+ let navigateTo;
783
+ if (hasValue && Array.isArray(valueToProcess)) {
784
+ let rangeValues = valueToProcess;
785
+ // If more than 2 entries, use first and last
786
+ if (rangeValues.length > 2) {
787
+ rangeValues = [rangeValues[0], rangeValues[rangeValues.length - 1]];
788
+ }
789
+ let startParts = rangeValues[0];
790
+ let endParts = rangeValues.length > 1 ? rangeValues[1] : undefined;
791
+ // Normalize reversed pairs so earlier date is start
792
+ if (endParts && isAfter(startParts, endParts)) {
793
+ const temp = startParts;
794
+ startParts = endParts;
795
+ endParts = temp;
796
+ }
797
+ this.activeParts = endParts ? { start: startParts, end: endParts } : { start: startParts };
798
+ /**
799
+ * When the user just completed the range interactively, stay on the
800
+ * current month instead of jumping back to the start. The flag is
801
+ * set in setActivePartsRange and consumed here.
802
+ */
803
+ if (endParts && this.rangeCompletedInteractively) {
804
+ this.rangeCompletedInteractively = false;
805
+ return;
806
+ }
807
+ navigateTo = startParts;
808
+ }
809
+ else {
810
+ this.activeParts = [];
811
+ navigateTo = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess;
812
+ }
813
+ const targetValue = clampDate(navigateTo, minParts, maxParts);
814
+ const { month, day, year } = targetValue;
815
+ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
816
+ const bodyIsVisible = el.classList.contains('datetime-ready');
817
+ const { isGridStyle, showMonthAndYear } = this;
818
+ if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
819
+ this.animateToDate(targetValue);
820
+ }
821
+ else {
822
+ this.setWorkingParts({ month, day, year, hour: 0, minute: 0, ampm: 'am' });
823
+ }
824
+ return;
825
+ }
690
826
  /**
691
827
  * If there are multiple values, clamp to the last one.
692
828
  * This is because the last value is the one that the user
@@ -848,6 +984,27 @@ export class Datetime {
848
984
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
849
985
  return hasDatePresentation && !preferWheel;
850
986
  }
987
+ get isMultipleMode() {
988
+ return this.selectionMode === 'multiple' || (this.selectionMode === undefined && this.multiple);
989
+ }
990
+ get isRangeMode() {
991
+ return this.selectionMode === 'range';
992
+ }
993
+ /**
994
+ * Type guard: returns true if the given activeParts is a DatetimeRangeParts object.
995
+ */
996
+ isRangeParts(parts) {
997
+ return this.isRangeMode && parts !== null && typeof parts === 'object' && !Array.isArray(parts) && 'start' in parts;
998
+ }
999
+ /**
1000
+ * Returns the current range parts if in range mode, or undefined otherwise.
1001
+ */
1002
+ get rangeParts() {
1003
+ if (this.isRangeParts(this.activeParts)) {
1004
+ return this.activeParts;
1005
+ }
1006
+ return undefined;
1007
+ }
851
1008
  yearValuesChanged() {
852
1009
  this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
853
1010
  }
@@ -883,6 +1040,19 @@ export class Datetime {
883
1040
  */
884
1041
  async confirm(closeOverlay = false) {
885
1042
  const { isCalendarPicker, activeParts, preferWheel, workingParts } = this;
1043
+ /**
1044
+ * In range mode, only confirm when both start and end are selected.
1045
+ */
1046
+ if (this.isRangeMode) {
1047
+ if (!this.isRangeParts(activeParts) || !activeParts.end) {
1048
+ return;
1049
+ }
1050
+ this.setValue([convertDataToISO(activeParts.start), convertDataToISO(activeParts.end)]);
1051
+ if (closeOverlay) {
1052
+ this.closeParentOverlay(CONFIRM_ROLE);
1053
+ }
1054
+ return;
1055
+ }
886
1056
  /**
887
1057
  * We only update the value if the presentation is not a calendar picker.
888
1058
  */
@@ -918,6 +1088,22 @@ export class Datetime {
918
1088
  * @param startDate A valid [ISO-8601 string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format) to reset the datetime state to.
919
1089
  */
920
1090
  async reset(startDate) {
1091
+ if (this.isRangeMode) {
1092
+ /**
1093
+ * In range mode, reset clears the full range. If a startDate
1094
+ * is provided, the calendar navigates to that month without
1095
+ * selecting anything so the user begins a fresh selection.
1096
+ */
1097
+ this.activeParts = [];
1098
+ if (startDate) {
1099
+ const parsed = parseDate(startDate);
1100
+ if (parsed) {
1101
+ const target = Array.isArray(parsed) ? parsed[0] : parsed;
1102
+ this.setWorkingParts(target);
1103
+ }
1104
+ }
1105
+ return;
1106
+ }
921
1107
  this.processValue(startDate);
922
1108
  }
923
1109
  /**
@@ -1153,8 +1339,11 @@ export class Datetime {
1153
1339
  });
1154
1340
  }
1155
1341
  componentWillLoad() {
1156
- const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
1157
- if (multiple) {
1342
+ const { el, formatOptions, highlightedDates, multiple, selectionMode, presentation, preferWheel } = this;
1343
+ if (selectionMode === undefined && multiple) {
1344
+ printIonWarning('[ion-datetime] - The "multiple" property is deprecated. Use selectionMode="multiple" instead.', el);
1345
+ }
1346
+ if (this.isMultipleMode) {
1158
1347
  if (presentation !== 'date') {
1159
1348
  printIonWarning('[ion-datetime] - Multiple date selection is only supported for presentation="date".', el);
1160
1349
  }
@@ -1162,6 +1351,17 @@ export class Datetime {
1162
1351
  printIonWarning('[ion-datetime] - Multiple date selection is not supported with preferWheel="true".', el);
1163
1352
  }
1164
1353
  }
1354
+ if (this.isRangeMode) {
1355
+ if (presentation !== 'date') {
1356
+ printIonWarning('[ion-datetime] - Range selection is only supported for presentation="date".', el);
1357
+ }
1358
+ if (preferWheel) {
1359
+ printIonWarning('[ion-datetime] - Range selection is not supported with preferWheel="true".', el);
1360
+ }
1361
+ }
1362
+ if (selectionMode !== undefined && multiple) {
1363
+ printIonWarning('[ion-datetime] - Both "selectionMode" and "multiple" are set. "selectionMode" takes precedence.', el);
1364
+ }
1165
1365
  if (highlightedDates !== undefined) {
1166
1366
  if (presentation !== 'date' && presentation !== 'date-time' && presentation !== 'time-date') {
1167
1367
  printIonWarning('[ion-datetime] - The highlightedDates property is only supported with the date, date-time, and time-date presentations.', el);
@@ -1216,6 +1416,12 @@ export class Datetime {
1216
1416
  * is disabled or readonly.
1217
1417
  */
1218
1418
  const isButtonDisabled = disabled || readonly;
1419
+ /**
1420
+ * In range mode, the confirm button should be disabled until
1421
+ * both a start and end date have been selected.
1422
+ */
1423
+ const isRangeIncomplete = this.isRangeMode && (!this.isRangeParts(this.activeParts) || !this.activeParts.end);
1424
+ const isConfirmDisabled = isButtonDisabled || isRangeIncomplete;
1219
1425
  const confirmFill = theme === 'ionic' ? 'solid' : undefined;
1220
1426
  const hasSlottedButtons = this.el.querySelector('[slot="buttons"]') !== null;
1221
1427
  if (!hasSlottedButtons && !showDefaultButtons && !showClearButton) {
@@ -1235,7 +1441,7 @@ export class Datetime {
1235
1441
  return (h("div", { class: "datetime-footer" }, h("div", { class: "datetime-buttons" }, h("div", { class: {
1236
1442
  ['datetime-action-buttons']: true,
1237
1443
  ['has-clear-button']: this.showClearButton,
1238
- } }, h("slot", { name: "buttons" }, showDefaultButtons && (h("ion-button", { id: "cancel-button", color: this.color, onClick: () => this.cancel(true), disabled: isButtonDisabled }, this.cancelText)), h("div", { class: "datetime-action-buttons-container" }, showClearButton && (h("ion-button", { id: "clear-button", color: this.color, onClick: () => clearButtonClick(), disabled: isButtonDisabled }, this.clearText)), showDefaultButtons && (h("ion-button", { id: "confirm-button", fill: confirmFill, color: this.color, onClick: () => this.confirm(true), disabled: isButtonDisabled }, this.doneText))))))));
1444
+ } }, h("slot", { name: "buttons" }, showDefaultButtons && (h("ion-button", { id: "cancel-button", color: this.color, onClick: () => this.cancel(true), disabled: isButtonDisabled }, this.cancelText)), h("div", { class: "datetime-action-buttons-container" }, showClearButton && (h("ion-button", { id: "clear-button", color: this.color, onClick: () => clearButtonClick(), disabled: isButtonDisabled }, this.clearText)), showDefaultButtons && (h("ion-button", { id: "confirm-button", fill: confirmFill, color: this.color, onClick: () => this.confirm(true), disabled: isConfirmDisabled }, this.doneText))))))));
1239
1445
  }
1240
1446
  /**
1241
1447
  * Wheel picker render methods
@@ -1424,8 +1630,8 @@ export class Datetime {
1424
1630
  ...this.workingParts,
1425
1631
  month: ev.detail.value,
1426
1632
  });
1427
- // Month wheel is navigation-only in multi-select mode as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1428
- if (!this.multiple) {
1633
+ // Month wheel is navigation-only in multi-select and range modes as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1634
+ if (!this.isMultipleMode && !this.isRangeMode) {
1429
1635
  this.setActiveParts({
1430
1636
  ...activePart,
1431
1637
  month: ev.detail.value,
@@ -1446,8 +1652,8 @@ export class Datetime {
1446
1652
  ...this.workingParts,
1447
1653
  year: ev.detail.value,
1448
1654
  });
1449
- // Year wheel is navigation-only in multi-select mode as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1450
- if (!this.multiple) {
1655
+ // Year wheel is navigation-only in multi-select and range modes as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1656
+ if (!this.isMultipleMode && !this.isRangeMode) {
1451
1657
  this.setActiveParts({
1452
1658
  ...activePart,
1453
1659
  year: ev.detail.value,
@@ -1591,7 +1797,7 @@ export class Datetime {
1591
1797
  'calendar-month-disabled': !isWorkingMonth && swipeDisabled,
1592
1798
  } }, h("div", { class: "calendar-month-grid" }, getDaysOfMonth(month, year, this.firstDayOfWeek % 7, this.showAdjacentDays).map((dateObject, index) => {
1593
1799
  const { day, dayOfWeek, isAdjacentDay } = dateObject;
1594
- const { el, highlightedDates, isDateEnabled, multiple, showAdjacentDays } = this;
1800
+ const { el, highlightedDates, isDateEnabled, showAdjacentDays } = this;
1595
1801
  let _month = month;
1596
1802
  let _year = year;
1597
1803
  if (showAdjacentDays && isAdjacentDay && day !== null) {
@@ -1620,7 +1826,7 @@ export class Datetime {
1620
1826
  }
1621
1827
  const referenceParts = { month: _month, day, year: _year, isAdjacentDay };
1622
1828
  const isCalendarPadding = day === null;
1623
- const { isActive, isToday, ariaLabel, ariaSelected, disabled: isDayDisabled, text, } = getCalendarDayState(this.locale, referenceParts, this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
1829
+ const { isActive, isToday, ariaLabel, ariaSelected, disabled: isDayDisabled, isRangeStart, isRangeEnd, isInRange, text, } = getCalendarDayState(this.locale, referenceParts, this.isRangeParts(this.activeParts) ? [] : this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues, this.rangeParts);
1624
1830
  const dateIsoString = convertDataToISO(referenceParts);
1625
1831
  let isCalDayDisabled = isCalMonthDisabled || isDayDisabled;
1626
1832
  if (!isCalDayDisabled && isDateEnabled !== undefined) {
@@ -1643,24 +1849,60 @@ export class Datetime {
1643
1849
  */
1644
1850
  const isCalDayConstrained = isCalDayDisabled && isDatetimeDisabled;
1645
1851
  const isButtonDisabled = isCalDayDisabled || isDatetimeDisabled;
1852
+ /**
1853
+ * Determine if this day is part of the range selection.
1854
+ * Range styling takes priority over highlightedDates.
1855
+ */
1856
+ const isPartOfRange = isRangeStart || isRangeEnd || isInRange;
1646
1857
  let dateStyle = undefined;
1647
1858
  /**
1648
- * Custom highlight styles should not override the style for selected dates,
1649
- * nor apply to "filler days" at the start of the grid.
1859
+ * Custom highlight styles should not override the style for selected dates
1860
+ * or range selection, nor apply to "filler days" at the start of the grid.
1650
1861
  */
1651
- if (highlightedDates !== undefined && !isActive && day !== null && !isAdjacentDay) {
1862
+ if (highlightedDates !== undefined && !isActive && !isPartOfRange && day !== null && !isAdjacentDay) {
1652
1863
  dateStyle = getHighlightStyles(highlightedDates, dateIsoString, el);
1653
1864
  }
1654
1865
  let dateParts = undefined;
1655
1866
  // "Filler days" at the beginning of the grid should not get the calendar day
1656
1867
  // CSS parts added to them
1657
1868
  if (!isCalendarPadding && !isAdjacentDay) {
1658
- dateParts = `calendar-day${isActive ? ' active' : ''}${isToday ? ' today' : ''}${isCalDayDisabled ? ' disabled' : ''}`;
1869
+ dateParts = `calendar-day${isActive || isRangeStart || isRangeEnd ? ' active' : ''}${isToday ? ' today' : ''}${isCalDayDisabled ? ' disabled' : ''}${isRangeStart ? ' range-start range-selection' : ''}${isRangeEnd ? ' range-end range-selection' : ''}${isInRange ? ' range-selection' : ''}`;
1659
1870
  }
1660
1871
  else if (isAdjacentDay) {
1661
- dateParts = `calendar-day${isCalDayDisabled ? ' disabled' : ''}`;
1872
+ /**
1873
+ * Adjacent days skip 'active' and 'today' parts but still
1874
+ * include range parts so ::part() styling is consistent
1875
+ * with the visual range track rendering.
1876
+ */
1877
+ dateParts = `calendar-day${isCalDayDisabled ? ' disabled' : ''}${isRangeStart ? ' range-start range-selection' : ''}${isRangeEnd ? ' range-end range-selection' : ''}${isInRange ? ' range-selection' : ''}`;
1662
1878
  }
1663
- return (h("div", { class: "calendar-day-wrapper" }, h("button", {
1879
+ /**
1880
+ * Build the wrapper's shadow part attribute for range track styling.
1881
+ */
1882
+ const hasCompleteRange = this.rangeParts?.end !== undefined;
1883
+ const wrapperParts = isPartOfRange && hasCompleteRange && !isCalendarPadding ? 'range-track' : undefined;
1884
+ /**
1885
+ * Build the aria-label for range mode accessibility.
1886
+ */
1887
+ let rangeAriaLabel = ariaLabel;
1888
+ if (this.isRangeMode && ariaLabel) {
1889
+ if (isRangeStart) {
1890
+ rangeAriaLabel += `, ${this.startDateLabel}`;
1891
+ }
1892
+ if (isRangeEnd) {
1893
+ rangeAriaLabel += `, ${this.endDateLabel}`;
1894
+ }
1895
+ if (isInRange) {
1896
+ rangeAriaLabel += ', in range';
1897
+ }
1898
+ }
1899
+ return (h("div", { class: {
1900
+ 'calendar-day-wrapper': true,
1901
+ // A start without an end is only the circle until the range is complete
1902
+ 'calendar-day-wrapper-range-start': isRangeStart && this.rangeParts?.end !== undefined,
1903
+ 'calendar-day-wrapper-in-range': isInRange,
1904
+ 'calendar-day-wrapper-range-end': isRangeEnd,
1905
+ }, part: wrapperParts }, h("button", {
1664
1906
  // We need to use !important for the inline styles here because
1665
1907
  // otherwise the CSS shadow parts will override these styles.
1666
1908
  // See https://github.com/WICG/webcomponents/issues/847
@@ -1676,28 +1918,40 @@ export class Datetime {
1676
1918
  }, tabindex: "-1", "data-day": day, "data-month": _month, "data-year": _year, "data-index": index, "data-day-of-week": dayOfWeek, disabled: isButtonDisabled, class: {
1677
1919
  'calendar-day-padding': isCalendarPadding,
1678
1920
  'calendar-day': true,
1679
- 'calendar-day-active': isActive,
1921
+ 'calendar-day-active': isActive || isRangeStart || isRangeEnd,
1680
1922
  'calendar-day-constrained': isCalDayConstrained,
1681
1923
  'calendar-day-today': isToday,
1682
1924
  'calendar-day-adjacent-day': isAdjacentDay,
1683
- }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": ariaSelected, "aria-label": ariaLabel, onClick: () => {
1925
+ 'calendar-day-range-start': isRangeStart,
1926
+ 'calendar-day-range-end': isRangeEnd,
1927
+ 'calendar-day-in-range': isInRange,
1928
+ }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": isActive || isRangeStart || isRangeEnd ? 'true' : ariaSelected, "aria-label": rangeAriaLabel, onClick: () => {
1684
1929
  if (isCalendarPadding) {
1685
1930
  return;
1686
1931
  }
1687
1932
  if (isAdjacentDay) {
1688
1933
  // The user selected a day outside the current month. Ignore this button, as the month will be re-rendered.
1689
1934
  this.el.blur();
1690
- this.activeParts = { ...activePart, ...referenceParts };
1935
+ if (!this.isRangeMode) {
1936
+ this.activeParts = { ...activePart, ...referenceParts };
1937
+ }
1691
1938
  this.animateToDate(referenceParts);
1692
- this.confirm();
1939
+ if (this.isRangeMode) {
1940
+ this.setActiveParts(referenceParts);
1941
+ }
1942
+ else {
1943
+ this.confirm();
1944
+ }
1693
1945
  }
1694
1946
  else {
1695
1947
  this.setWorkingParts({
1696
1948
  ...this.workingParts,
1697
1949
  ...referenceParts,
1698
1950
  });
1699
- // Multiple only needs date info so we can wipe out other fields like time.
1700
- if (multiple) {
1951
+ if (this.isRangeMode) {
1952
+ this.setActiveParts(referenceParts);
1953
+ }
1954
+ else if (this.isMultipleMode) {
1701
1955
  this.setActiveParts(referenceParts, isActive);
1702
1956
  }
1703
1957
  else {
@@ -1717,7 +1971,17 @@ export class Datetime {
1717
1971
  })));
1718
1972
  }
1719
1973
  renderCalendar(theme) {
1720
- return (h("div", { class: "datetime-calendar", key: "datetime-calendar" }, this.renderCalendarHeader(theme), this.renderCalendarBody()));
1974
+ return (h("div", { class: "datetime-calendar", key: "datetime-calendar" }, this.renderCalendarHeader(theme), this.renderCalendarBody(), this.isRangeMode && this.rangeParts && !this.rangeParts.end && (h("div", { "aria-live": "polite", role: "status", style: {
1975
+ position: 'absolute',
1976
+ width: '1px',
1977
+ height: '1px',
1978
+ padding: '0',
1979
+ margin: '-1px',
1980
+ overflow: 'hidden',
1981
+ clip: 'rect(0, 0, 0, 0)',
1982
+ whiteSpace: 'nowrap',
1983
+ border: '0',
1984
+ } }, `${this.startDateLabel} selected, select ${this.endDateLabel.toLowerCase()}`))));
1721
1985
  }
1722
1986
  renderTimeLabel() {
1723
1987
  const hasSlottedTimeLabel = this.el.querySelector('[slot="time-label"]') !== null;
@@ -1770,10 +2034,23 @@ export class Datetime {
1770
2034
  ];
1771
2035
  }
1772
2036
  getHeaderSelectedDateText() {
1773
- const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
2037
+ const { activeParts, formatOptions, titleSelectedDatesFormatter } = this;
1774
2038
  const isArray = Array.isArray(activeParts);
2039
+ const dateFormatOptions = formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' };
1775
2040
  let headerText;
1776
- if (multiple && isArray && activeParts.length !== 1) {
2041
+ if (this.isRangeMode) {
2042
+ headerText = this.getRangeHeaderText(formatOptions?.date ?? { month: 'short', day: 'numeric' });
2043
+ if (titleSelectedDatesFormatter !== undefined) {
2044
+ try {
2045
+ const rangeValues = this.getRangeValueArray();
2046
+ headerText = titleSelectedDatesFormatter(rangeValues);
2047
+ }
2048
+ catch (e) {
2049
+ printIonError('[ion-datetime] - Exception in provided `titleSelectedDatesFormatter`:', e);
2050
+ }
2051
+ }
2052
+ }
2053
+ else if (this.isMultipleMode && isArray && activeParts.length !== 1) {
1777
2054
  headerText = `${activeParts.length} days`; // default/fallback for multiple selection
1778
2055
  if (titleSelectedDatesFormatter !== undefined) {
1779
2056
  try {
@@ -1786,10 +2063,37 @@ export class Datetime {
1786
2063
  }
1787
2064
  else {
1788
2065
  // for exactly 1 day selected (multiple set or not), show a formatted version of that
1789
- headerText = getLocalizedDateTime(this.locale, this.getActivePartsWithFallback(), formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' });
2066
+ headerText = getLocalizedDateTime(this.locale, this.getActivePartsWithFallback(), dateFormatOptions);
1790
2067
  }
1791
2068
  return headerText;
1792
2069
  }
2070
+ /**
2071
+ * Returns the formatted range header text.
2072
+ * Shows "Start date - End date" with placeholders from startDateLabel/endDateLabel.
2073
+ */
2074
+ getRangeHeaderText(dateFormatOptions) {
2075
+ const range = this.rangeParts;
2076
+ if (!range) {
2077
+ return `${this.startDateLabel} - ${this.endDateLabel}`;
2078
+ }
2079
+ const startText = getLocalizedDateTime(this.locale, range.start, dateFormatOptions);
2080
+ const endText = range.end ? getLocalizedDateTime(this.locale, range.end, dateFormatOptions) : this.endDateLabel;
2081
+ return `${startText} - ${endText}`;
2082
+ }
2083
+ /**
2084
+ * Returns the current range value as an ISO string array for
2085
+ * titleSelectedDatesFormatter and ionChange.
2086
+ */
2087
+ getRangeValueArray() {
2088
+ const range = this.rangeParts;
2089
+ if (!range) {
2090
+ return [];
2091
+ }
2092
+ if (!range.end) {
2093
+ return [convertDataToISO(range.start)];
2094
+ }
2095
+ return [convertDataToISO(range.start), convertDataToISO(range.end)];
2096
+ }
1793
2097
  renderHeader(showExpandedHeader = true) {
1794
2098
  const hasSlottedTitle = this.el.querySelector('[slot="title"]') !== null;
1795
2099
  if (!hasSlottedTitle && !this.showDefaultTitle) {
@@ -1913,7 +2217,7 @@ export class Datetime {
1913
2217
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1914
2218
  const hasWheelVariant = hasDatePresentation && preferWheel;
1915
2219
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1916
- return (h(Host, { key: '6ee2389238b42b0d10c5f5254a8ba61bfc53c677', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: {
2220
+ return (h(Host, { key: '0a05835fa52e91147d72250941201b89fd850dda', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: {
1917
2221
  ...createColorClasses(color, {
1918
2222
  [theme]: true,
1919
2223
  ['datetime-readonly']: readonly,
@@ -1924,8 +2228,9 @@ export class Datetime {
1924
2228
  [`datetime-size-${size}`]: true,
1925
2229
  [`datetime-prefer-wheel`]: hasWheelVariant,
1926
2230
  [`datetime-grid`]: isGridStyle,
2231
+ [`datetime-range`]: this.isRangeMode,
1927
2232
  }),
1928
- } }, h("div", { key: '531803dfb77fa3d694c90b6fc45e71e304876fa8', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
2233
+ } }, h("div", { key: '5b5d6999f55b85523f65763a906f7f16c25f2eb2', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
1929
2234
  }
1930
2235
  static get is() { return "ion-datetime"; }
1931
2236
  static get encapsulation() { return "shadow"; }
@@ -2387,7 +2692,10 @@ export class Datetime {
2387
2692
  "required": false,
2388
2693
  "optional": false,
2389
2694
  "docs": {
2390
- "tags": [],
2695
+ "tags": [{
2696
+ "name": "deprecated",
2697
+ "text": "Use `selectionMode=\"multiple\"` instead."
2698
+ }],
2391
2699
  "text": "If `true`, multiple dates can be selected at once. Only\napplies to `presentation=\"date\"` and `preferWheel=\"false\"`."
2392
2700
  },
2393
2701
  "getter": false,
@@ -2396,6 +2704,65 @@ export class Datetime {
2396
2704
  "attribute": "multiple",
2397
2705
  "defaultValue": "false"
2398
2706
  },
2707
+ "selectionMode": {
2708
+ "type": "string",
2709
+ "mutable": false,
2710
+ "complexType": {
2711
+ "original": "'multiple' | 'range'",
2712
+ "resolved": "\"multiple\" | \"range\" | undefined",
2713
+ "references": {}
2714
+ },
2715
+ "required": false,
2716
+ "optional": true,
2717
+ "docs": {
2718
+ "tags": [],
2719
+ "text": "Controls the date selection behavior.\n`'multiple'` enables selecting multiple individual dates.\n`'range'` enables selecting a start and end date range.\nWhen unset, single-date selection is used.\n\nRange selection only works with `presentation=\"date\"` and `preferWheel=\"false\"`."
2720
+ },
2721
+ "getter": false,
2722
+ "setter": false,
2723
+ "reflect": false,
2724
+ "attribute": "selection-mode"
2725
+ },
2726
+ "startDateLabel": {
2727
+ "type": "string",
2728
+ "mutable": false,
2729
+ "complexType": {
2730
+ "original": "string",
2731
+ "resolved": "string",
2732
+ "references": {}
2733
+ },
2734
+ "required": false,
2735
+ "optional": false,
2736
+ "docs": {
2737
+ "tags": [],
2738
+ "text": "The text shown in the datetime header when the start of the range\nis empty, also used by screen readers to announce the start date.\nOnly applies when `selectionMode=\"range\"`."
2739
+ },
2740
+ "getter": false,
2741
+ "setter": false,
2742
+ "reflect": false,
2743
+ "attribute": "start-date-label",
2744
+ "defaultValue": "'Start date'"
2745
+ },
2746
+ "endDateLabel": {
2747
+ "type": "string",
2748
+ "mutable": false,
2749
+ "complexType": {
2750
+ "original": "string",
2751
+ "resolved": "string",
2752
+ "references": {}
2753
+ },
2754
+ "required": false,
2755
+ "optional": false,
2756
+ "docs": {
2757
+ "tags": [],
2758
+ "text": "The text shown in the datetime header when the end of the range\nis empty, also used by screen readers to announce the end date.\nOnly applies when `selectionMode=\"range\"`."
2759
+ },
2760
+ "getter": false,
2761
+ "setter": false,
2762
+ "reflect": false,
2763
+ "attribute": "end-date-label",
2764
+ "defaultValue": "'End date'"
2765
+ },
2399
2766
  "highlightedDates": {
2400
2767
  "type": "unknown",
2401
2768
  "mutable": false,
@@ -2754,6 +3121,12 @@ export class Datetime {
2754
3121
  "Promise": {
2755
3122
  "location": "global",
2756
3123
  "id": "global::Promise"
3124
+ },
3125
+ "DatetimeParts": {
3126
+ "location": "import",
3127
+ "path": "./datetime-interface",
3128
+ "id": "src/components/datetime/datetime-interface.ts::DatetimeParts",
3129
+ "referenceLocation": "DatetimeParts"
2757
3130
  }
2758
3131
  },
2759
3132
  "return": "Promise<void>"