diff options
author | Christian Fehmer <[email protected]> | 2024-09-20 08:59:04 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2024-09-20 08:59:04 +0200 |
commit | 0b854af30e3518cc4894649d890603ecd5d5ecd1 (patch) | |
tree | c6d6278ed1faa9828f9d3bb3bd70309c5f61015c | |
parent | 921ecb113f522fc2e85534768b1aadfb2fb162c5 (diff) | |
download | monkeytype-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.scss | 17 | ||||
-rw-r--r-- | frontend/src/ts/elements/leaderboards.ts | 4 | ||||
-rw-r--r-- | frontend/src/ts/elements/notifications.ts | 10 | ||||
-rw-r--r-- | frontend/src/ts/elements/scroll-to-top.ts | 6 | ||||
-rw-r--r-- | frontend/src/ts/pages/account.ts | 2 | ||||
-rw-r--r-- | frontend/src/ts/ready.ts | 2 | ||||
-rw-r--r-- | frontend/src/ts/test/caret.ts | 3 | ||||
-rw-r--r-- | frontend/src/ts/test/pb-crown.ts | 4 | ||||
-rw-r--r-- | frontend/src/ts/test/result.ts | 2 | ||||
-rw-r--r-- | frontend/src/ts/test/test-config.ts | 3 | ||||
-rw-r--r-- | frontend/src/ts/test/test-logic.ts | 7 | ||||
-rw-r--r-- | frontend/src/ts/utils/animated-modal.ts | 34 | ||||
-rw-r--r-- | frontend/src/ts/utils/misc.ts | 16 |
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 |