summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/shared/Popover.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/debugger/src/components/shared/Popover.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/components/shared/Popover.js')
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.js336
1 files changed, 336 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..5b5b78d364
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.js
@@ -0,0 +1,336 @@
+/* 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/>. */
+
+// @flow
+import React, { Component } from "react";
+import classNames from "classnames";
+import BracketArrow from "./BracketArrow";
+import SmartGap from "./SmartGap";
+
+import "./Popover.css";
+
+type Props = {
+ editorRef: ?HTMLDivElement,
+ targetPosition: Object,
+ children: ?React$Element<any>,
+ target: HTMLDivElement,
+ type?: "popover" | "tooltip",
+ mouseout: Function,
+};
+
+type Orientation = "up" | "down" | "right";
+type TargetMid = {
+ x: number,
+ y: number,
+};
+export type Coords = {
+ left: number,
+ top: number,
+ targetMid: TargetMid,
+ orientation: Orientation,
+};
+
+type State = { coords: Coords };
+
+class Popover extends Component<Props, State> {
+ $popover: ?HTMLDivElement;
+ $tooltip: ?HTMLDivElement;
+ $gap: ?HTMLDivElement;
+ timerId: ?TimeoutID;
+ wasOnGap: boolean;
+ state = {
+ coords: {
+ left: 0,
+ top: 0,
+ orientation: "down",
+ targetMid: { x: 0, y: 0 },
+ },
+ };
+ firstRender = true;
+ gapHeight: number;
+ gapHeight: number;
+
+ static defaultProps = {
+ type: "popover",
+ };
+
+ componentDidMount() {
+ const { type } = this.props;
+ // $FlowIgnore
+ this.gapHeight = this.$gap.getBoundingClientRect().height;
+ const coords =
+ type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+
+ this.firstRender = false;
+ this.startTimer();
+ }
+
+ 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;
+ }
+ return this.props.mouseout();
+ }
+
+ // 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: 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;
+ }
+ 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 - 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: 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) {
+ 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)}>
+ <SmartGap
+ token={this.props.target}
+ preview={this.$tooltip || this.$popover}
+ type={this.props.type}
+ gapHeight={this.gapHeight}
+ coords={this.state.coords}
+ // $FlowIgnore
+ offset={this.$gap.getBoundingClientRect().left}
+ />
+ </div>
+ );
+ }
+
+ getPopoverArrow(orientation: Orientation, left: number, top: number) {
+ 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 <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()}
+ </div>
+ );
+ }
+
+ renderTooltip() {
+ const { top, left, orientation } = this.state.coords;
+ return (
+ <div
+ className={classNames("tooltip", `orientation-${orientation}`)}
+ style={{ top, left }}
+ ref={c => (this.$tooltip = c)}
+ >
+ {this.getChildren()}
+ </div>
+ );
+ }
+
+ render() {
+ const { type } = this.props;
+
+ if (type === "tooltip") {
+ return this.renderTooltip();
+ }
+
+ return this.renderPopover();
+ }
+}
+
+export default Popover;