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

Issue number: resolves #25839

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Confirming the working day when a datetime using a wheel picker without
an initial value will result in a value of `undefined` instead of the
displayed working day the user sees.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- `preferWheel` uses the working value on confirmation

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev-build: `7.5.7-dev.11701896424.13d40ac9`

Co-authored-by: liamdebeasi <liamdebeasi@users.noreply.github.com>
This commit is contained in:
Sean Perkins
2023-12-08 14:34:49 -05:00
committed by GitHub
parent 37290df7ef
commit e886e3ff2f
4 changed files with 327 additions and 26 deletions

View File

@ -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) {
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();

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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);
// 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 (yearValues !== undefined) {
copyParts.year = findClosestValue(year, yearValues);
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;