fix(datetime): update active calendar display when value changes (#24244)

This commit is contained in:
Sean Perkins
2021-11-24 11:23:13 -05:00
committed by GitHub
parent 36a096c9b6
commit ec3bc52ff1
4 changed files with 138 additions and 19 deletions

View File

@ -93,6 +93,12 @@ export class Datetime implements ComponentInterface {
private minParts?: any;
private maxParts?: any;
/**
* 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;
@State() showMonthAndYear = false;
@State() activeParts: DatetimeParts = {
@ -291,6 +297,25 @@ export class Datetime implements ComponentInterface {
*/
@Watch('value')
protected valueChanged() {
if (this.hasValue()) {
/**
* 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 { month, day, year, hour, minute } = parseDate(this.value);
this.activePartsClone = {
...this.activeParts,
month,
day,
year,
hour,
minute
}
}
this.emitStyle();
this.ionChange.emit({
value: this.value
@ -911,7 +936,8 @@ export class Datetime implements ComponentInterface {
tzOffset,
ampm: hour >= 12 ? 'pm' : 'am'
}
this.activeParts = {
this.activePartsClone = this.activeParts = {
month,
day,
year,
@ -951,6 +977,10 @@ export class Datetime implements ComponentInterface {
this.ionBlur.emit();
}
private hasValue = () => {
return this.value != null && this.value !== '';
}
private nextMonth = () => {
const { calendarBodyRef } = this;
if (!calendarBodyRef) { return; }
@ -1135,7 +1165,7 @@ export class Datetime implements ComponentInterface {
{getDaysOfMonth(month, year, this.firstDayOfWeek % 7).map((dateObject, index) => {
const { day, dayOfWeek } = dateObject;
const referenceParts = { month, day, year };
const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(this.locale, referenceParts, this.activeParts, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(this.locale, referenceParts, this.activePartsClone, this.todayParts, this.minParts, this.maxParts, this.parsedDayValues);
return (
<button
@ -1213,21 +1243,22 @@ export class Datetime implements ComponentInterface {
ampmItems: PickerColumnItem[],
use24Hour: boolean
) {
const { color, workingParts } = this;
const { color, activePartsClone, workingParts } = this;
return (
<ion-picker-internal>
<ion-picker-column-internal
color={color}
value={workingParts.hour}
value={activePartsClone.hour}
items={hoursItems}
numericInput
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
...workingParts,
hour: ev.detail.value
});
this.setActiveParts({
...this.activeParts,
...activePartsClone,
hour: ev.detail.value
});
@ -1236,16 +1267,16 @@ export class Datetime implements ComponentInterface {
></ion-picker-column-internal>
<ion-picker-column-internal
color={color}
value={workingParts.minute}
value={activePartsClone.minute}
items={minutesItems}
numericInput
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
...workingParts,
minute: ev.detail.value
});
this.setActiveParts({
...this.activeParts,
...activePartsClone,
minute: ev.detail.value
});
@ -1254,19 +1285,19 @@ export class Datetime implements ComponentInterface {
></ion-picker-column-internal>
{ !use24Hour && <ion-picker-column-internal
color={color}
value={workingParts.ampm}
value={activePartsClone.ampm}
items={ampmItems}
onIonChange={(ev: CustomEvent) => {
const hour = calculateHourFromAMPM(this.workingParts, ev.detail.value);
const hour = calculateHourFromAMPM(workingParts, ev.detail.value);
this.setWorkingParts({
...this.workingParts,
...workingParts,
ampm: ev.detail.value,
hour
});
this.setActiveParts({
...this.workingParts,
...activePartsClone,
ampm: ev.detail.value,
hour
});
@ -1309,7 +1340,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{getFormattedTime(this.workingParts, use24Hour)}
{getFormattedTime(this.activePartsClone, use24Hour)}
</button>,
<ion-popover
alignment="center"

View File

@ -0,0 +1,37 @@
import { newE2EPage } from '@stencil/core/testing';
describe('datetime: setting the value', () => {
it('should update the active date', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/set-value?ionic:_testing=true'
});
await page.$eval('ion-datetime', (elm: any) => {
elm.value = '2021-11-25T12:40:00.000Z';
});
await page.waitForChanges();
const activeDate = await page.find('ion-datetime >>> .calendar-day-active');
expect(activeDate).toEqualText('25');
});
it('should update the active time', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/set-value?ionic:_testing=true'
});
await page.$eval('ion-datetime', (elm: any) => {
elm.value = '2021-11-25T12:40:00.000Z';
});
await page.waitForChanges();
const activeTime = await page.find('ion-datetime >>> .time-body');
expect(activeTime).toEqualText('12:40 PM');
})
})

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Set Value</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
ion-datetime {
width: 350px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Set Value</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-datetime value="2021-11-22T14:23:00.000Z" locale="en-US"></ion-datetime>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -24,6 +24,7 @@ import { PickerColumnItem } from './picker-column-internal-interfaces';
export class PickerColumnInternal implements ComponentInterface {
private destroyScrollListener?: () => void;
private hapticsStarted = false;
private isColumnVisible = false;
@State() isActive = false;
@ -64,6 +65,11 @@ export class PickerColumnInternal implements ComponentInterface {
@Watch('value')
valueChange() {
if (this.isColumnVisible) {
/**
* Only scroll the active item into view and emit the value
* change, when the picker column is actively visible to the user.
*/
const { items, value } = this;
this.scrollActiveItemIntoView();
@ -72,6 +78,7 @@ export class PickerColumnInternal implements ComponentInterface {
this.ionChange.emit(findItem);
}
}
}
/**
* Only setup scroll listeners
@ -86,11 +93,13 @@ export class PickerColumnInternal implements ComponentInterface {
if (ev.isIntersecting) {
this.scrollActiveItemIntoView();
this.initializeScrollListener();
this.isColumnVisible = true;
} else {
if (this.destroyScrollListener) {
this.destroyScrollListener();
this.destroyScrollListener = undefined;
}
this.isColumnVisible = false;
}
}
new IntersectionObserver(visibleCallback, { threshold: 0.01 }).observe(this.el);