diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 5d821bfc..dbc77428 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -252,20 +252,11 @@ const AppKeyHandlers = () => { }; const AppInner = () => { - const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false); - const prefersReducedMotionSetting = jotai.useAtomValue(atoms.reducedMotionPreferenceAtom); + const prefersReducedMotion = jotai.useAtomValue(atoms.prefersReducedMotionAtom); const client = jotai.useAtomValue(atoms.client); const windowData = jotai.useAtomValue(atoms.waveWindow); const isFullScreen = jotai.useAtomValue(atoms.isFullScreen); - React.useEffect(() => { - const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); - setPrefersReducedMotion(!reducedMotionQuery || reducedMotionQuery.matches); - reducedMotionQuery.addEventListener("change", () => { - setPrefersReducedMotion(reducedMotionQuery.matches); - }); - }, []); - if (client == null || windowData == null) { return (
@@ -279,7 +270,7 @@ const AppInner = () => {
diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 45cdcec6..b42afe63 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -124,7 +124,26 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } catch (_) { // do nothing } - const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsAtom)?.["window:reducedmotion"]); + + const reducedMotionSettingAtom = jotai.atom((get) => get(settingsAtom)?.["window:reducedmotion"]); + const reducedMotionSystemPreferenceAtom = jotai.atom(false); + + // Composite of the prefers-reduced-motion media query and the window:reducedmotion user setting. + const prefersReducedMotionAtom = jotai.atom((get) => { + const reducedMotionSetting = get(reducedMotionSettingAtom); + const reducedMotionSystemPreference = get(reducedMotionSystemPreferenceAtom); + return reducedMotionSetting || reducedMotionSystemPreference; + }); + + // Set up a handler for changes to the prefers-reduced-motion media query. + if (globalThis.window != null) { + const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); + globalStore.set(reducedMotionSystemPreferenceAtom, !reducedMotionQuery || reducedMotionQuery.matches); + reducedMotionQuery?.addEventListener("change", () => { + globalStore.set(reducedMotionSystemPreferenceAtom, reducedMotionQuery.matches); + }); + } + const typeAheadModalAtom = jotai.atom({}); const modalOpen = jotai.atom(false); const allConnStatusAtom = jotai.atom((get) => { @@ -146,7 +165,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { isFullScreen: isFullScreenAtom, controlShiftDelayAtom, updaterStatusAtom, - reducedMotionPreferenceAtom, + prefersReducedMotionAtom, typeAheadModalAtom, modalOpen, allConnStatus: allConnStatusAtom, diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 640cbb77..98725017 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -773,10 +773,12 @@ export class LayoutModel { return isFocused; }), numLeafs: this.numLeafs, + isResizing: this.isResizing, isMagnified: atom((get) => { const treeState = get(this.treeStateAtom); return treeState.magnifiedNodeId === nodeid; }), + animationTimeS: this.animationTimeS, ready: this.ready, disablePointerEvents: this.activeDrag, onClose: async () => await this.closeNode(nodeid), diff --git a/frontend/layout/lib/layoutModelHooks.ts b/frontend/layout/lib/layoutModelHooks.ts index 113eab91..92d9d745 100644 --- a/frontend/layout/lib/layoutModelHooks.ts +++ b/frontend/layout/lib/layoutModelHooks.ts @@ -5,7 +5,8 @@ import { atoms, globalStore, WOS } from "@/app/store/global"; import { fireAndForget } from "@/util/util"; import useResizeObserver from "@react-hook/resize-observer"; import { Atom, useAtomValue } from "jotai"; -import { CSSProperties, useEffect, useLayoutEffect, useState } from "react"; +import { CSSProperties, useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { debounce } from "throttle-debounce"; import { withLayoutTreeStateAtomFromTab } from "./layoutAtom"; import { LayoutModel } from "./layoutModel"; import { LayoutNode, NodeModel, TileLayoutContents } from "./types"; @@ -67,32 +68,26 @@ export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): export function useDebouncedNodeInnerRect(nodeModel: NodeModel): CSSProperties { const nodeInnerRect = useAtomValue(nodeModel.innerRect); + const animationTimeS = useAtomValue(nodeModel.animationTimeS); + const isMagnified = useAtomValue(nodeModel.isMagnified); + const isResizing = useAtomValue(nodeModel.isResizing); + const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); const [innerRect, setInnerRect] = useState(); - const [isTransitioning, setIsTransitioning] = useState(false); - useEffect(() => { - const onTransitionStart = () => { - setIsTransitioning(true); - }; - const onTransitionEnd = () => { - setIsTransitioning(false); - }; - if (nodeModel.displayContainerRef.current) { - nodeModel.displayContainerRef.current.addEventListener("transitionstart", onTransitionStart); - nodeModel.displayContainerRef.current.addEventListener("transitionend", onTransitionEnd); - } - - return () => { - nodeModel.displayContainerRef.current?.removeEventListener("transitionstart", onTransitionStart); - nodeModel.displayContainerRef.current?.removeEventListener("transitionend", onTransitionEnd); - }; - }, [nodeModel]); + const setInnerRectDebounced = useCallback( + debounce(animationTimeS * 1000, (nodeInnerRect) => { + setInnerRect(nodeInnerRect); + }), + [animationTimeS] + ); useLayoutEffect(() => { - if (!isTransitioning) { + if (prefersReducedMotion || isMagnified || isResizing) { setInnerRect(nodeInnerRect); + } else { + setInnerRectDebounced(nodeInnerRect); } - }, [nodeInnerRect, isTransitioning]); + }, [nodeInnerRect]); return innerRect; } diff --git a/frontend/layout/lib/types.ts b/frontend/layout/lib/types.ts index 13e74262..064a3bbe 100644 --- a/frontend/layout/lib/types.ts +++ b/frontend/layout/lib/types.ts @@ -335,6 +335,8 @@ export interface NodeModel { numLeafs: Atom; nodeId: string; blockId: string; + animationTimeS: Atom; + isResizing: Atom; isFocused: Atom; isMagnified: Atom; ready: Atom; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index e645e6c4..5281d3b8 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -18,7 +18,7 @@ declare global { activeTabId: jotai.Atom; // derrived from windowDataAtom isFullScreen: jotai.PrimitiveAtom; controlShiftDelayAtom: jotai.PrimitiveAtom; - reducedMotionPreferenceAtom: jotai.Atom; + prefersReducedMotionAtom: jotai.Atom; updaterStatusAtom: jotai.PrimitiveAtom; typeAheadModalAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom;