diff --git a/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx b/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx index 635351a..a4aa6ed 100644 --- a/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx +++ b/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx @@ -1,5 +1,5 @@ import * as q from '../../../../../backend/src/Model' -import React from 'react' +import React, { useEffect, useState, useMemo } from 'react' import TreeNode from '.' import { SettingsState } from '../../../reducers/Settings' import { sortedNodes } from '../../../sortedNodes' @@ -16,59 +16,54 @@ export interface Props { selectTopicAction: (treeNode: q.TreeNode) => void settings: SettingsState actions: typeof treeActions + theme: Theme } -interface State { - alreadyAdded: number +function useStagedRendering(treeNode: q.TreeNode) { + const [alreadyAdded, setAlreadyAdded] = useState(10) + const edges = treeNode.edgeArray + + useEffect(() => { + let renderMoreAnimationFrame: any + + if (alreadyAdded < edges.length) { + renderMoreAnimationFrame = (window as any).requestIdleCallback( + () => { + setAlreadyAdded(alreadyAdded * 1.5) + }, + { timeout: 500 } + ) + } + + return function cleanup() { + ;(window as any).cancelIdleCallback(renderMoreAnimationFrame) + } + }, [alreadyAdded, edges.length]) + + return alreadyAdded } -class TreeNodeSubnodes extends React.Component { - private renderMoreAnimationFrame?: any - constructor(props: Props) { - super(props) - this.state = { alreadyAdded: 10 } - } +function TreeNodeSubnodes(props: Props) { + const alreadyAdded = useStagedRendering(props.treeNode) - private renderMore() { - this.renderMoreAnimationFrame = (window as any).requestIdleCallback( - () => { - this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 }) - }, - { timeout: 500 } - ) - } - - public componentWillUnmount() { - ;(window as any).cancelIdleCallback(this.renderMoreAnimationFrame) - } - - public render() { - const edges = this.props.treeNode.edgeArray - if (edges.length === 0) { - return null - } - - if (this.state.alreadyAdded < edges.length) { - this.renderMore() - } - - const nodes = sortedNodes(this.props.settings, this.props.treeNode).slice(0, this.state.alreadyAdded) + return useMemo(() => { + const nodes = sortedNodes(props.settings, props.treeNode).slice(0, alreadyAdded) const listItems = nodes.map(node => { return ( ) }) - return {listItems} - } + return {listItems} + }, [alreadyAdded, props.lastUpdate, props.theme]) } const styles = (theme: Theme) => ({ @@ -79,4 +74,4 @@ const styles = (theme: Theme) => ({ }, }) -export default withStyles(styles)(TreeNodeSubnodes) +export default withStyles(styles, { withTheme: true })(TreeNodeSubnodes) diff --git a/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx b/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx index d23a505..5fe8fa0 100644 --- a/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx +++ b/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx @@ -1,22 +1,30 @@ import React, { useEffect } from 'react' export function useAnimationToIndicateTopicUpdate( + ref: React.MutableRefObject, lastUpdate: number, + className: string, selected: boolean, - setShowUpdateAnimation: React.Dispatch>, - showUpdateAnimation: boolean + shouldAnimate: boolean ) { useEffect(() => { - if (Date.now() - lastUpdate < 3000 && !selected) { - setShowUpdateAnimation(true) - } - }, [lastUpdate, selected]) - useEffect(() => { - if (showUpdateAnimation) { - const timeout = setTimeout(() => setShowUpdateAnimation(false), 500) + if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected) { + let animationFrame = requestAnimationFrame(() => { + ref.current && ref.current.classList.add(className) + }) + + const timeout = setTimeout( + () => + (animationFrame = requestAnimationFrame(() => { + ref.current && ref.current.classList.remove(className) + })), + 500 + ) + return function cleanup() { clearTimeout(timeout) + animationFrame && cancelAnimationFrame(animationFrame) } } - }, [showUpdateAnimation]) + }, [lastUpdate, selected, shouldAnimate, className]) } diff --git a/app/src/components/Tree/TreeNode/index.tsx b/app/src/components/Tree/TreeNode/index.tsx index 51903f5..ce6205e 100644 --- a/app/src/components/Tree/TreeNode/index.tsx +++ b/app/src/components/Tree/TreeNode/index.tsx @@ -1,56 +1,16 @@ import * as q from '../../../../../backend/src/Model' -import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeTitle from './TreeNodeTitle' import { SettingsState } from '../../../reducers/Settings' +import { styles } from './styles' import { Theme, withStyles } from '@material-ui/core/styles' import { TopicViewModel } from '../../../model/TopicViewModel' -import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions' import { treeActions } from '../../../actions' -import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors' import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate' import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback' import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState' - -const styles = (theme: Theme) => { - return { - collapsedSubnodes: { - color: theme.palette.text.secondary, - }, - displayBlock: { - display: 'block', - }, - node: { - display: 'block', - marginLeft: '10px', - '&:hover': { - backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light, - }, - }, - topicSelect: { - float: 'right' as 'right', - opacity: 0, - cursor: 'pointer', - marginTop: '-1px', - }, - subnodes: { - marginLeft: theme.spacing(1.5), - }, - selected: { - backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important', - }, - hover: {}, - title: { - borderRadius: '4px', - lineHeight: '1em', - display: 'inline-block' as 'inline-block', - whiteSpace: 'nowrap' as 'nowrap', - padding: '1px 4px 0px 4px', - height: '14px', - margin: '1px 0px 2px 0px', - }, - } -} +import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions' export interface Props { isRoot?: boolean @@ -69,13 +29,21 @@ export interface Props { function TreeNodeComponent(props: Props) { const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions) - const [showUpdateAnimation, setShowUpdateAnimation] = useState(false) const [collapsedOverride, setCollapsedOverride] = useState(undefined) const [selected, setSelected] = useState(false) const nodeRef = useRef() const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props) useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride) - useAnimationToIndicateTopicUpdate(lastUpdate, selected, setShowUpdateAnimation, showUpdateAnimation) + const animationClass = + props.theme.palette.type === 'light' ? props.classes.animationLight : props.classes.animationDark + + useAnimationToIndicateTopicUpdate( + nodeRef, + lastUpdate, + animationClass, + selected, + settings.get('highlightTopicUpdates') + ) const isCollapsed = Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand @@ -141,19 +109,12 @@ function TreeNodeComponent(props: Props) { ) } - const shouldStartAnimation = settings.get('highlightTopicUpdates') && showUpdateAnimation - const animationName = theme.palette.type === 'light' ? 'updateLight' : 'updateDark' - const animation = shouldStartAnimation - ? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` } - : {} - const highlightClass = selected ? classes.selected : '' return (
) - }, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation]) + }, [lastUpdate, treeNode, name, isCollapsed, selected, theme]) } export default withStyles(styles, { withTheme: true })(TreeNodeComponent) diff --git a/app/src/components/Tree/TreeNode/styles.ts b/app/src/components/Tree/TreeNode/styles.ts new file mode 100644 index 0000000..237ff50 --- /dev/null +++ b/app/src/components/Tree/TreeNode/styles.ts @@ -0,0 +1,52 @@ +import { blueGrey } from '@material-ui/core/colors' +import { Theme } from '@material-ui/core/styles' + +export const styles = (theme: Theme) => { + return { + animationLight: { + willChange: 'auto', + translateZ: 0, + animation: `updateLight 0.5s`, + }, + animationDark: { + willChange: 'auto', + translateZ: 0, + animation: `updateLight 0.5s`, + }, + collapsedSubnodes: { + color: theme.palette.text.secondary, + }, + displayBlock: { + display: 'block', + }, + node: { + display: 'block', + marginLeft: '10px', + '&:hover': { + backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light, + }, + }, + topicSelect: { + float: 'right' as 'right', + opacity: 0, + cursor: 'pointer', + marginTop: '-1px', + }, + subnodes: { + marginLeft: theme.spacing(1.5), + }, + selected: { + backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important', + }, + hover: {}, + title: { + borderRadius: '4px', + lineHeight: '1em', + display: 'inline-block' as 'inline-block', + whiteSpace: 'nowrap' as 'nowrap', + padding: '1px 4px 0px 4px', + height: '14px', + margin: '1px 0px 2px 0px', + }, + } +}