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;