diff --git a/.dumirc.ts b/.dumirc.ts new file mode 100644 index 00000000..263f4161 --- /dev/null +++ b/.dumirc.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'dumi'; +import path from 'path'; + +export default defineConfig({ + alias: { + 'rc-drawer$': path.resolve('src'), + 'rc-drawer/es': path.resolve('src'), + }, + mfsu: false, + favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], + themeConfig: { + name: 'Drawer', + logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', + }, +}); diff --git a/.fatherrc.js b/.fatherrc.js index 558df7ee..4ddbafd1 100644 --- a/.fatherrc.js +++ b/.fatherrc.js @@ -1,9 +1,5 @@ -export default { - cjs: 'babel', - esm: { type: 'babel', importLibToEs: true }, - preCommit: { - eslint: true, - prettier: true, - }, - runtimeHelpers: true, -}; \ No newline at end of file +import { defineConfig } from 'father'; + +export default defineConfig({ + plugins: ['@rc-component/father-plugin'], +}); \ No newline at end of file diff --git a/.gitignore b/.gitignore index 316b8cf3..76d4008d 100755 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ storybook .umi-production .umi-test .env.local + +# dumi +.dumi/tmp +.dumi/tmp-production \ No newline at end of file diff --git a/.umirc.ts b/.umirc.ts deleted file mode 100644 index f456278f..00000000 --- a/.umirc.ts +++ /dev/null @@ -1,19 +0,0 @@ -// more config: https://d.umijs.org/config -import { defineConfig } from 'dumi'; - -export default defineConfig({ - title: 'rc-drawer', - favicon: - 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - logo: - 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - outputPath: '.doc', - exportStatic: {}, - styles: [ - ` - .markdown table { - width: auto !important; - } - `, - ] -}); diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE diff --git a/README.md b/README.md index 8c008d3b..8369acba 100755 --- a/README.md +++ b/README.md @@ -44,8 +44,9 @@ ReactDom.render( | props | type | default | description | |------------|----------------|---------|----------------| | className | string | null | - | +| classNames | { mask?: string; wrapper?: string; } | - | pass className to target area | +| styles | { mask?: CSSProperties; wrapper?: CSSProperties; } | - | pass style to target area | | prefixCls | string | 'drawer' | prefix class | -| wrapperClassName | string | null | wrapper class name | | width | string \| number | null | drawer content wrapper width, drawer level transition width | | height | string \| number | null | drawer content wrapper height, drawer level transition height | | open | boolean | false | open or close menu | @@ -59,12 +60,17 @@ ReactDom.render( | showMask | boolean | true | mask is show | | maskClosable | boolean | true | Clicking on the mask (area outside the Drawer) to close the Drawer or not. | | maskStyle | CSSProperties | null | mask style | -| onChange | func | null | change callback(open) | | afterVisibleChange | func | null | transition end callback(open) | | onClose | func | null | close click function | -| keyboard | Boolean | true | Whether support press esc to close | +| keyboard | boolean | true | Whether support press esc to close | | contentWrapperStyle | CSSProperties | null | content wrapper style | -| autoFocus | Boolean | true | Whether focusing on the drawer after it opened | +| autoFocus | boolean | true | Whether focusing on the drawer after it opened | +| onMouseEnter | React.MouseEventHandler\ | - | Trigger when mouse enter drawer panel | +| onMouseOver | React.MouseEventHandler\ | - | Trigger when mouse over drawer panel | +| onMouseLeave | React.MouseEventHandler\ | - | Trigger when mouse leave drawer panel | +| onClick | React.MouseEventHandler\ | - | Trigger when mouse click drawer panel | +| onKeyDown | React.MouseEventHandler\ | - | Trigger when mouse keydown on drawer panel | +| onKeyUp | React.MouseEventHandler\ | - | Trigger when mouse keyup on drawer panel | > 2.0 Rename `onMaskClick` -> `onClose`, add `maskClosable`. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..1d722832 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,3 @@ +# ChangeLog + + diff --git a/docs/demo/base.md b/docs/demo/base.md index edb074a9..9768420a 100644 --- a/docs/demo/base.md +++ b/docs/demo/base.md @@ -1,3 +1,8 @@ -## base +--- +title: base +nav: + title: Demo + path: /demo +--- - + diff --git a/docs/demo/bodyProps.md b/docs/demo/bodyProps.md new file mode 100644 index 00000000..d3125202 --- /dev/null +++ b/docs/demo/bodyProps.md @@ -0,0 +1,8 @@ +--- +title: bodyProps +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/demo/change-remove.md b/docs/demo/change-remove.md index cc07baed..9f3fdab2 100644 --- a/docs/demo/change-remove.md +++ b/docs/demo/change-remove.md @@ -1,3 +1,9 @@ -## change-remove +--- +title: change-remove +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/demo/change.md b/docs/demo/change.md index 215e5f6d..f2c94fde 100644 --- a/docs/demo/change.md +++ b/docs/demo/change.md @@ -1,3 +1,9 @@ -## change +--- +title: change +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/demo/forceRender.md b/docs/demo/forceRender.md index 9f12a059..1b2837a9 100644 --- a/docs/demo/forceRender.md +++ b/docs/demo/forceRender.md @@ -1,3 +1,9 @@ -## Force Render +--- +title: Force Render +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/demo/getContainer.md b/docs/demo/getContainer.md index 64ec2a1b..671e2ee5 100644 --- a/docs/demo/getContainer.md +++ b/docs/demo/getContainer.md @@ -1,4 +1,10 @@ -## getContainer +--- +title: getContainer +nav: + title: Demo + path: /demo +--- - - + + + diff --git a/docs/demo/multiple.md b/docs/demo/multiple.md index 26c79bc2..d6046227 100644 --- a/docs/demo/multiple.md +++ b/docs/demo/multiple.md @@ -1,3 +1,9 @@ -## multiple +--- +title: multiple +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/demo/no-mask.md b/docs/demo/no-mask.md index f7baddf0..37ecff48 100644 --- a/docs/demo/no-mask.md +++ b/docs/demo/no-mask.md @@ -1,3 +1,9 @@ -## no-mask +--- +title: no-mask +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/demo/placement.md b/docs/demo/placement.md index ec4beae0..50278f29 100644 --- a/docs/demo/placement.md +++ b/docs/demo/placement.md @@ -1,3 +1,9 @@ -## placement +--- +title: placement +nav: + title: Demo + path: /demo +--- - + + diff --git a/docs/examples/base.tsx b/docs/examples/base.tsx index c6966a53..1a411b16 100755 --- a/docs/examples/base.tsx +++ b/docs/examples/base.tsx @@ -5,9 +5,6 @@ import motionProps from './motion'; const Demo = () => { const [open, setOpen] = useState(false); - const onChange = (bool: boolean) => { - // console.log('change: ', bool); - }; const onTouchEnd = () => { setOpen(false); }; @@ -17,12 +14,9 @@ const Demo = () => { return (
{ console.log('transitionEnd: ', c); }} diff --git a/docs/examples/bodyProps.tsx b/docs/examples/bodyProps.tsx new file mode 100755 index 00000000..15216937 --- /dev/null +++ b/docs/examples/bodyProps.tsx @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useState } from 'react'; +import Drawer from 'rc-drawer'; +import motionProps from './motion'; + +const Demo = () => { + const [open, setOpen] = useState(false); + const onTouchEnd = () => { + setOpen(false); + }; + const onSwitch = () => { + setOpen(c => !c); + }; + return ( +
+ { + console.log('transitionEnd: ', c); + }} + placement="right" + // width={400} + width="60%" + // Motion + {...motionProps} + onMouseEnter={() => { + console.log('mouseEnter'); + }} + onMouseLeave={() => { + console.log('mouseLeave'); + }} + > + content + +
+ +
+
+ ); +}; +export default Demo; diff --git a/docs/examples/change-remove.tsx b/docs/examples/change-remove.tsx index 4257f1df..5f70d8c1 100755 --- a/docs/examples/change-remove.tsx +++ b/docs/examples/change-remove.tsx @@ -14,97 +14,93 @@ import motionProps from './motion'; const SubMenu = Menu.SubMenu; const MenuItemGroup = Menu.ItemGroup; -class Demo extends React.Component { - public state = { - show: true, - }; - public componentDidMount() { + +function Demo() { + const [show, setShow] = React.useState(true); + React.useEffect(() => { setTimeout(() => { - this.setState({ - show: false, - }); + setShow(false); }, 2000); - } - public render() { - return ( -
- {this.state.show && ( - + {show && ( + + - + + Navigation One + + } > - - - Navigation One - - } - > - - Option 1 - Option 2 - - - Option 3 - Option 4 - - - - - Navigation Two - - } - > - Option 5 - Option 6 - - Option 7 - Option 8 - - - - - Navigation Three - - } - > - Option 9 - Option 10 - Option 11 - Option 12 + + Option 1 + Option 2 + + + Option 3 + Option 4 + + + + + Navigation Two + + } + > + Option 5 + Option 6 + + Option 7 + Option 8 - - - )} -
- 内容区块 -
+ + + + Navigation Three + + } + > + Option 9 + Option 10 + Option 11 + Option 12 + +
+
+ )} +
+ 内容区块
- ); - } +
+ ); } + export default Demo; diff --git a/docs/examples/change.tsx b/docs/examples/change.tsx index 57688169..ec037317 100755 --- a/docs/examples/change.tsx +++ b/docs/examples/change.tsx @@ -17,40 +17,30 @@ import motionProps from './motion'; const SubMenu = Menu.SubMenu; const MenuItemGroup = Menu.ItemGroup; -class Demo extends React.Component { - public state = { - open: true, - }; - public componentDidMount() { + +function Demo() { + const [open, setOpen] = React.useState(true); + + React.useEffect(() => { setTimeout(() => { - this.setState({ - open: false, - }); + setOpen(false); }, 2000); - } - public onChange = (bool: boolean) => { - console.log('change: ', bool); - }; - public onTouchEnd = () => { - this.setState({ - open: false, - }); - }; - public onSwitch = () => { - this.setState({ - open: !this.state.open, - }); + }, []); + + const onTouchEnd = () => { + setOpen(false); }; - public render() { - return ( -
+ + const onSwitch = () => { + setOpen(c => !c); + } + + return ( +
{ + open={open} + onClose={onTouchEnd} + afterOpenChange={(c: boolean) => { console.log('transitionEnd: ', c); }} width="20vw" @@ -124,7 +114,7 @@ class Demo extends React.Component { > 内容区块
- ); - } + ); } + export default Demo; diff --git a/docs/examples/multiple.tsx b/docs/examples/multiple.tsx index 77c0481b..04eab18c 100755 --- a/docs/examples/multiple.tsx +++ b/docs/examples/multiple.tsx @@ -11,83 +11,77 @@ import '../../assets/index.less'; import './assets/index.less'; import motionProps from './motion'; -class Demo extends React.Component { - public state = { - open: true, - openChild: true, - openChildren: true, - }; - public onClick = () => { - this.setState({ - open: !this.state.open, - }); - }; - public onChildClick = () => { - this.setState({ - openChild: !this.state.openChild, - }); - }; - public onChildrenClick = e => { - this.setState({ - openChildren: e.currentTarget instanceof HTMLButtonElement, - }); - }; +function Demo() { + const [open, setOpen] = React.useState(true); + const [openChild, setOpenChild] = React.useState(true); + const [openChildren, setOpenChildren] = React.useState(true); - public render() { - return ( -
-
- -
- -
- - -
- 二级抽屉 - - -
三级抽屉
-
-
-
-
-
-
- ); + function onClick() { + setOpen(!open); + } + + function onChildClick() { + setOpenChild(!openChild); + } + + function onChildrenClick(e) { + setOpenChildren(e.currentTarget instanceof HTMLButtonElement); } + + return ( +
+
+ +
+ +
+ + +
+ 二级抽屉 + + +
三级抽屉
+
+
+
+
+
+
+ ); } + export default Demo; diff --git a/docs/examples/no-mask.tsx b/docs/examples/no-mask.tsx index 8940dd2f..3e6a3c71 100755 --- a/docs/examples/no-mask.tsx +++ b/docs/examples/no-mask.tsx @@ -16,21 +16,16 @@ import './assets/index.less'; const { SubMenu } = Menu; const MenuItemGroup = Menu.ItemGroup; -class Demo extends React.Component { - public state = { - open: false, - }; - public onSwitch = () => { - const { open } = this.state; - this.setState({ - open: !open, - }); - }; +function Demo() { + const [open, setOpen] = React.useState(true); - public render() { - return ( -
+ const onSwitch = () => { + setOpen(!open); + } + + return ( +
内容区块
- ); - } + ); } export default Demo; diff --git a/docs/examples/placement.tsx b/docs/examples/placement.tsx index 14b075d7..7563ae49 100755 --- a/docs/examples/placement.tsx +++ b/docs/examples/placement.tsx @@ -11,41 +11,35 @@ import 'antd/lib/style'; import '../../assets/index.less'; import './assets/index.less'; +import type { Placement } from '@/Drawer'; const SubMenu = Menu.SubMenu; const MenuItemGroup = Menu.ItemGroup; const Option = Select.Option; -class Demo extends React.Component { - public state = { - placement: 'right', - childShow: true, - width: '20vw', - height: null, - }; - public onChange = (value: string) => { - this.setState( - { - placement: value, - width: value === 'right' || value === 'left' ? '20vw' : null, - height: value === 'right' || value === 'left' ? null : '20vh', - childShow: false, // 删除子级,删除切换时的过渡动画。。。 - }, - () => { - this.setState({ - childShow: true, - }); - }, - ); - }; - public render() { - return ( -
- {this.state.childShow && ( +function Demo() { + const [placement, setPlacement] = React.useState('right'); + const [childShow, setChildShow] = React.useState(true); + const [width, setWidth] = React.useState('20vw'); + const [height, setHeight] = React.useState(null); + + const onChange = (value: string) => { + setPlacement(value as Placement); + setWidth(value === 'right' || value === 'left' ? '20vw' : null); + setHeight(value === 'right' || value === 'left' ? null : '20vh'); + setChildShow(false); // 删除子级,删除切换时的过渡动画。。。 + setTimeout(() => { + setChildShow(true); + }); + } + + return ( +
+ {childShow && ( @@ -126,8 +120,7 @@ class Demo extends React.Component {
- ); - } + ); } export default Demo; diff --git a/docs/index.md b/docs/index.md index 595be25f..9c1a609c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,7 @@ --- -title: rc-drawer +hero: + title: rc-drawer + description: React Drawer Component --- diff --git a/now.json b/now.json index 620a430d..715b9413 100644 --- a/now.json +++ b/now.json @@ -6,7 +6,7 @@ { "src": "package.json", "use": "@now/static-build", - "config": { "distDir": ".doc" } + "config": { "distDir": "dist" } } ], "routes": [ diff --git a/package.json b/package.json index b2f41cd3..145e3e33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-drawer", - "version": "6.1.3", + "version": "6.5.2", "description": "drawer component for react", "keywords": [ "react", @@ -15,65 +15,66 @@ "drawer-animation" ], "homepage": "https://github.com/react-component/drawer", - "author": "155259966@qq.com", + "bugs": { + "url": "https://github.com/react-component/drawer/issues" + }, "repository": { "type": "git", "url": "https://github.com/react-component/drawer.git" }, - "bugs": { - "url": "https://github.com/react-component/drawer/issues" - }, + "license": "MIT", + "author": "155259966@qq.com", + "main": "./lib/index", + "module": "./es/index", "files": [ "lib", "assets/*.css", "es" ], - "licenses": "MIT", - "main": "./lib/index", - "module": "./es/index", "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", + "compile": "father build && lessc assets/index.less assets/index.css", "lint": "eslint src/ --ext .tsx,.ts", - "test": "umi-test", - "now-build": "npm run build" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "now-build": "npm run build", + "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", + "start": "dumi dev", + "test": "rc-test" }, "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/portal": "^1.0.0-6", + "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", - "rc-util": "^5.21.2" + "rc-util": "^5.36.0" }, "devDependencies": { "@ant-design/icons": "^4.7.0", + "@rc-component/father-plugin": "^1.0.0", "@testing-library/jest-dom": "^5.11.9", - "@testing-library/react": "^12.1.5", + "@testing-library/react": "^14.0.0", "@types/classnames": "^2.2.9", "@types/jest": "^27.0.2", "@types/raf": "^3.4.0", - "@types/react": "^17.0.9", - "@types/react-dom": "^17.0.6", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "@types/warning": "^3.0.0", - "@umijs/fabric": "^2.0.0", - "@umijs/test": "^3.5.23", "antd": "^4.20.2", - "dumi": "^1.1.40", - "eslint": "^8.35.0", - "father": "^2.30.21", - "father-build": "^1.22.1", + "dumi": "^2.2.0", + "eslint": "^8.56.0", + "eslint-plugin-jest": "^27.6.0", + "eslint-plugin-unicorn": "^50.0.1", + "father": "^4.0.0", "glob": "^7.1.6", "less": "^3.10.3", "np": "^7.5.0", - "prettier": "^2.6.2", - "react": "^16.10.2", - "react-dom": "^16.10.2", + "prettier": "^3.0.0", + "rc-test": "^7.0.9", + "react": "^18.0.0", + "react-dom": "^18.0.0", "typescript": "^4.6.4" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } } diff --git a/src/Drawer.tsx b/src/Drawer.tsx index 8c528b0d..a75b67e1 100644 --- a/src/Drawer.tsx +++ b/src/Drawer.tsx @@ -1,20 +1,27 @@ -import * as React from 'react'; -import Portal from '@rc-component/portal'; import type { PortalProps } from '@rc-component/portal'; +import Portal from '@rc-component/portal'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; -import DrawerPopup from './DrawerPopup'; +import * as React from 'react'; +import { RefContext } from './context'; +import type { DrawerPanelEvents } from './DrawerPanel'; import type { DrawerPopupProps } from './DrawerPopup'; +import DrawerPopup from './DrawerPopup'; import { warnCheck } from './util'; +import type { DrawerClassNames, DrawerStyles } from './inter'; export type Placement = 'left' | 'top' | 'right' | 'bottom'; export interface DrawerProps - extends Omit { + extends Omit, + DrawerPanelEvents { prefixCls?: string; open?: boolean; onClose?: (e: React.MouseEvent | React.KeyboardEvent) => void; destroyOnClose?: boolean; getContainer?: PortalProps['getContainer']; + panelRef?: React.Ref; + classNames?: DrawerClassNames; + styles?: DrawerStyles; } const Drawer: React.FC = props => { @@ -31,6 +38,15 @@ const Drawer: React.FC = props => { forceRender, afterOpenChange, destroyOnClose, + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + + // Refs + panelRef, } = props; const [animatedVisible, setAnimatedVisible] = React.useState(false); @@ -40,15 +56,24 @@ const Drawer: React.FC = props => { warnCheck(props); } + // ============================= Open ============================= + const [mounted, setMounted] = React.useState(false); + + useLayoutEffect(() => { + setMounted(true); + }, []); + + const mergedOpen = mounted ? open : false; + // ============================ Focus ============================= - const panelRef = React.useRef(); + const popupRef = React.useRef(); const lastActiveRef = React.useRef(); useLayoutEffect(() => { - if (open) { + if (mergedOpen) { lastActiveRef.current = document.activeElement as HTMLElement; } - }, [open]); + }, [mergedOpen]); // ============================= Open ============================= const internalAfterOpenChange: DrawerProps['afterOpenChange'] = @@ -59,20 +84,36 @@ const Drawer: React.FC = props => { if ( !nextVisible && lastActiveRef.current && - !panelRef.current?.contains(lastActiveRef.current) + !popupRef.current?.contains(lastActiveRef.current) ) { - lastActiveRef.current?.focus(); + lastActiveRef.current?.focus({ preventScroll: true }); } }; + // =========================== Context ============================ + const refContext = React.useMemo( + () => ({ + panel: panelRef, + }), + [panelRef], + ); + // ============================ Render ============================ - if (!forceRender && !animatedVisible && !open && destroyOnClose) { + if (!forceRender && !animatedVisible && !mergedOpen && destroyOnClose) { return null; } + const eventHandlers = { + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + }; const drawerPopupProps = { ...props, - open, + open: mergedOpen, prefixCls, placement, autoFocus, @@ -82,18 +123,21 @@ const Drawer: React.FC = props => { maskClosable, inline: getContainer === false, afterOpenChange: internalAfterOpenChange, - ref: panelRef, + ref: popupRef, + ...eventHandlers, }; return ( - - - + + + + + ); }; diff --git a/src/DrawerPanel.tsx b/src/DrawerPanel.tsx index db5a3e44..2e19d765 100644 --- a/src/DrawerPanel.tsx +++ b/src/DrawerPanel.tsx @@ -1,33 +1,72 @@ -import * as React from 'react'; import classNames from 'classnames'; +import { useComposeRef } from 'rc-util'; +import * as React from 'react'; +import { RefContext } from './context'; export interface DrawerPanelRef { focus: VoidFunction; } -export interface DrawerPanelProps { +export interface DrawerPanelEvents { + onMouseEnter?: React.MouseEventHandler; + onMouseOver?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + onClick?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyUp?: React.KeyboardEventHandler; +} + +export interface DrawerPanelProps extends DrawerPanelEvents { prefixCls: string; className?: string; + id?: string; style?: React.CSSProperties; children?: React.ReactNode; containerRef?: React.Ref; } const DrawerPanel = (props: DrawerPanelProps) => { - const { prefixCls, className, style, children, containerRef } = props; + const { + prefixCls, + className, + style, + children, + containerRef, + id, + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + } = props; + + const eventHandlers = { + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + }; + + const { panel: panelRef } = React.useContext(RefContext); + const mergedRef = useComposeRef(panelRef, containerRef); // =============================== Render =============================== return ( <> diff --git a/src/DrawerPopup.tsx b/src/DrawerPopup.tsx index a043fff5..bf6d5529 100644 --- a/src/DrawerPopup.tsx +++ b/src/DrawerPopup.tsx @@ -1,12 +1,15 @@ -import * as React from 'react'; import classNames from 'classnames'; -import CSSMotion from 'rc-motion'; import type { CSSMotionProps } from 'rc-motion'; -import DrawerPanel from './DrawerPanel'; -import DrawerContext from './context'; -import type { DrawerContextProps } from './context'; +import CSSMotion from 'rc-motion'; import KeyCode from 'rc-util/lib/KeyCode'; +import pickAttrs from 'rc-util/lib/pickAttrs'; +import * as React from 'react'; +import type { DrawerContextProps } from './context'; +import DrawerContext from './context'; +import type { DrawerPanelEvents } from './DrawerPanel'; +import DrawerPanel from './DrawerPanel'; import { parseWidthHeight } from './util'; +import type { DrawerClassNames, DrawerStyles } from './inter'; const sentinelStyle: React.CSSProperties = { width: 0, @@ -22,7 +25,7 @@ export interface PushConfig { distance?: number | string; } -export interface DrawerPopupProps { +export interface DrawerPopupProps extends DrawerPanelEvents { prefixCls: string; open?: boolean; inline?: boolean; @@ -38,6 +41,7 @@ export interface DrawerPopupProps { // Drawer placement?: Placement; + id?: string; className?: string; style?: React.CSSProperties; children?: React.ReactNode; @@ -60,6 +64,12 @@ export interface DrawerPopupProps { onClose?: ( event: React.MouseEvent | React.KeyboardEvent, ) => void; + + // classNames + classNames?: DrawerClassNames; + + // styles + styles?: DrawerStyles; } function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { @@ -73,6 +83,8 @@ function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { autoFocus, keyboard, + // classNames + classNames: drawerClassNames, // Root rootClassName, rootStyle, @@ -80,6 +92,7 @@ function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { // Drawer className, + id, style, motion, width, @@ -97,6 +110,14 @@ function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { // Events afterOpenChange, onClose, + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + + styles, } = props; // ================================ Refs ================================ @@ -206,11 +227,13 @@ function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { className={classNames( `${prefixCls}-mask`, motionMaskClassName, + drawerClassNames?.mask, maskClassName, )} style={{ ...motionMaskStyle, ...maskStyle, + ...styles?.mask, }} onClick={maskClosable && open ? onClose : undefined} ref={maskRef} @@ -249,6 +272,15 @@ function DrawerPopup(props: DrawerPopupProps, ref: React.Ref) { wrapperStyle.height = parseWidthHeight(height); } + const eventHandlers = { + onMouseEnter, + onMouseOver, + onMouseLeave, + onClick, + onKeyDown, + onKeyUp, + }; + const panelNode: React.ReactNode = ( ) {
{children} diff --git a/src/context.ts b/src/context.ts index 010335ae..1a054089 100644 --- a/src/context.ts +++ b/src/context.ts @@ -8,4 +8,10 @@ export interface DrawerContextProps { const DrawerContext = React.createContext(null); +export interface RefContextProps { + panel?: React.Ref; +} + +export const RefContext = React.createContext({}); + export default DrawerContext; diff --git a/src/inter.ts b/src/inter.ts new file mode 100644 index 00000000..614eddbc --- /dev/null +++ b/src/inter.ts @@ -0,0 +1,11 @@ +export interface DrawerClassNames { + mask?: string; + wrapper?: string; + content?: string; +} + +export interface DrawerStyles { + mask?: React.CSSProperties; + wrapper?: React.CSSProperties; + content?: React.CSSProperties; +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 9f8eb226..b28f88cf 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ import warning from 'rc-util/lib/warning'; +import canUseDom from 'rc-util/lib/Dom/canUseDom'; import type { DrawerProps } from './Drawer'; export function parseWidthHeight(value?: number | string) { @@ -18,4 +19,9 @@ export function warnCheck(props: DrawerProps) { !('wrapperClassName' in props), `'wrapperClassName' is removed. Please use 'rootClassName' instead.`, ); + + warning( + canUseDom() || !props.open, + `Drawer with 'open' in SSR is not work since no place to createPortal. Please move to 'useEffect' instead.`, + ); } diff --git a/tests/index.spec.tsx b/tests/index.spec.tsx index 3532b56c..e4d89c4d 100755 --- a/tests/index.spec.tsx +++ b/tests/index.spec.tsx @@ -61,23 +61,23 @@ describe('rc-drawer-menu', () => { placement: DrawerProps['placement']; transform: string; }[] = [ - { - placement: 'left', - transform: 'translateX(903px)', - }, - { - placement: 'right', - transform: 'translateX(-903px)', - }, - { - placement: 'top', - transform: 'translateY(903px)', - }, - { - placement: 'bottom', - transform: 'translateY(-903px)', - }, - ]; + { + placement: 'left', + transform: 'translateX(903px)', + }, + { + placement: 'right', + transform: 'translateX(-903px)', + }, + { + placement: 'top', + transform: 'translateY(903px)', + }, + { + placement: 'bottom', + transform: 'translateY(-903px)', + }, + ]; placementList.forEach(({ placement, transform }) => { it(placement, () => { @@ -359,4 +359,79 @@ describe('rc-drawer-menu', () => { ); errSpy.mockRestore(); }); + + + it('pass data props to internal div', () => { + const value = 'bamboo'; + const { unmount } = render(); + expect(document.querySelector('.rc-drawer-content-wrapper')).toHaveAttribute('data-attr', value); + unmount(); + }); + + it('support bodyProps', () => { + const enter = jest.fn(); + const leave = jest.fn(); + const { baseElement } = render( + , + ); + fireEvent.mouseOver(baseElement.querySelector('.rc-drawer-content')); + expect(enter).toHaveBeenCalled(); + fireEvent.mouseLeave(baseElement.querySelector('.rc-drawer-content')); + expect(leave).toHaveBeenCalled(); + }); + + it('pass id & className props to Panel', () => { + const { unmount } = render(); + expect( + document.querySelector('.rc-drawer-content') + ).toHaveClass('customer-className'); + expect( + document.querySelector('.rc-drawer-content') + ).toHaveAttribute('id', 'customer-id'); + unmount(); + }); + + it('should support classNames', () => { + const { unmount } = render( + + ); + expect( + document.querySelector('.rc-drawer-content-wrapper') + ).toHaveClass('customer-wrapper'); + expect( + document.querySelector('.rc-drawer-mask') + ).toHaveClass('customer-mask'); + expect( + document.querySelector('.rc-drawer-content') + ).toHaveClass('customer-content'); + unmount(); + }); + it('should support styles', () => { + const { unmount } = render( + + ); + expect( + document.querySelector('.rc-drawer-content-wrapper') + ).toHaveStyle('background: red'); + expect( + document.querySelector('.rc-drawer-mask') + ).toHaveStyle('background: blue'); + expect( + document.querySelector('.rc-drawer-content') + ).toHaveStyle('background: green'); + unmount(); + }); }); diff --git a/tests/ref.spec.tsx b/tests/ref.spec.tsx new file mode 100755 index 00000000..ec1c8cff --- /dev/null +++ b/tests/ref.spec.tsx @@ -0,0 +1,25 @@ +import { act, cleanup, render } from '@testing-library/react'; +import React from 'react'; +import Drawer from '../src'; + +describe('Drawer.ref', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + cleanup(); + }); + + it('support panelRef', () => { + const panelRef = React.createRef(); + render(); + + act(() => { + jest.runAllTimers(); + }); + + expect(panelRef.current).toHaveClass('rc-drawer-content'); + }); +}); diff --git a/tests/ssr.spec.tsx b/tests/ssr.spec.tsx new file mode 100755 index 00000000..af34004a --- /dev/null +++ b/tests/ssr.spec.tsx @@ -0,0 +1,78 @@ +import { render } from '@testing-library/react'; +import { renderToString } from 'react-dom/server'; +import React from 'react'; +import Drawer from '../src'; +// import canUseDom from 'rc-util/lib/Dom/canUseDom' + +global.canUseDom = true; + +jest.mock('rc-util/lib/Dom/canUseDom', () => { + // const canUseDom = jest.requireActual('rc-util/lib/Dom/canUseDom'); + return () => global.canUseDom; +}); + +describe('SSR', () => { + beforeEach(() => { + global.canUseDom = true; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('hydrate should not crash', () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const Demo = () => ( + +
+ + ); + + global.canUseDom = false; + const html = renderToString(); + + expect(html).toBeFalsy(); + + global.canUseDom = true; + + const container = document.createElement('div'); + container.innerHTML = html; + document.body.appendChild(container); + + render(, { container, hydrate: true }); + + expect(errSpy).toHaveBeenCalledWith( + "Warning: Drawer with 'open' in SSR is not work since no place to createPortal. Please move to 'useEffect' instead.", + ); + expect(errSpy).toBeCalledTimes(1); + + errSpy.mockRestore(); + }); + + // Since we use `useLayoutEffect` to avoid SSR warning. + // This may affect ref call. Let's check this also. + it('should not block ref', done => { + const Demo = ({ open }: any = {}) => { + const ref = React.useRef(); + + React.useEffect(() => { + if (open) { + expect(ref.current).toBeTruthy(); + done(); + } + }, [open]); + + return ( + +
+ + ); + }; + + const { rerender } = render(); + + rerender(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 503d249d..3e5cc41e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,6 @@ "@@/*": ["src/.umi/*"], "rc-drawer": ["src/index.ts"] } - } + }, + "include": [".dumi/**/*", ".dumirc.ts", "**/*.ts", "**/*.tsx"] }