Skip to content
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
8 changes: 4 additions & 4 deletions src/mixed-line-bar-chart/__integ__/announcements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ describe('Popover content is announced as plain text on hover', () => {
test(
'with expandable sub-items',
setupTest(`#/light/mixed-line-bar-chart/drilldown?useLinks=${useLinks}&expandableSubItems=true`, async page => {
const coordinateIndex = 3;
const wrapper = createWrapper().findMixedLineBarChart();
const barGroup = wrapper.findBarGroups().get(coordinateIndex).toSelector();
const barGroup = wrapper.findBarGroups().get(3).toSelector();
const nextBarGroup = wrapper.findBarGroups().get(4).toSelector();
const getLabel = () => page.getElementAttribute(barGroup, 'aria-label');
await page.hoverElement(barGroup);
await page.waitForAssertion(async () => {
Expand All @@ -56,11 +56,11 @@ describe('Popover content is announced as plain text on hover', () => {
await page.click(
wrapper.findDetailPopover().findContent().findExpandableSection().findExpandButton().toSelector()
);
// Pin and dismiss the poover,
// Pin and dismiss the popover,
// then hover over a different item and come back to hover the initial one.
await page.clickBarGroup(barGroup);
await page.click(wrapper.findDetailPopover().findDismissButton().toSelector());
await page.hoverElement(wrapper.findBarGroups().get(4).toSelector());
await page.hoverElement(nextBarGroup);
await page.hoverElement(barGroup);
await page.waitForAssertion(async () => {
const label = await getLabel();
Expand Down
2 changes: 2 additions & 0 deletions src/mixed-line-bar-chart/__integ__/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class MixedChartPage extends BasePageObject {
// If a popover was pinned, we need to unpin it before pinning another one.
if (this.currentIndex) {
await this.click(this.wrapper.findDetailPopover().findDismissButton().toSelector());
// Wait for popover dismiss reopen delay.
await this.pause(50);
Copy link
Member Author

Choose a reason for hiding this comment

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

The new implementation prevents the popover to be shown immediately after dismiss using a small timeout, which needs to be considered also in tests.

}
this.currentIndex = index;
await this.clickBarGroup(barGroup);
Expand Down
63 changes: 43 additions & 20 deletions src/mixed-line-bar-chart/__tests__/keyboard-navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@
import React from 'react';
import { render } from '@testing-library/react';

import { createWrapper } from '@cloudscape-design/test-utils-core/dom';

import { KeyCode } from '../../../lib/components/internal/keycode';
import MixedLineBarChart from '../../../lib/components/mixed-line-bar-chart';
import { MixedLineBarChartWrapper } from '../../../lib/components/test-utils/dom';
import { barSeries, lineSeries3, thresholdSeries } from './common';

describe('Keyboard navigation', () => {
test('opens popover for each series', () => {
const { container } = render(
function getChart() {
return createWrapper().findMixedLineBarChart()!;
}
function expectValues(a: Array<number>) {
for (let i = 0; i < a.length; i++) {
const value = a[i];
expect(getChart().findDetailPopover()!.findSeries()![i].findValue().getElement()).toHaveTextContent(
value.toString()
);
}
}
function focusApplication() {
getChart().findApplication()!.focus();
}
function goToNextDataPoint() {
getChart().findApplication()!.keydown(KeyCode.right);
}

test('opens popover for each series (mixed)', () => {
render(
<MixedLineBarChart
height={250}
xDomain={['Potatoes', 'Chocolate', 'Apples', 'Oranges']}
Expand All @@ -20,23 +39,7 @@ describe('Keyboard navigation', () => {
series={[barSeries, lineSeries3, thresholdSeries]}
/>
);

const chart = new MixedLineBarChartWrapper(container);
const application = chart.findApplication()!;

const expectValues = (a: Array<number>) => {
for (let i = 0; i < a.length; i++) {
const value = a[i];
expect(chart.findDetailPopover()!.findSeries()![i].findValue().getElement()).toHaveTextContent(
value.toString()
);
}
};

const goToNextDataPoint = () => application.keydown(KeyCode.right);

application.focus(); // Focusing the application opens the popover

focusApplication(); // Focusing the application opens the popover
expectValues([77, 7, 8]);
goToNextDataPoint();
expectValues([546, 5, 8]);
Expand All @@ -45,4 +48,24 @@ describe('Keyboard navigation', () => {
goToNextDataPoint();
expectValues([47, 7, 8]);
});

test('opens popover for each series (line)', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This test is needed to increase test coverage of the mixed chart container

render(
<MixedLineBarChart
height={250}
xDomain={['Potatoes', 'Chocolate', 'Apples', 'Oranges']}
yDomain={[0, 10]}
xScaleType="categorical"
series={[lineSeries3]}
/>
);
focusApplication(); // Focusing the application opens the popover
expectValues([7]);
goToNextDataPoint();
expectValues([5]);
goToNextDataPoint();
expectValues([9]);
goToNextDataPoint();
expectValues([7]);
});
});
28 changes: 11 additions & 17 deletions src/mixed-line-bar-chart/chart-container.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

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

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

const { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, dismissPopover } = usePopover();
const { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, hidePopover, dismissPopover } = usePopover();

// Allows to add a delay between popover is dismissed and handlers are enabled to prevent immediate popover reopening.
const [isHandlersDisabled, setHandlersDisabled] = useState(false);
Expand Down Expand Up @@ -251,13 +251,11 @@ export default function ChartContainer<T extends ChartDataTypes>({
setHighlightedPoint(point);
if (point) {
highlightSeries(point.series);
setVerticalMarkerX({
scaledX: point.x,
label: point.datum?.x ?? null,
});
setVerticalMarkerX({ scaledX: point.x, label: point.datum?.x ?? null });
showPopover();
Copy link
Member Author

Choose a reason for hiding this comment

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

We now show the popover the moment a point or series is highlighted, without relying on the dependencies change.

}
},
[setHighlightedGroupIndex, setHighlightedPoint, highlightSeries]
[setHighlightedGroupIndex, setHighlightedPoint, highlightSeries, showPopover]
);

const clearAllHighlights = useCallback(() => {
Expand All @@ -273,8 +271,9 @@ export default function ChartContainer<T extends ChartDataTypes>({
clearAllHighlights();
}
setVerticalMarkerX(marker);
showPopover();
},
[clearAllHighlights]
[clearAllHighlights, showPopover]
);

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

const clearHighlightedSeries = useCallback(() => {
clearAllHighlights();
dismissPopover();
}, [dismissPopover, clearAllHighlights]);
hidePopover();
Copy link
Member Author

Choose a reason for hiding this comment

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

Hiding the popover (e.g. when the cursor leaves a series) is not the same as dismissing it - we should not prevent its reopen in that case.

}, [hidePopover, clearAllHighlights]);

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

useLayoutEffect(() => {
if (highlightedX !== null || highlightedPoint !== null) {
showPopover();
}
}, [highlightedX, highlightedPoint, showPopover]);
Copy link
Member Author

Choose a reason for hiding this comment

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

If highlightedX was 1 (showing the popover for the 1st group) and then it changes as 1 -> null -> 1 in a quick succession, that intermediate transition to null is ignored by React 18. As result, the effect callback is not fired, thus not showing the popover.


const onPopoverDismiss = (outsideClick?: boolean) => {
dismissPopover();

Expand Down
23 changes: 19 additions & 4 deletions src/mixed-line-bar-chart/hooks/use-popover.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useState } from 'react';

import { useCallback, useRef, useState } from 'react';

// The delay prevents re-opening popover immediately upon dismissing,
// so that the popover actually closes. It can be reopened with the next
// hover or focus event that occurs after the delay.
const REOPEN_DELAY_MS = 50;
Copy link
Member Author

Choose a reason for hiding this comment

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

The popover should not be reopen immediately upon dismissal - that is an existing requirement. That was ensured by checking the highlighted series index/id: if same, the popover will not reopen. The new implementation achieves the same with a small timeout, yet now it is also possible to re-open the popover shortly after it is dismissed by hovering over the same series the popover was open for before.


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

const isPopoverOpen = state !== 'closed';
const isPopoverPinned = state === 'pinned';

const showPopover = useCallback(() => setState('open'), []);
const showPopover = useCallback(() => {
if (Date.now() - dismissedTimeRef.current > REOPEN_DELAY_MS) {
setState('open');
}
}, []);
const pinPopover = useCallback(() => setState('pinned'), []);
const dismissPopover = useCallback(() => setState('closed'), []);
const hidePopover = useCallback(() => setState('closed'), []);
const dismissPopover = useCallback(() => {
setState('closed');
dismissedTimeRef.current = Date.now();
}, []);

return { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, dismissPopover };
return { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, hidePopover, dismissPopover };
}
Loading