import React, { useReducer, Reducer, useEffect, useMemo } from 'react'; import { StackedTrackProps, StackedTrackState, StackedTrackAction } from './types'; import { isReactElement } from '../../utils'; function reducer(previousState: StackedTrackState, action: StackedTrackAction): StackedTrackState { switch (action.type) { case "childHeightChanged": if (action.height === previousState.heights[action.index]) return previousState; const newHeights = [ ...previousState.heights ]; newHeights[action.index] = action.height; return { ...previousState, heights: newHeights }; case "childrenAdded": return { ...previousState, heights: [ ...previousState.heights, ...action.heights ] }; } } /** * Automatically stacks a number of child tracks on top of each other, updating y-coordinates * appropriately as the tracks render themselves and update their heights. */ const StackedTracks: React.FC = props => { const [ state, dispatch ] = useReducer>( reducer, { heights: React.Children.map( props.children, child => isReactElement(child) && (child as React.ReactElement)!.props.height || 0 ) || [] } ); const offsets = useMemo( () => ( state.heights.reduce( (v, c) => [ ...v, c + v[v.length - 1] ], [ 0 ]) ), [ state.heights ]); useEffect( () => { state.heights.length < React.Children.toArray(props.children).length && dispatch({ type: 'childrenAdded', heights: React.Children.toArray(props.children).slice(state.heights.length).map( child => isReactElement(child) && (child as React.ReactElement)!.props.height || 0 ) }); }, [ props.children ]); useEffect( () => { props.onHeightChanged && props.onHeightChanged(state.heights.reduce( (x, c) => x + c, 0 )); }, [ state.heights ]); return ( { React.Children.map( props.children, (child: React.ReactNode, i: number) => ( isReactElement(child) ? ( { React.cloneElement( child, { ...child.props, svgRef: props.svgRef, onHeightChanged: (height: number) => { dispatch({ type: "childHeightChanged", index: i, height }); } } ) } ) : ( {child} ) ))} ); } export default StackedTracks;