From 3b5d694e34d6698c9f3a5da83513b171c0bc7221 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 15:17:20 +0000 Subject: [PATCH 01/20] refactor(datetime): use scroll listener to detect month changes --- core/src/components/datetime/datetime.tsx | 193 +++++----------------- 1 file changed, 42 insertions(+), 151 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index ae07f5819f4..413dfc058d7 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -67,7 +67,6 @@ export class Datetime implements ComponentInterface { private calendarBodyRef?: HTMLElement; private popoverRef?: HTMLIonPopoverElement; private clearFocusVisible?: () => void; - private overlayIsPresenting = false; /** * Whether to highlight the active day with a solid circle (as opposed @@ -87,7 +86,6 @@ export class Datetime implements ComponentInterface { private destroyCalendarIO?: () => void; private destroyKeyboardMO?: () => void; - private destroyOverlayListener?: () => void; private minParts?: any; private maxParts?: any; @@ -725,8 +723,6 @@ export class Datetime implements ComponentInterface { return; } - const mode = getIonMode(this); - /** * For performance reasons, we only render 3 * months at a time: The current month, the previous @@ -755,36 +751,46 @@ export class Datetime implements ComponentInterface { * scrollIntoView() will scroll entire page * if element is not in viewport. Use scrollLeft instead. */ - let endIO: IntersectionObserver | undefined; - let startIO: IntersectionObserver | undefined; writeTask(() => { calendarBodyRef.scrollLeft = startMonth.clientWidth * (isRTL(this.el) ? -1 : 1); - const ioCallback = (callbackType: 'start' | 'end', entries: IntersectionObserverEntry[]) => { - const refIO = callbackType === 'start' ? startIO : endIO; - const refMonth = callbackType === 'start' ? startMonth : endMonth; - const refMonthFn = callbackType === 'start' ? getPreviousMonth : getNextMonth; + + const getChangedMonth = (parts: DatetimeParts): DatetimeParts | undefined => { + const box = calendarBodyRef.getBoundingClientRect(); + const root = this.el!.shadowRoot!; /** - * If the month is not fully in view, do not do anything + * Get the element that is in the center of the calendar body. + * This will be an element inside of the active month. */ - const ev = entries[0]; - if (!ev.isIntersecting) { - return; - } + const elementInMonth = root.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2); + if (!elementInMonth) return; /** - * When presenting an inline overlay, - * subsequent presentations will cause - * the IO to fire again (since the overlay - * is now visible and therefore the calendar - * months are intersecting). + * From here, we can determine if the start + * month or the end month was scrolled into view. + * If no month was changed, then we can return from + * the scroll callback early. */ - if (this.overlayIsPresenting) { - this.overlayIsPresenting = false; + const month = elementInMonth.closest('.calendar-month'); + + if (month === startMonth) { + return getPreviousMonth(parts) + } else if (month === endMonth) { + return getNextMonth(parts) + } else { return; } + } + + const scrollCallback = () => { + /** + * If the month did not change + * then we can return early. + */ + const newDate = getChangedMonth(this.workingParts); + if (!newDate) return; - const { month, year, day } = refMonthFn(this.workingParts); + const { month, day, year } = newDate; if ( isMonthDisabled( @@ -798,25 +804,6 @@ export class Datetime implements ComponentInterface { return; } - /** - * On iOS, we need to set pointer-events: none - * when the user is almost done with the gesture - * so that they cannot quickly swipe while - * the scrollable container is snapping. - * Updating the container while snapping - * causes WebKit to snap incorrectly. - */ - if (mode === 'ios') { - const ratio = ev.intersectionRatio; - // `maxTouchPoints` will be 1 in device preview, but > 1 on device - const shouldDisable = Math.abs(ratio - 0.7) <= 0.1 && navigator.maxTouchPoints > 1; - - if (shouldDisable) { - calendarBodyRef.style.setProperty('pointer-events', 'none'); - return; - } - } - /** * Prevent scrolling for other browsers * to give the DOM time to update and the container @@ -824,16 +811,6 @@ export class Datetime implements ComponentInterface { */ calendarBodyRef.style.setProperty('overflow', 'hidden'); - /** - * Remove the IO temporarily - * otherwise you can sometimes get duplicate - * events when rubber banding. - */ - if (refIO === undefined) { - return; - } - refIO.disconnect(); - /** * Use a writeTask here to ensure * that the state is updated and the @@ -844,86 +821,28 @@ export class Datetime implements ComponentInterface { * if we did not do this. */ writeTask(() => { - // Disconnect all active intersection observers - // to avoid a re-render causing a duplicate event. - if (this.destroyCalendarIO) { - this.destroyCalendarIO(); - } - - raf(() => { - this.setWorkingParts({ - ...this.workingParts, - month, - day: day!, - year, - }); - - calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1); - calendarBodyRef.style.removeProperty('overflow'); - calendarBodyRef.style.removeProperty('pointer-events'); - - endIO?.observe(endMonth); - startIO?.observe(startMonth); + this.setWorkingParts({ + ...this.workingParts, + month, + day: day!, + year, }); - /** - * Now that state has been updated - * and the correct month is in view, - * we can resume the IO. - */ - if (refIO === undefined) { - return; - } - refIO.observe(refMonth); + calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1); + calendarBodyRef.style.removeProperty('overflow'); }); }; - const threshold = - mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1 ? [0.7, 1] : 1; - - // Intersection observers cannot accurately detect the - // intersection with a threshold of 1, when the observed - // element width is a sub-pixel value (i.e. 334.05px). - // Setting a root margin to 1px solves the issue. - const rootMargin = '1px'; - /** - * Listen on the first month to - * prepend a new month and on the last - * month to append a new month. - * The 0.7 threshold is required on ios - * so that we can remove pointer-events - * when adding new months. - * Adding to a scroll snapping container - * while the container is snapping does not - * completely work as expected in WebKit. - * Adding pointer-events: none allows us to - * avoid these issues. - * - * This should be fine on Chromium, but - * when you set pointer-events: none - * it applies to active gestures which is not - * something WebKit does. + * When the container finishes scrolling we + * need to update the DOM with the selected month. */ + let scrollTimeout: ReturnType | undefined; + calendarBodyRef.addEventListener('scroll', () => { + if (scrollTimeout) { clearTimeout(scrollTimeout); } - endIO = new IntersectionObserver((ev) => ioCallback('end', ev), { - threshold, - root: calendarBodyRef, - rootMargin, - }); - endIO.observe(endMonth); - - startIO = new IntersectionObserver((ev) => ioCallback('start', ev), { - threshold, - root: calendarBodyRef, - rootMargin, + scrollTimeout = setTimeout(scrollCallback, 250); }); - startIO.observe(startMonth); - - this.destroyCalendarIO = () => { - endIO?.disconnect(); - startIO?.disconnect(); - }; }); }; @@ -958,7 +877,6 @@ export class Datetime implements ComponentInterface { private initializeListeners() { this.initializeCalendarIOListeners(); this.initializeKeyboardListeners(); - this.initializeOverlayListener(); } componentDidLoad() { @@ -1053,37 +971,10 @@ export class Datetime implements ComponentInterface { this.prevPresentation = presentation; this.destroyInteractionListeners(); - if (this.destroyOverlayListener !== undefined) { - this.destroyOverlayListener(); - } this.initializeListeners(); } - /** - * When doing subsequent presentations of an inline - * overlay, the IO callback will fire again causing - * the calendar to go back one month. We need to listen - * for the presentation of the overlay so we can properly - * cancel that IO callback. - */ - private initializeOverlayListener = () => { - const overlay = this.el.closest('ion-popover, ion-modal'); - if (overlay === null) { - return; - } - - const overlayListener = () => { - this.overlayIsPresenting = true; - }; - - overlay.addEventListener('willPresent', overlayListener); - - this.destroyOverlayListener = () => { - overlay.removeEventListener('willPresent', overlayListener); - }; - }; - private processValue = (value?: string | null) => { this.highlightActiveParts = !!value; const valueToProcess = parseDate(value || getToday()); From eaf0b87ec5d181565bb359da9178c71f990989bb Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 15:18:46 +0000 Subject: [PATCH 02/20] chore(): remove webkit workaround --- core/src/components/datetime/datetime.scss | 54 ++-------------------- 1 file changed, 5 insertions(+), 49 deletions(-) diff --git a/core/src/components/datetime/datetime.scss b/core/src/components/datetime/datetime.scss index e7ad2f6e0f0..ffe275067b6 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -102,55 +102,11 @@ display: flex; } -/** - * Safari 14 has an issue where Intersection - * Observer is incorrectly fired when - * unhiding the calendar content. - * To workaround this, we set the opacity - * of the content to 0 and hide it offscreen. - * - * -webkit-named-image is something only WebKit supports - * so we use this to detect general WebKit support. - * aspect-ratio is only supported in Safari 15+ - * so by checking lack of aspect-ratio support, we know - * that we are in a pre-Safari 15 browser. - * - * TODO(FW-554): Remove when iOS 14 support is dropped. - */ -@supports (background: -webkit-named-image(apple-pay-logo-black)) and (not (aspect-ratio: 1/1)) { - :host(.show-month-and-year) .calendar-next-prev, - :host(.show-month-and-year) .calendar-days-of-week, - :host(.show-month-and-year) .calendar-body, - :host(.show-month-and-year) .datetime-time { - @include position(null, null, null, -99999px); - - position: absolute; - - /** - * Use visibility instead of - * opacity to ensure element - * cannot receive focus - */ - visibility: hidden; - pointer-events: none; - } -} - -/** - * This support check two cases: - * 1. A WebKit browser that supports aspect-ratio (Safari 15+) - * 2. Any non-WebKit browser. - * Note that just overriding this display: none is not - * sufficient to resolve the issue mentioned above, which - * is why we do another set of @supports checks. - */ -@supports (not (background: -webkit-named-image(apple-pay-logo-black))) or ((background: -webkit-named-image(apple-pay-logo-black)) and (aspect-ratio: 1/1)) { - :host(.show-month-and-year) .calendar-next-prev, - :host(.show-month-and-year) .calendar-days-of-week, - :host(.show-month-and-year) .calendar-body, - :host(.show-month-and-year) .datetime-time { - display: none; - } +:host(.show-month-and-year) .calendar-next-prev, +:host(.show-month-and-year) .calendar-days-of-week, +:host(.show-month-and-year) .calendar-body, +:host(.show-month-and-year) .datetime-time { + display: none; } :host(.month-year-picker-open) .datetime-footer { From 418407eed0755587bce9e403706c9ff123691302 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 15:20:18 +0000 Subject: [PATCH 03/20] chore(): lint --- core/src/components/datetime/datetime.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 413dfc058d7..4a3f7efd9a5 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -774,13 +774,13 @@ export class Datetime implements ComponentInterface { const month = elementInMonth.closest('.calendar-month'); if (month === startMonth) { - return getPreviousMonth(parts) + return getPreviousMonth(parts); } else if (month === endMonth) { - return getNextMonth(parts) + return getNextMonth(parts); } else { return; } - } + }; const scrollCallback = () => { /** @@ -839,7 +839,9 @@ export class Datetime implements ComponentInterface { */ let scrollTimeout: ReturnType | undefined; calendarBodyRef.addEventListener('scroll', () => { - if (scrollTimeout) { clearTimeout(scrollTimeout); } + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } scrollTimeout = setTimeout(scrollCallback, 250); }); From 30fda4570038bb26497511dde9b9dd6b1cfbcffc Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 15:20:40 +0000 Subject: [PATCH 04/20] chore(): tweak timeout --- core/src/components/datetime/datetime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 4a3f7efd9a5..4f5be8b02df 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -843,7 +843,7 @@ export class Datetime implements ComponentInterface { clearTimeout(scrollTimeout); } - scrollTimeout = setTimeout(scrollCallback, 250); + scrollTimeout = setTimeout(scrollCallback, 150); }); }); }; From 96ef7ab7c157492b2a1851b418920c0ab97f36e4 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 14:59:57 -0400 Subject: [PATCH 05/20] Revert "chore(): remove webkit workaround" This reverts commit eaf0b87ec5d181565bb359da9178c71f990989bb. --- core/src/components/datetime/datetime.scss | 54 ++++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/core/src/components/datetime/datetime.scss b/core/src/components/datetime/datetime.scss index ffe275067b6..e7ad2f6e0f0 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -102,11 +102,55 @@ display: flex; } -:host(.show-month-and-year) .calendar-next-prev, -:host(.show-month-and-year) .calendar-days-of-week, -:host(.show-month-and-year) .calendar-body, -:host(.show-month-and-year) .datetime-time { - display: none; +/** + * Safari 14 has an issue where Intersection + * Observer is incorrectly fired when + * unhiding the calendar content. + * To workaround this, we set the opacity + * of the content to 0 and hide it offscreen. + * + * -webkit-named-image is something only WebKit supports + * so we use this to detect general WebKit support. + * aspect-ratio is only supported in Safari 15+ + * so by checking lack of aspect-ratio support, we know + * that we are in a pre-Safari 15 browser. + * + * TODO(FW-554): Remove when iOS 14 support is dropped. + */ +@supports (background: -webkit-named-image(apple-pay-logo-black)) and (not (aspect-ratio: 1/1)) { + :host(.show-month-and-year) .calendar-next-prev, + :host(.show-month-and-year) .calendar-days-of-week, + :host(.show-month-and-year) .calendar-body, + :host(.show-month-and-year) .datetime-time { + @include position(null, null, null, -99999px); + + position: absolute; + + /** + * Use visibility instead of + * opacity to ensure element + * cannot receive focus + */ + visibility: hidden; + pointer-events: none; + } +} + +/** + * This support check two cases: + * 1. A WebKit browser that supports aspect-ratio (Safari 15+) + * 2. Any non-WebKit browser. + * Note that just overriding this display: none is not + * sufficient to resolve the issue mentioned above, which + * is why we do another set of @supports checks. + */ +@supports (not (background: -webkit-named-image(apple-pay-logo-black))) or ((background: -webkit-named-image(apple-pay-logo-black)) and (aspect-ratio: 1/1)) { + :host(.show-month-and-year) .calendar-next-prev, + :host(.show-month-and-year) .calendar-days-of-week, + :host(.show-month-and-year) .calendar-body, + :host(.show-month-and-year) .datetime-time { + display: none; + } } :host(.month-year-picker-open) .datetime-footer { From 886c77185a399b7d3455e9a1871a4f5d8698643c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 19:00:22 +0000 Subject: [PATCH 06/20] chore(): update comment --- core/src/components/datetime/datetime.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/components/datetime/datetime.scss b/core/src/components/datetime/datetime.scss index e7ad2f6e0f0..3d70d71e62f 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -103,9 +103,8 @@ } /** - * Safari 14 has an issue where Intersection - * Observer is incorrectly fired when - * unhiding the calendar content. + * Safari 14 has an issue where a scroll event + * is incorrectly fired when unhiding the calendar content. * To workaround this, we set the opacity * of the content to 0 and hide it offscreen. * From 53a167e28ecea156214de3e17182524a824b0b09 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 19:12:33 +0000 Subject: [PATCH 07/20] chore(): fix wording --- core/src/components/datetime/datetime.tsx | 45 +++++++++++++---------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 4f5be8b02df..a63ee5a6bf0 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -84,7 +84,7 @@ export class Datetime implements ComponentInterface { private parsedYearValues?: number[]; private parsedDayValues?: number[]; - private destroyCalendarIO?: () => void; + private destroyCalendarListener?: () => void; private destroyKeyboardMO?: () => void; private minParts?: any; @@ -717,7 +717,7 @@ export class Datetime implements ComponentInterface { }; }; - private initializeCalendarIOListeners = () => { + private initializeCalendarListener = () => { const calendarBodyRef = this.getCalendarBodyEl(); if (!calendarBodyRef) { return; @@ -782,7 +782,7 @@ export class Datetime implements ComponentInterface { } }; - const scrollCallback = () => { + const updateActiveMonth = () => { /** * If the month did not change * then we can return early. @@ -838,13 +838,18 @@ export class Datetime implements ComponentInterface { * need to update the DOM with the selected month. */ let scrollTimeout: ReturnType | undefined; - calendarBodyRef.addEventListener('scroll', () => { + const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); } - scrollTimeout = setTimeout(scrollCallback, 150); - }); + scrollTimeout = setTimeout(updateActiveMonth, 150); + } + calendarBodyRef.addEventListener('scroll', scrollCallback); + + this.destroyCalendarListener = () => { + calendarBodyRef.removeEventListener('scroll', scrollCallback); + } }); }; @@ -865,10 +870,10 @@ export class Datetime implements ComponentInterface { * if the datetime has been hidden/presented by a modal or popover. */ private destroyInteractionListeners = () => { - const { destroyCalendarIO, destroyKeyboardMO } = this; + const { destroyCalendarListener, destroyKeyboardMO } = this; - if (destroyCalendarIO !== undefined) { - destroyCalendarIO(); + if (destroyCalendarListener !== undefined) { + destroyCalendarListener(); } if (destroyKeyboardMO !== undefined) { @@ -877,7 +882,7 @@ export class Datetime implements ComponentInterface { }; private initializeListeners() { - this.initializeCalendarIOListeners(); + this.initializeCalendarListener(); this.initializeKeyboardListeners(); } @@ -1168,10 +1173,10 @@ export class Datetime implements ComponentInterface { value={workingParts.month} onIonChange={(ev: CustomEvent) => { // Due to a Safari 14 issue we need to destroy - // the intersection observer before we update state + // the scroll listener before we update state // and trigger a re-render. - if (this.destroyCalendarIO) { - this.destroyCalendarIO(); + if (this.destroyCalendarListener) { + this.destroyCalendarListener(); } this.setWorkingParts({ @@ -1186,9 +1191,9 @@ export class Datetime implements ComponentInterface { }); } - // We can re-attach the intersection observer after + // We can re-attach the scroll listener after // the working parts have been updated. - this.initializeCalendarIOListeners(); + this.initializeCalendarListener(); ev.stopPropagation(); }} @@ -1202,10 +1207,10 @@ export class Datetime implements ComponentInterface { value={workingParts.year} onIonChange={(ev: CustomEvent) => { // Due to a Safari 14 issue we need to destroy - // the intersection observer before we update state + // the scroll listener before we update state // and trigger a re-render. - if (this.destroyCalendarIO) { - this.destroyCalendarIO(); + if (this.destroyCalendarListener) { + this.destroyCalendarListener(); } this.setWorkingParts({ @@ -1220,9 +1225,9 @@ export class Datetime implements ComponentInterface { }); } - // We can re-attach the intersection observer after + // We can re-attach the scroll listener after // the working parts have been updated. - this.initializeCalendarIOListeners(); + this.initializeCalendarListener(); ev.stopPropagation(); }} From d872abb4ce38d8780623fc11d34ad869a58d6e58 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 19:16:21 +0000 Subject: [PATCH 08/20] chore(): add todo --- core/src/components/datetime/datetime.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index a63ee5a6bf0..8c545be8e40 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1172,6 +1172,7 @@ export class Datetime implements ComponentInterface { items={months} value={workingParts.month} onIonChange={(ev: CustomEvent) => { + // TODO(FW-1823) Remove this when iOS 14 support is dropped. // Due to a Safari 14 issue we need to destroy // the scroll listener before we update state // and trigger a re-render. @@ -1206,6 +1207,7 @@ export class Datetime implements ComponentInterface { items={years} value={workingParts.year} onIonChange={(ev: CustomEvent) => { + // TODO(FW-1823) Remove this when iOS 14 support is dropped. // Due to a Safari 14 issue we need to destroy // the scroll listener before we update state // and trigger a re-render. From b31b52b1b42bcc1d7eff7e28909f29978c29ab5c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 19:24:51 +0000 Subject: [PATCH 09/20] chore(): lint --- core/src/components/datetime/datetime.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 8c545be8e40..ef12f502866 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -844,12 +844,12 @@ export class Datetime implements ComponentInterface { } scrollTimeout = setTimeout(updateActiveMonth, 150); - } + }; calendarBodyRef.addEventListener('scroll', scrollCallback); this.destroyCalendarListener = () => { calendarBodyRef.removeEventListener('scroll', scrollCallback); - } + }; }); }; From a644f952526b723876bb15ede42949d2433c65f1 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 21:08:45 +0000 Subject: [PATCH 10/20] fix(datetime): apply snapping fix for ios --- core/src/components/datetime/datetime.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index ef12f502866..f78d46fae89 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -783,6 +783,9 @@ export class Datetime implements ComponentInterface { }; const updateActiveMonth = () => { + scrollTimeout = undefined; + calendarBodyRef.style.removeProperty('pointer-events'); + /** * If the month did not change * then we can return early. @@ -838,12 +841,29 @@ export class Datetime implements ComponentInterface { * need to update the DOM with the selected month. */ let scrollTimeout: ReturnType | undefined; + const mode = getIonMode(this); + const needsiOSRubberBandFix = mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1; const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); } - scrollTimeout = setTimeout(updateActiveMonth, 150); + /** + * On iOS it is possible to quickly rubber band + * the scroll area before the scroll timeout has fired. + * This results in users reaching the end of the scrollable + * container before the DOM has updated. + * By setting `touch-action: none` we can ensure that + * subsequent swipes do not happen while the container + * is snapping. + */ + if (needsiOSRubberBandFix) { + raf(() => { + calendarBodyRef.style.setProperty('pointer-events', 'none'); + }); + } + + scrollTimeout = setTimeout(updateActiveMonth, 50); }; calendarBodyRef.addEventListener('scroll', scrollCallback); From 9f213d16458f2292a6cd47698c9fe4eea7cb1c48 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 21:10:23 +0000 Subject: [PATCH 11/20] chore(): clean up --- core/src/components/datetime/datetime.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index f78d46fae89..ec0e7cb0eb0 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -744,6 +744,8 @@ export class Datetime implements ComponentInterface { const startMonth = months[0] as HTMLElement; const workingMonth = months[1] as HTMLElement; const endMonth = months[2] as HTMLElement; + const mode = getIonMode(this); + const needsiOSRubberBandFix = mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1; /** * Before setting up the IntersectionObserver, @@ -783,8 +785,9 @@ export class Datetime implements ComponentInterface { }; const updateActiveMonth = () => { - scrollTimeout = undefined; - calendarBodyRef.style.removeProperty('pointer-events'); + if (needsiOSRubberBandFix) { + calendarBodyRef.style.removeProperty('pointer-events'); + } /** * If the month did not change @@ -841,8 +844,6 @@ export class Datetime implements ComponentInterface { * need to update the DOM with the selected month. */ let scrollTimeout: ReturnType | undefined; - const mode = getIonMode(this); - const needsiOSRubberBandFix = mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1; const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); @@ -863,7 +864,7 @@ export class Datetime implements ComponentInterface { }); } - scrollTimeout = setTimeout(updateActiveMonth, 50); + scrollTimeout = setTimeout(updateActiveMonth, 150); }; calendarBodyRef.addEventListener('scroll', scrollCallback); From e87983770b52eb02f14a40fa38fa9371123a16e3 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 7 Jul 2022 21:14:53 +0000 Subject: [PATCH 12/20] chore(): make timeout a bit faster --- core/src/components/datetime/datetime.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index ec0e7cb0eb0..32c282a79a2 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -864,7 +864,8 @@ export class Datetime implements ComponentInterface { }); } - scrollTimeout = setTimeout(updateActiveMonth, 150); + // Wait ~3 frames + scrollTimeout = setTimeout(updateActiveMonth, 50); }; calendarBodyRef.addEventListener('scroll', scrollCallback); From ce88463fd79aa8171a1bc80ad427ae7e55cf030a Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 8 Jul 2022 16:54:18 +0000 Subject: [PATCH 13/20] perf(datetime): avoid unnecessary work on ios --- core/src/components/datetime/datetime.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 32c282a79a2..d37efb42a2d 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -787,6 +787,7 @@ export class Datetime implements ComponentInterface { const updateActiveMonth = () => { if (needsiOSRubberBandFix) { calendarBodyRef.style.removeProperty('pointer-events'); + appliediOSRubberBandFix = false; } /** @@ -844,6 +845,13 @@ export class Datetime implements ComponentInterface { * need to update the DOM with the selected month. */ let scrollTimeout: ReturnType | undefined; + + /** + * We do not want to attempt to set pointer-events + * multiple times within a single swipe gesture as + * that adds unnecessary work to the main thread. + */ + let appliediOSRubberBandFix = false; const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); @@ -854,14 +862,13 @@ export class Datetime implements ComponentInterface { * the scroll area before the scroll timeout has fired. * This results in users reaching the end of the scrollable * container before the DOM has updated. - * By setting `touch-action: none` we can ensure that + * By setting `pointer-events: none` we can ensure that * subsequent swipes do not happen while the container * is snapping. */ - if (needsiOSRubberBandFix) { - raf(() => { - calendarBodyRef.style.setProperty('pointer-events', 'none'); - }); + if (!appliediOSRubberBandFix && needsiOSRubberBandFix) { + calendarBodyRef.style.setProperty('pointer-events', 'none'); + appliediOSRubberBandFix = true; } // Wait ~3 frames From 47696a607f2e8b5ce45af5c0282bb7d410fa9055 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 12 Jul 2022 09:11:13 -0400 Subject: [PATCH 14/20] Update datetime.tsx --- core/src/components/datetime/datetime.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index d37efb42a2d..5e8ce1a655d 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -726,9 +726,8 @@ export class Datetime implements ComponentInterface { /** * For performance reasons, we only render 3 * months at a time: The current month, the previous - * month, and the next month. We have IntersectionObservers - * on the previous and next month elements to append/prepend - * new months. + * month, and the next month. We have a scroll listener + * on the calendar body to append/prepend new months. * * We can do this because Stencil is smart enough to not * re-create the .calendar-month containers, but rather From 7c65469feab92c45e4309c69924e60c4127af10b Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 12 Jul 2022 09:45:53 -0400 Subject: [PATCH 15/20] Update datetime.tsx --- core/src/components/datetime/datetime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 5e8ce1a655d..7bedf42ffc2 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -747,7 +747,7 @@ export class Datetime implements ComponentInterface { const needsiOSRubberBandFix = mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1; /** - * Before setting up the IntersectionObserver, + * Before setting up the scroll listener, * scroll the middle month into view. * scrollIntoView() will scroll entire page * if element is not in viewport. Use scrollLeft instead. From 6538ad486b1afe96007a6320207e27ce4fb197d6 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 14 Jul 2022 14:14:17 -0400 Subject: [PATCH 16/20] remove unneeded tests --- .../test/sub-pixel-width/datetime.e2e.ts | 45 --------------- .../datetime/test/sub-pixel-width/index.html | 53 ------------------ .../test/zoom/datetime-zoom-in.e2e.ts | 55 ------------------- .../test/zoom/datetime-zoom-out.e2e.ts | 55 ------------------- .../components/datetime/test/zoom/index.html | 48 ---------------- 5 files changed, 256 deletions(-) delete mode 100644 core/src/components/datetime/test/sub-pixel-width/datetime.e2e.ts delete mode 100644 core/src/components/datetime/test/sub-pixel-width/index.html delete mode 100644 core/src/components/datetime/test/zoom/datetime-zoom-in.e2e.ts delete mode 100644 core/src/components/datetime/test/zoom/datetime-zoom-out.e2e.ts delete mode 100644 core/src/components/datetime/test/zoom/index.html diff --git a/core/src/components/datetime/test/sub-pixel-width/datetime.e2e.ts b/core/src/components/datetime/test/sub-pixel-width/datetime.e2e.ts deleted file mode 100644 index de79e99ffcf..00000000000 --- a/core/src/components/datetime/test/sub-pixel-width/datetime.e2e.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { expect } from '@playwright/test'; -import { test } from '@utils/test/playwright'; - -test.describe('datetime: sub-pixel width', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/src/components/datetime/test/sub-pixel-width'); - }); - test('should update the month when next button is clicked', async ({ page }) => { - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - const openModalBtn = page.locator('#open-modal'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - await page.waitForSelector('.datetime-ready'); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - await buttons.nth(1).click(); - - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('March 2022'); - }); - - test('should update the month when prev button is clicked', async ({ page }) => { - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - const openModalBtn = page.locator('#open-modal'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - await page.waitForSelector('.datetime-ready'); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - await buttons.nth(0).click(); - - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('January 2022'); - }); -}); diff --git a/core/src/components/datetime/test/sub-pixel-width/index.html b/core/src/components/datetime/test/sub-pixel-width/index.html deleted file mode 100644 index 87e58f328d8..00000000000 --- a/core/src/components/datetime/test/sub-pixel-width/index.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - Datetime - Sub Pixel Width - - - - - - - - - - - - - Datetime - Sub Pixel Width - - - -

Modal

- Present Modal - -
- -
-
-
-
- - - - diff --git a/core/src/components/datetime/test/zoom/datetime-zoom-in.e2e.ts b/core/src/components/datetime/test/zoom/datetime-zoom-in.e2e.ts deleted file mode 100644 index 08cccc1ba30..00000000000 --- a/core/src/components/datetime/test/zoom/datetime-zoom-in.e2e.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect } from '@playwright/test'; -import { test } from '@utils/test/playwright'; - -test.use({ - viewport: { - width: 640, - height: 480, - }, - deviceScaleFactor: 2, -}); - -/** - * This test emulates zoom behavior in the browser to make sure - * that key functions of the ion-datetime continue to function even - * if the page is zoomed in or out. - */ -test.describe('datetime: zoom in interactivity', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/src/components/datetime/test/zoom'); - }); - - test('should update the month when next button is clicked', async ({ page }) => { - const openModalBtn = page.locator('#open-modal'); - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - - await buttons.nth(1).click(); - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('March 2022'); - }); - - test('should update the month when prev button is clicked', async ({ page }) => { - const openModalBtn = page.locator('#open-modal'); - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - - await buttons.nth(0).click(); - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('January 2022'); - }); -}); diff --git a/core/src/components/datetime/test/zoom/datetime-zoom-out.e2e.ts b/core/src/components/datetime/test/zoom/datetime-zoom-out.e2e.ts deleted file mode 100644 index c4e131417ba..00000000000 --- a/core/src/components/datetime/test/zoom/datetime-zoom-out.e2e.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect } from '@playwright/test'; -import { test } from '@utils/test/playwright'; - -test.use({ - viewport: { - width: 640, - height: 480, - }, - deviceScaleFactor: 0.75, -}); - -/** - * This test emulates zoom behavior in the browser to make sure - * that key functions of the ion-datetime continue to function even - * if the page is zoomed in or out. - */ -test.describe('datetime: zoom out interactivity', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/src/components/datetime/test/zoom'); - }); - - test('should update the month when next button is clicked', async ({ page }) => { - const openModalBtn = page.locator('#open-modal'); - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - - await buttons.nth(1).click(); - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('March 2022'); - }); - - test('should update the month when prev button is clicked', async ({ page }) => { - const openModalBtn = page.locator('#open-modal'); - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange'); - - await openModalBtn.click(); - await ionModalDidPresent.next(); - - const buttons = page.locator('ion-datetime .calendar-next-prev ion-button'); - - await buttons.nth(0).click(); - await datetimeMonthDidChange.next(); - - const monthYear = page.locator('ion-datetime .calendar-month-year'); - await expect(monthYear).toHaveText('January 2022'); - }); -}); diff --git a/core/src/components/datetime/test/zoom/index.html b/core/src/components/datetime/test/zoom/index.html deleted file mode 100644 index 087b4598787..00000000000 --- a/core/src/components/datetime/test/zoom/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - Datetime - Zoom - - - - - - - - - - - - - Datetime - Zoom - - - -

Modal

- Present Modal - -
- -
-
-
-
- - - - From 8e4b234536bbba811f1363963a300a248461c5ad Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 14 Jul 2022 19:03:41 +0000 Subject: [PATCH 17/20] fix(datetime): do not update dom if swipe in progress --- core/src/components/datetime/datetime.tsx | 61 ++++++++++++++--- .../datetime/test/basic/datetime.e2e.ts | 66 +++++++++++++++++++ 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 7bedf42ffc2..516b235bb55 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -760,11 +760,25 @@ export class Datetime implements ComponentInterface { const root = this.el!.shadowRoot!; /** - * Get the element that is in the center of the calendar body. - * This will be an element inside of the active month. + * Check the element on the left and right edges + * of the body. If the two elements are not the same + * then the user is partially swiped between two + * months. We should wait until they finish + * swiping in order to update the month. */ - const elementInMonth = root.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2); - if (!elementInMonth) return; + const elementOnLeft = root.elementFromPoint(box.x + 5, box.y + box.height / 2); + const elementOnRight = root.elementFromPoint(box.x + box.width - 5, box.y + box.height / 2); + + /** + * If there are no elements then the + * component may be re-rendering on a slow device. + */ + if (!elementOnLeft || !elementOnRight) return; + + const leftMonth = elementOnLeft.closest('.calendar-month'); + const rightMonth = elementOnRight.closest('.calendar-month'); + + if (leftMonth !== rightMonth) return; /** * From here, we can determine if the start @@ -772,11 +786,9 @@ export class Datetime implements ComponentInterface { * If no month was changed, then we can return from * the scroll callback early. */ - const month = elementInMonth.closest('.calendar-month'); - - if (month === startMonth) { + if (leftMonth === startMonth) { return getPreviousMonth(parts); - } else if (month === endMonth) { + } else if (leftMonth === endMonth) { return getNextMonth(parts); } else { return; @@ -784,6 +796,18 @@ export class Datetime implements ComponentInterface { }; const updateActiveMonth = () => { + /** + * If user is actively touching + * the screen, we need to wait + * for both the gesture to end + * and for snapping to finish + * otherwise the component will + * re-render mid gesture. + */ + if (isTouching) { + return; + } + if (needsiOSRubberBandFix) { calendarBodyRef.style.removeProperty('pointer-events'); appliediOSRubberBandFix = false; @@ -851,6 +875,16 @@ export class Datetime implements ComponentInterface { * that adds unnecessary work to the main thread. */ let appliediOSRubberBandFix = false; + + /** + * If user stops scrolling but is still + * actively touching the screen, they may + * decide to scroll again. In that case, + * we should wait for both the scrolling + * and touch gesture to end before + * updating the working parts. + */ + let isTouching = false; const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); @@ -873,9 +907,20 @@ export class Datetime implements ComponentInterface { // Wait ~3 frames scrollTimeout = setTimeout(updateActiveMonth, 50); }; + + const setIsTouching = () => { + isTouching = true; + }; + const clearIsTouching = () => { + isTouching = false; + }; + calendarBodyRef.addEventListener('touchstart', setIsTouching); + calendarBodyRef.addEventListener('touchend', clearIsTouching); calendarBodyRef.addEventListener('scroll', scrollCallback); this.destroyCalendarListener = () => { + calendarBodyRef.removeEventListener('touchstart', setIsTouching); + calendarBodyRef.removeEventListener('touchend', clearIsTouching); calendarBodyRef.removeEventListener('scroll', scrollCallback); }; }); diff --git a/core/src/components/datetime/test/basic/datetime.e2e.ts b/core/src/components/datetime/test/basic/datetime.e2e.ts index cf11c992d54..5f4a41fafe7 100644 --- a/core/src/components/datetime/test/basic/datetime.e2e.ts +++ b/core/src/components/datetime/test/basic/datetime.e2e.ts @@ -183,3 +183,69 @@ test.describe('datetime: footer', () => { ); }); }); + +test.describe('datetime: swiping', () => { + // eslint-disable-next-line no-empty-pattern + test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.metadata.rtl === true, 'This does not test LTR vs RTL layouts.'); + test.skip(testInfo.project.metadata.mode === 'ios', 'This does not have mode-specific logic.'); + }); + test('should move to prev month by swiping', async ({ page }) => { + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const calendarBody = page.locator('ion-datetime .calendar-body'); + const calendarHeader = page.locator('ion-datetime .calendar-month-year'); + + await expect(calendarHeader).toHaveText(/May 2022/); + + await calendarBody.evaluate((el: HTMLElement) => (el.scrollLeft = 0)); + await page.waitForChanges(); + + await expect(calendarHeader).toHaveText(/April 2022/); + }); + test('should move to next month by swiping', async ({ page }) => { + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const calendarBody = page.locator('ion-datetime .calendar-body'); + const calendarHeader = page.locator('ion-datetime .calendar-month-year'); + + await expect(calendarHeader).toHaveText(/May 2022/); + + await calendarBody.evaluate((el: HTMLElement) => (el.scrollLeft = el.scrollWidth)); + await page.waitForChanges(); + + await expect(calendarHeader).toHaveText(/June 2022/); + }); + test('should not re-render if swipe is in progress', async ({ page, browserName }) => { + test.skip(browserName === 'webkit', 'Wheel is not available in WebKit'); + + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const calendarBody = page.locator('ion-datetime .calendar-body'); + const calendarHeader = page.locator('ion-datetime .calendar-month-year'); + + await expect(calendarHeader).toHaveText(/May 2022/); + + const box = await calendarBody.boundingBox(); + + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.wheel(-50, 0); + await page.waitForChanges(); + + await expect(calendarHeader).toHaveText(/May 2022/); + } + }); +}); From fd814adc596eb9567a59088058ac67e22a693b31 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 14 Jul 2022 19:04:08 +0000 Subject: [PATCH 18/20] chore(): add comment --- core/src/components/datetime/datetime.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 516b235bb55..f6b37ae584d 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -765,6 +765,7 @@ export class Datetime implements ComponentInterface { * then the user is partially swiped between two * months. We should wait until they finish * swiping in order to update the month. + * A 5px margin allows us to avoid sub-pixel rendering quirks. */ const elementOnLeft = root.elementFromPoint(box.x + 5, box.y + box.height / 2); const elementOnRight = root.elementFromPoint(box.x + box.width - 5, box.y + box.height / 2); From ce571dc9433d518b9eaf02ecaf0d189a102723a5 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 14 Jul 2022 19:31:47 +0000 Subject: [PATCH 19/20] chore(): remove old logic --- core/src/components/datetime/datetime.tsx | 34 ++--------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index f6b37ae584d..61318ddae4b 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -779,6 +779,8 @@ export class Datetime implements ComponentInterface { const leftMonth = elementOnLeft.closest('.calendar-month'); const rightMonth = elementOnRight.closest('.calendar-month'); + console.log('change', leftMonth, rightMonth); + if (leftMonth !== rightMonth) return; /** @@ -797,18 +799,6 @@ export class Datetime implements ComponentInterface { }; const updateActiveMonth = () => { - /** - * If user is actively touching - * the screen, we need to wait - * for both the gesture to end - * and for snapping to finish - * otherwise the component will - * re-render mid gesture. - */ - if (isTouching) { - return; - } - if (needsiOSRubberBandFix) { calendarBodyRef.style.removeProperty('pointer-events'); appliediOSRubberBandFix = false; @@ -876,16 +866,6 @@ export class Datetime implements ComponentInterface { * that adds unnecessary work to the main thread. */ let appliediOSRubberBandFix = false; - - /** - * If user stops scrolling but is still - * actively touching the screen, they may - * decide to scroll again. In that case, - * we should wait for both the scrolling - * and touch gesture to end before - * updating the working parts. - */ - let isTouching = false; const scrollCallback = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); @@ -909,19 +889,9 @@ export class Datetime implements ComponentInterface { scrollTimeout = setTimeout(updateActiveMonth, 50); }; - const setIsTouching = () => { - isTouching = true; - }; - const clearIsTouching = () => { - isTouching = false; - }; - calendarBodyRef.addEventListener('touchstart', setIsTouching); - calendarBodyRef.addEventListener('touchend', clearIsTouching); calendarBodyRef.addEventListener('scroll', scrollCallback); this.destroyCalendarListener = () => { - calendarBodyRef.removeEventListener('touchstart', setIsTouching); - calendarBodyRef.removeEventListener('touchend', clearIsTouching); calendarBodyRef.removeEventListener('scroll', scrollCallback); }; }); From 4ce6f41344e6e458b434c02fbfce139bfd67b206 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 14 Jul 2022 19:58:59 +0000 Subject: [PATCH 20/20] fix(datetime): improve patch for zoomed pages --- core/src/components/datetime/datetime.tsx | 39 ++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 61318ddae4b..86b9b7ca4ee 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -760,28 +760,31 @@ export class Datetime implements ComponentInterface { const root = this.el!.shadowRoot!; /** - * Check the element on the left and right edges - * of the body. If the two elements are not the same - * then the user is partially swiped between two - * months. We should wait until they finish - * swiping in order to update the month. - * A 5px margin allows us to avoid sub-pixel rendering quirks. + * Get the element that is in the center of the calendar body. + * This will be an element inside of the active month. */ - const elementOnLeft = root.elementFromPoint(box.x + 5, box.y + box.height / 2); - const elementOnRight = root.elementFromPoint(box.x + box.width - 5, box.y + box.height / 2); - + const elementAtCenter = root.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2); /** - * If there are no elements then the + * If there is no element then the * component may be re-rendering on a slow device. */ - if (!elementOnLeft || !elementOnRight) return; - - const leftMonth = elementOnLeft.closest('.calendar-month'); - const rightMonth = elementOnRight.closest('.calendar-month'); + if (!elementAtCenter) return; - console.log('change', leftMonth, rightMonth); + const month = elementAtCenter.closest('.calendar-month'); + if (!month) return; - if (leftMonth !== rightMonth) return; + /** + * The edge of the month must be lined up with + * the edge of the calendar body in order for + * the component to update. Otherwise, it + * may be the case that the user has paused their + * swipe or the browser has not finished snapping yet. + * Rather than check if the x values are equal, + * we give it a tolerance of 2px to account for + * sub pixel rendering. + */ + const monthBox = month.getBoundingClientRect(); + if (Math.abs(monthBox.x - box.x) > 2) return; /** * From here, we can determine if the start @@ -789,9 +792,9 @@ export class Datetime implements ComponentInterface { * If no month was changed, then we can return from * the scroll callback early. */ - if (leftMonth === startMonth) { + if (month === startMonth) { return getPreviousMonth(parts); - } else if (leftMonth === endMonth) { + } else if (month === endMonth) { return getNextMonth(parts); } else { return;