├── .editorconfig ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── assets └── index.less ├── examples ├── simple.html ├── simple.js └── simple.less ├── index.js ├── package.json ├── src ├── Drawer.jsx └── index.js └── tests ├── Drawer.spec.js └── __snapshots__ └── Drawer.spec.js.snap /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | .cache 23 | dist 24 | *.css 25 | build 26 | lib 27 | /coverage 28 | yarn.lock 29 | package-lock.json 30 | es/ 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | notifications: 6 | email: 7 | - 8 | 9 | node_js: 10 | - 4.0.0 11 | 12 | before_install: 13 | - | 14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 15 | then 16 | echo "Only docs were updated, stopping build process." 17 | exit 18 | fi 19 | npm install npm@3.x -g 20 | phantomjs --version 21 | script: 22 | - | 23 | if [ "$TEST_TYPE" = test ]; then 24 | npm test 25 | else 26 | npm run $TEST_TYPE 27 | fi 28 | env: 29 | matrix: 30 | - TEST_TYPE=lint 31 | - TEST_TYPE=test 32 | - TEST_TYPE=coverage 33 | - TEST_TYPE=saucelabs 34 | 35 | 36 | matrix: 37 | allow_failures: 38 | - env: "TEST_TYPE=saucelabs" -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/m-drawer/c968110b6a79190c1e04ded36dff0bec178dfdfd/HISTORY.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rmc-drawer 2 | --- 3 | 4 | React Drawer Component 5 | 6 | 7 | [![NPM version][npm-image]][npm-url] 8 | [![build status][travis-image]][travis-url] 9 | [![Test coverage][coveralls-image]][coveralls-url] 10 | [![gemnasium deps][gemnasium-image]][gemnasium-url] 11 | [![npm download][download-image]][download-url] 12 | 13 | [npm-image]: http://img.shields.io/npm/v/rmc-drawer.svg?style=flat-square 14 | [npm-url]: http://npmjs.org/package/rmc-drawer 15 | [travis-image]: https://img.shields.io/travis/react-component/m-drawer.svg?style=flat-square 16 | [travis-url]: https://travis-ci.org/react-component/m-drawer 17 | [coveralls-image]: https://img.shields.io/coveralls/react-component/m-drawer.svg?style=flat-square 18 | [coveralls-url]: https://coveralls.io/r/react-component/m-drawer?branch=master 19 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/m-drawer.svg?style=flat-square 20 | [gemnasium-url]: https://gemnasium.com/react-component/m-drawer 21 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 22 | [node-url]: http://nodejs.org/download/ 23 | [download-image]: https://img.shields.io/npm/dm/rmc-drawer.svg?style=flat-square 24 | [download-url]: https://npmjs.org/package/rmc-drawer 25 | 26 | 27 | ## Screenshots 28 | 29 | 30 | 31 | 32 | ## Development 33 | 34 | ``` 35 | npm install 36 | npm start 37 | ``` 38 | 39 | ## Example 40 | 41 | http://localhost:8099/examples/ 42 | 43 | 44 | online example: http://react-component.github.io/m-drawer/ 45 | 46 | 47 | ## install 48 | 49 | 50 | [![rmc-drawer](https://nodei.co/npm/rmc-drawer.png)](https://npmjs.org/package/rmc-drawer) 51 | 52 | 53 | ## Usage 54 | 55 | ```js 56 | var Drawer = require('rmc-drawer'); 57 | var React = require('react'); 58 | React.render(, container); 59 | ``` 60 | 61 | ## API 62 | 63 | ### props 64 | 65 | | Property name | Description | Type | Default | 66 | |---------------|-------------|------|---------| 67 | | className | additional css class of root dom node | String | '' | 68 | | prefixCls | prefix class | String | 'rmc-drawer' | 69 | | children | The main content | any | n/a | 70 | | style | container styles. | Object | | 71 | | sidebarStyle | Inline styles. | Object | {} | 72 | | contentStyle | Inline styles. | Object | {} | 73 | | overlayStyle | Inline styles. | Object | {} | 74 | | dragHandleStyle | Inline styles. | Object | {} | 75 | | sidebar | The sidebar content | any | n/a | 76 | | onOpenChange | Callback called when the sidebar wants to change the open prop. Happens after sliding the sidebar and when the overlay is clicked when the sidebar is open. | Function | n/a | 77 | | open | If the sidebar should be open | Boolean | false | 78 | | position | where to place the sidebar | String | 'left', enum{'left', 'right', 'top', 'bottom'} | 79 | | docked | If the sidebar should be docked in document | Boolean | false | 80 | | transitions | If transitions should be enabled | Boolean | true | 81 | | touch | If touch gestures should be enabled | Boolean | true | 82 | | enableDragHandle | If dragHandle should be enabled | Boolean | true | 83 | | dragToggleDistance | Distance the sidebar has to be dragged before it will open/close after it is released. | Number | 30 | 84 | 85 | > change from [https://github.com/balloob/react-sidebar](https://github.com/balloob/react-sidebar) 86 | 87 | 88 | ## Test Case 89 | 90 | ``` 91 | npm test 92 | npm run chrome-test 93 | ``` 94 | 95 | ## Coverage 96 | 97 | ``` 98 | npm run coverage 99 | ``` 100 | 101 | open coverage/ dir 102 | 103 | ## License 104 | 105 | rmc-drawer is released under the MIT license. 106 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @drawerPrefixCls: rmc-drawer; 2 | .@{drawerPrefixCls} { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | overflow: hidden; 9 | 10 | &-sidebar { 11 | z-index: 2; 12 | position: absolute; 13 | transition: transform .3s ease-out; 14 | will-change: transform; 15 | overflow-y: auto; 16 | } 17 | &-draghandle { 18 | z-index: 1; 19 | position: absolute; 20 | background-color: rgba(50, 50, 50, 0.1); 21 | } 22 | &-overlay { 23 | z-index: 1; 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | bottom: 0; 29 | opacity: 0; 30 | visibility: hidden; 31 | transition: opacity 0.3s ease-out; 32 | background-color: rgba(0, 0, 0, 0.3); 33 | } 34 | &-content { 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | bottom: 0; 40 | overflow: auto; 41 | transition: left .3s ease-out, right .3s ease-out, top .3s ease-out, bottom .3s ease-out; 42 | } 43 | 44 | &&-left, 45 | &&-right { 46 | .@{drawerPrefixCls}-sidebar, 47 | .@{drawerPrefixCls}-draghandle { 48 | top: 0; 49 | bottom: 0; 50 | } 51 | .@{drawerPrefixCls}-draghandle { 52 | width: 20px; 53 | height: 100%; 54 | } 55 | } 56 | &&-top, 57 | &&-bottom { 58 | .@{drawerPrefixCls}-sidebar, 59 | .@{drawerPrefixCls}-draghandle { 60 | left: 0; 61 | right: 0; 62 | } 63 | .@{drawerPrefixCls}-draghandle { 64 | width: 100%; 65 | height: 20px; 66 | } 67 | } 68 | &&-left { 69 | .@{drawerPrefixCls}-sidebar { 70 | left: 0; 71 | transform: translateX(-100%); 72 | .@{drawerPrefixCls}-open& { 73 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); 74 | } 75 | } 76 | .@{drawerPrefixCls}-draghandle { 77 | left: 0; 78 | } 79 | } 80 | 81 | &&-right { 82 | .@{drawerPrefixCls}-sidebar { 83 | right: 0; 84 | transform: translateX(100%); 85 | .@{drawerPrefixCls}-open& { 86 | box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.15); 87 | } 88 | } 89 | .@{drawerPrefixCls}-draghandle { 90 | right: 0; 91 | } 92 | } 93 | 94 | &&-top { 95 | .@{drawerPrefixCls}-sidebar { 96 | top: 0; 97 | transform: translateY(-100%); 98 | .@{drawerPrefixCls}-open& { 99 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); 100 | } 101 | } 102 | .@{drawerPrefixCls}-draghandle { 103 | top: 0; 104 | } 105 | } 106 | 107 | &&-bottom { 108 | .@{drawerPrefixCls}-sidebar { 109 | bottom: 0; 110 | transform: translateY(100%); 111 | .@{drawerPrefixCls}-open& { 112 | box-shadow: 2px -2px 4px rgba(0, 0, 0, 0.15); 113 | } 114 | } 115 | .@{drawerPrefixCls}-draghandle { 116 | bottom: 0; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | 3 | import 'rmc-drawer/assets/index.less'; 4 | import './simple.less'; 5 | import Drawer from 'rmc-drawer'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | class App extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | docked: false, 15 | open: false, 16 | transitions: true, 17 | touch: true, 18 | enableDragHandle: true, 19 | position: 'left', 20 | dragToggleDistance: 30, 21 | }; 22 | } 23 | onOpenChange = (open) => { 24 | this.setState({ open }); 25 | } 26 | onDock = () => { 27 | const docked = !this.state.docked; 28 | this.setState({ 29 | docked, 30 | }); 31 | if (!docked) { 32 | this.onOpenChange(false); 33 | } 34 | } 35 | render() { 36 | const sidebar = (
37 |

38 | sidebar 39 | 42 |

43 |

this is content!

44 |
); 45 | 46 | const drawerProps = { 47 | docked: this.state.docked, 48 | open: this.state.open, 49 | touch: this.state.touch, 50 | enableDragHandle: this.state.enableDragHandle, 51 | position: this.state.position, 52 | dragToggleDistance: this.state.dragToggleDistance, 53 | transitions: this.state.transitions, 54 | onOpenChange: this.onOpenChange, 55 | }; 56 | return (
57 | 58 |
59 |

React component

60 | 63 |

64 | {['left', 'right', 'top', 'bottom'].map((i, index) => ( 67 | { this.setState({ position: e.target.value }); }} 70 | /> 71 | ))} 72 |

73 |

right content

74 |

bottom content

75 |
76 |
77 |
); 78 | } 79 | } 80 | 81 | ReactDOM.render(, document.getElementById('__react-content')); 82 | -------------------------------------------------------------------------------- /examples/simple.less: -------------------------------------------------------------------------------- 1 | 2 | .drawer-container { 3 | position: relative; 4 | height: 400px; 5 | border: 1px solid #ccc; 6 | .main { 7 | position: relative; 8 | padding: 10px; 9 | box-sizing: border-box; 10 | height: 100%; 11 | } 12 | } 13 | .rmc-drawer { 14 | &-sidebar { 15 | padding: 16px; 16 | background-color: #e4f6fe; 17 | } 18 | &-left &-sidebar, 19 | &-right &-sidebar, { 20 | height: 100%; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import Drawer from './src/'; 3 | export default Drawer; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rmc-drawer", 3 | "version": "0.4.11", 4 | "description": "drawer ui component for react", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-drawer", 9 | "drawer" 10 | ], 11 | "engines": { 12 | "node": ">=4.0.0" 13 | }, 14 | "homepage": "https://github.com/react-component/m-drawer", 15 | "author": "", 16 | "repository": "react-component/m-drawer", 17 | "bugs": "https://github.com/react-component/m-drawer/issues", 18 | "files": [ 19 | "es", 20 | "lib", 21 | "assets/*.css" 22 | ], 23 | "license": "MIT", 24 | "main": "./lib/index", 25 | "module": "./es/index", 26 | "config": { 27 | "port": 8099 28 | }, 29 | "scripts": { 30 | "build": "rc-tools run build", 31 | "dist": "rc-tools run dist", 32 | "compile": "rc-tools run compile --babel-runtime", 33 | "gh-pages": "rc-tools run gh-pages", 34 | "start": "rc-tools run server", 35 | "prepublish": "rc-tools run guard", 36 | "prepare": "rc-tools run guard", 37 | "prepublishOnly": "rc-tools run guard", 38 | "pub": "rc-tools run pub --babel-runtime", 39 | "lint": "rc-tools run lint", 40 | "test": "jest", 41 | "coverage": "jest --coverage && cat ./coverage/lcov.info | coveralls" 42 | }, 43 | "jest": { 44 | "collectCoverageFrom": [ 45 | "src/**/*" 46 | ], 47 | "transform": { 48 | "\\.jsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js" 49 | } 50 | }, 51 | "devDependencies": { 52 | "coveralls": "^2.13.1", 53 | "enzyme": "^2.6.0", 54 | "enzyme-to-json": "^1.4.5", 55 | "jest": "^18.0.0", 56 | "pre-commit": "1.x", 57 | "rc-tools": "6.x", 58 | "react": "15.x", 59 | "react-dom": "15.x", 60 | "react-test-renderer": "^15.6.1" 61 | }, 62 | "pre-commit": [ 63 | "lint" 64 | ], 65 | "dependencies": { 66 | "babel-runtime": "6.x", 67 | "classnames": "^2.2.4", 68 | "prop-types": "^15.5.10" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Drawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import classNames from 'classnames'; 5 | 6 | function getOffset(ele) { 7 | let el = ele; 8 | let _x = 0; 9 | let _y = 0; 10 | while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { 11 | _x += el.offsetLeft - el.scrollLeft; 12 | _y += el.offsetTop - el.scrollTop; 13 | el = el.offsetParent; 14 | } 15 | return { top: _y, left: _x }; 16 | } 17 | 18 | const CANCEL_DISTANCE_ON_SCROLL = 20; 19 | 20 | export default class Drawer extends React.Component { 21 | static propTypes = { 22 | prefixCls: PropTypes.string, 23 | className: PropTypes.string, 24 | // main content to render 25 | children: PropTypes.node.isRequired, 26 | 27 | // styles 28 | // styles: PropTypes.shape({ 29 | // dragHandle: PropTypes.object, 30 | // }), 31 | style: PropTypes.object, 32 | sidebarStyle: PropTypes.object, 33 | contentStyle: PropTypes.object, 34 | overlayStyle: PropTypes.object, 35 | dragHandleStyle: PropTypes.object, 36 | 37 | // sidebar content to render 38 | sidebar: PropTypes.node.isRequired, 39 | 40 | // boolean if sidebar should be docked 41 | docked: PropTypes.bool, 42 | 43 | // boolean if sidebar should slide open 44 | open: PropTypes.bool, 45 | 46 | // boolean if transitions should be disabled 47 | transitions: PropTypes.bool, 48 | 49 | // boolean if touch gestures are enabled 50 | touch: PropTypes.bool, 51 | enableDragHandle: PropTypes.bool, 52 | 53 | // where to place the sidebar 54 | position: PropTypes.oneOf(['left', 'right', 'top', 'bottom']), 55 | 56 | // distance we have to drag the sidebar to toggle open state 57 | dragToggleDistance: PropTypes.number, 58 | 59 | // callback called when the overlay is clicked 60 | onOpenChange: PropTypes.func, 61 | } 62 | 63 | static defaultProps = { 64 | prefixCls: 'rmc-drawer', 65 | sidebarStyle: {}, 66 | contentStyle: {}, 67 | overlayStyle: {}, 68 | dragHandleStyle: {}, 69 | docked: false, 70 | open: false, 71 | transitions: true, 72 | touch: true, 73 | enableDragHandle: true, 74 | position: 'left', 75 | dragToggleDistance: 30, 76 | onOpenChange: () => {}, 77 | } 78 | 79 | constructor(props) { 80 | super(props); 81 | 82 | this.state = { 83 | // the detected width of the sidebar in pixels 84 | sidebarWidth: 0, 85 | sidebarHeight: 0, 86 | sidebarTop: 0, 87 | dragHandleTop: 0, 88 | 89 | // keep track of touching params 90 | touchIdentifier: null, 91 | touchStartX: null, 92 | touchStartY: null, 93 | touchCurrentX: null, 94 | touchCurrentY: null, 95 | 96 | // if touch is supported by the browser 97 | touchSupported: typeof window === 'object' && 'ontouchstart' in window, 98 | }; 99 | } 100 | 101 | componentDidMount() { 102 | this.saveSidebarSize(); 103 | } 104 | 105 | componentDidUpdate() { 106 | // filter out the updates when we're touching 107 | if (!this.isTouching()) { 108 | this.saveSidebarSize(); 109 | } 110 | } 111 | 112 | onOverlayClicked = () => { 113 | if (this.props.open) { 114 | // see https://github.com/react-component/drawer/issues/9 115 | setTimeout(() => { 116 | this.props.onOpenChange(false, { overlayClicked: true }); 117 | }, 0); 118 | } 119 | } 120 | 121 | onTouchStart = (ev) => { 122 | // filter out if a user starts swiping with a second finger 123 | if (!this.isTouching()) { 124 | const touch = ev.targetTouches[0]; 125 | this.setState({ 126 | touchIdentifier: !this.notTouch ? touch.identifier : null, 127 | touchStartX: touch.clientX, 128 | touchStartY: touch.clientY, 129 | touchCurrentX: touch.clientX, 130 | touchCurrentY: touch.clientY, 131 | }); 132 | } 133 | } 134 | 135 | onTouchMove = (ev) => { 136 | // ev.preventDefault(); // cannot touchmove with FastClick 137 | if (this.isTouching()) { 138 | for (let ind = 0; ind < ev.targetTouches.length; ind++) { 139 | // we only care about the finger that we are tracking 140 | if (ev.targetTouches[ind].identifier === this.state.touchIdentifier) { 141 | this.setState({ 142 | touchCurrentX: ev.targetTouches[ind].clientX, 143 | touchCurrentY: ev.targetTouches[ind].clientY, 144 | }); 145 | break; 146 | } 147 | } 148 | } 149 | } 150 | 151 | onTouchEnd = () => { 152 | this.notTouch = false; 153 | if (this.isTouching()) { 154 | // trigger a change to open if sidebar has been dragged beyond dragToggleDistance 155 | const touchWidth = this.touchSidebarWidth(); 156 | 157 | if (this.props.open && touchWidth < this.state.sidebarWidth - this.props.dragToggleDistance || 158 | !this.props.open && touchWidth > this.props.dragToggleDistance) { 159 | this.props.onOpenChange(!this.props.open); 160 | } 161 | 162 | const touchHeight = this.touchSidebarHeight(); 163 | 164 | if (this.props.open && 165 | touchHeight < this.state.sidebarHeight - this.props.dragToggleDistance || 166 | !this.props.open && touchHeight > this.props.dragToggleDistance) { 167 | this.props.onOpenChange(!this.props.open); 168 | } 169 | 170 | this.setState({ 171 | touchIdentifier: null, 172 | touchStartX: null, 173 | touchStartY: null, 174 | touchCurrentX: null, 175 | touchCurrentY: null, 176 | }); 177 | } 178 | } 179 | 180 | // This logic helps us prevents the user from sliding the sidebar horizontally 181 | // while scrolling the sidebar vertically. When a scroll event comes in, we're 182 | // cancelling the ongoing gesture if it did not move horizontally much. 183 | onScroll = () => { 184 | if (this.isTouching() && this.inCancelDistanceOnScroll()) { 185 | this.setState({ 186 | touchIdentifier: null, 187 | touchStartX: null, 188 | touchStartY: null, 189 | touchCurrentX: null, 190 | touchCurrentY: null, 191 | }); 192 | } 193 | } 194 | 195 | // True if the on going gesture X distance is less than the cancel distance 196 | inCancelDistanceOnScroll = () => { 197 | let cancelDistanceOnScroll; 198 | switch (this.props.position) { 199 | case 'right': 200 | cancelDistanceOnScroll = Math.abs(this.state.touchCurrentX - this.state.touchStartX) < 201 | CANCEL_DISTANCE_ON_SCROLL; 202 | break; 203 | case 'bottom': 204 | cancelDistanceOnScroll = Math.abs(this.state.touchCurrentY - this.state.touchStartY) < 205 | CANCEL_DISTANCE_ON_SCROLL; 206 | break; 207 | case 'top': 208 | cancelDistanceOnScroll = Math.abs(this.state.touchStartY - this.state.touchCurrentY) < 209 | CANCEL_DISTANCE_ON_SCROLL; 210 | break; 211 | case 'left': 212 | default: 213 | cancelDistanceOnScroll = Math.abs(this.state.touchStartX - this.state.touchCurrentX) < 214 | CANCEL_DISTANCE_ON_SCROLL; 215 | } 216 | return cancelDistanceOnScroll; 217 | } 218 | 219 | isTouching = () => { 220 | return this.state.touchIdentifier !== null; 221 | } 222 | 223 | saveSidebarSize = () => { 224 | const sidebar = ReactDOM.findDOMNode(this.refs.sidebar); 225 | const width = sidebar.offsetWidth; 226 | const height = sidebar.offsetHeight; 227 | const sidebarTop = getOffset(ReactDOM.findDOMNode(this.refs.sidebar)).top; 228 | const dragHandleTop = getOffset(ReactDOM.findDOMNode(this.refs.dragHandle)).top; 229 | 230 | if (width !== this.state.sidebarWidth) { 231 | this.setState({ sidebarWidth: width }); 232 | } 233 | if (height !== this.state.sidebarHeight) { 234 | this.setState({ sidebarHeight: height }); 235 | } 236 | if (sidebarTop !== this.state.sidebarTop) { 237 | this.setState({ sidebarTop }); 238 | } 239 | if (dragHandleTop !== this.state.dragHandleTop) { 240 | this.setState({ dragHandleTop }); 241 | } 242 | } 243 | 244 | // calculate the sidebarWidth based on current touch info 245 | touchSidebarWidth = () => { 246 | // if the sidebar is open and start point of drag is inside the sidebar 247 | // we will only drag the distance they moved their finger 248 | // otherwise we will move the sidebar to be below the finger. 249 | if (this.props.position === 'right') { 250 | if (this.props.open && window.innerWidth - this.state.touchStartX < this.state.sidebarWidth) { 251 | if (this.state.touchCurrentX > this.state.touchStartX) { 252 | return this.state.sidebarWidth + this.state.touchStartX - this.state.touchCurrentX; 253 | } 254 | return this.state.sidebarWidth; 255 | } 256 | return Math.min(window.innerWidth - this.state.touchCurrentX, this.state.sidebarWidth); 257 | } 258 | 259 | if (this.props.position === 'left') { 260 | if (this.props.open && this.state.touchStartX < this.state.sidebarWidth) { 261 | if (this.state.touchCurrentX > this.state.touchStartX) { 262 | return this.state.sidebarWidth; 263 | } 264 | return this.state.sidebarWidth - this.state.touchStartX + this.state.touchCurrentX; 265 | } 266 | return Math.min(this.state.touchCurrentX, this.state.sidebarWidth); 267 | } 268 | } 269 | // calculate the sidebarHeight based on current touch info 270 | touchSidebarHeight = () => { 271 | // if the sidebar is open and start point of drag is inside the sidebar 272 | // we will only drag the distance they moved their finger 273 | // otherwise we will move the sidebar to be below the finger. 274 | if (this.props.position === 'bottom') { 275 | if (this.props.open && 276 | window.innerHeight - this.state.touchStartY < this.state.sidebarHeight) { 277 | if (this.state.touchCurrentY > this.state.touchStartY) { 278 | return this.state.sidebarHeight + this.state.touchStartY - this.state.touchCurrentY; 279 | } 280 | return this.state.sidebarHeight; 281 | } 282 | return Math.min(window.innerHeight - this.state.touchCurrentY, this.state.sidebarHeight); 283 | } 284 | 285 | if (this.props.position === 'top') { 286 | const touchStartOffsetY = this.state.touchStartY - this.state.sidebarTop; 287 | if (this.props.open && touchStartOffsetY < this.state.sidebarHeight) { 288 | if (this.state.touchCurrentY > this.state.touchStartY) { 289 | return this.state.sidebarHeight; 290 | } 291 | return this.state.sidebarHeight - this.state.touchStartY + this.state.touchCurrentY; 292 | } 293 | return Math.min(this.state.touchCurrentY - this.state.dragHandleTop, 294 | this.state.sidebarHeight); 295 | } 296 | } 297 | 298 | renderStyle = ({ sidebarStyle, isTouching, overlayStyle, contentStyle }) => { 299 | if (this.props.position === 'right' || this.props.position === 'left') { 300 | sidebarStyle.transform = `translateX(0%)`; 301 | sidebarStyle.WebkitTransform = `translateX(0%)`; 302 | if (isTouching) { 303 | const percentage = this.touchSidebarWidth() / this.state.sidebarWidth; 304 | // slide open to what we dragged 305 | if (this.props.position === 'right') { 306 | sidebarStyle.transform = `translateX(${(1 - percentage) * 100}%)`; 307 | sidebarStyle.WebkitTransform = `translateX(${(1 - percentage) * 100}%)`; 308 | } 309 | if (this.props.position === 'left') { 310 | sidebarStyle.transform = `translateX(-${(1 - percentage) * 100}%)`; 311 | sidebarStyle.WebkitTransform = `translateX(-${(1 - percentage) * 100}%)`; 312 | } 313 | // fade overlay to match distance of drag 314 | overlayStyle.opacity = percentage; 315 | overlayStyle.visibility = 'visible'; 316 | } 317 | if (contentStyle) { 318 | contentStyle[this.props.position] = `${this.state.sidebarWidth}px`; 319 | } 320 | } 321 | if (this.props.position === 'top' || this.props.position === 'bottom') { 322 | sidebarStyle.transform = `translateY(0%)`; 323 | sidebarStyle.WebkitTransform = `translateY(0%)`; 324 | if (isTouching) { 325 | const percentage = this.touchSidebarHeight() / this.state.sidebarHeight; 326 | // slide open to what we dragged 327 | if (this.props.position === 'bottom') { 328 | sidebarStyle.transform = `translateY(${(1 - percentage) * 100}%)`; 329 | sidebarStyle.WebkitTransform = `translateY(${(1 - percentage) * 100}%)`; 330 | } 331 | if (this.props.position === 'top') { 332 | sidebarStyle.transform = `translateY(-${(1 - percentage) * 100}%)`; 333 | sidebarStyle.WebkitTransform = `translateY(-${(1 - percentage) * 100}%)`; 334 | } 335 | // fade overlay to match distance of drag 336 | overlayStyle.opacity = percentage; 337 | overlayStyle.visibility = 'visible'; 338 | } 339 | if (contentStyle) { 340 | contentStyle[this.props.position] = `${this.state.sidebarHeight}px`; 341 | } 342 | } 343 | } 344 | 345 | render() { 346 | const { className, style, prefixCls, position, transitions, 347 | touch, enableDragHandle, sidebar, children, docked, open } = this.props; 348 | 349 | const sidebarStyle = { ...this.props.sidebarStyle }; 350 | const contentStyle = { ...this.props.contentStyle }; 351 | const overlayStyle = { ...this.props.overlayStyle }; 352 | 353 | const rootCls = { 354 | [className]: !!className, 355 | [prefixCls]: true, 356 | [`${prefixCls}-${position}`]: true, 357 | }; 358 | 359 | const rootProps = { style }; 360 | const isTouching = this.isTouching(); 361 | 362 | if (isTouching) { 363 | this.renderStyle({ sidebarStyle, isTouching: true, overlayStyle }); 364 | } else if (docked) { 365 | if (this.state.sidebarWidth !== 0) { 366 | rootCls[`${prefixCls}-docked`] = true; 367 | this.renderStyle({ sidebarStyle, contentStyle }); 368 | } 369 | } else if (open) { 370 | rootCls[`${prefixCls}-open`] = true; 371 | this.renderStyle({ sidebarStyle }); 372 | overlayStyle.opacity = 1; 373 | overlayStyle.visibility = 'visible'; 374 | } 375 | 376 | if (isTouching || !transitions) { 377 | sidebarStyle.transition = 'none'; 378 | sidebarStyle.WebkitTransition = 'none'; 379 | contentStyle.transition = 'none'; 380 | overlayStyle.transition = 'none'; 381 | } 382 | 383 | let dragHandle = null; 384 | 385 | if (this.state.touchSupported && touch) { 386 | if (open) { 387 | rootProps.onTouchStart = (ev) => { 388 | this.notTouch = true; 389 | this.onTouchStart(ev); 390 | }; 391 | rootProps.onTouchMove = this.onTouchMove; 392 | rootProps.onTouchEnd = this.onTouchEnd; 393 | rootProps.onTouchCancel = this.onTouchEnd; 394 | rootProps.onScroll = this.onScroll; 395 | } else if (enableDragHandle) { 396 | dragHandle = ( 397 |
); 402 | } 403 | } 404 | 405 | // const evt = {}; 406 | // // FastClick use touchstart instead of click 407 | // if (this.state.touchSupported) { 408 | // evt.onTouchStart = () => { 409 | // this.notTouch = true; 410 | // this.onOverlayClicked(); 411 | // }; 412 | // evt.onTouchEnd = () => { 413 | // this.notTouch = false; 414 | // this.setState({ 415 | // touchIdentifier: null, 416 | // }); 417 | // }; 418 | // } else { 419 | // evt.onClick = this.onOverlayClicked; 420 | // } 421 | 422 | return ( 423 |
424 |
427 | {sidebar} 428 |
429 | {/* 430 |
*/} 435 |
441 |
444 | {dragHandle} 445 | {children} 446 |
447 |
448 | ); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import Drawer from './Drawer'; 3 | export default Drawer; 4 | -------------------------------------------------------------------------------- /tests/Drawer.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React from 'react'; 3 | import { render, mount } from 'enzyme'; 4 | import { renderToJson } from 'enzyme-to-json'; 5 | import Drawer from '..'; 6 | 7 | describe('Drawer', () => { 8 | it('renders correctly', () => { 9 | const wrapper = render( 10 | sidebar

}> 11 |

React Sidebar is a sidebar component for React.

12 |
13 | ); 14 | expect(renderToJson(wrapper)).toMatchSnapshot(); 15 | expect(wrapper.find('.forTest').length === 1).toBe(true); 16 | }); 17 | 18 | it('should close the sidebar', () => { 19 | const cb = jest.fn(); 20 | const wrapper = mount( 21 | sidebar

} onOpenChange={cb} open> 22 |

React Sidebar is a sidebar component for React.

23 |
24 | ); 25 | // console.log(wrapper.html()) 26 | wrapper.find('.rmc-drawer-overlay').simulate('click'); 27 | expect(cb).toBeCalledWith(false, { overlayClicked: true }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/__snapshots__/Drawer.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Drawer renders correctly 1`] = ` 2 |
4 |
6 |

7 | sidebar 8 |

9 |
10 | 20 | `; 21 | --------------------------------------------------------------------------------