diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index a9250cfa32..8db5b2fe20 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -46,7 +46,15 @@ import { getPreviousYear, getStartOfWeek, } from './utils/manipulation'; -import { clampDate, convertToArrayOfNumbers, getPartsFromCalendarDay, parseAmPm, parseDate } from './utils/parse'; +import { + clampDate, + convertToArrayOfNumbers, + getPartsFromCalendarDay, + parseAmPm, + parseDate, + parseMaxParts, + parseMinParts, +} from './utils/parse'; import { getCalendarDayState, isDayDisabled, @@ -774,37 +782,24 @@ export class Datetime implements ComponentInterface { }; private processMinParts = () => { - if (this.min === undefined) { + const { min, todayParts } = this; + if (min === undefined) { this.minParts = undefined; return; } - const { month, day, year, hour, minute } = parseDate(this.min); - - this.minParts = { - month, - day, - year, - hour, - minute, - }; + this.minParts = parseMinParts(min, todayParts); }; private processMaxParts = () => { - if (this.max === undefined) { + const { max, todayParts } = this; + + if (max === undefined) { this.maxParts = undefined; return; } - const { month, day, year, hour, minute } = parseDate(this.max); - - this.maxParts = { - month, - day, - year, - hour, - minute, - }; + this.maxParts = parseMaxParts(max, todayParts); }; private initializeCalendarListener = () => { @@ -1140,7 +1135,8 @@ export class Datetime implements ComponentInterface { } private processValue = (value?: string | string[] | null) => { - this.highlightActiveParts = !!value; + const hasValue = !!value; + this.highlightActiveParts = hasValue; let valueToProcess = parseDate(value || getToday()); const { minParts, maxParts, multiple } = this; @@ -1149,7 +1145,17 @@ export class Datetime implements ComponentInterface { valueToProcess = (valueToProcess as DatetimeParts[])[0]; } - warnIfValueOutOfBounds(valueToProcess, minParts, maxParts); + /** + * Datetime should only warn of out of bounds values + * if set by the user. If the `value` is undefined, + * we will default to today's date which may be out + * of bounds. In this case, the warning makes it look + * like the developer did something wrong which is + * not true. + */ + if (hasValue) { + warnIfValueOutOfBounds(valueToProcess, minParts, maxParts); + } /** * If there are multiple values, pick an arbitrary one to clamp to. This way, diff --git a/core/src/components/datetime/test/minmax/datetime.e2e.ts b/core/src/components/datetime/test/minmax/datetime.e2e.ts index 4a97594361..f3e434d6ed 100644 --- a/core/src/components/datetime/test/minmax/datetime.e2e.ts +++ b/core/src/components/datetime/test/minmax/datetime.e2e.ts @@ -111,27 +111,34 @@ test.describe('datetime: minmax', () => { }); test.describe('setting value outside bounds should show in-bounds month', () => { - const testDisplayedMonth = async (page: E2EPage, content: string) => { + test.beforeEach(({ skip }) => { + skip.rtl(); + }); + const testDisplayedMonth = async (page: E2EPage, content: string, expectedString = 'June 2021') => { await page.setContent(content); await page.waitForSelector('.datetime-ready'); const calendarMonthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(calendarMonthYear).toHaveText('June 2021'); + await expect(calendarMonthYear).toHaveText(expectedString); }; - test('when min is defined', async ({ page }) => { + test('when min and value are defined', async ({ page }) => { await testDisplayedMonth(page, ``); }); - test('when max is defined', async ({ page }) => { + test('when max and value are defined', async ({ page }) => { await testDisplayedMonth(page, ``); }); - test('when both min and max are defined', async ({ page }) => { + test('when min, max, and value are defined', async ({ page }) => { await testDisplayedMonth( page, `` ); }); + + test('when max is defined', async ({ page }) => { + await testDisplayedMonth(page, ``, 'June 2012'); + }); }); }); diff --git a/core/src/components/datetime/test/parse.spec.ts b/core/src/components/datetime/test/parse.spec.ts index 641b075349..7e152a3c95 100644 --- a/core/src/components/datetime/test/parse.spec.ts +++ b/core/src/components/datetime/test/parse.spec.ts @@ -1,4 +1,4 @@ -import { clampDate, getPartsFromCalendarDay, parseAmPm } from '../utils/parse'; +import { clampDate, getPartsFromCalendarDay, parseAmPm, parseMinParts, parseMaxParts } from '../utils/parse'; describe('getPartsFromCalendarDay()', () => { it('should extract DatetimeParts from a calendar day element', () => { @@ -72,3 +72,89 @@ describe('parseAmPm()', () => { expect(parseAmPm(11)).toEqual('am'); }); }); + +describe('parseMinParts()', () => { + it('should fill in missing information when not provided', () => { + const today = { + day: 14, + month: 3, + year: 2022, + minute: 4, + hour: 2, + }; + expect(parseMinParts('2012', today)).toEqual({ + month: 1, + day: 1, + year: 2012, + hour: 0, + minute: 0, + }); + }); + it('should default to current year when only given HH:mm', () => { + const today = { + day: 14, + month: 3, + year: 2022, + minute: 4, + hour: 2, + }; + expect(parseMinParts('04:30', today)).toEqual({ + month: 1, + day: 1, + year: 2022, + hour: 4, + minute: 30, + }); + }); +}); + +describe('parseMaxParts()', () => { + it('should fill in missing information when not provided', () => { + const today = { + day: 14, + month: 3, + year: 2022, + minute: 4, + hour: 2, + }; + expect(parseMaxParts('2012', today)).toEqual({ + month: 12, + day: 31, + year: 2012, + hour: 23, + minute: 59, + }); + }); + it('should default to current year when only given HH:mm', () => { + const today = { + day: 14, + month: 3, + year: 2022, + minute: 4, + hour: 2, + }; + expect(parseMaxParts('04:30', today)).toEqual({ + month: 12, + day: 31, + year: 2022, + hour: 4, + minute: 30, + }); + }); + it('should fill in correct day during a leap year', () => { + const today = { + day: 14, + month: 3, + year: 2022, + minute: 4, + hour: 2, + }; + expect(parseMaxParts('2012-02', today)).toEqual({ + month: 2, + day: 29, + year: 2012, + hour: 23, + minute: 59, + }); + }); +}); diff --git a/core/src/components/datetime/utils/parse.ts b/core/src/components/datetime/utils/parse.ts index 4eb54ac7d2..ebfe2a94ea 100644 --- a/core/src/components/datetime/utils/parse.ts +++ b/core/src/components/datetime/utils/parse.ts @@ -1,6 +1,7 @@ import type { DatetimeParts } from '../datetime-interface'; import { isAfter, isBefore } from './comparison'; +import { getNumDaysInMonth } from './helpers'; const ISO_8601_REGEXP = // eslint-disable-next-line no-useless-escape @@ -138,3 +139,72 @@ export const clampDate = ( export const parseAmPm = (hour: number) => { return hour >= 12 ? 'pm' : 'am'; }; + +/** + * Takes a max date string and creates a DatetimeParts + * object, filling in any missing information. + * For example, max="2012" would fill in the missing + * month, day, hour, and minute information. + */ +export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeParts => { + const { month, day, year, hour, minute } = parseDate(max); + + /** + * When passing in `max` or `min`, developers + * can pass in any ISO-8601 string. This means + * that not all of the date/time fields are defined. + * For example, passing max="2012" is valid even though + * there is no month, day, hour, or minute data. + * However, all of this data is required when clamping the date + * so that the correct initial value can be selected. As a result, + * we need to fill in any omitted data with the min or max values. + */ + + const yearValue = year ?? todayParts.year; + const monthValue = month ?? 12; + return { + month: monthValue, + day: day ?? getNumDaysInMonth(monthValue, yearValue), + /** + * Passing in "HH:mm" is a valid ISO-8601 + * string, so we just default to the current year + * in this case. + */ + year: yearValue, + hour: hour ?? 23, + minute: minute ?? 59, + }; +}; + +/** + * Takes a min date string and creates a DatetimeParts + * object, filling in any missing information. + * For example, min="2012" would fill in the missing + * month, day, hour, and minute information. + */ +export const parseMinParts = (min: string, todayParts: DatetimeParts): DatetimeParts => { + const { month, day, year, hour, minute } = parseDate(min); + + /** + * When passing in `max` or `min`, developers + * can pass in any ISO-8601 string. This means + * that not all of the date/time fields are defined. + * For example, passing max="2012" is valid even though + * there is no month, day, hour, or minute data. + * However, all of this data is required when clamping the date + * so that the correct initial value can be selected. As a result, + * we need to fill in any omitted data with the min or max values. + */ + return { + month: month ?? 1, + day: day ?? 1, + /** + * Passing in "HH:mm" is a valid ISO-8601 + * string, so we just default to the current year + * in this case. + */ + year: year ?? todayParts.year, + hour: hour ?? 0, + minute: minute ?? 0, + }; +};