Skip to content

Commit

Permalink
refactor: migrate @rc-component/trigger (#226)
Browse files Browse the repository at this point in the history
* test: migrate rc-dropdown tests

* test: skip tests about offset

* chore: bump rc-trigger

* test: fix part

* test: clean up

* chore: update ci node ver

* chore: update ci node ver

* chore: add jest-environment-jsdom

* fix: handle ref

* chore: code clean

* chore: code clena

* chore: add husky

* test: add test cov

---------

Co-authored-by: 二货机器人 <smith3816@gmail.com>
Co-authored-by: MadCcc <1075746765@qq.com>
  • Loading branch information
3 people authored Apr 20, 2023
1 parent b41847c commit 2562c9b
Show file tree
Hide file tree
Showing 17 changed files with 846 additions and 710 deletions.
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
4 changes: 0 additions & 4 deletions jest.config.js

This file was deleted.

70 changes: 40 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
76 changes: 27 additions & 49 deletions src/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<
Expand Down Expand Up @@ -60,34 +63,33 @@ function Dropdown(props: DropdownProps, ref) {
visible,
trigger = ['hover'],
autoFocus,
overlay,
children,
onVisibleChange,
...otherProps
} = props;

const [triggerVisible, setTriggerVisible] = React.useState<boolean>();
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);
Expand All @@ -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 && <div className={`${prefixCls}-arrow`} />}
{overlayElement}
</>
);
};
const getMenuElement = () => <Overlay ref={overlayRef} overlay={overlay} prefixCls={prefixCls} arrow={arrow} />

const getMenuElementOrLambda = () => {
const { overlay } = props;
if (typeof overlay === 'function') {
return getMenuElement;
}
Expand All @@ -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<HTMLElement>}).ref) : undefined,
})

let triggerHideAction = hideAction;
if (!triggerHideAction && trigger.indexOf('contextMenu') !== -1) {
Expand All @@ -169,19 +147,19 @@ function Dropdown(props: DropdownProps, ref) {
popupStyle={overlayStyle}
action={trigger}
showAction={showAction}
hideAction={triggerHideAction || []}
hideAction={triggerHideAction}
popupPlacement={placement}
popupAlign={align}
popupTransitionName={transitionName}
popupAnimation={animation}
popupVisible={mergedVisible}
stretch={getMinOverlayWidthMatchTrigger() ? 'minWidth' : ''}
popup={getMenuElementOrLambda()}
onPopupVisibleChange={onVisibleChange}
onPopupVisibleChange={handleVisibleChange}
onPopupClick={onClick}
getPopupContainer={getPopupContainer}
>
{renderChildren()}
{childrenNode}
</Trigger>
);
}
Expand Down
30 changes: 30 additions & 0 deletions src/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -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<DropdownProps, 'overlay' | 'arrow' | 'prefixCls'>

const Overlay = forwardRef<HTMLElement, OverlayProps>((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<HTMLElement>})?.ref);

return (
<>
{arrow && <div className={`${prefixCls}-arrow`} />}
{React.cloneElement(overlayNode, { ref: supportRef(overlayNode) ? composedRef : undefined })}
</>
)
});

export default Overlay;
31 changes: 12 additions & 19 deletions src/hooks/useAccessibility.ts
Original file line number Diff line number Diff line change
@@ -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<any>;
onVisibleChange?: (visible: boolean) => void;
autoFocus?: boolean;
overlayRef?: React.RefObject<any>;
}

export default function useAccessibility({
visible,
setTriggerVisible,
triggerRef,
onVisibleChange,
autoFocus,
overlayRef,
}: UseAccessibilityProps) {
const focusMenuRef = React.useRef<boolean>(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;
}
Expand Down Expand Up @@ -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;
};
}
Expand Down
3 changes: 3 additions & 0 deletions tests/__mocks__/@rc-component/trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Trigger from '@rc-component/trigger/lib/mock';

export default Trigger;
3 changes: 0 additions & 3 deletions tests/__mocks__/rc-trigger.js

This file was deleted.

Loading

0 comments on commit 2562c9b

Please sign in to comment.