diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..d24fdfc
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npx lint-staged
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index 2ddb77c..0000000
--- a/jest.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- setupFiles: ["./tests/setup.js"],
- snapshotSerializers: [require.resolve("enzyme-to-json/serializer")]
-};
diff --git a/package.json b/package.json
index 73f9703..679c931 100644
--- a/package.json
+++ b/package.json
@@ -7,65 +7,75 @@
"react-dropdown"
],
"homepage": "http://github.com/react-component/dropdown",
- "maintainers": [
- "yiminghe@gmail.com",
- "hualei5280@gmail.com"
- ],
+ "bugs": {
+ "url": "http://github.com/react-component/dropdown/issues"
+ },
"repository": {
"type": "git",
"url": "git@github.com:react-component/dropdown.git"
},
- "bugs": {
- "url": "http://github.com/react-component/dropdown/issues"
- },
+ "license": "MIT",
+ "maintainers": [
+ "yiminghe@gmail.com",
+ "hualei5280@gmail.com"
+ ],
+ "main": "lib/index",
+ "module": "./es/index",
"files": [
"lib",
"es",
"assets/*.css"
],
- "main": "lib/index",
- "module": "./es/index",
- "license": "MIT",
"scripts": {
- "start": "dumi dev",
"build": "dumi build",
"compile": "father build && lessc assets/index.less assets/index.css",
- "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish",
+ "coverage": "rc-test --coverage",
"lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
- "test": "father test",
- "coverage": "father test --coverage",
- "now-build": "npm run build"
+ "now-build": "npm run build",
+ "prepare": "husky install",
+ "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish",
+ "start": "dumi dev",
+ "test": "rc-test"
+ },
+ "lint-staged": {
+ "**/*.{js,jsx,tsx,ts,md,json}": [
+ "prettier --write",
+ "git add"
+ ]
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@rc-component/trigger": "^1.7.0",
+ "classnames": "^2.2.6",
+ "rc-util": "^5.17.0"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^14.0.0",
"@types/classnames": "^2.2.6",
- "@types/enzyme": "^3.1.15",
- "@types/jest": "^26.0.12",
- "@types/react": "^16.8.19",
- "@types/react-dom": "^16.8.4",
+ "@types/jest": "^29.0.0",
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
"@types/warning": "^3.0.0",
"cross-env": "^7.0.0",
"dumi": "^1.1.38",
- "enzyme": "^3.3.0",
- "enzyme-adapter-react-16": "^1.0.2",
- "enzyme-to-json": "^3.4.0",
"father": "^2.13.2",
+ "husky": "^8.0.3",
+ "jest-environment-jsdom": "^29.5.0",
"jquery": "^3.3.1",
"less": "^3.11.1",
+ "lint-staged": "^13.2.1",
"np": "^6.0.0",
+ "prettier": "^2.8.7",
"rc-menu": "^9.5.2",
- "react": "^16.11.0",
- "react-dom": "^16.11.0",
+ "rc-test": "^7.0.14",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
"regenerator-runtime": "^0.13.9",
"typescript": "^4.0.2"
},
"peerDependencies": {
"react": ">=16.11.0",
"react-dom": ">=16.11.0"
- },
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "classnames": "^2.2.6",
- "rc-trigger": "^5.3.1",
- "rc-util": "^5.17.0"
}
}
diff --git a/src/Dropdown.tsx b/src/Dropdown.tsx
index 8853529..58e8709 100644
--- a/src/Dropdown.tsx
+++ b/src/Dropdown.tsx
@@ -1,15 +1,18 @@
import * as React from 'react';
-import Trigger from 'rc-trigger';
-import type { TriggerProps } from 'rc-trigger';
+import Trigger from '@rc-component/trigger';
+import type { TriggerProps } from '@rc-component/trigger';
import classNames from 'classnames';
import type {
AnimationType,
AlignType,
BuildInPlacements,
ActionType,
-} from 'rc-trigger/lib/interface';
+} from '@rc-component/trigger/lib/interface';
import Placements from './placements';
import useAccessibility from './hooks/useAccessibility';
+import Overlay from './Overlay';
+import { composeRef, supportRef } from 'rc-util/lib/ref';
+import { ReactElement } from 'react';
export interface DropdownProps
extends Pick<
@@ -60,6 +63,9 @@ function Dropdown(props: DropdownProps, ref) {
visible,
trigger = ['hover'],
autoFocus,
+ overlay,
+ children,
+ onVisibleChange,
...otherProps
} = props;
@@ -67,27 +73,23 @@ function Dropdown(props: DropdownProps, ref) {
const mergedVisible = 'visible' in props ? visible : triggerVisible;
const triggerRef = React.useRef(null);
+ const overlayRef = React.useRef(null);
+ const childRef = React.useRef(null);
React.useImperativeHandle(ref, () => triggerRef.current);
+ const handleVisibleChange = (newVisible: boolean) => {
+ setTriggerVisible(newVisible);
+ onVisibleChange?.(newVisible);
+ };
+
useAccessibility({
visible: mergedVisible,
- setTriggerVisible,
- triggerRef,
- onVisibleChange: props.onVisibleChange,
+ triggerRef: childRef,
+ onVisibleChange: handleVisibleChange,
autoFocus,
+ overlayRef,
});
- const getOverlayElement = (): React.ReactElement => {
- const { overlay } = props;
- let overlayElement: React.ReactElement;
- if (typeof overlay === 'function') {
- overlayElement = overlay();
- } else {
- overlayElement = overlay;
- }
- return overlayElement;
- };
-
const onClick = (e) => {
const { onOverlayClick } = props;
setTriggerVisible(false);
@@ -97,27 +99,9 @@ function Dropdown(props: DropdownProps, ref) {
}
};
- const onVisibleChange = (newVisible: boolean) => {
- const { onVisibleChange: onVisibleChangeProp } = props;
- setTriggerVisible(newVisible);
- if (typeof onVisibleChangeProp === 'function') {
- onVisibleChangeProp(newVisible);
- }
- };
-
- const getMenuElement = () => {
- const overlayElement = getOverlayElement();
-
- return (
- <>
- {arrow &&
}
- {overlayElement}
- >
- );
- };
+ const getMenuElement = () =>
const getMenuElementOrLambda = () => {
- const { overlay } = props;
if (typeof overlay === 'function') {
return getMenuElement;
}
@@ -141,16 +125,10 @@ function Dropdown(props: DropdownProps, ref) {
return `${prefixCls}-open`;
};
- const renderChildren = () => {
- const { children } = props;
- const childrenProps = children.props ? children.props : {};
- const childClassName = classNames(childrenProps.className, getOpenClassName());
- return mergedVisible && children
- ? React.cloneElement(children, {
- className: childClassName,
- })
- : children;
- };
+ const childrenNode = React.cloneElement(children, {
+ className: classNames(children.props?.className, mergedVisible && getOpenClassName()),
+ ref: supportRef(children) ? composeRef(childRef, (children as ReactElement & {ref: React.Ref}).ref) : undefined,
+ })
let triggerHideAction = hideAction;
if (!triggerHideAction && trigger.indexOf('contextMenu') !== -1) {
@@ -169,7 +147,7 @@ function Dropdown(props: DropdownProps, ref) {
popupStyle={overlayStyle}
action={trigger}
showAction={showAction}
- hideAction={triggerHideAction || []}
+ hideAction={triggerHideAction}
popupPlacement={placement}
popupAlign={align}
popupTransitionName={transitionName}
@@ -177,11 +155,11 @@ function Dropdown(props: DropdownProps, ref) {
popupVisible={mergedVisible}
stretch={getMinOverlayWidthMatchTrigger() ? 'minWidth' : ''}
popup={getMenuElementOrLambda()}
- onPopupVisibleChange={onVisibleChange}
+ onPopupVisibleChange={handleVisibleChange}
onPopupClick={onClick}
getPopupContainer={getPopupContainer}
>
- {renderChildren()}
+ {childrenNode}
);
}
diff --git a/src/Overlay.tsx b/src/Overlay.tsx
new file mode 100644
index 0000000..ee1c539
--- /dev/null
+++ b/src/Overlay.tsx
@@ -0,0 +1,30 @@
+import React, { forwardRef, ReactElement, useMemo } from 'react';
+import type { DropdownProps } from './Dropdown';
+import { composeRef, supportRef } from 'rc-util/lib/ref';
+
+export type OverlayProps = Pick
+
+const Overlay = forwardRef((props, ref) => {
+ const {overlay, arrow, prefixCls} = props;
+
+ const overlayNode = useMemo(() => {
+ let overlayElement: React.ReactElement;
+ if (typeof overlay === 'function') {
+ overlayElement = overlay();
+ } else {
+ overlayElement = overlay;
+ }
+ return overlayElement;
+ }, [overlay]);
+
+ const composedRef = composeRef(ref, (overlayNode as ReactElement & {ref: React.Ref})?.ref);
+
+ return (
+ <>
+ {arrow && }
+ {React.cloneElement(overlayNode, { ref: supportRef(overlayNode) ? composedRef : undefined })}
+ >
+ )
+});
+
+export default Overlay;
\ No newline at end of file
diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts
index cc01deb..8d2850d 100644
--- a/src/hooks/useAccessibility.ts
+++ b/src/hooks/useAccessibility.ts
@@ -1,43 +1,36 @@
-import * as React from 'react';
-import KeyCode from 'rc-util/lib/KeyCode';
-import raf from 'rc-util/lib/raf';
-import { getFocusNodeList } from 'rc-util/lib/Dom/focus';
+import KeyCode from "rc-util/lib/KeyCode";
+import raf from "rc-util/lib/raf";
+import * as React from "react";
const { ESC, TAB } = KeyCode;
interface UseAccessibilityProps {
visible: boolean;
- setTriggerVisible: (visible: boolean) => void;
triggerRef: React.RefObject;
onVisibleChange?: (visible: boolean) => void;
autoFocus?: boolean;
+ overlayRef?: React.RefObject;
}
export default function useAccessibility({
visible,
- setTriggerVisible,
triggerRef,
onVisibleChange,
autoFocus,
+ overlayRef,
}: UseAccessibilityProps) {
const focusMenuRef = React.useRef(false);
const handleCloseMenuAndReturnFocus = () => {
- if (visible && triggerRef.current) {
- triggerRef.current?.triggerRef?.current?.focus?.();
- setTriggerVisible(false);
- if (typeof onVisibleChange === 'function') {
- onVisibleChange(false);
- }
+ if (visible) {
+ triggerRef.current?.focus?.();
+ onVisibleChange?.(false);
}
};
const focusMenu = () => {
- const elements = getFocusNodeList(triggerRef.current?.popupRef?.current?.getElement?.());
- const firstElement = elements[0];
-
- if (firstElement?.focus) {
- firstElement.focus();
+ if (overlayRef.current?.focus) {
+ overlayRef.current.focus();
focusMenuRef.current = true;
return true;
}
@@ -67,13 +60,13 @@ export default function useAccessibility({
React.useEffect(() => {
if (visible) {
- window.addEventListener('keydown', handleKeyDown);
+ window.addEventListener("keydown", handleKeyDown);
if (autoFocus) {
// FIXME: hack with raf
raf(focusMenu, 3);
}
return () => {
- window.removeEventListener('keydown', handleKeyDown);
+ window.removeEventListener("keydown", handleKeyDown);
focusMenuRef.current = false;
};
}
diff --git a/tests/__mocks__/@rc-component/trigger.tsx b/tests/__mocks__/@rc-component/trigger.tsx
new file mode 100644
index 0000000..887c966
--- /dev/null
+++ b/tests/__mocks__/@rc-component/trigger.tsx
@@ -0,0 +1,3 @@
+import Trigger from '@rc-component/trigger/lib/mock';
+
+export default Trigger;
diff --git a/tests/__mocks__/rc-trigger.js b/tests/__mocks__/rc-trigger.js
deleted file mode 100644
index 3230307..0000000
--- a/tests/__mocks__/rc-trigger.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Trigger from 'rc-trigger/lib/mock';
-
-export default Trigger;
diff --git a/tests/__snapshots__/basic.test.js.snap b/tests/__snapshots__/basic.test.js.snap
deleted file mode 100644
index 140945b..0000000
--- a/tests/__snapshots__/basic.test.js.snap
+++ /dev/null
@@ -1,53 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`dropdown simply works 1`] = `
-Array [
- ,
- ,
-]
-`;
diff --git a/tests/__snapshots__/basic.test.tsx.snap b/tests/__snapshots__/basic.test.tsx.snap
new file mode 100644
index 0000000..14c07ac
--- /dev/null
+++ b/tests/__snapshots__/basic.test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`dropdown simply works 1`] = `
+
+`;
diff --git a/tests/basic.test.js b/tests/basic.test.js
deleted file mode 100644
index c0fdc72..0000000
--- a/tests/basic.test.js
+++ /dev/null
@@ -1,475 +0,0 @@
-/* eslint-disable react/button-has-type,react/no-find-dom-node,react/no-render-return-value,object-shorthand,func-names,max-len */
-import React, { createRef, forwardRef, useImperativeHandle } from 'react';
-import { mount } from 'enzyme';
-import Menu, { Divider, Item as MenuItem } from 'rc-menu';
-import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
-import { getPopupDomNode, sleep } from './utils';
-import Dropdown from '../src';
-import placements from '../src/placements';
-import '../assets/index.less';
-
-spyElementPrototypes(HTMLElement, {
- offsetParent: {
- get: () => document.body,
- },
- offsetLeft: {
- get: function () {
- return parseFloat(window.getComputedStyle(this).marginLeft) || 0;
- },
- },
- offsetTop: {
- get: function () {
- return parseFloat(window.getComputedStyle(this).marginTop) || 0;
- },
- },
- offsetHeight: {
- get: function () {
- return parseFloat(window.getComputedStyle(this).height) || 0;
- },
- },
- offsetWidth: {
- get: function () {
- return parseFloat(window.getComputedStyle(this).width) || 0;
- },
- },
-});
-
-describe('dropdown', () => {
- it('default visible', () => {
- const dropdown = mount(
- Test} visible>
-
- ,
- );
- expect(getPopupDomNode(dropdown) instanceof HTMLDivElement).toBeTruthy();
- expect(dropdown.find('.my-button').hasClass('rc-dropdown-open')).toBe(true);
- });
-
- it('supports constrolled visible prop', () => {
- const onVisibleChange = jest.fn();
- const dropdown = mount(
- Test}
- visible
- trigger={['click']}
- onVisibleChange={onVisibleChange}
- >
-
- ,
- );
- expect(getPopupDomNode(dropdown) instanceof HTMLDivElement).toBeTruthy();
- expect(dropdown.find('.my-button').hasClass('rc-dropdown-open')).toBe(true);
-
- dropdown.find('.my-button').simulate('click');
- expect(onVisibleChange).toHaveBeenCalledWith(false);
- });
-
- it('simply works', async () => {
- let clicked;
-
- function onClick({ key }) {
- clicked = key;
- }
-
- const onOverlayClick = jest.fn();
-
- const menu = (
-
- );
- const dropdown = mount(
-
-
- ,
- );
- expect(dropdown.find('.my-button')).toBeTruthy();
- expect(dropdown.find('.rc-dropdown')).toBeTruthy();
-
- dropdown.find('.my-button').simulate('click');
- expect(clicked).toBeUndefined();
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
- expect(dropdown.render()).toMatchSnapshot();
-
- dropdown.find('.my-menuitem').simulate('click');
- expect(clicked).toBe('1');
- expect(onOverlayClick).toHaveBeenCalled();
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(true);
- });
-
- it('re-align works', async () => {
- const buttonStyle = { width: 600, height: 20, marginLeft: 100 };
- const menu = (
-
- );
- const dropdown = mount(
-
-
- ,
- );
-
- dropdown.find('.my-btn').simulate('click');
- await sleep(500);
- expect(getPopupDomNode(dropdown).getAttribute('style')).toEqual(
- expect.stringContaining(
- `left: -${999 - buttonStyle.width - placements.bottomLeft.offset[0]}px; top: -${
- 999 - buttonStyle.height - placements.bottomLeft.offset[1]
- }px;`,
- ),
- );
- });
-
- // https://github.com/ant-design/ant-design/issues/9559
- it('should have correct menu width when switch from shorter menu to longer', async () => {
- class Example extends React.Component {
- state = { longList: true };
-
- getPopupDomNode() {
- return this.trigger.getPopupDomNode();
- }
-
- short = () => {
- this.setState({ longList: false });
- };
-
- long = () => {
- this.setState({ longList: true });
- };
-
- render() {
- const menuItems = [
- ,
- ,
- ];
- if (this.state.longList) {
- menuItems.push();
- }
- return (
- {
- this.trigger = node;
- }}
- overlay={}
- >
-
-
- );
- }
- }
- const dropdown = mount();
- dropdown.find('button').simulate('click');
- await sleep(500);
- expect(getPopupDomNode(dropdown).getAttribute('style')).toEqual(
- expect.stringContaining(
- `left: -${999 - placements.bottomLeft.offset[0]}px; top: -${
- 999 - placements.bottomLeft.offset[1]
- }px;`,
- ),
- );
-
- // Todo - offsetwidth
- });
-
- it('Test default minOverlayWidthMatchTrigger', async () => {
- const overlayWidth = 50;
- const overlay = Test
;
-
- const dropdown = mount(
-
-
- ,
- );
-
- dropdown.find('.my-button').simulate('click');
- await sleep(500);
- expect(getPopupDomNode(dropdown).getAttribute('style')).toEqual(
- expect.stringContaining('min-width: 100px'),
- );
- });
-
- it('user pass minOverlayWidthMatchTrigger', async () => {
- const overlayWidth = 50;
- const overlay = Test
;
-
- const dropdown = mount(
-
-
- ,
- );
-
- dropdown.find('.my-button').simulate('click');
- await sleep(500);
- expect(getPopupDomNode(dropdown).getAttribute('style')).not.toEqual(
- expect.stringContaining('min-width: 100px'),
- );
- });
-
- it('should support default openClassName', () => {
- const overlay = Test
;
- const dropdown = mount(
-
-
- ,
- );
- dropdown.find('.my-button').simulate('click');
- expect(dropdown.find('.my-button').prop('className')).toBe('my-button rc-dropdown-open');
- dropdown.find('.my-button').simulate('click');
- expect(dropdown.find('.my-button').prop('className')).toBe('my-button');
- });
-
- it('should support custom openClassName', async () => {
- const overlay = Test
;
- const dropdown = mount(
-
-
- ,
- );
-
- dropdown.find('.my-button').simulate('click');
- expect(dropdown.find('.my-button').prop('className')).toBe('my-button opened');
- dropdown.find('.my-button').simulate('click');
- expect(dropdown.find('.my-button').prop('className')).toBe('my-button');
- });
-
- it('overlay callback', async () => {
- const overlay = Test
;
- const dropdown = mount(
- overlay}>
-
- ,
- );
-
- dropdown.find('.my-button').simulate('click');
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
- });
-
- it('should support arrow', async () => {
- const overlay = Test
;
- const dropdown = mount(
-
-
- ,
- );
-
- dropdown.find('.my-button').simulate('click');
- await sleep(500);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-show-arrow')).toBe(true);
- expect(
- getPopupDomNode(dropdown).firstElementChild.classList.contains('rc-dropdown-arrow'),
- ).toBe(true);
- });
-
- it('Keyboard navigation works', async () => {
- const overlay = (
-
- );
- const dropdown = mount(
-
-
- ,
- );
- const trigger = dropdown.find('.my-button');
-
- // Open menu
- trigger.simulate('click');
- await sleep(200);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
-
- // Close menu with Esc
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 })); // Esc
- await sleep(200);
- expect(document.activeElement.className).toContain('my-button');
-
- // Open menu
- trigger.simulate('click');
- await sleep(200);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
-
- // Focus menu with Tab
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab
- expect(document.activeElement.className).toContain('menu');
-
- // Close menu with Tab
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab
- await sleep(200);
- expect(document.activeElement.className).toContain('my-button');
- });
-
- it('keyboard should work if menu is wrapped', async () => {
- const overlay = (
-
-
-
- );
- const dropdown = mount(
-
-
- ,
- );
- const trigger = dropdown.find('.my-button');
-
- // Open menu
- trigger.simulate('click');
- await sleep(200);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
-
- // Close menu with Esc
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 })); // Esc
- await sleep(200);
- expect(document.activeElement.className).toContain('my-button');
-
- // Open menu
- trigger.simulate('click');
- await sleep(200);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
-
- // Focus menu with Tab
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab
-
- // Close menu with Tab
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab
- await sleep(200);
- expect(document.activeElement.className).toContain('my-button');
- });
-
- it('support Menu expandIcon', async () => {
- const props = {
- overlay: (
- }>
- foo
-
- foo
-
-
- ),
- visible: true,
- getPopupContainer: (node) => node,
- };
-
- const wrapper = mount(
-
-
- ,
- );
- await sleep(500);
- expect(wrapper.find(Dropdown).find('#customExpandIcon').length).toBe(1);
- });
-
- it('should support customized menuRef', async () => {
- const menuRef = createRef();
- const props = {
- overlay: (
-
- ),
- visible: true,
- };
-
- const wrapper = mount(
-
-
- ,
- );
-
- await sleep(500);
- expect(menuRef.current).toBeTruthy();
- });
-
- it('should support trigger which not support focus', async () => {
- jest.useFakeTimers();
- const Button = forwardRef((props, ref) => {
- useImperativeHandle(ref, () => ({
- foo: () => {},
- }));
- return (
-
- );
- });
- const wrapper = mount(
- node}
- overlay={
-
- }
- >
-
- ,
- );
- wrapper.find('button').simulate('click');
- wrapper.find('li').first().simulate('click');
- jest.runAllTimers();
- jest.useRealTimers();
- });
-
- it('should support autoFocus', async () => {
- const overlay = (
-
- );
- const dropdown = mount(
-
-
- ,
- );
- const trigger = dropdown.find('.my-button');
-
- // Open menu
- trigger.simulate('click');
- await sleep(200);
- expect(getPopupDomNode(dropdown).classList.contains('rc-dropdown-hidden')).toBe(false);
- expect(document.activeElement.className).toContain('menu');
-
- // Close menu with Tab
- window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab
- await sleep(200);
- expect(document.activeElement.className).toContain('my-button');
- });
-});
diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx
new file mode 100644
index 0000000..e4d8c36
--- /dev/null
+++ b/tests/basic.test.tsx
@@ -0,0 +1,601 @@
+/* eslint-disable react/button-has-type,react/no-find-dom-node,react/no-render-return-value,object-shorthand,func-names,max-len */
+import { act, fireEvent } from "@testing-library/react";
+import Menu, { Divider, Item as MenuItem, MenuRef } from "rc-menu";
+import { _rs } from "rc-resize-observer";
+import { spyElementPrototypes } from "rc-util/lib/test/domHook";
+import * as React from "react";
+import {
+ createRef,
+ forwardRef,
+ HTMLAttributes,
+ useImperativeHandle,
+} from "react";
+import Dropdown from "../src";
+import { render, sleep } from "./utils";
+
+// Fix prettier rm this
+console.log(!!React);
+
+async function waitForTime() {
+ for (let i = 0; i < 10; i += 1) {
+ await act(async () => {
+ jest.runAllTimers();
+ });
+ }
+}
+
+async function triggerResize(target: Element) {
+ act(() => {
+ _rs([{ target } as ResizeObserverEntry]);
+ });
+
+ await waitForTime();
+}
+
+spyElementPrototypes(HTMLElement, {
+ offsetParent: {
+ get: () => document.body,
+ },
+ offsetLeft: {
+ get: function () {
+ return parseFloat(window.getComputedStyle(this).marginLeft) || 0;
+ },
+ },
+ offsetTop: {
+ get: function () {
+ return parseFloat(window.getComputedStyle(this).marginTop) || 0;
+ },
+ },
+ offsetHeight: {
+ get: function () {
+ return parseFloat(window.getComputedStyle(this).height) || 0;
+ },
+ },
+ offsetWidth: {
+ get: function () {
+ return parseFloat(window.getComputedStyle(this).width) || 0;
+ },
+ },
+
+ getBoundingClientRect: () => ({
+ width: 100,
+ height: 100,
+ }),
+});
+
+describe("dropdown", () => {
+ beforeEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it("default visible", () => {
+ const { container } = render(
+ Test} visible>
+
+
+ );
+ expect(container instanceof HTMLDivElement).toBeTruthy();
+ expect(
+ container
+ .querySelector(".my-button")
+ ?.classList.contains("rc-dropdown-open")
+ ).toBeTruthy();
+ });
+
+ it("supports controlled visible prop", () => {
+ const onVisibleChange = jest.fn();
+ const { container } = render(
+ Test}
+ visible
+ trigger={["click"]}
+ onVisibleChange={onVisibleChange}
+ >
+
+
+ );
+ expect(container instanceof HTMLDivElement).toBeTruthy();
+ expect(
+ container
+ .querySelector(".my-button")
+ ?.classList.contains("rc-dropdown-open")
+ ).toBeTruthy();
+
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(onVisibleChange).toHaveBeenCalledWith(false);
+ });
+
+ it("simply works", async () => {
+ let clicked;
+
+ function onClick({ key }) {
+ clicked = key;
+ }
+
+ const onOverlayClick = jest.fn();
+
+ const menu = (
+
+ );
+ const { container, baseElement } = render(
+
+
+
+ );
+ expect(container.querySelector(".my-button")).toBeTruthy();
+ // should not display until be triggered
+ expect(baseElement.querySelector(".rc-dropdown")).toBeFalsy();
+
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(clicked).toBeUndefined();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+ expect(container).toMatchSnapshot();
+
+ fireEvent.click(baseElement.querySelector(".my-menuitem"));
+ expect(clicked).toBe("1");
+ expect(onOverlayClick).toHaveBeenCalled();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeTruthy();
+ });
+
+ it("re-align works", async () => {
+ jest.useFakeTimers();
+
+ const onPopupAlign = jest.fn();
+
+ const buttonStyle = { width: 600, height: 20, marginLeft: 100 };
+ const menu = (
+
+ );
+ const { container } = render(
+
+
+
+ );
+
+ expect(onPopupAlign).not.toHaveBeenCalled();
+
+ fireEvent.click(container.querySelector(".my-btn"));
+ await waitForTime();
+
+ expect(onPopupAlign).toHaveBeenCalled();
+
+ jest.useRealTimers();
+ });
+
+ it("Test default minOverlayWidthMatchTrigger", async () => {
+ jest.useFakeTimers();
+
+ const overlayWidth = 50;
+ const overlay = Test
;
+
+ const { container, baseElement } = render(
+
+
+
+ );
+
+ await triggerResize(container.querySelector("button"));
+
+ expect(baseElement.querySelector(".rc-dropdown")).toHaveStyle({
+ minWidth: "100px",
+ });
+
+ jest.useRealTimers();
+ });
+
+ it("user pass minOverlayWidthMatchTrigger", async () => {
+ jest.useFakeTimers();
+
+ const overlayWidth = 50;
+ const overlay = Test
;
+
+ const { container, baseElement } = render(
+
+
+
+ );
+
+ await triggerResize(container.querySelector("button"));
+
+ expect(baseElement.querySelector(".rc-dropdown")).not.toHaveStyle({
+ minWidth: "100px",
+ });
+
+ jest.useRealTimers();
+ });
+
+ it("should support default openClassName", () => {
+ const overlay = Test
;
+ const { container } = render(
+
+
+
+ );
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(
+ container
+ .querySelector(".my-button")
+ .classList.contains("rc-dropdown-open")
+ ).toBeTruthy();
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(
+ container
+ .querySelector(".my-button")
+ .classList.contains("rc-dropdown-open")
+ ).toBeFalsy();
+ });
+
+ it("should support custom openClassName", async () => {
+ const overlay = Test
;
+ const { container } = render(
+
+
+
+ );
+
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(
+ container.querySelector(".my-button").classList.contains("opened")
+ ).toBeTruthy();
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(
+ container.querySelector(".my-button").classList.contains("opened")
+ ).toBeFalsy();
+ });
+
+ it("overlay callback", async () => {
+ const overlay = Test
;
+ const { container, baseElement } = render(
+ overlay}>
+
+
+ );
+
+ fireEvent.click(container.querySelector(".my-button"));
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+ });
+
+ it("should support arrow", async () => {
+ const overlay = Test
;
+ const { container, baseElement } = render(
+
+
+
+ );
+
+ fireEvent.click(container.querySelector(".my-button"));
+ await sleep(500);
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-show-arrow")
+ ).toBeTruthy();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .firstElementChild.classList.contains("rc-dropdown-arrow")
+ ).toBeTruthy();
+ });
+
+ it("Keyboard navigation works", async () => {
+ jest.useFakeTimers();
+
+ const overlay = (
+
+ );
+ const { container, baseElement } = render(
+
+
+
+ );
+ const trigger = container.querySelector(".my-button");
+
+ // Open menu;
+ fireEvent.click(trigger);
+ await waitForTime();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Close menu with Esc
+ fireEvent.keyDown(window, { key: "Esc", keyCode: 27 });
+ await waitForTime();
+ expect(document.activeElement.className).toContain("my-button");
+
+ // Open menu
+ fireEvent.click(trigger);
+ await waitForTime();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Focus menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+ expect(document.activeElement.className).toContain("menu");
+
+ // Close menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+ await waitForTime();
+ expect(document.activeElement.className).toContain("my-button");
+
+ jest.useRealTimers();
+ });
+
+ it("Tab should close menu if overlay cannot be focused", async () => {
+ jest.useFakeTimers();
+
+ const Overlay = () => test
;
+ const { container, baseElement } = render(
+ }>
+
+
+ );
+ const trigger = container.querySelector(".my-button");
+
+ // Open menu;
+ fireEvent.click(trigger);
+ await waitForTime();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Close menu with Esc
+ fireEvent.keyDown(window, { key: "Esc", keyCode: 27 });
+ await waitForTime();
+ expect(document.activeElement.className).toContain("my-button");
+
+ // Open menu
+ fireEvent.click(trigger);
+ await waitForTime();
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Close menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+ await waitForTime();
+ expect(document.activeElement.className).toContain("my-button");
+
+ jest.useRealTimers();
+ });
+
+ it("keyboard should work if menu is wrapped", async () => {
+ const overlay = (
+
+
+
+ );
+ const { container, baseElement } = render(
+
+
+
+ );
+ const trigger = container.querySelector(".my-button");
+
+ // Open menu
+ fireEvent.click(trigger);
+ await sleep(200);
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Close menu with Esc
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 27 })); // Esc
+ await sleep(200);
+ expect(document.activeElement.className).toContain("my-button");
+
+ // Open menu
+ fireEvent.click(trigger);
+ await sleep(200);
+ expect(
+ baseElement
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+
+ // Focus menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+
+ // Close menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+ await sleep(200);
+ expect(document.activeElement.className).toContain("my-button");
+ });
+
+ it("support Menu expandIcon", async () => {
+ const props = {
+ overlay: (
+ }>
+ foo
+
+ foo
+
+
+ ),
+ visible: true,
+ getPopupContainer: (node) => node,
+ };
+
+ const { container } = render(
+
+
+
+ );
+ await sleep(500);
+ expect(container.querySelector("#customExpandIcon")).toBeTruthy();
+ });
+
+ it("should support customized menuRef", async () => {
+ const menuRef = createRef();
+ const props = {
+ overlay: (
+
+ ),
+ visible: true,
+ };
+
+ render(
+
+
+
+ );
+
+ await sleep(500);
+ expect(menuRef.current).toBeTruthy();
+ });
+
+ it("should support trigger which not support focus", async () => {
+ jest.useFakeTimers();
+ const Button = forwardRef>(
+ (props, ref) => {
+ useImperativeHandle(ref, () => ({
+ foo: () => {},
+ }));
+ return (
+
+ );
+ }
+ );
+ const { container, baseElement } = render(
+ node}
+ overlay={
+
+ }
+ >
+
+
+ );
+ fireEvent.click(container.querySelector("button"));
+ fireEvent.click(baseElement.querySelectorAll("li")[0]);
+
+ jest.runAllTimers();
+ jest.useRealTimers();
+ });
+
+ it("should support autoFocus", async () => {
+ jest.useFakeTimers();
+
+ const overlay = (
+
+ );
+ const { container } = render(
+
+
+
+ );
+ const trigger = container.querySelector(".my-button");
+
+ // Open menu
+ fireEvent.click(trigger);
+
+ await waitForTime();
+
+ expect(
+ container
+ .querySelector(".rc-dropdown")
+ .classList.contains("rc-dropdown-hidden")
+ ).toBeFalsy();
+ expect(document.activeElement.className).toContain("menu");
+
+ // Close menu with Tab
+ window.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 9 })); // Tab
+
+ await waitForTime();
+
+ expect(document.activeElement.className).toContain("my-button");
+
+ jest.useRealTimers();
+ });
+});
diff --git a/tests/point.test.js b/tests/point.test.js
deleted file mode 100644
index 94ef7e6..0000000
--- a/tests/point.test.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable react/button-has-type,react/no-render-return-value */
-import React from 'react';
-import { mount } from 'enzyme';
-import Dropdown from '../src';
-import placements from '../src/placements';
-import { sleep, getPopupDomNode } from './utils';
-
-describe('point', () => {
- it('click show', async () => {
- const overlay = (
-
- Test
-
- );
-
- const dropdown = mount(
-
-
- ,
- );
-
- const pageStyle = {
- pageX: 9,
- pageY: 3,
- };
-
- dropdown.find('.my-button').simulate('contextmenu', pageStyle);
-
- await sleep(500);
-
- expect(getPopupDomNode(dropdown).getAttribute('style')).toEqual(
- expect.stringContaining(
- `left: -${999 - pageStyle.pageX - placements.bottomLeft.offset[0]}px; top: -${999 -
- pageStyle.pageY -
- placements.bottomLeft.offset[1]}px;`,
- ),
- );
- });
-});
diff --git a/tests/point.test.tsx b/tests/point.test.tsx
new file mode 100644
index 0000000..d7ed13a
--- /dev/null
+++ b/tests/point.test.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable react/button-has-type,react/no-render-return-value */
+import { act, fireEvent } from '@testing-library/react';
+import * as React from 'react';
+import Dropdown from '../src';
+import { render } from './utils';
+
+// Fix prettier rm this
+console.log(!!React);
+
+async function waitForTime() {
+ for (let i = 0; i < 10; i += 1) {
+ await act(async () => {
+ jest.runAllTimers();
+ });
+ }
+}
+
+describe('point', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ jest.useRealTimers();
+ });
+
+ it('click show', async () => {
+ const overlay = (
+
+ Test
+
+ );
+
+ const onPopupAlign = jest.fn();
+
+ const { container } = render(
+
+
+ ,
+ );
+
+ fireEvent.contextMenu(container.querySelector('.my-button'));
+ await waitForTime();
+
+ expect(container.querySelector('.rc-dropdown')).toBeTruthy();
+ });
+});
diff --git a/tests/setup.js b/tests/setup.js
deleted file mode 100644
index b4d522f..0000000
--- a/tests/setup.js
+++ /dev/null
@@ -1,12 +0,0 @@
-global.requestAnimationFrame =
- global.requestAnimationFrame ||
- function requestAnimationFrame(cb) {
- return setTimeout(cb, 0);
- };
-
-require('regenerator-runtime/runtime');
-
-const Enzyme = require('enzyme');
-const Adapter = require('enzyme-adapter-react-16');
-
-Enzyme.configure({ adapter: new Adapter() });
diff --git a/tests/utils.js b/tests/utils.js
index 0ff7b29..66236b7 100644
--- a/tests/utils.js
+++ b/tests/utils.js
@@ -1,15 +1,18 @@
-/* eslint-disable no-param-reassign */
-export function sleep(timeout = 0) {
- return new Promise(resolve => {
- setTimeout(() => {
- resolve();
- }, timeout);
+import { StrictMode } from 'react';
+import { render, act } from '@testing-library/react';
+
+const globalTimeout = global.setTimeout;
+
+export async function sleep(timeout = 0) {
+ await act(async () => {
+ await new Promise((resolve) => {
+ globalTimeout(resolve, timeout);
+ });
});
}
-export function getPopupDomNode(wrapper) {
- return wrapper
- .find('Trigger')
- .instance()
- .getPopupDomNode();
+function customRender(ui, options) {
+ return render(ui, { wrapper: StrictMode, ...options });
}
+
+export { customRender as render };
diff --git a/tsconfig.json b/tsconfig.json
index 929df7b..c6bd6b6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,10 +5,10 @@
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
- "jsx": "react",
+ "jsx": "react-jsx",
"skipLibCheck": true
},
- "include": ["./src", "./typings/"],
+ "include": ["./src", "./tests", "./typings/"],
"typings": "./typings/index.d.ts",
"exclude": [
"node_modules",