-
-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
916c9d6
commit a985127
Showing
43 changed files
with
1,719 additions
and
672 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserCondition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
86 changes: 86 additions & 0 deletions
86
src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserConditions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
57 changes: 5 additions & 52 deletions
57
src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
92 changes: 92 additions & 0 deletions
92
src/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
54 changes: 0 additions & 54 deletions
54
src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts
This file was deleted.
Oops, something went wrong.
5 changes: 0 additions & 5 deletions
5
src/infrastructure/RuntimeEnvironment/BrowserOs/IBrowserOsDetector.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.