Files
Liam DeBeasi e5226016a0 fix(refresher): native ios refresher works on iPadOS (#28620)
Issue number: resolves #28617

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

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?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- 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:
60303aad23/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

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.5.8-dev.11703088210.14a72b83`

Co-authored-by: Sean Perkins <sean-perkins@users.noreply.github.com>

---------

Co-authored-by: Sean Perkins <sean-perkins@user.noreply.github.com>
2023-12-20 17:28:27 +00:00

145 lines
5.3 KiB
TypeScript

import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, h } from '@stencil/core';
import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
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';
@Component({
tag: 'ion-refresher-content',
})
export class RefresherContent implements ComponentInterface {
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
@Element() el!: HTMLIonRefresherContentElement;
/**
* A static icon or a spinner to display when you begin to pull down.
* A spinner name can be provided to gradually show tick marks
* when pulling down on iOS devices.
*/
@Prop({ mutable: true }) pullingIcon?: SpinnerTypes | string | null;
/**
* The text you want to display when you begin to pull down.
* `pullingText` can accept either plaintext or HTML as a string.
* To display characters normally reserved for HTML, they
* must be escaped. For example `<Ionic>` would become
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* Content is parsed as plaintext by default.
* `innerHTMLTemplatesEnabled` must be set to `true` in the Ionic config
* before custom HTML can be used.
*/
@Prop() pullingText?: string | IonicSafeString;
/**
* An animated SVG spinner that shows when refreshing begins
*/
@Prop({ mutable: true }) refreshingSpinner?: SpinnerTypes | null;
/**
* The text you want to display when performing a refresh.
* `refreshingText` can accept either plaintext or HTML as a string.
* To display characters normally reserved for HTML, they
* must be escaped. For example `<Ionic>` would become
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* Content is parsed as plaintext by default.
* `innerHTMLTemplatesEnabled` must be set to `true` in the Ionic config
* before custom HTML can be used.
*/
@Prop() refreshingText?: string | IonicSafeString;
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 = hasRubberBandScrolling ? 'lines' : arrowDown;
this.pullingIcon = config.get(
'refreshingIcon',
mode === 'ios' && hasRubberBandScrolling ? config.get('spinner', overflowRefresher) : 'circular'
);
}
if (this.refreshingSpinner === undefined) {
const mode = getIonMode(this);
this.refreshingSpinner = config.get(
'refreshingSpinner',
config.get('spinner', mode === 'ios' ? 'lines' : 'circular')
);
}
}
private renderPullingText() {
const { customHTMLEnabled, pullingText } = this;
if (customHTMLEnabled) {
return <div class="refresher-pulling-text" innerHTML={sanitizeDOMString(pullingText)}></div>;
}
return <div class="refresher-pulling-text">{pullingText}</div>;
}
private renderRefreshingText() {
const { customHTMLEnabled, refreshingText } = this;
if (customHTMLEnabled) {
return <div class="refresher-refreshing-text" innerHTML={sanitizeDOMString(refreshingText)}></div>;
}
return <div class="refresher-refreshing-text">{refreshingText}</div>;
}
render() {
const pullingIcon = this.pullingIcon;
const hasSpinner = pullingIcon != null && (SPINNERS[pullingIcon] as any) !== undefined;
const mode = getIonMode(this);
return (
<Host class={mode}>
<div class="refresher-pulling">
{this.pullingIcon && hasSpinner && (
<div class="refresher-pulling-icon">
<div class="spinner-arrow-container">
<ion-spinner name={this.pullingIcon as SpinnerTypes} paused></ion-spinner>
{mode === 'md' && this.pullingIcon === 'circular' && (
<div class="arrow-container">
<ion-icon icon={caretBackSharp} aria-hidden="true"></ion-icon>
</div>
)}
</div>
</div>
)}
{this.pullingIcon && !hasSpinner && (
<div class="refresher-pulling-icon">
<ion-icon icon={this.pullingIcon} lazy={false} aria-hidden="true"></ion-icon>
</div>
)}
{this.pullingText !== undefined && this.renderPullingText()}
</div>
<div class="refresher-refreshing">
{this.refreshingSpinner && (
<div class="refresher-refreshing-icon">
<ion-spinner name={this.refreshingSpinner}></ion-spinner>
</div>
)}
{this.refreshingText !== undefined && this.renderRefreshingText()}
</div>
</Host>
);
}
}