Skip to content
Open
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
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.1",
"@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
1 change: 1 addition & 0 deletions src/UniqueProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ const UniqueProvider = ({
<Popup
ref={setPopupRef}
portal={Portal}
onEsc={mergedOptions.onEsc}
prefixCls={prefixCls}
popup={mergedOptions.popup}
className={clsx(
Expand Down
2 changes: 2 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { PortalProps } from '@rc-component/portal';
import type { TriggerProps } from './index';
import type { AlignType, ArrowTypeOuter, BuildInPlacements } from './interface';

Expand Down Expand Up @@ -34,6 +35,7 @@ export interface UniqueShowOptions {
arrow?: ArrowTypeOuter;
getPopupContainer?: TriggerProps['getPopupContainer'];
getPopupClassNameFromAlign?: (align: AlignType) => string;
onEsc?: PortalProps['onEsc'];
}

export interface UniqueContextProps {
Expand Down
10 changes: 10 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 @@ -347,6 +349,7 @@ export function generateTrigger(
getPopupContainer,
getPopupClassNameFromAlign,
id,
onEsc,
}));

// Handle controlled state changes for UniqueProvider
Expand Down Expand Up @@ -419,6 +422,12 @@ export function generateTrigger(
}, delay);
};

function onEsc({ top }: Parameters<PortalProps['onEsc']>[0]) {
if (top) {
triggerOpen(false);
}
}

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

Expand Down Expand Up @@ -830,6 +839,7 @@ export function generateTrigger(
forceRender={forceRender}
autoDestroy={mergedAutoDestroy}
getPopupContainer={getPopupContainer}
onEsc={onEsc}
// Arrow
align={alignInfo}
arrow={innerArrow}
Expand Down
76 changes: 76 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,75 @@ 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' });
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();
});
});
});
15 changes: 15 additions & 0 deletions tests/unique.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,4 +374,19 @@ 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'));
expect(baseElement.querySelector('.rc-trigger-popup-hidden')).toBeFalsy();

fireEvent.keyDown(window, { key: 'Escape' });
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