From ec3bc52ff194f1e4db4ce49548c1418c259b8795 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Wed, 24 Nov 2021 11:23:13 -0500 Subject: [PATCH] fix(datetime): update active calendar display when value changes (#24244) --- core/src/components/datetime/datetime.tsx | 59 ++++++++++++++----- .../components/datetime/test/set-value/e2e.ts | 37 ++++++++++++ .../datetime/test/set-value/index.html | 42 +++++++++++++ .../picker-column-internal.tsx | 19 ++++-- 4 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 core/src/components/datetime/test/set-value/e2e.ts create mode 100644 core/src/components/datetime/test/set-value/index.html diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 356cbc8ebd..fe1ece975f 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -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 ( , { + + 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'); + }) +}) + diff --git a/core/src/components/datetime/test/set-value/index.html b/core/src/components/datetime/test/set-value/index.html new file mode 100644 index 0000000000..4448abb706 --- /dev/null +++ b/core/src/components/datetime/test/set-value/index.html @@ -0,0 +1,42 @@ + + + + + + Datetime - Set Value + + + + + + + + + + + + + Datetime - Set Value + + + + + + + + + diff --git a/core/src/components/picker-column-internal/picker-column-internal.tsx b/core/src/components/picker-column-internal/picker-column-internal.tsx index 4168a765f5..efc4096d0c 100644 --- a/core/src/components/picker-column-internal/picker-column-internal.tsx +++ b/core/src/components/picker-column-internal/picker-column-internal.tsx @@ -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);