-
Notifications
You must be signed in to change notification settings - Fork 197
chore: Fixes mixed charts popover behaviour for React 18 #3851
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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']} | ||
|
@@ -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]); | ||
|
@@ -45,4 +48,24 @@ describe('Keyboard navigation', () => { | |
goToNextDataPoint(); | ||
expectValues([47, 7, 8]); | ||
}); | ||
|
||
test('opens popover for each series (line)', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
}); | ||
}); |
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'; | ||
|
||
|
@@ -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); | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(() => { | ||
|
@@ -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 | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If highlightedX was |
||
|
||
const onPopoverDismiss = (outsideClick?: boolean) => { | ||
dismissPopover(); | ||
|
||
|
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
} |
There was a problem hiding this comment.
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.