@ionic/core 9.0.11-dev.11793888057.13a7fa02 → 9.0.11-dev.11793890855.1bf6a6e5

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 (38) hide show
  1. package/components/ion-accordion.js +1 -1
  2. package/components/ion-datetime-button.js +1 -1
  3. package/components/ion-datetime.js +1 -1
  4. package/components/{p-BCQSEpww.js → p-B7ExaLVL.js} +1 -1
  5. package/dist/cjs/ion-accordion_2.cjs.entry.js +1 -1
  6. package/dist/cjs/ion-datetime-button.cjs.entry.js +33 -6
  7. package/dist/cjs/ion-datetime.cjs.entry.js +339 -37
  8. package/dist/cjs/ionic.cjs.js +1 -1
  9. package/dist/cjs/loader.cjs.js +1 -1
  10. package/dist/collection/components/accordion/accordion.ionic.css +11 -3
  11. package/dist/collection/components/datetime/datetime.ionic.css +211 -5
  12. package/dist/collection/components/datetime/datetime.ios.css +164 -0
  13. package/dist/collection/components/datetime/datetime.js +394 -36
  14. package/dist/collection/components/datetime/datetime.md.css +152 -0
  15. package/dist/collection/components/datetime/utils/state.js +18 -1
  16. package/dist/collection/components/datetime-button/datetime-button.js +34 -6
  17. package/dist/docs.json +153 -3
  18. package/dist/esm/{format-NT7iHsbA.js → format-DUHrIiJq.js} +1 -1
  19. package/dist/esm/ion-accordion_2.entry.js +1 -1
  20. package/dist/esm/ion-datetime-button.entry.js +34 -7
  21. package/dist/esm/ion-datetime.entry.js +340 -38
  22. package/dist/esm/ionic.js +1 -1
  23. package/dist/esm/loader.js +1 -1
  24. package/dist/html.html-data.json +21 -1
  25. package/dist/ionic/ionic.esm.js +1 -1
  26. package/dist/ionic/{p-8e2bd5e6.entry.js → p-8b107444.entry.js} +1 -1
  27. package/dist/ionic/{p-BtccSO5J.js → p-CaOPF8Xt.js} +1 -1
  28. package/dist/ionic/p-ceca04fb.entry.js +4 -0
  29. package/dist/ionic/p-ec99dc1f.entry.js +4 -0
  30. package/dist/types/components/datetime/datetime-interface.d.ts +4 -0
  31. package/dist/types/components/datetime/datetime.d.ts +73 -6
  32. package/dist/types/components/datetime/utils/state.d.ts +5 -2
  33. package/dist/types/components.d.ts +37 -4
  34. package/hydrate/index.js +381 -44
  35. package/hydrate/index.mjs +381 -44
  36. package/package.json +1 -1
  37. package/dist/ionic/p-0743a26a.entry.js +0 -4
  38. package/dist/ionic/p-ad46d2fd.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.
@@ -130,8 +135,22 @@ export class Datetime {
130
135
  /**
131
136
  * If `true`, multiple dates can be selected at once. Only
132
137
  * applies to `presentation="date"` and `preferWheel="false"`.
138
+ *
139
+ * @deprecated Use `selectionMode="multiple"` instead.
133
140
  */
134
141
  this.multiple = false;
142
+ /**
143
+ * The text shown in the datetime header when the start of the range
144
+ * is empty, also used by screen readers to announce the start date.
145
+ * Only applies when `selectionMode="range"`.
146
+ */
147
+ this.startDateLabel = 'Start date';
148
+ /**
149
+ * The text shown in the datetime header when the end of the range
150
+ * is empty, also used by screen readers to announce the end date.
151
+ * Only applies when `selectionMode="range"`.
152
+ */
153
+ this.endDateLabel = 'End date';
135
154
  /**
136
155
  * If `true`, a header will be shown above the calendar
137
156
  * picker. This will include both the slotted title, and
@@ -184,8 +203,8 @@ export class Datetime {
184
203
  */
185
204
  this.preferWheel = false;
186
205
  this.warnIfIncorrectValueUsage = () => {
187
- const { multiple, value } = this;
188
- if (!multiple && Array.isArray(value)) {
206
+ const { value } = this;
207
+ if (!this.isMultipleMode && !this.isRangeMode && Array.isArray(value)) {
189
208
  /**
190
209
  * We do some processing on the `value` array so
191
210
  * that it looks more like an array when logged to
@@ -194,7 +213,13 @@ export class Datetime {
194
213
  * Default toString() behavior: a,b
195
214
  * Custom behavior: ['a', 'b']
196
215
  */
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".
216
+ 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.
217
+
218
+ Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
219
+ `, this.el);
220
+ }
221
+ if (this.isRangeMode && Array.isArray(value) && value.length > 2) {
222
+ printIonWarning(`[ion-datetime] - More than 2 values were passed with selectionMode="range". Only the first and last values will be used.
198
223
 
199
224
  Value Passed: [${value.map((v) => `'${v}'`).join(', ')}]
200
225
  `, this.el);
@@ -220,6 +245,9 @@ export class Datetime {
220
245
  };
221
246
  this.getActivePart = () => {
222
247
  const { activeParts } = this;
248
+ if (this.isRangeParts(activeParts)) {
249
+ return activeParts.start;
250
+ }
223
251
  return Array.isArray(activeParts) ? activeParts[0] : activeParts;
224
252
  };
225
253
  this.closeParentOverlay = (role) => {
@@ -234,6 +262,33 @@ export class Datetime {
234
262
  ...parts,
235
263
  };
236
264
  };
265
+ /**
266
+ * Checks whether the span from start to end contains any disabled day.
267
+ * Used by range mode to prevent selecting across disabled dates.
268
+ */
269
+ this.rangeContainsDisabledDate = (start, end) => {
270
+ const { minParts, maxParts, parsedDayValues, parsedMonthValues, parsedYearValues, isDateEnabled } = this;
271
+ let cursor = getNextDay(start);
272
+ while (isBefore(cursor, end)) {
273
+ if (isDayDisabled(cursor, minParts, maxParts, parsedDayValues) ||
274
+ (parsedMonthValues !== undefined && !parsedMonthValues.includes(cursor.month)) ||
275
+ (parsedYearValues !== undefined && !parsedYearValues.includes(cursor.year))) {
276
+ return true;
277
+ }
278
+ if (isDateEnabled) {
279
+ try {
280
+ if (!isDateEnabled(convertDataToISO(cursor))) {
281
+ return true;
282
+ }
283
+ }
284
+ catch {
285
+ // Swallow user callback exceptions during validation
286
+ }
287
+ }
288
+ cursor = getNextDay(cursor);
289
+ }
290
+ return false;
291
+ };
237
292
  this.setActiveParts = (parts, removeDate = false) => {
238
293
  /** if the datetime component is in readonly mode,
239
294
  * allow browsing of the calendar without changing
@@ -243,7 +298,7 @@ export class Datetime {
243
298
  return;
244
299
  }
245
300
  this.hasUnconfirmedSelection = true;
246
- const { multiple, minParts, maxParts, activeParts } = this;
301
+ const { minParts, maxParts, activeParts } = this;
247
302
  /**
248
303
  * When setting the active parts, it is possible
249
304
  * to set invalid data. For example,
@@ -256,7 +311,10 @@ export class Datetime {
256
311
  */
257
312
  const validatedParts = validateParts(parts, minParts, maxParts);
258
313
  this.setWorkingParts(validatedParts);
259
- if (multiple) {
314
+ if (this.isRangeMode) {
315
+ this.setActivePartsRange(validatedParts);
316
+ }
317
+ else if (this.isMultipleMode) {
260
318
  const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
261
319
  if (removeDate) {
262
320
  this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
@@ -276,6 +334,25 @@ export class Datetime {
276
334
  }
277
335
  this.confirm();
278
336
  };
337
+ /**
338
+ * Handles range selection logic when a day is clicked.
339
+ *
340
+ * Creating a new range: first click sets start, second sets end.
341
+ * Modifying an existing range:
342
+ * - Before range → new start
343
+ * - Within range → new end
344
+ * - After range → new end
345
+ * If the resulting span contains a disabled date, start a new range instead.
346
+ */
347
+ this.setActivePartsRange = (parts) => {
348
+ const current = this.rangeParts;
349
+ if (!current || isBefore(parts, current.start) || this.rangeContainsDisabledDate(current.start, parts)) {
350
+ this.activeParts = { start: parts };
351
+ }
352
+ else {
353
+ this.activeParts = { start: current.start, end: parts };
354
+ }
355
+ };
279
356
  this.initializeKeyboardListeners = () => {
280
357
  const calendarBodyRef = this.calendarBodyRef;
281
358
  if (!calendarBodyRef) {
@@ -687,6 +764,57 @@ export class Datetime {
687
764
  if (hasValue) {
688
765
  warnIfValueOutOfBounds(valueToProcess, minParts, maxParts);
689
766
  }
767
+ /**
768
+ * Range mode: convert the value array into DatetimeRangeParts.
769
+ */
770
+ if (this.isRangeMode) {
771
+ /**
772
+ * The calendar navigates to the start of the range, or to the default
773
+ * date when there is nothing selected, so that an empty range mode
774
+ * opens on the same month as the other selection modes.
775
+ */
776
+ let navigateTo;
777
+ if (hasValue && Array.isArray(valueToProcess)) {
778
+ let rangeValues = valueToProcess;
779
+ // If more than 2 entries, use first and last
780
+ if (rangeValues.length > 2) {
781
+ rangeValues = [rangeValues[0], rangeValues[rangeValues.length - 1]];
782
+ }
783
+ let startParts = rangeValues[0];
784
+ let endParts = rangeValues.length > 1 ? rangeValues[1] : undefined;
785
+ // Normalize reversed pairs so earlier date is start
786
+ if (endParts && isAfter(startParts, endParts)) {
787
+ const temp = startParts;
788
+ startParts = endParts;
789
+ endParts = temp;
790
+ }
791
+ this.activeParts = endParts ? { start: startParts, end: endParts } : { start: startParts };
792
+ /**
793
+ * Stay on the end date's month when it is already showing, such as
794
+ * right after the user picks it, instead of jumping back to the start.
795
+ */
796
+ navigateTo = startParts;
797
+ if (endParts && endParts.month === workingParts.month && endParts.year === workingParts.year) {
798
+ navigateTo = endParts;
799
+ }
800
+ }
801
+ else {
802
+ this.activeParts = [];
803
+ navigateTo = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess;
804
+ }
805
+ const targetValue = clampDate(navigateTo, minParts, maxParts);
806
+ const { month, day, year } = targetValue;
807
+ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
808
+ const bodyIsVisible = el.classList.contains('datetime-ready');
809
+ const { isGridStyle, showMonthAndYear } = this;
810
+ if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
811
+ this.animateToDate(targetValue);
812
+ }
813
+ else {
814
+ this.setWorkingParts({ month, day, year, hour: 0, minute: 0, ampm: 'am' });
815
+ }
816
+ return;
817
+ }
690
818
  /**
691
819
  * If there are multiple values, clamp to the last one.
692
820
  * This is because the last value is the one that the user
@@ -848,6 +976,27 @@ export class Datetime {
848
976
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
849
977
  return hasDatePresentation && !preferWheel;
850
978
  }
979
+ get isMultipleMode() {
980
+ return this.selectionMode === 'multiple' || (this.selectionMode === undefined && this.multiple);
981
+ }
982
+ get isRangeMode() {
983
+ return this.selectionMode === 'range';
984
+ }
985
+ /**
986
+ * Type guard: returns true if the given activeParts is a DatetimeRangeParts object.
987
+ */
988
+ isRangeParts(parts) {
989
+ return this.isRangeMode && parts !== null && typeof parts === 'object' && !Array.isArray(parts) && 'start' in parts;
990
+ }
991
+ /**
992
+ * Returns the current range parts if in range mode, or undefined otherwise.
993
+ */
994
+ get rangeParts() {
995
+ if (this.isRangeParts(this.activeParts)) {
996
+ return this.activeParts;
997
+ }
998
+ return undefined;
999
+ }
851
1000
  yearValuesChanged() {
852
1001
  this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
853
1002
  }
@@ -883,6 +1032,19 @@ export class Datetime {
883
1032
  */
884
1033
  async confirm(closeOverlay = false) {
885
1034
  const { isCalendarPicker, activeParts, preferWheel, workingParts } = this;
1035
+ /**
1036
+ * In range mode, only confirm when both start and end are selected.
1037
+ */
1038
+ if (this.isRangeMode) {
1039
+ if (!this.isRangeParts(activeParts) || !activeParts.end) {
1040
+ return;
1041
+ }
1042
+ this.setValue([convertDataToISO(activeParts.start), convertDataToISO(activeParts.end)]);
1043
+ if (closeOverlay) {
1044
+ this.closeParentOverlay(CONFIRM_ROLE);
1045
+ }
1046
+ return;
1047
+ }
886
1048
  /**
887
1049
  * We only update the value if the presentation is not a calendar picker.
888
1050
  */
@@ -918,6 +1080,23 @@ export class Datetime {
918
1080
  * @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
1081
  */
920
1082
  async reset(startDate) {
1083
+ if (this.isRangeMode) {
1084
+ /**
1085
+ * In range mode, reset clears the full range. If a startDate
1086
+ * is provided, the calendar navigates to that month without
1087
+ * selecting anything so the user begins a fresh selection.
1088
+ */
1089
+ this.activeParts = [];
1090
+ this.hasUnconfirmedSelection = false;
1091
+ if (startDate) {
1092
+ const parsed = parseDate(startDate);
1093
+ if (parsed) {
1094
+ const target = Array.isArray(parsed) ? parsed[0] : parsed;
1095
+ this.setWorkingParts(clampDate(target, this.minParts, this.maxParts));
1096
+ }
1097
+ }
1098
+ return;
1099
+ }
921
1100
  this.processValue(startDate);
922
1101
  }
923
1102
  /**
@@ -1153,8 +1332,11 @@ export class Datetime {
1153
1332
  });
1154
1333
  }
1155
1334
  componentWillLoad() {
1156
- const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
1157
- if (multiple) {
1335
+ const { el, formatOptions, highlightedDates, multiple, selectionMode, presentation, preferWheel } = this;
1336
+ if (selectionMode === undefined && multiple) {
1337
+ printIonWarning('[ion-datetime] - The "multiple" property is deprecated. Use selectionMode="multiple" instead.', el);
1338
+ }
1339
+ if (this.isMultipleMode) {
1158
1340
  if (presentation !== 'date') {
1159
1341
  printIonWarning('[ion-datetime] - Multiple date selection is only supported for presentation="date".', el);
1160
1342
  }
@@ -1162,6 +1344,17 @@ export class Datetime {
1162
1344
  printIonWarning('[ion-datetime] - Multiple date selection is not supported with preferWheel="true".', el);
1163
1345
  }
1164
1346
  }
1347
+ if (this.isRangeMode) {
1348
+ if (presentation !== 'date') {
1349
+ printIonWarning('[ion-datetime] - Range selection is only supported for presentation="date".', el);
1350
+ }
1351
+ if (preferWheel) {
1352
+ printIonWarning('[ion-datetime] - Range selection is not supported with preferWheel="true".', el);
1353
+ }
1354
+ }
1355
+ if (selectionMode !== undefined && multiple) {
1356
+ printIonWarning('[ion-datetime] - Both "selectionMode" and "multiple" are set. "selectionMode" takes precedence.', el);
1357
+ }
1165
1358
  if (highlightedDates !== undefined) {
1166
1359
  if (presentation !== 'date' && presentation !== 'date-time' && presentation !== 'time-date') {
1167
1360
  printIonWarning('[ion-datetime] - The highlightedDates property is only supported with the date, date-time, and time-date presentations.', el);
@@ -1216,6 +1409,12 @@ export class Datetime {
1216
1409
  * is disabled or readonly.
1217
1410
  */
1218
1411
  const isButtonDisabled = disabled || readonly;
1412
+ /**
1413
+ * In range mode, the confirm button should be disabled until
1414
+ * both a start and end date have been selected.
1415
+ */
1416
+ const isRangeIncomplete = this.isRangeMode && (!this.isRangeParts(this.activeParts) || !this.activeParts.end);
1417
+ const isConfirmDisabled = isButtonDisabled || isRangeIncomplete;
1219
1418
  const confirmFill = theme === 'ionic' ? 'solid' : undefined;
1220
1419
  const hasSlottedButtons = this.el.querySelector('[slot="buttons"]') !== null;
1221
1420
  if (!hasSlottedButtons && !showDefaultButtons && !showClearButton) {
@@ -1235,7 +1434,7 @@ export class Datetime {
1235
1434
  return (h("div", { class: "datetime-footer" }, h("div", { class: "datetime-buttons" }, h("div", { class: {
1236
1435
  ['datetime-action-buttons']: true,
1237
1436
  ['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))))))));
1437
+ } }, 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
1438
  }
1240
1439
  /**
1241
1440
  * Wheel picker render methods
@@ -1424,8 +1623,8 @@ export class Datetime {
1424
1623
  ...this.workingParts,
1425
1624
  month: ev.detail.value,
1426
1625
  });
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) {
1626
+ // Month wheel is navigation-only in multi-select and range modes as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1627
+ if (!this.isMultipleMode && !this.isRangeMode) {
1429
1628
  this.setActiveParts({
1430
1629
  ...activePart,
1431
1630
  month: ev.detail.value,
@@ -1446,8 +1645,8 @@ export class Datetime {
1446
1645
  ...this.workingParts,
1447
1646
  year: ev.detail.value,
1448
1647
  });
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) {
1648
+ // Year wheel is navigation-only in multi-select and range modes as a fix for https://github.com/ionic-team/ionic-framework/issues/29673
1649
+ if (!this.isMultipleMode && !this.isRangeMode) {
1451
1650
  this.setActiveParts({
1452
1651
  ...activePart,
1453
1652
  year: ev.detail.value,
@@ -1591,7 +1790,7 @@ export class Datetime {
1591
1790
  'calendar-month-disabled': !isWorkingMonth && swipeDisabled,
1592
1791
  } }, h("div", { class: "calendar-month-grid" }, getDaysOfMonth(month, year, this.firstDayOfWeek % 7, this.showAdjacentDays).map((dateObject, index) => {
1593
1792
  const { day, dayOfWeek, isAdjacentDay } = dateObject;
1594
- const { el, highlightedDates, isDateEnabled, multiple, showAdjacentDays } = this;
1793
+ const { el, highlightedDates, isDateEnabled, showAdjacentDays } = this;
1595
1794
  let _month = month;
1596
1795
  let _year = year;
1597
1796
  if (showAdjacentDays && isAdjacentDay && day !== null) {
@@ -1620,7 +1819,7 @@ export class Datetime {
1620
1819
  }
1621
1820
  const referenceParts = { month: _month, day, year: _year, isAdjacentDay };
1622
1821
  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);
1822
+ 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
1823
  const dateIsoString = convertDataToISO(referenceParts);
1625
1824
  let isCalDayDisabled = isCalMonthDisabled || isDayDisabled;
1626
1825
  if (!isCalDayDisabled && isDateEnabled !== undefined) {
@@ -1643,24 +1842,57 @@ export class Datetime {
1643
1842
  */
1644
1843
  const isCalDayConstrained = isCalDayDisabled && isDatetimeDisabled;
1645
1844
  const isButtonDisabled = isCalDayDisabled || isDatetimeDisabled;
1845
+ /**
1846
+ * Determine if this day is part of the range selection.
1847
+ * Range styling takes priority over highlightedDates.
1848
+ */
1849
+ const isPartOfRange = isRangeStart || isRangeEnd || isInRange;
1646
1850
  let dateStyle = undefined;
1647
1851
  /**
1648
- * Custom highlight styles should not override the style for selected dates,
1649
- * nor apply to "filler days" at the start of the grid.
1852
+ * Custom highlight styles should not override the style for selected dates
1853
+ * or range selection, nor apply to "filler days" at the start of the grid.
1650
1854
  */
1651
- if (highlightedDates !== undefined && !isActive && day !== null && !isAdjacentDay) {
1855
+ if (highlightedDates !== undefined && !isActive && !isPartOfRange && day !== null && !isAdjacentDay) {
1652
1856
  dateStyle = getHighlightStyles(highlightedDates, dateIsoString, el);
1653
1857
  }
1654
1858
  let dateParts = undefined;
1655
1859
  // "Filler days" at the beginning of the grid should not get the calendar day
1656
1860
  // CSS parts added to them
1657
1861
  if (!isCalendarPadding && !isAdjacentDay) {
1658
- dateParts = `calendar-day${isActive ? ' active' : ''}${isToday ? ' today' : ''}${isCalDayDisabled ? ' disabled' : ''}`;
1862
+ 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
1863
  }
1660
1864
  else if (isAdjacentDay) {
1661
- dateParts = `calendar-day${isCalDayDisabled ? ' disabled' : ''}`;
1865
+ /**
1866
+ * Adjacent days skip 'active' and 'today' parts but still
1867
+ * include range parts so ::part() styling is consistent
1868
+ * with the visual range track rendering.
1869
+ */
1870
+ dateParts = `calendar-day${isCalDayDisabled ? ' disabled' : ''}${isRangeStart ? ' range-start range-selection' : ''}${isRangeEnd ? ' range-end range-selection' : ''}${isInRange ? ' range-selection' : ''}`;
1871
+ }
1872
+ /**
1873
+ * Build the wrapper's shadow part attribute for range track styling.
1874
+ */
1875
+ const hasCompleteRange = this.rangeParts?.end !== undefined;
1876
+ const wrapperParts = isPartOfRange && hasCompleteRange && !isCalendarPadding ? 'range-track' : undefined;
1877
+ /**
1878
+ * Build the aria-label for range mode accessibility.
1879
+ */
1880
+ let rangeAriaLabel = ariaLabel;
1881
+ if (this.isRangeMode && ariaLabel) {
1882
+ if (isRangeStart) {
1883
+ rangeAriaLabel += `, ${this.startDateLabel}`;
1884
+ }
1885
+ if (isRangeEnd) {
1886
+ rangeAriaLabel += `, ${this.endDateLabel}`;
1887
+ }
1662
1888
  }
1663
- return (h("div", { class: "calendar-day-wrapper" }, h("button", {
1889
+ return (h("div", { class: {
1890
+ 'calendar-day-wrapper': true,
1891
+ // A start without an end is only the circle until the range is complete
1892
+ 'calendar-day-wrapper-range-start': isRangeStart && this.rangeParts?.end !== undefined,
1893
+ 'calendar-day-wrapper-in-range': isInRange,
1894
+ 'calendar-day-wrapper-range-end': isRangeEnd,
1895
+ }, part: wrapperParts }, h("button", {
1664
1896
  // We need to use !important for the inline styles here because
1665
1897
  // otherwise the CSS shadow parts will override these styles.
1666
1898
  // See https://github.com/WICG/webcomponents/issues/847
@@ -1676,28 +1908,45 @@ export class Datetime {
1676
1908
  }, tabindex: "-1", "data-day": day, "data-month": _month, "data-year": _year, "data-index": index, "data-day-of-week": dayOfWeek, disabled: isButtonDisabled, class: {
1677
1909
  'calendar-day-padding': isCalendarPadding,
1678
1910
  'calendar-day': true,
1679
- 'calendar-day-active': isActive,
1911
+ 'calendar-day-active': isActive || isRangeStart || isRangeEnd,
1680
1912
  'calendar-day-constrained': isCalDayConstrained,
1681
1913
  'calendar-day-today': isToday,
1682
1914
  'calendar-day-adjacent-day': isAdjacentDay,
1683
- }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": ariaSelected, "aria-label": ariaLabel, onClick: () => {
1915
+ 'calendar-day-range-start': isRangeStart,
1916
+ 'calendar-day-range-end': isRangeEnd,
1917
+ 'calendar-day-in-range': isInRange,
1918
+ }, part: dateParts, "aria-hidden": isCalendarPadding ? 'true' : null, "aria-selected": isActive || isRangeStart || isRangeEnd ? 'true' : ariaSelected, "aria-label": rangeAriaLabel, onClick: () => {
1684
1919
  if (isCalendarPadding) {
1685
1920
  return;
1686
1921
  }
1687
1922
  if (isAdjacentDay) {
1688
1923
  // The user selected a day outside the current month. Ignore this button, as the month will be re-rendered.
1689
1924
  this.el.blur();
1690
- this.activeParts = { ...activePart, ...referenceParts };
1691
- this.animateToDate(referenceParts);
1692
- this.confirm();
1925
+ if (!this.isRangeMode) {
1926
+ this.activeParts = { ...activePart, ...referenceParts };
1927
+ }
1928
+ if (this.isRangeMode) {
1929
+ /**
1930
+ * Wait for the calendar to reach the adjacent month before
1931
+ * updating the range. setActiveParts also moves workingParts,
1932
+ * which would make the scroll overshoot by a month.
1933
+ */
1934
+ this.animateToDate(referenceParts).then(() => this.setActiveParts(referenceParts));
1935
+ }
1936
+ else {
1937
+ this.animateToDate(referenceParts);
1938
+ this.confirm();
1939
+ }
1693
1940
  }
1694
1941
  else {
1695
1942
  this.setWorkingParts({
1696
1943
  ...this.workingParts,
1697
1944
  ...referenceParts,
1698
1945
  });
1699
- // Multiple only needs date info so we can wipe out other fields like time.
1700
- if (multiple) {
1946
+ if (this.isRangeMode) {
1947
+ this.setActiveParts(referenceParts);
1948
+ }
1949
+ else if (this.isMultipleMode) {
1701
1950
  this.setActiveParts(referenceParts, isActive);
1702
1951
  }
1703
1952
  else {
@@ -1717,7 +1966,7 @@ export class Datetime {
1717
1966
  })));
1718
1967
  }
1719
1968
  renderCalendar(theme) {
1720
- return (h("div", { class: "datetime-calendar", key: "datetime-calendar" }, this.renderCalendarHeader(theme), this.renderCalendarBody()));
1969
+ return (h("div", { class: "datetime-calendar", key: "datetime-calendar" }, this.renderCalendarHeader(theme), this.renderCalendarBody(), this.isRangeMode && (h("div", { class: "datetime-range-announcement", "aria-live": "polite", role: "status" }, this.getHeaderSelectedDateText()))));
1721
1970
  }
1722
1971
  renderTimeLabel() {
1723
1972
  const hasSlottedTimeLabel = this.el.querySelector('[slot="time-label"]') !== null;
@@ -1770,10 +2019,23 @@ export class Datetime {
1770
2019
  ];
1771
2020
  }
1772
2021
  getHeaderSelectedDateText() {
1773
- const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
2022
+ const { activeParts, formatOptions, titleSelectedDatesFormatter } = this;
1774
2023
  const isArray = Array.isArray(activeParts);
2024
+ const dateFormatOptions = formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' };
1775
2025
  let headerText;
1776
- if (multiple && isArray && activeParts.length !== 1) {
2026
+ if (this.isRangeMode) {
2027
+ headerText = this.getRangeHeaderText(formatOptions?.date ?? { month: 'short', day: 'numeric' });
2028
+ if (titleSelectedDatesFormatter !== undefined) {
2029
+ try {
2030
+ const rangeValues = this.getRangeValueArray();
2031
+ headerText = titleSelectedDatesFormatter(rangeValues);
2032
+ }
2033
+ catch (e) {
2034
+ printIonError('[ion-datetime] - Exception in provided `titleSelectedDatesFormatter`:', e);
2035
+ }
2036
+ }
2037
+ }
2038
+ else if (this.isMultipleMode && isArray && activeParts.length !== 1) {
1777
2039
  headerText = `${activeParts.length} days`; // default/fallback for multiple selection
1778
2040
  if (titleSelectedDatesFormatter !== undefined) {
1779
2041
  try {
@@ -1786,10 +2048,37 @@ export class Datetime {
1786
2048
  }
1787
2049
  else {
1788
2050
  // 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' });
2051
+ headerText = getLocalizedDateTime(this.locale, this.getActivePartsWithFallback(), dateFormatOptions);
1790
2052
  }
1791
2053
  return headerText;
1792
2054
  }
2055
+ /**
2056
+ * Returns the formatted range header text.
2057
+ * Shows "Start date - End date" with placeholders from startDateLabel/endDateLabel.
2058
+ */
2059
+ getRangeHeaderText(dateFormatOptions) {
2060
+ const range = this.rangeParts;
2061
+ if (!range) {
2062
+ return `${this.startDateLabel} - ${this.endDateLabel}`;
2063
+ }
2064
+ const startText = getLocalizedDateTime(this.locale, range.start, dateFormatOptions);
2065
+ const endText = range.end ? getLocalizedDateTime(this.locale, range.end, dateFormatOptions) : this.endDateLabel;
2066
+ return `${startText} - ${endText}`;
2067
+ }
2068
+ /**
2069
+ * Returns the current range value as an ISO string array for
2070
+ * titleSelectedDatesFormatter and ionChange.
2071
+ */
2072
+ getRangeValueArray() {
2073
+ const range = this.rangeParts;
2074
+ if (!range) {
2075
+ return [];
2076
+ }
2077
+ if (!range.end) {
2078
+ return [convertDataToISO(range.start)];
2079
+ }
2080
+ return [convertDataToISO(range.start), convertDataToISO(range.end)];
2081
+ }
1793
2082
  renderHeader(showExpandedHeader = true) {
1794
2083
  const hasSlottedTitle = this.el.querySelector('[slot="title"]') !== null;
1795
2084
  if (!hasSlottedTitle && !this.showDefaultTitle) {
@@ -1913,7 +2202,7 @@ export class Datetime {
1913
2202
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1914
2203
  const hasWheelVariant = hasDatePresentation && preferWheel;
1915
2204
  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: {
2205
+ return (h(Host, { key: '2510c26c29afd1a34d6ee71a9ffa00b4a7dd716e', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: {
1917
2206
  ...createColorClasses(color, {
1918
2207
  [theme]: true,
1919
2208
  ['datetime-readonly']: readonly,
@@ -1924,8 +2213,9 @@ export class Datetime {
1924
2213
  [`datetime-size-${size}`]: true,
1925
2214
  [`datetime-prefer-wheel`]: hasWheelVariant,
1926
2215
  [`datetime-grid`]: isGridStyle,
2216
+ [`datetime-range`]: this.isRangeMode,
1927
2217
  }),
1928
- } }, h("div", { key: '531803dfb77fa3d694c90b6fc45e71e304876fa8', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
2218
+ } }, h("div", { key: 'b245de6585c7dc5ccfa6f0f8be8474206380acfe', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(theme)));
1929
2219
  }
1930
2220
  static get is() { return "ion-datetime"; }
1931
2221
  static get encapsulation() { return "shadow"; }
@@ -2371,7 +2661,7 @@ export class Datetime {
2371
2661
  "optional": true,
2372
2662
  "docs": {
2373
2663
  "tags": [],
2374
- "text": "A callback used to format the header text that shows how many\ndates are selected. Only used if there are 0 or more than 1\nselected (i.e. unused for exactly 1). By default, the header\ntext is set to \"numberOfDates days\".\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback."
2664
+ "text": "A callback used to format the header text that shows how many\ndates are selected. In multiple mode this is only used when\nthere are 0 or more than 1 selected. In range mode it is\nalways called with `[startDate]` or `[startDate, endDate]`.\nBy default, the header text is set to \"numberOfDates days\".\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback."
2375
2665
  },
2376
2666
  "getter": false,
2377
2667
  "setter": false
@@ -2387,7 +2677,10 @@ export class Datetime {
2387
2677
  "required": false,
2388
2678
  "optional": false,
2389
2679
  "docs": {
2390
- "tags": [],
2680
+ "tags": [{
2681
+ "name": "deprecated",
2682
+ "text": "Use `selectionMode=\"multiple\"` instead."
2683
+ }],
2391
2684
  "text": "If `true`, multiple dates can be selected at once. Only\napplies to `presentation=\"date\"` and `preferWheel=\"false\"`."
2392
2685
  },
2393
2686
  "getter": false,
@@ -2396,6 +2689,65 @@ export class Datetime {
2396
2689
  "attribute": "multiple",
2397
2690
  "defaultValue": "false"
2398
2691
  },
2692
+ "selectionMode": {
2693
+ "type": "string",
2694
+ "mutable": false,
2695
+ "complexType": {
2696
+ "original": "'multiple' | 'range'",
2697
+ "resolved": "\"multiple\" | \"range\" | undefined",
2698
+ "references": {}
2699
+ },
2700
+ "required": false,
2701
+ "optional": true,
2702
+ "docs": {
2703
+ "tags": [],
2704
+ "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\"`."
2705
+ },
2706
+ "getter": false,
2707
+ "setter": false,
2708
+ "reflect": false,
2709
+ "attribute": "selection-mode"
2710
+ },
2711
+ "startDateLabel": {
2712
+ "type": "string",
2713
+ "mutable": false,
2714
+ "complexType": {
2715
+ "original": "string",
2716
+ "resolved": "string",
2717
+ "references": {}
2718
+ },
2719
+ "required": false,
2720
+ "optional": false,
2721
+ "docs": {
2722
+ "tags": [],
2723
+ "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\"`."
2724
+ },
2725
+ "getter": false,
2726
+ "setter": false,
2727
+ "reflect": false,
2728
+ "attribute": "start-date-label",
2729
+ "defaultValue": "'Start date'"
2730
+ },
2731
+ "endDateLabel": {
2732
+ "type": "string",
2733
+ "mutable": false,
2734
+ "complexType": {
2735
+ "original": "string",
2736
+ "resolved": "string",
2737
+ "references": {}
2738
+ },
2739
+ "required": false,
2740
+ "optional": false,
2741
+ "docs": {
2742
+ "tags": [],
2743
+ "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\"`."
2744
+ },
2745
+ "getter": false,
2746
+ "setter": false,
2747
+ "reflect": false,
2748
+ "attribute": "end-date-label",
2749
+ "defaultValue": "'End date'"
2750
+ },
2399
2751
  "highlightedDates": {
2400
2752
  "type": "unknown",
2401
2753
  "mutable": false,
@@ -2438,7 +2790,7 @@ export class Datetime {
2438
2790
  "optional": true,
2439
2791
  "docs": {
2440
2792
  "tags": [],
2441
- "text": "The value of the datetime as a valid ISO 8601 datetime string.\nThis should be an array of strings only when `multiple=\"true\"`."
2793
+ "text": "The value of the datetime as a valid ISO 8601 datetime string.\nShould be an array of strings when using `multiple=\"true\"` or\n`selectionMode=\"range\"`. For range mode the array contains\n`[startDate, endDate]`, or `[startDate]` for a partial selection."
2442
2794
  },
2443
2795
  "getter": false,
2444
2796
  "setter": false,
@@ -2754,6 +3106,12 @@ export class Datetime {
2754
3106
  "Promise": {
2755
3107
  "location": "global",
2756
3108
  "id": "global::Promise"
3109
+ },
3110
+ "DatetimeParts": {
3111
+ "location": "import",
3112
+ "path": "./datetime-interface",
3113
+ "id": "src/components/datetime/datetime-interface.ts::DatetimeParts",
3114
+ "referenceLocation": "DatetimeParts"
2757
3115
  }
2758
3116
  },
2759
3117
  "return": "Promise<void>"