Skip to content
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

feat: Explore popovers should close on escape #19902

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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/react';

Expand All @@ -29,6 +29,16 @@ const createProps = (): Partial<PopoverProps> => ({
content: <span data-test="control-popover-content">Information</span>,
});

const TestComponent: React.FC<PopoverProps> = props => (
<div id="controlSections">
<div data-test="outer-container">
<ControlPopover {...props}>
<span data-test="control-popover">Click me</span>
</ControlPopover>
</div>
</div>
);

const setupTest = (props: Partial<PopoverProps> = createProps()) => {
const setStateMock = jest.fn();
jest
Expand All @@ -38,19 +48,12 @@ const setupTest = (props: Partial<PopoverProps> = createProps()) => {
state === 'right' ? setStateMock : jest.fn(),
]) as any);

const { container } = render(
<div data-test="outer-container">
<div id="controlSections">
<ControlPopover {...props}>
<span data-test="control-popover">Click me</span>
</ControlPopover>
</div>
</div>,
);
const { container, rerender } = render(<TestComponent {...props} />);

return {
props,
container,
rerender,
setStateMock,
};
};
Expand All @@ -67,7 +70,7 @@ test('Should render', () => {
expect(screen.getByTestId('control-popover-content')).toBeInTheDocument();
});

test('Should lock the vertical scroll when the popup is visible', () => {
test('Should lock the vertical scroll when the popover is visible', () => {
setupTest();
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
expect(screen.getByTestId('outer-container')).not.toHaveStyle(
Expand All @@ -83,7 +86,7 @@ test('Should lock the vertical scroll when the popup is visible', () => {
);
});

test('Should place popup at the top', async () => {
test('Should place popover at the top', async () => {
const { setStateMock } = setupTest({
...createProps(),
getVisibilityRatio: () => 0.2,
Expand All @@ -97,7 +100,7 @@ test('Should place popup at the top', async () => {
});
});

test('Should place popup at the center', async () => {
test('Should place popover at the center', async () => {
const { setStateMock } = setupTest({
...createProps(),
getVisibilityRatio: () => 0.5,
Expand All @@ -111,7 +114,7 @@ test('Should place popup at the center', async () => {
});
});

test('Should place popup at the bottom', async () => {
test('Should place popover at the bottom', async () => {
const { setStateMock } = setupTest({
...createProps(),
getVisibilityRatio: () => 0.7,
Expand All @@ -124,3 +127,55 @@ test('Should place popup at the bottom', async () => {
expect(setStateMock).toHaveBeenCalledWith('rightBottom');
});
});

test('Should close popover on escape press', async () => {
setupTest({
...createProps(),
destroyTooltipOnHide: true,
});

expect(screen.getByTestId('control-popover')).toBeInTheDocument();
expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument();
userEvent.click(screen.getByTestId('control-popover'));
expect(await screen.findByText('Control Popover Test')).toBeInTheDocument();

// Ensure that pressing any other key than escape does nothing
fireEvent.keyDown(screen.getByTestId('control-popover'), {
key: 'Enter',
code: 13,
charCode: 13,
});
expect(await screen.findByText('Control Popover Test')).toBeInTheDocument();

// Escape should close the popover
fireEvent.keyDown(screen.getByTestId('control-popover'), {
key: 'Escape',
code: 27,
charCode: 0,
});

await waitFor(() => {
expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument();
});
});

test('Controlled mode', async () => {
const baseProps = {
...createProps(),
destroyTooltipOnHide: true,
visible: false,
};

const { rerender } = setupTest(baseProps);

expect(screen.getByTestId('control-popover')).toBeInTheDocument();
expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument();

rerender(<TestComponent {...baseProps} visible />);
expect(await screen.findByText('Control Popover Test')).toBeInTheDocument();

rerender(<TestComponent {...baseProps} />);
await waitFor(() => {
expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useRef, useEffect } from 'react';
import React, { useCallback, useRef, useEffect, useState } from 'react';

import Popover, {
PopoverProps as BasePopoverProps,
Expand All @@ -25,7 +25,7 @@ import Popover, {

const sectionContainerId = 'controlSections';
const getSectionContainerElement = () =>
document.getElementById(sectionContainerId)?.parentElement;
document.getElementById(sectionContainerId)?.lastElementChild as HTMLElement;

const getElementYVisibilityRatioOnContainer = (node: HTMLElement) => {
const containerHeight = window?.innerHeight;
Expand All @@ -44,9 +44,14 @@ export type PopoverProps = BasePopoverProps & {
const ControlPopover: React.FC<PopoverProps> = ({
getPopupContainer,
getVisibilityRatio = getElementYVisibilityRatioOnContainer,
visible: visibleProp,
...props
}) => {
const triggerElementRef = useRef<HTMLElement>();

const [visible, setVisible] = useState(
Copy link
Contributor

Choose a reason for hiding this comment

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

Totally, I don't think that introducing visible state here is a good idea.
We can control visibleProps.
Currently you are mixing visible state and visible props to control visible, but it is not a good idea.

Copy link
Contributor Author

@diegomedina248 diegomedina248 May 2, 2022

Choose a reason for hiding this comment

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

We need the visible state locally to control the popover in this component.
The "visible" prop can be undefined, (leaving the component in an uncontrolled manner), that's why we need to take over the control, and can't rely on simple calling the onVisibleChange callback

visibleProp === undefined ? props.defaultVisible : visibleProp,
);
const [placement, setPlacement] = React.useState<TooltipPlacement>('right');

const calculatePlacement = useCallback(() => {
Expand All @@ -69,7 +74,11 @@ const ControlPopover: React.FC<PopoverProps> = ({

const element = getSectionContainerElement();
if (element) {
element.style.overflowY = visible ? 'hidden' : 'auto';
element.style.setProperty(
'overflow-y',
visible ? 'hidden' : 'auto',
'important',
);
}
},
[calculatePlacement],
Expand All @@ -88,25 +97,53 @@ const ControlPopover: React.FC<PopoverProps> = ({
);

const handleOnVisibleChange = useCallback(
(visible: boolean) => {
if (props.visible === undefined) {
(visible: boolean | undefined) => {
if (visible === undefined) {
diegomedina248 marked this conversation as resolved.
Show resolved Hide resolved
changeContainerScrollStatus(visible);
}

props.onVisibleChange?.(visible);
setVisible(!!visible);
props.onVisibleChange?.(!!visible);
},
[props, changeContainerScrollStatus],
);

const handleDocumentKeyDownListener = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape') {
setVisible(false);
props.onVisibleChange?.(false);
diegomedina248 marked this conversation as resolved.
Show resolved Hide resolved
}
},
[props],
);

useEffect(() => {
if (props.visible !== undefined) {
changeContainerScrollStatus(props.visible);
if (visibleProp !== undefined) {
setVisible(!!visibleProp);
}
}, [props.visible, changeContainerScrollStatus]);
}, [visibleProp]);

useEffect(() => {
if (visible !== undefined) {
changeContainerScrollStatus(visible);
}
}, [visible, changeContainerScrollStatus]);

useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleDocumentKeyDownListener);
}

return () => {
document.removeEventListener('keydown', handleDocumentKeyDownListener);
};
}, [handleDocumentKeyDownListener, visible]);

return (
<Popover
{...props}
visible={visible}
arrowPointAtCenter
placement={placement}
onVisibleChange={handleOnVisibleChange}
Expand Down