fix(sidebar): unstick interrupted panel drags so the bottom panel keeps tracking layout (#183)
* fix(sidebar): unstick interrupted panel drags so the bottom panel keeps tracking layout A width/height/corner drag whose pointer stream ends without pointerup (pointercancel, or pointer capture lost prematurely — e.g. a trackpad gesture interrupting the drag) left draggingWidth/draggingBottom/ draggingCorner stuck true: onPointerUp guards on hasPointerCapture and never ran, and no onPointerCancel/onLostPointerCapture handler existed. The stuck drag state pauses center-column measurement (draggingRef) permanently, so centerRect goes stale and the bottom panel keeps frozen inline left/right edges — it no longer expands when the right sidebar collapses or the app rail toggles until a full reload. Fix: mark each drag uncommitted on pointerdown and committed in pointerup before releasing capture; the new pointercancel / lostpointercapture handlers abort an uncommitted drag by reverting the DOM and layout variables to the store's committed sizes and clearing the dragging state, which resumes measurement. * fix(sidebar): clamp abort-drag rollback to collapsed/narrow geometry (P2 review) abortDrag wrote the committed width even when the right panel is collapsed or the viewport is narrow, where the layout-push effect had already written 0 to --dsh-sidebar-width — an interrupted drag then left the app shell squeezed by an invisible sidebar. Mirror the layout effect's clamping: write 0 when narrow or the panel is closed. --------- Co-authored-by: Menghuan1918 <menghuan2003@outlook.com>
This commit is contained in:
@@ -535,6 +535,26 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
pendingDrag.current = null
|
||||
}
|
||||
|
||||
/** Set once a drag's pointerup handler commits — premature capture loss
|
||||
* (pointercancel / lostpointercapture without pointerup) must then be told
|
||||
* apart from a normal release. */
|
||||
const dragCommitted = useRef(false)
|
||||
/** Abort a drag whose pointer stream was interrupted (pointercancel, or
|
||||
* capture lost before pointerup): no pointerup will arrive, so without
|
||||
* this the dragging state would stick true and center-column measurement
|
||||
* would stay paused forever — the bottom panel freezes at stale edges and
|
||||
* stops tracking sidebar/app-rail layout changes. Reverts the DOM and the
|
||||
* layout variables to the store's committed sizes. */
|
||||
const abortDrag = (reset: () => void): void => {
|
||||
if (dragCommitted.current) return
|
||||
stopDragScheduling()
|
||||
applyDrag(
|
||||
!narrow && state?.panelOpen === true ? Math.min(state?.width ?? 0, window.innerWidth) : 0,
|
||||
!narrow && state?.bottomOpen === true ? Math.min(state.bottomHeight, window.innerHeight) : 0,
|
||||
)
|
||||
reset()
|
||||
}
|
||||
|
||||
// Layout push: the app shell gives up the panel's width/height while the
|
||||
// panels are open (0 while collapsed), so the conversation and input bar
|
||||
// are squeezed instead of covered. The margins are capped at the viewport
|
||||
@@ -784,6 +804,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
dragCommitted.current = false
|
||||
widthDrag.current = { startX: event.clientX, startWidth: state.width }
|
||||
setDraggingWidth(true)
|
||||
}}
|
||||
@@ -796,12 +817,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
|
||||
dragCommitted.current = true
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
const { startX, startWidth } = widthDrag.current
|
||||
stopDragScheduling()
|
||||
store.reduce(s => setWidth(s, startWidth + (startX - event.clientX)))
|
||||
setDraggingWidth(false)
|
||||
}}
|
||||
onPointerCancel={() => { abortDrag(() => setDraggingWidth(false)) }}
|
||||
onLostPointerCapture={() => { abortDrag(() => setDraggingWidth(false)) }}
|
||||
/>
|
||||
)}
|
||||
<div className={css.panelBody}>
|
||||
@@ -833,6 +857,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
dragCommitted.current = false
|
||||
cornerDrag.current = {
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
@@ -850,12 +875,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
|
||||
dragCommitted.current = true
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
const { startX, startY, startWidth, startHeight } = cornerDrag.current
|
||||
stopDragScheduling()
|
||||
store.reduce(s => setBottomHeight(setWidth(s, startWidth + (startX - event.clientX)), startHeight + (startY - event.clientY)))
|
||||
setDraggingCorner(false)
|
||||
}}
|
||||
onPointerCancel={() => { abortDrag(() => setDraggingCorner(false)) }}
|
||||
onLostPointerCapture={() => { abortDrag(() => setDraggingCorner(false)) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -895,6 +923,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
dragCommitted.current = false
|
||||
bottomDrag.current = { startY: event.clientY, startHeight: state.bottomHeight }
|
||||
setDraggingBottom(true)
|
||||
}}
|
||||
@@ -906,12 +935,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
|
||||
dragCommitted.current = true
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
const { startY, startHeight } = bottomDrag.current
|
||||
stopDragScheduling()
|
||||
store.reduce(s => setBottomHeight(s, startHeight + (startY - event.clientY)))
|
||||
setDraggingBottom(false)
|
||||
}}
|
||||
onPointerCancel={() => { abortDrag(() => setDraggingBottom(false)) }}
|
||||
onLostPointerCapture={() => { abortDrag(() => setDraggingBottom(false)) }}
|
||||
/>
|
||||
{/*
|
||||
The bottom panel's own close control at its tab strip's right end
|
||||
|
||||
Reference in New Issue
Block a user