Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
"@rc-component/portal": "^2.1.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
Expand Down
4 changes: 4 additions & 0 deletions src/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Mask from './Mask';
import PopupContent from './PopupContent';
import useOffsetStyle from '../hooks/useOffsetStyle';
import { useEvent } from '@rc-component/util';
import type { PortalProps } from '@rc-component/portal';

export interface MobileConfig {
mask?: boolean;
Expand All @@ -24,6 +25,7 @@ export interface MobileConfig {
}

export interface PopupProps {
onEsc?: PortalProps['onEsc'];
prefixCls: string;
className?: string;
style?: React.CSSProperties;
Expand Down Expand Up @@ -87,6 +89,7 @@ export interface PopupProps {

const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
const {
onEsc,
popup,
className,
prefixCls,
Expand Down Expand Up @@ -234,6 +237,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
open={forceRender || isNodeVisible}
getContainer={getPopupContainer && (() => getPopupContainer(target))}
autoDestroy={autoDestroy}
onEsc={onEsc}
>
<Mask
prefixCls={prefixCls}
Expand Down
12 changes: 11 additions & 1 deletion src/UniqueProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import Portal from '@rc-component/portal';
import Portal, { type PortalProps } from '@rc-component/portal';
import TriggerContext, {
UniqueContext,
type UniqueContextProps,
Expand All @@ -18,13 +18,15 @@ import { getAlignPopupClassName } from '../util';

export interface UniqueProviderProps {
children: React.ReactNode;
onKeyDown?: (event: KeyboardEvent) => void;
/** Additional handle options data to do the customize info */
postTriggerProps?: (options: UniqueShowOptions) => UniqueShowOptions;
}

const UniqueProvider = ({
children,
postTriggerProps,
onKeyDown,
}: UniqueProviderProps) => {
const [trigger, open, options, onTargetVisibleChanged] = useTargetState();

Expand Down Expand Up @@ -91,6 +93,13 @@ const UniqueProvider = ({
onTargetVisibleChanged(visible);
});

const onEsc: PortalProps['onEsc'] = ({ top, event }) => {
Copy link
Member

Choose a reason for hiding this comment

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

这个 ESC 应该是不需要 UniqueProvider 关注的,无论与没有套这个 Provider 应该都可以通过 ESC 来关闭的

Copy link
Contributor Author

Choose a reason for hiding this comment

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

移到了Context

if (top) {
trigger(false);
}
onKeyDown?.(event);
};

// =========================== Align ============================
const [
ready,
Expand Down Expand Up @@ -184,6 +193,7 @@ const UniqueProvider = ({
<Popup
ref={setPopupRef}
portal={Portal}
onEsc={onEsc}
prefixCls={prefixCls}
popup={mergedOptions.popup}
className={clsx(
Expand Down
12 changes: 12 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import useAlign from './hooks/useAlign';
import useDelay from './hooks/useDelay';
import useWatch from './hooks/useWatch';
import useWinClick from './hooks/useWinClick';
import type { PortalProps } from '@rc-component/portal';

import type {
ActionType,
AlignType,
Expand Down Expand Up @@ -51,6 +53,7 @@ export interface TriggerRef {
// New version will not wrap popup with `rc-trigger-popup-content` when multiple children

export interface TriggerProps {
onKeyDown?: (event: KeyboardEvent) => void;
children:
| React.ReactElement<any>
| ((info: { open: boolean }) => React.ReactElement<any>);
Expand Down Expand Up @@ -146,6 +149,7 @@ export function generateTrigger(
const {
prefixCls = 'rc-trigger-popup',
children,
onKeyDown,

// Action
action = 'hover',
Expand Down Expand Up @@ -419,6 +423,13 @@ export function generateTrigger(
}, delay);
};

const onEsc: PortalProps['onEsc'] = ({ top, event }) => {
if (top) {
triggerOpen(false);
}
onKeyDown?.(event);
};

// ========================== Motion ============================
const [inMotion, setInMotion] = React.useState(false);

Expand Down Expand Up @@ -830,6 +841,7 @@ export function generateTrigger(
forceRender={forceRender}
autoDestroy={mergedAutoDestroy}
getPopupContainer={getPopupContainer}
onEsc={onEsc}
// Arrow
align={alignInfo}
arrow={innerArrow}
Expand Down
77 changes: 77 additions & 0 deletions tests/basic.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import ReactDOM, { createPortal } from 'react-dom';
import Trigger from '../src';
import { awaitFakeTimer, placementAlignMap } from './util';

jest.mock('@rc-component/util/lib/hooks/useId', () => {
const origin = jest.requireActual('react');
return origin.useId;
});

describe('Trigger.Basic', () => {
beforeAll(() => {
spyElementPrototypes(HTMLElement, {
Expand Down Expand Up @@ -1200,4 +1205,76 @@ describe('Trigger.Basic', () => {
await awaitFakeTimer();
expect(isPopupHidden()).toBeTruthy();
});

describe('keyboard', () => {
it('esc should close popup', async () => {
const { container } = render(
<Trigger action="click" popup={<strong>trigger</strong>}>
<div className="target" />
</Trigger>,
);

trigger(container, '.target');
expect(isPopupHidden()).toBeFalsy();

fireEvent.keyDown(window, { key: 'Escape' });
await awaitFakeTimer();
expect(isPopupHidden()).toBeTruthy();
});

it('non-escape key should not close popup', async () => {
const { container } = render(
<Trigger action="click" popup={<strong>trigger</strong>}>
<div className="target" />
</Trigger>,
);

trigger(container, '.target');
expect(isPopupHidden()).toBeFalsy();

fireEvent.keyDown(window, { key: 'Enter' });
expect(isPopupHidden()).toBeFalsy();
});

it('esc should close nested popup from inside out', async () => {
const NestedPopup = () => (
<Trigger
action="click"
popupClassName="inner-popup"
popup={<div>Inner Content</div>}
>
<button type="button" className="inner-target">
Inner Target
</button>
</Trigger>
);

const { container } = render(
<Trigger
action="click"
popupClassName="outer-popup"
popup={
<div className="outer-popup-content">
<NestedPopup />
</div>
}
>
<div className="outer-target" />
</Trigger>,
);

trigger(container, '.outer-target');
expect(isPopupClassHidden('.outer-popup')).toBeFalsy();

fireEvent.click(document.querySelector('.inner-target'));
expect(isPopupClassHidden('.inner-popup')).toBeFalsy();

fireEvent.keyDown(window, { key: 'Escape' });
expect(isPopupClassHidden('.inner-popup')).toBeTruthy();
expect(isPopupClassHidden('.outer-popup')).toBeFalsy();

fireEvent.keyDown(window, { key: 'Escape' });
expect(isPopupClassHidden('.outer-popup')).toBeTruthy();
});
});
});
17 changes: 17 additions & 0 deletions tests/unique.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,4 +374,21 @@ describe('Trigger.Unique', () => {
// Verify onAlign was called due to target change
expect(mockOnAlign).toHaveBeenCalled();
});

it('esc should close unique popup', async () => {
const { container,baseElement } = render(
<UniqueProvider>
<Trigger action={['click']} popup={<div>Popup</div>} unique>
<div className="target" />
</Trigger>
</UniqueProvider>,
);
fireEvent.click(container.querySelector('.target'));
await awaitFakeTimer();
expect(baseElement.querySelector('.rc-trigger-popup-hidden')).toBeFalsy();

fireEvent.keyDown(window, { key: 'Escape' });
await awaitFakeTimer();
expect(baseElement.querySelector('.rc-trigger-popup-hidden')).toBeTruthy();
});
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["@testing-library/jest-dom", "node"],
"paths": {
"@/*": ["src/*"],
"@@/*": [".dumi/tmp/*"],
Expand Down
Loading