diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index 4be182f43..10a2ec386 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -5,18 +5,18 @@ 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'; 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 }; @@ -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,12 +67,12 @@ 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'; }; -const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE]; +const allowedEventSources = [XYCHART_EVENT_SOURCE]; export default function XYChart< XScaleConfig extends ScaleConfig, @@ -105,13 +105,13 @@ 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, onPointerUp, - sources: eventSourceSubscriptions, + allowedSources: allowedEventSources, }); // if Context or dimensions are not available, wrap self in the needed providers @@ -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/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/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/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/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..d3eafa4cc 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -3,15 +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 { PointerEventParams, 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, @@ -26,10 +26,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 +34,12 @@ function BaseAreaSeries; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); const ownEventSourceKey = `${AREASERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); const numericScaleBaseline = useMemo(() => getScaleBaseline(horizontal ? xScale : yScale), [ @@ -108,6 +88,25 @@ function BaseAreaSeries) => + captureFocusEvents + ? glyphs.map(glyph => ( + + {defaultRenderGlyph({ + ...glyph, + color: 'transparent', + onFocus: eventEmitters.onFocus, + onBlur: eventEmitters.onBlur, + })} + + )) + : null, + [captureFocusEvents, eventEmitters.onFocus, eventEmitters.onBlur], + ); + return ( <> @@ -118,7 +117,7 @@ function BaseAreaSeries )} @@ -143,6 +142,17 @@ function BaseAreaSeries )} + {captureFocusEvents && ( + + )} ); } diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index eb85dc59b..1f959920e 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -7,18 +7,17 @@ import { Bar, BarsProps, DataContextType, - PointerEventParams, + NearestDatumArgs, + NearestDatumReturnType, SeriesProps, - TooltipContextType, } 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'; +import findNearestGroupDatum from '../../../utils/findNearestGroupDatum'; export type BaseBarGroupProps< XScale extends PositionScale, @@ -35,7 +34,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 +46,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,36 +99,24 @@ export default function BaseBarGroup< [sortBars, dataKeys, xScale, yScale, horizontal, padding], ); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], + const findNearestDatum = useCallback( + (params: NearestDatumArgs): NearestDatumReturnType => + findNearestGroupDatum(params, groupScale, horizontal), + [groupScale, horizontal], ); + 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, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + enableEvents, + findNearestDatum, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); const xZeroPosition = useMemo(() => (xScale ? getScaleBaseline(xScale) : 0), [xScale]); @@ -195,7 +184,7 @@ export default function BaseBarGroup< horizontal={horizontal} xScale={xScale} yScale={yScale} - {...pointerEventEmitters} + {...eventEmitters} /> ); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index 9670482dc..244c378e7 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -1,22 +1,14 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { - Bar, - BarsProps, - PointerEventParams, - SeriesProps, - TooltipContextType, -} from '../../../types'; +import { Bar, BarsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import getScaleBandwidth from '../../../utils/getScaleBandwidth'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; 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 useSeriesEvents from '../../../hooks/useSeriesEvents'; export type BaseBarSeriesProps< XScale extends AxisScale, @@ -42,10 +34,12 @@ function BaseBarSeries bar) as Bar[]; }, [barThickness, color, data, getScaledX, getScaledY, horizontal, xZeroPosition, yZeroPosition]); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); const ownEventSourceKey = `${BARSERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); return ( @@ -129,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 5b415431c..f4f41cafc 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, - PointerEventParams, + 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, @@ -40,7 +39,7 @@ export type BaseBarStackProps< } & Pick, 'offset' | 'order'> & Pick< SeriesProps, - 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'pointerEvents' + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'onBlur' | 'onFocus' | 'enableEvents' >; function BaseBarStack< @@ -52,10 +51,12 @@ function BaseBarStack< order, offset, BarsComponent, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, - pointerEvents = true, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + enableEvents = true, }: BaseBarStackProps) { type StackBar = SeriesPoint>; const { @@ -134,36 +135,30 @@ function BaseBarStack< barSeriesChildren, ]); - const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< - Datum - >; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - 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 && 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, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + 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, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); // if scales and data are not available in the registry, bail @@ -231,7 +226,7 @@ function BaseBarStack< horizontal={horizontal} xScale={xScale} yScale={yScale} - {...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 735e63a73..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,12 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { - GlyphProps, - GlyphsProps, - PointerEventParams, - 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< XScale extends AxisScale, @@ -27,13 +19,19 @@ export type BaseGlyphSeriesProps< renderGlyphs: (glyphsProps: GlyphsProps) => React.ReactNode; }; -function BaseGlyphSeries({ +export function BaseGlyphSeries< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +>({ data, dataKey, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, - pointerEvents = true, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + enableEvents = true, renderGlyphs, size = 8, xAccessor, @@ -47,36 +45,17 @@ function BaseGlyphSeries; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); const ownEventSourceKey = `${GLYPHSERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); const glyphs = useMemo( @@ -102,7 +81,7 @@ function BaseGlyphSeries{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 6cfc5334e..d2ba86f07 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -2,14 +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 { PointerEventParams, 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, @@ -20,19 +20,18 @@ export type BaseLineSeriesProps< PathComponent?: React.FC, '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, - onPointerMove: onPointerMoveProps, - onPointerOut: onPointerOutProps, - onPointerUp: onPointerUpProps, - pointerEvents = true, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + enableEvents = true, xAccessor, xScale, yAccessor, @@ -49,51 +48,64 @@ function BaseLineSeries; - const onPointerMove = useCallback( - (p: PointerEventParams) => { - showTooltip(p); - if (onPointerMoveProps) onPointerMoveProps(p); - }, - [showTooltip, onPointerMoveProps], - ); - const onPointerOut = useCallback( - (event: React.PointerEvent) => { - hideTooltip(); - if (onPointerOutProps) onPointerOutProps(event); - }, - [hideTooltip, onPointerOutProps], - ); const ownEventSourceKey = `${LINESERIES_EVENT_SOURCE}-${dataKey}`; - const pointerEventEmitters = usePointerEventEmitters({ - source: ownEventSourceKey, - onPointerMove: !!onPointerMoveProps && pointerEvents, - onPointerOut: !!onPointerOutProps && pointerEvents, - onPointerUp: !!onPointerUpProps && pointerEvents, - }); - usePointerEventHandlers({ + const eventEmitters = useSeriesEvents({ dataKey, - onPointerMove: pointerEvents ? onPointerMove : undefined, - onPointerOut: pointerEvents ? onPointerOut : undefined, - onPointerUp: pointerEvents ? onPointerUpProps : undefined, - sources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], + enableEvents, + onBlur, + onFocus, + onPointerMove, + onPointerOut, + onPointerUp, + source: ownEventSourceKey, + allowedSources: [XYCHART_EVENT_SOURCE, ownEventSourceKey], }); + // render invisible glyphs for focusing if onFocus/onBlur are defined + const captureFocusEvents = Boolean(onFocus || onBlur); + const renderGlyphs = useCallback( + ({ glyphs }: GlyphsProps) => + captureFocusEvents + ? glyphs.map(glyph => ( + + {defaultRenderGlyph({ + ...glyph, + color: 'transparent', + onFocus: eventEmitters.onFocus, + onBlur: eventEmitters.onBlur, + })} + + )) + : null, + [captureFocusEvents, eventEmitters.onFocus, eventEmitters.onBlur], + ); + return ( - - {({ path }) => ( - + + {({ path }) => ( + + )} + + {captureFocusEvents && ( + )} - + ); } diff --git a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx index fced8dbb5..b52442aca 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,10 +17,13 @@ export default function defaultRenderGlyph({ ; /** The source of the event. This can be anything, but for this package is the name of the component which emitted the event. */ @@ -24,11 +24,11 @@ export default function useEventEmitter( /** Handler invoked on emission of EventType event. */ handler?: Handler, /** Optional valid sources for EventType subscription. */ - sources?: string[], + allowedSources?: string[], ) { const emitter = useContext(EventEmitterContext); - const sourcesRef = useRef(); - 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/usePointerEventEmitters.ts b/packages/visx-xychart/src/hooks/useEventEmitters.ts similarity index 74% rename from packages/visx-xychart/src/hooks/usePointerEventEmitters.ts rename to packages/visx-xychart/src/hooks/useEventEmitters.ts index 8ada2b85e..23c5b6a61 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts +++ b/packages/visx-xychart/src/hooks/useEventEmitters.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/useEventHandlers.ts similarity index 52% rename from packages/visx-xychart/src/hooks/usePointerEventHandlers.ts rename to packages/visx-xychart/src/hooks/useEventHandlers.ts index 2892b4554..377f0494e 100644 --- a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts +++ b/packages/visx-xychart/src/hooks/useEventHandlers.ts @@ -1,6 +1,13 @@ -import { PointerEvent, useCallback, useContext } from 'react'; +import { AxisScale } from '@visx/axis'; +import { PointerEvent, FocusEvent, useCallback, useContext } from 'react'; import DataContext from '../context/DataContext'; -import { PointerEventParams } from '../types'; +import { isPointerEvent } from '../typeguards/events'; +import { + DataContextType, + EventHandlerParams, + NearestDatumArgs, + NearestDatumReturnType, +} from '../types'; import findNearestDatumX from '../utils/findNearestDatumX'; import findNearestDatumY from '../utils/findNearestDatumY'; import useEventEmitter, { HandlerParams } from './useEventEmitter'; @@ -8,39 +15,62 @@ import useEventEmitter, { HandlerParams } from './useEventEmitter'; export const POINTER_EVENTS_ALL = '__POINTER_EVENTS_ALL'; export const POINTER_EVENTS_NEAREST = '__POINTER_EVENTS_NEAREST'; -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. */ + 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[]; + allowedSources?: string[]; }; /** * 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); + allowedSources, +}: PointerEventHandlerParams) { + const { width, height, horizontal, dataRegistry, xScale, yScale } = (useContext( + DataContext, + ) as unknown) as DataContextType; - const handlePointerMoveOrUp = useCallback( + const findNearestDatum = + findNearestDatumProps || (horizontal ? findNearestDatumY : findNearestDatumX); + 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) { @@ -57,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, @@ -67,6 +98,7 @@ export default function usePointerEventHandlers({ yAccessor: entry.yAccessor, yScale, }); + if (nearestDatum) { pointerParamsByKey[key] = { key, svgPoint, event, ...nearestDatum }; @@ -86,33 +118,62 @@ 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); } }); } }, - [dataKey, dataRegistry, xScale, yScale, width, height, horizontal, onPointerMove, onPointerUp], + [ + dataKey, + dataRegistry, + xScale, + yScale, + width, + height, + findNearestDatum, + onPointerMove, + onPointerUp, + ], ); 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('pointerout', onPointerOut ? handlePointerOut : undefined, sources); - useEventEmitter('pointerup', onPointerUp ? handlePointerMoveOrUp : 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 new file mode 100644 index 000000000..e2363cf6e --- /dev/null +++ b/packages/visx-xychart/src/hooks/useSeriesEvents.ts @@ -0,0 +1,90 @@ +import { useCallback, useContext } from 'react'; +import { AxisScale } from '@visx/axis'; +import TooltipContext from '../context/TooltipContext'; +import { EventHandlerParams, SeriesProps, TooltipContextType } from '../types'; +import useEventEmitters from './useEventEmitters'; +import useEventHandlers, { PointerEventHandlerParams } from './useEventHandlers'; + +export type SeriesEventsParams< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +> = Pick< + SeriesProps, + 'enableEvents' | 'onBlur' | 'onFocus' | 'onPointerMove' | 'onPointerOut' | 'onPointerUp' +> & + Pick< + PointerEventHandlerParams, + 'dataKey' | 'allowedSources' | 'findNearestDatum' + > & { + /** The source of emitted events. */ + source: string; + }; + +/** This hook simplifies the logic for initializing Series event emitters + handlers. */ +export default function useSeriesEvents< + XScale extends AxisScale, + YScale extends AxisScale, + Datum extends object +>({ + dataKey, + enableEvents, + findNearestDatum, + onBlur: onBlurProps, + onFocus: onFocusProps, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + source, + allowedSources, +}: 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], + ); + useEventHandlers({ + dataKey, + findNearestDatum, + onBlur: enableEvents ? onBlur : undefined, + onFocus: enableEvents ? onFocus : undefined, + onPointerMove: enableEvents ? onPointerMove : undefined, + onPointerOut: enableEvents ? onPointerOut : undefined, + onPointerUp: enableEvents ? onPointerUpProps : undefined, + allowedSources, + }); + return useEventEmitters({ + source, + onBlur: !!onBlurProps && enableEvents, + onFocus: !!onFocusProps && enableEvents, + onPointerMove: !!onPointerMoveProps && enableEvents, + onPointerOut: !!onPointerOutProps && enableEvents, + onPointerUp: !!onPointerUpProps && enableEvents, + }); +} diff --git a/packages/visx-xychart/src/providers/TooltipProvider.tsx b/packages/visx-xychart/src/providers/TooltipProvider.tsx index 32f52b850..b35d189ff 100644 --- a/packages/visx-xychart/src/providers/TooltipProvider.tsx +++ b/packages/visx-xychart/src/providers/TooltipProvider.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef } from 'react'; import debounce from 'lodash/debounce'; import { useTooltip } from '@visx/tooltip'; import TooltipContext from '../context/TooltipContext'; -import { PointerEventParams, TooltipData } from '../types'; +import { EventHandlerParams, TooltipData } from '../types'; type TooltipProviderProps = { /** Debounce time for when `hideTooltip` is invoked. */ @@ -27,7 +27,7 @@ export default function TooltipProvider({ 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/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/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; }; 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, + }; +} 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( 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 89% rename from packages/visx-xychart/test/hooks/usePointerEventHandlers.test.tsx rename to packages/visx-xychart/test/hooks/useEventHandlers.test.tsx index b33b4a529..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,8 +32,8 @@ describe('usePointerEventHandlers', () => { const pointerUpListener = jest.fn(); const emit = useEventEmitter(); - usePointerEventHandlers({ - sources: [sourceId], + useEventHandlers({ + allowedSources: [sourceId], dataKey: series1.key, onPointerMove: pointerMoveListener, onPointerOut: pointerOutListener, @@ -73,13 +71,13 @@ describe('usePointerEventHandlers', () => { const pointerMoveListenerMultipleKeys = jest.fn(); const emit = useEventEmitter(); - usePointerEventHandlers({ - sources: [sourceId], + useEventHandlers({ + allowedSources: [sourceId], dataKey: POINTER_EVENTS_ALL, onPointerMove: pointerMoveListenerAll, }); - usePointerEventHandlers({ - sources: [sourceId], + useEventHandlers({ + allowedSources: [sourceId], dataKey: [series1.key, series2.key], onPointerMove: pointerMoveListenerMultipleKeys, }); 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 },