Skip to content

Commit 9404812

Browse files
authored
fix: Let line chart highlight all points at a given X coordinate usin… (#538)
1 parent 308d769 commit 9404812

File tree

12 files changed

+581
-208
lines changed

12 files changed

+581
-208
lines changed
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import Container from '~components/container';
6+
import Header from '~components/header';
7+
import Box from '~components/box';
8+
import LineChart from '~components/line-chart';
9+
import ScreenshotArea from '../utils/screenshot-area';
10+
11+
import { data1, commonProps, lineChartInstructions } from '../mixed-line-bar-chart/common';
12+
13+
export default function () {
14+
return (
15+
<ScreenshotArea>
16+
<h1>Line chart integration tests</h1>
17+
<Box padding="l">
18+
<Container header={<Header variant="h2">Line Chart</Header>}>
19+
<LineChart
20+
{...commonProps}
21+
id="chart"
22+
height={130}
23+
series={[{ title: 'Series 1', type: 'line', data: data1 }]}
24+
xDomain={[0, 32]}
25+
yDomain={[0, 50]}
26+
xTitle="Time"
27+
yTitle="Latency (ms)"
28+
xScaleType="linear"
29+
ariaLabel="Line chart"
30+
ariaDescription={lineChartInstructions}
31+
/>
32+
</Container>
33+
</Box>
34+
</ScreenshotArea>
35+
);
36+
}

src/internal/components/chart-plot/focus-outline.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import React, { useEffect, useRef } from 'react';
44

55
import styles from './styles.css.js';
66
import useFocusVisible from '../../hooks/focus-visible/index';
7+
import { Offset } from './interfaces';
78

89
export interface FocusOutlineProps {
910
elementKey?: null | string | number | boolean;
1011
elementRef?: React.RefObject<SVGSVGElement | SVGGElement>;
11-
offset?: number;
12+
offset?: Offset;
1213
}
1314

1415
export default function FocusOutline({ elementKey, elementRef, offset = 0 }: FocusOutlineProps) {
@@ -34,12 +35,14 @@ export default function FocusOutline({ elementKey, elementRef, offset = 0 }: Foc
3435
function showOutline(
3536
el: SVGRectElement,
3637
position: { x: number; y: number; width: number; height: number },
37-
offset: number
38+
offset: Offset
3839
) {
39-
el.setAttribute('x', (position.x - offset).toString());
40-
el.setAttribute('y', (position.y - offset).toString());
41-
el.setAttribute('width', (position.width + 2 * offset).toString());
42-
el.setAttribute('height', (position.height + 2 * offset).toString());
40+
const offsetX = typeof offset === 'number' ? offset : offset.x;
41+
const offsetY = typeof offset === 'number' ? offset : offset.y;
42+
el.setAttribute('x', (position.x - offsetX).toString());
43+
el.setAttribute('y', (position.y - offsetY).toString());
44+
el.setAttribute('width', (position.width + 2 * offsetX).toString());
45+
el.setAttribute('height', (position.height + 2 * offsetY).toString());
4346
el.style.visibility = 'visible';
4447
}
4548

src/internal/components/chart-plot/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import LiveRegion from '../live-region/index';
1111
import ApplicationController, { ApplicationRef } from './application-controller';
1212
import FocusOutline from './focus-outline';
1313
import focusSvgElement from '../../utils/focus-svg-element';
14+
import { Offset } from './interfaces';
1415

1516
const DEFAULT_PLOT_FOCUS_OFFSET = 3;
1617
const DEFAULT_ELEMENT_FOCUS_OFFSET = 3;
@@ -37,7 +38,7 @@ export interface ChartPlotProps {
3738
ariaRoleDescription?: string;
3839
activeElementKey?: null | string | number | boolean;
3940
activeElementRef?: React.RefObject<SVGGElement>;
40-
activeElementFocusOffset?: number;
41+
activeElementFocusOffset?: Offset;
4142
ariaLiveRegion?: React.ReactNode;
4243
isClickable?: boolean;
4344
isPrecise?: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
export type Offset = number | { x: number; y: number };

src/line-chart/__integ__/line-chart.test.ts

+224-43
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const popoverHeaderSelector = (wrapper: LineChartWrapper = chartWrapper) =>
1616
wrapper.findDetailPopover().findHeader().toSelector();
1717
const popoverContentSelector = (wrapper: LineChartWrapper = chartWrapper) =>
1818
wrapper.findDetailPopover().findContent().toSelector();
19+
const popoverDismissButtonSelector = (wrapper: LineChartWrapper = chartWrapper) =>
20+
wrapper.findDetailPopover().findDismissButton().toSelector();
1921

2022
function setupTest(url: string, testFn: (page: LineChartPageObject) => Promise<void>) {
2123
return useBrowser(async browser => {
@@ -26,47 +28,226 @@ function setupTest(url: string, testFn: (page: LineChartPageObject) => Promise<v
2628
}
2729

2830
describe('Keyboard navigation', () => {
29-
test(
30-
'line series are navigable with keyboard',
31-
setupTest('#/light/line-chart/test', async page => {
32-
await page.click('button');
33-
await page.keys(['Escape', 'Tab', 'ArrowRight']);
34-
35-
// First series is highlighted
36-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
37-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
38-
39-
// Move horizontally to the next point
40-
await page.keys(['ArrowRight']);
41-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
42-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
43-
44-
// Move horizontally to the last point
45-
await page.keys(['ArrowLeft', 'ArrowLeft']);
46-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('31');
47-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
48-
})
49-
);
50-
51-
test(
52-
'threshold series are navigable with keyboard',
53-
setupTest('#/light/line-chart/test', async page => {
54-
await page.click('button');
55-
await page.keys(['Escape', 'Tab', 'ArrowRight', 'ArrowUp']);
56-
57-
// Threshold is highlighted
58-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
59-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
60-
61-
// Move horizontally to the next point
62-
await page.keys(['ArrowRight']);
63-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
64-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
65-
66-
// Move horizontally to the last point
67-
await page.keys(['ArrowLeft', 'ArrowLeft']);
68-
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('31');
69-
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
70-
})
71-
);
31+
describe('with one single series', () => {
32+
const testPath = '#/light/line-chart/single-series';
33+
34+
test(
35+
'line series is navigable with keyboard',
36+
setupTest(testPath, async page => {
37+
await focusChart(page);
38+
39+
// First series is highlighted
40+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
41+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
42+
43+
// Move horizontally to the next point
44+
await page.keys(['ArrowRight']);
45+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
46+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
47+
48+
// Move horizontally to the last point
49+
await page.keys(['ArrowLeft', 'ArrowLeft']);
50+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('31');
51+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
52+
})
53+
);
54+
55+
test(
56+
'retains focus after dismissing popover',
57+
setupTest('#/light/line-chart/single-series', async page => {
58+
await focusChart(page);
59+
60+
// First series is highlighted
61+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
62+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
63+
64+
// Pin popover
65+
await page.keys(['Space']);
66+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(true);
67+
68+
// Dismiss popover
69+
await page.keys(['Space']);
70+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(false);
71+
72+
// Pin popover again, to prove that the focus on the element was not lost
73+
await page.keys(['Space']);
74+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(true);
75+
})
76+
);
77+
});
78+
79+
describe('with multiple series', () => {
80+
const testPath = '#/light/line-chart/test';
81+
82+
test(
83+
'line series are navigable with keyboard',
84+
setupTest(testPath, async page => {
85+
await focusChart(page);
86+
await page.keys(['ArrowDown']);
87+
88+
// First series is highlighted
89+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
90+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
91+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
92+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
93+
94+
// Move horizontally to the next point
95+
await page.keys(['ArrowRight']);
96+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
97+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
98+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
99+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
100+
101+
// Move horizontally to the last point
102+
await page.keys(['ArrowLeft', 'ArrowLeft']);
103+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('31');
104+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
105+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
106+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
107+
})
108+
);
109+
110+
test(
111+
'threshold series are navigable with keyboard',
112+
setupTest(testPath, async page => {
113+
await focusChart(page);
114+
await page.keys(['ArrowUp']);
115+
116+
// Threshold is highlighted
117+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
118+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 1');
119+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
120+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
121+
122+
// Move horizontally to the next point
123+
await page.keys(['ArrowRight']);
124+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
125+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 1');
126+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
127+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
128+
129+
// Move horizontally to the last point
130+
await page.keys(['ArrowLeft', 'ArrowLeft']);
131+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('31');
132+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 1');
133+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
134+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
135+
})
136+
);
137+
138+
test(
139+
'cycles through the different series',
140+
setupTest(testPath, async page => {
141+
await focusChart(page);
142+
143+
// All series are highlighted
144+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
145+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
146+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
147+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
148+
149+
// Move vertically to the first series
150+
await page.keys(['ArrowDown']);
151+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
152+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
153+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
154+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
155+
156+
// Move vertically to the next series
157+
await page.keys(['ArrowDown']);
158+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
159+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 1');
160+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
161+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
162+
163+
// Move vertically to the next series
164+
await page.keys(['ArrowDown']);
165+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
166+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 1');
167+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
168+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
169+
170+
// Circle back to initial state highlighting all series
171+
await page.keys(['ArrowDown']);
172+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
173+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
174+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
175+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
176+
})
177+
);
178+
179+
test(
180+
'retains focus after dismissing popover',
181+
setupTest(testPath, async page => {
182+
await focusChart(page);
183+
184+
// All series are highlighted
185+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
186+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
187+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
188+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
189+
190+
// Pin popover
191+
await page.keys(['Space']);
192+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(true);
193+
194+
// Dismiss popover
195+
await page.keys(['Space']);
196+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(false);
197+
198+
// Pin popover again, to prove that the focus on the element was not lost
199+
await page.keys(['Space']);
200+
await expect(page.isExisting(popoverDismissButtonSelector())).resolves.toBe(true);
201+
})
202+
);
203+
204+
test(
205+
'maintains X coordinate after switching between focusing a single series and all series',
206+
setupTest(testPath, async page => {
207+
await focusChart(page);
208+
209+
// All series are highlighted
210+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('0');
211+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
212+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
213+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
214+
215+
// Move horizontally to the next X coordinate
216+
await page.keys(['ArrowRight']);
217+
await expect(page.getText(popoverHeaderSelector())).resolves.not.toContain('0');
218+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
219+
220+
// Move vertically to the first series
221+
await page.keys(['ArrowDown']);
222+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('1');
223+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
224+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Series 2');
225+
await expect(page.getText(popoverContentSelector())).resolves.not.toContain('Threshold');
226+
227+
// Move horizontally to the next X coordinate
228+
await page.keys(['ArrowRight']);
229+
await expect(page.getText(popoverHeaderSelector())).resolves.not.toContain('1');
230+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('2');
231+
232+
// Move back up to highlight all series
233+
await page.keys(['ArrowUp']);
234+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('2');
235+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
236+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
237+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
238+
239+
// Move horizontally to the next X coordinate
240+
await page.keys(['ArrowRight']);
241+
await expect(page.getText(popoverHeaderSelector())).resolves.toContain('3');
242+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 1');
243+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Series 2');
244+
await expect(page.getText(popoverContentSelector())).resolves.toContain('Threshold');
245+
})
246+
);
247+
});
72248
});
249+
250+
async function focusChart(page: LineChartPageObject) {
251+
await page.click('button');
252+
await page.keys(['Escape', 'Tab', 'ArrowRight']);
253+
}

0 commit comments

Comments
 (0)