Skip to content

Commit

Permalink
new(xychart): add support for FocusEvents (#959)
Browse files Browse the repository at this point in the history
* new(xychart/useEvent*): add focus/blur events

* new(xychart): add focus/blur handlers to Series props, add handlers to bar+glyph series

* new(xychart): add focus/blur handling to Line and Area series

* new(xychart): add useSeriesEvents hook

* internal(xychart): simplify Series to use useSeriesEvents

* new(xychart): fix BarStack nearest datum logic

* internal(xychart/BaseBarGroup): simplify to use useSeriesEvents

* internal(xychart): sources => allowedSources, fix hook generics

* internal(xychart): usePointerEventEmitters/Handlers => useEventEmitters/Handlers

* new(xychart) add findNearestGroupDatum to improve UX

* fix(xychart/BaseBarGroup): set paddingInner

* test(xychart/findNearestDatum): add dataKey to mock props
  • Loading branch information
williaster authored Dec 8, 2020
1 parent c38aee7 commit 8219a41
Show file tree
Hide file tree
Showing 35 changed files with 577 additions and 342 deletions.
22 changes: 11 additions & 11 deletions packages/visx-xychart/src/components/XYChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -52,7 +52,7 @@ export type XYChartProps<
index,
key,
svgPoint,
}: PointerEventParams<Datum>) => void;
}: EventHandlerParams<Datum>) => void;
/** Callback invoked for onPointerOut events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */
onPointerOut?: (
/** The PointerEvent. */
Expand All @@ -67,12 +67,12 @@ export type XYChartProps<
index,
key,
svgPoint,
}: PointerEventParams<Datum>) => void;
}: EventHandlerParams<Datum>) => 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<AxisScaleOutput, any, any>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -171,7 +171,7 @@ export default function XYChart<
width={width - margin.left - margin.right}
height={height - margin.top - margin.bottom}
fill="transparent"
{...pointerEventEmitters}
{...eventEmitters}
/>
)}
</svg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function AnimatedAreaSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseAreaSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
>(props: Omit<BaseAreaSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseAreaSeries<XScale, YScale, Datum> {...props} PathComponent={AnimatedPath} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function AnimatedBarGroup<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
>(props: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarGroup<XScale, YScale, Datum> {...props} BarsComponent={AnimatedBars} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function AnimatedBarStack<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarStackProps<XScale, YScale, Datum>, 'BarsComponent'>) {
>(props: Omit<BaseBarStackProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarStack<XScale, YScale, Datum> {...props} BarsComponent={AnimatedBars} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function AnimatedLineSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
>(props: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseLineSeries<XScale, YScale, Datum> {...props} PathComponent={AnimatedPath} />;
}
2 changes: 1 addition & 1 deletion packages/visx-xychart/src/components/series/AreaSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export default function AreaSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseAreaSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
>(props: Omit<BaseAreaSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseAreaSeries<XScale, YScale, Datum> {...props} />;
}
2 changes: 1 addition & 1 deletion packages/visx-xychart/src/components/series/BarGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function BarGroup<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
>(props: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarGroup<XScale, YScale, Datum> {...props} BarsComponent={Bars} />;
}
6 changes: 3 additions & 3 deletions packages/visx-xychart/src/components/series/BarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import React from 'react';
import BaseBarSeries, { BaseBarSeriesProps } from './private/BaseBarSeries';
import Bars from './private/Bars';

function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
...props
}: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>) {
function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>(
props: Omit<BaseBarSeriesProps<XScale, YScale, Datum>, 'BarsComponent'>,
) {
return <BaseBarSeries<XScale, YScale, Datum> {...props} BarsComponent={Bars} />;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/visx-xychart/src/components/series/BarStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function BarStack<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarStackProps<XScale, YScale, Datum>, 'BarsComponent'>) {
>(props: Omit<BaseBarStackProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarStack<XScale, YScale, Datum> {...props} BarsComponent={Bars} />;
}
11 changes: 9 additions & 2 deletions packages/visx-xychart/src/components/series/GlyphSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ export default function GlyphSeries<
renderGlyph?: React.FC<GlyphProps<Datum>>;
}) {
const renderGlyphs = useCallback(
({ glyphs, onPointerMove, onPointerOut, onPointerUp }: GlyphsProps<XScale, YScale, Datum>) =>
({
glyphs,
onPointerMove,
onPointerOut,
onPointerUp,
onFocus,
onBlur,
}: GlyphsProps<XScale, YScale, Datum>) =>
glyphs.map(glyph => (
<React.Fragment key={glyph.key}>
{renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp })}
{renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp, onFocus, onBlur })}
</React.Fragment>
)),
[renderGlyph],
Expand Down
2 changes: 1 addition & 1 deletion packages/visx-xychart/src/components/series/LineSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export default function LineSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
>(props: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseLineSeries<XScale, YScale, Datum> {...props} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default function AnimatedBars<XScale extends AxisScale, YScale extends Ax
unique: true,
...useBarTransitionConfig({ horizontal, scale: horizontal ? xScale : yScale }),
});
const isFocusable = Boolean(rectProps.onFocus || rectProps.onBlur);

return (
// eslint-disable-next-line react/jsx-no-useless-fragment
Expand All @@ -65,6 +66,7 @@ export default function AnimatedBars<XScale extends AxisScale, YScale extends Ax
item == null || key == null ? null : (
<animated.rect
key={key}
tabIndex={isFocusable ? 0 : undefined}
className="visx-bar"
x={x}
y={y}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export default function AnimatedGlyphs<
horizontal,
xScale,
yScale,
onBlur,
onFocus,
onPointerMove,
onPointerOut,
onPointerUp,
Expand Down Expand Up @@ -90,6 +92,8 @@ export default function AnimatedGlyphs<
y: 0,
size: item.size,
color: 'currentColor', // allows us to animate the color of the <g /> element
onBlur,
onFocus,
onPointerMove,
onPointerOut,
onPointerUp,
Expand Down
9 changes: 8 additions & 1 deletion packages/visx-xychart/src/components/series/private/Bars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ export default function Bars({
yScale,
...rectProps
}: BarsProps<any, any>) {
const isFocusable = Boolean(rectProps.onFocus || rectProps.onBlur);
return (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{bars.map(({ key, ...barProps }) => (
<rect className="visx-bar" key={key} {...barProps} {...rectProps} />
<rect
key={key}
className="visx-bar"
tabIndex={isFocusable ? 0 : undefined}
{...barProps}
{...rectProps}
/>
))}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,21 +26,20 @@ export type BaseAreaSeriesProps<
lineProps?: Omit<LinePathProps<Datum>, 'data' | 'x' | 'y' | 'children' | 'defined'>;
/** Rendered component which is passed path props by BaseAreaSeries after processing. */
PathComponent?: React.FC<Omit<React.SVGProps<SVGPathElement>, 'ref'>> | 'path';
} & Omit<
React.SVGProps<SVGPathElement>,
'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref' | 'pointerEvents'
>;
} & Omit<React.SVGProps<SVGPathElement>, 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref'>;

function BaseAreaSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
PathComponent = 'path',
curve,
data,
dataKey,
lineProps,
onPointerMove: onPointerMoveProps,
onPointerOut: onPointerOutProps,
onPointerUp: onPointerUpProps,
pointerEvents = true,
onBlur,
onFocus,
onPointerMove,
onPointerOut,
onPointerUp,
enableEvents = true,
renderLine = true,
xAccessor,
xScale,
Expand All @@ -57,36 +56,17 @@ function BaseAreaSeries<XScale extends AxisScale, YScale extends AxisScale, Datu
);
const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222';

const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType<
Datum
>;
const onPointerMove = useCallback(
(p: PointerEventParams<Datum>) => {
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<XScale, YScale, Datum>({
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), [
Expand All @@ -108,6 +88,25 @@ function BaseAreaSeries<XScale extends AxisScale, YScale extends AxisScale, Datu
}
: { y0: numericScaleBaseline, y1: getScaledY };

// render invisible glyphs for focusing if onFocus/onBlur are defined
const captureFocusEvents = Boolean(onFocus || onBlur);
const renderGlyphs = useCallback(
({ glyphs }: GlyphsProps<XScale, YScale, Datum>) =>
captureFocusEvents
? glyphs.map(glyph => (
<React.Fragment key={glyph.key}>
{defaultRenderGlyph({
...glyph,
color: 'transparent',
onFocus: eventEmitters.onFocus,
onBlur: eventEmitters.onBlur,
})}
</React.Fragment>
))
: null,
[captureFocusEvents, eventEmitters.onFocus, eventEmitters.onBlur],
);

return (
<>
<Area {...xAccessors} {...yAccessors} {...areaProps} curve={curve} defined={isDefined}>
Expand All @@ -118,7 +117,7 @@ function BaseAreaSeries<XScale extends AxisScale, YScale extends AxisScale, Datu
fill={color}
{...areaProps}
d={path(data) || ''}
{...pointerEventEmitters}
{...eventEmitters}
/>
)}
</Area>
Expand All @@ -143,6 +142,17 @@ function BaseAreaSeries<XScale extends AxisScale, YScale extends AxisScale, Datu
)}
</LinePath>
)}
{captureFocusEvents && (
<BaseGlyphSeries
dataKey={dataKey}
data={data}
xAccessor={xAccessor}
yAccessor={yAccessor}
xScale={xScale}
yScale={yScale}
renderGlyphs={renderGlyphs}
/>
)}
</>
);
}
Expand Down
Loading

0 comments on commit 8219a41

Please sign in to comment.