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)