From dd17048c5dba15bb2f9934006381a73bdef1a8d9 Mon Sep 17 00:00:00 2001 From: Eliad Moosavi Date: Fri, 3 May 2019 15:24:57 -0400 Subject: [PATCH] fix(core): Fix tooltip positioning --- packages/core/demo/demo-data/pie-donut.ts | 4 +- packages/core/demo/index.scss | 2 +- packages/core/package.json | 2 +- packages/core/src/base-chart.ts | 2 +- .../core/src/components/positionService.ts | 237 ------------------ packages/core/src/components/tooltip.ts | 30 +-- packages/core/src/configuration.ts | 2 +- packages/core/src/style.scss | 4 +- 8 files changed, 24 insertions(+), 259 deletions(-) delete mode 100644 packages/core/src/components/positionService.ts diff --git a/packages/core/demo/demo-data/pie-donut.ts b/packages/core/demo/demo-data/pie-donut.ts index ebdf239ab4..d6ff9a851e 100644 --- a/packages/core/demo/demo-data/pie-donut.ts +++ b/packages/core/demo/demo-data/pie-donut.ts @@ -19,8 +19,8 @@ export const donutOptions = { }; export const pieData = { - labels: ["2V2N-9KYPM version 1", "L22I-P66EP-L22I-P66EP-L22I-P66EP", "JQAI-2M4L1", "J9DZ-F37AP", - "YEL48-Q6XK-YEL48", "P66EP-L22I-L22I", "Q6XK-YEL48", "XKB5-L6EP", "YEL48-Q6XK", "L22I-P66EP-L22I"], + labels: ["2V2N 9KYPM version 1", "L22I P66EP L22I P66EP L22I P66EP", "JQAI 2M4L1", "J9DZ F37AP", + "YEL48 Q6XK YEL48", "P66EP L22I L22I", "Q6XK YEL48", "XKB5 L6EP", "YEL48 Q6XK", "L22I P66EP L22I"], datasets: [ { label: "Dataset 1", diff --git a/packages/core/demo/index.scss b/packages/core/demo/index.scss index 938d022609..1fbe9f2b90 100644 --- a/packages/core/demo/index.scss +++ b/packages/core/demo/index.scss @@ -145,7 +145,7 @@ header.m-demo-header { display: block; height: 500px; min-width: 300px; - max-width: 510px; + max-width: 800px; padding: 30px; transition: box-shadow .1s ease-out; overflow: hidden; diff --git a/packages/core/package.json b/packages/core/package.json index 5106c871ce..077770c50f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,7 @@ "author": "IBM", "license": "Apache-2.0", "dependencies": { - "@carbon/utils-position": "1.0.0", + "@carbon/utils-position": "1.1.0", "babel-polyfill": "6.26.0", "d3": "4.13.0", "resize-observer-polyfill": "1.5.0" diff --git a/packages/core/src/base-chart.ts b/packages/core/src/base-chart.ts index cf9a1bbb91..5feb7cfb75 100644 --- a/packages/core/src/base-chart.ts +++ b/packages/core/src/base-chart.ts @@ -75,7 +75,7 @@ export class BaseChart { // Initialize charting components this.chartOverlay = new ChartOverlay(this.holder, this.options.overlay); - this.tooltip = new ChartTooltip(this.holder); + this.tooltip = new ChartTooltip(this.container.node()); if (configs.data) { this.setData(configs.data); diff --git a/packages/core/src/components/positionService.ts b/packages/core/src/components/positionService.ts deleted file mode 100644 index 9afd8cc932..0000000000 --- a/packages/core/src/components/positionService.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Utilites to manipulate the position of elements relative to other elements - */ - -export const PLACEMENTS = { - LEFT: "left", - RIGHT: "right", - TOP: "top", - BOTTOM: "bottom" -} - -export interface AbsolutePosition { - top: number; - left: number; - position?: AbsolutePosition; -} - -export type Offset = { top: number, left: number }; - -export type ReferenceRect = { - height: number; - width: number; -}; - -export type Positions = { - [key: string]: (referenceOffset: Offset, target: HTMLElement, referenceRect: ReferenceRect) => AbsolutePosition -}; - -export const defaultPositions: Positions = { - [PLACEMENTS.LEFT]: (referenceOffset: Offset, target: HTMLElement, referenceRect: ReferenceRect): AbsolutePosition => ({ - top: referenceOffset.top - Math.round(target.offsetHeight / 2) + Math.round(referenceRect.height / 2), - left: Math.round(referenceOffset.left - target.offsetWidth) - }), - [PLACEMENTS.RIGHT]: (referenceOffset: Offset, target: HTMLElement, referenceRect: ReferenceRect): AbsolutePosition => ({ - top: referenceOffset.top - Math.round(target.offsetHeight / 2) + Math.round(referenceRect.height / 2), - left: Math.round(referenceOffset.left + referenceRect.width) - }), - [PLACEMENTS.TOP]: (referenceOffset: Offset, target: HTMLElement, referenceRect: ReferenceRect): AbsolutePosition => ({ - top: Math.round(referenceOffset.top - target.offsetHeight), - left: referenceOffset.left - Math.round(target.offsetWidth / 2) + Math.round(referenceRect.width / 2) - }), - [PLACEMENTS.BOTTOM]: (referenceOffset: Offset, target: HTMLElement, referenceRect: ReferenceRect): AbsolutePosition => ({ - top: Math.round(referenceOffset.top + referenceRect.height), - left: referenceOffset.left - Math.round(target.offsetWidth / 2) + Math.round(referenceRect.width / 2) - }) -}; - -export default class Position { - protected positions = defaultPositions; - - constructor(positions: Positions = {}) { - this.positions = Object.assign({}, defaultPositions, positions); - } - - getRelativeOffset(target: HTMLElement): Offset { - // start with the initial element offsets - let offsets = { - left: target.offsetLeft, - top: target.offsetTop - }; - // get each static (i.e. not absolute or relative) offsetParent and sum the left/right offsets - while (target.offsetParent && getComputedStyle(target.offsetParent).position === "static") { - offsets.left += target.offsetLeft; - offsets.top += target.offsetTop; - target = target.offsetParent as HTMLElement; - } - return offsets; - } - - getAbsoluteOffset(target: HTMLElement): Offset { - let currentNode = target; - let margins = { - top: 0, - left: 0 - }; - - // searches for containing elements with additional margins - while (currentNode.offsetParent) { - const computed = getComputedStyle(currentNode.offsetParent); - // find static elements with additional margins - // since they tend to throw off our positioning - // (usually this is just the body) - if ( - computed.position === "static" && - computed.marginLeft && - computed.marginTop - ) { - if (parseInt(computed.marginTop, 10)) { - margins.top += parseInt(computed.marginTop, 10); - } - if (parseInt(computed.marginLeft, 10)) { - margins.left += parseInt(computed.marginLeft, 10); - } - } - - currentNode = currentNode.offsetParent as HTMLElement; - } - - const targetRect = target.getBoundingClientRect(); - const relativeRect = document.body.getBoundingClientRect(); - return { - top: targetRect.top - relativeRect.top + margins.top, - left: targetRect.left - relativeRect.left + margins.left - }; - } - - // finds the position relative to the `reference` element - findRelative(reference: Element, target: Element, placement: string): AbsolutePosition { - const referenceOffset = this.getRelativeOffset(reference as HTMLElement); - const referenceRect = reference.getBoundingClientRect(); - return this.calculatePosition(referenceOffset, referenceRect, target, placement); - } - - findAbsolute(reference: Element, target: Element, placement: string): AbsolutePosition { - const referenceOffset = this.getAbsoluteOffset(reference as HTMLElement); - const referenceRect = reference.getBoundingClientRect(); - return this.calculatePosition(referenceOffset, referenceRect, target, placement); - } - - findPosition(reference: Element, - target: Element, - placement: string, - offsetFunction = this.getAbsoluteOffset): AbsolutePosition { - const referenceOffset = offsetFunction(reference as HTMLElement); - const referenceRect = reference.getBoundingClientRect(); - return this.calculatePosition(referenceOffset, referenceRect, target, placement); - } - - findPositionAt(offset: Offset, target: Element, placement: string): AbsolutePosition { - return this.calculatePosition(offset, { height: 0, width: 0 }, target, placement); - } - - /** - * Get the dimensions of an element from an AbsolutePosition and a reference element - */ - getPlacementBox(target: HTMLElement, position: AbsolutePosition) { - const targetBottom = target.offsetHeight + position.top; - const targetRight = target.offsetWidth + position.left; - - return { - top: position.top, - bottom: targetBottom, - left: position.left, - right: targetRight - }; - } - - addOffset(position: AbsolutePosition, top = 0, left = 0): AbsolutePosition { - return Object.assign({}, position, { - top: position.top + top, - left: position.left + left - }); - } - - setElement(element: Element, position: AbsolutePosition): void { - (element as HTMLElement).style.top = `${position.top}px`; - (element as HTMLElement).style.left = `${position.left}px`; - } - - findBestPlacement(reference: Element, target: Element, placements: string[]) { - /** - * map over the array of placements and weight them based on the percentage of visible area - * where visible area is defined as the area not obscured by the window borders - */ - const weightedPlacements = placements.map(placement => { - const pos = this.findPosition(reference, target, placement); - let box = this.getPlacementBox((target as HTMLElement), pos); - let hiddenHeight = box.bottom - window.innerHeight - window.scrollY; - let hiddenWidth = box.right - window.innerWidth - window.scrollX; - // if the hiddenHeight or hiddenWidth is negative, reset to offsetHeight or offsetWidth - hiddenHeight = hiddenHeight < 0 ? (target as HTMLElement).offsetHeight : hiddenHeight; - hiddenWidth = hiddenWidth < 0 ? (target as HTMLElement).offsetWidth : hiddenWidth; - const area = (target as HTMLElement).offsetHeight * (target as HTMLElement).offsetWidth; - const hiddenArea = hiddenHeight * hiddenWidth; - let visibleArea = area - hiddenArea; - // if the visibleArea is 0 set it back to area (to calculate the percentage in a useful way) - visibleArea = visibleArea === 0 ? area : visibleArea; - const visiblePercent = visibleArea / area; - return { - placement, - weight: visiblePercent - }; - }); - - // sort the placements from best to worst - weightedPlacements.sort((a, b) => b.weight - a.weight); - // pick the best! - return weightedPlacements[0].placement; - } - - findBestPlacementAt(offset: Offset, reference: Element, target: Element, placements: string[]) { - /** - * map over the array of placements and weight them based on the percentage of visible area - * where visible area is defined as the area not obscured by the reference borders - */ - const weightedPlacements = placements.map(placement => { - const pos = this.findPositionAt(offset, target, placement); - let box = this.getPlacementBox((target as HTMLElement), pos); - let hiddenHeight = box.bottom - (reference as HTMLElement).offsetHeight; - let hiddenWidth = box.right - (reference as HTMLElement).offsetWidth; - // if the hiddenHeight or hiddenWidth is negative, reset to offsetHeight or offsetWidth - hiddenHeight = hiddenHeight < 0 ? (target as HTMLElement).offsetHeight : hiddenHeight; - hiddenWidth = hiddenWidth < 0 ? (target as HTMLElement).offsetWidth : hiddenWidth; - const area = (target as HTMLElement).offsetHeight * (target as HTMLElement).offsetWidth; - const hiddenArea = hiddenHeight * hiddenWidth; - let visibleArea = area - hiddenArea; - // if the visibleArea is 0 set it back to area (to calculate the percentage in a useful way) - visibleArea = visibleArea === 0 ? area : visibleArea; - - const visiblePercent = visibleArea / area; - return { - placement, - weight: visiblePercent - }; - }); - - // sort the placements from best to worst - weightedPlacements.sort((a, b) => b.weight - a.weight); - // pick the best! - return weightedPlacements[0].placement; - } - - protected calculatePosition( - referenceOffset: Offset, - referenceRect: ReferenceRect, - target: Element, - placement: string): AbsolutePosition { - - if (this.positions[placement]) { - return this.positions[placement](referenceOffset, target as HTMLElement, referenceRect); - } - console.error("No function found for placement, defaulting to 0,0"); - return { left: 0, top: 0 }; - } -} - -export const position = new Position(); diff --git a/packages/core/src/components/tooltip.ts b/packages/core/src/components/tooltip.ts index fa34acea62..2f526ce6d1 100644 --- a/packages/core/src/components/tooltip.ts +++ b/packages/core/src/components/tooltip.ts @@ -1,25 +1,24 @@ import * as Configuration from "../configuration"; // Carbon position service -// import Position, { position } from "@carbon/utils-position"; -import Position, { PLACEMENTS } from "./positionService"; +import Position, { PLACEMENTS } from "@carbon/utils-position"; // D3 Imports import { select, selectAll, mouse } from "d3-selection"; export class ChartTooltip { - holder: Element; + container: Element; positionService: Position = new Position(); - constructor(holder: Element) { - this.holder = holder; + constructor(container: Element) { + this.container = container; } - getRef = () => select(this.holder).select("div.chart-tooltip").node() as HTMLElement; + getRef = () => select(this.container).select("div.chart-tooltip").node() as HTMLElement; positionTooltip() { const target = this.getRef(); - const mouseRelativePos = mouse(this.holder as SVGSVGElement); + const mouseRelativePos = mouse(this.container as SVGSVGElement); // Find out whether tooltip should be shown on the left or right side const bestPlacementOption = this.positionService.findBestPlacementAt( @@ -27,12 +26,15 @@ export class ChartTooltip { left: mouseRelativePos[0], top: mouseRelativePos[1] }, - this.holder, target, [ PLACEMENTS.RIGHT, PLACEMENTS.LEFT - ] + ], + () => ({ + width: (this.container as HTMLElement).offsetWidth, + height: (this.container as HTMLElement).offsetHeight + }) ); // Get coordinates to where tooltip should be positioned @@ -54,7 +56,7 @@ export class ChartTooltip { selectAll(".chart-tooltip").remove(); // Draw tooltip - const tooltip = select(this.holder).append("div") + const tooltip = select(this.container).append("div") .attr("class", "tooltip chart-tooltip"); // Apply html content to the tooltip @@ -75,7 +77,7 @@ export class ChartTooltip { } hide() { - const tooltipRef = select(this.holder).select("div.chart-tooltip"); + const tooltipRef = select(this.container).select("div.chart-tooltip"); // Fade out and remove tooltipRef.style("opacity", 1) @@ -104,7 +106,7 @@ export class ChartTooltip { } addEventListeners() { - const tooltipRef = select(this.holder).select("div.chart-tooltip"); + const tooltipRef = select(this.container).select("div.chart-tooltip"); // Apply the event listeners to close the tooltip // setTimeout is there to avoid catching the click event that opened the tooltip @@ -113,7 +115,7 @@ export class ChartTooltip { window.addEventListener("keydown", this.handleTooltipEvents); // If clicked outside - this.holder.addEventListener("click", this.handleTooltipEvents); + this.container.addEventListener("click", this.handleTooltipEvents); // Stop clicking inside tooltip from bubbling up to window tooltipRef.on("click", () => { @@ -127,6 +129,6 @@ export class ChartTooltip { window.removeEventListener("keydown", this.handleTooltipEvents); // Remove eventlistener to close tooltip when clicked outside - this.holder.removeEventListener("click", this.handleTooltipEvents); + this.container.removeEventListener("click", this.handleTooltipEvents); } } diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index a164f6174f..d8a61e895e 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -692,7 +692,7 @@ export const tooltip = { magicTop1: 21, magicTop2: 22, magicLeft1: 11, - magicLeft2: 12, + magicLeft2: 10, fadeIn: { duration: 250 }, diff --git a/packages/core/src/style.scss b/packages/core/src/style.scss index 73a53f03c5..678e9df308 100644 --- a/packages/core/src/style.scss +++ b/packages/core/src/style.scss @@ -256,8 +256,8 @@ div.chart-overlay { position: absolute; padding: 10px; border-radius: 3px; - min-width: 110px; - max-width: 200px; + min-width: 80px; + max-width: 70%; word-wrap: break-word; z-index: 1059;