From 767708e401268f0a4f26fe0fd524886ec90b1bd7 Mon Sep 17 00:00:00 2001 From: Zukilover Date: Sun, 5 Jan 2025 22:32:02 +0700 Subject: [PATCH] Fix/blur shrink ondelete (#2596) Co-authored-by: Raine Revere --- src/components/LayoutTree.tsx | 2 +- src/components/Subthought.tsx | 12 ++++++-- src/durations.config.ts | 2 ++ src/hooks/useCachedThoughtHtml.ts | 46 +++++++++++++++++++++++++++++++ src/recipes/fadeTransition.ts | 23 ++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/hooks/useCachedThoughtHtml.ts diff --git a/src/components/LayoutTree.tsx b/src/components/LayoutTree.tsx index 40b6f258c7..be6dcbae72 100644 --- a/src/components/LayoutTree.tsx +++ b/src/components/LayoutTree.tsx @@ -491,7 +491,7 @@ const TreeNode = ({ // The FadeTransition is only responsible for fade out on unmount; // or for fade in on mounting of a new thought. // See autofocusChanged for normal opacity transition. - duration={isEmpty ? 'nodeFadeIn' : 'nodeFadeOut'} + duration={isEmpty ? 'nodeFadeIn' : 'nodeDissolve'} nodeRef={fadeThoughtRef} in={transitionGroupsProps.in} unmountOnExit diff --git a/src/components/Subthought.tsx b/src/components/Subthought.tsx index 276c64441c..18081e2694 100644 --- a/src/components/Subthought.tsx +++ b/src/components/Subthought.tsx @@ -6,6 +6,7 @@ import LazyEnv from '../@types/LazyEnv' import Path from '../@types/Path' import SimplePath from '../@types/SimplePath' import ThoughtId from '../@types/ThoughtId' +import useCachedThoughtHtml from '../hooks/useCachedThoughtHtml' import useChangeRef from '../hooks/useChangeRef' import attributeEquals from '../selectors/attributeEquals' import findFirstEnvContextWithZoom from '../selectors/findFirstEnvContextWithZoom' @@ -59,6 +60,8 @@ const Subthought = ({ const state = store.getState() const ref = useRef(null) const thought = useSelector(state => getThoughtById(state, head(simplePath)), shallowEqual) + // Cache the thought HTML before it is deleted so that we can animate on unmount + const cachedThoughtHtmlRef = useCachedThoughtHtml({ thought, elementRef: ref }) const noOtherContexts = useSelector( state => thought && isContextViewActive(state, simplePath) && getContexts(state, thought.value).length <= 1, ) @@ -114,9 +117,12 @@ const Subthought = ({ ref.current.style.opacity = opacity }) - // Short circuit if thought has already been removed. - // This can occur in a re-render even when thought is defined in the parent component. - if (!thought) return null + // If the thought has unmounted, return the cached static HTML from the ref so that it can animate out. + if (!thought) { + return cachedThoughtHtmlRef.current ? ( +
+ ) : null + } return ( <> diff --git a/src/durations.config.ts b/src/durations.config.ts index 2efd835de3..0f5583145c 100644 --- a/src/durations.config.ts +++ b/src/durations.config.ts @@ -32,6 +32,8 @@ const durationsConfig = { nodeFadeIn: 80, /* A fade out animation that is triggered when a node unmounts. See autofocusChanged for normal opacity animations. */ nodeFadeOut: 80, + /* A dissolve animation that is triggered when a node is deleted. */ + nodeDissolve: 80, } as const export default durationsConfig diff --git a/src/hooks/useCachedThoughtHtml.ts b/src/hooks/useCachedThoughtHtml.ts new file mode 100644 index 0000000000..feabff25bb --- /dev/null +++ b/src/hooks/useCachedThoughtHtml.ts @@ -0,0 +1,46 @@ +import React, { useEffect, useRef } from 'react' +import { cx } from '../../styled-system/css' +import { editableRecipe } from '../../styled-system/recipes' +import Thought from '../@types/Thought' + +// Create a regular expression to match and remove BEM modifier (variant) classes +const editableClass = cx( + editableRecipe({ + preventAutoscroll: true, + }), +).split(' ') + +// Filter classes containing '--' to identify variant classes +const modifiers = editableClass.filter(cls => cls.includes('--')) + +// Precompile the regex once, so it's not recreated on each render +const regexPreventAutoscroll = new RegExp(`\\b(${modifiers.join('|')})\\b`, 'g') + +/** + * Custom hook to capture and cache the static HTML string of a node. Useful for animating a thought after it has been deleted. Removes preventAutoscroll classes to avoid accidentally rendering at 0% opacity. + * + * @param thought - The thought object. + * @param elementRef - The ref of the Thought container element. + * @returns The cached HTML string. + */ +const useCachedThoughtHtml = ({ + thought, + elementRef, +}: { + thought: Thought + elementRef: React.RefObject +}) => { + // Cache the DOM before it is deleted + const cachedHTMLRef = useRef(null) + + // Capture the static innerHTML of the thought container whenever the thought changes + useEffect(() => { + if (thought && elementRef.current) { + cachedHTMLRef.current = elementRef.current.innerHTML.replace(regexPreventAutoscroll, '').trim() + } + }, [thought, elementRef]) + + return cachedHTMLRef +} + +export default useCachedThoughtHtml diff --git a/src/recipes/fadeTransition.ts b/src/recipes/fadeTransition.ts index a70acf3255..40da783fcf 100644 --- a/src/recipes/fadeTransition.ts +++ b/src/recipes/fadeTransition.ts @@ -35,6 +35,29 @@ const fadeTransitionRecipe = defineSlotRecipe({ enter: { opacity: 1 }, exitActive: { transition: `opacity {durations.nodeFadeOut} ease-out` }, }, + nodeDissolve: { + enter: { + transform: 'scale3d(1, 1, 1)', + filter: 'blur(0)', + }, + enterActive: { + transform: 'scale3d(1, 1, 1)', + filter: 'blur(0)', + transition: `opacity {durations.nodeDissolve} ease-out, transform {durations.nodeDissolve} ease-out, filter {durations.nodeDissolve} ease-out`, + }, + exit: { + transform: 'scale3d(1, 1, 1)', + filter: 'blur(0)', + transformOrigin: 'left', + }, + exitActive: { + opacity: 0, + transform: 'scale3d(0.5, 0.5, 0.5)', + filter: 'blur(4px)', + transformOrigin: 'left', + transition: `opacity {durations.nodeDissolve} ease-out, transform {durations.nodeDissolve} ease-out, filter {durations.nodeDissolve} ease-out`, + }, + }, }, }, staticCss: ['*'],