Skip to content

Commit

Permalink
feat: Allow mouse to enter popover in charts (#565)
Browse files Browse the repository at this point in the history
Co-authored-by: Abdallah Alhalees <[email protected]>
Co-authored-by: Aleksandra Danilina <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2022
1 parent 305d9ec commit 4d7fb7b
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 71 deletions.
16 changes: 16 additions & 0 deletions src/area-chart/__integ__/area-chart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ describe('Popover', () => {
})
);

test(
'popover is shown when mouse is over popover',
setupTest('#/light/area-chart/test', 'Linear latency chart', async page => {
await page.setWindowSize({ width: 2000, height: 800 });
await expect(page.hasPopover()).resolves.toBe(false);

await page.focusPlot();
await expect(page.getPopoverTitle()).resolves.toBe('1s');

await page.hoverElement(page.chart.findDetailPopover().findHeader().toSelector());
await expect(page.getPopoverTitle()).resolves.toBe('1s');
})
);

test(
'popover can be pinned/unpinned by clicking on the plot',
setupTest('#/light/area-chart/test', 'Linear latency chart', async page => {
Expand Down Expand Up @@ -219,6 +233,7 @@ describe('Keyboard navigation', () => {
test(
'can navigate between data points within series',
setupTest('#/light/area-chart/test', 'Linear latency chart', async page => {
await page.setWindowSize({ width: 2000, height: 800 });
await page.focusPlot();

await expect(page.getPopoverTitle()).resolves.toBe('1s');
Expand Down Expand Up @@ -331,6 +346,7 @@ describe('Focus delegation', () => {
test(
'preserves series highlight when focused away from plot',
setupTest('#/light/area-chart/test', 'Linear latency chart', async page => {
await page.setWindowSize({ width: 2000, height: 800 });
await page.focusPlot();

await page.keys(['ArrowDown', 'ArrowRight']);
Expand Down
180 changes: 177 additions & 3 deletions src/area-chart/__tests__/area-chart-use-chart-model.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';
import React, { useState, useImperativeHandle, useRef } from 'react';

import useChartModel, { UseChartModelProps } from '../model/use-chart-model';
import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
import { ChartDataTypes } from '../../internal/components/cartesian-chart/interfaces';
import { act, render } from '@testing-library/react';
import { act, render, fireEvent } from '@testing-library/react';
import { AreaChartProps } from '../interfaces';
import { KeyCode } from '../../internal/keycode';
import { useReaction } from '../model/async-store';
Expand All @@ -17,6 +17,8 @@ class UseChartModelWrapper extends ElementWrapper {
highlightedPoint: 'highlighted-point',
highlightedSeries: 'highlighted-series',
highlightedX: 'highlighted-x',
plot: 'plot',
popover: 'popover',
};

findHighlightedPoint() {
Expand All @@ -30,25 +32,42 @@ class UseChartModelWrapper extends ElementWrapper {
findHighlightedX() {
return this.findByClassName(UseChartModelWrapper.selectors.highlightedX);
}

findPlot() {
return this.findByClassName(UseChartModelWrapper.selectors.plot);
}

findDetailPopover() {
return this.findByClassName(UseChartModelWrapper.selectors.popover);
}
}

function RenderChartModelHook(props: UseChartModelProps<ChartDataTypes>) {
const [highlightedSeries, setHighlightedSeries] = useState<null | AreaChartProps.Series<ChartDataTypes>>(null);
const [visibleSeries, setVisibleSeries] = useState(props.visibleSeries);
const [highlightedPoint, setHighlightedPoint] = useState<PlotPoint<ChartDataTypes> | null>(null);
const [highlightedX, setHighlightedX] = useState<readonly PlotPoint<ChartDataTypes>[] | null>(null);
const svgRef = useRef(null);
const popoverRef = useRef(null);

const { computed, handlers, interactions } = useChartModel({
const { computed, handlers, interactions, refs } = useChartModel({
...props,
highlightedSeries,
setHighlightedSeries,
visibleSeries,
setVisibleSeries,
popoverRef,
});

useReaction(interactions, state => state.highlightedPoint, setHighlightedPoint);
useReaction(interactions, state => state.highlightedX, setHighlightedX);

useImperativeHandle(refs.plot, () => ({
svg: svgRef.current!,
focusPlot: jest.fn(),
focusApplication: jest.fn(),
}));

return (
<div
onFocus={event => {
Expand All @@ -65,6 +84,17 @@ function RenderChartModelHook(props: UseChartModelProps<ChartDataTypes>) {
<span className={UseChartModelWrapper.selectors.highlightedX}>
{highlightedX === null ? null : highlightedX[0].x}
</span>
<svg
onMouseMove={handlers.onSVGMouseMove}
onMouseOut={handlers.onSVGMouseOut}
className={UseChartModelWrapper.selectors.plot}
ref={svgRef}
>
area plot
</svg>
<div className={UseChartModelWrapper.selectors.popover} ref={popoverRef} onMouseLeave={handlers.onPopoverLeave}>
<div>Popover</div>
</div>
<span>{visibleSeries[0].title}</span>
</div>
);
Expand Down Expand Up @@ -115,6 +145,7 @@ describe('useChartModel', () => {
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});
act(() => wrapper.focus());

Expand Down Expand Up @@ -149,6 +180,7 @@ describe('useChartModel', () => {
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});
act(() => wrapper.focus());

Expand All @@ -171,6 +203,7 @@ describe('useChartModel', () => {
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});
act(() => wrapper.focus());
expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('0');
Expand All @@ -179,5 +212,146 @@ describe('useChartModel', () => {
expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');
});
});

describe('Detail Popover', () => {
test('does not clear highlighted X on mouseOut if moving within popover', () => {
const { wrapper } = renderChartModelHook({
height: 0,
highlightedSeries: null,
setHighlightedSeries: (_series: AreaChartProps.Series<ChartDataTypes> | null) => _series,
setVisibleSeries: (_series: readonly AreaChartProps.Series<ChartDataTypes>[]) => _series,
width: 0,
xDomain: undefined,
xScaleType: 'linear',
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});

const mouseMoveEvent = {
relatedTarget: wrapper.findPlot()?.getElement(),
clientX: 100,
clientY: 100,
} as any;

act(() => {
fireEvent.mouseMove(wrapper.findPlot()!.getElement(), mouseMoveEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');

const mouseOutEvent = {
relatedTarget: wrapper.findDetailPopover()?.getElement(),
clientX: 0,
clientY: 0,
} as any;

act(() => {
fireEvent.mouseOut(wrapper.findPlot()!.getElement(), mouseOutEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');
});

test('clear highlighted X when mouse leaves popover', () => {
const { wrapper } = renderChartModelHook({
height: 0,
highlightedSeries: null,
setHighlightedSeries: (_series: AreaChartProps.Series<ChartDataTypes> | null) => _series,
setVisibleSeries: (_series: readonly AreaChartProps.Series<ChartDataTypes>[]) => _series,
width: 0,
xDomain: undefined,
xScaleType: 'linear',
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});

const mouseMoveEvent = {
relatedTarget: wrapper.findPlot()?.getElement(),
clientX: 100,
clientY: 100,
} as any;

act(() => {
fireEvent.mouseMove(wrapper.findPlot()!.getElement(), mouseMoveEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');

const mouseOutEvent = {
relatedTarget: wrapper.findDetailPopover()?.getElement(),
clientX: 0,
clientY: 0,
} as any;

act(() => {
fireEvent.mouseOut(wrapper.findPlot()!.getElement(), mouseOutEvent);
});

const mouseLeaveEvent = {
relatedTarget: wrapper.getElement(),
clientX: 400,
clientY: 400,
} as any;

act(() => {
fireEvent.mouseLeave(wrapper.findDetailPopover()!.getElement(), mouseLeaveEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('');
});
test('keep highlighted X when mouse leaves popover but in plot', () => {
const { wrapper } = renderChartModelHook({
height: 0,
highlightedSeries: null,
setHighlightedSeries: (_series: AreaChartProps.Series<ChartDataTypes> | null) => _series,
setVisibleSeries: (_series: readonly AreaChartProps.Series<ChartDataTypes>[]) => _series,
width: 0,
xDomain: undefined,
xScaleType: 'linear',
yScaleType: 'linear',
externalSeries: series,
visibleSeries: series,
popoverRef: { current: null },
});

const mouseMoveEvent = {
relatedTarget: wrapper.findPlot()?.getElement(),
clientX: 100,
clientY: 100,
} as any;

act(() => {
fireEvent.mouseMove(wrapper.findPlot()!.getElement(), mouseMoveEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');

const mouseOutEvent = {
relatedTarget: wrapper.findDetailPopover()?.getElement(),
clientX: 0,
clientY: 0,
} as any;

act(() => {
fireEvent.mouseOut(wrapper.findPlot()!.getElement(), mouseOutEvent);
});

const mouseLeaveEvent = {
relatedTarget: wrapper.findPlot()!.getElement(),
clientX: 100,
clientY: 100,
} as any;

act(() => {
fireEvent.mouseLeave(wrapper.findDetailPopover()!.getElement(), mouseLeaveEvent);
});

expect(wrapper.findHighlightedX()?.getElement()).toHaveTextContent('1');
});
});
});
});
2 changes: 2 additions & 0 deletions src/area-chart/elements/chart-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export default function AreaChartPopover<T extends AreaChartProps.DataTypes>({
trackKey: highlightDetails.highlightIndex,
dismissButton: highlightDetails.isPopoverPinned,
onDismiss: model.handlers.onPopoverDismiss,
onMouseLeave: model.handlers.onPopoverLeave,
ref: model.refs.popoverRef,
};

return (
Expand Down
2 changes: 2 additions & 0 deletions src/area-chart/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function InternalAreaChart<T extends AreaChartProps.DataTypes>({
}: InternalAreaChartProps<T>) {
const baseProps = getBaseProps(props);
const containerRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);

if (isDevelopment) {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down Expand Up @@ -102,6 +103,7 @@ export default function InternalAreaChart<T extends AreaChartProps.DataTypes>({
yScaleType,
height,
width,
popoverRef,
});

const { isEmpty, isNoMatch, showChart } = getChartStatus({
Expand Down
6 changes: 4 additions & 2 deletions src/area-chart/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export interface ChartModel<T extends AreaChartProps.DataTypes> {
getInternalSeries(series: AreaChartProps.Series<T>): ChartModel.InternalSeries<T>;
computed: ChartModel.ComputedProps<T>;
handlers: {
onSVGMouseMove: (event: React.MouseEvent<SVGElement, MouseEvent>) => void;
onSVGMouseOut: (event: React.MouseEvent<SVGElement, MouseEvent>) => void;
onSVGMouseMove: (event: React.MouseEvent<SVGElement>) => void;
onSVGMouseOut: (event: React.MouseEvent<SVGElement>) => void;
onSVGMouseDown: (event: React.MouseEvent<SVGSVGElement>) => void;
onSVGKeyDown: (event: React.KeyboardEvent) => void;
onSVGFocus: (event: React.FocusEvent<Element>, trigger: 'mouse' | 'keyboard') => void;
Expand All @@ -27,12 +27,14 @@ export interface ChartModel<T extends AreaChartProps.DataTypes> {
onPopoverDismiss: (outsideClick?: boolean) => void;
onContainerBlur: () => void;
onDocumentKeyDown: (event: KeyboardEvent) => void;
onPopoverLeave: (event: React.MouseEvent) => void;
};
interactions: ReadonlyAsyncStore<ChartModel.InteractionsState<T>>;
refs: {
plot: React.RefObject<ChartPlotRef>;
container: React.RefObject<HTMLDivElement>;
verticalMarker: React.RefObject<SVGLineElement>;
popoverRef: React.RefObject<HTMLElement>;
};
}

Expand Down
Loading

0 comments on commit 4d7fb7b

Please sign in to comment.