Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/blur shrink ondelete #2596

Merged
merged 8 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
8 changes: 8 additions & 0 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 useCachedNode from '../hooks/useCachedNode'
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 DOM before it is deleted
const cachedHTMLRef = useCachedNode({ thought, elementRef: ref })
const noOtherContexts = useSelector(
state => thought && isContextViewActive(state, simplePath) && getContexts(state, thought.value).length <= 1,
)
Expand Down Expand Up @@ -114,6 +117,11 @@ const Subthought = ({
ref.current.style.opacity = opacity
})

// If the thought is deleted, return the cached static HTML from the ref
if (!thought && cachedHTMLRef.current) {
return <div dangerouslySetInnerHTML={{ __html: cachedHTMLRef.current }} />
zukilover marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
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
57 changes: 57 additions & 0 deletions src/hooks/useCachedNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 regex = new RegExp(`\\b(${modifiers.join('|')})\\b`, 'g')

/**
* Custom hook to capture and cache the static HTML string of a node.
*
* @param thought - The thought object.
* @param elementRef - The ref of the node.
* @returns The cached HTML string.
*/
const useCachedNode = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest a more specific name for this hook, e.g. useCachedThoughtHtml, since it has several thought-specific pieces of functionality.

We can also add to the JSDOC to explain a little more about what it does and where it's useful.

thought,
elementRef: ref,
}: {
thought: Thought
elementRef: React.RefObject<HTMLDivElement>
}) => {
// Cache the DOM before it is deleted
const cachedHTMLRef = useRef<string | null>(null)

/**
* Cleans up editable classes from the provided HTML string.
*
* @param htmlString - The HTML string to clean up.
* @returns The cleaned HTML string.
*/
const cleanUpEditableClasses = (htmlString: string) => {
// Replace BEM modifier classes with an empty string
return htmlString.replace(regex, '').trim()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this is just a single .replace, it doesn't need to be wrapped in a function.


// Capture the static HTML string when the thought is first rendered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commend needs a slight tweak. It captures the HTML not just when the thought is first rendered, but whenever the thought changes.

useEffect(() => {
if (thought && ref.current) {
cachedHTMLRef.current = cleanUpEditableClasses(ref.current.innerHTML)
}
}, [thought, ref])

return cachedHTMLRef
}

export default useCachedNode
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
Loading