@ionic/core 8.8.20-dev.11791573097.1f843ab0 → 8.8.20-dev.11792194859.16be5d7f

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 (33) hide show
  1. package/components/ion-datetime-button.js +1 -1
  2. package/components/ion-datetime.js +1 -1
  3. package/components/{p-BE21dB_a.js → p-Bpr3edV8.js} +1 -1
  4. package/dist/cjs/ion-datetime-button.cjs.entry.js +28 -10
  5. package/dist/cjs/ion-datetime_3.cjs.entry.js +354 -30
  6. package/dist/cjs/ionic.cjs.js +1 -1
  7. package/dist/cjs/loader.cjs.js +1 -1
  8. package/dist/collection/components/datetime/datetime.ionic.css +144 -1
  9. package/dist/collection/components/datetime/datetime.ios.css +118 -0
  10. package/dist/collection/components/datetime/datetime.js +407 -27
  11. package/dist/collection/components/datetime/datetime.md.css +117 -0
  12. package/dist/collection/components/datetime/utils/state.js +18 -1
  13. package/dist/collection/components/datetime-button/datetime-button.js +28 -10
  14. package/dist/docs.json +151 -1
  15. package/dist/esm/{format-2WlZcMvG.js → format-BNGmd1mx.js} +1 -1
  16. package/dist/esm/ion-datetime-button.entry.js +29 -11
  17. package/dist/esm/ion-datetime_3.entry.js +355 -31
  18. package/dist/esm/ionic.js +1 -1
  19. package/dist/esm/loader.js +1 -1
  20. package/dist/html.html-data.json +20 -0
  21. package/dist/ionic/ionic.esm.js +1 -1
  22. package/dist/ionic/p-4aa6153d.entry.js +4 -0
  23. package/dist/ionic/p-4f2e482f.entry.js +4 -0
  24. package/dist/ionic/{p-7k7Q1A38.js → p-CrgdPEJR.js} +1 -1
  25. package/dist/types/components/datetime/datetime-interface.d.ts +4 -0
  26. package/dist/types/components/datetime/datetime.d.ts +66 -2
  27. package/dist/types/components/datetime/utils/state.d.ts +5 -2
  28. package/dist/types/components.d.ts +33 -0
  29. package/hydrate/index.js +390 -40
  30. package/hydrate/index.mjs +390 -40
  31. package/package.json +1 -1
  32. package/dist/ionic/p-3a4f9b8a.entry.js +0 -4
  33. package/dist/ionic/p-f6768728.entry.js +0 -4
@@ -52,6 +52,11 @@ import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from "./ut
52
52
  * @part calendar-day today - The calendar day that contains the current day.
53
53
  * @part calendar-day disabled - The calendar day that is disabled.
54
54
  *
55
+ * @part range-start - The calendar day button that is the start of a range selection.
56
+ * @part range-end - The calendar day button that is the end of a range selection.
57
+ * @part range-selection - The calendar day button that is part of a range selection (includes start and end).
58
+ * @part range-track - The calendar day wrapper element whose ::before pseudo-element draws the range highlight track.
59
+ *
55
60
  * @part datetime-header - The datetime header contains the content for the `title` slot and the selected date.
56
61
  * @part datetime-title - The element that contains the `title` slot content.
57
62
  * @part datetime-selected-date - The element that contains the selected date.
@@ -141,8 +146,22 @@ export class Datetime {
141
146
  /**
142
147
  * If `true`, multiple dates can be selected at once. Only
143
148
  * applies to `presentation="date"` and `preferWheel="false"`.
149
+ *
150
+ * @deprecated Use `selectionMode="multiple"` instead.
144
151
  */
145
152
  this.multiple = false;
153
+ /**
154
+ * The text shown in the datetime header when the start of the range
155
+ * is empty, also used by screen readers to announce the start date.
156
+ * Only applies when `selectionMode="range"`.
157
+ */
158
+ this.startDateLabel = 'Start date';
159
+ /**
160
+ * The text shown in the datetime header when the end of the range
161
+ * is empty, also used by screen readers to announce the end date.
162
+ * Only applies when `selectionMode="range"`.
163
+ */
164
+ this.endDateLabel = 'End date';
146
165
  /**
147
166
  * If `true`, a header will be shown above the calendar
148
167
  * picker. This will include both the slotted title, and
@@ -195,8 +214,8 @@ export class Datetime {
195
214
  */
196
215
  this.preferWheel = false;
197
216
  this.warnIfIncorrectValueUsage = () => {
198
- const { multiple, value } = this;
199
- if (!multiple && Array.isArray(value)) {
217
+ const { value } = this;
218
+ if (!this.isMultipleMode && !this.isRangeMode && Array.isArray(value)) {
200
219
  /**
201
220
  * We do some processing on the `value` array so
202
221
  * that it looks more like an array when logged to
@@ -205,7 +224,13 @@ export class Datetime {
205
224
  * Default toString() behavior: a,b
206
225
  * Custom behavior: ['a', 'b']
207
226
  */
208
- 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".
227
+ 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.
228
+
229
+ Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
230
+ `, this.el);
231
+ }
232
+ if (this.isRangeMode && Array.isArray(value) && value.length > 2) {
233
+ printIonWarning(`[ion-datetime] - More than 2 values were passed with selectionMode="range". Only the first and last values will be used.
209
234
 
210
235
  Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
211
236
  `, this.el);
@@ -232,6 +257,9 @@ export class Datetime {
232
257
  };
233
258
  this.getActivePart = () => {
234
259
  const { activeParts } = this;
260
+ if (this.isRangeParts(activeParts)) {
261
+ return activeParts.start;
262
+ }
235
263
  return Array.isArray(activeParts) ? activeParts[0] : activeParts;
236
264
  };
237
265
  this.closeParentOverlay = (role) => {
@@ -243,6 +271,33 @@ export class Datetime {
243
271
  this.setWorkingParts = (parts) => {
244
272
  this.workingParts = Object.assign({}, parts);
245
273
  };
274
+ /**
275
+ * Checks whether the span from start to end contains any disabled day.
276
+ * Used by range mode to prevent selecting across disabled dates.
277
+ */
278
+ this.rangeContainsDisabledDate = (start, end) => {
279
+ const { minParts, maxParts, parsedDayValues, isDateEnabled } = this;
280
+ let cursor = getNextDay(start);
281
+ while (isBefore(cursor, end) || isSameDay(cursor, end)) {
282
+ if (isSameDay(cursor, end))
283
+ break;
284
+ if (isDayDisabled(cursor, minParts, maxParts, parsedDayValues)) {
285
+ return true;
286
+ }
287
+ if (isDateEnabled) {
288
+ try {
289
+ if (!isDateEnabled(convertDataToISO(cursor))) {
290
+ return true;
291
+ }
292
+ }
293
+ catch (_a) {
294
+ // Swallow user callback exceptions during validation
295
+ }
296
+ }
297
+ cursor = getNextDay(cursor);
298
+ }
299
+ return false;
300
+ };
246
301
  this.setActiveParts = (parts, removeDate = false) => {
247
302
  /** if the datetime component is in readonly mode,
248
303
  * allow browsing of the calendar without changing
@@ -251,7 +306,7 @@ export class Datetime {
251
306
  if (this.readonly) {
252
307
  return;
253
308
  }
254
- const { multiple, minParts, maxParts, activeParts } = this;
309
+ const { minParts, maxParts, activeParts } = this;
255
310
  /**
256
311
  * When setting the active parts, it is possible
257
312
  * to set invalid data. For example,
@@ -264,7 +319,10 @@ export class Datetime {
264
319
  */
265
320
  const validatedParts = validateParts(parts, minParts, maxParts);
266
321
  this.setWorkingParts(validatedParts);
267
- if (multiple) {
322
+ if (this.isRangeMode) {
323
+ this.setActivePartsRange(validatedParts);
324
+ }
325
+ else if (this.isMultipleMode) {
268
326
  const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
269
327
  if (removeDate) {
270
328
  this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
@@ -282,6 +340,70 @@ export class Datetime {
282
340
  }
283
341
  this.confirm();
284
342
  };
343
+ /**
344
+ * Handles range selection logic when a day is clicked.
345
+ *
346
+ * Creating a new range: first click sets start, second sets end.
347
+ * Modifying an existing range:
348
+ * - Before range → new start
349
+ * - Within range → new end
350
+ * - After range → new end
351
+ * If the resulting span contains a disabled date, start a new range instead.
352
+ */
353
+ this.setActivePartsRange = (parts) => {
354
+ const current = this.rangeParts;
355
+ if (!current) {
356
+ // No range yet — start a new one
357
+ this.activeParts = { start: parts };
358
+ return;
359
+ }
360
+ const { start, end } = current;
361
+ if (end) {
362
+ // A complete range exists
363
+ if (isBefore(parts, start)) {
364
+ // Before range → new start
365
+ this.activeParts = { start: parts };
366
+ }
367
+ else if (isSameDay(parts, start) || (isAfter(parts, start) && isBefore(parts, end))) {
368
+ // Within range (including start) → set new end
369
+ if (this.rangeContainsDisabledDate(start, parts)) {
370
+ this.activeParts = { start: parts };
371
+ }
372
+ else {
373
+ this.activeParts = { start, end: parts };
374
+ }
375
+ }
376
+ else {
377
+ // After or equal to end → new end
378
+ if (this.rangeContainsDisabledDate(start, parts)) {
379
+ this.activeParts = { start: parts };
380
+ }
381
+ else {
382
+ this.activeParts = { start, end: parts };
383
+ }
384
+ }
385
+ }
386
+ else {
387
+ // Only start date, no end yet
388
+ if (isSameDay(parts, start)) {
389
+ // Same day → single-day range
390
+ this.activeParts = { start, end: parts };
391
+ }
392
+ else if (isBefore(parts, start)) {
393
+ // Before start → new start
394
+ this.activeParts = { start: parts };
395
+ }
396
+ else {
397
+ // After start → set end if no disabled dates in between
398
+ if (this.rangeContainsDisabledDate(start, parts)) {
399
+ this.activeParts = { start: parts };
400
+ }
401
+ else {
402
+ this.activeParts = { start, end: parts };
403
+ }
404
+ }
405
+ }
406
+ };
285
407
  this.initializeKeyboardListeners = () => {
286
408
  const calendarBodyRef = this.calendarBodyRef;
287
409
  if (!calendarBodyRef) {
@@ -679,6 +801,43 @@ export class Datetime {
679
801
  if (hasValue) {
680
802
  warnIfValueOutOfBounds(valueToProcess, minParts, maxParts);
681
803
  }
804
+ /**
805
+ * Range mode: convert the value array into DatetimeRangeParts.
806
+ */
807
+ if (this.isRangeMode) {
808
+ if (hasValue && Array.isArray(valueToProcess)) {
809
+ let rangeValues = valueToProcess;
810
+ // If more than 2 entries, use first and last
811
+ if (rangeValues.length > 2) {
812
+ rangeValues = [rangeValues[0], rangeValues[rangeValues.length - 1]];
813
+ }
814
+ let startParts = rangeValues[0];
815
+ let endParts = rangeValues.length > 1 ? rangeValues[1] : undefined;
816
+ // Normalize reversed pairs so earlier date is start
817
+ if (endParts && isAfter(startParts, endParts)) {
818
+ const temp = startParts;
819
+ startParts = endParts;
820
+ endParts = temp;
821
+ }
822
+ this.activeParts = endParts ? { start: startParts, end: endParts } : { start: startParts };
823
+ // Navigate calendar to the start date
824
+ const targetValue = clampDate(startParts, minParts, maxParts);
825
+ const { month, day, year } = targetValue;
826
+ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
827
+ const bodyIsVisible = el.classList.contains('datetime-ready');
828
+ const { isGridStyle, showMonthAndYear } = this;
829
+ if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
830
+ this.animateToDate(targetValue);
831
+ }
832
+ else {
833
+ this.setWorkingParts({ month, day, year, hour: 0, minute: 0, ampm: 'am' });
834
+ }
835
+ }
836
+ else {
837
+ this.activeParts = [];
838
+ }
839
+ return;
840
+ }
682
841
  /**
683
842
  * If there are multiple values, clamp to the last one.
684
843
  * This is because the last value is the one that the user
@@ -839,6 +998,27 @@ export class Datetime {
839
998
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
840
999
  return hasDatePresentation && !preferWheel;
841
1000
  }
1001
+ get isMultipleMode() {
1002
+ return this.selectionMode === 'multiple' || (this.selectionMode === undefined && this.multiple);
1003
+ }
1004
+ get isRangeMode() {
1005
+ return this.selectionMode === 'range';
1006
+ }
1007
+ /**
1008
+ * Type guard: returns true if the given activeParts is a DatetimeRangeParts object.
1009
+ */
1010
+ isRangeParts(parts) {
1011
+ return this.isRangeMode && parts !== null && typeof parts === 'object' && !Array.isArray(parts) && 'start' in parts;
1012
+ }
1013
+ /**
1014
+ * Returns the current range parts if in range mode, or undefined otherwise.
1015
+ */
1016
+ get rangeParts() {
1017
+ if (this.isRangeParts(this.activeParts)) {
1018
+ return this.activeParts;
1019
+ }
1020
+ return undefined;
1021
+ }
842
1022
  yearValuesChanged() {
843
1023
  this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
844
1024
  }
@@ -874,6 +1054,18 @@ export class Datetime {
874
1054
  */
875
1055
  async confirm(closeOverlay = false) {
876
1056
  const { isCalendarPicker, activeParts, preferWheel, workingParts } = this;
1057
+ /**
1058
+ * In range mode, only confirm when both start and end are selected.
1059
+ */
1060
+ if (this.isRangeMode) {
1061
+ if (this.isRangeParts(activeParts) && activeParts.end) {
1062
+ this.setValue([convertDataToISO(activeParts.start), convertDataToISO(activeParts.end)]);
1063
+ }
1064
+ if (closeOverlay) {
1065
+ this.closeParentOverlay(CONFIRM_ROLE);
1066
+ }
1067
+ return;
1068
+ }
877
1069
  /**
878
1070
  * We only update the value if the presentation is not a calendar picker.
879
1071
  */
@@ -908,6 +1100,22 @@ export class Datetime {
908
1100
  * @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.
909
1101
  */
910
1102
  async reset(startDate) {
1103
+ if (this.isRangeMode) {
1104
+ /**
1105
+ * In range mode, reset clears the full range. If a startDate
1106
+ * is provided, the calendar navigates to that month without
1107
+ * selecting anything so the user begins a fresh selection.
1108
+ */
1109
+ this.activeParts = [];
1110
+ if (startDate) {
1111
+ const parsed = parseDate(startDate);
1112
+ if (parsed) {
1113
+ const target = Array.isArray(parsed) ? parsed[0] : parsed;
1114
+ this.setWorkingParts(target);
1115
+ }
1116
+ }
1117
+ return;
1118
+ }
911
1119
  this.processValue(startDate);
912
1120
  }
913
1121
  /**
@@ -919,6 +1127,14 @@ export class Datetime {
919
1127
  * @param closeOverlay If `true`, closes the parent overlay. Defaults to `false`.
920
1128
  */
921
1129
  async cancel(closeOverlay = false) {
1130
+ /**
1131
+ * In range mode, clear the visible selection (including partial ranges)
1132
+ * so the component returns to an empty state. The value property is
1133
+ * left untouched, matching single/multiple mode behavior.
1134
+ */
1135
+ if (this.isRangeMode) {
1136
+ this.activeParts = [];
1137
+ }
922
1138
  this.ionCancel.emit();
923
1139
  if (closeOverlay) {
924
1140
  this.closeParentOverlay(CANCEL_ROLE);
@@ -1099,8 +1315,11 @@ export class Datetime {
1099
1315
  });
1100
1316
  }
1101
1317
  componentWillLoad() {
1102
- const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
1103
- if (multiple) {
1318
+ const { el, formatOptions, highlightedDates, multiple, selectionMode, presentation, preferWheel } = this;
1319
+ if (selectionMode === undefined && multiple) {
1320
+ printIonWarning('[ion-datetime] - The "multiple" property is deprecated. Use selectionMode="multiple" instead.', el);
1321
+ }
1322
+ if (this.isMultipleMode) {
1104
1323
  if (presentation !== 'date') {
1105
1324
  printIonWarning('[ion-datetime] - Multiple date selection is only supported for presentation="date".', el);
1106
1325
  }
@@ -1108,6 +1327,17 @@ export class Datetime {
1108
1327
  printIonWarning('[ion-datetime] - Multiple date selection is not supported with preferWheel="true".', el);
1109
1328
  }
1110
1329
  }
1330
+ if (this.isRangeMode) {
1331
+ if (presentation !== 'date') {
1332
+ printIonWarning('[ion-datetime] - Range selection is only supported for presentation="date".', el);
1333
+ }
1334
+ if (preferWheel) {
1335
+ printIonWarning('[ion-datetime] - Range selection is not supported with preferWheel="true".', el);
1336
+ }
1337
+ }
1338
+ if (selectionMode !== undefined && multiple) {
1339
+ printIonWarning('[ion-datetime] - Both "selectionMode" and "multiple" are set. "selectionMode" takes precedence.', el);
1340
+ }
1111
1341
  if (highlightedDates !== undefined) {
1112
1342
  if (presentation !== 'date' && presentation !== 'date-time' && presentation !== 'time-date') {
1113
1343
  printIonWarning('[ion-datetime] - The highlightedDates property is only supported with the date, date-time, and time-date presentations.', el);
@@ -1162,6 +1392,12 @@ export class Datetime {
1162
1392
  * is disabled or readonly.
1163
1393
  */
1164
1394
  const isButtonDisabled = disabled || readonly;
1395
+ /**
1396
+ * In range mode, the confirm button should be disabled until
1397
+ * both a start and end date have been selected.
1398
+ */
1399
+ const isRangeIncomplete = this.isRangeMode && (!this.isRangeParts(this.activeParts) || !this.activeParts.end);
1400
+ const isConfirmDisabled = isButtonDisabled || isRangeIncomplete;
1165
1401
  const confirmFill = theme === 'ionic' ? 'solid' : undefined;
1166
1402
  const hasSlottedButtons = this.el.querySelector('[slot="buttons"]') !== null;
1167
1403
  if (!hasSlottedButtons && !showDefaultButtons && !showClearButton) {
@@ -1181,7 +1417,7 @@ export class Datetime {
1181
1417
  return (h("div", { class: "datetime-footer" }, h("div", { class: "datetime-buttons" }, h("div", { class: {
1182
1418
  ['datetime-action-buttons']: true,
1183
1419
  ['has-clear-button']: this.showClearButton,
1184
- } }, 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))))))));
1420
+ } }, 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))))))));
1185
1421
  }
1186
1422
  /**
1187
1423
  * Wheel picker render methods
@@ -1481,7 +1717,7 @@ export class Datetime {
1481
1717
  'calendar-month-disabled': !isWorkingMonth && swipeDisabled,
1482
1718
  } }, h("div", { class: "calendar-month-grid" }, getDaysOfMonth(month, year, this.firstDayOfWeek % 7, this.showAdjacentDays).map((dateObject, index) => {
1483
1719
  const { day, dayOfWeek, isAdjacentDay } = dateObject;
1484
- const { el, highlightedDates, isDateEnabled, multiple, showAdjacentDays } = this;
1720
+ const { el, highlightedDates, isDateEnabled, showAdjacentDays } = this;
1485
1721
  let _month = month;
1486
1722
  let _year = year;
1487
1723
  if (showAdjacentDays && isAdjacentDay && day !== null) {
@@ -1510,7 +1746,7 @@ export class Datetime {
1510
1746
  }
1511
1747
  const referenceParts = { month: _month, day, year: _year, isAdjacentDay };
1512
1748
  const isCalendarPadding = day === null;
1513
- const { isActive, isToday, ariaLabel, ariaSelected, disabled: isDayDisabled, text, } = getCalendarDayState(this.locale, referenceParts, this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
1749
+ 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);
1514
1750
  const dateIsoString = convertDataToISO(referenceParts);
1515
1751
  let isCalDayDisabled = isCalMonthDisabled || isDayDisabled;
1516
1752
  if (!isCalDayDisabled && isDateEnabled !== undefined) {
@@ -1533,24 +1769,50 @@ export class Datetime {
1533
1769
  */
1534
1770
  const isCalDayConstrained = isCalDayDisabled && isDatetimeDisabled;
1535
1771
  const isButtonDisabled = isCalDayDisabled || isDatetimeDisabled;
1772
+ /**
1773
+ * Determine if this day is part of the range selection.
1774
+ * Range styling takes priority over highlightedDates.
1775
+ */
1776
+ const isPartOfRange = isRangeStart || isRangeEnd || isInRange;
1536
1777
  let dateStyle = undefined;
1537
1778
  /**
1538
- * Custom highlight styles should not override the style for selected dates,
1539
- * nor apply to "filler days" at the start of the grid.
1779
+ * Custom highlight styles should not override the style for selected dates
1780
+ * or range selection, nor apply to "filler days" at the start of the grid.
1540
1781
  */
1541
- if (highlightedDates !== undefined && !isActive && day !== null && !isAdjacentDay) {
1782
+ if (highlightedDates !== undefined && !isActive && !isPartOfRange && day !== null && !isAdjacentDay) {
1542
1783
  dateStyle = getHighlightStyles(highlightedDates, dateIsoString, el);
1543
1784
  }
1544
1785
  let dateParts = undefined;
1545
1786
  // "Filler days" at the beginning of the grid should not get the calendar day
1546
1787
  // CSS parts added to them
1547
1788
  if (!isCalendarPadding && !isAdjacentDay) {
1548
- dateParts = `calendar-day${isActive ? ' active' : ''}${isToday ? ' today' : ''}${isCalDayDisabled ? ' disabled' : ''}`;
1789
+ dateParts = `calendar-day${isActive || isRangeStart || isRangeEnd ? ' active' : ''}${isToday ? ' today' : ''}${isCalDayDisabled ? ' disabled' : ''}${isRangeStart ? ' range-start range-selection' : ''}${isRangeEnd ? ' range-end range-selection' : ''}${isInRange ? ' range-selection' : ''}`;
1549
1790
  }
1550
1791
  else if (isAdjacentDay) {
1551
1792
  dateParts = `calendar-day${isCalDayDisabled ? ' disabled' : ''}`;
1552
1793
  }
1553
- return (h("div", { class: "calendar-day-wrapper" }, h("button", {
1794
+ /**
1795
+ * Build the wrapper's shadow part attribute for range track styling.
1796
+ */
1797
+ const wrapperParts = isPartOfRange && !isCalendarPadding && !isAdjacentDay ? 'range-track' : undefined;
1798
+ /**
1799
+ * Build the aria-label for range mode accessibility.
1800
+ */
1801
+ let rangeAriaLabel = ariaLabel;
1802
+ if (this.isRangeMode && ariaLabel) {
1803
+ if (isRangeStart) {
1804
+ rangeAriaLabel = `${ariaLabel}, ${this.startDateLabel}`;
1805
+ }
1806
+ if (isRangeEnd) {
1807
+ rangeAriaLabel = `${ariaLabel}, ${this.endDateLabel}`;
1808
+ }
1809
+ }
1810
+ return (h("div", { class: {
1811
+ 'calendar-day-wrapper': true,
1812
+ 'calendar-day-wrapper-range-start': isRangeStart,
1813
+ 'calendar-day-wrapper-in-range': isInRange,
1814
+ 'calendar-day-wrapper-range-end': isRangeEnd,
1815
+ }, part: wrapperParts }, h("button", {
1554
1816
  // We need to use !important for the inline styles here because
1555
1817
  // otherwise the CSS shadow parts will override these styles.
1556
1818
  // See https://github.com/WICG/webcomponents/issues/847
@@ -1566,25 +1828,37 @@ export class Datetime {
1566
1828
  }, tabindex: "-1", "data-day": day, "data-month": _month, "data-year": _year, "data-index": index, "data-day-of-week": dayOfWeek, disabled: isButtonDisabled, class: {
1567
1829
  'calendar-day-padding': isCalendarPadding,
1568
1830
  'calendar-day': true,
1569
- 'calendar-day-active': isActive,
1831
+ 'calendar-day-active': isActive || isRangeStart || isRangeEnd,
1570
1832
  'calendar-day-constrained': isCalDayConstrained,
1571
1833
  'calendar-day-today': isToday,
1572
1834
  'calendar-day-adjacent-day': isAdjacentDay,
1573
- }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": ariaSelected, "aria-label": ariaLabel, onClick: () => {
1835
+ 'calendar-day-range-start': isRangeStart,
1836
+ 'calendar-day-range-end': isRangeEnd,
1837
+ 'calendar-day-in-range': isInRange,
1838
+ }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": isActive || isRangeStart || isRangeEnd ? 'true' : ariaSelected, "aria-label": rangeAriaLabel, onClick: () => {
1574
1839
  if (isCalendarPadding) {
1575
1840
  return;
1576
1841
  }
1577
1842
  if (isAdjacentDay) {
1578
1843
  // The user selected a day outside the current month. Ignore this button, as the month will be re-rendered.
1579
1844
  this.el.blur();
1580
- this.activeParts = Object.assign(Object.assign({}, activePart), referenceParts);
1845
+ if (!this.isRangeMode) {
1846
+ this.activeParts = Object.assign(Object.assign({}, activePart), referenceParts);
1847
+ }
1581
1848
  this.animateToDate(referenceParts);
1582
- this.confirm();
1849
+ if (this.isRangeMode) {
1850
+ this.setActiveParts(referenceParts);
1851
+ }
1852
+ else {
1853
+ this.confirm();
1854
+ }
1583
1855
  }
1584
1856
  else {
1585
1857
  this.setWorkingParts(Object.assign(Object.assign({}, this.workingParts), referenceParts));
1586
- // Multiple only needs date info so we can wipe out other fields like time.
1587
- if (multiple) {
1858
+ if (this.isRangeMode) {
1859
+ this.setActiveParts(referenceParts);
1860
+ }
1861
+ else if (this.isMultipleMode) {
1588
1862
  this.setActiveParts(referenceParts, isActive);
1589
1863
  }
1590
1864
  else {
@@ -1655,10 +1929,23 @@ export class Datetime {
1655
1929
  }
1656
1930
  getHeaderSelectedDateText() {
1657
1931
  var _a;
1658
- const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
1932
+ const { activeParts, formatOptions, titleSelectedDatesFormatter } = this;
1659
1933
  const isArray = Array.isArray(activeParts);
1934
+ const dateFormatOptions = (_a = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.date) !== null && _a !== void 0 ? _a : { weekday: 'short', month: 'short', day: 'numeric' };
1660
1935
  let headerText;
1661
- if (multiple && isArray && activeParts.length !== 1) {
1936
+ if (this.isRangeMode) {
1937
+ headerText = this.getRangeHeaderText(dateFormatOptions);
1938
+ if (titleSelectedDatesFormatter !== undefined) {
1939
+ try {
1940
+ const rangeValues = this.getRangeValueArray();
1941
+ headerText = titleSelectedDatesFormatter(rangeValues);
1942
+ }
1943
+ catch (e) {
1944
+ printIonError('[ion-datetime] - Exception in provided `titleSelectedDatesFormatter`:', e);
1945
+ }
1946
+ }
1947
+ }
1948
+ else if (this.isMultipleMode && isArray && activeParts.length !== 1) {
1662
1949
  headerText = `${activeParts.length} days`; // default/fallback for multiple selection
1663
1950
  if (titleSelectedDatesFormatter !== undefined) {
1664
1951
  try {
@@ -1671,10 +1958,35 @@ export class Datetime {
1671
1958
  }
1672
1959
  else {
1673
1960
  // for exactly 1 day selected (multiple set or not), show a formatted version of that
1674
- headerText = getLocalizedDateTime(this.locale, this.getActivePartsWithFallback(), (_a = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.date) !== null && _a !== void 0 ? _a : { weekday: 'short', month: 'short', day: 'numeric' });
1961
+ headerText = getLocalizedDateTime(this.locale, this.getActivePartsWithFallback(), dateFormatOptions);
1675
1962
  }
1676
1963
  return headerText;
1677
1964
  }
1965
+ /**
1966
+ * Returns the formatted range header text.
1967
+ * Shows "Start date - End date" with placeholders from startDateLabel/endDateLabel.
1968
+ */
1969
+ getRangeHeaderText(dateFormatOptions) {
1970
+ const range = this.rangeParts;
1971
+ if (!range) {
1972
+ return `${this.startDateLabel} - ${this.endDateLabel}`;
1973
+ }
1974
+ const startText = getLocalizedDateTime(this.locale, range.start, dateFormatOptions);
1975
+ const endText = range.end ? getLocalizedDateTime(this.locale, range.end, dateFormatOptions) : this.endDateLabel;
1976
+ return `${startText} - ${endText}`;
1977
+ }
1978
+ /**
1979
+ * Returns the current range value as an ISO string array for
1980
+ * titleSelectedDatesFormatter and ionChange.
1981
+ */
1982
+ getRangeValueArray() {
1983
+ const range = this.rangeParts;
1984
+ if (!range)
1985
+ return [];
1986
+ if (!range.end)
1987
+ return [convertDataToISO(range.start)];
1988
+ return [convertDataToISO(range.start), convertDataToISO(range.end)];
1989
+ }
1678
1990
  renderHeader(showExpandedHeader = true) {
1679
1991
  const hasSlottedTitle = this.el.querySelector('[slot="title"]') !== null;
1680
1992
  if (!hasSlottedTitle && !this.showDefaultTitle) {
@@ -1832,7 +2144,7 @@ export class Datetime {
1832
2144
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1833
2145
  const hasWheelVariant = hasDatePresentation && preferWheel;
1834
2146
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1835
- return (h(Host, { key: '04fb1740b1d722d20cdbb32b19d9a3b7d3334db0', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
2147
+ return (h(Host, { key: '8ec46f2a5bc855c3d6210f72305b66655ad59b93', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1836
2148
  [theme]: true,
1837
2149
  ['datetime-readonly']: readonly,
1838
2150
  ['datetime-disabled']: disabled,
@@ -1842,7 +2154,7 @@ export class Datetime {
1842
2154
  [`datetime-size-${size}`]: true,
1843
2155
  [`datetime-prefer-wheel`]: hasWheelVariant,
1844
2156
  [`datetime-grid`]: isGridStyle,
1845
- })) }, h("div", { key: '0cbb85b4c71e521c81b37a98da0123a1c3a15fac', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
2157
+ })) }, h("div", { key: 'b7bf9b1e681b8d1a5a24a27c758a528c03a203fc', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
1846
2158
  }
1847
2159
  static get is() { return "ion-datetime"; }
1848
2160
  static get encapsulation() { return "shadow"; }
@@ -2305,7 +2617,10 @@ export class Datetime {
2305
2617
  "required": false,
2306
2618
  "optional": false,
2307
2619
  "docs": {
2308
- "tags": [],
2620
+ "tags": [{
2621
+ "name": "deprecated",
2622
+ "text": "Use `selectionMode=\"multiple\"` instead."
2623
+ }],
2309
2624
  "text": "If `true`, multiple dates can be selected at once. Only\napplies to `presentation=\"date\"` and `preferWheel=\"false\"`."
2310
2625
  },
2311
2626
  "getter": false,
@@ -2314,6 +2629,65 @@ export class Datetime {
2314
2629
  "attribute": "multiple",
2315
2630
  "defaultValue": "false"
2316
2631
  },
2632
+ "selectionMode": {
2633
+ "type": "string",
2634
+ "mutable": false,
2635
+ "complexType": {
2636
+ "original": "'multiple' | 'range'",
2637
+ "resolved": "\"multiple\" | \"range\" | undefined",
2638
+ "references": {}
2639
+ },
2640
+ "required": false,
2641
+ "optional": true,
2642
+ "docs": {
2643
+ "tags": [],
2644
+ "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\"`."
2645
+ },
2646
+ "getter": false,
2647
+ "setter": false,
2648
+ "reflect": false,
2649
+ "attribute": "selection-mode"
2650
+ },
2651
+ "startDateLabel": {
2652
+ "type": "string",
2653
+ "mutable": false,
2654
+ "complexType": {
2655
+ "original": "string",
2656
+ "resolved": "string",
2657
+ "references": {}
2658
+ },
2659
+ "required": false,
2660
+ "optional": false,
2661
+ "docs": {
2662
+ "tags": [],
2663
+ "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\"`."
2664
+ },
2665
+ "getter": false,
2666
+ "setter": false,
2667
+ "reflect": false,
2668
+ "attribute": "start-date-label",
2669
+ "defaultValue": "'Start date'"
2670
+ },
2671
+ "endDateLabel": {
2672
+ "type": "string",
2673
+ "mutable": false,
2674
+ "complexType": {
2675
+ "original": "string",
2676
+ "resolved": "string",
2677
+ "references": {}
2678
+ },
2679
+ "required": false,
2680
+ "optional": false,
2681
+ "docs": {
2682
+ "tags": [],
2683
+ "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\"`."
2684
+ },
2685
+ "getter": false,
2686
+ "setter": false,
2687
+ "reflect": false,
2688
+ "attribute": "end-date-label",
2689
+ "defaultValue": "'End date'"
2690
+ },
2317
2691
  "highlightedDates": {
2318
2692
  "type": "unknown",
2319
2693
  "mutable": false,
@@ -2672,6 +3046,12 @@ export class Datetime {
2672
3046
  "Promise": {
2673
3047
  "location": "global",
2674
3048
  "id": "global::Promise"
3049
+ },
3050
+ "DatetimeParts": {
3051
+ "location": "import",
3052
+ "path": "./datetime-interface",
3053
+ "id": "src/components/datetime/datetime-interface.ts::DatetimeParts",
3054
+ "referenceLocation": "DatetimeParts"
2675
3055
  }
2676
3056
  },
2677
3057
  "return": "Promise<void>"