UNPKG

10.8 kBJavaScriptView Raw
1import React from 'react';
2import ReactDOM from 'react-dom';
3
4const CANCEL_DISTANCE_ON_SCROLL = 20;
5
6const styles = {
7 root: {
8 position: 'absolute',
9 top: 0,
10 left: 0,
11 right: 0,
12 bottom: 0,
13 overflow: 'hidden',
14 },
15 sidebar: {
16 zIndex: 2,
17 position: 'absolute',
18 top: 0,
19 bottom: 0,
20 transition: 'transform .3s ease-out',
21 WebkitTransition: '-webkit-transform .3s ease-out',
22 willChange: 'transform',
23 overflowY: 'auto',
24 },
25 content: {
26 position: 'absolute',
27 top: 0,
28 left: 0,
29 right: 0,
30 bottom: 0,
31 overflow: 'auto',
32 transition: 'left .3s ease-out, right .3s ease-out',
33 },
34 overlay: {
35 zIndex: 1,
36 position: 'fixed',
37 top: 0,
38 left: 0,
39 right: 0,
40 bottom: 0,
41 opacity: 0,
42 visibility: 'hidden',
43 transition: 'opacity .3s ease-out',
44 backgroundColor: 'rgba(0,0,0,.3)',
45 },
46 dragHandle: {
47 zIndex: 1,
48 position: 'fixed',
49 top: 0,
50 bottom: 0,
51 },
52};
53
54class Sidebar extends React.Component {
55 constructor(props) {
56 super(props);
57
58 this.state = {
59 // the detected width of the sidebar in pixels
60 sidebarWidth: 0,
61
62 // keep track of touching params
63 touchIdentifier: null,
64 touchStartX: null,
65 touchStartY: null,
66 touchCurrentX: null,
67 touchCurrentY: null,
68
69 // if touch is supported by the browser
70 dragSupported: typeof window === 'object' && 'ontouchstart' in window,
71 };
72
73 this.overlayClicked = this.overlayClicked.bind(this);
74 this.onTouchStart = this.onTouchStart.bind(this);
75 this.onTouchMove = this.onTouchMove.bind(this);
76 this.onTouchEnd = this.onTouchEnd.bind(this);
77 this.onScroll = this.onScroll.bind(this);
78 }
79
80 componentDidMount() {
81 this.saveSidebarWidth();
82 }
83
84 componentDidUpdate() {
85 // filter out the updates when we're touching
86 if (!this.isTouching()) {
87 this.saveSidebarWidth();
88 }
89 }
90
91 onTouchStart(ev) {
92 // filter out if a user starts swiping with a second finger
93 if (!this.isTouching()) {
94 const touch = ev.targetTouches[0];
95 this.setState({
96 touchIdentifier: touch.identifier,
97 touchStartX: touch.clientX,
98 touchStartY: touch.clientY,
99 touchCurrentX: touch.clientX,
100 touchCurrentY: touch.clientY,
101 });
102 }
103 }
104
105 onTouchMove(ev) {
106 if (this.isTouching()) {
107 for (let ind = 0; ind < ev.targetTouches.length; ind++) {
108 // we only care about the finger that we are tracking
109 if (ev.targetTouches[ind].identifier === this.state.touchIdentifier) {
110 this.setState({
111 touchCurrentX: ev.targetTouches[ind].clientX,
112 touchCurrentY: ev.targetTouches[ind].clientY,
113 });
114 break;
115 }
116 }
117 }
118 }
119
120 onTouchEnd() {
121 if (this.isTouching()) {
122 // trigger a change to open if sidebar has been dragged beyond dragToggleDistance
123 const touchWidth = this.touchSidebarWidth();
124
125 if (this.props.open && touchWidth < this.state.sidebarWidth - this.props.dragToggleDistance ||
126 !this.props.open && touchWidth > this.props.dragToggleDistance) {
127 this.props.onSetOpen(!this.props.open);
128 }
129
130 this.setState({
131 touchIdentifier: null,
132 touchStartX: null,
133 touchStartY: null,
134 touchCurrentX: null,
135 touchCurrentY: null,
136 });
137 }
138 }
139
140 // This logic helps us prevents the user from sliding the sidebar horizontally
141 // while scrolling the sidebar vertically. When a scroll event comes in, we're
142 // cancelling the ongoing gesture if it did not move horizontally much.
143 onScroll() {
144 if (this.isTouching() && this.inCancelDistanceOnScroll()) {
145 this.setState({
146 touchIdentifier: null,
147 touchStartX: null,
148 touchStartY: null,
149 touchCurrentX: null,
150 touchCurrentY: null,
151 });
152 }
153 }
154
155 // True if the on going gesture X distance is less than the cancel distance
156 inCancelDistanceOnScroll() {
157 let cancelDistanceOnScroll;
158
159 if (this.props.pullRight) {
160 cancelDistanceOnScroll = Math.abs(this.state.touchCurrentX - this.state.touchStartX) <
161 CANCEL_DISTANCE_ON_SCROLL;
162 } else {
163 cancelDistanceOnScroll = Math.abs(this.state.touchStartX - this.state.touchCurrentX) <
164 CANCEL_DISTANCE_ON_SCROLL;
165 }
166 return cancelDistanceOnScroll;
167 }
168
169 isTouching() {
170 return this.state.touchIdentifier !== null;
171 }
172
173 overlayClicked() {
174 if (this.props.open) {
175 this.props.onSetOpen(false);
176 }
177 }
178
179 saveSidebarWidth() {
180 const width = ReactDOM.findDOMNode(this.refs.sidebar).offsetWidth;
181
182 if (width !== this.state.sidebarWidth) {
183 this.setState({sidebarWidth: width});
184 }
185 }
186
187 // calculate the sidebarWidth based on current touch info
188 touchSidebarWidth() {
189 // if the sidebar is open and start point of drag is inside the sidebar
190 // we will only drag the distance they moved their finger
191 // otherwise we will move the sidebar to be below the finger.
192 if (this.props.pullRight) {
193 if (this.props.open && window.innerWidth - this.state.touchStartX < this.state.sidebarWidth) {
194 if (this.state.touchCurrentX > this.state.touchStartX) {
195 return this.state.sidebarWidth + this.state.touchStartX - this.state.touchCurrentX;
196 }
197 return this.state.sidebarWidth;
198 }
199 return Math.min(window.innerWidth - this.state.touchCurrentX, this.state.sidebarWidth);
200 }
201
202 if (this.props.open && this.state.touchStartX < this.state.sidebarWidth) {
203 if (this.state.touchCurrentX > this.state.touchStartX) {
204 return this.state.sidebarWidth;
205 }
206 return this.state.sidebarWidth - this.state.touchStartX + this.state.touchCurrentX;
207 }
208 return Math.min(this.state.touchCurrentX, this.state.sidebarWidth);
209 }
210
211 render() {
212 const sidebarStyle = {...styles.sidebar};
213 const contentStyle = {...styles.content};
214 const overlayStyle = {...styles.overlay};
215 const useTouch = this.state.dragSupported && this.props.touch;
216 const isTouching = this.isTouching();
217 const rootProps = {
218 style: styles.root,
219 };
220 let dragHandle;
221
222 // sidebarStyle right/left
223 if (this.props.pullRight) {
224 sidebarStyle.right = 0;
225 sidebarStyle.transform = 'translateX(100%)';
226 sidebarStyle.WebkitTransform = 'translateX(100%)';
227 if (this.props.shadow) {
228 sidebarStyle.boxShadow = '-2px 2px 4px rgba(0, 0, 0, 0.15)';
229 }
230 } else {
231 sidebarStyle.left = 0;
232 sidebarStyle.transform = 'translateX(-100%)';
233 sidebarStyle.WebkitTransform = 'translateX(-100%)';
234 if (this.props.shadow) {
235 sidebarStyle.boxShadow = '2px 2px 4px rgba(0, 0, 0, 0.15)';
236 }
237 }
238
239 if (isTouching) {
240 const percentage = this.touchSidebarWidth() / this.state.sidebarWidth;
241
242 // slide open to what we dragged
243 if (this.props.pullRight) {
244 sidebarStyle.transform = `translateX(${(1 - percentage) * 100}%)`;
245 sidebarStyle.WebkitTransform = `translateX(${(1 - percentage) * 100}%)`;
246 } else {
247 sidebarStyle.transform = `translateX(-${(1 - percentage) * 100}%)`;
248 sidebarStyle.WebkitTransform = `translateX(-${(1 - percentage) * 100}%)`;
249 }
250
251 // fade overlay to match distance of drag
252 overlayStyle.opacity = percentage;
253 overlayStyle.visibility = 'visible';
254 } else if (this.props.docked) {
255 // show sidebar
256 if (this.state.sidebarWidth !== 0) {
257 sidebarStyle.transform = `translateX(0%)`;
258 sidebarStyle.WebkitTransform = `translateX(0%)`;
259 }
260
261 // make space on the left/right side of the content for the sidebar
262 if (this.props.pullRight) {
263 contentStyle.right = `${this.state.sidebarWidth}px`;
264 } else {
265 contentStyle.left = `${this.state.sidebarWidth}px`;
266 }
267 } else if (this.props.open) {
268 // slide open sidebar
269 sidebarStyle.transform = `translateX(0%)`;
270 sidebarStyle.WebkitTransform = `translateX(0%)`;
271
272 // show overlay
273 overlayStyle.opacity = 1;
274 overlayStyle.visibility = 'visible';
275 }
276
277 if (isTouching || !this.props.transitions) {
278 sidebarStyle.transition = 'none';
279 sidebarStyle.WebkitTransition = 'none';
280 contentStyle.transition = 'none';
281 overlayStyle.transition = 'none';
282 }
283
284 if (useTouch) {
285 if (this.props.open) {
286 rootProps.onTouchStart = this.onTouchStart;
287 rootProps.onTouchMove = this.onTouchMove;
288 rootProps.onTouchEnd = this.onTouchEnd;
289 rootProps.onTouchCancel = this.onTouchEnd;
290 rootProps.onScroll = this.onScroll;
291 } else {
292 const dragHandleStyle = {...styles.dragHandle};
293 dragHandleStyle.width = this.props.touchHandleWidth;
294
295 // dragHandleStyle right/left
296 if (this.props.pullRight) {
297 dragHandleStyle.right = 0;
298 } else {
299 dragHandleStyle.left = 0;
300 }
301
302 dragHandle = (
303 <div style={dragHandleStyle}
304 onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
305 onTouchEnd={this.onTouchEnd} onTouchCancel={this.onTouchEnd} />);
306 }
307 }
308
309 return (
310 <div {...rootProps}>
311 <div style={sidebarStyle} ref="sidebar">
312 {this.props.sidebar}
313 </div>
314 <div style={overlayStyle}
315 onClick={this.overlayClicked} onTouchTap={this.overlayClicked} />
316 <div style={contentStyle}>
317 {dragHandle}
318 {this.props.children}
319 </div>
320 </div>
321 );
322 }
323}
324
325Sidebar.propTypes = {
326 // main content to render
327 children: React.PropTypes.node.isRequired,
328
329 // sidebar content to render
330 sidebar: React.PropTypes.node.isRequired,
331
332 // boolean if sidebar should be docked
333 docked: React.PropTypes.bool,
334
335 // boolean if sidebar should slide open
336 open: React.PropTypes.bool,
337
338 // boolean if transitions should be disabled
339 transitions: React.PropTypes.bool,
340
341 // boolean if touch gestures are enabled
342 touch: React.PropTypes.bool,
343
344 // max distance from the edge we can start touching
345 touchHandleWidth: React.PropTypes.number,
346
347 // Place the sidebar on the right
348 pullRight: React.PropTypes.bool,
349
350 // Enable/Disable sidebar shadow
351 shadow: React.PropTypes.bool,
352
353 // distance we have to drag the sidebar to toggle open state
354 dragToggleDistance: React.PropTypes.number,
355
356 // callback called when the overlay is clicked
357 onSetOpen: React.PropTypes.func,
358};
359
360Sidebar.defaultProps = {
361 docked: false,
362 open: false,
363 transitions: true,
364 touch: true,
365 touchHandleWidth: 20,
366 pullRight: false,
367 shadow: true,
368 dragToggleDistance: 30,
369 onSetOpen: () => {},
370};
371
372export default Sidebar;