Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
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) {
Copy link
Member

Choose a reason for hiding this comment

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

还需要判断一下是否是 click 触发的,如果是 hover 触发的 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.

这里我理解是不对的,Esc应该始终能够触发Tooltip关闭。这也是无障碍规范里提到过的,然后就是我观察了其它主流的a11y友好的组件库中,Tooltip的行为也都是支持hover后Esc关闭的

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