-
Notifications
You must be signed in to change notification settings - Fork 204
fix: Fixes chart popover click outside when in iframe #4005
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 |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; | ||
| import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; | ||
|
|
||
| import createWrapper from '../../../../../lib/components/test-utils/selectors'; | ||
|
|
||
| describe.each([false, true])('iframe=%s', iframe => { | ||
| test( | ||
| 'can be unpinned by clicking outside', | ||
| useBrowser(async browser => { | ||
| const page = new BasePageObject(browser); | ||
| await browser.url(`#/light/charts.test?iframe=${iframe}`); | ||
| await page.runInsideIframe('#content-iframe', iframe, async () => { | ||
| const chart = createWrapper().findLineChart(); | ||
| const popover = chart.findDetailPopover(); | ||
| const popoverDismiss = popover.findDismissButton(); | ||
|
|
||
| // Pins popover on the first point. | ||
| await page.click('h2'); | ||
| await page.keys(['Tab', 'ArrowRight', 'Enter']); | ||
| await page.waitForVisible(popoverDismiss.toSelector()); | ||
|
|
||
| // Unpin by clicking outside the chart. | ||
| await page.click('h1'); | ||
| await expect(page.isDisplayed(popover.toSelector())).resolves.toBe(false); | ||
| }); | ||
| }) | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import React from 'react'; | ||
| import { act, render } from '@testing-library/react'; | ||
|
|
||
| import ChartPopover from '../../../../../lib/components/internal/components/chart-popover'; | ||
|
|
||
| describe('click outside', () => { | ||
| function TestComponent({ onDismiss }: { onDismiss: () => void }) { | ||
| return ( | ||
| <div> | ||
| <div id="outside">x</div> | ||
| <div id="container">x</div> | ||
| <ChartPopover | ||
| trackRef={{ current: null }} | ||
| container={document.querySelector('#container')} | ||
| onDismiss={onDismiss} | ||
| > | ||
| <div id="content">x</div> | ||
| </ChartPopover> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function nextFrame() { | ||
| return act(async () => { | ||
| await new Promise(resolve => requestAnimationFrame(resolve)); | ||
| }); | ||
| } | ||
|
|
||
| // We render the component twice to ensure the container reference is set correctly. | ||
| function renderPopover({ onDismiss }: { onDismiss: () => void }) { | ||
| const { rerender } = render(<TestComponent onDismiss={onDismiss} />); | ||
| rerender(<TestComponent onDismiss={onDismiss} />); | ||
| } | ||
|
|
||
| test('calls popover dismiss on outside click', () => { | ||
| const onDismiss = jest.fn(); | ||
| renderPopover({ onDismiss }); | ||
|
|
||
| document.querySelector('#outside')!.dispatchEvent(new Event('mousedown', { bubbles: true })); | ||
| expect(onDismiss).toHaveBeenCalledWith(true); | ||
| }); | ||
|
|
||
| test('does not call popover dismiss when clicked inside container', () => { | ||
| const onDismiss = jest.fn(); | ||
| renderPopover({ onDismiss }); | ||
|
|
||
| document.querySelector('#container')!.dispatchEvent(new Event('mousedown', { bubbles: true })); | ||
| expect(onDismiss).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('does not call popover dismiss when clicked inside popover', () => { | ||
| const onDismiss = jest.fn(); | ||
| renderPopover({ onDismiss }); | ||
|
|
||
| document.querySelector('#content')!.dispatchEvent(new Event('mousedown', { bubbles: true })); | ||
| expect(onDismiss).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('calls popover dismiss when clicked inside popover and then outside', async () => { | ||
| const onDismiss = jest.fn(); | ||
| renderPopover({ onDismiss }); | ||
|
|
||
| document.querySelector('#content')!.dispatchEvent(new Event('mousedown', { bubbles: true })); | ||
| expect(onDismiss).not.toHaveBeenCalled(); | ||
|
|
||
| await nextFrame(); | ||
|
|
||
| document.querySelector('#outside')!.dispatchEvent(new Event('mousedown', { bubbles: true })); | ||
| expect(onDismiss).toHaveBeenCalledWith(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,6 @@ import PopoverBody from '../../../popover/body'; | |
| import PopoverContainer from '../../../popover/container'; | ||
| import { PopoverProps } from '../../../popover/interfaces'; | ||
| import { getBaseProps } from '../../base-component'; | ||
| import { nodeBelongs } from '../../utils/node-belongs'; | ||
|
|
||
| import popoverStyles from '../../../popover/styles.css.js'; | ||
| import styles from './styles.css.js'; | ||
|
|
@@ -87,24 +86,31 @@ function ChartPopover( | |
| ) { | ||
| const baseProps = getBaseProps(restProps); | ||
| const popoverObjectRef = useRef<HTMLDivElement | null>(null); | ||
|
|
||
| const popoverRef = useMergeRefs(popoverObjectRef, ref); | ||
|
|
||
| const clickFrameId = useRef<number | null>(null); | ||
|
Member
Author
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 use the same implementation in the internal popover component. The chart popover cannot use it because it imports the popover container instead. |
||
| const onMouseDown = () => { | ||
| // Indicate there was a click inside popover recently. | ||
| clickFrameId.current = requestAnimationFrame(() => (clickFrameId.current = null)); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const onDocumentClick = (event: MouseEvent) => { | ||
| if ( | ||
| event.target && | ||
| !nodeBelongs(popoverObjectRef.current, event.target as Element) && // click not in popover | ||
| !nodeContains(container, event.target as Element) // click not in segment | ||
| ) { | ||
| onDismiss(true); | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener('mousedown', onDocumentClick, { capture: true }); | ||
| return () => { | ||
| document.removeEventListener('mousedown', onDocumentClick, { capture: true }); | ||
| }; | ||
| if (popoverObjectRef.current) { | ||
| const document = popoverObjectRef.current.ownerDocument; | ||
| const onDocumentClick = (event: MouseEvent) => { | ||
| // Dismiss popover unless there was a click inside within the last animation frame. | ||
| // Ignore clicks inside the chart as those are handled separately. | ||
| if (clickFrameId.current === null && !nodeContains(container, event.target as Element)) { | ||
| onDismiss(true); | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener('mousedown', onDocumentClick); | ||
|
|
||
| return () => { | ||
| document.removeEventListener('mousedown', onDocumentClick); | ||
| }; | ||
| } | ||
| }, [container, onDismiss]); | ||
|
|
||
| // In chart popovers, dismiss button is present when they are pinned, so both values are equivalent. | ||
|
|
@@ -117,6 +123,7 @@ function ChartPopover( | |
| ref={popoverRef} | ||
| onMouseEnter={onMouseEnter} | ||
| onMouseLeave={onMouseLeave} | ||
| onMouseDown={onMouseDown} | ||
| onBlur={onBlur} | ||
| // The tabIndex makes it so that clicking inside popover assigns this element as blur target. | ||
| // That is necessary in charts to ensure the blur target is within the chart and no cleanup is needed. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import VerticalMarker from '../internal/components/cartesian-chart/vertical-mark | |
| import ChartPlot, { ChartPlotRef } from '../internal/components/chart-plot'; | ||
| import { useHeightMeasure } from '../internal/hooks/container-queries/use-height-measure'; | ||
| import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; | ||
| import { isElement } from '../internal/utils/dom'; | ||
| import { nodeBelongs } from '../internal/utils/node-belongs'; | ||
| import useContainerWidth from '../internal/utils/use-container-width'; | ||
| import BarGroups from './bar-groups'; | ||
|
|
@@ -391,11 +392,7 @@ export default function ChartContainer<T extends ChartDataTypes>({ | |
|
|
||
| const onApplicationBlur = (event: React.FocusEvent<Element>) => { | ||
| const blurTarget = event.relatedTarget || event.target; | ||
| if ( | ||
| blurTarget === null || | ||
| !(blurTarget instanceof Element) || | ||
|
Member
Author
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 See: Screen.Recording.2025-11-04.at.10.18.49.movReplacing the check with |
||
| !nodeBelongs(containerRefObject.current, blurTarget) | ||
| ) { | ||
| if (blurTarget === null || !isElement(blurTarget) || !nodeBelongs(containerRefObject.current, blurTarget)) { | ||
| clearHighlightedSeries(); | ||
| setVerticalMarkerX(null); | ||
|
|
||
|
|
||
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.
@gethinwebster I changed popover activation from pointer to keyboard because the former approach did not work with React 16 test pages: the page.hoverElement() did not help with offsetting the click. I did test the feature manually on both React versions.