diff --git a/site/src/app/roadmap/MobilePopup.scss b/site/src/app/roadmap/MobilePopup.scss index 792b0f905..e593b5949 100644 --- a/site/src/app/roadmap/MobilePopup.scss +++ b/site/src/app/roadmap/MobilePopup.scss @@ -9,7 +9,8 @@ flex-direction: column; flex-shrink: 0; overflow: auto; - border-radius: 8px 8px 0 0; + overscroll-behavior: contain; + border-radius: 16px 16px 0 0; // prevents child element z index escaping (i.e. any child of this element // may not appear above any element outside of the .program-requirements element) diff --git a/site/src/app/roadmap/MobilePopup.tsx b/site/src/app/roadmap/MobilePopup.tsx index 703a0c7a3..c6f568234 100644 --- a/site/src/app/roadmap/MobilePopup.tsx +++ b/site/src/app/roadmap/MobilePopup.tsx @@ -16,6 +16,78 @@ const MobilePopup: FC = ({ show, onClose, className, id, child const isMobile = useIsMobile(); const overlayRef = useRef(null); const popupRef = useRef(null); + const popupTouchStartY = useRef(0); + const overlayTouchStartY = useRef(0); + + useEffect(() => { + const element = popupRef.current; + const overlay = overlayRef.current; + let isScrolling: boolean = false; + + if (!element) return; + + const handleTouchStart = (e: TouchEvent) => { + if (element.scrollTop !== 0) { + isScrolling = true; + } + popupTouchStartY.current = e.touches[0].clientY; + element.style.transition = 'none'; + }; + + const handleTouchEnd = (e: TouchEvent) => { + if (e.changedTouches[0].clientY - popupTouchStartY.current > 400 && !isScrolling) { + onClose(); + } + element.style.transform = ''; + element.style.transition = ''; + isScrolling = false; + }; + + const handleTouchMovePopup = (e: TouchEvent) => { + const delta = e.touches[0].clientY - popupTouchStartY.current; + if (!isScrolling) { + e.preventDefault(); + element.style.transform = delta > 0 ? `translateY(${delta}px)` : 'translateY(0)'; + } + }; + + const handleOverLayTouchStart = (e: TouchEvent) => { + overlayTouchStartY.current = e.touches[0].clientY; + element.style.transition = 'none'; + }; + + const handleOverlayTouchEnd = (e: TouchEvent) => { + if (e.changedTouches[0].clientY - overlayTouchStartY.current > 50) { + onClose(); + } + element.style.transform = ''; + element.style.transition = ''; + }; + + const handleOverlayTouchMove = (e: TouchEvent) => { + const delta = e.touches[0].clientY - overlayTouchStartY.current; + if (delta > 0) { + element.style.transform = `translateY(${delta}px)`; + } + }; + + element?.addEventListener('touchstart', handleTouchStart); + element?.addEventListener('touchend', handleTouchEnd); + element?.addEventListener('touchmove', handleTouchMovePopup, { passive: false }); + + overlay?.addEventListener('touchstart', handleOverLayTouchStart); + overlay?.addEventListener('touchend', handleOverlayTouchEnd); + overlay?.addEventListener('touchmove', handleOverlayTouchMove); + + return () => { + element.removeEventListener('touchstart', handleTouchStart); + element.removeEventListener('touchend', handleTouchEnd); + element.removeEventListener('touchmove', handleTouchMovePopup, { passive: false } as EventListenerOptions); + overlay?.removeEventListener('touchstart', handleOverLayTouchStart); + overlay?.removeEventListener('touchend', handleOverlayTouchEnd); + overlay?.removeEventListener('touchmove', handleOverlayTouchMove); + }; + }, [show, onClose]); useEffect(() => { if (!isMobile) return; diff --git a/site/src/globals.scss b/site/src/globals.scss index be2ae9949..3c3d193c8 100644 --- a/site/src/globals.scss +++ b/site/src/globals.scss @@ -46,7 +46,7 @@ $mui-transition-time: 268ms; width: 100%; bottom: 0; z-index: $zIndex; - max-height: calc(100% - 40px); + max-height: calc(100% - 53px); padding-bottom: 56px; transform: translateY(100%); transition: transform 0.3s;