diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index d2c6cd6d0b5..421b993d3ae 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -492,7 +492,7 @@ export class Datetime implements ComponentInterface { */ @Method() async confirm(closeOverlay = false) { - const { isCalendarPicker, activeParts } = this; + const { isCalendarPicker, activeParts, preferWheel, workingParts } = this; /** * We only update the value if the presentation is not a calendar picker. @@ -500,7 +500,16 @@ export class Datetime implements ComponentInterface { if (activeParts !== undefined || !isCalendarPicker) { const activePartsIsArray = Array.isArray(activeParts); if (activePartsIsArray && activeParts.length === 0) { - this.setValue(undefined); + if (preferWheel) { + /** + * If the datetime is using a wheel picker, but the + * active parts are empty, then the user has confirmed the + * initial value (working parts) presented to them. + */ + this.setValue(convertDataToISO(workingParts)); + } else { + this.setValue(undefined); + } } else { this.setValue(convertDataToISO(activeParts)); } @@ -1356,11 +1365,21 @@ export class Datetime implements ComponentInterface { const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues)); const todayParts = (this.todayParts = parseDate(getToday())!); - this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues); this.processMinParts(); this.processMaxParts(); + this.defaultParts = getClosestValidDate({ + refParts: todayParts, + monthValues, + dayValues, + yearValues, + hourValues, + minuteValues, + minParts: this.minParts, + maxParts: this.maxParts, + }); + this.processValue(this.value); this.emitStyle(); diff --git a/core/src/components/datetime/test/manipulation.spec.ts b/core/src/components/datetime/test/manipulation.spec.ts index 8cfdbcf65b9..daebf7c33ea 100644 --- a/core/src/components/datetime/test/manipulation.spec.ts +++ b/core/src/components/datetime/test/manipulation.spec.ts @@ -16,6 +16,7 @@ import { subtractDays, addDays, validateParts, + getClosestValidDate, } from '../utils/manipulation'; describe('addDays()', () => { @@ -558,3 +559,160 @@ describe('validateParts()', () => { ).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 }); }); }); + +describe('getClosestValidDate()', () => { + it('should match a date with only month/day/year', () => { + // October 10, 2023 + const refParts = { month: 10, day: 10, year: 2023 }; + // April 10, 2021 + const minParts = { month: 4, day: 10, year: 2021 }; + // September 14, 2021 + const maxParts = { month: 9, day: 14, year: 2021 }; + + // September 4, 2021 + const expected = { month: 9, day: 4, year: 2021, dayOfWeek: undefined }; + + expect( + getClosestValidDate({ + refParts, + monthValues: [2, 3, 7, 9, 10], + dayValues: [4, 15, 25], + yearValues: [2020, 2021, 2023], + maxParts, + minParts, + }) + ).toEqual(expected); + }); + + it('should match a date when the reference date is before the min', () => { + // April 2, 2020 3:20 PM + const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 }; + // September 10, 2021 10:10 AM + const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 }; + // September 14, 2021 10:11 AM + const maxParts = { month: 9, day: 14, year: 2021, hour: 10, minute: 11 }; + + // September 11, 2021 11:15 AM + const expected = { + year: 2021, + day: 11, + month: 9, + hour: 11, + minute: 15, + ampm: 'am', + dayOfWeek: undefined, + }; + + expect( + getClosestValidDate({ + refParts, + monthValues: [4, 9, 11], + dayValues: [11, 12, 13, 14], + yearValues: [2020, 2021, 2023], + hourValues: [9, 10, 11], + minuteValues: [11, 12, 13, 14, 15], + maxParts, + minParts, + }) + ).toEqual(expected); + }); + + it('should match a date when the reference date is before the min', () => { + // April 2, 2020 3:20 PM + const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 }; + // September 10, 2021 10:10 AM + const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 }; + // September 10, 2021 10:15 AM + const maxParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 15 }; + + // September 10, 2021 10:15 AM + const expected = { + month: 9, + day: 10, + year: 2021, + hour: 10, + minute: 15, + ampm: 'am', + dayOfWeek: undefined, + }; + + expect( + getClosestValidDate({ + refParts, + monthValues: [4, 9, 11], + dayValues: [10, 12, 13, 14], + yearValues: [2020, 2021, 2023], + hourValues: [9, 10, 11], + minuteValues: [11, 12, 13, 14, 15], + minParts, + maxParts, + }) + ).toEqual(expected); + }); + + it('should only clamp minutes if within the same day and hour as min/max', () => { + // April 2, 2020 9:16 AM + const refParts = { month: 4, day: 2, year: 2020, hour: 9, minute: 16 }; + // September 10, 2021 10:10 AM + const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 }; + // September 10, 2021 11:15 AM + const maxParts = { month: 9, day: 10, year: 2021, hour: 11, minute: 15 }; + + // September 10, 2021 10:16 AM + const expected = { + month: 9, + day: 10, + year: 2021, + hour: 10, + minute: 16, + ampm: 'am', + dayOfWeek: undefined, + }; + + expect( + getClosestValidDate({ + refParts, + monthValues: [4, 9, 11], + dayValues: [10, 12, 13, 14], + yearValues: [2020, 2021, 2023], + hourValues: [9, 10, 11], + minuteValues: [10, 15, 16], + minParts, + maxParts, + }) + ).toEqual(expected); + }); + + it('should return the closest valid date after adjusting the allowed year', () => { + // April 2, 2022 9:16 AM + const refParts = { month: 4, day: 2, year: 2022, hour: 9, minute: 16 }; + // September 10, 2021 10:10 AM + const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 }; + // September 10, 2023 11:15 AM + const maxParts = { month: 9, day: 10, year: 2023, hour: 11, minute: 15 }; + + // April 2, 2022 9:16 AM + const expected = { + month: 4, + day: 2, + year: 2022, + hour: 9, + minute: 16, + ampm: 'am', + dayOfWeek: undefined, + }; + + expect( + getClosestValidDate({ + refParts, + monthValues: [4, 9, 11], + dayValues: [2, 10, 12, 13, 14], + yearValues: [2020, 2021, 2022, 2023], + hourValues: [9, 10, 11], + minuteValues: [10, 15, 16], + minParts, + maxParts, + }) + ).toEqual(expected); + }); +}); diff --git a/core/src/components/datetime/test/prefer-wheel/datetime.spec.ts b/core/src/components/datetime/test/prefer-wheel/datetime.spec.ts new file mode 100644 index 00000000000..55dff30f2dc --- /dev/null +++ b/core/src/components/datetime/test/prefer-wheel/datetime.spec.ts @@ -0,0 +1,31 @@ +import { newSpecPage } from '@stencil/core/testing'; + +import { Datetime } from '../../datetime'; + +describe('datetime: preferWheel', () => { + beforeEach(() => { + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + global.IntersectionObserver = mockIntersectionObserver; + }); + + it('should select the working day when clicking the confirm button', async () => { + const page = await newSpecPage({ + components: [Datetime], + html: '', + }); + + const datetime = page.body.querySelector('ion-datetime')!; + const confirmButton = datetime.shadowRoot!.querySelector('#confirm-button')!; + + confirmButton.click(); + + await page.waitForChanges(); + + expect(datetime.value).toBe('2021-12-31T23:59:00'); + }); +}); diff --git a/core/src/components/datetime/utils/manipulation.ts b/core/src/components/datetime/utils/manipulation.ts index 0846182f1ec..152f2f62fec 100644 --- a/core/src/components/datetime/utils/manipulation.ts +++ b/core/src/components/datetime/utils/manipulation.ts @@ -1,6 +1,6 @@ import type { DatetimeParts } from '../datetime-interface'; -import { isSameDay } from './comparison'; +import { isAfter, isBefore, isSameDay } from './comparison'; import { getNumDaysInMonth } from './helpers'; import { clampDate, parseAmPm } from './parse'; @@ -424,44 +424,137 @@ export const validateParts = ( * Returns the closest date to refParts * that also meets the constraints of * the *Values params. - * @param refParts The reference date - * @param monthValues The allowed month values - * @param dayValues The allowed day (of the month) values - * @param yearValues The allowed year values - * @param hourValues The allowed hour values - * @param minuteValues The allowed minute values */ -export const getClosestValidDate = ( - refParts: DatetimeParts, - monthValues?: number[], - dayValues?: number[], - yearValues?: number[], - hourValues?: number[], - minuteValues?: number[] -) => { +export const getClosestValidDate = ({ + refParts, + monthValues, + dayValues, + yearValues, + hourValues, + minuteValues, + minParts, + maxParts, +}: { + /** + * The reference date + */ + refParts: DatetimeParts; + /** + * The allowed month values + */ + monthValues?: number[]; + /** + * The allowed day (of the month) values + */ + dayValues?: number[]; + /** + * The allowed year values + */ + yearValues?: number[]; + /** + * The allowed hour values + */ + hourValues?: number[]; + /** + * The allowed minute values + */ + minuteValues?: number[]; + /** + * The minimum date that can be returned + */ + minParts?: DatetimeParts; + /** + * The maximum date that can be returned + */ + maxParts?: DatetimeParts; +}) => { const { hour, minute, day, month, year } = refParts; const copyParts = { ...refParts, dayOfWeek: undefined }; + if (yearValues !== undefined) { + // Filters out years that are out of the min/max bounds + const filteredYears = yearValues.filter((year) => { + if (minParts !== undefined && year < minParts.year) { + return false; + } + + if (maxParts !== undefined && year > maxParts.year) { + return false; + } + + return true; + }); + copyParts.year = findClosestValue(year, filteredYears); + } + if (monthValues !== undefined) { - copyParts.month = findClosestValue(month, monthValues); + // Filters out months that are out of the min/max bounds + const filteredMonths = monthValues.filter((month) => { + if (minParts !== undefined && copyParts.year === minParts.year && month < minParts.month) { + return false; + } + + if (maxParts !== undefined && copyParts.year === maxParts.year && month > maxParts.month) { + return false; + } + + return true; + }); + copyParts.month = findClosestValue(month, filteredMonths); } // Day is nullable but cannot be undefined if (day !== null && dayValues !== undefined) { - copyParts.day = findClosestValue(day, dayValues); - } - - if (yearValues !== undefined) { - copyParts.year = findClosestValue(year, yearValues); + // Filters out days that are out of the min/max bounds + const filteredDays = dayValues.filter((day) => { + if (minParts !== undefined && isBefore({ ...copyParts, day }, minParts)) { + return false; + } + if (maxParts !== undefined && isAfter({ ...copyParts, day }, maxParts)) { + return false; + } + return true; + }); + copyParts.day = findClosestValue(day, filteredDays); } if (hour !== undefined && hourValues !== undefined) { - copyParts.hour = findClosestValue(hour, hourValues); + // Filters out hours that are out of the min/max bounds + const filteredHours = hourValues.filter((hour) => { + if (minParts?.hour !== undefined && isSameDay(copyParts, minParts) && hour < minParts.hour) { + return false; + } + if (maxParts?.hour !== undefined && isSameDay(copyParts, maxParts) && hour > maxParts.hour) { + return false; + } + return true; + }); + copyParts.hour = findClosestValue(hour, filteredHours); copyParts.ampm = parseAmPm(copyParts.hour); } if (minute !== undefined && minuteValues !== undefined) { - copyParts.minute = findClosestValue(minute, minuteValues); + // Filters out minutes that are out of the min/max bounds + const filteredMinutes = minuteValues.filter((minute) => { + if ( + minParts?.minute !== undefined && + isSameDay(copyParts, minParts) && + copyParts.hour === minParts.hour && + minute < minParts.minute + ) { + return false; + } + if ( + maxParts?.minute !== undefined && + isSameDay(copyParts, maxParts) && + copyParts.hour === maxParts.hour && + minute > maxParts.minute + ) { + return false; + } + return true; + }); + copyParts.minute = findClosestValue(minute, filteredMinutes); } return copyParts;