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 missing frame when animating thought up/down #2765

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion src/components/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import preventAutoscroll, { preventAutoscrollEnd } from '../device/preventAutoscroll'
import * as selection from '../device/selection'
import globals from '../globals'
import useTreeNodeAnimation from '../hooks/useTreeNodeAnimation'
import findDescendant from '../selectors/findDescendant'
import { anyChild, getAllChildrenAsThoughts } from '../selectors/getChildren'
import getContexts from '../selectors/getContexts'
Expand Down Expand Up @@ -135,6 +136,7 @@ const Editable = ({
const oldValueRef = useRef(value)
const nullRef = useRef<HTMLInputElement>(null)
const contentRef = editableRef || nullRef
const { isAnimating } = useTreeNodeAnimation()

/** Used to prevent edit mode from being incorrectly activated on long tap. The default browser behavior must be prevented if setCursorOnThought was just called. */
// https://github.com/cybersemics/em/issues/1793
Expand Down Expand Up @@ -593,7 +595,7 @@ const Editable = ({
className={cx(
multiline ? multilineRecipe() : null,
editableRecipe({
preventAutoscroll: true,
preventAutoscroll: !isAnimating,
}),
className,
)}
Expand Down
442 changes: 222 additions & 220 deletions src/components/LayoutTree.tsx
raineorshine marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions src/components/TreeNodeAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ReactElement, cloneElement, useLayoutEffect, useRef, useState } from 'react'
import { TreeNodeAnimationContext } from './TreeNodeAnimationContext'

interface TreeNodeAnimationProviderProps {
children: ReactElement
y: number
transitionGroupsProps?: object
}

/**
* TreeNodeAnimationProvider component to provide animation context to its children.
*
* @param props - The props for the provider.
* @param props.children - The children components.
* @param props.y - The initial y value.
* @param [props.transitionGroupsProps] - Additional props to pass to the children.
* @returns The provider component.
*/
const TreeNodeAnimationProvider = ({
children,
y: _y,
...transitionGroupsProps
}: TreeNodeAnimationProviderProps): ReactElement => {
const isAnimatingRef = useRef(false)
const [y, setY] = useState(0)
const nodeRef = useRef<HTMLDivElement>(null)

useLayoutEffect(() => {
const currentNode = nodeRef.current
/**
* Handle the transition end event.
*/
const handleTransitionEnd = () => {
// Mark as animation done after the transition ends
requestAnimationFrame(() => {
isAnimatingRef.current = false
})
}

if (currentNode) {
currentNode.addEventListener('transitionend', handleTransitionEnd)
}

if (y !== _y) {
// Mark as animating
isAnimatingRef.current = true
// When y changes React re-renders the component with the new value of y. It will result in a visual change in the DOM.
// Because this is a state-driven change, React applies the updated value to the DOM, which causes the browser to recognize that
// a CSS property has changed, thereby triggering the CSS transition.
// Without this additional render, updates get batched and subsequent CSS transitions may not work properly. For example, when moving a thought down, it would not animate.
setY(_y)
}

return () => {
if (currentNode) {
currentNode.removeEventListener('transitionend', handleTransitionEnd)
}
}
}, [y, _y])

return (
<TreeNodeAnimationContext.Provider value={{ isAnimating: isAnimatingRef.current, y }}>
{cloneElement(children, { ...transitionGroupsProps, ref: nodeRef })}
</TreeNodeAnimationContext.Provider>
)
}

export default TreeNodeAnimationProvider
8 changes: 8 additions & 0 deletions src/components/TreeNodeAnimationContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react'

export interface TreeNodeAnimationContextProps {
isAnimating: boolean
y: number
}

export const TreeNodeAnimationContext = createContext<TreeNodeAnimationContextProps | undefined>(undefined)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
trevinhofmann marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/hooks/useTreeNodeAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useContext } from 'react'
import { TreeNodeAnimationContext, TreeNodeAnimationContextProps } from '../components/TreeNodeAnimationContext'

/** Custom hook to use the TreeNodeAnimation context. Will throw an error if used outside of a TreeNodeAnimationProvider. */
const useTreeNodeAnimation = (): TreeNodeAnimationContextProps => {
const context = useContext(TreeNodeAnimationContext)
if (!context) {
throw new Error('useTreeNodeAnimation must be used within a TreeNodeAnimationProvider')
}
return context
}

export default useTreeNodeAnimation
Loading