From 04981a2826cee00932d8e8f1036534b3e04e2bc0 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Thu, 3 Dec 2020 19:21:34 -0800 Subject: [PATCH 01/12] new(xychart/useEvent*): add focus/blur events --- .../visx-xychart/src/hooks/useEventEmitter.ts | 6 +-- .../src/hooks/usePointerEventEmitters.ts | 14 +++++ .../src/hooks/usePointerEventHandlers.ts | 53 +++++++++++++------ 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/visx-xychart/src/hooks/useEventEmitter.ts b/packages/visx-xychart/src/hooks/useEventEmitter.ts index 7af1c1883..65187e89e 100644 --- a/packages/visx-xychart/src/hooks/useEventEmitter.ts +++ b/packages/visx-xychart/src/hooks/useEventEmitter.ts @@ -2,11 +2,11 @@ import { useCallback, useContext, useEffect, useRef } from 'react'; import { localPoint } from '@visx/event'; import EventEmitterContext from '../context/EventEmitterContext'; -export type EventType = 'pointermove' | 'pointerout' | 'pointerup'; +export type EventType = 'pointermove' | 'pointerout' | 'pointerup' | 'focus' | 'blur'; export type HandlerParams = { - /** The react PointerEvent. */ - event: React.PointerEvent; + /** The react PointerEvent or FocusEvent. */ + event: React.PointerEvent | React.FocusEvent; /** Position of the PointerEvent in svg coordinates. */ svgPoint: ReturnType; /** The source of the event. This can be anything, but for this package is the name of the component which emitted the event. */ diff --git a/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts b/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts index 8ada2b85e..23c5b6a61 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts +++ b/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts @@ -4,6 +4,8 @@ import useEventEmitter from './useEventEmitter'; type PointerEventEmitterParams = { /** Source of the events, e.g., the component name. */ source: string; + onBlur?: boolean; + onFocus?: boolean; onPointerMove?: boolean; onPointerOut?: boolean; onPointerUp?: boolean; @@ -18,6 +20,8 @@ export default function usePointerEventEmitters({ onPointerOut = true, onPointerMove = true, onPointerUp = true, + onFocus = false, + onBlur = false, }: PointerEventEmitterParams) { const emit = useEventEmitter(); @@ -33,9 +37,19 @@ export default function usePointerEventEmitters({ (event: React.PointerEvent) => emit?.('pointerup', event, source), [emit, source], ); + const emitFocus = useCallback((event: React.FocusEvent) => emit?.('focus', event, source), [ + emit, + source, + ]); + const emitBlur = useCallback((event: React.FocusEvent) => emit?.('blur', event, source), [ + emit, + source, + ]); return { onPointerMove: onPointerMove ? emitPointerMove : undefined, + onFocus: onFocus ? emitFocus : undefined, + onBlur: onBlur ? emitBlur : undefined, onPointerOut: onPointerOut ? emitPointerOut : undefined, onPointerUp: onPointerUp ? emitPointerUp : undefined, }; diff --git a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts index 2892b4554..83dc47a28 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts +++ b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts @@ -1,6 +1,7 @@ -import { PointerEvent, useCallback, useContext } from 'react'; +import { PointerEvent, FocusEvent, useCallback, useContext } from 'react'; import DataContext from '../context/DataContext'; -import { PointerEventParams } from '../types'; +import { isPointerEvent } from '../typeguards/events'; +import { EventHandlerParams } from '../types'; import findNearestDatumX from '../utils/findNearestDatumX'; import findNearestDatumY from '../utils/findNearestDatumY'; import useEventEmitter, { HandlerParams } from './useEventEmitter'; @@ -11,12 +12,16 @@ export const POINTER_EVENTS_NEAREST = '__POINTER_EVENTS_NEAREST'; type PointerEventHandlerParams = { /** Controls whether callbacks are invoked for one or more registered dataKeys, the nearest dataKey, or all dataKeys. */ dataKey: string | string[] | typeof POINTER_EVENTS_NEAREST | typeof POINTER_EVENTS_ALL; // last two are eaten by string + /** Callback invoked onFocus for one or more series based on dataKey. */ + onFocus?: (params: EventHandlerParams) => void; + /** Callback invoked onBlur. */ + onBlur?: (event: FocusEvent) => void; /** Callback invoked onPointerMove for one or more series based on dataKey. */ - onPointerMove?: (params: PointerEventParams) => void; - /** Callback invoked onPointerOut for one or more series based on dataKey. */ + onPointerMove?: (params: EventHandlerParams) => void; + /** Callback invoked onPointerOut. */ onPointerOut?: (event: PointerEvent) => void; /** Callback invoked onPointerUp for one or more series based on dataKey. */ - onPointerUp?: (params: PointerEventParams) => void; + onPointerUp?: (params: EventHandlerParams) => void; /** Valid event sources for which to invoke handlers. */ sources?: string[]; }; @@ -27,6 +32,8 @@ type PointerEventHandlerParams = { */ export default function usePointerEventHandlers({ dataKey, + onBlur, + onFocus, onPointerMove, onPointerOut, onPointerUp, @@ -34,13 +41,13 @@ export default function usePointerEventHandlers({ }: PointerEventHandlerParams) { const { width, height, horizontal, dataRegistry, xScale, yScale } = useContext(DataContext); - const handlePointerMoveOrUp = useCallback( + const handlePointerMoveUpOrFocus = useCallback( (params?: HandlerParams) => { const { svgPoint, event } = params || {}; - const pointerParamsByKey: { [dataKey: string]: PointerEventParams } = {}; + const pointerParamsByKey: { [dataKey: string]: EventHandlerParams } = {}; // nearest Datum across all dataKeys, if relevant - let nearestDatumPointerParams: PointerEventParams | null = null; + let nearestDatumPointerParams: EventHandlerParams | null = null; let nearestDatumDistance = Infinity; if (params && event && svgPoint && width && height && xScale && yScale) { @@ -67,6 +74,7 @@ export default function usePointerEventHandlers({ yAccessor: entry.yAccessor, yScale, }); + if (nearestDatum) { pointerParamsByKey[key] = { key, svgPoint, event, ...nearestDatum }; @@ -86,18 +94,19 @@ export default function usePointerEventHandlers({ } }); - const pointerParams: (null | PointerEventParams)[] = + const pointerParams: (null | EventHandlerParams)[] = dataKey === POINTER_EVENTS_NEAREST ? [nearestDatumPointerParams] : dataKey === POINTER_EVENTS_ALL || Array.isArray(dataKey) ? Object.values(pointerParamsByKey) : [pointerParamsByKey[dataKey]]; - pointerParams.forEach(p => { - if (p?.event.type === 'pointerup' && onPointerUp) { - onPointerUp(p); - } else if (p?.event.type === 'pointermove') { - if (onPointerMove) onPointerMove(p); + pointerParams.forEach(pointerParam => { + const eventType = pointerParam?.event.type; + if (eventType === 'pointerup' && onPointerUp && pointerParam) { + onPointerUp(pointerParam); + } else if ((eventType === 'pointermove' || eventType === 'focus') && pointerParam) { + if (onPointerMove) onPointerMove(pointerParam); } }); } @@ -107,12 +116,22 @@ export default function usePointerEventHandlers({ const handlePointerOut = useCallback( (params?: HandlerParams) => { - if (params && onPointerOut) onPointerOut(params.event); + const event = params?.event; + if (event && isPointerEvent(event) && onPointerOut) onPointerOut(event); }, [onPointerOut], ); + const handleBlur = useCallback( + (params?: HandlerParams) => { + const event = params?.event; + if (event && !isPointerEvent(event) && onBlur) onBlur(event); + }, + [onBlur], + ); - useEventEmitter('pointermove', onPointerMove ? handlePointerMoveOrUp : undefined, sources); + useEventEmitter('pointermove', onPointerMove ? handlePointerMoveUpOrFocus : undefined, sources); useEventEmitter('pointerout', onPointerOut ? handlePointerOut : undefined, sources); - useEventEmitter('pointerup', onPointerUp ? handlePointerMoveOrUp : undefined, sources); + useEventEmitter('pointerup', onPointerUp ? handlePointerMoveUpOrFocus : undefined, sources); + useEventEmitter('focus', onFocus ? handlePointerMoveUpOrFocus : undefined, sources); + useEventEmitter('blur', onBlur ? handleBlur : undefined, sources); } From 23983cfc4703f90724baad1c447aa6c1b4c06151 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Thu, 3 Dec 2020 19:23:31 -0800 Subject: [PATCH 02/12] new(xychart): add focus/blur handlers to Series props, add handlers to bar+glyph series --- .../visx-xychart/src/components/XYChart.tsx | 6 +-- .../src/components/series/GlyphSeries.tsx | 11 ++++- .../series/private/AnimatedBars.tsx | 2 + .../series/private/AnimatedGlyphs.tsx | 4 ++ .../src/components/series/private/Bars.tsx | 9 +++- .../series/private/BaseAreaSeries.tsx | 4 +- .../series/private/BaseBarGroup.tsx | 4 +- .../series/private/BaseBarSeries.tsx | 41 ++++++++++------- .../series/private/BaseBarStack.tsx | 4 +- .../series/private/BaseGlyphSeries.tsx | 41 ++++++++++------- .../series/private/BaseLineSeries.tsx | 4 +- .../series/private/defaultRenderGlyph.tsx | 3 ++ .../src/providers/TooltipProvider.tsx | 4 +- .../visx-xychart/src/typeguards/events.ts | 6 +++ packages/visx-xychart/src/types/series.ts | 45 +++++++++++++++---- packages/visx-xychart/src/types/tooltip.ts | 4 +- 16 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 packages/visx-xychart/src/typeguards/events.ts diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index 4be182f43..52f60a531 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -5,7 +5,7 @@ import { AxisScaleOutput } from '@visx/axis'; import { ScaleConfig } from '@visx/scale'; import DataContext from '../context/DataContext'; -import { Margin, PointerEventParams } from '../types'; +import { Margin, EventHandlerParams } from '../types'; import useEventEmitter from '../hooks/useEventEmitter'; import EventEmitterProvider from '../providers/EventEmitterProvider'; import TooltipContext from '../context/TooltipContext'; @@ -52,7 +52,7 @@ export type XYChartProps< index, key, svgPoint, - }: PointerEventParams) => void; + }: EventHandlerParams) => void; /** Callback invoked for onPointerOut events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */ onPointerOut?: ( /** The PointerEvent. */ @@ -67,7 +67,7 @@ export type XYChartProps< index, key, svgPoint, - }: PointerEventParams) => void; + }: EventHandlerParams) => void; /** Whether to invoke PointerEvent handlers for all dataKeys, or the nearest dataKey. */ pointerEventsDataKey?: 'all' | 'nearest'; }; diff --git a/packages/visx-xychart/src/components/series/GlyphSeries.tsx b/packages/visx-xychart/src/components/series/GlyphSeries.tsx index 1c88b24cb..126b8bd3a 100644 --- a/packages/visx-xychart/src/components/series/GlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/GlyphSeries.tsx @@ -15,10 +15,17 @@ export default function GlyphSeries< renderGlyph?: React.FC>; }) { const renderGlyphs = useCallback( - ({ glyphs, onPointerMove, onPointerOut, onPointerUp }: GlyphsProps) => + ({ + glyphs, + onPointerMove, + onPointerOut, + onPointerUp, + onFocus, + onBlur, + }: GlyphsProps) => glyphs.map(glyph => ( - {renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp })} + {renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp, onFocus, onBlur })} )), [renderGlyph], diff --git a/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx b/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx index 07e32b564..8793db5cf 100644 --- a/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx +++ b/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx @@ -54,6 +54,7 @@ export default function AnimatedBars element + onBlur, + onFocus, onPointerMove, onPointerOut, onPointerUp, diff --git a/packages/visx-xychart/src/components/series/private/Bars.tsx b/packages/visx-xychart/src/components/series/private/Bars.tsx index 44567cf78..6beeeada3 100644 --- a/packages/visx-xychart/src/components/series/private/Bars.tsx +++ b/packages/visx-xychart/src/components/series/private/Bars.tsx @@ -9,11 +9,18 @@ export default function Bars({ yScale, ...rectProps }: BarsProps) { + const isFocusable = Boolean(rectProps.onFocus || rectProps.onBlur); return ( // eslint-disable-next-line react/jsx-no-useless-fragment <> {bars.map(({ key, ...barProps }) => ( - + ))} ); diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx index 7e3a13cd4..87df67661 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -3,7 +3,7 @@ import { AxisScale } from '@visx/axis'; import Area, { AreaProps } from '@visx/shape/lib/shapes/Area'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import DataContext from '../../../context/DataContext'; -import { PointerEventParams, SeriesProps, TooltipContextType } from '../../../types'; +import { EventHandlerParams, SeriesProps, TooltipContextType } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import getScaleBaseline from '../../../utils/getScaleBaseline'; @@ -61,7 +61,7 @@ function BaseAreaSeries; const onPointerMove = useCallback( - (p: PointerEventParams) => { + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); }, diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index eb85dc59b..463ac3c93 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -7,7 +7,7 @@ import { Bar, BarsProps, DataContextType, - PointerEventParams, + EventHandlerParams, SeriesProps, TooltipContextType, } from '../../../types'; @@ -102,7 +102,7 @@ export default function BaseBarGroup< Datum >; const onPointerMove = useCallback( - (p: PointerEventParams) => { + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); }, diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index 9670482dc..512db5aa7 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -4,7 +4,7 @@ import DataContext from '../../../context/DataContext'; import { Bar, BarsProps, - PointerEventParams, + EventHandlerParams, SeriesProps, TooltipContextType, } from '../../../types'; @@ -17,6 +17,7 @@ import { BARSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import TooltipContext from '../../../context/TooltipContext'; +import { isPointerEvent } from '../../../typeguards/events'; export type BaseBarSeriesProps< XScale extends AxisScale, @@ -42,10 +43,12 @@ function BaseBarSeries; - const onPointerMove = useCallback( - (p: PointerEventParams) => { + const onPointerMoveOrFocus = useCallback( + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); + if (onFocusProps) onFocusProps(p); }, - [showTooltip, onPointerMoveProps], + [showTooltip, onPointerMoveProps, onFocusProps], ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { + const onPointerOutOrBlur = useCallback( + (event: React.PointerEvent | React.FocusEvent) => { hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); + if (event) { + if (onPointerOutProps && isPointerEvent(event)) onPointerOutProps(event); + else if (onBlurProps && !isPointerEvent(event)) onBlurProps(event); + } }, - [hideTooltip, onPointerOutProps], + [hideTooltip, onPointerOutProps, onBlurProps], ); const ownEventSourceKey = `${BARSERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, }); usePointerEventHandlers({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + onBlur: enableEvents ? onPointerOutOrBlur : undefined, + onFocus: enableEvents ? onPointerMoveOrFocus : undefined, + onPointerMove: enableEvents ? onPointerMoveOrFocus : undefined, + onPointerOut: enableEvents ? onPointerOutOrBlur : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx index 5b415431c..6552b4bf3 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx @@ -15,7 +15,7 @@ import { BarStackDatum, CombinedStackData, DataContextType, - PointerEventParams, + EventHandlerParams, SeriesProps, TooltipContextType, } from '../../../types'; @@ -138,7 +138,7 @@ function BaseBarStack< Datum >; const onPointerMove = useCallback( - (p: PointerEventParams) => { + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); }, diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index 735e63a73..c8a3d2569 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -4,7 +4,7 @@ import DataContext from '../../../context/DataContext'; import { GlyphProps, GlyphsProps, - PointerEventParams, + EventHandlerParams, SeriesProps, TooltipContextType, } from '../../../types'; @@ -15,6 +15,7 @@ import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { GLYPHSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import TooltipContext from '../../../context/TooltipContext'; +import { isPointerEvent } from '../../../typeguards/events'; export type BaseGlyphSeriesProps< XScale extends AxisScale, @@ -30,10 +31,12 @@ export type BaseGlyphSeriesProps< function BaseGlyphSeries({ data, dataKey, + onBlur: onBlurProps, + onFocus: onFocusProps, onPointerMove: onPointerMoveProps, onPointerOut: onPointerOutProps, onPointerUp: onPointerUpProps, - pointerEvents = true, + enableEvents = true, renderGlyphs, size = 8, xAccessor, @@ -50,32 +53,40 @@ function BaseGlyphSeries; - const onPointerMove = useCallback( - (p: PointerEventParams) => { + const onPointerMoveOrFocus = useCallback( + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); + if (onFocusProps) onFocusProps(p); }, - [showTooltip, onPointerMoveProps], + [showTooltip, onPointerMoveProps, onFocusProps], ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { + const onPointerOutOrBlur = useCallback( + (event: React.PointerEvent | React.FocusEvent) => { hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); + if (event) { + if (onPointerOutProps && isPointerEvent(event)) onPointerOutProps(event); + else if (onBlurProps && !isPointerEvent(event)) onBlurProps(event); + } }, - [hideTooltip, onPointerOutProps], + [hideTooltip, onPointerOutProps, onBlurProps], ); const ownEventSourceKey = `${GLYPHSERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, }); usePointerEventHandlers({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + onBlur: enableEvents ? onPointerOutOrBlur : undefined, + onFocus: enableEvents ? onPointerMoveOrFocus : undefined, + onPointerMove: enableEvents ? onPointerMoveOrFocus : undefined, + onPointerOut: enableEvents ? onPointerOutOrBlur : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); diff --git a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx index 6cfc5334e..63e8b7423 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -2,7 +2,7 @@ import React, { useContext, useCallback } from 'react'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { PointerEventParams, SeriesProps, TooltipContextType } from '../../../types'; +import { EventHandlerParams, SeriesProps, TooltipContextType } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import TooltipContext from '../../../context/TooltipContext'; @@ -53,7 +53,7 @@ function BaseLineSeries; const onPointerMove = useCallback( - (p: PointerEventParams) => { + (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); }, diff --git a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx index fced8dbb5..f4cad473c 100644 --- a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx +++ b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx @@ -7,6 +7,8 @@ export default function defaultRenderGlyph({ x, y, size, + onBlur, + onFocus, onPointerMove, onPointerOut, onPointerUp, @@ -15,6 +17,7 @@ export default function defaultRenderGlyph({ ({ const debouncedHideTooltip = useRef | null>(null); const showTooltip = useRef( - ({ svgPoint, index, key, datum, distanceX, distanceY }: PointerEventParams) => { + ({ svgPoint, index, key, datum, distanceX, distanceY }: EventHandlerParams) => { // cancel any hideTooltip calls so it won't hide after invoking the logic below if (debouncedHideTooltip.current) { debouncedHideTooltip.current.cancel(); diff --git a/packages/visx-xychart/src/typeguards/events.ts b/packages/visx-xychart/src/typeguards/events.ts new file mode 100644 index 000000000..3105a81b9 --- /dev/null +++ b/packages/visx-xychart/src/typeguards/events.ts @@ -0,0 +1,6 @@ +// functional definition of a PointerEvent (mouse, touch) +export function isPointerEvent( + event?: React.PointerEvent | React.FocusEvent, +): event is React.PointerEvent { + return !!event && ('clientX' in event || 'changedTouches' in event); +} diff --git a/packages/visx-xychart/src/types/series.ts b/packages/visx-xychart/src/types/series.ts index e17730856..6872ca766 100644 --- a/packages/visx-xychart/src/types/series.ts +++ b/packages/visx-xychart/src/types/series.ts @@ -1,10 +1,10 @@ -import { PointerEvent } from 'react'; +import { PointerEvent, FocusEvent } from 'react'; import { AxisScale } from '@visx/axis'; import { ScaleInput } from '@visx/scale'; import { Series, SeriesPoint } from 'd3-shape'; /** Call signature of PointerEvent callback. */ -export type PointerEventParams = { +export type EventHandlerParams = { /** Series key that datum belongs to. */ key: string; /** Index of datum in series data array. */ @@ -17,8 +17,8 @@ export type PointerEventParams = { distanceY?: number; /** Coordinates of the event in svg space. */ svgPoint?: { x: number; y: number }; - /** The PointerEvent. */ - event: PointerEvent; + /** The PointerEvent or FocusEvent. */ + event: PointerEvent | FocusEvent; }; export type SeriesProps< @@ -48,7 +48,7 @@ export type SeriesProps< index, key, svgPoint, - }: PointerEventParams) => void; + }: EventHandlerParams) => void; /** * Callback invoked for onPointerOut events. By default XYChart will capture and emit * PointerEvents, invoking this function for any Series with a defined handler. @@ -73,9 +73,30 @@ export type SeriesProps< index, key, svgPoint, - }: PointerEventParams) => void; - /** Whether the Series emits and subscribes to PointerEvents (including Tooltip triggering). */ - pointerEvents?: boolean; + }: EventHandlerParams) => void; + /** + * Callback invoked for onFocus events for the nearest Datum to the FocusEvent. + * XYChart will NOT capture and emit FocusEvents, they are emitted from individual Series glyph shapes. + */ + onFocus?: ({ + datum, + distanceX, + distanceY, + event, + index, + key, + svgPoint, + }: EventHandlerParams) => void; + /** + * Callback invoked for onBlur events for the nearest Datum to the FocusEvent. + * XYChart will NOT capture and emit FocusEvents, they are emitted from individual Series glyph shapes. + */ + onBlur?: ( + /** The FocusEvent. */ + event: React.FocusEvent, + ) => void; + /** Whether the Series emits and subscribes to PointerEvents and FocusEvents (including Tooltip triggering). */ + enableEvents?: boolean; }; /** Bar shape. */ @@ -126,6 +147,10 @@ export type GlyphsProps< yScale: YScale; horizontal?: boolean; glyphs: GlyphProps[]; + /** Callback to invoke for onBlur. */ + onBlur?: (event: FocusEvent) => void; + /** Callback to invoke for onFocus. */ + onFocus?: (event: FocusEvent) => void; /** Callback to invoke for onPointerMove. */ onPointerMove?: (event: PointerEvent) => void; /** Callback to invoke for onPointerOut. */ @@ -149,6 +174,10 @@ export type GlyphProps = { size: number; /** Color of Glyph. */ color: string; + /** Callback to invoke for onBlur. */ + onBlur?: (event: FocusEvent) => void; + /** Callback to invoke for onFocus. */ + onFocus?: (event: FocusEvent) => void; /** Callback to invoke for onPointerMove. */ onPointerMove?: (event: PointerEvent) => void; /** Callback to invoke for onPointerOut. */ diff --git a/packages/visx-xychart/src/types/tooltip.ts b/packages/visx-xychart/src/types/tooltip.ts index 1b96d5b85..6748643dc 100644 --- a/packages/visx-xychart/src/types/tooltip.ts +++ b/packages/visx-xychart/src/types/tooltip.ts @@ -1,5 +1,5 @@ import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip'; -import { PointerEventParams } from './series'; +import { EventHandlerParams } from './series'; export type TooltipDatum = { /** Series key that datum belongs to. */ @@ -20,5 +20,5 @@ export type TooltipData = { }; export type TooltipContextType = UseTooltipParams> & { - showTooltip: (params: PointerEventParams) => void; + showTooltip: (params: EventHandlerParams) => void; }; From f1e0895932f481594134610f9bd03bca8c33283b Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 13:28:15 -0800 Subject: [PATCH 03/12] new(xychart): add focus/blur handling to Line and Area series --- .../components/series/AnimatedAreaSeries.tsx | 2 +- .../components/series/AnimatedBarGroup.tsx | 2 +- .../components/series/AnimatedBarStack.tsx | 2 +- .../components/series/AnimatedLineSeries.tsx | 2 +- .../src/components/series/AreaSeries.tsx | 2 +- .../src/components/series/BarGroup.tsx | 2 +- .../src/components/series/BarSeries.tsx | 6 +- .../src/components/series/BarStack.tsx | 2 +- .../src/components/series/LineSeries.tsx | 2 +- .../series/private/BaseAreaSeries.tsx | 75 ++++++++++++--- .../series/private/BaseBarSeries.tsx | 36 ++++--- .../series/private/BaseBarStack.tsx | 16 ++-- .../series/private/BaseGlyphSeries.tsx | 44 ++++++--- .../series/private/BaseLineSeries.tsx | 94 ++++++++++++++----- .../series/private/defaultRenderGlyph.tsx | 2 + 15 files changed, 207 insertions(+), 82 deletions(-) diff --git a/packages/visx-xychart/src/components/series/AnimatedAreaSeries.tsx b/packages/visx-xychart/src/components/series/AnimatedAreaSeries.tsx index 7031b9637..e02c3ddfe 100644 --- a/packages/visx-xychart/src/components/series/AnimatedAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/AnimatedAreaSeries.tsx @@ -7,6 +7,6 @@ export default function AnimatedAreaSeries< XScale extends AxisScale, YScale extends AxisScale, Datum extends object ->({ ...props }: Omit, 'PathComponent'>) { +>(props: Omit, 'PathComponent'>) { return {...props} PathComponent={AnimatedPath} />; } diff --git a/packages/visx-xychart/src/components/series/AnimatedBarGroup.tsx b/packages/visx-xychart/src/components/series/AnimatedBarGroup.tsx index 6047e7f9c..cb2b1f098 100644 --- a/packages/visx-xychart/src/components/series/AnimatedBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/AnimatedBarGroup.tsx @@ -7,6 +7,6 @@ export default function AnimatedBarGroup< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ ...props }: Omit, 'BarsComponent'>) { +>(props: Omit, 'BarsComponent'>) { return {...props} BarsComponent={AnimatedBars} />; } diff --git a/packages/visx-xychart/src/components/series/AnimatedBarStack.tsx b/packages/visx-xychart/src/components/series/AnimatedBarStack.tsx index 02b5c6d58..abc085023 100644 --- a/packages/visx-xychart/src/components/series/AnimatedBarStack.tsx +++ b/packages/visx-xychart/src/components/series/AnimatedBarStack.tsx @@ -7,6 +7,6 @@ export default function AnimatedBarStack< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ ...props }: Omit, 'BarsComponent'>) { +>(props: Omit, 'BarsComponent'>) { return {...props} BarsComponent={AnimatedBars} />; } diff --git a/packages/visx-xychart/src/components/series/AnimatedLineSeries.tsx b/packages/visx-xychart/src/components/series/AnimatedLineSeries.tsx index c904dc29e..d808adba3 100644 --- a/packages/visx-xychart/src/components/series/AnimatedLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/AnimatedLineSeries.tsx @@ -7,6 +7,6 @@ export default function AnimatedLineSeries< XScale extends AxisScale, YScale extends AxisScale, Datum extends object ->({ ...props }: Omit, 'PathComponent'>) { +>(props: Omit, 'PathComponent'>) { return {...props} PathComponent={AnimatedPath} />; } diff --git a/packages/visx-xychart/src/components/series/AreaSeries.tsx b/packages/visx-xychart/src/components/series/AreaSeries.tsx index 325351a10..61fc37fd9 100644 --- a/packages/visx-xychart/src/components/series/AreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/AreaSeries.tsx @@ -6,6 +6,6 @@ export default function AreaSeries< XScale extends AxisScale, YScale extends AxisScale, Datum extends object ->({ ...props }: Omit, 'PathComponent'>) { +>(props: Omit, 'PathComponent'>) { return {...props} />; } diff --git a/packages/visx-xychart/src/components/series/BarGroup.tsx b/packages/visx-xychart/src/components/series/BarGroup.tsx index f2142e795..ff37b7602 100644 --- a/packages/visx-xychart/src/components/series/BarGroup.tsx +++ b/packages/visx-xychart/src/components/series/BarGroup.tsx @@ -7,6 +7,6 @@ export default function BarGroup< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ ...props }: Omit, 'BarsComponent'>) { +>(props: Omit, 'BarsComponent'>) { return {...props} BarsComponent={Bars} />; } diff --git a/packages/visx-xychart/src/components/series/BarSeries.tsx b/packages/visx-xychart/src/components/series/BarSeries.tsx index 473e22737..3e6d08a22 100644 --- a/packages/visx-xychart/src/components/series/BarSeries.tsx +++ b/packages/visx-xychart/src/components/series/BarSeries.tsx @@ -3,9 +3,9 @@ import React from 'react'; import BaseBarSeries, { BaseBarSeriesProps } from './private/BaseBarSeries'; import Bars from './private/Bars'; -function BarSeries({ - ...props -}: Omit, 'BarsComponent'>) { +function BarSeries( + props: Omit, 'BarsComponent'>, +) { return {...props} BarsComponent={Bars} />; } diff --git a/packages/visx-xychart/src/components/series/BarStack.tsx b/packages/visx-xychart/src/components/series/BarStack.tsx index 67c0fae9e..5f48bae05 100644 --- a/packages/visx-xychart/src/components/series/BarStack.tsx +++ b/packages/visx-xychart/src/components/series/BarStack.tsx @@ -7,6 +7,6 @@ export default function BarStack< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ ...props }: Omit, 'BarsComponent'>) { +>(props: Omit, 'BarsComponent'>) { return {...props} BarsComponent={Bars} />; } diff --git a/packages/visx-xychart/src/components/series/LineSeries.tsx b/packages/visx-xychart/src/components/series/LineSeries.tsx index b3f6d9341..5272af81c 100644 --- a/packages/visx-xychart/src/components/series/LineSeries.tsx +++ b/packages/visx-xychart/src/components/series/LineSeries.tsx @@ -6,6 +6,6 @@ export default function LineSeries< XScale extends AxisScale, YScale extends AxisScale, Datum extends object ->({ ...props }: Omit, 'PathComponent'>) { +>(props: Omit, 'PathComponent'>) { return {...props} />; } diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx index 87df67661..b5b0b13be 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -3,7 +3,7 @@ import { AxisScale } from '@visx/axis'; import Area, { AreaProps } from '@visx/shape/lib/shapes/Area'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import DataContext from '../../../context/DataContext'; -import { EventHandlerParams, SeriesProps, TooltipContextType } from '../../../types'; +import { EventHandlerParams, GlyphsProps, SeriesProps, TooltipContextType } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import getScaleBaseline from '../../../utils/getScaleBaseline'; @@ -12,6 +12,8 @@ import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { AREASERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import TooltipContext from '../../../context/TooltipContext'; +import { BaseGlyphSeries } from './BaseGlyphSeries'; +import defaultRenderGlyph from './defaultRenderGlyph'; export type BaseAreaSeriesProps< XScale extends AxisScale, @@ -26,10 +28,7 @@ export type BaseAreaSeriesProps< lineProps?: Omit, 'data' | 'x' | 'y' | 'children' | 'defined'>; /** Rendered component which is passed path props by BaseAreaSeries after processing. */ PathComponent?: React.FC, 'ref'>> | 'path'; -} & Omit< - React.SVGProps, - 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref' | 'pointerEvents' - >; +} & Omit, 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref'>; function BaseAreaSeries({ PathComponent = 'path', @@ -37,10 +36,12 @@ function BaseAreaSeries) => { + showTooltip(p); + if (onFocusProps) onFocusProps(p); + }, + [showTooltip, onFocusProps], + ); const onPointerOut = useCallback( (event: React.PointerEvent) => { hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); + if (event && onPointerOutProps) onPointerOutProps(event); }, [hideTooltip, onPointerOutProps], ); + const onBlur = useCallback( + (event: React.FocusEvent) => { + hideTooltip(); + if (event && onBlurProps) onBlurProps(event); + }, + [hideTooltip, onBlurProps], + ); const ownEventSourceKey = `${AREASERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, }); usePointerEventHandlers({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + onBlur: enableEvents ? onBlur : undefined, + onFocus: enableEvents ? onFocus : undefined, + onPointerMove: enableEvents ? onPointerMove : undefined, + onPointerOut: enableEvents ? onPointerOut : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -108,6 +127,24 @@ function BaseAreaSeries) => + pointerEventEmitters.onFocus || pointerEventEmitters.onBlur + ? glyphs.map(glyph => ( + + {defaultRenderGlyph({ + ...glyph, + color: 'transparent', + onFocus: pointerEventEmitters.onFocus, + onBlur: pointerEventEmitters.onBlur, + })} + + )) + : null, + [pointerEventEmitters.onFocus, pointerEventEmitters.onBlur], + ); + return ( <> @@ -143,6 +180,18 @@ function BaseAreaSeries )} + {/** Capture and emit FocusEvents */} + {(onFocusProps || onBlurProps) && ( + + )} ); } diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index 512db5aa7..195c0deb3 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -96,23 +96,33 @@ function BaseBarSeries; - const onPointerMoveOrFocus = useCallback( + const onPointerMove = useCallback( (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onFocus = useCallback( + (p: EventHandlerParams) => { + showTooltip(p); if (onFocusProps) onFocusProps(p); }, - [showTooltip, onPointerMoveProps, onFocusProps], + [showTooltip, onFocusProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (event && onPointerOutProps) onPointerOutProps(event); + }, + [hideTooltip, onPointerOutProps], ); - const onPointerOutOrBlur = useCallback( - (event: React.PointerEvent | React.FocusEvent) => { + const onBlur = useCallback( + (event: React.FocusEvent) => { hideTooltip(); - if (event) { - if (onPointerOutProps && isPointerEvent(event)) onPointerOutProps(event); - else if (onBlurProps && !isPointerEvent(event)) onBlurProps(event); - } + if (event && onBlurProps) onBlurProps(event); }, - [hideTooltip, onPointerOutProps, onBlurProps], + [hideTooltip, onBlurProps], ); const ownEventSourceKey = `${BARSERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ @@ -125,10 +135,10 @@ function BaseBarSeries, 'offset' | 'order'> & Pick< SeriesProps, - 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'pointerEvents' + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'enableEvents' >; function BaseBarStack< @@ -55,7 +55,7 @@ function BaseBarStack< onPointerMove: onPointerMoveProps, onPointerOut: onPointerOutProps, onPointerUp: onPointerUpProps, - pointerEvents = true, + enableEvents = true, }: BaseBarStackProps) { type StackBar = SeriesPoint>; const { @@ -154,15 +154,15 @@ function BaseBarStack< const ownEventSourceKey = `${BARSTACK_EVENT_SOURCE}-${dataKeys.join('-')}`; const pointerEventEmitters = usePointerEventEmitters({ source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, }); usePointerEventHandlers({ dataKey: dataKeys, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + onPointerMove: enableEvents ? onPointerMove : undefined, + onPointerOut: enableEvents ? onPointerOut : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index c8a3d2569..7b93a35ab 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -15,7 +15,6 @@ import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { GLYPHSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import TooltipContext from '../../../context/TooltipContext'; -import { isPointerEvent } from '../../../typeguards/events'; export type BaseGlyphSeriesProps< XScale extends AxisScale, @@ -28,7 +27,11 @@ export type BaseGlyphSeriesProps< renderGlyphs: (glyphsProps: GlyphsProps) => React.ReactNode; }; -function BaseGlyphSeries({ +export function BaseGlyphSeries< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +>({ data, dataKey, onBlur: onBlurProps, @@ -53,23 +56,34 @@ function BaseGlyphSeries; - const onPointerMoveOrFocus = useCallback( + const onPointerMove = useCallback( (p: EventHandlerParams) => { showTooltip(p); if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onFocus = useCallback( + (p: EventHandlerParams) => { + console.log(p); + showTooltip(p); if (onFocusProps) onFocusProps(p); }, - [showTooltip, onPointerMoveProps, onFocusProps], + [showTooltip, onFocusProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (event && onPointerOutProps) onPointerOutProps(event); + }, + [hideTooltip, onPointerOutProps], ); - const onPointerOutOrBlur = useCallback( - (event: React.PointerEvent | React.FocusEvent) => { + const onBlur = useCallback( + (event: React.FocusEvent) => { hideTooltip(); - if (event) { - if (onPointerOutProps && isPointerEvent(event)) onPointerOutProps(event); - else if (onBlurProps && !isPointerEvent(event)) onBlurProps(event); - } + if (event && onBlurProps) onBlurProps(event); }, - [hideTooltip, onPointerOutProps, onBlurProps], + [hideTooltip, onBlurProps], ); const ownEventSourceKey = `${GLYPHSERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ @@ -82,10 +96,10 @@ function BaseGlyphSeries, 'ref'>> | 'path'; /** Sets the curve factory (from @visx/curve or d3-curve) for the line generator. Defaults to curveLinear. */ curve?: LinePathProps['curve']; -} & Omit< - React.SVGProps, - 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref' | 'pointerEvents' - >; +} & Omit, 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref'>; function BaseLineSeries({ curve, data, dataKey, + onBlur: onBlurProps, + onFocus: onFocusProps, onPointerMove: onPointerMoveProps, onPointerOut: onPointerOutProps, onPointerUp: onPointerUpProps, - pointerEvents = true, + enableEvents = true, xAccessor, xScale, yAccessor, @@ -59,6 +60,13 @@ function BaseLineSeries) => { + showTooltip(p); + if (onFocusProps) onFocusProps(p); + }, + [showTooltip, onFocusProps], + ); const onPointerOut = useCallback( (event: React.PointerEvent) => { hideTooltip(); @@ -66,34 +74,76 @@ function BaseLineSeries { + hideTooltip(); + if (event && onBlurProps) onBlurProps(event); + }, + [hideTooltip, onBlurProps], + ); const ownEventSourceKey = `${LINESERIES_EVENT_SOURCE}-${dataKey}`; const pointerEventEmitters = usePointerEventEmitters({ source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, }); usePointerEventHandlers({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + onBlur: enableEvents ? onBlur : undefined, + onFocus: enableEvents ? onFocus : undefined, + onPointerMove: enableEvents ? onPointerMove : undefined, + onPointerOut: enableEvents ? onPointerOut : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); + // render invisible glyphs for focusing if onFocus/onBlur are defined + const renderGlyphs = useCallback( + ({ glyphs }: GlyphsProps) => + pointerEventEmitters.onFocus || pointerEventEmitters.onBlur + ? glyphs.map(glyph => ( + + {defaultRenderGlyph({ + ...glyph, + color: 'transparent', + onFocus: pointerEventEmitters.onFocus, + onBlur: pointerEventEmitters.onBlur, + })} + + )) + : null, + [pointerEventEmitters.onFocus, pointerEventEmitters.onBlur], + ); + return ( - - {({ path }) => ( - + + {({ path }) => ( + + )} + + {(onFocusProps || onBlurProps) && ( + )} - + ); } diff --git a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx index f4cad473c..b52442aca 100644 --- a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx +++ b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx @@ -22,6 +22,8 @@ export default function defaultRenderGlyph({ r={size / 2} cx={x} cy={y} + onBlur={onBlur} + onFocus={onFocus} onPointerMove={onPointerMove} onPointerOut={onPointerOut} onPointerUp={onPointerUp} From 1b6eb96753da7123ee2894692c5e0a32595520a8 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 13:58:53 -0800 Subject: [PATCH 04/12] new(xychart): add useSeriesEvents hook --- .../visx-xychart/src/hooks/useSeriesEvents.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 packages/visx-xychart/src/hooks/useSeriesEvents.ts diff --git a/packages/visx-xychart/src/hooks/useSeriesEvents.ts b/packages/visx-xychart/src/hooks/useSeriesEvents.ts new file mode 100644 index 000000000..701b46690 --- /dev/null +++ b/packages/visx-xychart/src/hooks/useSeriesEvents.ts @@ -0,0 +1,77 @@ +import { AxisScale } from '@visx/axis'; +import { useCallback, useContext } from 'react'; +import TooltipContext from '../context/TooltipContext'; +import { EventHandlerParams, SeriesProps, TooltipContextType } from '../types'; +import usePointerEventEmitters from './usePointerEventEmitters'; +import usePointerEventHandlers, { PointerEventHandlerParams } from './usePointerEventHandlers'; + +export type SeriesEventsParams = Pick< + SeriesProps, + 'enableEvents' | 'onBlur' | 'onFocus' | 'onPointerMove' | 'onPointerOut' | 'onPointerUp' +> & + Pick, 'dataKey' | 'sources'> & { + /** The source of emitted events. */ + source: string; + }; + +/** This hook simplifies the logic for initializing Series event emitters + handlers. */ +export default function useSeriesEvents({ + dataKey, + enableEvents, + onBlur: onBlurProps, + onFocus: onFocusProps, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + source, + sources, +}: SeriesEventsParams) { + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (params: EventHandlerParams) => { + showTooltip(params); + if (onPointerMoveProps) onPointerMoveProps(params); + }, + [showTooltip, onPointerMoveProps], + ); + const onFocus = useCallback( + (params: EventHandlerParams) => { + showTooltip(params); + if (onFocusProps) onFocusProps(params); + }, + [showTooltip, onFocusProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (event && onPointerOutProps) onPointerOutProps(event); + }, + [hideTooltip, onPointerOutProps], + ); + const onBlur = useCallback( + (event: React.FocusEvent) => { + hideTooltip(); + if (event && onBlurProps) onBlurProps(event); + }, + [hideTooltip, onBlurProps], + ); + usePointerEventHandlers({ + dataKey, + onBlur: enableEvents ? onBlur : undefined, + onFocus: enableEvents ? onFocus : undefined, + onPointerMove: enableEvents ? onPointerMove : undefined, + onPointerOut: enableEvents ? onPointerOut : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, + sources, + }); + return usePointerEventEmitters({ + source, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, + }); +} From 3cdab8445f950df70af55311a5ade837303d7ba1 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 13:59:37 -0800 Subject: [PATCH 05/12] internal(xychart): simplify Series to use useSeriesEvents --- .../series/private/BaseAreaSeries.tsx | 83 +++++-------------- .../series/private/BaseBarSeries.tsx | 78 ++++------------- .../series/private/BaseBarStack.tsx | 4 +- .../series/private/BaseGlyphSeries.tsx | 67 ++++----------- .../series/private/BaseLineSeries.tsx | 82 +++++------------- .../src/hooks/usePointerEventHandlers.ts | 2 +- 6 files changed, 79 insertions(+), 237 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx index b5b0b13be..49ceacf1e 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -3,17 +3,15 @@ import { AxisScale } from '@visx/axis'; import Area, { AreaProps } from '@visx/shape/lib/shapes/Area'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import DataContext from '../../../context/DataContext'; -import { EventHandlerParams, GlyphsProps, SeriesProps, TooltipContextType } from '../../../types'; +import { GlyphsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; -import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { AREASERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; -import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; -import TooltipContext from '../../../context/TooltipContext'; import { BaseGlyphSeries } from './BaseGlyphSeries'; import defaultRenderGlyph from './defaultRenderGlyph'; +import useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseAreaSeriesProps< XScale extends AxisScale, @@ -36,11 +34,11 @@ function BaseAreaSeries; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onFocus = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onFocusProps) onFocusProps(p); - }, - [showTooltip, onFocusProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (event && onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); - const onBlur = useCallback( - (event: React.FocusEvent) => { - hideTooltip(); - if (event && onBlurProps) onBlurProps(event); - }, - [hideTooltip, onBlurProps], - ); const ownEventSourceKey = `${AREASERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onBlur: !!onBlurProps && enableEvents, - onFocus: !!onFocusProps && enableEvents, - onPointerMove: !!onPointerMoveProps && enableEvents, - onPointerOut: !!onPointerOutProps && enableEvents, - onPointerUp: !!onPointerUpProps && enableEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onBlur: enableEvents ? onBlur : undefined, - onFocus: enableEvents ? onFocus : undefined, - onPointerMove: enableEvents ? onPointerMove : undefined, - onPointerOut: enableEvents ? onPointerOut : undefined, - onPointerUp: enableEvents ? onPointerUpProps : undefined, + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -128,21 +89,22 @@ function BaseAreaSeries) => - pointerEventEmitters.onFocus || pointerEventEmitters.onBlur + captureFocusEvents ? glyphs.map(glyph => ( {defaultRenderGlyph({ ...glyph, color: 'transparent', - onFocus: pointerEventEmitters.onFocus, - onBlur: pointerEventEmitters.onBlur, + onFocus: eventEmitters.onFocus, + onBlur: eventEmitters.onBlur, })} )) : null, - [pointerEventEmitters.onFocus, pointerEventEmitters.onBlur], + [captureFocusEvents, eventEmitters.onFocus, eventEmitters.onBlur], ); return ( @@ -155,7 +117,7 @@ function BaseAreaSeries )} @@ -180,8 +142,7 @@ function BaseAreaSeries )} - {/** Capture and emit FocusEvents */} - {(onFocusProps || onBlurProps) && ( + {captureFocusEvents && ( bar) as Bar[]; }, [barThickness, color, data, getScaledX, getScaledY, horizontal, xZeroPosition, yZeroPosition]); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onFocus = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onFocusProps) onFocusProps(p); - }, - [showTooltip, onFocusProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (event && onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); - const onBlur = useCallback( - (event: React.FocusEvent) => { - hideTooltip(); - if (event && onBlurProps) onBlurProps(event); - }, - [hideTooltip, onBlurProps], - ); const ownEventSourceKey = `${BARSERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onBlur: !!onBlurProps && enableEvents, - onFocus: !!onFocusProps && enableEvents, - onPointerMove: !!onPointerMoveProps && enableEvents, - onPointerOut: !!onPointerOutProps && enableEvents, - onPointerUp: !!onPointerUpProps && enableEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onBlur: enableEvents ? onBlur : undefined, - onFocus: enableEvents ? onFocus : undefined, - onPointerMove: enableEvents ? onPointerMove : undefined, - onPointerOut: enableEvents ? onPointerOut : undefined, - onPointerUp: enableEvents ? onPointerUpProps : undefined, + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -150,7 +104,7 @@ function BaseBarSeries ); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx index 5fa343fae..999a31be0 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx @@ -40,7 +40,7 @@ export type BaseBarStackProps< } & Pick, 'offset' | 'order'> & Pick< SeriesProps, - 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'enableEvents' + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'onBlur' | 'onFocus' | 'enableEvents' >; function BaseBarStack< @@ -52,6 +52,8 @@ function BaseBarStack< order, offset, BarsComponent, + onBlur: onBlurProps, + onFocus: onFocusProps, onPointerMove: onPointerMoveProps, onPointerOut: onPointerOutProps, onPointerUp: onPointerUpProps, diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index 7b93a35ab..f634e7d68 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -15,6 +15,7 @@ import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { GLYPHSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import TooltipContext from '../../../context/TooltipContext'; +import useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseGlyphSeriesProps< XScale extends AxisScale, @@ -34,11 +35,11 @@ export function BaseGlyphSeries< >({ data, dataKey, - onBlur: onBlurProps, - onFocus: onFocusProps, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, enableEvents = true, renderGlyphs, size = 8, @@ -53,54 +54,16 @@ export function BaseGlyphSeries< // @TODO allow override const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222'; - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onFocus = useCallback( - (p: EventHandlerParams) => { - console.log(p); - showTooltip(p); - if (onFocusProps) onFocusProps(p); - }, - [showTooltip, onFocusProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (event && onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); - const onBlur = useCallback( - (event: React.FocusEvent) => { - hideTooltip(); - if (event && onBlurProps) onBlurProps(event); - }, - [hideTooltip, onBlurProps], - ); const ownEventSourceKey = `${GLYPHSERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onBlur: !!onBlurProps && enableEvents, - onFocus: !!onFocusProps && enableEvents, - onPointerMove: !!onPointerMoveProps && enableEvents, - onPointerOut: !!onPointerOutProps && enableEvents, - onPointerUp: !!onPointerUpProps && enableEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onBlur: enableEvents ? onBlur : undefined, - onFocus: enableEvents ? onFocus : undefined, - onPointerMove: enableEvents ? onPointerMove : undefined, - onPointerOut: enableEvents ? onPointerOut : undefined, - onPointerUp: enableEvents ? onPointerUpProps : undefined, + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -127,7 +90,7 @@ export function BaseGlyphSeries< return ( // eslint-disable-next-line react/jsx-no-useless-fragment - <>{renderGlyphs({ glyphs, xScale, yScale, horizontal, ...pointerEventEmitters })} + <>{renderGlyphs({ glyphs, xScale, yScale, horizontal, ...eventEmitters })} ); } diff --git a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx index 407cb79ec..9d9155562 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -2,16 +2,14 @@ import React, { useContext, useCallback } from 'react'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { EventHandlerParams, GlyphsProps, SeriesProps, TooltipContextType } from '../../../types'; +import { GlyphsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; -import TooltipContext from '../../../context/TooltipContext'; import isValidNumber from '../../../typeguards/isValidNumber'; import { LINESERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; -import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; -import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; import { BaseGlyphSeries } from './BaseGlyphSeries'; import defaultRenderGlyph from './defaultRenderGlyph'; +import useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseLineSeriesProps< XScale extends AxisScale, @@ -28,11 +26,11 @@ function BaseLineSeries; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onFocus = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onFocusProps) onFocusProps(p); - }, - [showTooltip, onFocusProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); - const onBlur = useCallback( - (event: React.FocusEvent) => { - hideTooltip(); - if (event && onBlurProps) onBlurProps(event); - }, - [hideTooltip, onBlurProps], - ); const ownEventSourceKey = `${LINESERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onBlur: !!onBlurProps && enableEvents, - onFocus: !!onFocusProps && enableEvents, - onPointerMove: !!onPointerMoveProps && enableEvents, - onPointerOut: !!onPointerOutProps && enableEvents, - onPointerUp: !!onPointerUpProps && enableEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onBlur: enableEvents ? onBlur : undefined, - onFocus: enableEvents ? onFocus : undefined, - onPointerMove: enableEvents ? onPointerMove : undefined, - onPointerOut: enableEvents ? onPointerOut : undefined, - onPointerUp: enableEvents ? onPointerUpProps : undefined, + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); // render invisible glyphs for focusing if onFocus/onBlur are defined + const captureFocusEvents = Boolean(onFocus || onBlur); const renderGlyphs = useCallback( ({ glyphs }: GlyphsProps) => - pointerEventEmitters.onFocus || pointerEventEmitters.onBlur + captureFocusEvents ? glyphs.map(glyph => ( {defaultRenderGlyph({ ...glyph, color: 'transparent', - onFocus: pointerEventEmitters.onFocus, - onBlur: pointerEventEmitters.onBlur, + onFocus: eventEmitters.onFocus, + onBlur: eventEmitters.onBlur, })} )) : null, - [pointerEventEmitters.onFocus, pointerEventEmitters.onBlur], + [captureFocusEvents, eventEmitters.onFocus, eventEmitters.onBlur], ); return ( @@ -128,11 +90,11 @@ function BaseLineSeries )} - {(onFocusProps || onBlurProps) && ( + {captureFocusEvents && ( = { +export type PointerEventHandlerParams = { /** Controls whether callbacks are invoked for one or more registered dataKeys, the nearest dataKey, or all dataKeys. */ dataKey: string | string[] | typeof POINTER_EVENTS_NEAREST | typeof POINTER_EVENTS_ALL; // last two are eaten by string /** Callback invoked onFocus for one or more series based on dataKey. */ From faff92cb7ebbd78f1eee2b32cc55c1af64ba2845 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 16:30:44 -0800 Subject: [PATCH 06/12] new(xychart): fix BarStack nearest datum logic --- .../series/private/BaseBarStack.tsx | 64 ++++++++----------- .../src/hooks/usePointerEventHandlers.ts | 48 ++++++++++++-- .../visx-xychart/src/hooks/useSeriesEvents.ts | 25 ++++++-- packages/visx-xychart/src/types/event.ts | 1 + .../src/utils/getBarStackRegistryData.ts | 6 +- 5 files changed, 93 insertions(+), 51 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx index 999a31be0..6b735e6cb 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx @@ -15,18 +15,17 @@ import { BarStackDatum, CombinedStackData, DataContextType, - EventHandlerParams, + NearestDatumArgs, + NearestDatumReturnType, SeriesProps, - TooltipContextType, } from '../../../types'; import isValidNumber from '../../../typeguards/isValidNumber'; import isChildWithProps from '../../../typeguards/isChildWithProps'; import combineBarBarStackData, { getStackValue } from '../../../utils/combineBarStackData'; import getBarStackRegistryData from '../../../utils/getBarStackRegistryData'; -import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { BARSTACK_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; -import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; -import TooltipContext from '../../../context/TooltipContext'; +import useSeriesEvents from '../../../hooks/useSeriesEvents'; +import findNearestStackDatum from '../../../utils/findNearestStackDatum'; export type BaseBarStackProps< XScale extends PositionScale, @@ -52,11 +51,11 @@ function BaseBarStack< order, offset, BarsComponent, - onBlur: onBlurProps, - onFocus: onFocusProps, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, enableEvents = true, }: BaseBarStackProps) { type StackBar = SeriesPoint>; @@ -136,35 +135,28 @@ function BaseBarStack< barSeriesChildren, ]); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); + const findNearestDatum = useCallback( + ( + params: NearestDatumArgs>, + ): NearestDatumReturnType => { + const childData = barSeriesChildren.find(child => child.props.dataKey === params.dataKey) + ?.props?.data; + return childData ? findNearestStackDatum(params, childData, horizontal) : null; }, - [hideTooltip, onPointerOutProps], + [barSeriesChildren, horizontal], ); + const ownEventSourceKey = `${BARSTACK_EVENT_SOURCE}-${dataKeys.join('-')}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && enableEvents, - onPointerOut: !!onPointerOutProps && enableEvents, - onPointerUp: !!onPointerUpProps && enableEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey: dataKeys, - onPointerMove: enableEvents ? onPointerMove : undefined, - onPointerOut: enableEvents ? onPointerOut : undefined, - onPointerUp: enableEvents ? onPointerUpProps : undefined, + enableEvents, + findNearestDatum, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -233,7 +225,7 @@ function BaseBarStack< horizontal={horizontal} xScale={xScale} yScale={yScale} - {...pointerEventEmitters} + {...eventEmitters} /> ); diff --git a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts index 2ef403bec..33d502dac 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts +++ b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts @@ -1,7 +1,13 @@ +import { AxisScale } from '@visx/axis'; import { PointerEvent, FocusEvent, useCallback, useContext } from 'react'; import DataContext from '../context/DataContext'; import { isPointerEvent } from '../typeguards/events'; -import { EventHandlerParams } from '../types'; +import { + DataContextType, + EventHandlerParams, + NearestDatumArgs, + NearestDatumReturnType, +} from '../types'; import findNearestDatumX from '../utils/findNearestDatumX'; import findNearestDatumY from '../utils/findNearestDatumY'; import useEventEmitter, { HandlerParams } from './useEventEmitter'; @@ -9,9 +15,17 @@ import useEventEmitter, { HandlerParams } from './useEventEmitter'; export const POINTER_EVENTS_ALL = '__POINTER_EVENTS_ALL'; export const POINTER_EVENTS_NEAREST = '__POINTER_EVENTS_NEAREST'; -export type PointerEventHandlerParams = { +export type PointerEventHandlerParams< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +> = { /** Controls whether callbacks are invoked for one or more registered dataKeys, the nearest dataKey, or all dataKeys. */ dataKey: string | string[] | typeof POINTER_EVENTS_NEAREST | typeof POINTER_EVENTS_ALL; // last two are eaten by string + /** Optionally override the findNearestDatum logic. */ + findNearestDatum?: ( + params: NearestDatumArgs, + ) => NearestDatumReturnType; /** Callback invoked onFocus for one or more series based on dataKey. */ onFocus?: (params: EventHandlerParams) => void; /** Callback invoked onBlur. */ @@ -30,17 +44,26 @@ export type PointerEventHandlerParams = { * Hook that returns PointerEvent handlers that invoke the passed pointer * handlers with the nearest datum to the event for the passed dataKey. */ -export default function usePointerEventHandlers({ +export default function usePointerEventHandlers< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +>({ dataKey, + findNearestDatum: findNearestDatumProps, onBlur, onFocus, onPointerMove, onPointerOut, onPointerUp, sources, -}: PointerEventHandlerParams) { - const { width, height, horizontal, dataRegistry, xScale, yScale } = useContext(DataContext); +}: PointerEventHandlerParams) { + const { width, height, horizontal, dataRegistry, xScale, yScale } = (useContext( + DataContext, + ) as unknown) as DataContextType; + const findNearestDatum = + findNearestDatumProps || (horizontal ? findNearestDatumY : findNearestDatumX); const handlePointerMoveUpOrFocus = useCallback( (params?: HandlerParams) => { const { svgPoint, event } = params || {}; @@ -64,7 +87,8 @@ export default function usePointerEventHandlers({ dataKeys.forEach(key => { const entry = dataRegistry?.get(key); if (entry) { - const nearestDatum = (horizontal ? findNearestDatumY : findNearestDatumX)({ + const nearestDatum = findNearestDatum({ + dataKey: key, data: entry.data, height, point: svgPoint, @@ -111,7 +135,17 @@ export default function usePointerEventHandlers({ }); } }, - [dataKey, dataRegistry, xScale, yScale, width, height, horizontal, onPointerMove, onPointerUp], + [ + dataKey, + dataRegistry, + xScale, + yScale, + width, + height, + findNearestDatum, + onPointerMove, + onPointerUp, + ], ); const handlePointerOut = useCallback( diff --git a/packages/visx-xychart/src/hooks/useSeriesEvents.ts b/packages/visx-xychart/src/hooks/useSeriesEvents.ts index 701b46690..be05053c7 100644 --- a/packages/visx-xychart/src/hooks/useSeriesEvents.ts +++ b/packages/visx-xychart/src/hooks/useSeriesEvents.ts @@ -1,23 +1,35 @@ -import { AxisScale } from '@visx/axis'; import { useCallback, useContext } from 'react'; +import { AxisScale } from '@visx/axis'; import TooltipContext from '../context/TooltipContext'; import { EventHandlerParams, SeriesProps, TooltipContextType } from '../types'; import usePointerEventEmitters from './usePointerEventEmitters'; import usePointerEventHandlers, { PointerEventHandlerParams } from './usePointerEventHandlers'; -export type SeriesEventsParams = Pick< - SeriesProps, +export type SeriesEventsParams< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +> = Pick< + SeriesProps, 'enableEvents' | 'onBlur' | 'onFocus' | 'onPointerMove' | 'onPointerOut' | 'onPointerUp' > & - Pick, 'dataKey' | 'sources'> & { + Pick< + PointerEventHandlerParams, + 'dataKey' | 'sources' | 'findNearestDatum' + > & { /** The source of emitted events. */ source: string; }; /** This hook simplifies the logic for initializing Series event emitters + handlers. */ -export default function useSeriesEvents({ +export default function useSeriesEvents< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +>({ dataKey, enableEvents, + findNearestDatum, onBlur: onBlurProps, onFocus: onFocusProps, onPointerMove: onPointerMoveProps, @@ -25,7 +37,7 @@ export default function useSeriesEvents({ onPointerUp: onPointerUpProps, source, sources, -}: SeriesEventsParams) { +}: SeriesEventsParams) { const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< Datum >; @@ -59,6 +71,7 @@ export default function useSeriesEvents({ ); usePointerEventHandlers({ dataKey, + findNearestDatum, onBlur: enableEvents ? onBlur : undefined, onFocus: enableEvents ? onFocus : undefined, onPointerMove: enableEvents ? onPointerMove : undefined, diff --git a/packages/visx-xychart/src/types/event.ts b/packages/visx-xychart/src/types/event.ts index 8a129634e..bd5679149 100644 --- a/packages/visx-xychart/src/types/event.ts +++ b/packages/visx-xychart/src/types/event.ts @@ -10,6 +10,7 @@ export type NearestDatumArgs< YScale extends AxisScale, Datum extends object > = { + dataKey: string; point: { x: number; y: number } | null; xAccessor: (d: Datum) => ScaleInput; yAccessor: (d: Datum) => ScaleInput; diff --git a/packages/visx-xychart/src/utils/getBarStackRegistryData.ts b/packages/visx-xychart/src/utils/getBarStackRegistryData.ts index b935ad5f7..bec84b8da 100644 --- a/packages/visx-xychart/src/utils/getBarStackRegistryData.ts +++ b/packages/visx-xychart/src/utils/getBarStackRegistryData.ts @@ -1,5 +1,5 @@ import { AxisScale } from '@visx/axis'; -import { getSecondItem } from '@visx/shape/lib/util/accessors'; +import { getFirstItem, getSecondItem } from '@visx/shape/lib/util/accessors'; import { extent } from 'd3-array'; import { BarStackData, BarStackDatum, DataRegistryEntry } from '../types'; @@ -7,9 +7,11 @@ const getStack = ( bar: BarStackDatum, ) => bar?.data?.stack; +// returns average of top + bottom of bar (the middle) as this enables more accurately +// finding the nearest datum to a FocusEvent (which is based on the middle of the rect bounding box) const getNumericValue = ( bar: BarStackDatum, -) => getSecondItem(bar); // corresponds to y1, the upper value (topline). +) => (getFirstItem(bar) + getSecondItem(bar)) / 2; /** Constructs the `DataRegistryEntry`s for a BarStack, using the stacked data. */ export default function getBarStackRegistryData( From 7e81c274dcc4b63dcee1329302325ac8d1f675bf Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 16:31:22 -0800 Subject: [PATCH 07/12] internal(xychart/BaseBarGroup): simplify to use useSeriesEvents --- .../series/private/BaseBarGroup.tsx | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index 463ac3c93..af756f5d1 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -3,22 +3,13 @@ import { PositionScale } from '@visx/shape/lib/types'; import { scaleBand } from '@visx/scale'; import isChildWithProps from '../../../typeguards/isChildWithProps'; import { BaseBarSeriesProps } from './BaseBarSeries'; -import { - Bar, - BarsProps, - DataContextType, - EventHandlerParams, - SeriesProps, - TooltipContextType, -} from '../../../types'; +import { Bar, BarsProps, DataContextType, SeriesProps } from '../../../types'; import DataContext from '../../../context/DataContext'; import getScaleBandwidth from '../../../utils/getScaleBandwidth'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; import { BARGROUP_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; -import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; -import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; -import TooltipContext from '../../../context/TooltipContext'; +import useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseBarGroupProps< XScale extends PositionScale, @@ -35,7 +26,7 @@ export type BaseBarGroupProps< BarsComponent: React.FC>; } & Pick< SeriesProps, - 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'pointerEvents' + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'onBlur' | 'onFocus' | 'enableEvents' >; export default function BaseBarGroup< @@ -47,10 +38,12 @@ export default function BaseBarGroup< padding = 0.1, sortBars, BarsComponent, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, - pointerEvents = true, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + enableEvents = true, }: BaseBarGroupProps) { const { colorScale, @@ -98,35 +91,16 @@ export default function BaseBarGroup< [sortBars, dataKeys, xScale, yScale, horizontal, padding], ); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: EventHandlerParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); const ownEventSourceKey = `${BARGROUP_EVENT_SOURCE}-${dataKeys.join('-')}}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey: dataKeys, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); @@ -195,7 +169,7 @@ export default function BaseBarGroup< horizontal={horizontal} xScale={xScale} yScale={yScale} - {...pointerEventEmitters} + {...eventEmitters} /> ); From da0715b1eb79189942a41e5288f8a2f41c88deef Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 16:43:49 -0800 Subject: [PATCH 08/12] internal(xychart): sources => allowedSources, fix hook generics --- .../visx-xychart/src/components/XYChart.tsx | 4 ++-- .../series/private/BaseAreaSeries.tsx | 4 ++-- .../series/private/BaseBarGroup.tsx | 4 ++-- .../series/private/BaseBarSeries.tsx | 4 ++-- .../series/private/BaseBarStack.tsx | 3 ++- .../series/private/BaseGlyphSeries.tsx | 4 ++-- .../series/private/BaseLineSeries.tsx | 4 ++-- .../visx-xychart/src/hooks/useEventEmitter.ts | 10 ++++----- .../src/hooks/usePointerEventHandlers.ts | 22 +++++++++++++------ .../visx-xychart/src/hooks/useSeriesEvents.ts | 6 ++--- .../hooks/usePointerEventHandlers.test.tsx | 6 ++--- 11 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index 52f60a531..f3f8cf0d6 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -72,7 +72,7 @@ export type XYChartProps< pointerEventsDataKey?: 'all' | 'nearest'; }; -const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE]; +const allowedEventSources = [XYCHART_EVENT_SOURCE]; export default function XYChart< XScaleConfig extends ScaleConfig, @@ -111,7 +111,7 @@ export default function XYChart< onPointerMove, onPointerOut, onPointerUp, - sources: eventSourceSubscriptions, + allowedSources: allowedEventSources, }); // if Context or dimensions are not available, wrap self in the needed providers diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx index 49ceacf1e..d3eafa4cc 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -57,7 +57,7 @@ function BaseAreaSeries({ + const eventEmitters = useSeriesEvents({ dataKey, enableEvents, onBlur, @@ -66,7 +66,7 @@ function BaseAreaSeries getScaleBaseline(horizontal ? xScale : yScale), [ diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index af756f5d1..f7cafb595 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -92,7 +92,7 @@ export default function BaseBarGroup< ); const ownEventSourceKey = `${BARGROUP_EVENT_SOURCE}-${dataKeys.join('-')}}`; - const eventEmitters = useSeriesEvents({ + const eventEmitters = useSeriesEvents({ dataKey: dataKeys, enableEvents, onBlur, @@ -101,7 +101,7 @@ export default function BaseBarGroup< onPointerOut, onPointerUp, source: ownEventSourceKey, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); const xZeroPosition = useMemo(() => (xScale ? getScaleBaseline(xScale) : 0), [xScale]); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index 71a17a3c1..244c378e7 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -85,7 +85,7 @@ function BaseBarSeries({ + const eventEmitters = useSeriesEvents({ dataKey, enableEvents, onBlur, @@ -94,7 +94,7 @@ function BaseBarSeries({ dataKey: dataKeys, enableEvents, + // @ts-ignore Datum input + return type are expected to be the same type but they differ for BarStack (registry data is StackedDatum, return type is user Datum) findNearestDatum, onBlur, onFocus, @@ -157,7 +158,7 @@ function BaseBarStack< onPointerOut, onPointerUp, source: ownEventSourceKey, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); // if scales and data are not available in the registry, bail diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index f634e7d68..afb0cb1fd 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -55,7 +55,7 @@ export function BaseGlyphSeries< const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222'; const ownEventSourceKey = `${GLYPHSERIES_EVENT_SOURCE}-${dataKey}`; - const eventEmitters = useSeriesEvents({ + const eventEmitters = useSeriesEvents({ dataKey, enableEvents, onBlur, @@ -64,7 +64,7 @@ export function BaseGlyphSeries< onPointerOut, onPointerUp, source: ownEventSourceKey, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); const glyphs = useMemo( diff --git a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx index 9d9155562..d2ba86f07 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -49,7 +49,7 @@ function BaseLineSeries({ + const eventEmitters = useSeriesEvents({ dataKey, enableEvents, onBlur, @@ -58,7 +58,7 @@ function BaseLineSeries(); - sourcesRef.current = sources; // use ref so sources[] can change without creating new handlers + const allowedSourcesRef = useRef(); + allowedSourcesRef.current = allowedSources; // use ref so allowedSources[] can change without creating new handlers // wrap emitter.emit so we can enforce stricter type signature const emit = useCallback( @@ -45,8 +45,8 @@ export default function useEventEmitter( // register handler, with source filtering as needed const handlerWithSourceFilter: Handler = (params?: HandlerParams) => { if ( - !sourcesRef.current || - (params?.source && sourcesRef.current?.includes(params.source)) + !allowedSourcesRef.current || + (params?.source && allowedSourcesRef.current?.includes(params.source)) ) { handler(params); } diff --git a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts index 33d502dac..377f0494e 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts +++ b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts @@ -37,7 +37,7 @@ export type PointerEventHandlerParams< /** Callback invoked onPointerUp for one or more series based on dataKey. */ onPointerUp?: (params: EventHandlerParams) => void; /** Valid event sources for which to invoke handlers. */ - sources?: string[]; + allowedSources?: string[]; }; /** @@ -56,7 +56,7 @@ export default function usePointerEventHandlers< onPointerMove, onPointerOut, onPointerUp, - sources, + allowedSources, }: PointerEventHandlerParams) { const { width, height, horizontal, dataRegistry, xScale, yScale } = (useContext( DataContext, @@ -163,9 +163,17 @@ export default function usePointerEventHandlers< [onBlur], ); - useEventEmitter('pointermove', onPointerMove ? handlePointerMoveUpOrFocus : undefined, sources); - useEventEmitter('pointerout', onPointerOut ? handlePointerOut : undefined, sources); - useEventEmitter('pointerup', onPointerUp ? handlePointerMoveUpOrFocus : undefined, sources); - useEventEmitter('focus', onFocus ? handlePointerMoveUpOrFocus : undefined, sources); - useEventEmitter('blur', onBlur ? handleBlur : undefined, sources); + useEventEmitter( + 'pointermove', + onPointerMove ? handlePointerMoveUpOrFocus : undefined, + allowedSources, + ); + useEventEmitter('pointerout', onPointerOut ? handlePointerOut : undefined, allowedSources); + useEventEmitter( + 'pointerup', + onPointerUp ? handlePointerMoveUpOrFocus : undefined, + allowedSources, + ); + useEventEmitter('focus', onFocus ? handlePointerMoveUpOrFocus : undefined, allowedSources); + useEventEmitter('blur', onBlur ? handleBlur : undefined, allowedSources); } diff --git a/packages/visx-xychart/src/hooks/useSeriesEvents.ts b/packages/visx-xychart/src/hooks/useSeriesEvents.ts index be05053c7..cfb1ddce0 100644 --- a/packages/visx-xychart/src/hooks/useSeriesEvents.ts +++ b/packages/visx-xychart/src/hooks/useSeriesEvents.ts @@ -15,7 +15,7 @@ export type SeriesEventsParams< > & Pick< PointerEventHandlerParams, - 'dataKey' | 'sources' | 'findNearestDatum' + 'dataKey' | 'allowedSources' | 'findNearestDatum' > & { /** The source of emitted events. */ source: string; @@ -36,7 +36,7 @@ export default function useSeriesEvents< onPointerOut: onPointerOutProps, onPointerUp: onPointerUpProps, source, - sources, + allowedSources, }: SeriesEventsParams) { const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< Datum @@ -77,7 +77,7 @@ export default function useSeriesEvents< onPointerMove: enableEvents ? onPointerMove : undefined, onPointerOut: enableEvents ? onPointerOut : undefined, onPointerUp: enableEvents ? onPointerUpProps : undefined, - sources, + allowedSources, }); return usePointerEventEmitters({ source, diff --git a/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx b/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx index b33b4a529..f08b2856c 100644 --- a/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx +++ b/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx @@ -35,7 +35,7 @@ describe('usePointerEventHandlers', () => { const emit = useEventEmitter(); usePointerEventHandlers({ - sources: [sourceId], + allowedSources: [sourceId], dataKey: series1.key, onPointerMove: pointerMoveListener, onPointerOut: pointerOutListener, @@ -74,12 +74,12 @@ describe('usePointerEventHandlers', () => { const emit = useEventEmitter(); usePointerEventHandlers({ - sources: [sourceId], + allowedSources: [sourceId], dataKey: POINTER_EVENTS_ALL, onPointerMove: pointerMoveListenerAll, }); usePointerEventHandlers({ - sources: [sourceId], + allowedSources: [sourceId], dataKey: [series1.key, series2.key], onPointerMove: pointerMoveListenerMultipleKeys, }); From 09cf4d00faaf6006b96b605f18f26d7206f38109 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 16:48:18 -0800 Subject: [PATCH 09/12] internal(xychart): usePointerEventEmitters/Handlers => useEventEmitters/Handlers --- packages/visx-xychart/src/components/XYChart.tsx | 12 ++++++------ .../components/series/private/BaseGlyphSeries.tsx | 11 +---------- ...PointerEventEmitters.ts => useEventEmitters.ts} | 0 ...PointerEventHandlers.ts => useEventHandlers.ts} | 0 packages/visx-xychart/src/hooks/useSeriesEvents.ts | 8 ++++---- ...Emitters.test.tsx => useEventEmitters.test.tsx} | 10 +++++----- ...Handlers.test.tsx => useEventHandlers.test.tsx} | 14 ++++++-------- 7 files changed, 22 insertions(+), 33 deletions(-) rename packages/visx-xychart/src/hooks/{usePointerEventEmitters.ts => useEventEmitters.ts} (100%) rename packages/visx-xychart/src/hooks/{usePointerEventHandlers.ts => useEventHandlers.ts} (100%) rename packages/visx-xychart/test/hooks/{usePointerEventEmitters.test.tsx => useEventEmitters.test.tsx} (78%) rename packages/visx-xychart/test/hooks/{usePointerEventHandlers.test.tsx => useEventHandlers.test.tsx} (91%) diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index f3f8cf0d6..10a2ec386 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -11,12 +11,12 @@ import EventEmitterProvider from '../providers/EventEmitterProvider'; import TooltipContext from '../context/TooltipContext'; import TooltipProvider from '../providers/TooltipProvider'; import DataProvider, { DataProviderProps } from '../providers/DataProvider'; -import usePointerEventEmitters from '../hooks/usePointerEventEmitters'; +import useEventEmitters from '../hooks/useEventEmitters'; import { XYCHART_EVENT_SOURCE } from '../constants'; -import usePointerEventHandlers, { +import useEventHandlers, { POINTER_EVENTS_ALL, POINTER_EVENTS_NEAREST, -} from '../hooks/usePointerEventHandlers'; +} from '../hooks/useEventHandlers'; const DEFAULT_MARGIN = { top: 50, right: 50, bottom: 50, left: 50 }; @@ -105,8 +105,8 @@ export default function XYChart< } }, [setDimensions, width, height, margin]); - const pointerEventEmitters = usePointerEventEmitters({ source: XYCHART_EVENT_SOURCE }); - usePointerEventHandlers({ + const eventEmitters = useEventEmitters({ source: XYCHART_EVENT_SOURCE }); + useEventHandlers({ dataKey: pointerEventsDataKey === 'nearest' ? POINTER_EVENTS_NEAREST : POINTER_EVENTS_ALL, onPointerMove, onPointerOut, @@ -171,7 +171,7 @@ export default function XYChart< width={width - margin.left - margin.right} height={height - margin.top - margin.bottom} fill="transparent" - {...pointerEventEmitters} + {...eventEmitters} /> )} diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index afb0cb1fd..707563066 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -1,20 +1,11 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { - GlyphProps, - GlyphsProps, - EventHandlerParams, - SeriesProps, - TooltipContextType, -} from '../../../types'; +import { GlyphProps, GlyphsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import isValidNumber from '../../../typeguards/isValidNumber'; -import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; import { GLYPHSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; -import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; -import TooltipContext from '../../../context/TooltipContext'; import useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseGlyphSeriesProps< diff --git a/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts b/packages/visx-xychart/src/hooks/useEventEmitters.ts similarity index 100% rename from packages/visx-xychart/src/hooks/usePointerEventEmitters.ts rename to packages/visx-xychart/src/hooks/useEventEmitters.ts diff --git a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts b/packages/visx-xychart/src/hooks/useEventHandlers.ts similarity index 100% rename from packages/visx-xychart/src/hooks/usePointerEventHandlers.ts rename to packages/visx-xychart/src/hooks/useEventHandlers.ts diff --git a/packages/visx-xychart/src/hooks/useSeriesEvents.ts b/packages/visx-xychart/src/hooks/useSeriesEvents.ts index cfb1ddce0..e2363cf6e 100644 --- a/packages/visx-xychart/src/hooks/useSeriesEvents.ts +++ b/packages/visx-xychart/src/hooks/useSeriesEvents.ts @@ -2,8 +2,8 @@ import { useCallback, useContext } from 'react'; import { AxisScale } from '@visx/axis'; import TooltipContext from '../context/TooltipContext'; import { EventHandlerParams, SeriesProps, TooltipContextType } from '../types'; -import usePointerEventEmitters from './usePointerEventEmitters'; -import usePointerEventHandlers, { PointerEventHandlerParams } from './usePointerEventHandlers'; +import useEventEmitters from './useEventEmitters'; +import useEventHandlers, { PointerEventHandlerParams } from './useEventHandlers'; export type SeriesEventsParams< XScale extends AxisScale, @@ -69,7 +69,7 @@ export default function useSeriesEvents< }, [hideTooltip, onBlurProps], ); - usePointerEventHandlers({ + useEventHandlers({ dataKey, findNearestDatum, onBlur: enableEvents ? onBlur : undefined, @@ -79,7 +79,7 @@ export default function useSeriesEvents< onPointerUp: enableEvents ? onPointerUpProps : undefined, allowedSources, }); - return usePointerEventEmitters({ + return useEventEmitters({ source, onBlur: !!onBlurProps && enableEvents, onFocus: !!onFocusProps && enableEvents, diff --git a/packages/visx-xychart/test/hooks/usePointerEventEmitters.test.tsx b/packages/visx-xychart/test/hooks/useEventEmitters.test.tsx similarity index 78% rename from packages/visx-xychart/test/hooks/usePointerEventEmitters.test.tsx rename to packages/visx-xychart/test/hooks/useEventEmitters.test.tsx index c5c162489..e8f142d06 100644 --- a/packages/visx-xychart/test/hooks/usePointerEventEmitters.test.tsx +++ b/packages/visx-xychart/test/hooks/useEventEmitters.test.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from 'react'; import { mount } from 'enzyme'; import { EventEmitterProvider, useEventEmitter } from '../../src'; -import usePointerEventEmitters from '../../src/hooks/usePointerEventEmitters'; +import useEventEmitters from '../../src/hooks/useEventEmitters'; -describe('usePointerEventEmitters', () => { +describe('useEventEmitters', () => { it('should be defined', () => { - expect(usePointerEventEmitters).toBeDefined(); + expect(useEventEmitters).toBeDefined(); }); it('should provide an emitter for each callback specified', () => { expect.assertions(1); const Component = () => { - const emitters = usePointerEventEmitters({ source: 'visx', onPointerOut: false }); + const emitters = useEventEmitters({ source: 'visx', onPointerOut: false }); expect(emitters).toEqual({ onPointerMove: expect.any(Function), onPointerOut: undefined, @@ -34,7 +34,7 @@ describe('usePointerEventEmitters', () => { const source = 'sourceId'; const listener = jest.fn(); useEventEmitter('pointerup', listener, [source]); - const emitters = usePointerEventEmitters({ source }); + const emitters = useEventEmitters({ source }); useEffect(() => { if (emitters.onPointerUp) { diff --git a/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx b/packages/visx-xychart/test/hooks/useEventHandlers.test.tsx similarity index 91% rename from packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx rename to packages/visx-xychart/test/hooks/useEventHandlers.test.tsx index f08b2856c..be1adc634 100644 --- a/packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx +++ b/packages/visx-xychart/test/hooks/useEventHandlers.test.tsx @@ -1,9 +1,7 @@ import React, { useEffect } from 'react'; import { mount } from 'enzyme'; import { EventEmitterProvider, useEventEmitter, DataContext } from '../../src'; -import usePointerEventHandlers, { - POINTER_EVENTS_ALL, -} from '../../src/hooks/usePointerEventHandlers'; +import useEventHandlers, { POINTER_EVENTS_ALL } from '../../src/hooks/useEventHandlers'; import getDataContext from '../mocks/getDataContext'; const series1 = { key: 'series1', data: [{}], xAccessor: () => 4, yAccessor: () => 7 }; @@ -12,7 +10,7 @@ const series2 = { key: 'series2', data: [{}], xAccessor: () => 4, yAccessor: () const getEvent = (eventType: string) => (new MouseEvent(eventType) as unknown) as React.PointerEvent; -describe('usePointerEventHandlers', () => { +describe('useEventHandlers', () => { function setup(children: React.ReactNode) { return mount( @@ -22,7 +20,7 @@ describe('usePointerEventHandlers', () => { } it('should be defined', () => { - expect(usePointerEventHandlers).toBeDefined(); + expect(useEventHandlers).toBeDefined(); }); it('should invoke handlers for each pointer event handler specified', () => { expect.assertions(3); @@ -34,7 +32,7 @@ describe('usePointerEventHandlers', () => { const pointerUpListener = jest.fn(); const emit = useEventEmitter(); - usePointerEventHandlers({ + useEventHandlers({ allowedSources: [sourceId], dataKey: series1.key, onPointerMove: pointerMoveListener, @@ -73,12 +71,12 @@ describe('usePointerEventHandlers', () => { const pointerMoveListenerMultipleKeys = jest.fn(); const emit = useEventEmitter(); - usePointerEventHandlers({ + useEventHandlers({ allowedSources: [sourceId], dataKey: POINTER_EVENTS_ALL, onPointerMove: pointerMoveListenerAll, }); - usePointerEventHandlers({ + useEventHandlers({ allowedSources: [sourceId], dataKey: [series1.key, series2.key], onPointerMove: pointerMoveListenerMultipleKeys, From 65ee9071d09a7da03fc6e9e350d760c459c936c3 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 17:25:41 -0800 Subject: [PATCH 10/12] new(xychart) add findNearestGroupDatum to improve UX --- .../series/private/BaseBarGroup.tsx | 19 +++++++- .../src/utils/findNearestGroupDatum.ts | 48 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 packages/visx-xychart/src/utils/findNearestGroupDatum.ts diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index f7cafb595..7f5c781ef 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -3,13 +3,21 @@ import { PositionScale } from '@visx/shape/lib/types'; import { scaleBand } from '@visx/scale'; import isChildWithProps from '../../../typeguards/isChildWithProps'; import { BaseBarSeriesProps } from './BaseBarSeries'; -import { Bar, BarsProps, DataContextType, SeriesProps } from '../../../types'; +import { + Bar, + BarsProps, + DataContextType, + NearestDatumArgs, + NearestDatumReturnType, + SeriesProps, +} from '../../../types'; import DataContext from '../../../context/DataContext'; import getScaleBandwidth from '../../../utils/getScaleBandwidth'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; import { BARGROUP_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; import useSeriesEvents from '../../../hooks/useSeriesEvents'; +import findNearestGroupDatum from '../../../utils/findNearestGroupDatum'; export type BaseBarGroupProps< XScale extends PositionScale, @@ -35,7 +43,7 @@ export default function BaseBarGroup< Datum extends object >({ children, - padding = 0.1, + padding = 0.5, sortBars, BarsComponent, onBlur, @@ -91,10 +99,17 @@ export default function BaseBarGroup< [sortBars, dataKeys, xScale, yScale, horizontal, padding], ); + const findNearestDatum = useCallback( + (params: NearestDatumArgs): NearestDatumReturnType => + findNearestGroupDatum(params, groupScale, horizontal), + [groupScale, horizontal], + ); + const ownEventSourceKey = `${BARGROUP_EVENT_SOURCE}-${dataKeys.join('-')}}`; const eventEmitters = useSeriesEvents({ dataKey: dataKeys, enableEvents, + findNearestDatum, onBlur, onFocus, onPointerMove, diff --git a/packages/visx-xychart/src/utils/findNearestGroupDatum.ts b/packages/visx-xychart/src/utils/findNearestGroupDatum.ts new file mode 100644 index 000000000..6f7bdd318 --- /dev/null +++ b/packages/visx-xychart/src/utils/findNearestGroupDatum.ts @@ -0,0 +1,48 @@ +import { PositionScale } from '@visx/shape/lib/types'; +import { ScaleTypeToD3Scale } from '@visx/scale'; +import { NearestDatumArgs } from '../types'; +import findNearestDatumX from './findNearestDatumX'; +import findNearestDatumY from './findNearestDatumY'; + +/** + * This is a wrapper around findNearestDatumX/Y for BarGroup, accounting for a + * Bar's group scale offset (which findNearestDatum does not). + */ +export default function findNearestGroupDatum< + XScale extends PositionScale, + YScale extends PositionScale, + Datum extends object +>( + nearestDatumArgs: NearestDatumArgs, + groupScale: ScaleTypeToD3Scale['band'], + horizontal?: boolean, +) { + const { dataKey, xAccessor, yAccessor, xScale, yScale, point } = nearestDatumArgs; + const datum = (horizontal ? findNearestDatumY : findNearestDatumX)(nearestDatumArgs); + + if (!datum || !point) return null; + + const barGroupOffset = groupScale(dataKey); + const barWidth = groupScale.step(); // @TODO this doesn't currently account for initial paddingOuter + + if (horizontal) { + const groupPosition = yScale(yAccessor(datum.datum)); + const barStart = (groupPosition ?? Infinity) + (barGroupOffset ?? Infinity); + const barEnd = barStart + barWidth; + const cursorIsOnBar = point.y >= barStart && point.y <= barEnd; + return { + ...datum, + distanceX: 0, + distanceY: cursorIsOnBar ? 0 : datum.distanceY, + }; + } + const groupPosition = xScale(xAccessor(datum.datum)); + const barStart = (groupPosition ?? Infinity) + (barGroupOffset ?? Infinity); + const barEnd = barStart + barWidth; + const cursorIsOnBar = point.x >= barStart && point.x <= barEnd; + return { + ...datum, + distanceY: 0, + distanceX: cursorIsOnBar ? 0 : datum.distanceX, + }; +} From ca5bc8b3e85529bd5e15c6035df3138ccfa31cf0 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 4 Dec 2020 17:47:34 -0800 Subject: [PATCH 11/12] fix(xychart/BaseBarGroup): set paddingInner --- .../visx-xychart/src/components/series/private/BaseBarGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index 7f5c781ef..1f959920e 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -43,7 +43,7 @@ export default function BaseBarGroup< Datum extends object >({ children, - padding = 0.5, + padding = 0.1, sortBars, BarsComponent, onBlur, From fcaa55c5dbd951a5b330bf2e3bbc2f46183df460 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Mon, 7 Dec 2020 11:20:40 -0800 Subject: [PATCH 12/12] test(xychart/findNearestDatum): add dataKey to mock props --- packages/visx-xychart/test/utils/findNearestDatum.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/visx-xychart/test/utils/findNearestDatum.test.ts b/packages/visx-xychart/test/utils/findNearestDatum.test.ts index c70ed30c8..9da763251 100644 --- a/packages/visx-xychart/test/utils/findNearestDatum.test.ts +++ b/packages/visx-xychart/test/utils/findNearestDatum.test.ts @@ -10,6 +10,7 @@ import { BarStackDatum, NearestDatumArgs } from '../../src'; type Datum = { xVal: number; yVal: string }; const params: NearestDatumArgs = { + dataKey: 'visx', width: 10, height: 10, point: { x: 3, y: 8 },