diff --git a/core/src/components.d.ts b/core/src/components.d.ts index e131af76b9..ab4f626cf3 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -798,7 +798,7 @@ export namespace Components { */ "readonly": boolean; /** - * Resets the internal state of the datetime but does not update the value. Passing a valid ISO-8601 string will reset the state of the component to the provided date. If no value is provided, the internal state will be reset to today. + * Resets the internal state of the datetime but does not update the value. Passing a valid ISO-8601 string will reset the state of the component to the provided date. If no value is provided, the internal state will be reset to the clamped value of the min, max and today. */ "reset": (startDate?: string | undefined) => Promise; /** diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 21298826f3..d9e9c518dd 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -37,7 +37,7 @@ import { getPreviousYear, getStartOfWeek, } from './utils/manipulation'; -import { convertToArrayOfNumbers, getPartsFromCalendarDay, parseDate } from './utils/parse'; +import { clampDate, convertToArrayOfNumbers, getPartsFromCalendarDay, parseDate } from './utils/parse'; import { getCalendarDayState, isDayDisabled, @@ -472,7 +472,7 @@ export class Datetime implements ComponentInterface { /** * Resets the internal state of the datetime but does not update the value. * Passing a valid ISO-8601 string will reset the state of the component to the provided date. - * If no value is provided, the internal state will be reset to today. + * If no value is provided, the internal state will be reset to the clamped value of the min, max and today. */ @Method() async reset(startDate?: string) { @@ -1083,8 +1083,8 @@ export class Datetime implements ComponentInterface { private processValue = (value?: string | null) => { this.highlightActiveParts = !!value; - const valueToProcess = value || getToday(); - const { month, day, year, hour, minute, tzOffset } = parseDate(valueToProcess); + const valueToProcess = parseDate(value || getToday()); + const { month, day, year, hour, minute, tzOffset } = clampDate(valueToProcess, this.minParts, this.maxParts); this.setWorkingParts({ month, @@ -1093,7 +1093,7 @@ export class Datetime implements ComponentInterface { hour, minute, tzOffset, - ampm: hour >= 12 ? 'pm' : 'am', + ampm: hour! >= 12 ? 'pm' : 'am', }); this.activeParts = { @@ -1103,7 +1103,7 @@ export class Datetime implements ComponentInterface { hour, minute, tzOffset, - ampm: hour >= 12 ? 'pm' : 'am', + ampm: hour! >= 12 ? 'pm' : 'am', }; }; @@ -1676,8 +1676,8 @@ export class Datetime implements ComponentInterface { const { hours, minutes, am, pm } = generateTime( workingParts, use24Hour ? 'h23' : 'h12', - this.minParts, - this.maxParts, + this.value ? this.minParts : undefined, + this.value ? this.maxParts : undefined, this.parsedHourValues, this.parsedMinuteValues ); diff --git a/core/src/components/datetime/test/minmax/datetime.e2e.ts b/core/src/components/datetime/test/minmax/datetime.e2e.ts index 2b66f462c1..7e01cbc3da 100644 --- a/core/src/components/datetime/test/minmax/datetime.e2e.ts +++ b/core/src/components/datetime/test/minmax/datetime.e2e.ts @@ -47,4 +47,33 @@ test.describe('datetime: minmax', () => { expect(nextButton).toBeDisabled(); expect(prevButton).toBeEnabled(); }); + + test.describe('when the datetime does not have a value', () => { + test('all time values should be available for selection', async ({ page }) => { + /** + * When the datetime does not have an initial value and today falls outside of + * the specified min and max values, all times values should be available for selection. + */ + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + + await page.click('.time-body'); + await ionPopoverDidPresent.next(); + + const hours = page.locator( + 'ion-popover ion-picker-column-internal:nth-child(1) .picker-item:not(.picker-item-empty)' + ); + const minutes = page.locator( + 'ion-popover ion-picker-column-internal:nth-child(2) .picker-item:not(.picker-item-empty)' + ); + + expect(await hours.count()).toBe(12); + expect(await minutes.count()).toBe(60); + }); + }); }); diff --git a/core/src/components/datetime/test/parse.spec.ts b/core/src/components/datetime/test/parse.spec.ts index 2ede204529..f69362c4ab 100644 --- a/core/src/components/datetime/test/parse.spec.ts +++ b/core/src/components/datetime/test/parse.spec.ts @@ -1,4 +1,4 @@ -import { getPartsFromCalendarDay } from '../utils/parse'; +import { clampDate, getPartsFromCalendarDay } from '../utils/parse'; describe('getPartsFromCalendarDay()', () => { it('should extract DatetimeParts from a calendar day element', () => { @@ -18,3 +18,46 @@ describe('getPartsFromCalendarDay()', () => { }); // TODO: parseDate() + +describe('clampDate()', () => { + const minParts = { + year: 2021, + month: 6, + day: 5, + }; + + const maxParts = { + year: 2021, + month: 8, + day: 19, + }; + it('should return the max month when the value is greater than the max', () => { + const dateParts = { + year: 2022, + month: 5, + day: 24, + }; + const value = clampDate(dateParts, minParts, maxParts); + expect(value).toStrictEqual(maxParts); + }); + + it('should return the min month when the value is less than the min', () => { + const dateParts = { + year: 2020, + month: 5, + day: 24, + }; + const value = clampDate(dateParts, minParts, maxParts); + expect(value).toStrictEqual(minParts); + }); + + it('should return the value when the value is greater than the min and less than the max', () => { + const dateParts = { + year: 2021, + month: 7, + day: 10, + }; + const value = clampDate(dateParts, minParts, maxParts); + expect(value).toStrictEqual(dateParts); + }); +}); diff --git a/core/src/components/datetime/utils/parse.ts b/core/src/components/datetime/utils/parse.ts index dd0a7ad753..c6e5e47d5f 100644 --- a/core/src/components/datetime/utils/parse.ts +++ b/core/src/components/datetime/utils/parse.ts @@ -1,5 +1,7 @@ import type { DatetimeParts } from '../datetime-interface'; +import { isAfter, isBefore } from './comparison'; + const ISO_8601_REGEXP = // eslint-disable-next-line no-useless-escape /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; @@ -106,3 +108,16 @@ export const parseDate = (val: string | undefined | null): any | undefined => { tzOffset, }; }; + +export const clampDate = ( + dateParts: DatetimeParts, + minParts?: DatetimeParts, + maxParts?: DatetimeParts +): DatetimeParts => { + if (minParts && isBefore(dateParts, minParts)) { + return minParts; + } else if (maxParts && isAfter(dateParts, maxParts)) { + return maxParts; + } + return dateParts; +};