-
Notifications
You must be signed in to change notification settings - Fork 691
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new(xychart): add (EventEmitter, Tooltip)Context + basic Tooltips (#825)
* deps(xychart): add mitt * new(xychart): add EventEmitter context, provider, types, and hook * new(xychart): add findNearestDatum utils * new(xychart): add TooltipContext type, context, and provider * new(xychart): add Tooltip, update Example * new(xychart): add tooltip functionality to BarSeries * fix(xychart): clean up * fix(xychart): fix test, improve tooltip prop annotations and containerRef logic * internal(xychart): svgCoords => svgPoint + point, svgCoord => scaledValue * test(xychart): add Event + Tooltip tests (#835) * test(xychart): organize test/ into subfolders that mirror src/ * test(xychart): add EventEmitterProvider + TooltipProvider tests * test(xychart): add Tooltip test, add UseTooltipPortalOptions props * test(xychart): add useEventEmitter tests * test(xychart): add findNearestDatum tests * fix(xychart): test + type fixes
- Loading branch information
1 parent
0da32a7
commit 4b08bcf
Showing
35 changed files
with
837 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import React, { useCallback, useContext } from 'react'; | ||
import { useTooltipInPortal, defaultStyles } from '@visx/tooltip'; | ||
import { TooltipProps as BaseTooltipProps } from '@visx/tooltip/lib/tooltips/Tooltip'; | ||
import { PickD3Scale } from '@visx/scale'; | ||
import { UseTooltipPortalOptions } from '@visx/tooltip/lib/hooks/useTooltipInPortal'; | ||
|
||
import TooltipContext from '../context/TooltipContext'; | ||
import DataContext from '../context/DataContext'; | ||
import { TooltipContextType } from '../types'; | ||
|
||
export type RenderTooltipParams = TooltipContextType & { | ||
colorScale?: PickD3Scale<'ordinal', string, string>; | ||
}; | ||
|
||
export type TooltipProps = { | ||
/** | ||
* When TooltipContext.tooltipOpen=true, this function is invoked and if the | ||
* return value is non-null, its content is rendered inside the tooltip container. | ||
* Content will be rendered in an HTML parent. | ||
*/ | ||
renderTooltip: (params: RenderTooltipParams) => React.ReactNode; | ||
/** | ||
* Tooltip depends on ResizeObserver, which may be pollyfilled globally | ||
* or injected into this component. | ||
*/ | ||
resizeObserverPolyfill?: UseTooltipPortalOptions['polyfill']; | ||
} & Omit<BaseTooltipProps, 'left' | 'top' | 'children'> & | ||
Pick<UseTooltipPortalOptions, 'debounce' | 'detectBounds' | 'scroll'>; | ||
|
||
const INVISIBLE_STYLES: React.CSSProperties = { | ||
position: 'absolute', | ||
left: 0, | ||
top: 0, | ||
opacity: 0, | ||
width: 0, | ||
height: 0, | ||
pointerEvents: 'none', | ||
}; | ||
|
||
export default function Tooltip({ | ||
renderTooltip, | ||
debounce, | ||
detectBounds, | ||
resizeObserverPolyfill, | ||
scroll, | ||
...tooltipProps | ||
}: TooltipProps) { | ||
const { colorScale, theme } = useContext(DataContext) || {}; | ||
const tooltipContext = useContext(TooltipContext); | ||
const { containerRef, TooltipInPortal } = useTooltipInPortal({ | ||
debounce, | ||
detectBounds, | ||
polyfill: resizeObserverPolyfill, | ||
scroll, | ||
}); | ||
|
||
// To correctly position itself in a Portal, the tooltip must know its container bounds | ||
// this is done by rendering an invisible node which can be used to find its parents element | ||
const setContainerRef = useCallback( | ||
(ownRef: HTMLElement | SVGElement | null) => { | ||
containerRef(ownRef?.parentElement ?? null); | ||
}, | ||
[containerRef], | ||
); | ||
|
||
const tooltipContent = tooltipContext?.tooltipOpen | ||
? renderTooltip({ ...tooltipContext, colorScale }) | ||
: null; | ||
|
||
return ( | ||
// Tooltip can be rendered as a child of SVG or HTML since its output is rendered in a Portal. | ||
// So use svg element to find container ref because it's a valid child of SVG and HTML parents. | ||
<> | ||
<svg ref={setContainerRef} style={INVISIBLE_STYLES} /> | ||
{tooltipContext?.tooltipOpen && tooltipContent != null && ( | ||
<TooltipInPortal | ||
left={tooltipContext?.tooltipLeft} | ||
top={tooltipContext?.tooltipTop} | ||
style={{ | ||
...defaultStyles, | ||
background: theme?.backgroundColor ?? 'white', | ||
boxShadow: `0 1px 2px ${ | ||
theme?.htmlLabelStyles?.color ? `${theme?.htmlLabelStyles?.color}55` : '#22222255' | ||
}`, | ||
|
||
...theme?.htmlLabelStyles, | ||
}} | ||
{...tooltipProps} | ||
> | ||
{tooltipContent} | ||
</TooltipInPortal> | ||
)} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createContext } from 'react'; | ||
import { EventEmitterContextType } from '../types'; | ||
|
||
const EventEmitterContext = createContext<EventEmitterContextType | null>(null); | ||
|
||
export default EventEmitterContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createContext } from 'react'; | ||
import { TooltipContextType } from '../types'; | ||
|
||
const TooltipContext = createContext<TooltipContextType | null>(null); | ||
|
||
export default TooltipContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useCallback, useContext, useEffect } from 'react'; | ||
import { localPoint } from '@visx/event'; | ||
import EventEmitterContext from '../context/EventEmitterContext'; | ||
|
||
export type EventType = 'mousemove' | 'mouseout' | 'touchmove' | 'touchend' | 'click'; | ||
export type HandlerParams = { | ||
event: React.MouseEvent | React.TouchEvent; | ||
svgPoint: ReturnType<typeof localPoint>; | ||
}; | ||
export type Handler = (params?: HandlerParams) => void; | ||
|
||
/** | ||
* Hook for optionally subscribing to a specified EventType, | ||
* and returns emitter for emitting events. | ||
*/ | ||
export default function useEventEmitter(eventType?: EventType, handler?: Handler) { | ||
const emitter = useContext(EventEmitterContext); | ||
|
||
/** wrap emitter.emit so we can enforce stricter type signature */ | ||
const emit = useCallback( | ||
(type: EventType, event: HandlerParams['event']) => | ||
emitter?.emit<HandlerParams>(type, { event, svgPoint: localPoint(event) }), | ||
[emitter], | ||
); | ||
|
||
useEffect(() => { | ||
if (emitter && eventType && handler) { | ||
emitter.on<HandlerParams>(eventType, handler); | ||
return () => emitter?.off<HandlerParams>(eventType, handler); | ||
} | ||
return undefined; | ||
}, [emitter, eventType, handler]); | ||
|
||
return emitter ? emit : null; | ||
} |
Oops, something went wrong.