Created
June 1, 2019 11:16
-
-
Save tahnik/21df52acdb4fcf4895917e16182c6b26 to your computer and use it in GitHub Desktop.
Material Slidable Bottom Sheet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<!-- c here means custom --> | |
<div class="c-bottom-sheet"> | |
<div | |
:class="`overlay ${value ? 'overlay-active' : ''}`" | |
@click="closeSheet()" | |
></div> | |
<div :class="`content ${contentActiveClass} ${uid}`" id="bottom-sheet"> | |
<slot></slot> | |
</div> | |
</div> | |
</template> | |
<script> | |
const TOUCH_FLAGS = { | |
START: 0, | |
DRAG_DOWN: 1, | |
DRAG_UP: 2, | |
}; | |
export default { | |
props: [ | |
'value', | |
'noExpand', | |
], | |
data() { | |
return { | |
touchFlag: null, | |
content: null, | |
translateX: 0, | |
dismissable: false, | |
trackDirectionY: null, | |
tDYChange: null, | |
scrollable: false, | |
expanded: false, | |
uid: null, | |
popped: false, | |
}; | |
}, | |
beforeMount() { | |
this.uid = Math.random().toString(36); | |
}, | |
mounted() { | |
[this.content] = document.getElementsByClassName(this.uid); | |
window.addEventListener('popstate', this.handlePop, false); | |
// Some sheets are not expandable (like nav drawer) | |
if (this.noExpand === true) { | |
return; | |
} | |
this.content.addEventListener('touchstart', this.handleTouchStart, false); | |
this.content.addEventListener('touchmove', this.handleTouchMove, false); | |
this.content.addEventListener('touchend', this.handleTouchEnd, false); | |
this.content.addEventListener('touchcancel', this.handleTouchCancel, false); | |
}, | |
beforeDestroy() { | |
window.removeEventListener('popstate', this.handlePop); | |
}, | |
watch: { | |
value(newV) { | |
if (newV === true) { | |
/** | |
* Each time a sheet is open, add a extra route to the history. | |
* This is to prevent the phone's back button from changing the route | |
* when a sheet is open. The back button will close the sheet instead. | |
*/ | |
window.history.pushState({}, ''); | |
} | |
}, | |
}, | |
computed: { | |
contentActiveClass() { | |
if (!this.value) { | |
return; | |
} | |
if (this.noExpand !== true) { | |
return 'content-active'; | |
} | |
return 'content-active-no-expand'; | |
}, | |
}, | |
methods: { | |
handlePop() { | |
this.popped = true; | |
this.closeSheet(); | |
}, | |
closeSheet() { | |
this.content.removeAttribute('style'); | |
this.$emit('input', false); | |
this.scrollable = false; | |
this.expanded = false; | |
/** | |
* If the user closed the sheet by tapping outside the sheet | |
* The history will have an extra route. Which means the user will | |
* have to back twice to go to the previous route. | |
* We manually remove that route to prevent such behavior. | |
*/ | |
if (this.popped === false) { | |
this.$router.back(); | |
} | |
this.popped = false; | |
}, | |
handleTouchStart(e) { | |
this.flag = TOUCH_FLAGS.START; | |
this.trackDirectionY = e.touches[0].clientY; | |
this.tDYChange = e.changedTouches[0].clientY; | |
if (this.content.scrollTop < 20) { | |
// Means we are ready to close the bottom sheet | |
this.dismissable = true; | |
} | |
}, | |
handleTouchMove(e) { | |
/** | |
* This prevents scrolling especially when the sheet is in the | |
* initial state | |
*/ | |
if (this.scrollable === false) { | |
e.preventDefault(); | |
} | |
const { content } = this; | |
// Used for checking if user is dragging up or down | |
const tDYChangeTemp = e.changedTouches[0].clientY; | |
if ( | |
this.trackDirectionY > this.tDYChange | |
|| this.tDYChange > tDYChangeTemp | |
) { | |
this.flag = TOUCH_FLAGS.DRAG_UP; | |
} else if ( | |
this.trackDirectionY < this.tDYChange | |
&& this.tDYChange < tDYChangeTemp | |
) { | |
this.flag = TOUCH_FLAGS.DRAG_DOWN; | |
} | |
this.tDYChange = tDYChangeTemp; | |
if (this.dismissable && this.flag === TOUCH_FLAGS.DRAG_DOWN) { | |
/** | |
* Show the pull down animation only if the bottom sheet is closable | |
*/ | |
if (this.expanded) { | |
content.style.transform = 'translateY(10%) translateZ(0)'; | |
} else { | |
content.style.transform = 'translateY(50%) translateZ(0)'; | |
} | |
this.translateX += 1.5; | |
// This prevents scrolling when the sheet is being pulled | |
this.scrollable = false; | |
} | |
if ( | |
this.translateX > 100 | |
|| (this.dismissable && this.flag === TOUCH_FLAGS.DRAG_UP) | |
) { | |
/** | |
* Not sure about the future of this code. | |
* Essentially, if the user has pulled for more than 100 pixels | |
* we are just going to pull the sheet up again (basically, reset everything). | |
* This means the user has to quickly pull down to close the sheet. | |
*/ | |
content.style.transition = 'transform 0.5s'; | |
content.style.transform = 'translateY(0px) translateZ(0)'; | |
this.dismissable = false; | |
this.translateX = 0; | |
this.expanded = true; | |
// Enable scrolling after the end of animation | |
setTimeout(() => { | |
this.scrollable = true; | |
}, 500); | |
} | |
}, | |
handleTouchEnd() { | |
const { content } = this; | |
// Pull down animation might have overridden default transition. Set it back again. | |
content.style.transition = 'all 0.3s cubic-bezier(0.0, 0.0, 0.2, 1)'; | |
// // This is to remove the custom translateY set by pull down animation | |
// content.removeAttribute('style'); | |
if (this.flag === TOUCH_FLAGS.START) { | |
// Just a click, we don't care | |
this.flag = null; | |
console.log('Just a click'); | |
} else if (this.flag === TOUCH_FLAGS.DRAG_DOWN) { | |
this.flag = null; | |
this.translateX = 0; | |
if (this.dismissable) { | |
this.closeSheet(); | |
this.dismissable = false; | |
} | |
} | |
}, | |
handleTouchCancel() { | |
this.content.style.transition = 'all 0.3s cubic-bezier(0.0, 0.0, 0.2, 1)'; | |
}, | |
}, | |
}; | |
</script> | |
<style lang="scss"> | |
.c-bottom-sheet { | |
.content { | |
overscroll-behavior: contain; | |
max-height: 90vh; | |
background: white; | |
width: 100%; | |
position: fixed; | |
z-index: 300; | |
bottom: 0; | |
border-top-right-radius: 16px; | |
border-top-left-radius: 16px; | |
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.22), 0px 14px 56px rgba(0, 0, 0, 0.25); | |
will-change: transform; | |
transition: all 0.3s cubic-bezier(0.0, 0.0, 0.2, 1); | |
transform: translateY(100%) translateZ(0); | |
overflow-y: scroll; | |
overflow-x: hidden; | |
-webkit-font-smoothing: antialiased; | |
} | |
.overlay { | |
position: fixed; | |
top: 0; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
z-index: 299; | |
background: rgba(0, 0, 0, 0.3); | |
opacity: 0; | |
transition: opacity 0.25s; | |
pointer-events: none; | |
} | |
.overlay-active { | |
opacity: 1; | |
pointer-events: initial; | |
} | |
.content-active { | |
transform: translateY(40%) translateZ(0); | |
} | |
.content-active-no-expand { | |
transform: translateY(0) translateZ(0); | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment