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:
undergroundwires
2023-12-11 05:24:27 +01:00
parent 916c9d62d9
commit a9851272ae
43 changed files with 1719 additions and 672 deletions

View File

@@ -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(),
];
}
}

View File

@@ -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;