mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(datetime): account for allowed values when setting default date (#26093)
resolves #24722
This commit is contained in:
@@ -36,6 +36,7 @@ import { is24Hour, isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth }
|
||||
import {
|
||||
calculateHourFromAMPM,
|
||||
convertDataToISO,
|
||||
getClosestValidDate,
|
||||
getEndOfWeek,
|
||||
getNextDay,
|
||||
getNextMonth,
|
||||
@@ -96,7 +97,8 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
private minParts?: any;
|
||||
private maxParts?: any;
|
||||
private todayParts = parseDate(getToday());
|
||||
private todayParts!: DatetimeParts;
|
||||
private defaultParts!: DatetimeParts;
|
||||
|
||||
private prevPresentation: string | null = null;
|
||||
|
||||
@@ -559,13 +561,13 @@ export class Datetime implements ComponentInterface {
|
||||
* may not be set. This function works
|
||||
* by returning the first selected date in
|
||||
* "activePartsClone" and then falling back to
|
||||
* today's DatetimeParts if no active date is selected.
|
||||
* defaultParts if no active date is selected.
|
||||
*/
|
||||
private getDefaultPart = (): DatetimeParts => {
|
||||
const { activePartsClone, todayParts } = this;
|
||||
private getActivePartsWithFallback = () => {
|
||||
const { activePartsClone, defaultParts } = this;
|
||||
|
||||
const firstPart = Array.isArray(activePartsClone) ? activePartsClone[0] : activePartsClone;
|
||||
return firstPart ?? todayParts;
|
||||
return firstPart ?? defaultParts;
|
||||
};
|
||||
|
||||
private closeParentOverlay = () => {
|
||||
@@ -780,24 +782,24 @@ export class Datetime implements ComponentInterface {
|
||||
};
|
||||
|
||||
private processMinParts = () => {
|
||||
const { min, todayParts } = this;
|
||||
const { min, defaultParts } = this;
|
||||
if (min === undefined) {
|
||||
this.minParts = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.minParts = parseMinParts(min, todayParts);
|
||||
this.minParts = parseMinParts(min, defaultParts);
|
||||
};
|
||||
|
||||
private processMaxParts = () => {
|
||||
const { max, todayParts } = this;
|
||||
const { max, defaultParts } = this;
|
||||
|
||||
if (max === undefined) {
|
||||
this.maxParts = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.maxParts = parseMaxParts(max, todayParts);
|
||||
this.maxParts = parseMaxParts(max, defaultParts);
|
||||
};
|
||||
|
||||
private initializeCalendarListener = () => {
|
||||
@@ -1158,7 +1160,7 @@ export class Datetime implements ComponentInterface {
|
||||
* TODO FW-2646 remove value !== ''
|
||||
*/
|
||||
const hasValue = value !== '' && value !== null && value !== undefined;
|
||||
let valueToProcess = parseDate(hasValue ? value : getToday());
|
||||
let valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
|
||||
|
||||
const { minParts, maxParts, multiple } = this;
|
||||
if (!multiple && Array.isArray(value)) {
|
||||
@@ -1236,12 +1238,16 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
this.processMinParts();
|
||||
this.processMaxParts();
|
||||
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
|
||||
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
|
||||
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
|
||||
const yearValues = (this.parsedYearValues = convertToArrayOfNumbers(this.yearValues));
|
||||
const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues));
|
||||
|
||||
const todayParts = (this.todayParts = parseDate(getToday()));
|
||||
this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
|
||||
this.processValue(this.value);
|
||||
this.parsedHourValues = convertToArrayOfNumbers(this.hourValues);
|
||||
this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
|
||||
this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues);
|
||||
this.parsedYearValues = convertToArrayOfNumbers(this.yearValues);
|
||||
this.parsedDayValues = convertToArrayOfNumbers(this.dayValues);
|
||||
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
@@ -1396,9 +1402,9 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderCombinedDatePickerColumn() {
|
||||
const { workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
|
||||
const { defaultParts, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
/**
|
||||
* By default, generate a range of 3 months:
|
||||
@@ -1464,12 +1470,12 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
/**
|
||||
* If we have selected a day already, then default the column
|
||||
* to that value. Otherwise, default it to today.
|
||||
* to that value. Otherwise, set it to the default date.
|
||||
*/
|
||||
const todayString =
|
||||
workingParts.day !== null
|
||||
? `${workingParts.year}-${workingParts.month}-${workingParts.day}`
|
||||
: `${todayParts.year}-${todayParts.month}-${todayParts.day}`;
|
||||
: `${defaultParts.year}-${defaultParts.month}-${defaultParts.day}`;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
@@ -1555,7 +1561,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
const shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
|
||||
const years = shouldRenderYears
|
||||
? getYearColumnData(this.locale, this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
|
||||
? getYearColumnData(this.locale, this.defaultParts, this.minParts, this.maxParts, this.parsedYearValues)
|
||||
: [];
|
||||
|
||||
/**
|
||||
@@ -1588,14 +1594,14 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
class="day-column"
|
||||
color={this.color}
|
||||
items={days}
|
||||
value={(workingParts.day !== null ? workingParts.day : this.todayParts.day) ?? undefined}
|
||||
value={(workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined}
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
|
||||
// Due to a Safari 14 issue we need to destroy
|
||||
@@ -1632,7 +1638,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
@@ -1675,7 +1681,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
@@ -1739,7 +1745,7 @@ export class Datetime implements ComponentInterface {
|
||||
const { workingParts } = this;
|
||||
if (hoursData.length === 0) return [];
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
@@ -1767,7 +1773,7 @@ export class Datetime implements ComponentInterface {
|
||||
const { workingParts } = this;
|
||||
if (minutesData.length === 0) return [];
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
@@ -1797,7 +1803,7 @@ export class Datetime implements ComponentInterface {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
|
||||
|
||||
return (
|
||||
@@ -1911,7 +1917,7 @@ export class Datetime implements ComponentInterface {
|
||||
// can free-scroll the calendar.
|
||||
const isWorkingMonth = this.workingParts.month === month && this.workingParts.year === year;
|
||||
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2041,7 +2047,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
private renderTimeOverlay() {
|
||||
const use24Hour = is24Hour(this.locale, this.hourCycle);
|
||||
const activePart = this.getDefaultPart();
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return [
|
||||
<div class="time-header">{this.renderTimeLabel()}</div>,
|
||||
@@ -2122,7 +2128,7 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
} else {
|
||||
// for exactly 1 day selected (multiple set or not), show a formatted version of that
|
||||
headerText = getMonthAndDay(this.locale, this.getDefaultPart());
|
||||
headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback());
|
||||
}
|
||||
|
||||
return headerText;
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('datetime: values', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('md');
|
||||
});
|
||||
test('should render correct days', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime locale="en-US" presentation="date" day-values="1,2,3"></ion-datetime>
|
||||
@@ -49,6 +53,105 @@ test.describe('datetime: values', () => {
|
||||
const items = page.locator('ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)');
|
||||
await expect(items).toHaveText(['01', '02', '03']);
|
||||
});
|
||||
test('should adjust default parts for allowed hour and minute values', async ({ page }) => {
|
||||
/**
|
||||
* Mock today's date for testing.
|
||||
* Playwright does not support this natively
|
||||
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
|
||||
*/
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="time" locale="en-US" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2022-10-10T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const minuteItems = page.locator('ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)');
|
||||
await expect(minuteItems).toHaveText(['00', '15', '30', '45']);
|
||||
await expect(minuteItems.nth(1)).toHaveClass(/picker-item-active/);
|
||||
|
||||
const hourItems = page.locator('ion-picker-column-internal:nth-of-type(1) .picker-item:not(.picker-item-empty)');
|
||||
await expect(hourItems).toHaveText(['2']);
|
||||
await expect(hourItems.nth(0)).toHaveClass(/picker-item-active/);
|
||||
|
||||
/**
|
||||
* Since the allowed hour is 2AM, the time period
|
||||
* should switch from PM to AM.
|
||||
*/
|
||||
const ampmItems = page.locator('ion-picker-column-internal:nth-of-type(3) .picker-item:not(.picker-item-empty)');
|
||||
await expect(ampmItems).toHaveText(['AM', 'PM']);
|
||||
await expect(ampmItems.nth(0)).toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('should adjust default parts month for allowed month values', async ({ page }) => {
|
||||
/**
|
||||
* Mock today's date for testing.
|
||||
* Playwright does not support this natively
|
||||
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
|
||||
*/
|
||||
await page.setContent(`
|
||||
<ion-datetime prefer-wheel="true" presentation="date" locale="en-US" month-values="01" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2022-10-10T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const monthItems = page.locator('.month-column .picker-item:not(.picker-item-empty)');
|
||||
await expect(monthItems).toHaveText(['January']);
|
||||
await expect(monthItems.nth(0)).toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('today date highlight should persist even if disallowed from dayValues', async ({ page }) => {
|
||||
/**
|
||||
* Mock today's date for testing.
|
||||
* Playwright does not support this natively
|
||||
* so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
|
||||
*/
|
||||
await page.setContent(`
|
||||
<ion-datetime day-values="9" presentation="date" locale="en-US"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2022-10-10T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const todayButton = page.locator('.calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
|
||||
|
||||
await expect(todayButton).toHaveClass(/calendar-day-today/);
|
||||
});
|
||||
});
|
||||
|
||||
test('setting value to empty string should treat it as having no date', async ({ page, skip }) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DatetimeParts } from '../datetime-interface';
|
||||
|
||||
import { isSameDay } from './comparison';
|
||||
import { getNumDaysInMonth } from './helpers';
|
||||
import { parseAmPm } from './parse';
|
||||
|
||||
const twoDigit = (val: number | undefined): string => {
|
||||
return ('0' + (val !== undefined ? Math.abs(val) : '0')).slice(-2);
|
||||
@@ -429,3 +430,83 @@ export const validateParts = (
|
||||
|
||||
return partsCopy;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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[]
|
||||
) => {
|
||||
const { hour, minute, day, month, year } = refParts;
|
||||
const copyParts = { ...refParts, dayOfWeek: undefined };
|
||||
|
||||
if (monthValues !== undefined) {
|
||||
copyParts.month = findClosestValue(month, monthValues);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (hour !== undefined && hourValues !== undefined) {
|
||||
copyParts.hour = findClosestValue(hour, hourValues);
|
||||
copyParts.ampm = parseAmPm(copyParts.hour);
|
||||
}
|
||||
|
||||
if (minute !== undefined && minuteValues !== undefined) {
|
||||
copyParts.minute = findClosestValue(minute, minuteValues);
|
||||
}
|
||||
|
||||
return copyParts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the value in "values" that is
|
||||
* numerically closest to "reference".
|
||||
* This function assumes that "values" is
|
||||
* already sorted in ascending order.
|
||||
* @param reference The reference number to use
|
||||
* when finding the closest value
|
||||
* @param values The allowed values that will be
|
||||
* searched to find the closest value to "reference"
|
||||
*/
|
||||
const findClosestValue = (reference: number, values: number[]) => {
|
||||
let closestValue = values[0];
|
||||
let rank = Math.abs(closestValue - reference);
|
||||
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const value = values[i];
|
||||
/**
|
||||
* This code prioritizes the first
|
||||
* closest result. Given two values
|
||||
* with the same distance from reference,
|
||||
* this code will prioritize the smaller of
|
||||
* the two values.
|
||||
*/
|
||||
const valueRank = Math.abs(value - reference);
|
||||
if (valueRank < rank) {
|
||||
closestValue = value;
|
||||
rank = valueRank;
|
||||
}
|
||||
}
|
||||
|
||||
return closestValue;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user