Skip to content

Commit

Permalink
Merge pull request #756 from hshoff/chris--useTooltipInPortal
Browse files Browse the repository at this point in the history
new(vx-tooltip): add useTooltipInPortal
  • Loading branch information
williaster committed Jun 25, 2020
2 parents 4daa5e8 + 63bdc9e commit e441821
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 103 deletions.
1 change: 0 additions & 1 deletion packages/vx-demo/package.json
Expand Up @@ -78,7 +78,6 @@
"react-markdown": "^4.3.1",
"react-spring": "^8.0.27",
"react-tilt": "^0.1.4",
"react-use-measure": "^2.0.1",
"recompose": "^0.26.0",
"topojson-client": "^3.0.0"
},
Expand Down
12 changes: 11 additions & 1 deletion packages/vx-demo/src/pages/docs/tooltip.tsx
@@ -1,5 +1,11 @@
import React from 'react';
import TooltipReadme from '!!raw-loader!../../../../vx-tooltip/Readme.md';
import Tooltip from '../../../../vx-tooltip/src/tooltips/Tooltip';
import TooltipWithBounds from '../../../../vx-tooltip/src/tooltips/TooltipWithBounds';
import useTooltip from '../../../../vx-tooltip/src/hooks/useTooltip';
import useTooltipInPortal from '../../../../vx-tooltip/src/hooks/useTooltipInPortal';
import Portal from '../../../../vx-tooltip/src/Portal';

import DocPage from '../../components/DocPage';
import TooltipTile from '../../components/Gallery/TooltipTile';
import DotsTile from '../../components/Gallery/DotsTile';
Expand All @@ -9,4 +15,8 @@ import AreaTile from '../../components/Gallery/AreaTile';

const examples = [TooltipTile, DotsTile, BarStackHorizontalTile, StatsPlotTile, AreaTile];

export default () => <DocPage examples={examples} readme={TooltipReadme} vxPackage="tooltip" />;
const components = [TooltipWithBounds, Tooltip, Portal, useTooltip, useTooltipInPortal];

export default () => (
<DocPage components={components} examples={examples} readme={TooltipReadme} vxPackage="tooltip" />
);
18 changes: 12 additions & 6 deletions packages/vx-demo/src/sandboxes/vx-barstack/Example.tsx
Expand Up @@ -7,7 +7,7 @@ import { AxisBottom } from '@vx/axis';
import cityTemperature, { CityTemperature } from '@vx/mock-data/lib/mocks/cityTemperature';
import { scaleBand, scaleLinear, scaleOrdinal } from '@vx/scale';
import { timeParse, timeFormat } from 'd3-time-format';
import { useTooltip, Tooltip, defaultStyles } from '@vx/tooltip';
import { useTooltip, useTooltipInPortal, defaultStyles } from '@vx/tooltip';
import { LegendOrdinal } from '@vx/legend';

type CityName = 'New York' | 'San Francisco' | 'Austin';
Expand Down Expand Up @@ -92,6 +92,8 @@ export default function Example({
showTooltip,
} = useTooltip<TooltipData>();

const { containerRef, TooltipInPortal } = useTooltipInPortal();

if (width < 10) return null;
// bounds
const xMax = width;
Expand All @@ -103,7 +105,7 @@ export default function Example({
return width < 10 ? null : (
// relative position is needed for correct tooltip positioning
<div style={{ position: 'relative' }}>
<svg width={width} height={height}>
<svg ref={containerRef} width={width} height={height}>
<rect x={0} y={0} width={width} height={height} fill={background} rx={14} />
<Grid<string, number>
top={margin.top}
Expand Down Expand Up @@ -146,8 +148,7 @@ export default function Example({
onMouseMove={event => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const top = event.clientY - margin.top - bar.height;
const offset = (dateScale.paddingInner() * dateScale.step()) / 2;
const left = bar.x + bar.width + offset;
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: top,
Expand Down Expand Up @@ -187,15 +188,20 @@ export default function Example({
</div>

{tooltipOpen && tooltipData && (
<Tooltip top={tooltipTop} left={tooltipLeft} style={tooltipStyles}>
<TooltipInPortal
key={Math.random()} // update tooltip bounds each render
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
>
<div style={{ color: colorScale(tooltipData.key) }}>
<strong>{tooltipData.key}</strong>
</div>
<div>{tooltipData.bar.data[tooltipData.key]}</div>
<div>
<small>{formatDate(getDate(tooltipData.bar.data))}</small>
</div>
</Tooltip>
</TooltipInPortal>
)}
</div>
);
Expand Down
133 changes: 55 additions & 78 deletions packages/vx-demo/src/sandboxes/vx-tooltip/Example.tsx
@@ -1,20 +1,19 @@
import React, { useState, useCallback } from 'react';
import useMeasure from 'react-use-measure';
import { Tooltip, TooltipWithBounds, useTooltip, defaultStyles, Portal } from '@vx/tooltip/src';
import {
Tooltip,
TooltipWithBounds,
useTooltip,
useTooltipInPortal,
defaultStyles,
} from '@vx/tooltip';

export type TooltipProps = {
width: number;
height: number;
showControls?: boolean;
};

interface TooltipData {
text: string;
containerX: number;
containerY: number;
pageX: number;
pageY: number;
}
type TooltipData = string;

const positionIndicatorSize = 8;

Expand All @@ -28,64 +27,57 @@ const tooltipStyles = {
};

export default function Example({ width, height, showControls = true }: TooltipProps) {
// bounds of the container are needed to convert page coordinates to container coordinates
const [ref, ownBounds] = useMeasure({ scroll: true });
const [detectBounds, setDetectBounds] = useState(true);
const [renderInPortal, setRenderInPortal] = useState(false);
const [tooltipShouldDetectBounds, setTooltipShouldDetectBounds] = useState(true);
const [renderTooltipInPortal, setRenderTooltipInPortal] = useState(false);

const { containerRef, containerBounds, TooltipInPortal } = useTooltipInPortal({
scroll: true,
detectBounds: tooltipShouldDetectBounds,
});

const {
showTooltip,
hideTooltip,
tooltipOpen,
tooltipData,
tooltipLeft,
tooltipTop,
tooltipLeft = 0,
tooltipTop = 0,
} = useTooltip<TooltipData>({
// initial tooltip state
tooltipOpen: true,
tooltipLeft: width / 3,
tooltipTop: height / 3,
tooltipData: {
text: 'Move me with your mouse or finger',
containerX: width / 3,
containerY: height / 3,
pageX: 0,
pageY: 0,
},
tooltipData: 'Move me with your mouse or finger',
});

// event handlers
const handleMouseMove = useCallback(
(event: React.MouseEvent | React.TouchEvent) => {
const pageX = 'pageX' in event ? event.pageX : 0;
const pageY = 'pageY' in event ? event.pageY : 0;
const containerX = ('clientX' in event ? event.clientX : 0) - ownBounds.left;
const containerY = ('clientY' in event ? event.clientY : 0) - ownBounds.top;
// coordinates should be relative to the container in which Tooltip is rendered
const containerX = ('clientX' in event ? event.clientX : 0) - containerBounds.left;
const containerY = ('clientY' in event ? event.clientY : 0) - containerBounds.top;

showTooltip({
tooltipLeft: renderInPortal ? pageX : containerX,
tooltipTop: renderInPortal ? pageY : containerY,
tooltipData: {
containerX,
containerY,
pageX,
pageY,
text: detectBounds
? 'I detect my container boundary'
: 'I will get clipped by my container',
},
tooltipLeft: containerX,
tooltipTop: containerY,
tooltipData: tooltipShouldDetectBounds
? 'I detect my container boundary'
: 'I will get clipped by my container',
});
},
[showTooltip, ownBounds, detectBounds, renderInPortal],
[showTooltip, tooltipShouldDetectBounds, containerBounds],
);

const TooltipComponent = detectBounds ? TooltipWithBounds : Tooltip;
const TooltipWrapper = renderInPortal ? Portal : React.Fragment;
const TooltipComponent = renderTooltipInPortal
? TooltipInPortal
: tooltipShouldDetectBounds
? TooltipWithBounds
: Tooltip;

return (
<>
<div
ref={ref}
ref={containerRef}
className="tooltip-example"
style={{ width, height }}
onMouseMove={handleMouseMove}
Expand All @@ -96,36 +88,30 @@ export default function Example({ width, height, showControls = true }: TooltipP
<div
className="position-indicator"
style={{
width: positionIndicatorSize,
height: positionIndicatorSize,
transform: `translate(${(tooltipData?.containerX ?? 0) -
positionIndicatorSize / 2}px, ${(tooltipData?.containerY ?? 0) -
transform: `translate(${tooltipLeft - positionIndicatorSize / 2}px, ${tooltipTop -
positionIndicatorSize / 2}px)`,
}}
/>
<div
className="crosshair horizontal"
style={{ transform: `translateY(${tooltipData?.containerY ?? 0}px)` }}
style={{ transform: `translateY(${tooltipTop}px)` }}
/>
<div
className="crosshair vertical"
style={{ transform: `translateX(${tooltipData?.containerX ?? 0}px)` }}
style={{ transform: `translateX(${tooltipLeft}px)` }}
/>
<TooltipWrapper>
<TooltipComponent
key={Math.random()} // needed for bounds to update correctly
left={(tooltipLeft ?? 0) + (detectBounds ? 0 : 10)}
top={(tooltipTop ?? 0) + (detectBounds ? 0 : 10)}
style={tooltipStyles}
>
{tooltipData?.text}
<br />
<br />
<strong>left</strong> {tooltipLeft?.toFixed(0)}px
<br />
<strong>top</strong> {tooltipTop?.toFixed(0)}px
</TooltipComponent>
</TooltipWrapper>
<TooltipComponent
key={Math.random()} // needed for bounds to update correctly
left={tooltipLeft}
top={tooltipTop}
style={tooltipStyles}
>
{tooltipData}
<br />
<br />
<strong>left</strong> {tooltipLeft?.toFixed(0)}px&nbsp;&nbsp;
<strong>top</strong> {tooltipTop?.toFixed(0)}px
</TooltipComponent>
</>
) : (
<div className="no-tooltip">Move or touch the canvas to see the tooltip</div>
Expand All @@ -135,22 +121,11 @@ export default function Example({ width, height, showControls = true }: TooltipP
<label>
<input
type="checkbox"
checked={renderInPortal}
defaultChecked={renderTooltipInPortal}
onClick={e => {
// if rendered in clickable container, don't trigger that event
e.stopPropagation();
const nextRenderInPortal = !renderInPortal;
setRenderInPortal(nextRenderInPortal);
if (tooltipOpen && tooltipData) {
// update the tooltip coordinates to account for the Portal change
showTooltip({
tooltipData,
tooltipLeft:
tooltipData?.[nextRenderInPortal ? 'pageX' : 'containerX'] ?? tooltipLeft,
tooltipTop:
tooltipData?.[nextRenderInPortal ? 'pageY' : 'containerY'] ?? tooltipTop,
});
}
setRenderTooltipInPortal(!renderTooltipInPortal);
}}
/>
&nbsp;rendering in Portal
Expand All @@ -166,16 +141,16 @@ export default function Example({ width, height, showControls = true }: TooltipP
<label>
<input
type="checkbox"
checked={detectBounds}
onChange={() => setDetectBounds(!detectBounds)}
checked={tooltipShouldDetectBounds}
onChange={() => setTooltipShouldDetectBounds(!tooltipShouldDetectBounds)}
/>
&nbsp;Tooltip with boundary detection
</label>

<button onClick={() => hideTooltip()}>Hide tooltip</button>
</div>
)}
<style jsx>{`
<style>{`
.tooltip-example {
z-index: 0;
position: relative;
Expand All @@ -201,6 +176,8 @@ export default function Example({ width, height, showControls = true }: TooltipP
margin-right: 8px;
}
.position-indicator {
width: ${positionIndicatorSize}px;
height: ${positionIndicatorSize}px;
border-radius: 50%;
background: #35477d;
position: absolute;
Expand Down
1 change: 0 additions & 1 deletion packages/vx-demo/src/sandboxes/vx-tooltip/package.json
Expand Up @@ -12,7 +12,6 @@
"react": "^16",
"react-dom": "^16",
"react-scripts-ts": "3.1.0",
"react-use-measure": "^2.0.1",
"typescript": "^3"
},
"keywords": [
Expand Down

0 comments on commit e441821

Please sign in to comment.