Skip to content

Instantly share code, notes, and snippets.

@tahnik
Created June 1, 2019 11:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tahnik/21df52acdb4fcf4895917e16182c6b26 to your computer and use it in GitHub Desktop.
Save tahnik/21df52acdb4fcf4895917e16182c6b26 to your computer and use it in GitHub Desktop.
Material Slidable Bottom Sheet
<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