aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChristian Fehmer <[email protected]>2024-09-20 08:59:04 +0200
committerGitHub <[email protected]>2024-09-20 08:59:04 +0200
commit0b854af30e3518cc4894649d890603ecd5d5ecd1 (patch)
treec6d6278ed1faa9828f9d3bb3bd70309c5f61015c
parent921ecb113f522fc2e85534768b1aadfb2fb162c5 (diff)
downloadmonkeytype-0b854af30e3518cc4894649d890603ecd5d5ecd1.tar.gz
monkeytype-0b854af30e3518cc4894649d890603ecd5d5ecd1.zip
feat: reduce motion if prefered (@fehmer) (#5866)
Disable unnecessary animations if the user prefers reduced motion. I have not changed the animations on e.g. line scrolling or smooth caret as there is always motion and the user can already decide how it behaves in the configuration.
-rw-r--r--frontend/src/styles/media-queries.scss17
-rw-r--r--frontend/src/ts/elements/leaderboards.ts4
-rw-r--r--frontend/src/ts/elements/notifications.ts10
-rw-r--r--frontend/src/ts/elements/scroll-to-top.ts6
-rw-r--r--frontend/src/ts/pages/account.ts2
-rw-r--r--frontend/src/ts/ready.ts2
-rw-r--r--frontend/src/ts/test/caret.ts3
-rw-r--r--frontend/src/ts/test/pb-crown.ts4
-rw-r--r--frontend/src/ts/test/result.ts2
-rw-r--r--frontend/src/ts/test/test-config.ts3
-rw-r--r--frontend/src/ts/test/test-logic.ts7
-rw-r--r--frontend/src/ts/utils/animated-modal.ts34
-rw-r--r--frontend/src/ts/utils/misc.ts16
13 files changed, 77 insertions, 33 deletions
diff --git a/frontend/src/styles/media-queries.scss b/frontend/src/styles/media-queries.scss
index c0d90de91..53730d5ec 100644
--- a/frontend/src/styles/media-queries.scss
+++ b/frontend/src/styles/media-queries.scss
@@ -1039,3 +1039,20 @@ body {
display: block !important;
}
}
+
+@media (prefers-reduced-motion) {
+ *:not(.fa-spin, #backgroundLoader, .preloader) {
+ animation: none !important;
+ transition: none !important;
+
+ &::after,
+ &::before {
+ animation: none !important;
+ transition: none !important;
+ }
+ }
+
+ html {
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts
index d5f1b9c25..f80c47632 100644
--- a/frontend/src/ts/elements/leaderboards.ts
+++ b/frontend/src/ts/elements/leaderboards.ts
@@ -384,7 +384,7 @@ export function hide(): void {
{
opacity: 0,
},
- 100,
+ Misc.applyReducedMotion(100),
() => {
languageSelector?.destroy();
languageSelector = undefined;
@@ -750,7 +750,7 @@ export function show(): void {
{
opacity: 1,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
void update();
startTimer();
diff --git a/frontend/src/ts/elements/notifications.ts b/frontend/src/ts/elements/notifications.ts
index 49af00944..17e22424d 100644
--- a/frontend/src/ts/elements/notifications.ts
+++ b/frontend/src/ts/elements/notifications.ts
@@ -116,7 +116,7 @@ class Notification {
{
marginTop: newHeight - oldHeight,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
$("#notificationCenter .history").css("margin-top", 0);
$("#notificationCenter .history").prepend(`
@@ -132,7 +132,7 @@ class Notification {
{
opacity: 1,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
$(`#notificationCenter .notif[id='${this.id}']`).css(
"opacity",
@@ -221,13 +221,13 @@ class Notification {
{
opacity: 0,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
$(`#notificationCenter .notif[id='${this.id}']`).animate(
{
height: 0,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
$(`#notificationCenter .notif[id='${this.id}']`).remove();
}
@@ -243,7 +243,7 @@ class Notification {
{
opacity: 0,
},
- 125,
+ Misc.applyReducedMotion(125),
() => {
$(
`#bannerCenter .banner[id='${this.id}'], #bannerCenter .psa[id='${this.id}']`
diff --git a/frontend/src/ts/elements/scroll-to-top.ts b/frontend/src/ts/elements/scroll-to-top.ts
index b9d2e5f47..6c69b8b8b 100644
--- a/frontend/src/ts/elements/scroll-to-top.ts
+++ b/frontend/src/ts/elements/scroll-to-top.ts
@@ -1,9 +1,13 @@
import * as ActivePage from "../states/active-page";
+import { prefersReducedMotion } from "../utils/misc";
let visible = false;
$(document).on("click", ".scrollToTopButton", () => {
- window.scrollTo({ top: 0, behavior: "smooth" });
+ window.scrollTo({
+ top: 0,
+ behavior: prefersReducedMotion() ? "instant" : "smooth",
+ });
});
$(window).on("scroll", () => {
diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts
index 41697d683..d5d7fd621 100644
--- a/frontend/src/ts/pages/account.ts
+++ b/frontend/src/ts/pages/account.ts
@@ -1138,7 +1138,7 @@ $(".pageAccount #accountHistoryChart").on("click", () => {
{
scrollTop: scrollTo,
},
- 500
+ Misc.applyReducedMotion(500)
);
$(".resultRow").removeClass("active");
$(`#result-${index}`).addClass("active");
diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts
index 03c681500..5ae62c550 100644
--- a/frontend/src/ts/ready.ts
+++ b/frontend/src/ts/ready.ts
@@ -30,7 +30,7 @@ $((): void => {
.css("opacity", "0")
.removeClass("hidden")
.stop(true, true)
- .animate({ opacity: 1 }, 250);
+ .animate({ opacity: 1 }, Misc.applyReducedMotion(250));
if (ConnectionState.get()) {
void ServerConfiguration.sync().then(() => {
if (!ServerConfiguration.get()?.users.signUp) {
diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts
index 214182d4f..f68be0c76 100644
--- a/frontend/src/ts/test/caret.ts
+++ b/frontend/src/ts/test/caret.ts
@@ -5,6 +5,7 @@ import * as TestInput from "./test-input";
import * as SlowTimer from "../states/slow-timer";
import * as TestState from "../test/test-state";
import * as TestWords from "./test-words";
+import { prefersReducedMotion } from "../utils/misc";
export let caretAnimating = true;
const caret = document.querySelector("#caret") as HTMLElement;
@@ -246,7 +247,7 @@ export async function updatePosition(noAnim = false): Promise<void> {
window.scrollTo({
left: 0,
top: newscrolltop,
- behavior: "smooth",
+ behavior: prefersReducedMotion() ? "instant" : "smooth",
});
}
}
diff --git a/frontend/src/ts/test/pb-crown.ts b/frontend/src/ts/test/pb-crown.ts
index 67c47ade8..ece051767 100644
--- a/frontend/src/ts/test/pb-crown.ts
+++ b/frontend/src/ts/test/pb-crown.ts
@@ -1,3 +1,5 @@
+import { applyReducedMotion } from "../utils/misc";
+
export function hide(): void {
visible = false;
$("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden");
@@ -25,7 +27,7 @@ export function show(): void {
{
opacity: 1,
},
- 250,
+ applyReducedMotion(250),
"easeOutCubic"
);
}
diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts
index 0cddfe238..9b8a292ec 100644
--- a/frontend/src/ts/test/result.ts
+++ b/frontend/src/ts/test/result.ts
@@ -955,7 +955,7 @@ export async function update(
{
opacity: 1,
},
- 125
+ Misc.applyReducedMotion(125)
);
const canQuickRestart = Misc.canQuickRestart(
diff --git a/frontend/src/ts/test/test-config.ts b/frontend/src/ts/test/test-config.ts
index 980f494b0..b349307f4 100644
--- a/frontend/src/ts/test/test-config.ts
+++ b/frontend/src/ts/test/test-config.ts
@@ -6,6 +6,7 @@ import { Mode } from "@monkeytype/contracts/schemas/shared";
import Config from "../config";
import * as ConfigEvent from "../observables/config-event";
import * as ActivePage from "../states/active-page";
+import { applyReducedMotion } from "../utils/misc";
export function show(): void {
$("#testConfig").removeClass("invisible");
@@ -74,7 +75,7 @@ export async function update(previous: Mode, current: Mode): Promise<void> {
zen: "zen",
};
- const animTime = 250;
+ const animTime = applyReducedMotion(250);
const easing = {
both: "easeInOutSine",
in: "easeInSine",
diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts
index 85d75070c..cd94e5efc 100644
--- a/frontend/src/ts/test/test-logic.ts
+++ b/frontend/src/ts/test/test-logic.ts
@@ -146,6 +146,7 @@ export function restart(options = {} as RestartOptions): void {
};
options = { ...defaultOptions, ...options };
+ const animationTime = options.noAnim ? 0 : Misc.applyReducedMotion(125);
if (TestUI.testRestarting || TestUI.resultCalculating) {
event?.preventDefault();
@@ -289,7 +290,7 @@ export function restart(options = {} as RestartOptions): void {
{
opacity: 0,
},
- options.noAnim ? 0 : 125,
+ animationTime,
async () => {
$("#result").addClass("hidden");
$("#typingTest").css("opacity", 0).removeClass("hidden");
@@ -350,7 +351,7 @@ export function restart(options = {} as RestartOptions): void {
{
opacity: 1,
},
- options.noAnim ? 0 : 125,
+ animationTime,
() => {
TimerProgress.reset();
LiveSpeed.reset();
@@ -1258,7 +1259,7 @@ async function saveResult(
// maxWidth: "10rem",
opacity: 1,
},
- 500
+ Misc.applyReducedMotion(500)
);
$("#result .stats .dailyLeaderboard .bottom").html(
Format.rank(data.dailyLeaderboardRank, { fallback: "" })
diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts
index 5af98f4b0..af8f06c1a 100644
--- a/frontend/src/ts/utils/animated-modal.ts
+++ b/frontend/src/ts/utils/animated-modal.ts
@@ -1,4 +1,4 @@
-import { isPopupVisible } from "./misc";
+import { applyReducedMotion, isPopupVisible } from "./misc";
import * as Skeleton from "./skeleton";
type CustomAnimation = {
@@ -211,14 +211,15 @@ export default class AnimatedModal<
return;
}
- const modalAnimationDuration =
+ const modalAnimationDuration = applyReducedMotion(
(options?.customAnimation?.modal?.durationMs ??
options?.animationDurationMs ??
this.customShowAnimations?.modal?.durationMs ??
DEFAULT_ANIMATION_DURATION) *
- (options?.modalChain !== undefined
- ? MODAL_ONLY_ANIMATION_MULTIPLIER
- : 1);
+ (options?.modalChain !== undefined
+ ? MODAL_ONLY_ANIMATION_MULTIPLIER
+ : 1)
+ );
if (options?.modalChain !== undefined) {
this.previousModalInChain = options.modalChain;
@@ -252,10 +253,11 @@ export default class AnimatedModal<
to: { opacity: "1" },
easing: "swing",
};
- const wrapperAnimationDuration =
+ const wrapperAnimationDuration = applyReducedMotion(
options?.customAnimation?.wrapper?.durationMs ??
- this.customShowAnimations?.wrapper?.durationMs ??
- DEFAULT_ANIMATION_DURATION;
+ this.customShowAnimations?.wrapper?.durationMs ??
+ DEFAULT_ANIMATION_DURATION
+ );
const animationMode =
this.previousModalInChain !== undefined
@@ -335,24 +337,26 @@ export default class AnimatedModal<
const modalAnimation =
options?.customAnimation?.modal ?? this.customHideAnimations?.modal;
- const modalAnimationDuration =
+ const modalAnimationDuration = applyReducedMotion(
(options?.customAnimation?.modal?.durationMs ??
options?.animationDurationMs ??
this.customHideAnimations?.modal?.durationMs ??
DEFAULT_ANIMATION_DURATION) *
- (this.previousModalInChain !== undefined
- ? MODAL_ONLY_ANIMATION_MULTIPLIER
- : 1);
+ (this.previousModalInChain !== undefined
+ ? MODAL_ONLY_ANIMATION_MULTIPLIER
+ : 1)
+ );
const wrapperAnimation = options?.customAnimation?.wrapper ??
this.customHideAnimations?.wrapper ?? {
from: { opacity: "1" },
to: { opacity: "0" },
easing: "swing",
};
- const wrapperAnimationDuration =
+ const wrapperAnimationDuration = applyReducedMotion(
options?.customAnimation?.wrapper?.durationMs ??
- this.customHideAnimations?.wrapper?.durationMs ??
- DEFAULT_ANIMATION_DURATION;
+ this.customHideAnimations?.wrapper?.durationMs ??
+ DEFAULT_ANIMATION_DURATION
+ );
const animationMode =
this.previousModalInChain !== undefined
? "modalOnly"
diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts
index 3f9b1bef9..fa550f2c9 100644
--- a/frontend/src/ts/utils/misc.ts
+++ b/frontend/src/ts/utils/misc.ts
@@ -327,6 +327,7 @@ export async function swapElements(
return Promise.resolve();
}
): Promise<boolean | undefined> {
+ totalDuration = applyReducedMotion(totalDuration);
if (
(el1.hasClass("hidden") && !el2.hasClass("hidden")) ||
(!el1.hasClass("hidden") && el2.hasClass("hidden"))
@@ -554,7 +555,7 @@ export async function promiseAnimation(
easing: string
): Promise<void> {
return new Promise((resolve) => {
- el.animate(animation, duration, easing, resolve);
+ el.animate(animation, applyReducedMotion(duration), easing, resolve);
});
}
@@ -750,4 +751,17 @@ export function deepClone<T>(obj: T | T[]): T | T[] {
return clonedObj;
}
+export function prefersReducedMotion(): boolean {
+ return matchMedia?.("(prefers-reduced-motion)")?.matches;
+}
+
+/**
+ * Reduce the animation time based on the browser preference `prefers-reduced-motion`.
+ * @param animationTime
+ * @returns `0` if user prefers reduced-motion, else the given animationTime
+ */
+export function applyReducedMotion(animationTime: number): number {
+ return prefersReducedMotion() ? 0 : animationTime;
+}
+
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES