From 62e14304e25d2bd4a388d9e4fa4ac50b2145bfc5 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 1 Dec 2023 11:11:18 -0500 Subject: [PATCH 1/3] fix(refresher): native ios refresher works on iPadOS --- .../refresher-content/refresher-content.tsx | 12 +++++-- .../components/refresher/refresher.utils.ts | 36 ++++++++++++------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/core/src/components/refresher-content/refresher-content.tsx b/core/src/components/refresher-content/refresher-content.tsx index 35b6554a77d..df94479c9d1 100644 --- a/core/src/components/refresher-content/refresher-content.tsx +++ b/core/src/components/refresher-content/refresher-content.tsx @@ -1,13 +1,13 @@ import type { ComponentInterface } from '@stencil/core'; import { Component, Element, Host, Prop, h } from '@stencil/core'; import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config'; -import { isPlatform } from '@utils/platform'; import { sanitizeDOMString } from '@utils/sanitization'; import { arrowDown, caretBackSharp } from 'ionicons/icons'; import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; import type { IonicSafeString } from '../../utils/sanitization'; +import { supportsRubberBandScrolling } from '../refresher/refresher.utils'; import type { SpinnerTypes } from '../spinner/spinner-configs'; import { SPINNERS } from '../spinner/spinner-configs'; @@ -63,11 +63,17 @@ export class RefresherContent implements ComponentInterface { componentWillLoad() { if (this.pullingIcon === undefined) { + /** + * The native iOS refresher uses a spinner instead of + * an icon, so we need to see if this device supports + * the native iOS refresher. + */ + const hasRubberBandScrolling = supportsRubberBandScrolling(); const mode = getIonMode(this); - const overflowRefresher = (this.el.style as any).webkitOverflowScrolling !== undefined ? 'lines' : arrowDown; + const overflowRefresher = hasRubberBandScrolling ? 'lines' : arrowDown; this.pullingIcon = config.get( 'refreshingIcon', - mode === 'ios' && isPlatform('mobile') ? config.get('spinner', overflowRefresher) : 'circular' + mode === 'ios' && hasRubberBandScrolling ? config.get('spinner', overflowRefresher) : 'circular' ); } if (this.refreshingSpinner === undefined) { diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index 8ae86de2581..679c8bf70a9 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -207,17 +207,29 @@ export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElem const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner'); return ( - pullingSpinner !== null && - refreshingSpinner !== null && - /** - * We use webkitOverflowScrolling for feature detection with rubber band scrolling - * on iOS. When doing referenceEl.style, webkitOverflowScrolling is undefined on non-iOS platforms. - * However, it will be the empty string on iOS. - * Note that we do not use getPropertyValue (and thus need to cast as any) because calling - * getPropertyValue('-webkit-overflow-scrolling') will return the empty string if it is not - * set on the element, even if the platform does not support that. - */ - ((mode === 'ios' && isPlatform('mobile') && (referenceEl.style as any).webkitOverflowScrolling !== undefined) || - mode === 'md') + (pullingSpinner !== null && refreshingSpinner !== null && mode === 'ios' && supportsRubberBandScrolling()) || + mode === 'md' + ); +}; + +/** + * In order to use the native iOS refresher the device must support rubber band scrolling. + * The ios + mobile platform check ensures that desktop Safari is not included. Desktop Safari + * has a slightly different rubber band effect that is not compatible with the native refresher + * in Ionic. + * + * We also need to be careful not to include devices that spoof their user agent. + * For example, when using iOS emulation in Chrome the user agent will be spoofed such that + * isPlatform('ios') and isPlatform('mobile') both return true. To work around this, + * we check to see if the apple-pay-logo is supported as a named image which is only + * true on Apple devices. + * + * We previously checked referencEl.style.webkitOverflowScrolling to explicitly check + * for rubber band support. However, this property was removed on iPadOS and it's possible + * that this will be removed on iOS in the future too. + */ +export const supportsRubberBandScrolling = () => { + return ( + isPlatform('ios') && isPlatform('mobile') && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)') ); }; From 87995301114b8e90cc9ac234d2d2fc1d7c2d2559 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 1 Dec 2023 11:32:16 -0500 Subject: [PATCH 2/3] fix: parentheses are hard --- .../components/refresher/refresher.utils.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index 679c8bf70a9..7b488a0072d 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -195,23 +195,6 @@ export const translateElement = (el?: HTMLElement, value?: string, duration = 20 // Utils // ----------------------------- -export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElement, mode: string) => { - const refresherContent = referenceEl.querySelector('ion-refresher-content'); - if (!refresherContent) { - return Promise.resolve(false); - } - - await new Promise((resolve) => componentOnReady(refresherContent, resolve)); - - const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner'); - const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner'); - - return ( - (pullingSpinner !== null && refreshingSpinner !== null && mode === 'ios' && supportsRubberBandScrolling()) || - mode === 'md' - ); -}; - /** * In order to use the native iOS refresher the device must support rubber band scrolling. * The ios + mobile platform check ensures that desktop Safari is not included. Desktop Safari @@ -233,3 +216,21 @@ export const supportsRubberBandScrolling = () => { isPlatform('ios') && isPlatform('mobile') && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)') ); }; + +export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElement, mode: string) => { + const refresherContent = referenceEl.querySelector('ion-refresher-content'); + if (!refresherContent) { + return Promise.resolve(false); + } + + await new Promise((resolve) => componentOnReady(refresherContent, resolve)); + + const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner'); + const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner'); + + return ( + pullingSpinner !== null && + refreshingSpinner !== null && + ((mode === 'ios' && supportsRubberBandScrolling()) || mode === 'md') + ); +}; From 4a72b83e2fc11dd38ca719306746e765a49eef13 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 20 Dec 2023 11:02:38 -0500 Subject: [PATCH 3/3] refactor: add improved detection Co-authored-by: Sean Perkins --- core/src/components/refresher/refresher.utils.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index 7b488a0072d..4becab356d0 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -1,7 +1,6 @@ import { writeTask } from '@stencil/core'; import { createAnimation } from '@utils/animation/animation'; import { clamp, componentOnReady, transitionEndAsync } from '@utils/helpers'; -import { isPlatform } from '@utils/platform'; // MD Native Refresher // ----------------------------- @@ -197,24 +196,21 @@ export const translateElement = (el?: HTMLElement, value?: string, duration = 20 /** * In order to use the native iOS refresher the device must support rubber band scrolling. - * The ios + mobile platform check ensures that desktop Safari is not included. Desktop Safari - * has a slightly different rubber band effect that is not compatible with the native refresher - * in Ionic. + * As part of this, we need to exclude Desktop Safari because it has a slightly different rubber band effect that is not compatible with the native refresher in Ionic. * * We also need to be careful not to include devices that spoof their user agent. * For example, when using iOS emulation in Chrome the user agent will be spoofed such that - * isPlatform('ios') and isPlatform('mobile') both return true. To work around this, + * navigator.maxTouchPointer > 0. To work around this, * we check to see if the apple-pay-logo is supported as a named image which is only * true on Apple devices. * * We previously checked referencEl.style.webkitOverflowScrolling to explicitly check * for rubber band support. However, this property was removed on iPadOS and it's possible * that this will be removed on iOS in the future too. + * */ export const supportsRubberBandScrolling = () => { - return ( - isPlatform('ios') && isPlatform('mobile') && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)') - ); + return navigator.maxTouchPoints > 0 && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)'); }; export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElement, mode: string) => {