diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/debugger/src/components/shared/Popover.js | |
parent | Initial commit. (diff) | |
download | firefox-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.js | 336 |
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; |