/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ // @flow import React, { Component } from "react"; import classNames from "classnames"; import BracketArrow from "./BracketArrow"; import "./Popover.css"; type Props = { editorRef: ?HTMLDivElement, targetPosition: Object, children: ?React$Element, onMouseLeave: (e: SyntheticMouseEvent) => void, type?: "popover" | "tooltip" }; type Orientation = "up" | "down" | "right"; type TargetMid = { x: number, y: number }; type State = { left: number, top: number, targetMid: TargetMid, orientation: Orientation }; class Popover extends Component { $popover: ?HTMLDivElement; $tooltip: ?HTMLDivElement; constructor(props: Props) { super(props); this.state = { left: 0, top: 0, targetMid: { x: 0, y: 0 }, orientation: "up" }; } static defaultProps = { onMouseLeave: () => {}, type: "popover" }; componentDidMount() { const { type } = this.props; const { left, top, orientation, targetMid } = type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords(); // eslint-disable-next-line react/no-did-mount-set-state this.setState({ left, top, orientation, targetMid }); } calculateLeft( target: ClientRect, editor: ClientRect, popover: ClientRect, orientation?: Orientation ) { const estimatedLeft = target.left; const estimatedRight = estimatedLeft + popover.width; const isOverflowingRight = estimatedRight > editor.right; if (orientation === "right") { return target.left + target.width + 10; } if (isOverflowingRight) { const adjustedLeft = editor.right - popover.width - 8; return adjustedLeft; } return estimatedLeft; } calculateTopForRightOrientation = ( target: ClientRect, editor: ClientRect, popover: ClientRect ) => { if (popover.height < editor.height) { const rightOrientationTop = target.top - popover.height / 2; if (rightOrientationTop < editor.top) { return editor.top; } const rightOrientationBottom = rightOrientationTop + popover.height; if (rightOrientationBottom > editor.bottom) { return editor.bottom - popover.height; } return rightOrientationTop; } return 0; }; calculateOrientation( target: ClientRect, editor: ClientRect, popover: ClientRect ) { const estimatedBottom = target.bottom + popover.height; if (editor.bottom > estimatedBottom) { return "down"; } const upOrientationTop = target.top - popover.height; if (upOrientationTop > editor.top) { return "up"; } return "right"; } calculateTop = ( target: ClientRect, editor: ClientRect, popover: ClientRect, orientation?: string ) => { if (orientation === "down") { return target.bottom; } if (orientation === "up") { return target.top - popover.height; } return this.calculateTopForRightOrientation(target, editor, popover); }; getPopoverCoords() { if (this.$popover && this.props.editorRef) { const popover = this.$popover; const editor = this.props.editorRef; const popoverRect = popover.getBoundingClientRect(); const editorRect = editor.getBoundingClientRect(); const targetRect = this.props.targetPosition; const orientation = this.calculateOrientation( targetRect, editorRect, popoverRect ); const top = this.calculateTop( targetRect, editorRect, popoverRect, orientation ); const popoverLeft = this.calculateLeft( targetRect, editorRect, popoverRect, orientation ); let targetMid; if (orientation === "right") { targetMid = { x: -14, y: targetRect.top - top - 2 }; } else { targetMid = { x: targetRect.left - popoverLeft + targetRect.width / 2 - 8, y: 0 }; } return { left: popoverLeft, top, orientation, targetMid }; } return { left: 0, top: 0, orientation: "down", targetMid: { x: 0, y: 0 } }; } getTooltipCoords() { if (this.$tooltip && this.props.editorRef) { const tooltip = this.$tooltip; const editor = this.props.editorRef; const tooltipRect = tooltip.getBoundingClientRect(); const editorRect = editor.getBoundingClientRect(); const targetRect = this.props.targetPosition; const left = this.calculateLeft(targetRect, editorRect, tooltipRect); const top = targetRect.top - tooltipRect.height; return { left, top, orientation: "up", targetMid: { x: 0, y: 0 } }; } return { left: 0, top: 0, orientation: "up", targetMid: { x: 0, y: 0 } }; } getChildren() { const { children } = this.props; const { orientation } = this.state; const gap =
; return orientation === "up" ? [children, gap] : [gap, children]; } getPopoverArrow(orientation: Orientation, left: number, top: number) { const arrowProps = {}; if (orientation === "up") { Object.assign(arrowProps, { orientation: "down", bottom: 5, left }); } else if (orientation === "down") { Object.assign(arrowProps, { orientation: "up", top: -7, left }); } else { Object.assign(arrowProps, { orientation: "left", top, left: -14 }); } return ; } onMouseLeave = (e: SyntheticMouseEvent) => { const { onMouseLeave } = this.props; if (/^(bracket-arrow|gap)$/.test(e.currentTarget.className)) { return; } onMouseLeave(e); }; renderPopover() { const { top, left, orientation, targetMid } = this.state; const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y); return (
(this.$popover = c)} > {arrow} {this.getChildren()}
); } renderTooltip() { const { onMouseLeave } = this.props; const { top, left } = this.state; return (
(this.$tooltip = c)} > {this.getChildren()}
); } render() { const { type } = this.props; if (type === "tooltip") { return this.renderTooltip(); } return this.renderPopover(); } } export default Popover;