Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate @rc-component/trigger #226

Merged
merged 13 commits into from
Apr 20, 2023
Merged
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
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