bottom-sheet
Version:
Bottom Sheet implemented as a Vanilla Web Component
157 lines (156 loc) • 5.61 kB
JavaScript
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:**/"; }
}