fix: prevent sticky sidebar hover drag state
This commit is contained in:
parent
d8cd5567e0
commit
ecdbc8d4df
4 changed files with 44 additions and 7 deletions
BIN
docs/pr-media/sidebar-hover-drag/after-hover-qa.png
Normal file
BIN
docs/pr-media/sidebar-hover-drag/after-hover-qa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
|
|
@ -2509,15 +2509,31 @@ function renderSessionListFromCache(){
|
||||||
let _tapTimer=null;
|
let _tapTimer=null;
|
||||||
let _pointerDownX=0;
|
let _pointerDownX=0;
|
||||||
let _pointerDownY=0;
|
let _pointerDownY=0;
|
||||||
|
let _pointerActive=false;
|
||||||
let _isDragging=false;
|
let _isDragging=false;
|
||||||
let _clearDragTimer=null;
|
let _clearDragTimer=null;
|
||||||
|
const _clearPointerDragState=()=>{
|
||||||
|
_pointerActive=false;
|
||||||
|
if(_isDragging){
|
||||||
|
_isDragging=false;
|
||||||
|
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||||
|
_clearDragTimer=setTimeout(()=>{el.classList.remove('dragging');_clearDragTimer=null;},50);
|
||||||
|
}
|
||||||
|
};
|
||||||
el.onpointerdown=(e)=>{
|
el.onpointerdown=(e)=>{
|
||||||
if(e.pointerType==='mouse' && e.button!==0) return;
|
if(e.pointerType==='mouse' && e.button!==0) return;
|
||||||
|
_pointerActive=true;
|
||||||
_pointerDownX=e.clientX;
|
_pointerDownX=e.clientX;
|
||||||
_pointerDownY=e.clientY;
|
_pointerDownY=e.clientY;
|
||||||
_isDragging=false;
|
_isDragging=false;
|
||||||
|
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||||
|
el.classList.remove('dragging');
|
||||||
};
|
};
|
||||||
el.onpointermove=(e)=>{
|
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(!_pointerActive) return;
|
||||||
if(_isDragging) return;
|
if(_isDragging) return;
|
||||||
const dx=Math.abs(e.clientX-_pointerDownX);
|
const dx=Math.abs(e.clientX-_pointerDownX);
|
||||||
const dy=Math.abs(e.clientY-_pointerDownY);
|
const dy=Math.abs(e.clientY-_pointerDownY);
|
||||||
|
|
@ -2528,8 +2544,11 @@ function renderSessionListFromCache(){
|
||||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
el.onpointercancel=_clearPointerDragState;
|
||||||
|
el.onpointerleave=()=>{ if(_pointerActive) _clearPointerDragState(); };
|
||||||
el.onpointerup=(e)=>{
|
el.onpointerup=(e)=>{
|
||||||
if(e.pointerType==='mouse' && e.button!==0) return; // ignore right/middle click
|
if(e.pointerType==='mouse' && e.button!==0) return; // ignore right/middle click
|
||||||
|
_pointerActive=false;
|
||||||
if(_renamingSid) return;
|
if(_renamingSid) return;
|
||||||
if(actions&&actions.contains(e.target)) return;
|
if(actions&&actions.contains(e.target)) return;
|
||||||
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session')) return;
|
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session')) return;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -14,13 +15,21 @@ pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||||
|
|
||||||
|
|
||||||
def _run_node(source: str) -> str:
|
def _run_node(source: str) -> str:
|
||||||
result = subprocess.run(
|
with tempfile.NamedTemporaryFile(
|
||||||
[NODE, "-e", source],
|
"w", suffix=".cjs", encoding="utf-8", dir=REPO_ROOT, delete=False
|
||||||
cwd=str(REPO_ROOT),
|
) as script:
|
||||||
capture_output=True,
|
script.write(source)
|
||||||
text=True,
|
script_path = Path(script.name)
|
||||||
timeout=10,
|
try:
|
||||||
)
|
result = subprocess.run(
|
||||||
|
[NODE, str(script_path)],
|
||||||
|
cwd=str(REPO_ROOT),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
script_path.unlink(missing_ok=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise RuntimeError(result.stderr)
|
raise RuntimeError(result.stderr)
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,15 @@ def test_timestamp_hidden_when_attention_state_is_present():
|
||||||
assert ".session-item.unread:not(:hover):not(:focus-within):not(.menu-open) .session-actions" in STYLE_CSS
|
assert ".session-item.unread:not(:hover):not(:focus-within):not(.menu-open) .session-actions" in STYLE_CSS
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_mouse_hover_does_not_mark_session_row_dragging():
|
||||||
|
"""Pointermove fires during ordinary hover; drag styling must require an active press."""
|
||||||
|
assert "let _pointerActive=false;" in SESSIONS_JS
|
||||||
|
assert "_pointerActive=true;" in SESSIONS_JS
|
||||||
|
assert "if(!_pointerActive) return;" in SESSIONS_JS
|
||||||
|
assert "_pointerActive=false;" in SESSIONS_JS
|
||||||
|
assert ".session-item.dragging:hover" in STYLE_CSS
|
||||||
|
|
||||||
|
|
||||||
def test_sidebar_uses_local_inflight_state_for_immediate_spinner():
|
def test_sidebar_uses_local_inflight_state_for_immediate_spinner():
|
||||||
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
|
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue