From e5226016a0f0b066a7bd7fc9997f905d3b87fbc4 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 20 Dec 2023 12:28:27 -0500 Subject: [PATCH] fix(refresher): native ios refresher works on iPadOS (#28620) Issue number: resolves #28617 --------- ## What is the current behavior? We currently check to see if `webkitOverflowScrolling` is supported on the refresher's style object in order to enable to native iOS refresher. This works well for iOS, but it does not work for iPadOS. This is because this property was removed in iPadOS 13: https://developer.apple.com/documentation/safari-release-notes/safari-13-release-notes > Disabled -webkit-overflow-scrolling: touch on iPad. All frames and scrollable overflow areas now use accelerated one-finger scrolling without changing stacking. As a result, the native iOS refresher does not activate on iPadOS. ## What is the new behavior? - I think it's safe to assume that `webkitOverflowScrolling` may be removed on iOS in the future too since it was already removed on iPadOS. As a result, I implemented a solution that avoids checking this. - The `CSS.supports` check is required because otherwise the native iOS refresher would be activated in an emulated environment such as Chrome dev tools because the user agent is spoofed. The `apple-pay-logo-black` named image is only supported on Apple devices. Risks: - Apple could remove the `apple-pay-logo-black` named image in the future. However, we currently use this check elsewhere in Ionic too and it has worked well: https://github.com/ionic-team/ionic-framework/blob/60303aad23f823488afc8f8824e9c72e3ab86acc/core/src/components/datetime/datetime.ios.scss#L177. - Apple could add touch emulation to desktop Safari which could cause the native refresher to activate when using responsive design mode for testing. However, this would only impact app developer and would not impact production use cases. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Dev build: `7.5.8-dev.11703088210.14a72b83` Co-authored-by: Sean Perkins --------- Co-authored-by: Sean Perkins --- .../refresher-content/refresher-content.tsx | 12 +++++-- .../components/refresher/refresher.utils.ts | 31 ++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/core/src/components/refresher-content/refresher-content.tsx b/core/src/components/refresher-content/refresher-content.tsx index 35b6554a77..df94479c9d 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 8ae86de258..4becab356d 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 // ----------------------------- @@ -195,6 +194,25 @@ export const translateElement = (el?: HTMLElement, value?: string, duration = 20 // Utils // ----------------------------- +/** + * In order to use the native iOS refresher the device must support rubber band scrolling. + * 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 + * 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 navigator.maxTouchPoints > 0 && 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) { @@ -209,15 +227,6 @@ export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElem 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') + ((mode === 'ios' && supportsRubberBandScrolling()) || mode === 'md') ); };