Bug 1548369 - Render an SVG in the gap between the token and the preview where it shouldn't unrender the preview right away (WIP)

Still WIP

Differential Revision: https://phabricator.services.mozilla.com/D34910

--HG--
extra : moz-landing-system : lando
This commit is contained in:
jaril 2019-06-28 21:17:51 +00:00
Родитель b9289cedad
Коммит 6e05066510
8 изменённых файлов: 694 добавлений и 94 удалений

Просмотреть файл

@ -12,6 +12,14 @@
box-shadow: 1px 2px 3px var(--popup-shadow-color);
}
.gap svg {
pointer-events: none;
}
.gap polygon {
pointer-events: auto;
}
.theme-dark .popover .preview-popup {
box-shadow: 1px 2px 3px var(--popup-shadow-color);
}
@ -95,7 +103,7 @@
.tooltip .gap {
height: 4px;
padding-top: 4px;
padding-top: 0px;
}
.add-to-expression-bar {

Просмотреть файл

@ -6,7 +6,6 @@
import React, { Component } from "react";
import { connect } from "../../../utils/connect";
import { isTesting } from "devtools-environment";
import Reps from "devtools-reps";
const {
@ -30,7 +29,6 @@ import { createObjectClient } from "../../../client/firefox";
import "./Popup.css";
import type { Coords } from "../../shared/Popover";
import type { ThreadContext } from "../../../types";
import type { Preview } from "../../../reducers/types";
@ -48,32 +46,20 @@ type Props = {
clearPreview: typeof actions.clearPreview,
};
type State = {
top: number,
};
export class Popup extends Component<Props, State> {
export class Popup extends Component<Props> {
marker: any;
pos: any;
popover: ?React$ElementRef<typeof Popover>;
timerId: ?IntervalID;
constructor(props: Props) {
super(props);
this.state = {
top: 0,
};
}
componentDidMount() {
this.startTimer();
this.addHighlightToToken();
}
componentWillUnmount() {
if (this.timerId) {
clearInterval(this.timerId);
}
this.removeHighlightFromToken();
}
@ -93,39 +79,15 @@ export class Popup extends Component<Props, State> {
}
}
startTimer() {
this.timerId = setInterval(this.onInterval, 300);
}
onInterval = () => {
const { preview, clearPreview, cx } = this.props;
// Don't clear the current preview if mouse is hovered on
// the current preview's element (target) or the popup element
// Note, we disregard while testing because it is impossible to hover
const currentTarget = preview.target;
if (
isTesting() ||
currentTarget.matches(":hover") ||
!this.popover ||
(this.popover.$popover && this.popover.$popover.matches(":hover")) ||
(this.popover.$tooltip && this.popover.$tooltip.matches(":hover"))
) {
return;
}
// Clear the interval and the preview if it is not hovered
// on the current preview's element or the popup element
clearInterval(this.timerId);
return clearPreview(cx);
};
calculateMaxHeight = () => {
const { editorRef } = this.props;
if (!editorRef) {
return "auto";
}
return editorRef.getBoundingClientRect().height - this.state.top;
return (
editorRef.getBoundingClientRect().height +
editorRef.getBoundingClientRect().top
);
};
renderFunctionPreview() {
@ -226,8 +188,9 @@ export class Popup extends Component<Props, State> {
return "popover";
}
onPopoverCoords = (coords: Coords) => {
this.setState({ top: coords.top });
onMouseOut = () => {
const { clearPreview, cx } = this.props;
clearPreview(cx);
};
render() {
@ -245,9 +208,9 @@ export class Popup extends Component<Props, State> {
<Popover
targetPosition={cursorPos}
type={type}
onPopoverCoords={this.onPopoverCoords}
editorRef={editorRef}
ref={a => (this.popover = a)}
target={this.props.preview.target}
mouseout={this.onMouseOut}
>
{this.renderPreview()}
</Popover>

Просмотреть файл

@ -9,23 +9,17 @@
--left-offset: -55px;
}
.popover {
position: fixed;
z-index: 100;
}
.popover.orientation-right {
display: flex;
flex-direction: row;
}
.popover.orientation-right .gap {
padding-left: var(--gap-size);
width: var(--gap-size);
}
.popover:not(.orientation-right) .gap {
height: var(--gap-size);
padding-top: var(--gap-size);
margin-left: var(--left-offset);
}

Просмотреть файл

@ -6,6 +6,8 @@
import React, { Component } from "react";
import classNames from "classnames";
import BracketArrow from "./BracketArrow";
import SmartGap from "./SmartGap";
import { isTesting } from "devtools-environment";
import "./Popover.css";
@ -13,8 +15,9 @@ type Props = {
editorRef: ?HTMLDivElement,
targetPosition: Object,
children: ?React$Element<any>,
onPopoverCoords: Function,
target: HTMLDivElement,
type?: "popover" | "tooltip",
mouseout: Function,
};
type Orientation = "up" | "down" | "right";
@ -34,6 +37,9 @@ type State = { coords: Coords };
class Popover extends Component<Props, State> {
$popover: ?HTMLDivElement;
$tooltip: ?HTMLDivElement;
$gap: ?HTMLDivElement;
timerId: ?TimeoutID;
wasOnGap: boolean;
state = {
coords: {
left: 0,
@ -42,14 +48,18 @@ class Popover extends Component<Props, State> {
targetMid: { x: 0, y: 0 },
},
};
firstRender = true;
gapHeight: number;
gapHeight: number;
static defaultProps = {
onPopoverCoords: () => {},
type: "popover",
};
componentDidMount() {
const { type } = this.props;
// $FlowIgnore
this.gapHeight = this.$gap.getBoundingClientRect().height;
const coords =
type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
@ -57,9 +67,52 @@ class Popover extends Component<Props, State> {
this.setState({ coords });
}
this.props.onPopoverCoords(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
// Note, we disregard while testing because it is impossible to hover
if (
isTesting() ||
isHoveredOnPopover ||
isHoveredOnTooltip ||
isHoveredOnTarget
) {
this.wasOnGap = false;
this.timerId = setTimeout(this.onTimeout, 0);
return;
}
this.props.mouseout();
};
calculateLeft(
target: ClientRect,
editor: ClientRect,
@ -84,18 +137,18 @@ class Popover extends Component<Props, State> {
editor: ClientRect,
popover: ClientRect
) => {
if (popover.height < editor.height) {
if (popover.height <= editor.height) {
const rightOrientationTop = target.top - popover.height / 2;
if (rightOrientationTop < editor.top) {
return editor.top;
return editor.top - target.height;
}
const rightOrientationBottom = rightOrientationTop + popover.height;
if (rightOrientationBottom > editor.bottom) {
return editor.bottom - popover.height;
return editor.bottom + target.height - popover.height + this.gapHeight;
}
return rightOrientationTop;
}
return 0;
return editor.top - target.height;
};
calculateOrientation(
@ -205,9 +258,30 @@ class Popover extends Component<Props, State> {
getChildren() {
const { children } = this.props;
const { orientation } = this.state.coords;
const gap = <div className="gap" key="gap" />;
return orientation === "up" ? [children, gap] : [gap, children];
const coords = this.state.coords;
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) {
@ -243,10 +317,10 @@ class Popover extends Component<Props, State> {
}
renderTooltip() {
const { top, left } = this.state.coords;
const { top, left, orientation } = this.state.coords;
return (
<div
className="tooltip"
className={classNames("tooltip", `orientation-${orientation}`)}
style={{ top, left }}
ref={c => (this.$tooltip = c)}
>

Просмотреть файл

@ -0,0 +1,169 @@
/* 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 from "react";
import type { Coords } from "./Popover";
type Props = {
token: HTMLDivElement,
preview: ?HTMLDivElement,
type: ?string,
gapHeight: number,
coords: Coords,
offset: number,
};
function shorten(coordinates) {
// In cases where the token is wider than the preview, the smartGap
// gets distorted. This shortens the coordinate array so that the smartGap
// is only touching 2 corners of the token (instead of all 4 corners)
coordinates.splice(0, 2);
coordinates.splice(4, 2);
return coordinates;
}
function getSmartGapCoordinates(
preview: ClientRect,
token: ClientRect,
offset: number,
orientation: string,
gapHeight: number,
coords: Coords
) {
if (orientation === "up") {
const coordinates = [
token.left - coords.left + offset,
token.top + token.height - (coords.top + preview.height) + gapHeight,
0,
0,
preview.width + offset,
0,
token.left + token.width - coords.left + offset,
token.top + token.height - (coords.top + preview.height) + gapHeight,
token.left + token.width - coords.left + offset,
token.top - (coords.top + preview.height) + gapHeight,
token.left - coords.left + offset,
token.top - (coords.top + preview.height) + gapHeight,
];
return preview.width > token.width ? coordinates : shorten(coordinates);
}
if (orientation === "down") {
const coordinates = [
token.left + token.width - (coords.left + preview.top) + offset,
0,
preview.width + offset,
coords.top - token.top + gapHeight,
0,
coords.top - token.top + gapHeight,
token.left - (coords.left + preview.top) + offset,
0,
token.left - (coords.left + preview.top) + offset,
token.height,
token.left + token.width - (coords.left + preview.top) + offset,
token.height,
];
return preview.width > token.width ? coordinates : shorten(coordinates);
}
return [
0,
token.top - coords.top,
gapHeight + token.width,
0,
gapHeight + token.width,
preview.height - gapHeight,
0,
token.top + token.height - coords.top,
token.width,
token.top + token.height - coords.top,
token.width,
token.top - coords.top,
];
}
function getSmartGapDimensions(
previewRect: ClientRect,
tokenRect: ClientRect,
offset: number,
orientation: string,
gapHeight: number,
coords: Coords
) {
if (orientation === "up") {
return {
height:
tokenRect.top +
tokenRect.height -
coords.top -
previewRect.height +
gapHeight,
width: Math.max(previewRect.width, tokenRect.width) + offset,
};
}
if (orientation === "down") {
return {
height: coords.top - tokenRect.top + gapHeight,
width: Math.max(previewRect.width, tokenRect.width) + offset,
};
}
return {
height: previewRect.height - gapHeight,
width: coords.left - tokenRect.left + gapHeight,
};
}
export default function SmartGap({
token,
preview,
type,
gapHeight,
coords,
offset,
}: Props) {
const tokenRect = token.getBoundingClientRect();
// $FlowIgnore
const previewRect = preview.getBoundingClientRect();
const orientation = coords.orientation;
let optionalMarginLeft, optionalMarginTop;
if (orientation === "down") {
optionalMarginTop = -tokenRect.height;
} else if (orientation === "right") {
optionalMarginLeft = -tokenRect.width;
}
const { height, width } = getSmartGapDimensions(
previewRect,
tokenRect,
-offset,
orientation,
gapHeight,
coords
);
const coordinates = getSmartGapCoordinates(
previewRect,
tokenRect,
-offset,
orientation,
gapHeight,
coords
);
return (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
style={{
height: height,
width: width,
position: "absolute",
marginLeft: optionalMarginLeft,
marginTop: optionalMarginTop,
}}
>
<polygon points={coordinates} fill="transparent" />
</svg>
);
}

Просмотреть файл

@ -20,6 +20,7 @@ CompiledModules(
'ResultList.js',
'SearchInput.js',
'SourceIcon.js',
'SmartGap.js',
)
DevToolsModules(

Просмотреть файл

@ -5,7 +5,7 @@
// @flow
import React from "react";
import { mount, shallow } from "enzyme";
import { mount } from "enzyme";
import Popover from "../Popover";
@ -26,6 +26,21 @@ describe("Popover", () => {
};
},
};
const targetRef: any = {
getBoundingClientRect() {
return {
x: 0,
y: 0,
width: 100,
height: 100,
top: 250,
right: 0,
bottom: 0,
left: 20,
};
},
};
const targetPosition = {
x: 100,
y: 200,
@ -42,18 +57,22 @@ describe("Popover", () => {
onKeyDown={onKeyDown}
editorRef={editorRef}
targetPosition={targetPosition}
mouseout={() => {}}
target={targetRef}
>
<h1>Poppy!</h1>
</Popover>
);
const tooltip = shallow(
const tooltip = mount(
<Popover
type="tooltip"
onMouseLeave={onMouseLeave}
onKeyDown={onKeyDown}
editorRef={editorRef}
targetPosition={targetPosition}
mouseout={() => {}}
target={targetRef}
>
<h1>Toolie!</h1>
</Popover>
@ -75,6 +94,8 @@ describe("Popover", () => {
onKeyDown={onKeyDown}
editorRef={editorRef}
targetPosition={targetPosition}
mouseout={() => {}}
target={targetRef}
>
<h1>Poppy!</h1>
</Popover>
@ -90,6 +111,8 @@ describe("Popover", () => {
onKeyDown={onKeyDown}
editorRef={editorRef}
targetPosition={targetPosition}
mouseout={() => {}}
target={targetRef}
>
<h1>Toolie!</h1>
</Popover>
@ -126,6 +149,8 @@ describe("Popover", () => {
onKeyDown={onKeyDown}
editorRef={editor}
targetPosition={target}
mouseout={() => {}}
target={targetRef}
>
<h1>Toolie!</h1>
</Popover>
@ -164,6 +189,8 @@ describe("Popover", () => {
onKeyDown={onKeyDown}
editorRef={editor}
targetPosition={target}
mouseout={() => {}}
target={targetRef}
>
<h1>Toolie!</h1>
</Popover>

Просмотреть файл

@ -7,9 +7,14 @@ exports[`Popover mount popover 1`] = `
"getBoundingClientRect": [Function],
}
}
mouseout={[Function]}
onKeyDown={[MockFunction]}
onMouseLeave={[MockFunction]}
onPopoverCoords={[Function]}
target={
Object {
"getBoundingClientRect": [Function],
}
}
targetPosition={
Object {
"bottom": 0,
@ -29,14 +34,14 @@ exports[`Popover mount popover 1`] = `
style={
Object {
"left": 500,
"top": 250,
"top": -50,
}
}
>
<BracketArrow
left={-4}
orientation="left"
top={-202}
top={98}
>
<div
className="bracket-arrow left"
@ -44,7 +49,7 @@ exports[`Popover mount popover 1`] = `
Object {
"bottom": undefined,
"left": -4,
"top": -202,
"top": 98,
}
}
/>
@ -52,7 +57,91 @@ exports[`Popover mount popover 1`] = `
<div
className="gap"
key="gap"
/>
>
<SmartGap
coords={
Object {
"left": 500,
"orientation": "right",
"targetMid": Object {
"x": -14,
"y": 98,
},
"top": -50,
}
}
gapHeight={0}
offset={0}
preview={
<div
class="popover orientation-right"
style="top: -50px; left: 500px;"
>
<div
class="bracket-arrow left"
style="left: -4px; top: 98px;"
/>
<div
class="gap"
>
<svg
style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points="0,300,100,0,100,0,0,400,100,400,100,300"
/>
</svg>
</div>
<h1>
Poppy!
</h1>
</div>
}
token={
Object {
"getBoundingClientRect": [Function],
}
}
type="popover"
>
<svg
style={
Object {
"height": 0,
"marginLeft": -100,
"marginTop": undefined,
"position": "absolute",
"width": 480,
}
}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points={
Array [
0,
300,
100,
0,
100,
0,
0,
400,
100,
400,
100,
300,
]
}
/>
</svg>
</SmartGap>
</div>
<h1>
Poppy!
</h1>
@ -67,9 +156,14 @@ exports[`Popover mount tooltip 1`] = `
"getBoundingClientRect": [Function],
}
}
mouseout={[Function]}
onKeyDown={[MockFunction]}
onMouseLeave={[MockFunction]}
onPopoverCoords={[Function]}
target={
Object {
"getBoundingClientRect": [Function],
}
}
targetPosition={
Object {
"bottom": 0,
@ -85,7 +179,7 @@ exports[`Popover mount tooltip 1`] = `
type="tooltip"
>
<div
className="tooltip"
className="tooltip orientation-down"
style={
Object {
"left": -8,
@ -96,7 +190,83 @@ exports[`Popover mount tooltip 1`] = `
<div
className="gap"
key="gap"
/>
>
<SmartGap
coords={
Object {
"left": -8,
"orientation": "down",
"targetMid": Object {
"x": 0,
"y": 0,
},
"top": 0,
}
}
gapHeight={0}
offset={0}
preview={
<div
class="tooltip orientation-down"
style="top: 0px; left: -8px;"
>
<div
class="gap"
>
<svg
style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points="0,-250,0,-250,28,100,128,100"
/>
</svg>
</div>
<h1>
Toolie!
</h1>
</div>
}
token={
Object {
"getBoundingClientRect": [Function],
}
}
type="tooltip"
>
<svg
style={
Object {
"height": -250,
"marginLeft": undefined,
"marginTop": -100,
"position": "absolute",
"width": 100,
}
}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points={
Array [
0,
-250,
0,
-250,
28,
100,
128,
100,
]
}
/>
</svg>
</SmartGap>
</div>
<h1>
Toolie!
</h1>
@ -105,23 +275,128 @@ exports[`Popover mount tooltip 1`] = `
`;
exports[`Popover render (tooltip) 1`] = `
<div
className="tooltip"
style={
<Popover
editorRef={
Object {
"left": 0,
"top": 0,
"getBoundingClientRect": [Function],
}
}
mouseout={[Function]}
onKeyDown={[MockFunction]}
onMouseLeave={[MockFunction]}
target={
Object {
"getBoundingClientRect": [Function],
}
}
targetPosition={
Object {
"bottom": 0,
"height": 300,
"left": 200,
"right": 0,
"top": 50,
"width": 300,
"x": 100,
"y": 200,
}
}
type="tooltip"
>
<div
className="gap"
key="gap"
/>
<h1>
Toolie!
</h1>
</div>
className="tooltip orientation-down"
style={
Object {
"left": -8,
"top": 0,
}
}
>
<div
className="gap"
key="gap"
>
<SmartGap
coords={
Object {
"left": -8,
"orientation": "down",
"targetMid": Object {
"x": 0,
"y": 0,
},
"top": 0,
}
}
gapHeight={0}
offset={0}
preview={
<div
class="tooltip orientation-down"
style="top: 0px; left: -8px;"
>
<div
class="gap"
>
<svg
style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points="0,-250,0,-250,28,100,128,100"
/>
</svg>
</div>
<h1>
Toolie!
</h1>
</div>
}
token={
Object {
"getBoundingClientRect": [Function],
}
}
type="tooltip"
>
<svg
style={
Object {
"height": -250,
"marginLeft": undefined,
"marginTop": -100,
"position": "absolute",
"width": 100,
}
}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points={
Array [
0,
-250,
0,
-250,
28,
100,
128,
100,
]
}
/>
</svg>
</SmartGap>
</div>
<h1>
Toolie!
</h1>
</div>
</Popover>
`;
exports[`Popover render 1`] = `
@ -131,9 +406,14 @@ exports[`Popover render 1`] = `
"getBoundingClientRect": [Function],
}
}
mouseout={[Function]}
onKeyDown={[MockFunction]}
onMouseLeave={[MockFunction]}
onPopoverCoords={[Function]}
target={
Object {
"getBoundingClientRect": [Function],
}
}
targetPosition={
Object {
"bottom": 0,
@ -153,14 +433,14 @@ exports[`Popover render 1`] = `
style={
Object {
"left": 500,
"top": 250,
"top": -50,
}
}
>
<BracketArrow
left={-4}
orientation="left"
top={-202}
top={98}
>
<div
className="bracket-arrow left"
@ -168,7 +448,7 @@ exports[`Popover render 1`] = `
Object {
"bottom": undefined,
"left": -4,
"top": -202,
"top": 98,
}
}
/>
@ -176,7 +456,91 @@ exports[`Popover render 1`] = `
<div
className="gap"
key="gap"
/>
>
<SmartGap
coords={
Object {
"left": 500,
"orientation": "right",
"targetMid": Object {
"x": -14,
"y": 98,
},
"top": -50,
}
}
gapHeight={0}
offset={0}
preview={
<div
class="popover orientation-right"
style="top: -50px; left: 500px;"
>
<div
class="bracket-arrow left"
style="left: -4px; top: 98px;"
/>
<div
class="gap"
>
<svg
style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points="0,300,100,0,100,0,0,400,100,400,100,300"
/>
</svg>
</div>
<h1>
Poppy!
</h1>
</div>
}
token={
Object {
"getBoundingClientRect": [Function],
}
}
type="popover"
>
<svg
style={
Object {
"height": 0,
"marginLeft": -100,
"marginTop": undefined,
"position": "absolute",
"width": 480,
}
}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
fill="transparent"
points={
Array [
0,
300,
100,
0,
100,
0,
0,
400,
100,
400,
100,
300,
]
}
/>
</svg>
</SmartGap>
</div>
<h1>
Poppy!
</h1>