feat: Popover can be closed by ESC when trigger is focus or click (#47928)

* feat:Add keydown event to handle escape

* fix

* fix:use exact same logic with popconfirm

* fix:move same logic from popconfirm to popover

* fix

* fix

* add test

* fix

* fix

* fix

* test size-limit

* fix

* fix popconfirm test

* fix

* fix

---------

Co-authored-by: afc163 <afc163@gmail.com>
This commit is contained in:
Cooper 2024-03-22 10:33:56 +08:00 committed by GitHub
parent 0b00db48bd
commit ca6c932b39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 19 deletions

View File

@ -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';
@ -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);
@ -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<

View File

@ -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(
@ -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);
});
});

View File

@ -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 {
@ -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,
overlayStyle = {},
...otherProps
} = props;
@ -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
@ -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>,
);
}) as React.ForwardRefExoticComponent<
React.PropsWithoutRef<PopoverProps> & React.RefAttributes<unknown>