summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/shared/Popover.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/shared/Popover.js')
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.js324
1 files changed, 324 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/shared/Popover.js b/devtools/client/debugger/src/components/shared/Popover.js
new file mode 100644
index 0000000000..8748e36418
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.js
@@ -0,0 +1,324 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import BracketArrow from "./BracketArrow";
+import SmartGap from "./SmartGap";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Popover extends Component {
+ state = {
+ coords: {
+ left: 0,
+ top: 0,
+ orientation: "down",
+ targetMid: { x: 0, y: 0 },
+ },
+ };
+ firstRender = true;
+
+ static defaultProps = {
+ type: "popover",
+ };
+
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ editorRef: PropTypes.object.isRequired,
+ mouseout: PropTypes.func.isRequired,
+ target: PropTypes.object.isRequired,
+ targetPosition: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { type } = this.props;
+ this.gapHeight = this.$gap.getBoundingClientRect().height;
+ const coords =
+ type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+
+ this.firstRender = false;
+ this.startTimer();
+ }
+
+ componentDidUpdate(prevProps) {
+ // We have to update `coords` when the Popover type changes
+ if (
+ prevProps.type != this.props.type ||
+ prevProps.target !== this.props.target
+ ) {
+ const coords =
+ this.props.type == "popover"
+ ? this.getPopoverCoords()
+ : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.timerId) {
+ clearTimeout(this.timerId);
+ }
+ }
+
+ startTimer() {
+ this.timerId = setTimeout(this.onTimeout, 0);
+ }
+
+ onTimeout = () => {
+ const isHoveredOnGap = this.$gap && this.$gap.matches(":hover");
+ const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover");
+ const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover");
+ const isHoveredOnTarget = this.props.target.matches(":hover");
+
+ if (isHoveredOnGap) {
+ if (!this.wasOnGap) {
+ this.wasOnGap = true;
+ this.timerId = setTimeout(this.onTimeout, 200);
+ return;
+ }
+ this.props.mouseout();
+ return;
+ }
+
+ // Don't clear the current preview if mouse is hovered on
+ // the current preview's token (target) or the popup element
+ if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) {
+ this.wasOnGap = false;
+ this.timerId = setTimeout(this.onTimeout, 0);
+ return;
+ }
+
+ this.props.mouseout();
+ };
+
+ calculateLeft(target, editor, popover, orientation) {
+ const estimatedLeft = target.left;
+ const estimatedRight = estimatedLeft + popover.width;
+ const isOverflowingRight = estimatedRight > editor.right;
+ if (orientation === "right") {
+ return target.left + target.width;
+ }
+ if (isOverflowingRight) {
+ const adjustedLeft = editor.right - popover.width - 8;
+ return adjustedLeft;
+ }
+ return estimatedLeft;
+ }
+
+ calculateTopForRightOrientation = (target, editor, popover) => {
+ if (popover.height <= editor.height) {
+ const rightOrientationTop = target.top - popover.height / 2;
+ if (rightOrientationTop < editor.top) {
+ return editor.top - target.height;
+ }
+ const rightOrientationBottom = rightOrientationTop + popover.height;
+ if (rightOrientationBottom > editor.bottom) {
+ return editor.bottom + target.height - popover.height + this.gapHeight;
+ }
+ return rightOrientationTop;
+ }
+ return editor.top - target.height;
+ };
+
+ calculateOrientation(target, editor, popover) {
+ 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, editor, popover, orientation) => {
+ 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) {
+ return null;
+ }
+
+ 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,
+ };
+ }
+
+ getTooltipCoords() {
+ if (!this.$tooltip || !this.props.editorRef) {
+ return null;
+ }
+ 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 enoughRoomForTooltipAbove =
+ targetRect.top - editorRect.top > tooltipRect.height;
+ const top = enoughRoomForTooltipAbove
+ ? targetRect.top - tooltipRect.height
+ : targetRect.bottom;
+
+ return {
+ left,
+ top,
+ orientation: enoughRoomForTooltipAbove ? "up" : "down",
+ targetMid: { x: 0, y: 0 },
+ };
+ }
+
+ getChildren() {
+ const { children } = this.props;
+ const { coords } = this.state;
+ const gap = this.getGap();
+
+ return coords.orientation === "up" ? [children, gap] : [gap, children];
+ }
+
+ getGap() {
+ if (this.firstRender) {
+ return div({
+ className: "gap",
+ key: "gap",
+ ref: a => (this.$gap = a),
+ });
+ }
+
+ return div(
+ {
+ className: "gap",
+ key: "gap",
+ ref: a => (this.$gap = a),
+ },
+ React.createElement(SmartGap, {
+ token: this.props.target,
+ preview: this.$tooltip || this.$popover,
+ type: this.props.type,
+ gapHeight: this.gapHeight,
+ coords: this.state.coords,
+ offset: this.$gap.getBoundingClientRect().left,
+ })
+ );
+ }
+
+ getPopoverArrow(orientation, left, top) {
+ let arrowProps = {};
+
+ if (orientation === "up") {
+ arrowProps = { orientation: "down", bottom: 10, left };
+ } else if (orientation === "down") {
+ arrowProps = { orientation: "up", top: -2, left };
+ } else {
+ arrowProps = { orientation: "left", top, left: -4 };
+ }
+ return React.createElement(BracketArrow, arrowProps);
+ }
+
+ renderPopover() {
+ const { top, left, orientation, targetMid } = this.state.coords;
+ const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y);
+ return div(
+ {
+ className: classnames("popover", `orientation-${orientation}`, {
+ up: orientation === "up",
+ }),
+ style: {
+ top,
+ left,
+ },
+ ref: c => (this.$popover = c),
+ },
+ arrow,
+ this.getChildren()
+ );
+ }
+
+ renderTooltip() {
+ const { top, left, orientation } = this.state.coords;
+ return div(
+ {
+ className: `tooltip orientation-${orientation}`,
+ style: {
+ top,
+ left,
+ },
+ ref: c => (this.$tooltip = c),
+ },
+ this.getChildren()
+ );
+ }
+
+ render() {
+ const { type } = this.props;
+
+ if (type === "tooltip") {
+ return this.renderTooltip();
+ }
+
+ return this.renderPopover();
+ }
+}
+
+export default Popover;