import { Component, createElement, ComponentClass, ReactNode } from 'react'; import getPassThrough, { PassTroughFunction } from '../../utils/getPassThrough'; import measureElement from '../../utils/measureElement'; export type OriginTopLeft = 'topLeft'; export type OriginTopRight = 'topRight'; export type OriginBottomLeft = 'bottomLeft'; export type OriginBottomRight = 'bottomRight'; export type Origin = | OriginTopLeft | OriginTopRight | OriginBottomLeft | OriginBottomRight; export const ORIGINS = { BOTTOM_LEFT: 'bottomLeft' as OriginBottomLeft, BOTTOM_RIGHT: 'bottomRight' as OriginBottomRight, TOP_LEFT: 'topLeft' as OriginTopLeft, TOP_RIGHT: 'topRight' as OriginTopRight, }; export interface ClippingBoxProps { active: boolean; children: ReactNode; className?: string; innerRef(instance: HTMLElement): void; origin: Origin; } export interface InnerNodeProps { active: boolean; height?: number; innerRef(instance: HTMLElement): void; origin: Origin; width?: number; } export interface WrapperNodeProps { active: boolean; className?: string; height?: number; innerRef(instance: HTMLElement): void; width?: number; } export interface OutlineNodeProps { active: boolean; height?: number; origin: Origin; width?: number; } export interface ClippingBoxState { active: boolean; height?: number; width?: number; } export interface ClippingBoxFactoryArgs { InnerNode: ComponentClass; OutlineNode: ComponentClass; WrapperNode: ComponentClass; passthrough: PassTroughFunction< ClippingBoxProps, 'InnerNode' | 'WrapperNode' | 'OutlineNode' >; } export default function clippingBoxFactory({ InnerNode, OutlineNode, WrapperNode, passthrough, }: ClippingBoxFactoryArgs): ComponentClass { const passProps = getPassThrough(passthrough); return class ClippingBox extends Component< ClippingBoxProps, ClippingBoxState > { static defaultProps = { active: false, origin: 'topLeft' as OriginTopLeft, }; state = { active: this.props.active, height: undefined, width: undefined, }; clipperNode: HTMLElement | null = null; requestPositionFrame: number | null = null; componentDidMount() { this.requestPositionFrame = requestAnimationFrame(() => { if (this.clipperNode) { measureElement(this.clipperNode).then(({ height, width }) => { this.setState({ height, width }); }); } }); } componentDidUpdate(prevProps) { if ( this.props.active !== prevProps.active || this.props.children !== prevProps.children ) { this.resize(); } } componentWillUnmount() { if (this.requestPositionFrame) { cancelAnimationFrame(this.requestPositionFrame); } } handleInnerNode = node => { this.clipperNode = node; }; resize() { this.requestPositionFrame = requestAnimationFrame(() => { if (this.clipperNode) { const { height, width } = this.clipperNode.getBoundingClientRect(); this.setState({ active: this.props.active, height, width }); } }); } render() { const { active, height, width } = this.state; const { children, className, innerRef, origin } = this.props; return createElement( WrapperNode, { ...passProps(this.props, 'WrapperNode', this), active, className, height, innerRef, width, }, createElement(OutlineNode, { ...passProps(this.props, 'OutlineNode', this), active, height, origin, width, }), createElement( InnerNode, { ...passProps(this.props, 'InnerNode', this), innerRef: this.handleInnerNode, active, height, origin, width, }, children, ), ); } }; }