UNPKG

bottom-sheet

Version:

Bottom Sheet implemented as a Vanilla Web Component

157 lines (156 loc) 5.61 kB
import { styler, inertia, listen, pointer, value, calc, tween } from 'popmotion'; import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock/lib/bodyScrollLock.es6.js'; import { cubicBezier } from '@popmotion/easing'; const mix = calc.getValueFromProgress; export class BottomSheet { constructor() { this.subscriptions = []; this.dragging = false; this.progress = 0; this.arrow = false; this.initialPosition = 'bottom'; this.onValueChange = (v) => { this.progress = 1 - (v / this.boundaryHeight); this._screen.progress = this.progress; }; this.boundaryHeight = 0; this.setBoundariesHeight = () => { this.boundaryHeight = this.containerEl.getBoundingClientRect().height - this.sheetEl.getBoundingClientRect().height; }; } draggingChanged() { const action = this.dragging ? disableBodyScroll : enableBodyScroll; action(this.containerEl); } async animateSheet(to, tweenProps) { const target = this.sheetY; const from = target.get(); if (from === to) return Promise.resolve(); return new Promise((resolve) => { const opts = Object.assign({ from, to }, tweenProps); const sub = this.sheetY.subscribe((v) => { if (v === opts.to) { resolve(); sub.unsubscribe(); return; } }); tween(opts).start(target); }); } async open() { return this.animateSheet(0, { ease: cubicBezier(0.23, 1, 0.320, 1) }); } async close() { return this.animateSheet(this.boundaryHeight, { ease: cubicBezier(0.23, 1, 0.320, 1) }); } async componentWillLoad() { this._screen = await this.screen.componentOnReady(); } componentDidLoad() { this._screen.connectedBottomSheet = this.element; this.setBoundariesHeight(); this.sheetStyler = styler(this.sheetEl); const initialY = (this.initialPosition === 'bottom') ? this.boundaryHeight : 0; this.sheetY = value(initialY, v => this.sheetStyler.set('y', v)); this.subscriptions = [...this.subscriptions, this.sheetY.subscribe(this.onValueChange)]; listen(this.sheetEl, 'mousedown touchstart').start(() => { this.dragging = true; const max = this.boundaryHeight; const tug = 0.2; const applyOverdrag = v => { if (v < 0) return mix(0, v, tug); if (v > max) return mix(max, v, tug); return v; }; this.pointer = pointer({ y: this.sheetY.get() }) .pipe(({ y }) => y, applyOverdrag); this.pointer.start(this.sheetY); }); listen(document, 'mouseup touchend').start(() => { this.dragging = false; this.intertia = inertia({ min: 0, max: this.boundaryHeight, from: this.sheetY.get(), velocity: this.sheetY.getVelocity(), power: 0.2, bounceStiffness: 400, bounceDamping: 22.5 }); this.intertia.start(this.sheetY); }); } componentDidUnload() { for (const subscription of this.subscriptions) { subscription.unsubscribe(); } this.subscriptions = undefined; clearAllBodyScrollLocks(); } resizeHandler() { console.log('resize'); this.setBoundariesHeight(); } hostData() { return { class: { 'is-dragging': this.dragging, 'is-closed': this.progress <= 0.09, 'is-open': this.progress >= 0.99 }, style: { '--progress': `${this.progress}` } }; } render() { return (h("div", { class: "container", ref: el => this.containerEl = el }, h("div", { class: "sheet", ref: el => this.sheetEl = el }, h("div", { class: "sheet-header" }, this.arrow && h("bottom-sheet-indicator", null), h("slot", { name: "sheet-header" })), h("div", { class: "sheet-content" }, h("slot", null))))); } static get is() { return "bottom-sheet"; } static get encapsulation() { return "shadow"; } static get properties() { return { "arrow": { "type": Boolean, "attr": "arrow" }, "close": { "method": true }, "dragging": { "state": true, "watchCallbacks": ["draggingChanged"] }, "element": { "elementRef": true }, "initialPosition": { "type": String, "attr": "initial-position" }, "open": { "method": true }, "progress": { "state": true }, "screen": { "connect": "bottom-sheet-screen" } }; } static get listeners() { return [{ "name": "window:resize", "method": "resizeHandler", "passive": true }]; } static get style() { return "/**style-placeholder:bottom-sheet:**/"; } }