Files
apps/components/BottomSheet.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

194 lines
5.9 KiB
JavaScript

css(`
bottomsheet- {
scrollbar-width: none;
-ms-overflow-style: none;
}
bottomsheet- .VStack::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
bottomsheet- .VStack::-webkit-scrollbar-thumb {
background: transparent;
}
bottomsheet- .VStack::-webkit-scrollbar-track {
background: transparent;
}
`)
class BottomSheet extends Shadow {
swipeDragStartX = null;
swipeDragStartY = null;
swipeDragStartTime = null;
isSwiping = false;
swipeAxisLocked = false;
swipeIsVertical = false;
swipeTranslate = 0;
SWIPE_COMMIT_DISTANCE = window.outerHeight * 0.25;
SWIPE_VELOCITY_THRESHOLD = 0.4;
renderContent = null;
_closeOverride = null;
constructor(zOffset = 0) {
super()
this.zOffset = zOffset;
}
show(renderFn, onClose = null) {
this._closeOverride = null;
this.renderContent = renderFn;
this._onClose = onClose;
this.rerender();
requestAnimationFrame(() => requestAnimationFrame(() => this.setSheet(true)));
}
replace(renderFn) {
this.renderContent = renderFn;
this.rerender();
if (this.sheetEl) {
this.sheetEl.style.transition = "none";
this.sheetEl.style.top = "7.5%";
this.sheetEl.style.pointerEvents = "auto";
}
if (this.overlayEl) {
this.overlayEl.style.transition = "none";
this.overlayEl.style.background = "rgba(0, 0, 0, 0.3)";
this.overlayEl.style.pointerEvents = "auto";
}
}
render() {
ZStack(() => {
this.overlayEl = ZStack()
.position("fixed")
.inset(0)
.zIndex(90 + this.zOffset)
.background("transparent")
.pointerEvents("none")
.onTap(() => this.toggle())
this.sheetEl = ZStack(() => {
VStack(() => {
if (this.renderContent) this.renderContent()
})
.height(100, pct)
.position("relative")
})
.position("fixed")
.height(92.5, pct)
.top(100, pct)
.left(0).right(0)
.zIndex(100 + this.zOffset)
.background("var(--main)")
.borderTopLeftRadius("10px").borderTopRightRadius("10px")
.boxSizing("border-box")
.transition("top .3s")
.pointerEvents("none")
.onTouch((start, e) => this.handleSwipeTouch(start, e))
})
}
get isOpen() {
return this.sheetEl?.style.top === "7.5%";
}
toggle() {
this.setSheet(!this.isOpen);
}
forceClose() {
this._closeOverride = null
this.setSheet(false)
}
setSheet(open) {
if (!open && this._closeOverride) { this._closeOverride(); return }
if (this.overlayEl) {
this.overlayEl.style.transition = "background 0.3s";
this.overlayEl.style.pointerEvents = open ? "auto" : "none";
this.overlayEl.style.background = open ? "rgba(0, 0, 0, 0.3)" : "transparent";
}
if (this.sheetEl) {
this.sheetEl.style.transition = "top 0.3s";
this.sheetEl.style.top = open ? "7.5%" : "100%";
this.sheetEl.style.pointerEvents = open ? "auto" : "none";
}
if (!open) {
if (this._onClose) {
const cb = this._onClose;
this._onClose = null;
setTimeout(cb, 300);
}
this.isSwiping = false;
this.swipeTranslate = 0;
this.swipeDragStartX = null;
this.swipeDragStartY = null;
document.removeEventListener("touchmove", this.onSwipeMove);
}
}
handleSwipeTouch(start, e) {
if (start) {
if (this.sheetEl?.style.top !== "7.5%") return;
e.stopPropagation();
this.swipeDragStartX = e.touches[0].clientX;
this.swipeDragStartY = e.touches[0].clientY;
this.swipeDragStartTime = Date.now();
this.isSwiping = true;
this.swipeAxisLocked = false;
this.swipeIsVertical = false;
document.addEventListener("touchmove", this.onSwipeMove, { passive: false });
} else {
if (!this.isSwiping) return;
document.removeEventListener("touchmove", this.onSwipeMove);
if (!this.swipeIsVertical) {
this.isSwiping = false;
this.setSheet(true);
return;
}
const delta = e.changedTouches[0].clientY - this.swipeDragStartY;
const velocity = Math.abs(delta) / (Date.now() - this.swipeDragStartTime);
const shouldCommit = delta > 0 && (delta > this.SWIPE_COMMIT_DISTANCE || velocity > this.SWIPE_VELOCITY_THRESHOLD);
this.swipeTranslate = 0;
this.setSheet(!shouldCommit);
this.isSwiping = false;
this.swipeDragStartX = null;
this.swipeDragStartY = null;
}
}
onSwipeMove = (e) => {
if (!this.isSwiping) return;
const dx = e.touches[0].clientX - this.swipeDragStartX;
const dy = e.touches[0].clientY - this.swipeDragStartY;
if (!this.swipeAxisLocked) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
this.swipeAxisLocked = true;
this.swipeIsVertical = Math.abs(dy) > Math.abs(dx);
}
if (!this.swipeIsVertical) return;
const delta = e.touches[0].clientY - this.swipeDragStartY;
const scrollEl = this.querySelector(".VStack");
if (delta <= 0 || (scrollEl && scrollEl.scrollTop > 0)) return;
e.preventDefault();
this.swipeTranslate = delta;
this.sheetEl.style.transition = "";
this.sheetEl.style.top = `calc(7.5% + ${delta}px)`;
}
}
register(BottomSheet)