From 4af48dd2f9a3db6ed8d7644b65c82de9e9b8f1b1 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 11 Sep 2024 09:34:49 +0000 Subject: [PATCH] web: add UserActivation polyfill for browsers that don't have it --- web/src/lib/polyfills.ts | 1 + web/src/lib/polyfills/user-activation.ts | 54 ++++++++++++++++++++++++ web/src/lib/types/generic.ts | 1 + web/src/routes/+layout.svelte | 1 + 4 files changed, 57 insertions(+) create mode 100644 web/src/lib/polyfills.ts create mode 100644 web/src/lib/polyfills/user-activation.ts diff --git a/web/src/lib/polyfills.ts b/web/src/lib/polyfills.ts new file mode 100644 index 00000000..b5917b5c --- /dev/null +++ b/web/src/lib/polyfills.ts @@ -0,0 +1 @@ +import "./polyfills/user-activation"; diff --git a/web/src/lib/polyfills/user-activation.ts b/web/src/lib/polyfills/user-activation.ts new file mode 100644 index 00000000..fc2e968e --- /dev/null +++ b/web/src/lib/polyfills/user-activation.ts @@ -0,0 +1,54 @@ +import { browser } from "$app/environment"; +import type { Writeable } from "$lib/types/generic"; + +if (browser && !navigator.userActivation) { + const TRANSIENT_TIMEOUT = navigator.userAgent.includes('Firefox') ? 5000 : 2000; + let _timeout: number | undefined; + + const userActivation: Writeable = { + isActive: false, + hasBeenActive: false + }; + + const receiveEvent = (e: Event) => { + // An activation triggering input event is any event whose isTrusted attribute is true [...] + if (!e.isTrusted) return; + + // and whose type is one of: + if (e instanceof PointerEvent) { + if ( + // "pointerdown", provided the event's pointerType is "mouse"; + (e.type === 'pointerdown' && e.pointerType !== 'mouse') + // "pointerup", provided the event's pointerType is not "mouse"; + || (e.type === 'pointerup' && e.pointerType === 'mouse') + ) + return; + } else if (e instanceof KeyboardEvent) { + // "keydown", provided the key is neither the Esc key nor a shortcut key + // reserved by the user agent; + if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) + return; + + // the handling for this is a bit more complex, + // but this is fine for our use case + if (e.key !== 'Return' && e.key !== 'Enter' && e.key.length > 1) + return; + } + + userActivation.hasBeenActive = true; + userActivation.isActive = true; + + clearTimeout(_timeout); + _timeout = window.setTimeout(() => { + userActivation.isActive = false; + _timeout = undefined; + }, TRANSIENT_TIMEOUT); + } + + // https://html.spec.whatwg.org/multipage/interaction.html#the-useractivation-interface + for (const event of [ 'keydown', 'mousedown', 'pointerdown', 'pointerup', 'touchend' ]) { + window.addEventListener(event, receiveEvent); + } + + (navigator.userActivation as UserActivation) = userActivation; +} diff --git a/web/src/lib/types/generic.ts b/web/src/lib/types/generic.ts index 9807cea5..59844106 100644 --- a/web/src/lib/types/generic.ts +++ b/web/src/lib/types/generic.ts @@ -9,3 +9,4 @@ export type RecursivePartial = { export type DefaultImport = () => Promise<{ default: T }>; export type Optional = T | undefined; +export type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 78421620..5263ffa2 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -8,6 +8,7 @@ import { browser } from "$app/environment"; import { afterNavigate } from "$app/navigation"; + import "$lib/polyfills"; import env from "$lib/env"; import settings from "$lib/state/settings"; import locale from "$lib/i18n/locale";