fix(datetime): ensure that default month shown is always in bounds (#25351)

Resolves #25320
This commit is contained in:
Sean Perkins
2022-06-02 22:27:25 -04:00
committed by GitHub
parent 4195f7ee28
commit 866d4528ad
5 changed files with 97 additions and 10 deletions

View File

@ -798,7 +798,7 @@ export namespace Components {
*/ */
"readonly": boolean; "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<void>; "reset": (startDate?: string | undefined) => Promise<void>;
/** /**

View File

@ -37,7 +37,7 @@ import {
getPreviousYear, getPreviousYear,
getStartOfWeek, getStartOfWeek,
} from './utils/manipulation'; } from './utils/manipulation';
import { convertToArrayOfNumbers, getPartsFromCalendarDay, parseDate } from './utils/parse'; import { clampDate, convertToArrayOfNumbers, getPartsFromCalendarDay, parseDate } from './utils/parse';
import { import {
getCalendarDayState, getCalendarDayState,
isDayDisabled, isDayDisabled,
@ -472,7 +472,7 @@ export class Datetime implements ComponentInterface {
/** /**
* Resets the internal state of the datetime but does not update the value. * 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. * 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() @Method()
async reset(startDate?: string) { async reset(startDate?: string) {
@ -1083,8 +1083,8 @@ export class Datetime implements ComponentInterface {
private processValue = (value?: string | null) => { private processValue = (value?: string | null) => {
this.highlightActiveParts = !!value; this.highlightActiveParts = !!value;
const valueToProcess = value || getToday(); const valueToProcess = parseDate(value || getToday());
const { month, day, year, hour, minute, tzOffset } = parseDate(valueToProcess); const { month, day, year, hour, minute, tzOffset } = clampDate(valueToProcess, this.minParts, this.maxParts);
this.setWorkingParts({ this.setWorkingParts({
month, month,
@ -1093,7 +1093,7 @@ export class Datetime implements ComponentInterface {
hour, hour,
minute, minute,
tzOffset, tzOffset,
ampm: hour >= 12 ? 'pm' : 'am', ampm: hour! >= 12 ? 'pm' : 'am',
}); });
this.activeParts = { this.activeParts = {
@ -1103,7 +1103,7 @@ export class Datetime implements ComponentInterface {
hour, hour,
minute, minute,
tzOffset, 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( const { hours, minutes, am, pm } = generateTime(
workingParts, workingParts,
use24Hour ? 'h23' : 'h12', use24Hour ? 'h23' : 'h12',
this.minParts, this.value ? this.minParts : undefined,
this.maxParts, this.value ? this.maxParts : undefined,
this.parsedHourValues, this.parsedHourValues,
this.parsedMinuteValues this.parsedMinuteValues
); );

View File

@ -47,4 +47,33 @@ test.describe('datetime: minmax', () => {
expect(nextButton).toBeDisabled(); expect(nextButton).toBeDisabled();
expect(prevButton).toBeEnabled(); 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(`
<ion-datetime min="2022-04-22T04:10:00" max="2022-05-21T21:30:00"></ion-datetime>
`);
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);
});
});
}); });

View File

@ -1,4 +1,4 @@
import { getPartsFromCalendarDay } from '../utils/parse'; import { clampDate, getPartsFromCalendarDay } from '../utils/parse';
describe('getPartsFromCalendarDay()', () => { describe('getPartsFromCalendarDay()', () => {
it('should extract DatetimeParts from a calendar day element', () => { it('should extract DatetimeParts from a calendar day element', () => {
@ -18,3 +18,46 @@ describe('getPartsFromCalendarDay()', () => {
}); });
// TODO: parseDate() // 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);
});
});

View File

@ -1,5 +1,7 @@
import type { DatetimeParts } from '../datetime-interface'; import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore } from './comparison';
const ISO_8601_REGEXP = const ISO_8601_REGEXP =
// eslint-disable-next-line no-useless-escape // 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}))?)?)?$/; /^(\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, 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;
};