Skip to content

Commit 3745083

Browse files
authored
fix(datetime): account for allowed values when setting default date (#26093)
resolves #24722
1 parent c3882d8 commit 3745083

File tree

3 files changed

+220
-30
lines changed

3 files changed

+220
-30
lines changed

core/src/components/datetime/datetime.tsx

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { is24Hour, isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth }
3636
import {
3737
calculateHourFromAMPM,
3838
convertDataToISO,
39+
getClosestValidDate,
3940
getEndOfWeek,
4041
getNextDay,
4142
getNextMonth,
@@ -96,7 +97,8 @@ export class Datetime implements ComponentInterface {
9697

9798
private minParts?: any;
9899
private maxParts?: any;
99-
private todayParts = parseDate(getToday());
100+
private todayParts!: DatetimeParts;
101+
private defaultParts!: DatetimeParts;
100102

101103
private prevPresentation: string | null = null;
102104

@@ -559,13 +561,13 @@ export class Datetime implements ComponentInterface {
559561
* may not be set. This function works
560562
* by returning the first selected date in
561563
* "activePartsClone" and then falling back to
562-
* today's DatetimeParts if no active date is selected.
564+
* defaultParts if no active date is selected.
563565
*/
564-
private getDefaultPart = (): DatetimeParts => {
565-
const { activePartsClone, todayParts } = this;
566+
private getActivePartsWithFallback = () => {
567+
const { activePartsClone, defaultParts } = this;
566568

567569
const firstPart = Array.isArray(activePartsClone) ? activePartsClone[0] : activePartsClone;
568-
return firstPart ?? todayParts;
570+
return firstPart ?? defaultParts;
569571
};
570572

571573
private closeParentOverlay = () => {
@@ -780,24 +782,24 @@ export class Datetime implements ComponentInterface {
780782
};
781783

782784
private processMinParts = () => {
783-
const { min, todayParts } = this;
785+
const { min, defaultParts } = this;
784786
if (min === undefined) {
785787
this.minParts = undefined;
786788
return;
787789
}
788790

789-
this.minParts = parseMinParts(min, todayParts);
791+
this.minParts = parseMinParts(min, defaultParts);
790792
};
791793

792794
private processMaxParts = () => {
793-
const { max, todayParts } = this;
795+
const { max, defaultParts } = this;
794796

795797
if (max === undefined) {
796798
this.maxParts = undefined;
797799
return;
798800
}
799801

800-
this.maxParts = parseMaxParts(max, todayParts);
802+
this.maxParts = parseMaxParts(max, defaultParts);
801803
};
802804

803805
private initializeCalendarListener = () => {
@@ -1158,7 +1160,7 @@ export class Datetime implements ComponentInterface {
11581160
* TODO FW-2646 remove value !== ''
11591161
*/
11601162
const hasValue = value !== '' && value !== null && value !== undefined;
1161-
let valueToProcess = parseDate(hasValue ? value : getToday());
1163+
let valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
11621164

11631165
const { minParts, maxParts, multiple } = this;
11641166
if (!multiple && Array.isArray(value)) {
@@ -1236,12 +1238,16 @@ export class Datetime implements ComponentInterface {
12361238

12371239
this.processMinParts();
12381240
this.processMaxParts();
1241+
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
1242+
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
1243+
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
1244+
const yearValues = (this.parsedYearValues = convertToArrayOfNumbers(this.yearValues));
1245+
const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues));
1246+
1247+
const todayParts = (this.todayParts = parseDate(getToday()));
1248+
this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
12391249
this.processValue(this.value);
1240-
this.parsedHourValues = convertToArrayOfNumbers(this.hourValues);
1241-
this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
1242-
this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues);
1243-
this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
1244-
this.parsedDayValues = convertToArrayOfNumbers(this.dayValues);
1250+
12451251
this.emitStyle();
12461252
}
12471253

@@ -1396,9 +1402,9 @@ export class Datetime implements ComponentInterface {
13961402
}
13971403

13981404
private renderCombinedDatePickerColumn() {
1399-
const { workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
1405+
const { defaultParts, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
14001406

1401-
const activePart = this.getDefaultPart();
1407+
const activePart = this.getActivePartsWithFallback();
14021408

14031409
/**
14041410
* By default, generate a range of 3 months:
@@ -1464,12 +1470,12 @@ export class Datetime implements ComponentInterface {
14641470

14651471
/**
14661472
* If we have selected a day already, then default the column
1467-
* to that value. Otherwise, default it to today.
1473+
* to that value. Otherwise, set it to the default date.
14681474
*/
14691475
const todayString =
14701476
workingParts.day !== null
14711477
? `${workingParts.year}-${workingParts.month}-${workingParts.day}`
1472-
: `${todayParts.year}-${todayParts.month}-${todayParts.day}`;
1478+
: `${defaultParts.year}-${defaultParts.month}-${defaultParts.day}`;
14731479

14741480
return (
14751481
<ion-picker-column-internal
@@ -1555,7 +1561,7 @@ export class Datetime implements ComponentInterface {
15551561

15561562
const shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
15571563
const years = shouldRenderYears
1558-
? getYearColumnData(this.locale, this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
1564+
? getYearColumnData(this.locale, this.defaultParts, this.minParts, this.maxParts, this.parsedYearValues)
15591565
: [];
15601566

15611567
/**
@@ -1588,14 +1594,14 @@ export class Datetime implements ComponentInterface {
15881594

15891595
const { workingParts } = this;
15901596

1591-
const activePart = this.getDefaultPart();
1597+
const activePart = this.getActivePartsWithFallback();
15921598

15931599
return (
15941600
<ion-picker-column-internal
15951601
class="day-column"
15961602
color={this.color}
15971603
items={days}
1598-
value={(workingParts.day !== null ? workingParts.day : this.todayParts.day) ?? undefined}
1604+
value={(workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined}
15991605
onIonChange={(ev: CustomEvent) => {
16001606
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
16011607
// Due to a Safari 14 issue we need to destroy
@@ -1632,7 +1638,7 @@ export class Datetime implements ComponentInterface {
16321638

16331639
const { workingParts } = this;
16341640

1635-
const activePart = this.getDefaultPart();
1641+
const activePart = this.getActivePartsWithFallback();
16361642

16371643
return (
16381644
<ion-picker-column-internal
@@ -1675,7 +1681,7 @@ export class Datetime implements ComponentInterface {
16751681

16761682
const { workingParts } = this;
16771683

1678-
const activePart = this.getDefaultPart();
1684+
const activePart = this.getActivePartsWithFallback();
16791685

16801686
return (
16811687
<ion-picker-column-internal
@@ -1739,7 +1745,7 @@ export class Datetime implements ComponentInterface {
17391745
const { workingParts } = this;
17401746
if (hoursData.length === 0) return [];
17411747

1742-
const activePart = this.getDefaultPart();
1748+
const activePart = this.getActivePartsWithFallback();
17431749

17441750
return (
17451751
<ion-picker-column-internal
@@ -1767,7 +1773,7 @@ export class Datetime implements ComponentInterface {
17671773
const { workingParts } = this;
17681774
if (minutesData.length === 0) return [];
17691775

1770-
const activePart = this.getDefaultPart();
1776+
const activePart = this.getActivePartsWithFallback();
17711777

17721778
return (
17731779
<ion-picker-column-internal
@@ -1797,7 +1803,7 @@ export class Datetime implements ComponentInterface {
17971803
return [];
17981804
}
17991805

1800-
const activePart = this.getDefaultPart();
1806+
const activePart = this.getActivePartsWithFallback();
18011807
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
18021808

18031809
return (
@@ -1911,7 +1917,7 @@ export class Datetime implements ComponentInterface {
19111917
// can free-scroll the calendar.
19121918
const isWorkingMonth = this.workingParts.month === month && this.workingParts.year === year;
19131919

1914-
const activePart = this.getDefaultPart();
1920+
const activePart = this.getActivePartsWithFallback();
19151921

19161922
return (
19171923
<div
@@ -2041,7 +2047,7 @@ export class Datetime implements ComponentInterface {
20412047

20422048
private renderTimeOverlay() {
20432049
const use24Hour = is24Hour(this.locale, this.hourCycle);
2044-
const activePart = this.getDefaultPart();
2050+
const activePart = this.getActivePartsWithFallback();
20452051

20462052
return [
20472053
<div class="time-header">{this.renderTimeLabel()}</div>,
@@ -2122,7 +2128,7 @@ export class Datetime implements ComponentInterface {
21222128
}
21232129
} else {
21242130
// for exactly 1 day selected (multiple set or not), show a formatted version of that
2125-
headerText = getMonthAndDay(this.locale, this.getDefaultPart());
2131+
headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback());
21262132
}
21272133

21282134
return headerText;

core/src/components/datetime/test/values/datetime.e2e.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test';
22
import { test } from '@utils/test/playwright';
33

44
test.describe('datetime: values', () => {
5+
test.beforeEach(({ skip }) => {
6+
skip.rtl();
7+
skip.mode('md');
8+
});
59
test('should render correct days', async ({ page }) => {
610
await page.setContent(`
711
<ion-datetime locale="en-US" presentation="date" day-values="1,2,3"></ion-datetime>
@@ -49,6 +53,105 @@ test.describe('datetime: values', () => {
4953
const items = page.locator('ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)');
5054
await expect(items).toHaveText(['01', '02', '03']);
5155
});
56+
test('should adjust default parts for allowed hour and minute values', async ({ page }) => {
57+
/**
58+
* Mock today's date for testing.
59+
* Playwright does not support this natively
60+
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
61+
*/
62+
await page.setContent(`
63+
<ion-datetime presentation="time" locale="en-US" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
64+
65+
<script>
66+
const mockToday = '2022-10-10T16:22';
67+
Date = class extends Date {
68+
constructor(...args) {
69+
if (args.length === 0) {
70+
super(mockToday)
71+
} else {
72+
super(...args);
73+
}
74+
}
75+
}
76+
</script>
77+
`);
78+
79+
await page.waitForSelector('.datetime-ready');
80+
81+
const minuteItems = page.locator('ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)');
82+
await expect(minuteItems).toHaveText(['00', '15', '30', '45']);
83+
await expect(minuteItems.nth(1)).toHaveClass(/picker-item-active/);
84+
85+
const hourItems = page.locator('ion-picker-column-internal:nth-of-type(1) .picker-item:not(.picker-item-empty)');
86+
await expect(hourItems).toHaveText(['2']);
87+
await expect(hourItems.nth(0)).toHaveClass(/picker-item-active/);
88+
89+
/**
90+
* Since the allowed hour is 2AM, the time period
91+
* should switch from PM to AM.
92+
*/
93+
const ampmItems = page.locator('ion-picker-column-internal:nth-of-type(3) .picker-item:not(.picker-item-empty)');
94+
await expect(ampmItems).toHaveText(['AM', 'PM']);
95+
await expect(ampmItems.nth(0)).toHaveClass(/picker-item-active/);
96+
});
97+
test('should adjust default parts month for allowed month values', async ({ page }) => {
98+
/**
99+
* Mock today's date for testing.
100+
* Playwright does not support this natively
101+
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
102+
*/
103+
await page.setContent(`
104+
<ion-datetime prefer-wheel="true" presentation="date" locale="en-US" month-values="01" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
105+
106+
<script>
107+
const mockToday = '2022-10-10T16:22';
108+
Date = class extends Date {
109+
constructor(...args) {
110+
if (args.length === 0) {
111+
super(mockToday)
112+
} else {
113+
super(...args);
114+
}
115+
}
116+
}
117+
</script>
118+
`);
119+
120+
await page.waitForSelector('.datetime-ready');
121+
122+
const monthItems = page.locator('.month-column .picker-item:not(.picker-item-empty)');
123+
await expect(monthItems).toHaveText(['January']);
124+
await expect(monthItems.nth(0)).toHaveClass(/picker-item-active/);
125+
});
126+
test('today date highlight should persist even if disallowed from dayValues', async ({ page }) => {
127+
/**
128+
* Mock today's date for testing.
129+
* Playwright does not support this natively
130+
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
131+
*/
132+
await page.setContent(`
133+
<ion-datetime day-values="9" presentation="date" locale="en-US"></ion-datetime>
134+
135+
<script>
136+
const mockToday = '2022-10-10T16:22';
137+
Date = class extends Date {
138+
constructor(...args) {
139+
if (args.length === 0) {
140+
super(mockToday)
141+
} else {
142+
super(...args);
143+
}
144+
}
145+
}
146+
</script>
147+
`);
148+
149+
await page.waitForSelector('.datetime-ready');
150+
151+
const todayButton = page.locator('.calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
152+
153+
await expect(todayButton).toHaveClass(/calendar-day-today/);
154+
});
52155
});
53156

54157
test('setting value to empty string should treat it as having no date', async ({ page, skip }) => {

0 commit comments

Comments
 (0)