Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(datetime): prefer wheel sets working value on confirmation #28520

Merged
merged 7 commits into from Dec 8, 2023
25 changes: 22 additions & 3 deletions core/src/components/datetime/datetime.tsx
Expand Up @@ -492,15 +492,24 @@ export class Datetime implements ComponentInterface {
*/
@Method()
async confirm(closeOverlay = false) {
const { isCalendarPicker, activeParts } = this;
const { isCalendarPicker, activeParts, preferWheel, workingParts } = this;
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved

/**
* We only update the value if the presentation is not a calendar picker.
*/
if (activeParts !== undefined || !isCalendarPicker) {
const activePartsIsArray = Array.isArray(activeParts);
if (activePartsIsArray && activeParts.length === 0) {
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved
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));
}
Expand Down Expand Up @@ -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();
Expand Down
92 changes: 92 additions & 0 deletions core/src/components/datetime/test/manipulation.spec.ts
Expand Up @@ -16,6 +16,7 @@ import {
subtractDays,
addDays,
validateParts,
getClosestValidDate,
} from '../utils/manipulation';

describe('addDays()', () => {
Expand Down Expand Up @@ -558,3 +559,94 @@ 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);
});
});
31 changes: 31 additions & 0 deletions 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: '<ion-datetime prefer-wheel="true" max="2021" show-default-buttons="true"></ion-datetime>',
});

const datetime = page.body.querySelector<HTMLIonDatetimeElement>('ion-datetime')!;
const confirmButton = datetime.shadowRoot!.querySelector<HTMLIonButtonElement>('#confirm-button')!;

confirmButton.click();

await page.waitForChanges();

expect(datetime.value).toBe('2021-12-31T23:59:00');
});
});
110 changes: 89 additions & 21 deletions 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';

Expand Down Expand Up @@ -424,44 +424,112 @@ 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 };
const copyParts = clampDate({ ...refParts, dayOfWeek: undefined }, minParts, maxParts);
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved

if (monthValues !== undefined) {
copyParts.month = findClosestValue(month, monthValues);
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved
// Filters out months that are out of the min/max bounds
const filteredMonths = monthValues.filter(
(month) =>
!(minParts !== undefined && month < minParts.month!) && !(maxParts !== undefined && month > maxParts.month!)
);
copyParts.month = findClosestValue(month, filteredMonths);
}

// Day is nullable but cannot be undefined
if (day !== null && dayValues !== undefined) {
copyParts.day = findClosestValue(day, dayValues);
// Filters out days that are out of the min/max bounds
const filteredDays = dayValues.filter((day) => {
if (minParts && isBefore({ ...copyParts, day }, minParts)) {
return false;
}
if (maxParts && isAfter({ ...copyParts, day }, maxParts)) {
return false;
}
return true;
});
copyParts.day = findClosestValue(day, filteredDays);
}

if (yearValues !== undefined) {
copyParts.year = findClosestValue(year, yearValues);
// Filters out years that are out of the min/max bounds
const filteredYears = yearValues.filter(
(year) => !(minParts !== undefined && year < minParts.year!) && !(maxParts !== undefined && year > maxParts.year!)
);
copyParts.year = findClosestValue(year, filteredYears);
}

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) && minute < minParts.minute) {
return false;
}
if (maxParts?.minute !== undefined && isSameDay(copyParts, maxParts) && minute > maxParts.minute) {
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return true;
});
copyParts.minute = findClosestValue(minute, filteredMinutes);
}

return copyParts;
Expand Down