Checkpoint polished session swipe interactions
This commit is contained in:
parent
1084afd37f
commit
ca6736407e
3 changed files with 183 additions and 116 deletions
|
|
@ -3615,6 +3615,9 @@ function renderSessionListFromCache(){
|
||||||
menuBtn.setAttribute('aria-haspopup','menu');
|
menuBtn.setAttribute('aria-haspopup','menu');
|
||||||
menuBtn.setAttribute('aria-label','Conversation actions');
|
menuBtn.setAttribute('aria-label','Conversation actions');
|
||||||
menuBtn.innerHTML=ICONS.more;
|
menuBtn.innerHTML=ICONS.more;
|
||||||
|
const stopMenuPointer=(e)=>e.stopPropagation();
|
||||||
|
menuBtn.onpointerdown=stopMenuPointer;
|
||||||
|
menuBtn.onpointerup=stopMenuPointer;
|
||||||
menuBtn.onclick=(e)=>{
|
menuBtn.onclick=(e)=>{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -3637,6 +3640,7 @@ function renderSessionListFromCache(){
|
||||||
const _makeSessionSwipeAffordance=(side,icon,label)=>{
|
const _makeSessionSwipeAffordance=(side,icon,label)=>{
|
||||||
const affordance=document.createElement('div');
|
const affordance=document.createElement('div');
|
||||||
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
|
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
|
||||||
|
affordance.setAttribute('aria-hidden','true');
|
||||||
const badge=document.createElement('span');
|
const badge=document.createElement('span');
|
||||||
badge.className='session-swipe-badge';
|
badge.className='session-swipe-badge';
|
||||||
badge.innerHTML=li(icon,18);
|
badge.innerHTML=li(icon,18);
|
||||||
|
|
@ -3653,10 +3657,10 @@ function renderSessionListFromCache(){
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use pointerup + manual double-tap detection instead of onclick/ondblclick.
|
// Use release events + manual double-tap detection instead of onclick/ondblclick.
|
||||||
// onclick/ondblclick are unreliable on touch devices (iPad Safari especially):
|
// onclick/ondblclick are unreliable on touch devices (iPad Safari especially):
|
||||||
// hover-triggered layout shifts, ghost clicks, and 300ms delay all break
|
// hover-triggered layout shifts, ghost clicks, and 300ms delay all break
|
||||||
// single-tap navigation. pointerup fires immediately on both mouse & touch.
|
// single-tap navigation.
|
||||||
// Mouse clicks are instant; touch presses need a 300ms delay to distinguish
|
// Mouse clicks are instant; touch presses need a 300ms delay to distinguish
|
||||||
// a tap from a scroll-drag gesture on mobile.
|
// a tap from a scroll-drag gesture on mobile.
|
||||||
// Movement promotes pressing into dragging; drag release cancels a pending tap.
|
// Movement promotes pressing into dragging; drag release cancels a pending tap.
|
||||||
|
|
@ -3673,15 +3677,16 @@ function renderSessionListFromCache(){
|
||||||
let _pointerY=0;
|
let _pointerY=0;
|
||||||
let _gesturePointerType='';
|
let _gesturePointerType='';
|
||||||
const _longPressDelay=400;
|
const _longPressDelay=400;
|
||||||
const _swipeActionThreshold=144;
|
const _archiveSwipeActionThreshold=128;
|
||||||
|
const _deleteSwipeActionThreshold=128;
|
||||||
const _swipeCancelRatio=0.75;
|
const _swipeCancelRatio=0.75;
|
||||||
const _committedSwipeDuration=_sessionPrefersReducedMotion()?0:420;
|
const _committedSwipeDuration=_sessionPrefersReducedMotion()?0:420;
|
||||||
const _clearLongPressTimer=()=>{
|
const _clearLongPressTimer=()=>{
|
||||||
if(_longPressTimer){clearTimeout(_longPressTimer);_longPressTimer=null;}
|
if(_longPressTimer){clearTimeout(_longPressTimer);_longPressTimer=null;}
|
||||||
if(!_longPressMenuOpened) el.classList.remove('long-pressing');
|
if(!_longPressMenuOpened) el.classList.remove('long-pressing');
|
||||||
};
|
};
|
||||||
const _beginSessionGesture=(clientX,clientY)=>{
|
const _beginSessionGesture=(clientX,clientY,pointerType='')=>{
|
||||||
_gesturePointerType='';
|
_gesturePointerType=pointerType;
|
||||||
_pointerDownX=clientX;
|
_pointerDownX=clientX;
|
||||||
_pointerDownY=clientY;
|
_pointerDownY=clientY;
|
||||||
_pointerX=clientX;
|
_pointerX=clientX;
|
||||||
|
|
@ -3690,7 +3695,9 @@ function renderSessionListFromCache(){
|
||||||
_swipeTracking=false;
|
_swipeTracking=false;
|
||||||
_longPressMenuOpened=false;
|
_longPressMenuOpened=false;
|
||||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||||
el.classList.remove('dragging','swipe-committed');
|
el.classList.remove('dragging','swipe-committed','swipe-removing');
|
||||||
|
el.style.removeProperty('height');
|
||||||
|
el.style.removeProperty('min-height');
|
||||||
};
|
};
|
||||||
const _scheduleSessionLongPressMenu=()=>{
|
const _scheduleSessionLongPressMenu=()=>{
|
||||||
_clearLongPressTimer();
|
_clearLongPressTimer();
|
||||||
|
|
@ -3707,8 +3714,11 @@ function renderSessionListFromCache(){
|
||||||
const _isSessionSwipeTarget=()=>{
|
const _isSessionSwipeTarget=()=>{
|
||||||
return _gesturePointerType!=='mouse'&&!readOnly&&!_renamingSid&&!_sessionSelectMode;
|
return _gesturePointerType!=='mouse'&&!readOnly&&!_renamingSid&&!_sessionSelectMode;
|
||||||
};
|
};
|
||||||
|
const _isSessionActionTarget=(target)=>{
|
||||||
|
return !!(actions&&target&&actions.contains(target));
|
||||||
|
};
|
||||||
const _trackHorizontalSwipe=(dx,dy)=>{
|
const _trackHorizontalSwipe=(dx,dy)=>{
|
||||||
if(dx>16&&dx>dy*1.2) _swipeTracking=true;
|
if(dx>8&&dx>dy*1.1) _swipeTracking=true;
|
||||||
};
|
};
|
||||||
const _promoteSessionDrag=(dx,dy)=>{
|
const _promoteSessionDrag=(dx,dy)=>{
|
||||||
if(_gestureState!=='pressing'||(dx<=5&&dy<=5)) return;
|
if(_gestureState!=='pressing'||(dx<=5&&dy<=5)) return;
|
||||||
|
|
@ -3717,6 +3727,19 @@ function renderSessionListFromCache(){
|
||||||
el.classList.add('dragging');
|
el.classList.add('dragging');
|
||||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||||
};
|
};
|
||||||
|
const _updateSessionGesture=(clientX,clientY)=>{
|
||||||
|
if(_gestureState==='idle') return false;
|
||||||
|
_pointerX=clientX;
|
||||||
|
_pointerY=clientY;
|
||||||
|
const signedDx=clientX-_pointerDownX;
|
||||||
|
const signedDy=clientY-_pointerDownY;
|
||||||
|
const dx=Math.abs(signedDx);
|
||||||
|
const dy=Math.abs(signedDy);
|
||||||
|
_promoteSessionDrag(dx,dy);
|
||||||
|
_trackHorizontalSwipe(dx,dy);
|
||||||
|
if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx);
|
||||||
|
return _swipeTracking;
|
||||||
|
};
|
||||||
const _canSwipeDeleteSession=()=>{
|
const _canSwipeDeleteSession=()=>{
|
||||||
return _isSessionSwipeTarget()&&!_isMessagingSession(s)&&!_isCliSession(s);
|
return _isSessionSwipeTarget()&&!_isMessagingSession(s)&&!_isCliSession(s);
|
||||||
};
|
};
|
||||||
|
|
@ -3726,15 +3749,23 @@ function renderSessionListFromCache(){
|
||||||
const overshoot=Math.max(0,Math.abs(rawOffset)-72);
|
const overshoot=Math.max(0,Math.abs(rawOffset)-72);
|
||||||
const offset=Math.sign(rawOffset)*(Math.abs(revealedOffset)+Math.sqrt(overshoot)*5);
|
const offset=Math.sign(rawOffset)*(Math.abs(revealedOffset)+Math.sqrt(overshoot)*5);
|
||||||
const progress=Math.min(1,Math.abs(revealedOffset)/72);
|
const progress=Math.min(1,Math.abs(revealedOffset)/72);
|
||||||
|
const reveal=Math.min(132,Math.max(36,Math.abs(rawOffset)+24));
|
||||||
|
const iconScale=1+Math.min(.45,Math.max(0,Math.abs(rawOffset)-52)/130);
|
||||||
el.style.setProperty('--session-swipe-offset',offset+'px');
|
el.style.setProperty('--session-swipe-offset',offset+'px');
|
||||||
|
el.style.setProperty('--session-swipe-reveal',reveal+'px');
|
||||||
|
el.style.setProperty('--session-swipe-icon-scale',iconScale);
|
||||||
el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5));
|
el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5));
|
||||||
el.classList.toggle('swiping-right',offset>0);
|
el.classList.toggle('swiping-right',offset>0);
|
||||||
el.classList.toggle('swiping-left',offset<0);
|
el.classList.toggle('swiping-left',offset<0);
|
||||||
};
|
};
|
||||||
const _clearSessionSwipePaint=()=>{
|
const _clearSessionSwipePaint=()=>{
|
||||||
el.style.removeProperty('--session-swipe-offset');
|
el.style.removeProperty('--session-swipe-offset');
|
||||||
|
el.style.removeProperty('--session-swipe-reveal');
|
||||||
|
el.style.removeProperty('--session-swipe-icon-scale');
|
||||||
el.style.removeProperty('--session-swipe-progress');
|
el.style.removeProperty('--session-swipe-progress');
|
||||||
el.classList.remove('swiping-right','swiping-left','swipe-committed');
|
el.style.removeProperty('height');
|
||||||
|
el.style.removeProperty('min-height');
|
||||||
|
el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing');
|
||||||
};
|
};
|
||||||
const _settleSessionSwipePaint=()=>{
|
const _settleSessionSwipePaint=()=>{
|
||||||
el.classList.remove('dragging');
|
el.classList.remove('dragging');
|
||||||
|
|
@ -3752,7 +3783,8 @@ function renderSessionListFromCache(){
|
||||||
};
|
};
|
||||||
const _handleSessionSwipe=(signedDx,signedDy)=>{
|
const _handleSessionSwipe=(signedDx,signedDy)=>{
|
||||||
if(_gestureState==='committed'||!_isSessionSwipeTarget()) return false;
|
if(_gestureState==='committed'||!_isSessionSwipeTarget()) return false;
|
||||||
if(Math.abs(signedDx)<_swipeActionThreshold) return false;
|
const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;
|
||||||
|
if(Math.abs(signedDx)<actionThreshold) return false;
|
||||||
if(Math.abs(signedDy)>Math.abs(signedDx)*_swipeCancelRatio) return false;
|
if(Math.abs(signedDy)>Math.abs(signedDx)*_swipeCancelRatio) return false;
|
||||||
_gestureState='committed';
|
_gestureState='committed';
|
||||||
_clearLongPressTimer();
|
_clearLongPressTimer();
|
||||||
|
|
@ -3766,10 +3798,11 @@ function renderSessionListFromCache(){
|
||||||
if(!archived) _settleSessionSwipePaint();
|
if(!archived) _settleSessionSwipePaint();
|
||||||
},_committedSwipeDuration);
|
},_committedSwipeDuration);
|
||||||
}else if(_canSwipeDeleteSession()){
|
}else if(_canSwipeDeleteSession()){
|
||||||
el.classList.remove('dragging');
|
_completeSessionSwipePaint(signedDx);
|
||||||
|
const completedAt=Date.now();
|
||||||
deleteSession(s.session_id,async()=>{
|
deleteSession(s.session_id,async()=>{
|
||||||
_completeSessionSwipePaint(signedDx);
|
const remaining=_committedSwipeDuration-(Date.now()-completedAt);
|
||||||
await new Promise(resolve=>setTimeout(resolve,_committedSwipeDuration));
|
if(remaining>0) await new Promise(resolve=>setTimeout(resolve,remaining));
|
||||||
}).then((deleted)=>{
|
}).then((deleted)=>{
|
||||||
if(!deleted) _settleSessionSwipePaint();
|
if(!deleted) _settleSessionSwipePaint();
|
||||||
});
|
});
|
||||||
|
|
@ -3784,7 +3817,11 @@ function renderSessionListFromCache(){
|
||||||
return _handleSessionSwipe(_pointerX-_pointerDownX,_pointerY-_pointerDownY);
|
return _handleSessionSwipe(_pointerX-_pointerDownX,_pointerY-_pointerDownY);
|
||||||
};
|
};
|
||||||
const _clearPointerDragState=()=>{
|
const _clearPointerDragState=()=>{
|
||||||
const wasDragging=_gestureState==='dragging';
|
if(_gestureState==='committed'){
|
||||||
|
_clearLongPressTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wasDragging=_gestureState==='dragging'||_swipeTracking;
|
||||||
_gestureState='idle';
|
_gestureState='idle';
|
||||||
_clearLongPressTimer();
|
_clearLongPressTimer();
|
||||||
if(wasDragging){
|
if(wasDragging){
|
||||||
|
|
@ -3792,75 +3829,42 @@ function renderSessionListFromCache(){
|
||||||
_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
el.onpointerdown=(e)=>{
|
const _finishSessionGesture=(clientX,clientY,target,pointerType)=>{
|
||||||
if(e.pointerType==='mouse' && e.button!==0) return;
|
const wasDragging=_gestureState==='dragging'||_swipeTracking;
|
||||||
_beginSessionGesture(e.clientX,e.clientY);
|
|
||||||
_gesturePointerType=e.pointerType||'';
|
|
||||||
if(e.pointerType==='touch'||e.pointerType==='pen'){
|
|
||||||
_scheduleSessionLongPressMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
el.onpointermove=(e)=>{
|
|
||||||
// Plain hover also dispatches pointermove. Only mark a row as dragging
|
|
||||||
// after an actual press starts on this row; otherwise hovered rows stay
|
|
||||||
// faded until the next sidebar rerender clears their DOM nodes.
|
|
||||||
if(_gestureState==='idle') return;
|
|
||||||
_pointerX=e.clientX;
|
|
||||||
_pointerY=e.clientY;
|
|
||||||
const dx=Math.abs(e.clientX-_pointerDownX);
|
|
||||||
const dy=Math.abs(e.clientY-_pointerDownY);
|
|
||||||
_promoteSessionDrag(dx,dy);
|
|
||||||
const signedDx=e.clientX-_pointerDownX;
|
|
||||||
const signedDy=e.clientY-_pointerDownY;
|
|
||||||
_trackHorizontalSwipe(Math.abs(signedDx),Math.abs(signedDy));
|
|
||||||
if(_isSessionSwipeTarget()&&(_swipeTracking||Math.abs(signedDx)>Math.abs(signedDy))) _paintSessionSwipe(signedDx);
|
|
||||||
};
|
|
||||||
el.onpointercancel=_clearPointerDragState;
|
|
||||||
el.onpointerleave=()=>{
|
|
||||||
if(_gesturePointerType==='mouse'&&_gestureState!=='idle') _clearPointerDragState();
|
|
||||||
};
|
|
||||||
el.onpointerup=(e)=>{
|
|
||||||
if(e.pointerType==='mouse' && e.button!==0) return; // ignore right/middle click
|
|
||||||
const wasDragging=_gestureState==='dragging';
|
|
||||||
_clearLongPressTimer();
|
_clearLongPressTimer();
|
||||||
if(_renamingSid) return;
|
if(_renamingSid){_gestureState='idle';return false;}
|
||||||
if(actions&&actions.contains(e.target)) return;
|
if(_isSessionActionTarget(target)){_gestureState='idle';return false;}
|
||||||
_pointerX=e.clientX;
|
_pointerX=clientX;
|
||||||
_pointerY=e.clientY;
|
_pointerY=clientY;
|
||||||
_commitSessionSwipe();
|
_commitSessionSwipe();
|
||||||
if(_longPressMenuOpened||_gestureState==='committed'){e.stopPropagation();return;}
|
if(_longPressMenuOpened){_gestureState='idle';return true;}
|
||||||
if(_sessionActionMenu&&!_sessionActionMenu.contains(e.target)){
|
if(_gestureState==='committed') return true;
|
||||||
|
if(_sessionActionMenu&&!_sessionActionMenu.contains(target)){
|
||||||
closeSessionActionMenu();
|
closeSessionActionMenu();
|
||||||
e.stopPropagation();
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session,.session-lineage-count,.session-lineage-segments,.session-lineage-segment')) return;
|
if(target&&target.closest&&target.closest('.session-child-count,.session-child-sessions,.session-child-session,.session-lineage-count,.session-lineage-segments,.session-lineage-segment')) return false;
|
||||||
if(_sessionSelectMode){e.stopPropagation();if(!readOnly)toggleSessionSelect(s.session_id);return;}
|
if(_sessionSelectMode){if(!readOnly)toggleSessionSelect(s.session_id);return true;}
|
||||||
// If the pointer moved enough to be a drag, cancel any pending tap
|
|
||||||
if(wasDragging){
|
if(wasDragging){
|
||||||
clearTimeout(_tapTimer);_tapTimer=null;_lastTapTime=0;
|
clearTimeout(_tapTimer);_tapTimer=null;_lastTapTime=0;
|
||||||
_gestureState='idle';
|
_gestureState='idle';
|
||||||
_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
_gestureState='idle';
|
_gestureState='idle';
|
||||||
const now=Date.now();
|
const now=Date.now();
|
||||||
if(now-_lastTapTime<350){
|
if(now-_lastTapTime<350){
|
||||||
// Double-tap: rename
|
|
||||||
clearTimeout(_tapTimer);
|
clearTimeout(_tapTimer);
|
||||||
_tapTimer=null;
|
_tapTimer=null;
|
||||||
_lastTapTime=0;
|
_lastTapTime=0;
|
||||||
el.classList.remove('loading');
|
el.classList.remove('loading');
|
||||||
startRename();
|
startRename();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
_lastTapTime=now;
|
_lastTapTime=now;
|
||||||
// Single tap: wait to ensure it's not the first of a double-tap,
|
|
||||||
// then navigate. Mouse is instant; touch needs delay to suppress
|
|
||||||
// accidental navigation during scroll-drag lifts.
|
|
||||||
clearTimeout(_tapTimer);
|
clearTimeout(_tapTimer);
|
||||||
const delay=e.pointerType==='mouse'?0:300;
|
const delay=pointerType==='mouse'?0:300;
|
||||||
if(e.pointerType!=='mouse') el.classList.add('loading');
|
if(pointerType!=='mouse') el.classList.add('loading');
|
||||||
_tapTimer=setTimeout(async()=>{
|
_tapTimer=setTimeout(async()=>{
|
||||||
_tapTimer=null;
|
_tapTimer=null;
|
||||||
_lastTapTime=0;
|
_lastTapTime=0;
|
||||||
|
|
@ -3878,6 +3882,32 @@ function renderSessionListFromCache(){
|
||||||
el.classList.remove('loading');
|
el.classList.remove('loading');
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
el.onpointerdown=(e)=>{
|
||||||
|
if(e.pointerType==='touch') return;
|
||||||
|
if(e.pointerType==='mouse' && e.button!==0) return;
|
||||||
|
if(_isSessionActionTarget(e.target)) return;
|
||||||
|
_beginSessionGesture(e.clientX,e.clientY,e.pointerType||'');
|
||||||
|
if(e.pointerType==='pen'){
|
||||||
|
_scheduleSessionLongPressMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.onpointermove=(e)=>{
|
||||||
|
if(e.pointerType==='touch') return;
|
||||||
|
// Plain hover also dispatches pointermove. Only mark a row as dragging
|
||||||
|
// after an actual press starts on this row; otherwise hovered rows stay
|
||||||
|
// faded until the next sidebar rerender clears their DOM nodes.
|
||||||
|
_updateSessionGesture(e.clientX,e.clientY);
|
||||||
|
};
|
||||||
|
el.onpointercancel=_clearPointerDragState;
|
||||||
|
el.onpointerleave=()=>{
|
||||||
|
if(_gestureState!=='idle') _clearPointerDragState();
|
||||||
|
};
|
||||||
|
el.onpointerup=(e)=>{
|
||||||
|
if(e.pointerType==='touch') return;
|
||||||
|
if(e.pointerType==='mouse' && e.button!==0) return; // ignore right/middle click
|
||||||
|
if(_finishSessionGesture(e.clientX,e.clientY,e.target,e.pointerType)) e.stopPropagation();
|
||||||
};
|
};
|
||||||
// Add ondblclick for more reliable double-click detection
|
// Add ondblclick for more reliable double-click detection
|
||||||
el.ondblclick=(e)=>{
|
el.ondblclick=(e)=>{
|
||||||
|
|
@ -3895,40 +3925,22 @@ function renderSessionListFromCache(){
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
el.addEventListener('touchstart',(e)=>{
|
el.addEventListener('touchstart',(e)=>{
|
||||||
|
if(_isSessionActionTarget(e.target)) return;
|
||||||
const touch=e.changedTouches&&e.changedTouches[0];
|
const touch=e.changedTouches&&e.changedTouches[0];
|
||||||
if(!touch) return;
|
if(!touch) return;
|
||||||
_beginSessionGesture(touch.clientX,touch.clientY);
|
_beginSessionGesture(touch.clientX,touch.clientY,'touch');
|
||||||
_gesturePointerType='touch';
|
|
||||||
_scheduleSessionLongPressMenu();
|
_scheduleSessionLongPressMenu();
|
||||||
},{passive:true});
|
},{passive:true});
|
||||||
el.addEventListener('touchmove',(e)=>{
|
el.addEventListener('touchmove',(e)=>{
|
||||||
if(_gestureState==='idle') return;
|
|
||||||
const touch=e.changedTouches&&e.changedTouches[0];
|
const touch=e.changedTouches&&e.changedTouches[0];
|
||||||
if(!touch) return;
|
if(!touch) return;
|
||||||
_pointerX=touch.clientX;
|
if(_updateSessionGesture(touch.clientX,touch.clientY)) e.preventDefault();
|
||||||
_pointerY=touch.clientY;
|
|
||||||
const signedDx=touch.clientX-_pointerDownX;
|
|
||||||
const signedDy=touch.clientY-_pointerDownY;
|
|
||||||
const dx=Math.abs(signedDx);
|
|
||||||
const dy=Math.abs(signedDy);
|
|
||||||
_promoteSessionDrag(dx,dy);
|
|
||||||
_trackHorizontalSwipe(dx,dy);
|
|
||||||
if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx);
|
|
||||||
if(_swipeTracking) e.preventDefault();
|
|
||||||
},{passive:false});
|
},{passive:false});
|
||||||
|
el.addEventListener('touchcancel',_clearPointerDragState,{passive:true});
|
||||||
el.addEventListener('touchend',(e)=>{
|
el.addEventListener('touchend',(e)=>{
|
||||||
const wasDragging=_gestureState==='dragging';
|
|
||||||
_clearLongPressTimer();
|
|
||||||
const touch=e.changedTouches&&e.changedTouches[0];
|
const touch=e.changedTouches&&e.changedTouches[0];
|
||||||
if(touch){
|
if(!touch) return;
|
||||||
_pointerX=touch.clientX;
|
if(_finishSessionGesture(touch.clientX,touch.clientY,e.target,'touch')) e.stopPropagation();
|
||||||
_pointerY=touch.clientY;
|
|
||||||
}
|
|
||||||
_commitSessionSwipe();
|
|
||||||
if(wasDragging){
|
|
||||||
if(_gestureState!=='committed') _clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
|
||||||
}
|
|
||||||
if(_gestureState!=='committed') _gestureState='idle';
|
|
||||||
},{passive:true});
|
},{passive:true});
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -664,8 +664,8 @@
|
||||||
box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 38%,transparent);
|
box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 38%,transparent);
|
||||||
animation:session-long-press .4s cubic-bezier(.2,.8,.2,1) both;
|
animation:session-long-press .4s cubic-bezier(.2,.8,.2,1) both;
|
||||||
}
|
}
|
||||||
.session-item.swiping-right{background:color-mix(in srgb,var(--warning) 18%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--warning) 60%,transparent);}
|
.session-item.swiping-right{background:color-mix(in srgb,var(--warning) 16%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--warning) 48%,transparent);}
|
||||||
.session-item.swiping-left{background:color-mix(in srgb,var(--error) 16%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--error) 60%,transparent);}
|
.session-item.swiping-left{background:color-mix(in srgb,var(--error) 14%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--error) 48%,transparent);}
|
||||||
.session-swipe-affordance{
|
.session-swipe-affordance{
|
||||||
position:absolute;
|
position:absolute;
|
||||||
top:0;
|
top:0;
|
||||||
|
|
@ -675,37 +675,47 @@
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:center;
|
justify-content:center;
|
||||||
gap:3px;
|
gap:3px;
|
||||||
width:72px;
|
width:var(--session-swipe-reveal,0px);
|
||||||
padding:0 10px;
|
min-width:36px;
|
||||||
|
padding:0 8px;
|
||||||
box-sizing:border-box;
|
box-sizing:border-box;
|
||||||
opacity:0;
|
opacity:0;
|
||||||
|
overflow:hidden;
|
||||||
transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18));
|
transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18));
|
||||||
transform-origin:center;
|
|
||||||
pointer-events:none;
|
pointer-events:none;
|
||||||
|
z-index:0;
|
||||||
}
|
}
|
||||||
.session-swipe-affordance-right{
|
.session-swipe-affordance-right{
|
||||||
left:0;
|
left:0;
|
||||||
color:var(--warning);
|
color:#fff;
|
||||||
|
background:var(--warning);
|
||||||
|
border-radius:8px 6px 6px 8px;
|
||||||
|
transform-origin:left center;
|
||||||
}
|
}
|
||||||
.session-swipe-affordance-left{
|
.session-swipe-affordance-left{
|
||||||
right:0;
|
right:0;
|
||||||
color:var(--error);
|
color:#fff;
|
||||||
|
background:var(--error);
|
||||||
|
border-radius:6px 8px 8px 6px;
|
||||||
|
transform-origin:right center;
|
||||||
}
|
}
|
||||||
.session-item.swiping-right .session-swipe-affordance-right,
|
.session-item.swiping-right .session-swipe-affordance-right,
|
||||||
.session-item.swiping-left .session-swipe-affordance-left{
|
.session-item.swiping-left .session-swipe-affordance-left{
|
||||||
opacity:var(--session-swipe-progress,0);
|
opacity:var(--session-swipe-progress,0);
|
||||||
}
|
}
|
||||||
|
.session-text,.session-state-indicator,.session-actions{z-index:1;}
|
||||||
.session-swipe-badge{
|
.session-swipe-badge{
|
||||||
width:30px;
|
width:28px;
|
||||||
height:30px;
|
height:28px;
|
||||||
border-radius:999px;
|
border-radius:0;
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:center;
|
justify-content:center;
|
||||||
color:#fff;
|
color:#fff;
|
||||||
|
transform:scaleX(var(--session-swipe-icon-scale,1));
|
||||||
|
transform-origin:center;
|
||||||
}
|
}
|
||||||
.session-swipe-affordance-right .session-swipe-badge{background:var(--warning);}
|
.session-swipe-badge svg{display:block;stroke-width:2.2;}
|
||||||
.session-swipe-affordance-left .session-swipe-badge{background:var(--error);}
|
|
||||||
.session-swipe-label{
|
.session-swipe-label{
|
||||||
max-width:58px;
|
max-width:58px;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
|
|
@ -730,16 +740,16 @@
|
||||||
.session-item.swipe-committed .session-swipe-affordance{transition:opacity .18s ease,transform .18s ease;}
|
.session-item.swipe-committed .session-swipe-affordance{transition:opacity .18s ease,transform .18s ease;}
|
||||||
/* Suppress hover highlight during drag to avoid visual noise mid-scroll */
|
/* Suppress hover highlight during drag to avoid visual noise mid-scroll */
|
||||||
.session-item.dragging:hover{background:transparent;color:var(--muted);}
|
.session-item.dragging:hover{background:transparent;color:var(--muted);}
|
||||||
.session-item.dragging.swiping-right{background:color-mix(in srgb,var(--warning) 18%,var(--surface));}
|
.session-item.dragging.swiping-right{background:color-mix(in srgb,var(--warning) 16%,var(--surface));}
|
||||||
.session-item.dragging.swiping-left{background:color-mix(in srgb,var(--error) 16%,var(--surface));}
|
.session-item.dragging.swiping-left{background:color-mix(in srgb,var(--error) 14%,var(--surface));}
|
||||||
/* Restore hover padding-right only for mouse (hover:hover) devices.
|
/* Restore hover padding-right only for mouse (hover:hover) devices.
|
||||||
Touch/iPad (hover:none) must NOT expand padding-right on :hover —
|
Touch/iPad (hover:none) must NOT expand padding-right on :hover —
|
||||||
the expansion causes a layout-reflow mid-tap that moves session-actions
|
the expansion causes a layout-reflow mid-tap that moves session-actions
|
||||||
under the finger, triggering stopPropagation and swallowing navigation. */
|
under the finger, triggering stopPropagation and swallowing navigation. */
|
||||||
@media (hover:hover){:root:not(.dark) .session-item:hover{background:var(--hover-bg);color:var(--text);}.session-item:hover{padding-right:40px;}}
|
@media (hover:hover){:root:not(.dark) .session-item:hover{background:var(--hover-bg);color:var(--text);}.session-item:hover{padding-right:40px;}}
|
||||||
.session-item.active{background:var(--accent-bg);color:var(--accent);}
|
.session-item.active{background:var(--accent-bg);color:var(--accent);}
|
||||||
.session-item.active.swiping-right{background:color-mix(in srgb,var(--warning) 22%,var(--accent-bg));}
|
.session-item.active.swiping-right{background:color-mix(in srgb,var(--warning) 20%,var(--accent-bg));}
|
||||||
.session-item.active.swiping-left{background:color-mix(in srgb,var(--error) 20%,var(--accent-bg));}
|
.session-item.active.swiping-left{background:color-mix(in srgb,var(--error) 18%,var(--accent-bg));}
|
||||||
.session-item.streaming .session-title{color:var(--accent);}
|
.session-item.streaming .session-title{color:var(--accent);}
|
||||||
.session-item.streaming .session-title-row{color:var(--text);}
|
.session-item.streaming .session-title-row{color:var(--text);}
|
||||||
.session-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden;}
|
.session-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden;}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||||
assert "_openSessionActionMenu(s, el)" in SESSIONS_JS
|
assert "_openSessionActionMenu(s, el)" in SESSIONS_JS
|
||||||
assert "@media (hover:none) and (pointer:coarse)" in STYLE_CSS
|
assert "@media (hover:none) and (pointer:coarse)" in STYLE_CSS
|
||||||
assert ".session-actions{display:none;}" in STYLE_CSS
|
assert ".session-actions{display:none;}" in STYLE_CSS
|
||||||
assert "const _beginSessionGesture=(clientX,clientY)=>{" in SESSIONS_JS
|
assert "const _beginSessionGesture=(clientX,clientY,pointerType='')=>{" in SESSIONS_JS
|
||||||
assert "const _scheduleSessionLongPressMenu=()=>{" in SESSIONS_JS
|
assert "const _scheduleSessionLongPressMenu=()=>{" in SESSIONS_JS
|
||||||
mobile_touch = STYLE_CSS[STYLE_CSS.find("@media (hover:none) and (pointer:coarse)"):STYLE_CSS.find("@media (max-width: 340px)")]
|
mobile_touch = STYLE_CSS[STYLE_CSS.find("@media (hover:none) and (pointer:coarse)"):STYLE_CSS.find("@media (max-width: 340px)")]
|
||||||
assert ".session-item{padding-right:6px;}" in mobile_touch
|
assert ".session-item{padding-right:6px;}" in mobile_touch
|
||||||
|
|
@ -46,35 +46,59 @@ def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||||
|
|
||||||
|
|
||||||
def test_open_session_menu_consumes_next_row_activation():
|
def test_open_session_menu_consumes_next_row_activation():
|
||||||
assert "if(_sessionActionMenu&&!_sessionActionMenu.contains(e.target)){" in SESSIONS_JS
|
assert "if(_sessionActionMenu&&!_sessionActionMenu.contains(target)){" in SESSIONS_JS
|
||||||
assert "closeSessionActionMenu();" in SESSIONS_JS
|
assert "closeSessionActionMenu();" in SESSIONS_JS
|
||||||
assert "e.stopPropagation();" in SESSIONS_JS
|
assert "e.stopPropagation();" in SESSIONS_JS
|
||||||
|
assert "const stopMenuPointer=(e)=>e.stopPropagation();" in SESSIONS_JS
|
||||||
|
assert "menuBtn.onpointerdown=stopMenuPointer;" in SESSIONS_JS
|
||||||
|
assert "menuBtn.onpointerup=stopMenuPointer;" in SESSIONS_JS
|
||||||
|
menu_btn_idx = SESSIONS_JS.find("menuBtn.onpointerdown=stopMenuPointer;")
|
||||||
|
menu_click_idx = SESSIONS_JS.find("menuBtn.onclick=(e)=>{", menu_btn_idx)
|
||||||
|
assert menu_btn_idx > 0 and menu_click_idx > menu_btn_idx
|
||||||
|
assert "const _isSessionActionTarget=(target)=>{" in SESSIONS_JS
|
||||||
|
assert "return !!(actions&&target&&actions.contains(target));" in SESSIONS_JS
|
||||||
|
assert "if(_isSessionActionTarget(e.target)) return;" in SESSIONS_JS
|
||||||
|
assert "if(_isSessionActionTarget(target)){_gestureState='idle';return false;}" in SESSIONS_JS
|
||||||
|
assert "if(_longPressMenuOpened){_gestureState='idle';return true;}" in SESSIONS_JS
|
||||||
|
finish_idx = SESSIONS_JS.find("const _finishSessionGesture=(clientX,clientY,target,pointerType)=>{")
|
||||||
|
dismiss_idx = SESSIONS_JS.find("if(_sessionActionMenu&&!_sessionActionMenu.contains(target)){", finish_idx)
|
||||||
|
load_idx = SESSIONS_JS.find("await loadSession(s.session_id)", finish_idx)
|
||||||
pointerup_idx = SESSIONS_JS.find("el.onpointerup=(e)=>{")
|
pointerup_idx = SESSIONS_JS.find("el.onpointerup=(e)=>{")
|
||||||
dismiss_idx = SESSIONS_JS.find("if(_sessionActionMenu&&!_sessionActionMenu.contains(e.target)){", pointerup_idx)
|
assert finish_idx > 0 and load_idx > finish_idx
|
||||||
load_idx = SESSIONS_JS.find("await loadSession(s.session_id)", pointerup_idx)
|
assert dismiss_idx > finish_idx and dismiss_idx < load_idx
|
||||||
assert pointerup_idx > 0 and load_idx > pointerup_idx
|
assert "if(_finishSessionGesture(e.clientX,e.clientY,e.target,e.pointerType)) e.stopPropagation();" in SESSIONS_JS[pointerup_idx:]
|
||||||
assert dismiss_idx > pointerup_idx and dismiss_idx < load_idx
|
|
||||||
|
|
||||||
|
|
||||||
def test_session_swipes_archive_right_and_delete_left():
|
def test_session_swipes_archive_right_and_delete_left():
|
||||||
assert "_gesturePointerType!=='mouse'" in SESSIONS_JS
|
assert "_gesturePointerType!=='mouse'" in SESSIONS_JS
|
||||||
assert "_swipeTracking=true" in SESSIONS_JS
|
assert "_swipeTracking=true" in SESSIONS_JS
|
||||||
assert "const _trackHorizontalSwipe=(dx,dy)=>{" in SESSIONS_JS
|
assert "const _trackHorizontalSwipe=(dx,dy)=>{" in SESSIONS_JS
|
||||||
assert "_swipeActionThreshold=144" in SESSIONS_JS
|
assert "_archiveSwipeActionThreshold=128" in SESSIONS_JS
|
||||||
|
assert "_deleteSwipeActionThreshold=128" in SESSIONS_JS
|
||||||
assert "_committedSwipeDuration=_sessionPrefersReducedMotion()?0:420" in SESSIONS_JS
|
assert "_committedSwipeDuration=_sessionPrefersReducedMotion()?0:420" in SESSIONS_JS
|
||||||
assert "const _handleSessionSwipe=(signedDx,signedDy)=>{" in SESSIONS_JS
|
assert "const _handleSessionSwipe=(signedDx,signedDy)=>{" in SESSIONS_JS
|
||||||
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||Math.abs(signedDx)>Math.abs(signedDy))) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
assert "const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;" in SESSIONS_JS
|
||||||
|
assert "if(Math.abs(signedDx)<actionThreshold) return false;" in SESSIONS_JS
|
||||||
|
assert "const _updateSessionGesture=(clientX,clientY)=>{" in SESSIONS_JS
|
||||||
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
||||||
|
assert "_updateSessionGesture(e.clientX,e.clientY);" in SESSIONS_JS
|
||||||
|
assert "if(_updateSessionGesture(touch.clientX,touch.clientY)) e.preventDefault();" in SESSIONS_JS
|
||||||
|
assert "_beginSessionGesture(touch.clientX,touch.clientY,'touch');" in SESSIONS_JS
|
||||||
assert "if(signedDx>0){" in SESSIONS_JS
|
assert "if(signedDx>0){" in SESSIONS_JS
|
||||||
assert "_archiveSession(s,!s.archived)" in SESSIONS_JS
|
assert "_archiveSession(s,!s.archived)" in SESSIONS_JS
|
||||||
|
assert "const completedAt=Date.now();" in SESSIONS_JS
|
||||||
|
assert "const remaining=_committedSwipeDuration-(Date.now()-completedAt);" in SESSIONS_JS
|
||||||
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
||||||
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
|
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
|
||||||
assert "let _gestureState='idle';" in SESSIONS_JS
|
assert "let _gestureState='idle';" in SESSIONS_JS
|
||||||
assert "_gestureState='dragging';" in SESSIONS_JS
|
assert "_gestureState='dragging';" in SESSIONS_JS
|
||||||
assert "const _promoteSessionDrag=(dx,dy)=>{" in SESSIONS_JS
|
assert "const _promoteSessionDrag=(dx,dy)=>{" in SESSIONS_JS
|
||||||
assert "if(_gesturePointerType==='mouse'&&_gestureState!=='idle') _clearPointerDragState();" in SESSIONS_JS
|
|
||||||
assert "const _commitSessionSwipe=()=>{" in SESSIONS_JS
|
assert "const _commitSessionSwipe=()=>{" in SESSIONS_JS
|
||||||
assert "_commitSessionSwipe();" in SESSIONS_JS
|
assert "_commitSessionSwipe();" in SESSIONS_JS
|
||||||
|
assert "const wasDragging=_gestureState==='dragging'||_swipeTracking;" in SESSIONS_JS
|
||||||
|
assert "if(_gestureState==='committed'){" in SESSIONS_JS
|
||||||
|
assert SESSIONS_JS.count("if(e.pointerType==='touch') return;") >= 3
|
||||||
|
assert "el.onpointercancel=_clearPointerDragState;" in SESSIONS_JS
|
||||||
|
|
||||||
|
|
||||||
def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
||||||
|
|
@ -84,11 +108,21 @@ def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
||||||
assert "const overshoot=Math.max(0,Math.abs(rawOffset)-72)" in SESSIONS_JS
|
assert "const overshoot=Math.max(0,Math.abs(rawOffset)-72)" in SESSIONS_JS
|
||||||
assert "Math.sqrt(overshoot)*5" in SESSIONS_JS
|
assert "Math.sqrt(overshoot)*5" in SESSIONS_JS
|
||||||
assert "el.style.setProperty('--session-swipe-offset',offset+'px')" in SESSIONS_JS
|
assert "el.style.setProperty('--session-swipe-offset',offset+'px')" in SESSIONS_JS
|
||||||
|
assert "const reveal=Math.min(132,Math.max(36,Math.abs(rawOffset)+24));" in SESSIONS_JS
|
||||||
|
assert "const iconScale=1+Math.min(.45,Math.max(0,Math.abs(rawOffset)-52)/130);" in SESSIONS_JS
|
||||||
|
assert "el.style.setProperty('--session-swipe-reveal',reveal+'px')" in SESSIONS_JS
|
||||||
|
assert "el.style.setProperty('--session-swipe-icon-scale',iconScale)" in SESSIONS_JS
|
||||||
assert "const progress=Math.min(1,Math.abs(revealedOffset)/72)" in SESSIONS_JS
|
assert "const progress=Math.min(1,Math.abs(revealedOffset)/72)" in SESSIONS_JS
|
||||||
assert "el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5))" in SESSIONS_JS
|
assert "el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5))" in SESSIONS_JS
|
||||||
assert "const _clearSessionSwipePaint=()=>{" in SESSIONS_JS
|
assert "const _clearSessionSwipePaint=()=>{" in SESSIONS_JS
|
||||||
|
assert "el.style.removeProperty('--session-swipe-reveal');" in SESSIONS_JS
|
||||||
|
assert "el.style.removeProperty('--session-swipe-icon-scale');" in SESSIONS_JS
|
||||||
|
assert "el.style.removeProperty('height');" in SESSIONS_JS
|
||||||
|
assert "el.style.removeProperty('min-height');" in SESSIONS_JS
|
||||||
|
assert "el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing')" in SESSIONS_JS
|
||||||
assert "const _settleSessionSwipePaint=()=>{" in SESSIONS_JS
|
assert "const _settleSessionSwipePaint=()=>{" in SESSIONS_JS
|
||||||
assert "const _completeSessionSwipePaint=(signedDx)=>{" in SESSIONS_JS
|
assert "const _completeSessionSwipePaint=(signedDx)=>{" in SESSIONS_JS
|
||||||
|
assert "el.classList.remove('dragging');" in SESSIONS_JS
|
||||||
assert "el.classList.add('swipe-committed')" in SESSIONS_JS
|
assert "el.classList.add('swipe-committed')" in SESSIONS_JS
|
||||||
assert "el.style.height=rect.height+'px'" in SESSIONS_JS
|
assert "el.style.height=rect.height+'px'" in SESSIONS_JS
|
||||||
assert "requestAnimationFrame(()=>el.classList.add('swipe-removing'))" in SESSIONS_JS
|
assert "requestAnimationFrame(()=>el.classList.add('swipe-removing'))" in SESSIONS_JS
|
||||||
|
|
@ -96,26 +130,32 @@ def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
||||||
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
||||||
assert "const archived=await _archiveSession(s,!s.archived);" in SESSIONS_JS
|
assert "const archived=await _archiveSession(s,!s.archived);" in SESSIONS_JS
|
||||||
assert "if(!archived) _settleSessionSwipePaint();" in SESSIONS_JS
|
assert "if(!archived) _settleSessionSwipePaint();" in SESSIONS_JS
|
||||||
assert "await new Promise(resolve=>setTimeout(resolve,_committedSwipeDuration));" in SESSIONS_JS
|
assert "if(remaining>0) await new Promise(resolve=>setTimeout(resolve,remaining));" in SESSIONS_JS
|
||||||
assert "async function deleteSession(sid, beforeDelete=null){" in SESSIONS_JS
|
assert "async function deleteSession(sid, beforeDelete=null){" in SESSIONS_JS
|
||||||
|
assert "if(beforeDelete) await beforeDelete();" in SESSIONS_JS
|
||||||
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
|
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
|
||||||
assert ".session-item.swiping-right" in STYLE_CSS
|
assert ".session-item.swiping-right" in STYLE_CSS
|
||||||
assert ".session-item.swiping-left" in STYLE_CSS
|
assert ".session-item.swiping-left" in STYLE_CSS
|
||||||
assert "const _makeSessionSwipeAffordance=(side,icon,label)=>{" in SESSIONS_JS
|
assert "const _makeSessionSwipeAffordance=(side,icon,label)=>{" in SESSIONS_JS
|
||||||
|
assert "affordance.setAttribute('aria-hidden','true');" in SESSIONS_JS
|
||||||
assert "_makeSessionSwipeAffordance('right',s.archived?'undo':'archive'" in SESSIONS_JS
|
assert "_makeSessionSwipeAffordance('right',s.archived?'undo':'archive'" in SESSIONS_JS
|
||||||
assert "_makeSessionSwipeAffordance('left','trash-2'" in SESSIONS_JS
|
assert "_makeSessionSwipeAffordance('left','trash-2'" in SESSIONS_JS
|
||||||
assert ".session-swipe-affordance{" in STYLE_CSS
|
assert ".session-swipe-affordance{" in STYLE_CSS
|
||||||
assert "opacity:var(--session-swipe-progress,0)" in STYLE_CSS
|
assert "opacity:var(--session-swipe-progress,0)" in STYLE_CSS
|
||||||
|
assert "width:var(--session-swipe-reveal,0px)" in STYLE_CSS
|
||||||
|
assert ".session-item.swiping-right{background:color-mix(in srgb,var(--warning) 16%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--warning) 48%,transparent);}" in STYLE_CSS
|
||||||
|
assert ".session-item.swiping-left{background:color-mix(in srgb,var(--error) 14%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--error) 48%,transparent);}" in STYLE_CSS
|
||||||
|
assert "background:var(--warning)" in STYLE_CSS
|
||||||
|
assert "background:var(--error)" in STYLE_CSS
|
||||||
assert ".session-item.swiping-right .session-swipe-affordance-right" in STYLE_CSS
|
assert ".session-item.swiping-right .session-swipe-affordance-right" in STYLE_CSS
|
||||||
assert ".session-item.swiping-left .session-swipe-affordance-left" in STYLE_CSS
|
assert ".session-item.swiping-left .session-swipe-affordance-left" in STYLE_CSS
|
||||||
assert "transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18))" in STYLE_CSS
|
assert "transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18))" in STYLE_CSS
|
||||||
assert ".session-swipe-badge{" in STYLE_CSS
|
assert ".session-swipe-badge{" in STYLE_CSS
|
||||||
|
assert "transform:scaleX(var(--session-swipe-icon-scale,1))" in STYLE_CSS
|
||||||
assert ".session-swipe-label{" in STYLE_CSS
|
assert ".session-swipe-label{" in STYLE_CSS
|
||||||
assert "transform .5s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
assert "transform .5s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
||||||
assert ".session-item.dragging.swiping-right" in STYLE_CSS
|
assert ".session-item.dragging.swiping-right" in STYLE_CSS
|
||||||
assert ".session-item.dragging.swiping-left" in STYLE_CSS
|
assert ".session-item.dragging.swiping-left" in STYLE_CSS
|
||||||
assert ".session-item.active.swiping-right" in STYLE_CSS
|
|
||||||
assert ".session-item.active.swiping-left" in STYLE_CSS
|
|
||||||
assert ".session-item.dragging{transition:background .15s,color .15s,box-shadow .15s ease;}" in STYLE_CSS
|
assert ".session-item.dragging{transition:background .15s,color .15s,box-shadow .15s ease;}" in STYLE_CSS
|
||||||
assert ".session-item.swipe-committed" in STYLE_CSS
|
assert ".session-item.swipe-committed" in STYLE_CSS
|
||||||
assert ".session-item.swipe-removing{" in STYLE_CSS
|
assert ".session-item.swipe-removing{" in STYLE_CSS
|
||||||
|
|
@ -139,7 +179,7 @@ def test_session_removal_reflows_surviving_rows_smoothly():
|
||||||
assert "row.style.setProperty('--session-reflow-offset',delta+'px')" in SESSIONS_JS
|
assert "row.style.setProperty('--session-reflow-offset',delta+'px')" in SESSIONS_JS
|
||||||
assert "row.classList.add('session-reflowing')" in SESSIONS_JS
|
assert "row.classList.add('session-reflowing')" in SESSIONS_JS
|
||||||
assert "row.style.setProperty('--session-reflow-offset','0px')" in SESSIONS_JS
|
assert "row.style.setProperty('--session-reflow-offset','0px')" in SESSIONS_JS
|
||||||
assert SESSIONS_JS.count("const reflowPositions=_captureSessionReflowPositions();") >= 2
|
assert "const reflowPositions=_captureSessionReflowPositions();" in SESSIONS_JS
|
||||||
assert SESSIONS_JS.count("_pendingSessionReflowPositions=reflowPositions;") >= 2
|
assert SESSIONS_JS.count("_pendingSessionReflowPositions=reflowPositions;") >= 2
|
||||||
assert "_playQueuedSessionReflowAnimation();" in SESSIONS_JS
|
assert "_playQueuedSessionReflowAnimation();" in SESSIONS_JS
|
||||||
assert ".session-item.session-reflowing{transition:background .15s,color .15s,transform .36s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;will-change:transform;}" in STYLE_CSS
|
assert ".session-item.session-reflowing{transition:background .15s,color .15s,transform .36s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;will-change:transform;}" in STYLE_CSS
|
||||||
|
|
@ -148,9 +188,14 @@ def test_session_removal_reflows_surviving_rows_smoothly():
|
||||||
def test_ios_touch_events_drive_session_swipes():
|
def test_ios_touch_events_drive_session_swipes():
|
||||||
assert "el.addEventListener('touchstart'" in SESSIONS_JS
|
assert "el.addEventListener('touchstart'" in SESSIONS_JS
|
||||||
assert "el.addEventListener('touchmove'" in SESSIONS_JS
|
assert "el.addEventListener('touchmove'" in SESSIONS_JS
|
||||||
|
assert "el.addEventListener('touchcancel',_clearPointerDragState" in SESSIONS_JS
|
||||||
assert "el.addEventListener('touchend'" in SESSIONS_JS
|
assert "el.addEventListener('touchend'" in SESSIONS_JS
|
||||||
|
assert "const _finishSessionGesture=(clientX,clientY,target,pointerType)=>{" in SESSIONS_JS
|
||||||
assert "{passive:false}" in SESSIONS_JS
|
assert "{passive:false}" in SESSIONS_JS
|
||||||
assert "e.preventDefault()" in SESSIONS_JS
|
assert "if(_updateSessionGesture(touch.clientX,touch.clientY)) e.preventDefault();" in SESSIONS_JS
|
||||||
|
assert SESSIONS_JS.count("if(e.pointerType==='touch') return;") >= 3
|
||||||
|
assert "if(_finishSessionGesture(touch.clientX,touch.clientY,e.target,'touch')) e.stopPropagation();" in SESSIONS_JS
|
||||||
|
assert "window.PointerEvent" not in SESSIONS_JS
|
||||||
|
|
||||||
|
|
||||||
def test_touch_session_rows_preserve_vertical_scroll():
|
def test_touch_session_rows_preserve_vertical_scroll():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue