Fix touch state not being activated in iOS Safari
This commit resolves the issue with the `:active` pseudo-class not
activating in mobile Safari on iOS devices. It introduces a workaround
specifically for mobile Safari on iOS/iPadOS to enable the `:active`
pseudo-class. This ensures a consistent and responsive user interface
in response to touch states on mobile Safari.
Other supporting changes:
- Introduce new test utility functions such as `createWindowEventSpies`
and `formatAssertionMessage` to improve code reusability and
maintainability.
- Improve browser detection:
- Add detection for iPadOS and Windows 10 Mobile.
- Add touch support detection to correctly determine iPadOS vs macOS.
- Fix misidentification of some Windows 10 Mobile platforms as Windows
Phone.
- Improve test coverage and refactor tests.
This commit is contained in:
@@ -4,10 +4,34 @@ export enum OperatingSystem {
|
||||
Linux,
|
||||
KaiOS,
|
||||
ChromeOS,
|
||||
BlackBerryOS,
|
||||
BlackBerry,
|
||||
BlackBerryTabletOS,
|
||||
Android,
|
||||
iOS,
|
||||
iPadOS,
|
||||
|
||||
/**
|
||||
* Legacy: Released in 1999, discontinued in 2013, succeeded by BlackBerry10.
|
||||
*/
|
||||
BlackBerryOS,
|
||||
|
||||
/**
|
||||
* Legacy: Released in 2013, discontinued in 2015, succeeded by {@link OperatingSystem.Android}.
|
||||
*/
|
||||
BlackBerry10,
|
||||
|
||||
/**
|
||||
* Legacy: Released in 2010, discontinued in 2017,
|
||||
* succeeded by {@link OperatingSystem.Windows10Mobile}.
|
||||
*/
|
||||
WindowsPhone,
|
||||
|
||||
/**
|
||||
* Legacy: Released in 2015, discontinued in 2017, succeeded by {@link OperatingSystem.Android}.
|
||||
*/
|
||||
Windows10Mobile,
|
||||
|
||||
/**
|
||||
* Also known as "BlackBerry PlayBook OS"
|
||||
* Legacy: Released in 2011, discontinued in 2014, succeeded by {@link OperatingSystem.Android}.
|
||||
*/
|
||||
BlackBerryTabletOS,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export class WindowInjectedLogger implements Logger {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(windowVariables: WindowVariables | undefined | null = window) {
|
||||
if (!windowVariables) { // do not trust strict null checks for global objects
|
||||
if (!windowVariables) { // do not trust strictNullChecks for global objects
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!windowVariables.log) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export enum TouchSupportExpectation {
|
||||
MustExist,
|
||||
MustNotExist,
|
||||
}
|
||||
|
||||
export interface BrowserCondition {
|
||||
readonly operatingSystem: OperatingSystem;
|
||||
|
||||
readonly existingPartsInSameUserAgent: readonly string[];
|
||||
|
||||
readonly notExistingPartsInUserAgent?: readonly string[];
|
||||
|
||||
readonly touchSupport?: TouchSupportExpectation;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { BrowserCondition, TouchSupportExpectation } from './BrowserCondition';
|
||||
|
||||
// They include "Android", "iPhone" in their user agents.
|
||||
const WindowsMobileIdentifiers: readonly string[] = [
|
||||
'Windows Phone',
|
||||
'Windows Mobile',
|
||||
] as const;
|
||||
|
||||
export const BrowserConditions: readonly BrowserCondition[] = [
|
||||
{
|
||||
operatingSystem: OperatingSystem.KaiOS,
|
||||
existingPartsInSameUserAgent: ['KAIOS'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.ChromeOS,
|
||||
existingPartsInSameUserAgent: ['CrOS'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.BlackBerryOS,
|
||||
existingPartsInSameUserAgent: ['BlackBerry'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.BlackBerryTabletOS,
|
||||
existingPartsInSameUserAgent: ['RIM Tablet OS'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.BlackBerry10,
|
||||
existingPartsInSameUserAgent: ['BB10'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.Android,
|
||||
existingPartsInSameUserAgent: ['Android'],
|
||||
notExistingPartsInUserAgent: [...WindowsMobileIdentifiers],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.Android,
|
||||
existingPartsInSameUserAgent: ['Adr'],
|
||||
notExistingPartsInUserAgent: [...WindowsMobileIdentifiers],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
existingPartsInSameUserAgent: ['iPhone'],
|
||||
notExistingPartsInUserAgent: [...WindowsMobileIdentifiers],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
existingPartsInSameUserAgent: ['iPod'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
existingPartsInSameUserAgent: ['iPad'],
|
||||
// On Safari, only for older iPads running ≤ iOS 12 reports `iPad`
|
||||
// Other browsers report `iPad` both for older devices (≤ iOS 12) and newer (≥ iPadOS 13)
|
||||
// We detect all as `iPadOS` for simplicity.
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
existingPartsInSameUserAgent: ['Macintosh'], // Reported by Safari on iPads running ≥ iPadOS 13
|
||||
touchSupport: TouchSupportExpectation.MustExist, // Safari same user agent as desktop macOS
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.Linux,
|
||||
existingPartsInSameUserAgent: ['Linux'],
|
||||
notExistingPartsInUserAgent: ['Android', 'Adr'],
|
||||
},
|
||||
{
|
||||
operatingSystem: OperatingSystem.Windows,
|
||||
existingPartsInSameUserAgent: ['Windows'],
|
||||
notExistingPartsInUserAgent: [...WindowsMobileIdentifiers],
|
||||
},
|
||||
...['Windows Phone OS', 'Windows Phone 8'].map((userAgentPart) => ({
|
||||
operatingSystem: OperatingSystem.WindowsPhone,
|
||||
existingPartsInSameUserAgent: [userAgentPart],
|
||||
})),
|
||||
...['Windows Mobile', 'Windows Phone 10'].map((userAgentPart) => ({
|
||||
operatingSystem: OperatingSystem.Windows10Mobile,
|
||||
existingPartsInSameUserAgent: [userAgentPart],
|
||||
})),
|
||||
{
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
existingPartsInSameUserAgent: ['Macintosh'],
|
||||
notExistingPartsInUserAgent: ['like Mac OS X'], // Eliminate iOS and iPadOS for Safari
|
||||
touchSupport: TouchSupportExpectation.MustNotExist, // Distinguish from iPadOS for Safari
|
||||
},
|
||||
] as const;
|
||||
@@ -1,57 +1,10 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { DetectorBuilder } from './DetectorBuilder';
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||
private readonly detectors = BrowserDetectors;
|
||||
|
||||
public detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
for (const detector of this.detectors) {
|
||||
const os = detector.detect(userAgent);
|
||||
if (os !== undefined) {
|
||||
return os;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export interface BrowserEnvironment {
|
||||
readonly isTouchSupported: boolean;
|
||||
readonly userAgent: string;
|
||||
}
|
||||
|
||||
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||
const BrowserDetectors = [
|
||||
define(OperatingSystem.KaiOS, (b) => b
|
||||
.mustInclude('KAIOS')),
|
||||
define(OperatingSystem.ChromeOS, (b) => b
|
||||
.mustInclude('CrOS')),
|
||||
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||
.mustInclude('BlackBerry')),
|
||||
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||
.mustInclude('RIM Tablet OS')),
|
||||
define(OperatingSystem.BlackBerry, (b) => b
|
||||
.mustInclude('BB10')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.iOS, (b) => b
|
||||
.mustInclude('like Mac OS X')),
|
||||
define(OperatingSystem.Linux, (b) => b
|
||||
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||
define(OperatingSystem.Windows, (b) => b
|
||||
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.WindowsPhone, (b) => b
|
||||
.mustInclude('Windows Phone')),
|
||||
define(OperatingSystem.macOS, (b) => b
|
||||
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||
];
|
||||
|
||||
function define(
|
||||
os: OperatingSystem,
|
||||
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||
): IBrowserOsDetector {
|
||||
const builder = new DetectorBuilder(os);
|
||||
applyRules(builder);
|
||||
return builder.build();
|
||||
export interface BrowserOsDetector {
|
||||
detect(environment: BrowserEnvironment): OperatingSystem | undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { BrowserEnvironment, BrowserOsDetector } from './BrowserOsDetector';
|
||||
import { BrowserCondition, TouchSupportExpectation } from './BrowserCondition';
|
||||
import { BrowserConditions } from './BrowserConditions';
|
||||
|
||||
export class ConditionBasedOsDetector implements BrowserOsDetector {
|
||||
constructor(private readonly conditions: readonly BrowserCondition[] = BrowserConditions) {
|
||||
validateConditions(conditions);
|
||||
}
|
||||
|
||||
public detect(environment: BrowserEnvironment): OperatingSystem | undefined {
|
||||
if (!environment.userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
for (const condition of this.conditions) {
|
||||
if (satisfiesCondition(condition, environment)) {
|
||||
return condition.operatingSystem;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function satisfiesCondition(
|
||||
condition: BrowserCondition,
|
||||
browserEnvironment: BrowserEnvironment,
|
||||
): boolean {
|
||||
const { userAgent } = browserEnvironment;
|
||||
if (condition.touchSupport !== undefined) {
|
||||
if (!satisfiesTouchExpectation(condition.touchSupport, browserEnvironment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (condition.existingPartsInSameUserAgent.some((part) => !userAgent.includes(part))) {
|
||||
return false;
|
||||
}
|
||||
if (condition.notExistingPartsInUserAgent?.some((part) => userAgent.includes(part))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function satisfiesTouchExpectation(
|
||||
expectation: TouchSupportExpectation,
|
||||
browserEnvironment: BrowserEnvironment,
|
||||
): boolean {
|
||||
switch (expectation) {
|
||||
case TouchSupportExpectation.MustExist:
|
||||
if (!browserEnvironment.isTouchSupported) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case TouchSupportExpectation.MustNotExist:
|
||||
if (browserEnvironment.isTouchSupported) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported touch support expectation: ${TouchSupportExpectation[expectation]}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateConditions(conditions: readonly BrowserCondition[]) {
|
||||
if (!conditions.length) {
|
||||
throw new Error('empty conditions');
|
||||
}
|
||||
for (const condition of conditions) {
|
||||
validateCondition(condition);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCondition(condition: BrowserCondition) {
|
||||
if (!condition.existingPartsInSameUserAgent.length) {
|
||||
throw new Error('Each condition must include at least one identifiable part of the user agent string.');
|
||||
}
|
||||
const duplicates = getDuplicates([
|
||||
...condition.existingPartsInSameUserAgent,
|
||||
...(condition.notExistingPartsInUserAgent ?? []),
|
||||
]);
|
||||
if (duplicates.length > 0) {
|
||||
throw new Error(`Found duplicate entries in user agent parts: ${duplicates.join(', ')}. Each part should be unique.`);
|
||||
}
|
||||
if (condition.touchSupport !== undefined) {
|
||||
assertInRange(condition.touchSupport, TouchSupportExpectation);
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((text, index) => texts.indexOf(text) !== index);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class DetectorBuilder {
|
||||
private readonly existingPartsInUserAgent = new Array<string>();
|
||||
|
||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||
|
||||
constructor(private readonly os: OperatingSystem) { }
|
||||
|
||||
public mustInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.existingPartsInUserAgent);
|
||||
}
|
||||
|
||||
public mustNotInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.notExistingPartsInUserAgent);
|
||||
}
|
||||
|
||||
public build(): IBrowserOsDetector {
|
||||
if (!this.existingPartsInUserAgent.length) {
|
||||
throw new Error('Must include at least a part');
|
||||
}
|
||||
return {
|
||||
detect: (agent) => this.detect(agent),
|
||||
};
|
||||
}
|
||||
|
||||
private detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
return this.os;
|
||||
}
|
||||
|
||||
private add(part: string, array: string[]): DetectorBuilder {
|
||||
if (!part) {
|
||||
throw new Error('part is empty or undefined');
|
||||
}
|
||||
if (this.existingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as existing part`);
|
||||
}
|
||||
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as not existing part`);
|
||||
}
|
||||
array.push(part);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IBrowserOsDetector {
|
||||
detect(userAgent: string): OperatingSystem | undefined;
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { ConditionBasedOsDetector } from './BrowserOs/ConditionBasedOsDetector';
|
||||
import { BrowserEnvironment, BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||
import { isTouchEnabledDevice } from './TouchSupportDetection';
|
||||
|
||||
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||
@@ -18,11 +19,10 @@ export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
protected constructor(
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
browserOsDetector: BrowserOsDetector = new ConditionBasedOsDetector(),
|
||||
touchDetector = isTouchEnabledDevice,
|
||||
) {
|
||||
if (!window) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!window) { throw new Error('missing window'); } // do not trust strictNullChecks for global objects
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isDesktop = isDesktop(window);
|
||||
if (this.isDesktop) {
|
||||
@@ -31,7 +31,11 @@ export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
this.os = undefined;
|
||||
const userAgent = getUserAgent(window);
|
||||
if (userAgent) {
|
||||
this.os = browserOsDetector.detect(userAgent);
|
||||
const browserEnvironment: BrowserEnvironment = {
|
||||
userAgent,
|
||||
isTouchSupported: touchDetector(),
|
||||
};
|
||||
this.os = browserOsDetector.detect(browserEnvironment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
export function isTouchEnabledDevice(
|
||||
browserTouchAccessor: BrowserTouchSupportAccessor = GlobalTouchSupportAccessor,
|
||||
): boolean {
|
||||
return TouchSupportChecks.some(
|
||||
(check) => check(browserTouchAccessor),
|
||||
);
|
||||
}
|
||||
|
||||
export interface BrowserTouchSupportAccessor {
|
||||
navigatorMaxTouchPoints: () => number | undefined;
|
||||
windowMatchMediaMatches: (query: string) => boolean;
|
||||
documentOntouchend: () => undefined | unknown;
|
||||
windowTouchEvent: () => undefined | unknown;
|
||||
}
|
||||
|
||||
const TouchSupportChecks: ReadonlyArray<(accessor: BrowserTouchSupportAccessor) => boolean> = [
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
❌ Touch-enabled Windows laptop: Chrome
|
||||
(Chromium has removed ontouch* events on desktop since Chrome 70+.)
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.documentOntouchend() !== undefined,
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => {
|
||||
const maxTouchPoints = accessor.navigatorMaxTouchPoints();
|
||||
return maxTouchPoints !== undefined && maxTouchPoints > 0;
|
||||
},
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.windowMatchMediaMatches('(any-pointer: coarse)'),
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.windowTouchEvent() !== undefined,
|
||||
];
|
||||
|
||||
const GlobalTouchSupportAccessor: BrowserTouchSupportAccessor = {
|
||||
navigatorMaxTouchPoints: () => navigator.maxTouchPoints,
|
||||
windowMatchMediaMatches: (query: string) => window.matchMedia(query)?.matches,
|
||||
documentOntouchend: () => document.ontouchend,
|
||||
windowTouchEvent: () => window.TouchEvent,
|
||||
} as const;
|
||||
@@ -2,6 +2,7 @@ import { Bootstrapper } from './Bootstrapper';
|
||||
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
||||
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
||||
import { DependencyBootstrapper } from './Modules/DependencyBootstrapper';
|
||||
import { MobileSafariActivePseudoClassEnabler } from './Modules/MobileSafariActivePseudoClassEnabler';
|
||||
import type { App } from 'vue';
|
||||
|
||||
export class ApplicationBootstrapper implements Bootstrapper {
|
||||
@@ -19,6 +20,7 @@ export class ApplicationBootstrapper implements Bootstrapper {
|
||||
new RuntimeSanityValidator(),
|
||||
new DependencyBootstrapper(),
|
||||
new AppInitializationLogger(),
|
||||
new MobileSafariActivePseudoClassEnabler(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Bootstrapper } from '../Bootstrapper';
|
||||
|
||||
export class MobileSafariActivePseudoClassEnabler implements Bootstrapper {
|
||||
constructor(
|
||||
private readonly currentEnvironment = RuntimeEnvironment.CurrentEnvironment,
|
||||
private readonly browser: BrowserAccessor = GlobalBrowserAccessor,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public async bootstrap(): Promise<void> {
|
||||
if (!isMobileSafari(this.currentEnvironment, this.browser.getNavigatorUserAgent())) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
Workaround to fix issue with `:active` pseudo-class not working on mobile Safari.
|
||||
This is required so `hover-or-touch` mixin works properly.
|
||||
Last tested: iPhone with iOS 17.1.1
|
||||
See:
|
||||
- Source: https://stackoverflow.com/a/33681490
|
||||
- Snapshot 1: https://web.archive.org/web/20231112151701/https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari/33681490#33681490
|
||||
- Snapshot 2: tps://archive.ph/r1zpJ
|
||||
*/
|
||||
this.browser.addWindowEventListener('touchstart', () => {}, {
|
||||
/*
|
||||
- Setting to `true` removes the need for scrolling to block on touch and wheel
|
||||
event listeners.
|
||||
- If set to `true`, it indicates that the function specified by listener will
|
||||
never call `preventDefault`.
|
||||
- Defaults to `true` on Safari for `touchstart`.
|
||||
*/
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface BrowserAccessor {
|
||||
getNavigatorUserAgent(): string;
|
||||
addWindowEventListener(...args: Parameters<typeof window.addEventListener>): void;
|
||||
}
|
||||
|
||||
function isMobileSafari(environment: IRuntimeEnvironment, userAgent: string): boolean {
|
||||
if (!isMobileAppleOperatingSystem(environment)) {
|
||||
return false;
|
||||
}
|
||||
return isSafari(userAgent);
|
||||
}
|
||||
|
||||
function isMobileAppleOperatingSystem(environment: IRuntimeEnvironment): boolean {
|
||||
if (environment.os === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (![OperatingSystem.iOS, OperatingSystem.iPadOS].includes(environment.os)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSafari(userAgent: string): boolean {
|
||||
if (!userAgent) {
|
||||
return false;
|
||||
}
|
||||
return SafariUserAgentIdentifiers.every((i) => userAgent.includes(i))
|
||||
&& NonSafariBrowserIdentifiers.every((i) => !userAgent.includes(i));
|
||||
}
|
||||
|
||||
const GlobalBrowserAccessor: BrowserAccessor = {
|
||||
getNavigatorUserAgent: () => navigator.userAgent,
|
||||
addWindowEventListener: (...args) => window.addEventListener(...args),
|
||||
} as const;
|
||||
|
||||
const SafariUserAgentIdentifiers = [
|
||||
'Safari',
|
||||
] as const;
|
||||
|
||||
const NonSafariBrowserIdentifiers = [
|
||||
// Chrome:
|
||||
'Chrome',
|
||||
'CriOS',
|
||||
// Firefox:
|
||||
'FxiOS',
|
||||
// Opera:
|
||||
'OPiOS',
|
||||
'OPR', // Opera Desktop and Android
|
||||
'Opera', // Opera Mini
|
||||
'OPT',
|
||||
// Edge:
|
||||
'EdgiOS', // Edge on iOS/iPadOS
|
||||
'Edg', // Edge on macOS
|
||||
'EdgA', // Edge on Android
|
||||
'Edge', // Microsoft Edge Legacy
|
||||
// UC Browser:
|
||||
'UCBrowser',
|
||||
// Baidu:
|
||||
'BaiduHD',
|
||||
'BaiduBrowser',
|
||||
'baiduboxapp',
|
||||
'baidubrowser',
|
||||
// QQ Browser:
|
||||
'MQQBrowser',
|
||||
] as const;
|
||||
Reference in New Issue
Block a user