@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.
- package/components/ion-accordion.js +1 -1
- package/components/ion-datetime-button.js +1 -1
- package/components/ion-datetime.js +1 -1
- package/components/{p-BCQSEpww.js → p-B7ExaLVL.js} +1 -1
- package/dist/cjs/ion-accordion_2.cjs.entry.js +1 -1
- package/dist/cjs/ion-datetime-button.cjs.entry.js +33 -6
- package/dist/cjs/ion-datetime.cjs.entry.js +339 -37
- package/dist/cjs/ionic.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/accordion/accordion.ionic.css +11 -3
- package/dist/collection/components/datetime/datetime.ionic.css +211 -5
- package/dist/collection/components/datetime/datetime.ios.css +164 -0
- package/dist/collection/components/datetime/datetime.js +394 -36
- package/dist/collection/components/datetime/datetime.md.css +152 -0
- package/dist/collection/components/datetime/utils/state.js +18 -1
- package/dist/collection/components/datetime-button/datetime-button.js +34 -6
- package/dist/docs.json +153 -3
- package/dist/esm/{format-NT7iHsbA.js → format-DUHrIiJq.js} +1 -1
- package/dist/esm/ion-accordion_2.entry.js +1 -1
- package/dist/esm/ion-datetime-button.entry.js +34 -7
- package/dist/esm/ion-datetime.entry.js +340 -38
- package/dist/esm/ionic.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/html.html-data.json +21 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/{p-8e2bd5e6.entry.js → p-8b107444.entry.js} +1 -1
- package/dist/ionic/{p-BtccSO5J.js → p-CaOPF8Xt.js} +1 -1
- package/dist/ionic/p-ceca04fb.entry.js +4 -0
- package/dist/ionic/p-ec99dc1f.entry.js +4 -0
- package/dist/types/components/datetime/datetime-interface.d.ts +4 -0
- package/dist/types/components/datetime/datetime.d.ts +73 -6
- package/dist/types/components/datetime/utils/state.d.ts +5 -2
- package/dist/types/components.d.ts +37 -4
- package/hydrate/index.js +381 -44
- package/hydrate/index.mjs +381 -44
- package/package.json +1 -1
- package/dist/ionic/p-0743a26a.entry.js +0 -4
- 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 {
|
|
188
|
-
if (!
|
|
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
|
|
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 {
|
|
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 (
|
|
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:
|
|
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
|
|
1428
|
-
if (!this.
|
|
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
|
|
1450
|
-
if (!this.
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
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,
|
|
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 (
|
|
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(),
|
|
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: '
|
|
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: '
|
|
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.
|
|
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.\
|
|
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>"
|