UNPKG

react-sidebar

Version:
403 lines (349 loc) 11.8 kB
import React from 'react'; import ReactDOM from 'react-dom'; const CANCEL_DISTANCE_ON_SCROLL = 20; const defaultStyles = { root: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, overflow: 'hidden', }, sidebar: { zIndex: 2, position: 'absolute', top: 0, bottom: 0, transition: 'transform .3s ease-out', WebkitTransition: '-webkit-transform .3s ease-out', willChange: 'transform', overflowY: 'auto', }, content: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, overflow: 'auto', transition: 'left .3s ease-out, right .3s ease-out', }, overlay: { zIndex: 1, position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, opacity: 0, visibility: 'hidden', transition: 'opacity .3s ease-out', backgroundColor: 'rgba(0,0,0,.3)', }, dragHandle: { zIndex: 1, position: 'fixed', top: 0, bottom: 0, }, }; class Sidebar extends React.Component { constructor(props) { super(props); this.state = { // the detected width of the sidebar in pixels sidebarWidth: 0, // keep track of touching params touchIdentifier: null, touchStartX: null, touchStartY: null, touchCurrentX: null, touchCurrentY: null, // if touch is supported by the browser dragSupported: false, }; this.overlayClicked = this.overlayClicked.bind(this); this.onTouchStart = this.onTouchStart.bind(this); this.onTouchMove = this.onTouchMove.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this); this.onScroll = this.onScroll.bind(this); } componentDidMount() { this.setState({ dragSupported: typeof window === 'object' && 'ontouchstart' in window, }); this.saveSidebarWidth(); } componentDidUpdate() { // filter out the updates when we're touching if (!this.isTouching()) { this.saveSidebarWidth(); } } onTouchStart(ev) { // filter out if a user starts swiping with a second finger if (!this.isTouching()) { const touch = ev.targetTouches[0]; this.setState({ touchIdentifier: touch.identifier, touchStartX: touch.clientX, touchStartY: touch.clientY, touchCurrentX: touch.clientX, touchCurrentY: touch.clientY, }); } } onTouchMove(ev) { if (this.isTouching()) { for (let ind = 0; ind < ev.targetTouches.length; ind++) { // we only care about the finger that we are tracking if (ev.targetTouches[ind].identifier === this.state.touchIdentifier) { this.setState({ touchCurrentX: ev.targetTouches[ind].clientX, touchCurrentY: ev.targetTouches[ind].clientY, }); break; } } } } onTouchEnd() { if (this.isTouching()) { // trigger a change to open if sidebar has been dragged beyond dragToggleDistance const touchWidth = this.touchSidebarWidth(); if (this.props.open && touchWidth < this.state.sidebarWidth - this.props.dragToggleDistance || !this.props.open && touchWidth > this.props.dragToggleDistance) { this.props.onSetOpen(!this.props.open); } this.setState({ touchIdentifier: null, touchStartX: null, touchStartY: null, touchCurrentX: null, touchCurrentY: null, }); } } // This logic helps us prevents the user from sliding the sidebar horizontally // while scrolling the sidebar vertically. When a scroll event comes in, we're // cancelling the ongoing gesture if it did not move horizontally much. onScroll() { if (this.isTouching() && this.inCancelDistanceOnScroll()) { this.setState({ touchIdentifier: null, touchStartX: null, touchStartY: null, touchCurrentX: null, touchCurrentY: null, }); } } // True if the on going gesture X distance is less than the cancel distance inCancelDistanceOnScroll() { let cancelDistanceOnScroll; if (this.props.pullRight) { cancelDistanceOnScroll = Math.abs(this.state.touchCurrentX - this.state.touchStartX) < CANCEL_DISTANCE_ON_SCROLL; } else { cancelDistanceOnScroll = Math.abs(this.state.touchStartX - this.state.touchCurrentX) < CANCEL_DISTANCE_ON_SCROLL; } return cancelDistanceOnScroll; } isTouching() { return this.state.touchIdentifier !== null; } overlayClicked() { if (this.props.open) { this.props.onSetOpen(false); } } saveSidebarWidth() { const width = ReactDOM.findDOMNode(this.refs.sidebar).offsetWidth; if (width !== this.state.sidebarWidth) { this.setState({sidebarWidth: width}); } } // calculate the sidebarWidth based on current touch info touchSidebarWidth() { // if the sidebar is open and start point of drag is inside the sidebar // we will only drag the distance they moved their finger // otherwise we will move the sidebar to be below the finger. if (this.props.pullRight) { if (this.props.open && window.innerWidth - this.state.touchStartX < this.state.sidebarWidth) { if (this.state.touchCurrentX > this.state.touchStartX) { return this.state.sidebarWidth + this.state.touchStartX - this.state.touchCurrentX; } return this.state.sidebarWidth; } return Math.min(window.innerWidth - this.state.touchCurrentX, this.state.sidebarWidth); } if (this.props.open && this.state.touchStartX < this.state.sidebarWidth) { if (this.state.touchCurrentX > this.state.touchStartX) { return this.state.sidebarWidth; } return this.state.sidebarWidth - this.state.touchStartX + this.state.touchCurrentX; } return Math.min(this.state.touchCurrentX, this.state.sidebarWidth); } render() { const sidebarStyle = {...defaultStyles.sidebar, ...this.props.styles.sidebar}; const contentStyle = {...defaultStyles.content, ...this.props.styles.content}; const overlayStyle = {...defaultStyles.overlay, ...this.props.styles.overlay}; const useTouch = this.state.dragSupported && this.props.touch; const isTouching = this.isTouching(); const rootProps = { className: this.props.rootClassName, style: {...defaultStyles.root, ...this.props.styles.root}, }; let dragHandle; // sidebarStyle right/left if (this.props.pullRight) { sidebarStyle.right = 0; sidebarStyle.transform = 'translateX(100%)'; sidebarStyle.WebkitTransform = 'translateX(100%)'; if (this.props.shadow) { sidebarStyle.boxShadow = '-2px 2px 4px rgba(0, 0, 0, 0.15)'; } } else { sidebarStyle.left = 0; sidebarStyle.transform = 'translateX(-100%)'; sidebarStyle.WebkitTransform = 'translateX(-100%)'; if (this.props.shadow) { sidebarStyle.boxShadow = '2px 2px 4px rgba(0, 0, 0, 0.15)'; } } if (isTouching) { const percentage = this.touchSidebarWidth() / this.state.sidebarWidth; // slide open to what we dragged if (this.props.pullRight) { sidebarStyle.transform = `translateX(${(1 - percentage) * 100}%)`; sidebarStyle.WebkitTransform = `translateX(${(1 - percentage) * 100}%)`; } else { sidebarStyle.transform = `translateX(-${(1 - percentage) * 100}%)`; sidebarStyle.WebkitTransform = `translateX(-${(1 - percentage) * 100}%)`; } // fade overlay to match distance of drag overlayStyle.opacity = percentage; overlayStyle.visibility = 'visible'; } else if (this.props.docked) { // show sidebar if (this.state.sidebarWidth !== 0) { sidebarStyle.transform = `translateX(0%)`; sidebarStyle.WebkitTransform = `translateX(0%)`; } // make space on the left/right side of the content for the sidebar if (this.props.pullRight) { contentStyle.right = `${this.state.sidebarWidth}px`; } else { contentStyle.left = `${this.state.sidebarWidth}px`; } } else if (this.props.open) { // slide open sidebar sidebarStyle.transform = `translateX(0%)`; sidebarStyle.WebkitTransform = `translateX(0%)`; // show overlay overlayStyle.opacity = 1; overlayStyle.visibility = 'visible'; } if (isTouching || !this.props.transitions) { sidebarStyle.transition = 'none'; sidebarStyle.WebkitTransition = 'none'; contentStyle.transition = 'none'; overlayStyle.transition = 'none'; } if (useTouch) { if (this.props.open) { rootProps.onTouchStart = this.onTouchStart; rootProps.onTouchMove = this.onTouchMove; rootProps.onTouchEnd = this.onTouchEnd; rootProps.onTouchCancel = this.onTouchEnd; rootProps.onScroll = this.onScroll; } else { const dragHandleStyle = {...defaultStyles.dragHandle, ...this.props.styles.dragHandle}; dragHandleStyle.width = this.props.touchHandleWidth; // dragHandleStyle right/left if (this.props.pullRight) { dragHandleStyle.right = 0; } else { dragHandleStyle.left = 0; } dragHandle = ( <div style={dragHandleStyle} onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove} onTouchEnd={this.onTouchEnd} onTouchCancel={this.onTouchEnd} />); } } return ( <div {...rootProps}> <div className={this.props.sidebarClassName} style={sidebarStyle} ref="sidebar"> {this.props.sidebar} </div> <div className={this.props.overlayClassName} style={overlayStyle} role="presentation" tabIndex="0" onClick={this.overlayClicked} /> <div className={this.props.contentClassName} style={contentStyle}> {dragHandle} {this.props.children} </div> </div> ); } } Sidebar.propTypes = { // main content to render children: React.PropTypes.node.isRequired, // styles styles: React.PropTypes.shape({ root: React.PropTypes.object, sidebar: React.PropTypes.object, content: React.PropTypes.object, overlay: React.PropTypes.object, dragHandle: React.PropTypes.object, }), // root component optional class rootClassName: React.PropTypes.string, // sidebar optional class sidebarClassName: React.PropTypes.string, // content optional class contentClassName: React.PropTypes.string, // overlay optional class overlayClassName: React.PropTypes.string, // sidebar content to render sidebar: React.PropTypes.node.isRequired, // boolean if sidebar should be docked docked: React.PropTypes.bool, // boolean if sidebar should slide open open: React.PropTypes.bool, // boolean if transitions should be disabled transitions: React.PropTypes.bool, // boolean if touch gestures are enabled touch: React.PropTypes.bool, // max distance from the edge we can start touching touchHandleWidth: React.PropTypes.number, // Place the sidebar on the right pullRight: React.PropTypes.bool, // Enable/Disable sidebar shadow shadow: React.PropTypes.bool, // distance we have to drag the sidebar to toggle open state dragToggleDistance: React.PropTypes.number, // callback called when the overlay is clicked onSetOpen: React.PropTypes.func, }; Sidebar.defaultProps = { docked: false, open: false, transitions: true, touch: true, touchHandleWidth: 20, pullRight: false, shadow: true, dragToggleDistance: 30, onSetOpen: () => {}, styles: {}, }; export default Sidebar;