Skip to content

Commit

Permalink
Fix/blur shrink ondelete (cybersemics#2596)
Browse files Browse the repository at this point in the history
Co-authored-by: Raine Revere <[email protected]>
  • Loading branch information
2 people authored and snqb committed Jan 5, 2025
1 parent 0e0feb8 commit 767708e
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/components/LayoutTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions src/components/Subthought.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -59,6 +60,8 @@ const Subthought = ({
const state = store.getState()
const ref = useRef<HTMLDivElement>(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,
)
Expand Down Expand Up @@ -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 ? (
<div dangerouslySetInnerHTML={{ __html: cachedThoughtHtmlRef.current }} />
) : null
}

return (
<>
Expand Down
2 changes: 2 additions & 0 deletions src/durations.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
46 changes: 46 additions & 0 deletions src/hooks/useCachedThoughtHtml.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>
}) => {
// Cache the DOM before it is deleted
const cachedHTMLRef = useRef<string | null>(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
23 changes: 23 additions & 0 deletions src/recipes/fadeTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: ['*'],
Expand Down

0 comments on commit 767708e

Please sign in to comment.