Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(xychart): add support for FocusEvents #959

Merged
merged 12 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-named since no longer specific to PointerEvents

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'>) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these were all double spread unnecessarily

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}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noting that this implementation requires the SVG 2.0 spec – SVG 1.1 doesn't support tabIndex which means this may not work on older browsers. the alternative approach (used in data-ui) was to wrap each element in an <a /> element, which are consistently tab-able by all browsers.

For now I think this more modern approach is more ideal since it's simpler and it is supported across all modern browsers
image

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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointerEvents => enableEvents throughout

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