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: Popover can be closed by ESC when trigger is focus or click #47928

Merged
merged 19 commits into from Mar 22, 2024
Merged
24 changes: 6 additions & 18 deletions components/popconfirm/index.tsx
Expand Up @@ -2,11 +2,9 @@ import * as React from 'react';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import KeyCode from 'rc-util/lib/KeyCode';
import omit from 'rc-util/lib/omit';

import type { RenderFunction } from '../_util/getRenderPropValue';
import { cloneElement } from '../_util/reactNode';
import type { ButtonProps, LegacyButtonType } from '../button/button';
import { ConfigContext } from '../config-provider';
import Popover from '../popover';
Expand Down Expand Up @@ -78,18 +76,15 @@ const Popconfirm = React.forwardRef<TooltipRef, PopconfirmProps>((props, ref) =>
props.onCancel?.call(this, e);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.keyCode === KeyCode.ESC && open) {
settingOpen(false, e);
}
};

const onInternalOpenChange = (value: boolean) => {
const onInternalOpenChange = (
value: boolean,
e?: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLDivElement>,
) => {
const { disabled = false } = props;
if (disabled) {
return;
}
settingOpen(value);
settingOpen(value, e);
};

const prefixCls = getPrefixCls('popconfirm', customizePrefixCls);
Expand Down Expand Up @@ -119,14 +114,7 @@ const Popconfirm = React.forwardRef<TooltipRef, PopconfirmProps>((props, ref) =>
}
data-popover-inject
>
{cloneElement(children, {
onKeyDown: (e: React.KeyboardEvent<any>) => {
if (React.isValidElement(children)) {
children?.props.onKeyDown?.(e);
}
onKeyDown(e);
},
})}
{children}
</Popover>,
);
}) as React.ForwardRefExoticComponent<
Expand Down
21 changes: 21 additions & 0 deletions components/popover/__tests__/index.test.tsx
Expand Up @@ -11,6 +11,11 @@ const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanelDoNotUseOrYouWillBe
describe('Popover', () => {
mountTest(Popover);

const eventObject = expect.objectContaining({
target: expect.anything(),
preventDefault: expect.any(Function),
});

it('should show overlay when trigger is clicked', () => {
const ref = React.createRef<TooltipRef>();
const { container } = render(
Expand Down Expand Up @@ -94,4 +99,20 @@ describe('Popover', () => {
render(<InternalPanelDoNotUseOrYouWillBeFired content={null} title={null} trigger="click" />);
}).not.toThrow();
});

it('should be closed by pressing ESC', () => {
const onOpenChange = jest.fn((_, e) => {
e?.persist?.();
});
const wrapper = render(
<Popover title="Title" trigger="click" onOpenChange={onOpenChange}>
<span>Delete</span>
</Popover>,
);
const triggerNode = wrapper.container.querySelectorAll('span')[0];
fireEvent.click(triggerNode);
expect(onOpenChange).toHaveBeenLastCalledWith(true, undefined);
fireEvent.keyDown(triggerNode, { key: 'Escape', keyCode: 27 });
expect(onOpenChange).toHaveBeenLastCalledWith(false, eventObject);
});
});
44 changes: 43 additions & 1 deletion components/popover/index.tsx
Expand Up @@ -11,9 +11,17 @@ import PurePanel from './PurePanel';
// CSSINJS
import useStyle from './style';

import KeyCode from 'rc-util/lib/KeyCode';
import { cloneElement } from '../_util/reactNode';
import useMergedState from 'rc-util/lib/hooks/useMergedState';

export interface PopoverProps extends AbstractTooltipProps {
title?: React.ReactNode | RenderFunction;
content?: React.ReactNode | RenderFunction;
onOpenChange?: (
open: boolean,
e?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
}

interface OverlayProps {
Expand All @@ -37,8 +45,10 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
overlayClassName,
placement = 'top',
trigger = 'hover',
children,
mouseEnterDelay = 0.1,
mouseLeaveDelay = 0.1,
onOpenChange,
CooperHash marked this conversation as resolved.
Show resolved Hide resolved
overlayStyle = {},
...otherProps
} = props;
Expand All @@ -49,6 +59,27 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
const rootPrefixCls = getPrefixCls();

const overlayCls = classNames(overlayClassName, hashId, cssVarCls);
const [open, setOpen] = useMergedState(false, {
value: props.open ?? props.visible,
});

const settingOpen = (
value: boolean,
e?: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLDivElement>,
) => {
setOpen(value, true);
onOpenChange?.(value, e);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.keyCode === KeyCode.ESC) {
settingOpen(false, e);
}
};

const onInternalOpenChange = (value: boolean) => {
settingOpen(value);
};

return wrapCSSVar(
<Tooltip
Expand All @@ -61,12 +92,23 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
prefixCls={prefixCls}
overlayClassName={overlayCls}
ref={ref}
open={open}
onOpenChange={onInternalOpenChange}
overlay={
title || content ? <Overlay prefixCls={prefixCls} title={title} content={content} /> : null
}
transitionName={getTransitionName(rootPrefixCls, 'zoom-big', otherProps.transitionName)}
data-popover-inject
/>,
>
{cloneElement(children, {
onKeyDown: (e: React.KeyboardEvent<any>) => {
if (React.isValidElement(children)) {
children?.props.onKeyDown?.(e);
}
onKeyDown(e);
},
})}
</Tooltip>,
CooperHash marked this conversation as resolved.
Show resolved Hide resolved
);
}) as React.ForwardRefExoticComponent<
React.PropsWithoutRef<PopoverProps> & React.RefAttributes<unknown>
Expand Down