mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-07 23:16:52 +08:00
fix(datetime): scroll to newly selected date when value changes (#27806)
Issue number: Resolves #26391 --------- <!-- 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. --> When updating the `value` programmatically on an `ion-datetime` after it has already been created: - With grid style: The selected date visually updates, but the calendar does not scroll to the newly selected month. - With wheel style: The selected date does not visually update, i.e. the wheels do not move to show the newly selected date. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Grid style datetimes now scroll to the selected date using the same animation as when clicking the next/prev month buttons. - This animation mirrors the behavior in both MUI and native iOS. See the [design doc](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/datetime/0003-datetime-async-value.md) for more information and screen recordings. - The animation will not occur if the month/year did not change, or when the datetime is hidden. - Wheel style datetimes now visually update to the selected date. No animation occurs, also mirroring native. - The `parseDate` util has also had its type signatures updated to account for returning `undefined` when the date string is improperly formatted. This was missed when the util was refactored to support multiple date selection. ## 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. --> - Docs PR: https://github.com/ionic-team/ionic-docs/pull/3053 - While this can technically be considered a bug fix, we are merging it into a feature branch for safety; it's a fairly significant change to how datetime behaves, and may interfere with custom logic when updating a datetime's value async. - Jumping to the newly selected value is handled by replacing everything [here](https://github.com/ionic-team/ionic-framework/pull/27806/files#diff-4a407530c60e3cf72bcc11acdd21c4803a94bf47ea81b99e757db1c93d2735b8L364-L407) with `processValue()`. This covers both wheel and grid datetimes. - `activePartsClone` as a whole was also removed. It was added in https://github.com/ionic-team/ionic-framework/pull/24244 to enable changing `activeParts` without triggering a rerender (and thus jumping to the new value) but since we now want to do that jump, the clone is no longer needed. - The animation code might be tricky to follow, so I recorded going through it: https://github.com/ionic-team/ionic-framework/assets/90629384/1afa5762-f493-441a-b662-f0429f2d86a7
This commit is contained in:
@ -206,6 +206,10 @@ export class DatetimeButton implements ComponentInterface {
|
||||
*/
|
||||
const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
|
||||
|
||||
if (!parsedDatetimes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If developers incorrectly use multiple="true"
|
||||
* with non "date" datetimes, then just select
|
||||
|
||||
@ -117,11 +117,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
private prevPresentation: string | null = null;
|
||||
|
||||
/**
|
||||
* Duplicate reference to `activeParts` that does not trigger a re-render of the component.
|
||||
* Allows caching an instance of the `activeParts` in between render cycles.
|
||||
*/
|
||||
private activePartsClone: DatetimeParts | DatetimeParts[] = [];
|
||||
private resolveForceDateScrolling?: () => void;
|
||||
|
||||
@State() showMonthAndYear = false;
|
||||
|
||||
@ -140,6 +136,17 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
@State() isTimePopoverOpen = false;
|
||||
|
||||
/**
|
||||
* When defined, will force the datetime to render the month
|
||||
* containing the specified date. Currently, this should only
|
||||
* be used to enable immediately auto-scrolling to the new month,
|
||||
* and should then be reset to undefined once the transition is
|
||||
* finished and the forced month is now in view.
|
||||
*
|
||||
* Applies to grid-style datetimes only.
|
||||
*/
|
||||
@State() forceRenderDate?: DatetimeParts;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
@ -221,6 +228,12 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
@Prop() presentation: DatetimePresentation = 'date-time';
|
||||
|
||||
private get isGridStyle() {
|
||||
const { presentation, preferWheel } = this;
|
||||
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
|
||||
return hasDatePresentation && !preferWheel;
|
||||
}
|
||||
|
||||
/**
|
||||
* The text to display on the picker's cancel button.
|
||||
*/
|
||||
@ -302,11 +315,6 @@ export class Datetime implements ComponentInterface {
|
||||
this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
|
||||
}
|
||||
|
||||
@Watch('activeParts')
|
||||
protected activePartsChanged() {
|
||||
this.activePartsClone = this.activeParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* The locale to use for `ion-datetime`. This
|
||||
* impacts month and day name formatting.
|
||||
@ -356,54 +364,11 @@ export class Datetime implements ComponentInterface {
|
||||
* Update the datetime value when the value changes
|
||||
*/
|
||||
@Watch('value')
|
||||
protected valueChanged() {
|
||||
const { value, minParts, maxParts, workingParts } = this;
|
||||
protected async valueChanged() {
|
||||
const { value } = this;
|
||||
|
||||
if (this.hasValue()) {
|
||||
this.warnIfIncorrectValueUsage();
|
||||
|
||||
/**
|
||||
* Clones the value of the `activeParts` to the private clone, to update
|
||||
* the date display on the current render cycle without causing another render.
|
||||
*
|
||||
* This allows us to update the current value's date/time display without
|
||||
* refocusing or shifting the user's display (leaves the user in place).
|
||||
*/
|
||||
const valueDateParts = parseDate(value);
|
||||
if (valueDateParts) {
|
||||
warnIfValueOutOfBounds(valueDateParts, minParts, maxParts);
|
||||
|
||||
if (Array.isArray(valueDateParts)) {
|
||||
this.activePartsClone = [...valueDateParts];
|
||||
} else {
|
||||
const { month, day, year, hour, minute } = valueDateParts;
|
||||
const ampm = hour != null ? (hour >= 12 ? 'pm' : 'am') : undefined;
|
||||
|
||||
this.activePartsClone = {
|
||||
...this.activeParts,
|
||||
month,
|
||||
day,
|
||||
year,
|
||||
hour,
|
||||
minute,
|
||||
ampm,
|
||||
};
|
||||
|
||||
/**
|
||||
* The working parts am/pm value must be updated when the value changes, to
|
||||
* ensure the time picker hour column values are generated correctly.
|
||||
*
|
||||
* Note that we don't need to do this if valueDateParts is an array, since
|
||||
* multiple="true" does not apply to time pickers.
|
||||
*/
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
ampm,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
printIonWarning(`Unable to parse date string: ${value}. Please provide a valid ISO 8601 datetime string.`);
|
||||
}
|
||||
this.processValue(value);
|
||||
}
|
||||
|
||||
this.emitStyle();
|
||||
@ -596,9 +561,9 @@ export class Datetime implements ComponentInterface {
|
||||
* data. This should be used when rendering an
|
||||
* interface in an environment where the `value`
|
||||
* may not be set. This function works
|
||||
* by returning the first selected date in
|
||||
* "activePartsClone" and then falling back to
|
||||
* defaultParts if no active date is selected.
|
||||
* by returning the first selected date and then
|
||||
* falling back to defaultParts if no active date
|
||||
* is selected.
|
||||
*/
|
||||
private getActivePartsWithFallback = () => {
|
||||
const { defaultParts } = this;
|
||||
@ -606,8 +571,8 @@ export class Datetime implements ComponentInterface {
|
||||
};
|
||||
|
||||
private getActivePart = () => {
|
||||
const { activePartsClone } = this;
|
||||
return Array.isArray(activePartsClone) ? activePartsClone[0] : activePartsClone;
|
||||
const { activeParts } = this;
|
||||
return Array.isArray(activeParts) ? activeParts[0] : activeParts;
|
||||
};
|
||||
|
||||
private closeParentOverlay = () => {
|
||||
@ -627,7 +592,7 @@ export class Datetime implements ComponentInterface {
|
||||
};
|
||||
|
||||
private setActiveParts = (parts: DatetimeParts, removeDate = false) => {
|
||||
const { multiple, minParts, maxParts, activePartsClone } = this;
|
||||
const { multiple, minParts, maxParts, activeParts } = this;
|
||||
|
||||
/**
|
||||
* When setting the active parts, it is possible
|
||||
@ -643,16 +608,7 @@ export class Datetime implements ComponentInterface {
|
||||
this.setWorkingParts(validatedParts);
|
||||
|
||||
if (multiple) {
|
||||
/**
|
||||
* We read from activePartsClone here because valueChanged() only updates that,
|
||||
* so it's the more reliable source of truth. If we read from activeParts, then
|
||||
* if you click July 1, manually set the value to July 2, and then click July 3,
|
||||
* the new value would be [July 1, July 3], ignoring the value set.
|
||||
*
|
||||
* We can then pass the new value to activeParts (rather than activePartsClone)
|
||||
* since the clone will be updated automatically by activePartsChanged().
|
||||
*/
|
||||
const activePartsArray = Array.isArray(activePartsClone) ? activePartsClone : [activePartsClone];
|
||||
const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
|
||||
if (removeDate) {
|
||||
this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
|
||||
} else {
|
||||
@ -908,6 +864,20 @@ export class Datetime implements ComponentInterface {
|
||||
const monthBox = month.getBoundingClientRect();
|
||||
if (Math.abs(monthBox.x - box.x) > 2) return;
|
||||
|
||||
/**
|
||||
* If we're force-rendering a month, assume we've
|
||||
* scrolled to that and return it.
|
||||
*
|
||||
* If forceRenderDate is ever used in a context where the
|
||||
* forced month is not immediately auto-scrolled to, this
|
||||
* should be updated to also check whether `month` has the
|
||||
* same month and year as the forced date.
|
||||
*/
|
||||
const { forceRenderDate } = this;
|
||||
if (forceRenderDate !== undefined) {
|
||||
return { month: forceRenderDate.month, year: forceRenderDate.year, day: forceRenderDate.day };
|
||||
}
|
||||
|
||||
/**
|
||||
* From here, we can determine if the start
|
||||
* month or the end month was scrolled into view.
|
||||
@ -976,6 +946,10 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
|
||||
calendarBodyRef.style.removeProperty('overflow');
|
||||
|
||||
if (this.resolveForceDateScrolling) {
|
||||
this.resolveForceDateScrolling();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -1193,13 +1167,21 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
private processValue = (value?: string | string[] | null) => {
|
||||
const hasValue = value !== null && value !== undefined;
|
||||
const hasValue = value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
|
||||
const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
|
||||
|
||||
const { minParts, maxParts } = this;
|
||||
const { minParts, maxParts, workingParts, el } = this;
|
||||
|
||||
this.warnIfIncorrectValueUsage();
|
||||
|
||||
/**
|
||||
* Return early if the value wasn't parsed correctly, such as
|
||||
* if an improperly formatted date string was provided.
|
||||
*/
|
||||
if (!valueToProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Datetime should only warn of out of bounds values
|
||||
* if set by the user. If the `value` is undefined,
|
||||
@ -1218,19 +1200,11 @@ export class Datetime implements ComponentInterface {
|
||||
* that the values don't necessarily have to be in order.
|
||||
*/
|
||||
const singleValue = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess;
|
||||
const targetValue = clampDate(singleValue, minParts, maxParts);
|
||||
|
||||
const { month, day, year, hour, minute } = clampDate(singleValue, minParts, maxParts);
|
||||
const { month, day, year, hour, minute } = targetValue;
|
||||
const ampm = parseAmPm(hour!);
|
||||
|
||||
this.setWorkingParts({
|
||||
month,
|
||||
day,
|
||||
year,
|
||||
hour,
|
||||
minute,
|
||||
ampm,
|
||||
});
|
||||
|
||||
/**
|
||||
* Since `activeParts` indicates a value that
|
||||
* been explicitly selected either by the
|
||||
@ -1258,6 +1232,67 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
this.activeParts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Only animate if:
|
||||
* 1. We're using grid style (wheel style pickers should just jump to new value)
|
||||
* 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to)
|
||||
* 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example)
|
||||
* 4. The month/year picker is not open (since you wouldn't see the animation anyway)
|
||||
*/
|
||||
const didChangeMonth =
|
||||
(month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
|
||||
const bodyIsVisible = el.classList.contains('datetime-ready');
|
||||
const { isGridStyle, showMonthAndYear } = this;
|
||||
if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
|
||||
this.animateToDate(targetValue);
|
||||
} else {
|
||||
/**
|
||||
* We only need to do this if we didn't just animate to a new month,
|
||||
* since that calls prevMonth/nextMonth which calls setWorkingParts for us.
|
||||
*/
|
||||
this.setWorkingParts({
|
||||
month,
|
||||
day,
|
||||
year,
|
||||
hour,
|
||||
minute,
|
||||
ampm,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private animateToDate = async (targetValue: DatetimeParts) => {
|
||||
const { workingParts } = this;
|
||||
|
||||
/**
|
||||
* Tell other render functions that we need to force the
|
||||
* target month to appear in place of the actual next/prev month.
|
||||
* Because this is a State variable, a rerender will be triggered
|
||||
* automatically, updating the rendered months.
|
||||
*/
|
||||
this.forceRenderDate = targetValue;
|
||||
|
||||
/**
|
||||
* Flag that we've started scrolling to the forced date.
|
||||
* The resolve function will be called by the datetime's
|
||||
* scroll listener when it's done updating everything.
|
||||
* This is a replacement for making prev/nextMonth async,
|
||||
* since the logic we're waiting on is in a listener.
|
||||
*/
|
||||
const forceDateScrollingPromise = new Promise<void>((resolve) => {
|
||||
this.resolveForceDateScrolling = resolve;
|
||||
});
|
||||
|
||||
/**
|
||||
* Animate smoothly to the forced month. This will also update
|
||||
* workingParts and correct the surrounding months for us.
|
||||
*/
|
||||
const targetMonthIsBefore = isBefore(targetValue, workingParts);
|
||||
targetMonthIsBefore ? this.prevMonth() : this.nextMonth();
|
||||
await forceDateScrollingPromise;
|
||||
this.resolveForceDateScrolling = undefined;
|
||||
this.forceRenderDate = undefined;
|
||||
};
|
||||
|
||||
componentWillLoad() {
|
||||
@ -1286,16 +1321,18 @@ 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()));
|
||||
const todayParts = (this.todayParts = parseDate(getToday())!);
|
||||
this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
|
||||
|
||||
this.processMinParts();
|
||||
this.processMaxParts();
|
||||
|
||||
this.processValue(this.value);
|
||||
|
||||
this.emitStyle();
|
||||
@ -2042,7 +2079,7 @@ export class Datetime implements ComponentInterface {
|
||||
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
|
||||
this.locale,
|
||||
referenceParts,
|
||||
this.activePartsClone,
|
||||
this.activeParts,
|
||||
this.todayParts,
|
||||
this.minParts,
|
||||
this.maxParts,
|
||||
@ -2151,7 +2188,7 @@ export class Datetime implements ComponentInterface {
|
||||
private renderCalendarBody() {
|
||||
return (
|
||||
<div class="calendar-body ion-focusable" ref={(el) => (this.calendarBodyRef = el)} tabindex="0">
|
||||
{generateMonths(this.workingParts).map(({ month, year }) => {
|
||||
{generateMonths(this.workingParts, this.forceRenderDate).map(({ month, year }) => {
|
||||
return this.renderMonth(month, year);
|
||||
})}
|
||||
</div>
|
||||
@ -2360,7 +2397,19 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size } = this;
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
disabled,
|
||||
el,
|
||||
color,
|
||||
readonly,
|
||||
showMonthAndYear,
|
||||
preferWheel,
|
||||
presentation,
|
||||
size,
|
||||
isGridStyle,
|
||||
} = this;
|
||||
const mode = getIonMode(this);
|
||||
const isMonthAndYearPresentation =
|
||||
presentation === 'year' || presentation === 'month' || presentation === 'month-year';
|
||||
@ -2368,7 +2417,6 @@ export class Datetime implements ComponentInterface {
|
||||
const monthYearPickerOpen = showMonthAndYear && !isMonthAndYearPresentation;
|
||||
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
|
||||
const hasWheelVariant = hasDatePresentation && preferWheel;
|
||||
const hasGrid = hasDatePresentation && !preferWheel;
|
||||
|
||||
renderHiddenInput(true, el, name, formatValue(value), disabled);
|
||||
|
||||
@ -2387,7 +2435,7 @@ export class Datetime implements ComponentInterface {
|
||||
[`datetime-presentation-${presentation}`]: true,
|
||||
[`datetime-size-${size}`]: true,
|
||||
[`datetime-prefer-wheel`]: hasWheelVariant,
|
||||
[`datetime-grid`]: hasGrid,
|
||||
[`datetime-grid`]: isGridStyle,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
||||
@ -244,6 +244,30 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await ionChange.next();
|
||||
});
|
||||
|
||||
test('should jump to selected date when programmatically updating value', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2019-05-30"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2021-05-25T12:40:00.000Z'));
|
||||
await page.waitForChanges();
|
||||
|
||||
const selectedMonth = datetime.locator('.month-column .picker-item-active');
|
||||
const selectedDay = datetime.locator('.day-column .picker-item-active');
|
||||
const selectedYear = datetime.locator('.year-column .picker-item-active');
|
||||
|
||||
await expect(selectedMonth).toHaveText(/May/);
|
||||
await expect(selectedDay).toHaveText(/25/);
|
||||
await expect(selectedYear).toHaveText(/2021/);
|
||||
});
|
||||
|
||||
test.describe('datetime: date wheel localization', () => {
|
||||
test('should correctly localize the date data', async ({ page }) => {
|
||||
await page.setContent(
|
||||
|
||||
@ -15,6 +15,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const activeDate = page.locator('ion-datetime .calendar-day-active');
|
||||
await expect(activeDate).toHaveText('25');
|
||||
});
|
||||
|
||||
test('should update the active time when value is initially set', async ({ page }) => {
|
||||
await page.goto('/src/components/datetime/test/set-value', config);
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
@ -27,6 +28,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const activeDate = page.locator('ion-datetime .time-body');
|
||||
await expect(activeDate).toHaveText('12:40 PM');
|
||||
});
|
||||
|
||||
test('should update active item when value is not initially set', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
@ -39,23 +41,8 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const activeDayButton = page.locator('.calendar-day-active');
|
||||
const monthYearButton = page.locator('.calendar-month-year');
|
||||
const monthColumn = page.locator('.month-column');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2021-10-05'));
|
||||
|
||||
// Open month/year picker
|
||||
await monthYearButton.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Select October 2021
|
||||
// The year will automatically switch to 2021 when selecting 10
|
||||
await monthColumn.locator('.picker-item[data-value="10"]').click();
|
||||
await ionChange.next();
|
||||
|
||||
// Close month/year picker
|
||||
await monthYearButton.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Check that correct day is highlighted
|
||||
@ -63,5 +50,17 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await expect(activeDayButton).toHaveAttribute('data-month', '10');
|
||||
await expect(activeDayButton).toHaveAttribute('data-year', '2021');
|
||||
});
|
||||
|
||||
test('should scroll to new month when value is initially set and then updated', async ({ page }) => {
|
||||
await page.goto('/src/components/datetime/test/set-value', config);
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2021-05-25T12:40:00.000Z'));
|
||||
await page.waitForChanges();
|
||||
|
||||
const calendarHeader = datetime.locator('.calendar-month-year');
|
||||
await expect(calendarHeader).toHaveText(/May 2021/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -254,12 +254,23 @@ export const generateTime = (
|
||||
* Given DatetimeParts, generate the previous,
|
||||
* current, and and next months.
|
||||
*/
|
||||
export const generateMonths = (refParts: DatetimeParts): DatetimeParts[] => {
|
||||
return [
|
||||
getPreviousMonth(refParts),
|
||||
{ month: refParts.month, year: refParts.year, day: refParts.day },
|
||||
getNextMonth(refParts),
|
||||
];
|
||||
export const generateMonths = (refParts: DatetimeParts, forcedDate?: DatetimeParts): DatetimeParts[] => {
|
||||
const current = { month: refParts.month, year: refParts.year, day: refParts.day };
|
||||
|
||||
/**
|
||||
* If we're forcing a month to appear, and it's different from the current month,
|
||||
* ensure it appears by replacing the next or previous month as appropriate.
|
||||
*/
|
||||
if (forcedDate !== undefined && (refParts.month !== forcedDate.month || refParts.year !== forcedDate.year)) {
|
||||
const forced = { month: forcedDate.month, year: forcedDate.year, day: forcedDate.day };
|
||||
const forcedMonthIsBefore = isBefore(forced, current);
|
||||
|
||||
return forcedMonthIsBefore
|
||||
? [forced, current, getNextMonth(refParts)]
|
||||
: [getPreviousMonth(refParts), current, forced];
|
||||
}
|
||||
|
||||
return [getPreviousMonth(refParts), current, getNextMonth(refParts)];
|
||||
};
|
||||
|
||||
export const getMonthColumnData = (
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
|
||||
import type { DatetimeParts } from '../datetime-interface';
|
||||
|
||||
import { isAfter, isBefore } from './comparison';
|
||||
@ -56,14 +58,32 @@ export const getPartsFromCalendarDay = (el: HTMLElement): DatetimeParts => {
|
||||
* We do not use the JS Date object here because
|
||||
* it adjusts the date for the current timezone.
|
||||
*/
|
||||
export function parseDate(val: string): DatetimeParts;
|
||||
export function parseDate(val: string[]): DatetimeParts[];
|
||||
export function parseDate(val: string): DatetimeParts | undefined;
|
||||
export function parseDate(val: string[]): DatetimeParts[] | undefined;
|
||||
export function parseDate(val: undefined | null): undefined;
|
||||
export function parseDate(val: string | string[]): DatetimeParts | DatetimeParts[];
|
||||
export function parseDate(val: string | string[]): DatetimeParts | DatetimeParts[] | undefined;
|
||||
export function parseDate(val: string | string[] | undefined | null): DatetimeParts | DatetimeParts[] | undefined;
|
||||
export function parseDate(val: string | string[] | undefined | null): DatetimeParts | DatetimeParts[] | undefined {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((valStr) => parseDate(valStr));
|
||||
const parsedArray: DatetimeParts[] = [];
|
||||
for (const valStr of val) {
|
||||
const parsedVal = parseDate(valStr);
|
||||
|
||||
/**
|
||||
* If any of the values weren't parsed correctly, consider
|
||||
* the entire batch incorrect. This simplifies the type
|
||||
* signatures by having "undefined" be a general error case
|
||||
* instead of returning (Datetime | undefined)[], which is
|
||||
* harder for TS to perform type narrowing on.
|
||||
*/
|
||||
if (!parsedVal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
parsedArray.push(parsedVal);
|
||||
}
|
||||
|
||||
return parsedArray;
|
||||
}
|
||||
|
||||
// manually parse IS0 cuz Date.parse cannot be trusted
|
||||
@ -85,6 +105,7 @@ export function parseDate(val: string | string[] | undefined | null): DatetimePa
|
||||
|
||||
if (parse === null) {
|
||||
// wasn't able to parse the ISO datetime
|
||||
printIonWarning(`Unable to parse date string: ${val}. Please provide a valid ISO 8601 datetime string.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -132,8 +153,10 @@ export const parseAmPm = (hour: number) => {
|
||||
* For example, max="2012" would fill in the missing
|
||||
* month, day, hour, and minute information.
|
||||
*/
|
||||
export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeParts => {
|
||||
const { month, day, year, hour, minute } = parseDate(max);
|
||||
export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeParts | undefined => {
|
||||
const parsedMax = parseDate(max);
|
||||
if (!parsedMax) return;
|
||||
const { month, day, year, hour, minute } = parsedMax;
|
||||
|
||||
/**
|
||||
* When passing in `max` or `min`, developers
|
||||
@ -168,8 +191,10 @@ export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeP
|
||||
* For example, min="2012" would fill in the missing
|
||||
* month, day, hour, and minute information.
|
||||
*/
|
||||
export const parseMinParts = (min: string, todayParts: DatetimeParts): DatetimeParts => {
|
||||
const { month, day, year, hour, minute } = parseDate(min);
|
||||
export const parseMinParts = (min: string, todayParts: DatetimeParts): DatetimeParts | undefined => {
|
||||
const parsedMin = parseDate(min);
|
||||
if (!parsedMin) return;
|
||||
const { month, day, year, hour, minute } = parsedMin;
|
||||
|
||||
/**
|
||||
* When passing in `max` or `min`, developers
|
||||
|
||||
Reference in New Issue
Block a user