Skip to content

Commit 2a71e86

Browse files
authored
chore: Fixes mixed charts popover behaviour for React 18 (#3851)
1 parent ac036d3 commit 2a71e86

File tree

5 files changed

+79
-45
lines changed

5 files changed

+79
-45
lines changed

src/mixed-line-bar-chart/__integ__/announcements.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ describe('Popover content is announced as plain text on hover', () => {
3737
test(
3838
'with expandable sub-items',
3939
setupTest(`#/light/mixed-line-bar-chart/drilldown?useLinks=${useLinks}&expandableSubItems=true`, async page => {
40-
const coordinateIndex = 3;
4140
const wrapper = createWrapper().findMixedLineBarChart();
42-
const barGroup = wrapper.findBarGroups().get(coordinateIndex).toSelector();
41+
const barGroup = wrapper.findBarGroups().get(3).toSelector();
42+
const nextBarGroup = wrapper.findBarGroups().get(4).toSelector();
4343
const getLabel = () => page.getElementAttribute(barGroup, 'aria-label');
4444
await page.hoverElement(barGroup);
4545
await page.waitForAssertion(async () => {
@@ -56,11 +56,11 @@ describe('Popover content is announced as plain text on hover', () => {
5656
await page.click(
5757
wrapper.findDetailPopover().findContent().findExpandableSection().findExpandButton().toSelector()
5858
);
59-
// Pin and dismiss the poover,
59+
// Pin and dismiss the popover,
6060
// then hover over a different item and come back to hover the initial one.
6161
await page.clickBarGroup(barGroup);
6262
await page.click(wrapper.findDetailPopover().findDismissButton().toSelector());
63-
await page.hoverElement(wrapper.findBarGroups().get(4).toSelector());
63+
await page.hoverElement(nextBarGroup);
6464
await page.hoverElement(barGroup);
6565
await page.waitForAssertion(async () => {
6666
const label = await getLabel();

src/mixed-line-bar-chart/__integ__/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export class MixedChartPage extends BasePageObject {
2929
// If a popover was pinned, we need to unpin it before pinning another one.
3030
if (this.currentIndex) {
3131
await this.click(this.wrapper.findDetailPopover().findDismissButton().toSelector());
32+
// Wait for popover dismiss reopen delay.
33+
await this.pause(50);
3234
}
3335
this.currentIndex = index;
3436
await this.clickBarGroup(barGroup);

src/mixed-line-bar-chart/__tests__/keyboard-navigation.test.tsx

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,33 @@
44
import React from 'react';
55
import { render } from '@testing-library/react';
66

7+
import { createWrapper } from '@cloudscape-design/test-utils-core/dom';
8+
79
import { KeyCode } from '../../../lib/components/internal/keycode';
810
import MixedLineBarChart from '../../../lib/components/mixed-line-bar-chart';
9-
import { MixedLineBarChartWrapper } from '../../../lib/components/test-utils/dom';
1011
import { barSeries, lineSeries3, thresholdSeries } from './common';
1112

1213
describe('Keyboard navigation', () => {
13-
test('opens popover for each series', () => {
14-
const { container } = render(
14+
function getChart() {
15+
return createWrapper().findMixedLineBarChart()!;
16+
}
17+
function expectValues(a: Array<number>) {
18+
for (let i = 0; i < a.length; i++) {
19+
const value = a[i];
20+
expect(getChart().findDetailPopover()!.findSeries()![i].findValue().getElement()).toHaveTextContent(
21+
value.toString()
22+
);
23+
}
24+
}
25+
function focusApplication() {
26+
getChart().findApplication()!.focus();
27+
}
28+
function goToNextDataPoint() {
29+
getChart().findApplication()!.keydown(KeyCode.right);
30+
}
31+
32+
test('opens popover for each series (mixed)', () => {
33+
render(
1534
<MixedLineBarChart
1635
height={250}
1736
xDomain={['Potatoes', 'Chocolate', 'Apples', 'Oranges']}
@@ -20,23 +39,7 @@ describe('Keyboard navigation', () => {
2039
series={[barSeries, lineSeries3, thresholdSeries]}
2140
/>
2241
);
23-
24-
const chart = new MixedLineBarChartWrapper(container);
25-
const application = chart.findApplication()!;
26-
27-
const expectValues = (a: Array<number>) => {
28-
for (let i = 0; i < a.length; i++) {
29-
const value = a[i];
30-
expect(chart.findDetailPopover()!.findSeries()![i].findValue().getElement()).toHaveTextContent(
31-
value.toString()
32-
);
33-
}
34-
};
35-
36-
const goToNextDataPoint = () => application.keydown(KeyCode.right);
37-
38-
application.focus(); // Focusing the application opens the popover
39-
42+
focusApplication(); // Focusing the application opens the popover
4043
expectValues([77, 7, 8]);
4144
goToNextDataPoint();
4245
expectValues([546, 5, 8]);
@@ -45,4 +48,24 @@ describe('Keyboard navigation', () => {
4548
goToNextDataPoint();
4649
expectValues([47, 7, 8]);
4750
});
51+
52+
test('opens popover for each series (line)', () => {
53+
render(
54+
<MixedLineBarChart
55+
height={250}
56+
xDomain={['Potatoes', 'Chocolate', 'Apples', 'Oranges']}
57+
yDomain={[0, 10]}
58+
xScaleType="categorical"
59+
series={[lineSeries3]}
60+
/>
61+
);
62+
focusApplication(); // Focusing the application opens the popover
63+
expectValues([7]);
64+
goToNextDataPoint();
65+
expectValues([5]);
66+
goToNextDataPoint();
67+
expectValues([9]);
68+
goToNextDataPoint();
69+
expectValues([7]);
70+
});
4871
});

src/mixed-line-bar-chart/chart-container.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
44

55
import { getIsRtl, useMergeRefs } from '@cloudscape-design/component-toolkit/internal';
66

@@ -223,7 +223,7 @@ export default function ChartContainer<T extends ChartDataTypes>({
223223
const scaledSeries = makeScaledSeries(visibleSeries, xAxisProps.scale, yAxisProps.scale);
224224
const barGroups: ScaledBarGroup<T>[] = makeScaledBarGroups(visibleSeries, xAxisProps.scale, plotWidth, plotHeight, y);
225225

226-
const { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, dismissPopover } = usePopover();
226+
const { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, hidePopover, dismissPopover } = usePopover();
227227

228228
// Allows to add a delay between popover is dismissed and handlers are enabled to prevent immediate popover reopening.
229229
const [isHandlersDisabled, setHandlersDisabled] = useState(false);
@@ -251,13 +251,11 @@ export default function ChartContainer<T extends ChartDataTypes>({
251251
setHighlightedPoint(point);
252252
if (point) {
253253
highlightSeries(point.series);
254-
setVerticalMarkerX({
255-
scaledX: point.x,
256-
label: point.datum?.x ?? null,
257-
});
254+
setVerticalMarkerX({ scaledX: point.x, label: point.datum?.x ?? null });
255+
showPopover();
258256
}
259257
},
260-
[setHighlightedGroupIndex, setHighlightedPoint, highlightSeries]
258+
[setHighlightedGroupIndex, setHighlightedPoint, highlightSeries, showPopover]
261259
);
262260

263261
const clearAllHighlights = useCallback(() => {
@@ -273,8 +271,9 @@ export default function ChartContainer<T extends ChartDataTypes>({
273271
clearAllHighlights();
274272
}
275273
setVerticalMarkerX(marker);
274+
showPopover();
276275
},
277-
[clearAllHighlights]
276+
[clearAllHighlights, showPopover]
278277
);
279278

280279
// Highlight all points and bars at a given X index in a mixed line and bar chart
@@ -283,14 +282,15 @@ export default function ChartContainer<T extends ChartDataTypes>({
283282
highlightSeries(null);
284283
setHighlightedPoint(null);
285284
setHighlightedGroupIndex(groupIndex);
285+
showPopover();
286286
},
287-
[highlightSeries, setHighlightedPoint, setHighlightedGroupIndex]
287+
[highlightSeries, setHighlightedPoint, setHighlightedGroupIndex, showPopover]
288288
);
289289

290290
const clearHighlightedSeries = useCallback(() => {
291291
clearAllHighlights();
292-
dismissPopover();
293-
}, [dismissPopover, clearAllHighlights]);
292+
hidePopover();
293+
}, [hidePopover, clearAllHighlights]);
294294

295295
const { isGroupNavigation, ...handlers } = useNavigation({
296296
series,
@@ -349,12 +349,6 @@ export default function ChartContainer<T extends ChartDataTypes>({
349349
return () => document.removeEventListener('keydown', onKeyDown);
350350
}, [dismissPopover]);
351351

352-
useLayoutEffect(() => {
353-
if (highlightedX !== null || highlightedPoint !== null) {
354-
showPopover();
355-
}
356-
}, [highlightedX, highlightedPoint, showPopover]);
357-
358352
const onPopoverDismiss = (outsideClick?: boolean) => {
359353
dismissPopover();
360354

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import { useCallback, useState } from 'react';
3+
4+
import { useCallback, useRef, useState } from 'react';
5+
6+
// The delay prevents re-opening popover immediately upon dismissing,
7+
// so that the popover actually closes. It can be reopened with the next
8+
// hover or focus event that occurs after the delay.
9+
const REOPEN_DELAY_MS = 50;
410

511
export function usePopover() {
12+
const dismissedTimeRef = useRef(Date.now() - REOPEN_DELAY_MS);
613
const [state, setState] = useState<'open' | 'closed' | 'pinned'>('closed');
714

815
const isPopoverOpen = state !== 'closed';
916
const isPopoverPinned = state === 'pinned';
1017

11-
const showPopover = useCallback(() => setState('open'), []);
18+
const showPopover = useCallback(() => {
19+
if (Date.now() - dismissedTimeRef.current > REOPEN_DELAY_MS) {
20+
setState('open');
21+
}
22+
}, []);
1223
const pinPopover = useCallback(() => setState('pinned'), []);
13-
const dismissPopover = useCallback(() => setState('closed'), []);
24+
const hidePopover = useCallback(() => setState('closed'), []);
25+
const dismissPopover = useCallback(() => {
26+
setState('closed');
27+
dismissedTimeRef.current = Date.now();
28+
}, []);
1429

15-
return { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, dismissPopover };
30+
return { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, hidePopover, dismissPopover };
1631
}

0 commit comments

Comments
 (0)