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;