Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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 }) => {
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) {
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);
}
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
93 changes: 93 additions & 0 deletions tests/basic.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1200,4 +1200,97 @@ describe('Trigger.Basic', () => {
await awaitFakeTimer();
expect(isPopupHidden()).toBeTruthy();
});

describe('keyboard', () => {
const useIdModule = require('@rc-component/util/lib/hooks/useId');
let useIdSpy;
let uuid = 0;

beforeEach(() => {
useIdSpy = jest.spyOn(useIdModule, 'default').mockImplementation(() => {
const idRef = React.useRef();

if (!idRef.current) {
uuid += 1;
idRef.current = `test-id-${uuid}`;
}

return idRef.current;
});
});

afterEach(() => {
useIdSpy.mockRestore();
});

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