mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
fix(datetime): update active calendar display when value changes (#24244)
This commit is contained in:
@ -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"
|
||||
|
37
core/src/components/datetime/test/set-value/e2e.ts
Normal file
37
core/src/components/datetime/test/set-value/e2e.ts
Normal 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');
|
||||
})
|
||||
})
|
||||
|
42
core/src/components/datetime/test/set-value/index.html
Normal file
42
core/src/components/datetime/test/set-value/index.html
Normal 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>
|
@ -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,12 +65,18 @@ export class PickerColumnInternal implements ComponentInterface {
|
||||
|
||||
@Watch('value')
|
||||
valueChange() {
|
||||
const { items, value } = this;
|
||||
this.scrollActiveItemIntoView();
|
||||
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();
|
||||
|
||||
const findItem = items.find(item => item.value === value);
|
||||
if (findItem) {
|
||||
this.ionChange.emit(findItem);
|
||||
const findItem = items.find(item => item.value === value);
|
||||
if (findItem) {
|
||||
this.ionChange.emit(findItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
Reference in New Issue
Block a user