feat: add PWA sidebar edge swipe
This commit is contained in:
1 parent
3a98700a6b
commit
b0f7a7bdff
3 files changed
+112
No files matched your search
@@ -3,6 +3,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Installed/mobile PWA sessions now support an edge swipe from the left side of the screen to open the mobile sidebar drawer, while preserving the existing hamburger and overlay controls.
|
||||
|
||||
## [v0.51.129] — 2026-05-24 — Release DA (stage-batch11 — 4-PR feature + perf batch)
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -228,6 +228,71 @@ function closeMobileSidebar(){
|
||||
if(overlay)overlay.classList.remove('visible');
|
||||
}
|
||||
|
||||
const _PWA_SIDEBAR_SWIPE_EDGE=28;
|
||||
const _PWA_SIDEBAR_SWIPE_TRIGGER=72;
|
||||
const _PWA_SIDEBAR_SWIPE_MAX_VERTICAL=48;
|
||||
let _pwaSidebarSwipe=null;
|
||||
|
||||
function _isPwaStandalone(){
|
||||
try{
|
||||
return document.documentElement.classList.contains('pwa-standalone')
|
||||
|| window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone===true;
|
||||
}catch(_){return false;}
|
||||
}
|
||||
|
||||
function _isInteractiveSwipeTarget(target){
|
||||
try{return !!(target&&target.closest&&target.closest('input,textarea,select,button,a,[contenteditable="true"],.topbar-chips,.composer-left,.sidebar,.rightpanel'));}
|
||||
catch(_){return false;}
|
||||
}
|
||||
|
||||
function _openMobileSidebarFromGesture(){
|
||||
if(_isDesktopWidth())return;
|
||||
const sidebar=document.querySelector('.sidebar');
|
||||
const overlay=$('mobileOverlay');
|
||||
if(!sidebar)return;
|
||||
const layout=document.querySelector('.layout');
|
||||
if(layout)layout.classList.remove('sidebar-collapsed');
|
||||
sidebar.classList.remove('sidebar-collapsed');
|
||||
try{document.documentElement.removeAttribute('data-sidebar-collapsed');}catch(_){}
|
||||
sidebar.classList.add('mobile-open');
|
||||
if(overlay)overlay.classList.add('visible');
|
||||
}
|
||||
|
||||
function _onPwaSidebarSwipeStart(e){
|
||||
if(!_isPwaStandalone()||_isDesktopWidth())return;
|
||||
if(e.pointerType==='mouse'||(e.pointerType&&e.pointerType!=='touch'&&e.pointerType!=='pen'))return;
|
||||
if(document.querySelector('.sidebar')?.classList.contains('mobile-open'))return;
|
||||
const clientX=Number(e.clientX)||0;
|
||||
if(clientX>_PWA_SIDEBAR_SWIPE_EDGE)return;
|
||||
if(_isInteractiveSwipeTarget(e.target))return;
|
||||
_pwaSidebarSwipe={startX:clientX,startY:Number(e.clientY)||0,active:true,opened:false};
|
||||
}
|
||||
|
||||
function _onPwaSidebarSwipeMove(e){
|
||||
const swipe=_pwaSidebarSwipe;
|
||||
if(!swipe||!swipe.active||swipe.opened)return;
|
||||
const dx=(Number(e.clientX)||0)-swipe.startX;
|
||||
const dy=(Number(e.clientY)||0)-swipe.startY;
|
||||
if(dx<0||Math.abs(dy)>_PWA_SIDEBAR_SWIPE_MAX_VERTICAL*1.5){_pwaSidebarSwipe=null;return;}
|
||||
if(dx>=_PWA_SIDEBAR_SWIPE_TRIGGER&&Math.abs(dy)<=_PWA_SIDEBAR_SWIPE_MAX_VERTICAL&&dx>Math.abs(dy)*1.5){
|
||||
if(e.cancelable)e.preventDefault();
|
||||
swipe.opened=true;
|
||||
_openMobileSidebarFromGesture();
|
||||
}
|
||||
}
|
||||
|
||||
function _onPwaSidebarSwipeEnd(){_pwaSidebarSwipe=null;}
|
||||
function _onPwaSidebarSwipeCancel(){_pwaSidebarSwipe=null;}
|
||||
|
||||
function _installPwaSidebarSwipeGesture(){
|
||||
window.addEventListener('pointerdown', _onPwaSidebarSwipeStart, {passive:true});
|
||||
window.addEventListener('pointermove', _onPwaSidebarSwipeMove, {passive:false});
|
||||
window.addEventListener('pointerup', _onPwaSidebarSwipeEnd, {passive:true});
|
||||
window.addEventListener('pointercancel', _onPwaSidebarSwipeCancel, {passive:true});
|
||||
}
|
||||
_installPwaSidebarSwipeGesture();
|
||||
|
||||
// ── Desktop sidebar collapse toggle ────────────────────────────────────────
|
||||
// Two discoverability paths into the same state:
|
||||
// (1) Click the already-active rail icon → collapse / expand the sidebar.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_pwa_edge_swipe_gesture_is_registered_for_mobile_sidebar():
|
||||
assert "function _installPwaSidebarSwipeGesture" in BOOT_JS
|
||||
assert "window.addEventListener('pointerdown', _onPwaSidebarSwipeStart" in BOOT_JS
|
||||
assert "window.addEventListener('pointermove', _onPwaSidebarSwipeMove" in BOOT_JS
|
||||
assert "window.addEventListener('pointerup', _onPwaSidebarSwipeEnd" in BOOT_JS
|
||||
assert "window.addEventListener('pointercancel', _onPwaSidebarSwipeCancel" in BOOT_JS
|
||||
|
||||
|
||||
def test_pwa_sidebar_swipe_is_edge_gated_standalone_and_horizontal():
|
||||
assert "_isPwaStandalone()" in BOOT_JS
|
||||
assert "_PWA_SIDEBAR_SWIPE_EDGE" in BOOT_JS
|
||||
assert "_PWA_SIDEBAR_SWIPE_TRIGGER" in BOOT_JS
|
||||
assert "_PWA_SIDEBAR_SWIPE_MAX_VERTICAL" in BOOT_JS
|
||||
assert "clientX>_PWA_SIDEBAR_SWIPE_EDGE" in BOOT_JS.replace(" ", "")
|
||||
assert "dx>=_PWA_SIDEBAR_SWIPE_TRIGGER" in BOOT_JS.replace(" ", "")
|
||||
assert "Math.abs(dy)<=_PWA_SIDEBAR_SWIPE_MAX_VERTICAL" in BOOT_JS.replace(" ", "")
|
||||
assert "dx>Math.abs(dy)*1.5" in BOOT_JS.replace(" ", "")
|
||||
|
||||
assert "input,textarea,select,button,a,[contenteditable=\"true\"],.topbar-chips,.composer-left,.sidebar,.rightpanel" in BOOT_JS
|
||||
assert ".messages" not in BOOT_JS[BOOT_JS.find("function _isInteractiveSwipeTarget"):BOOT_JS.find("function _openMobileSidebarFromGesture")]
|
||||
|
||||
|
||||
def test_pwa_sidebar_swipe_opens_existing_mobile_drawer_without_desktop_collapse():
|
||||
assert "_openMobileSidebarFromGesture" in BOOT_JS
|
||||
assert "sidebar.classList.remove('sidebar-collapsed')" in BOOT_JS
|
||||
assert "sidebar.classList.add('mobile-open')" in BOOT_JS
|
||||
assert "overlay.classList.add('visible')" in BOOT_JS
|
||||
assert "toggleSidebar(" not in BOOT_JS[BOOT_JS.find("function _openMobileSidebarFromGesture"):BOOT_JS.find("function _installPwaSidebarSwipeGesture")]
|
||||
|
||||
|
||||
def test_pwa_sidebar_swipe_does_not_disable_horizontal_scrollers_globally():
|
||||
compact = STYLE_CSS.replace(" ", "")
|
||||
assert "html{touch-action" not in compact
|
||||
assert "body{touch-action" not in compact
|
||||
assert ".layout{touch-action" not in compact
|
||||
Reference in new issue
Block a user