import React from "react" export interface VerticalLayoutComponentProps { /** Height of component */ height: number /** Fraction to allocate for fractional heights. Should total 1.0. Keyed by key of child */ relativeHeights: { [key: string]: number } /** Children to render */ children?: React.ReactNode } interface VerticalLayoutComponentState { availableHeight: number | null } // Lays out divs vertically, allowing fractional allocation combined with auto-sized ones // Children must all have keys // Children will be cloned with height: prop set in case of fractional ones export default class VerticalLayoutComponent extends React.Component< VerticalLayoutComponentProps, VerticalLayoutComponentState > { contents: { [key: string]: any } = {} constructor(props: VerticalLayoutComponentProps) { super(props) this.state = { availableHeight: null } } componentDidUpdate(prevProps: VerticalLayoutComponentProps) { if ( prevProps.height !== this.props.height || JSON.stringify(prevProps.relativeHeights) !== JSON.stringify(this.props.relativeHeights) ) { this.recalculateSize() } } componentDidMount() { // Use setTimeout to ensure DOM is fully rendered before calculating sizes setTimeout(() => this.recalculateSize(), 0) } recalculateSize = () => { // Calculate available height let availableHeight = this.props.height React.Children.forEach(this.props.children, (child) => { if (!React.isValidElement(child)) { return } // If child has relative height, skip if (child.key && this.props.relativeHeights[child.key]) { return } const node = child.key != null ? this.contents[child.key] : null if (node) { availableHeight -= node.offsetHeight } }) this.setState({ availableHeight }) } // Get a subcomponent getComponent(key: string) { return this.contents[key] } render() { // Calculate scaling return (
{React.Children.map(this.props.children, (child) => { if (!React.isValidElement(child)) { return child } // If variable height if (child.key != null && this.props.relativeHeights[child.key] != null) { // If available height is known, render variable if (this.state.availableHeight != null) { const height = this.state.availableHeight * this.props.relativeHeights[child.key] return (
{React.cloneElement(child, { height, ref: (c: any) => { this.contents[child.key!] = c // Forward the original ref if it exists const originalRef = (child.props as any).ref ?? (child as any).ref if (originalRef != null) { if (typeof originalRef === 'function') { originalRef(c) } else if (originalRef && typeof originalRef === 'object') { originalRef.current = c } } } })}
) } // Otherwise don't show until available height is known return null } if (child.key != null) { return React.cloneElement(child, { ref: (c: any) => { this.contents[child.key!] = c // Forward the original ref if it exists const originalRef = (child.props as any).ref ?? (child as any).ref if (originalRef != null) { if (typeof originalRef === 'function') { originalRef(c) } else if (originalRef && typeof originalRef === 'object') { originalRef.current = c } } } }) } return child })}
) } }