import * as React from "react"; import { compose, pure, withState } from "recompose"; import { portal } from "../portal/controller"; import { Bounds, mergeBounds, getBoundsSize, Point } from "tandem-common"; import { BaseContentProps } from "./view.pc"; const calcPortalPosition = ( centered: boolean, anchorRect: Bounds, portalRect: Bounds ) => { const portalSize = getBoundsSize(portalRect); const anchorSize = getBoundsSize(anchorRect); return { left: Math.min( anchorRect.left + (centered ? (anchorRect.right - anchorRect.left) / 2 : 0), window.innerWidth - portalSize.width ), top: Math.min( anchorRect.top + anchorSize.height, window.innerHeight - portalSize.height ) }; }; // ick, but okay for now. Will need to change // when tests are added let _popoverCount = 0; export type Props = { onShouldClose: any; centered?: boolean; anchorRect: Bounds; children?: any; updateContentPosition?: (position: Point, rect: Bounds) => Point; } & BaseContentProps; export default compose( (Base: React.ComponentClass) => { return class extends React.Component { private _emptySpaceListener: any; private _scrollListener: any; private _popoverIndex: number; setContainer = (container: HTMLDivElement) => { const { onShouldClose } = this.props; if (this._emptySpaceListener) { if (this._popoverIndex) { _popoverCount--; } document.body.removeEventListener( "mousedown", this._emptySpaceListener ); document.removeEventListener("scroll", this._scrollListener, true); this._emptySpaceListener = null; } if (container && onShouldClose) { // note that we keep track of how many popovers there are currently in the root in case any pop over // opens a separate popover -- in this case the user will need to click multiple times in order to close // all popovers this._popoverIndex = ++_popoverCount; document.body.addEventListener( "mousedown", (this._emptySpaceListener = event => { if ( !container.contains(event.target) && _popoverCount === this._popoverIndex ) { // beat onClick handler for dropdown button setImmediate(() => { onShouldClose(event); }); } }) ); document.addEventListener( "scroll", (this._scrollListener = event => { if (!container.contains(event.target)) { onShouldClose(event); } }), true ); } }; render() { const { anchorRect, children, setContainer, ...rest } = this.props; return anchorRect ? (
{children}
) : null; } }; }, pure, withState(`style`, `setStyle`, null), portal({ didMount: ({ centered, anchorRect, setStyle, updateContentPosition }) => portalMount => { if ( !portalMount || !portalMount.children[0] || !portalMount.children[0].children[0] ) { return; } const popoverRect = calcInnerBounds(portalMount.children[0].children[0] .children[0] as HTMLElement); let position = calcPortalPosition(centered, anchorRect, popoverRect); if (updateContentPosition) { position = updateContentPosition(position, popoverRect); } const newStyle = { position: "absolute", zIndex: 1024, ...position }; setStyle(newStyle); } }) ); const calcInnerBounds = ( element: HTMLElement, maxDepth: number = 0, depth: number = 0 ): Bounds => { const rect: ClientRect = element.getBoundingClientRect(); if (depth >= maxDepth) return rect; return mergeBounds( ...Array.from(element.children).reduce( (rects, child: HTMLElement) => { return [...rects, calcInnerBounds(child)]; }, [rect] ) ); };