fix(terminal): stop wheel replay when mouse reporting is disabled (#8616)

This commit is contained in:
Rod Boev 2026-07-24 03:25:07 -04:00 committed by GitHub
parent 739fce5287
commit 4e670d3e4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 102 additions and 5 deletions

View File

@ -393,6 +393,7 @@ describe('terminal mouse wheel multiplier', () => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'any' },
rows: 24
},
{ getTuiMouseWheelMultiplier: () => 1 }
@ -418,6 +419,81 @@ describe('terminal mouse wheel multiplier', () => {
expect(shouldMultiplyTerminalMouseWheel(dispatched[0]!, target)).toBe(false)
})
it('does not replay with a stale active mouse-reporting class', async () => {
vi.stubGlobal('WheelEvent', TestWheelEvent)
const handlers: ((event: WheelEvent) => boolean)[] = []
const target = Object.assign(new EventTarget(), {
classList: {
contains: (className: string) => className === 'enable-mouse-events'
}
}) as unknown as EventTarget & HTMLElement
const dispatched: WheelEvent[] = []
target.addEventListener('wheel', (event) => dispatched.push(event as WheelEvent))
const terminal = {
attachCustomWheelEventHandler: (handler: (event: WheelEvent) => boolean) => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'none' as const },
rows: 24
}
attachTerminalMouseWheelMultiplier(terminal)
const event = new TestWheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaMode: DOM_DELTA_PIXEL,
deltaY: 12
}) as WheelEvent
Object.defineProperty(event, 'wheelDeltaY', {
configurable: true,
value: -120
})
expect(handlers[0]?.(event)).toBe(true)
await Promise.resolve()
expect(dispatched).toHaveLength(0)
})
it('discards pending replay when mouse reporting turns off before drain', async () => {
vi.stubGlobal('WheelEvent', TestWheelEvent)
const handlers: ((event: WheelEvent) => boolean)[] = []
const target = Object.assign(new EventTarget(), {
classList: {
contains: (className: string) => className === 'enable-mouse-events'
}
}) as unknown as EventTarget & HTMLElement
const dispatched: WheelEvent[] = []
target.addEventListener('wheel', (event) => dispatched.push(event as WheelEvent))
const terminal = {
attachCustomWheelEventHandler: (handler: (event: WheelEvent) => boolean) => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'any' as 'any' | 'none' },
rows: 24
}
attachTerminalMouseWheelMultiplier(terminal)
const event = new TestWheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaMode: DOM_DELTA_PIXEL,
deltaY: 12
}) as WheelEvent
Object.defineProperty(event, 'wheelDeltaY', {
configurable: true,
value: -120
})
expect(handlers[0]?.(event)).toBe(false)
terminal.modes.mouseTrackingMode = 'none'
await Promise.resolve()
expect(dispatched).toHaveLength(0)
})
it('replays trackpad-like TUI pixel scrolling with responsive direction reversal', async () => {
vi.stubGlobal('WheelEvent', TestWheelEvent)
const handlers: ((event: WheelEvent) => boolean)[] = []
@ -434,6 +510,7 @@ describe('terminal mouse wheel multiplier', () => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'any' },
rows: 24
},
{ getTuiMouseWheelMultiplier: () => 1 }
@ -486,6 +563,7 @@ describe('terminal mouse wheel multiplier', () => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'any' },
rows: 24
},
{ getTuiMouseWheelMultiplier: () => 1 }
@ -520,6 +598,7 @@ describe('terminal mouse wheel multiplier', () => {
handlers.push(handler)
},
element: target,
modes: { mouseTrackingMode: 'any' },
rows: 24
},
{

View File

@ -21,7 +21,9 @@ const XTERM_MOUSE_REPORTING_CLASS = 'enable-mouse-events'
const REPLAYED_WHEEL_EVENT_PROPERTY = '__orcaReplayedTerminalWheelEvent'
const DOM_DELTA_LINE = 1
type TerminalWheelTarget = Pick<Terminal, 'attachCustomWheelEventHandler' | 'element' | 'rows'>
type TerminalWheelTarget = Pick<Terminal, 'attachCustomWheelEventHandler' | 'element' | 'rows'> & {
modes: Pick<Terminal['modes'], 'mouseTrackingMode'>
}
type TerminalMouseWheelMultiplierOptions = {
getTuiMouseWheelMultiplier?: () => number | undefined
@ -117,7 +119,10 @@ export function shouldMultiplyTerminalMouseWheel(
return true
}
function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState): void {
function drainTerminalTuiWheelReports(
state: TerminalTuiMouseWheelReplayState,
terminal: TerminalWheelTarget
): void {
const target = state.pendingTarget
const event = state.pendingEvent
if (!target || !event || state.pendingReports <= 0) {
@ -125,6 +130,15 @@ function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState):
return
}
if (terminal.modes.mouseTrackingMode === 'none') {
state.pendingReports = 0
state.drainScheduled = false
state.pendingDirection = 0
state.pendingEvent = null
state.pendingTarget = null
return
}
const reportsToDispatch = state.pendingReports
for (let i = 0; i < reportsToDispatch; i += 1) {
target.dispatchEvent(cloneWheelReportEvent(event))
@ -138,6 +152,7 @@ function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState):
function queueTerminalTuiWheelReports(
state: TerminalTuiMouseWheelReplayState,
terminal: TerminalWheelTarget,
target: EventTarget,
event: WheelEvent,
reportCount: number
@ -164,7 +179,7 @@ function queueTerminalTuiWheelReports(
// Why: dispatch after xterm returns from the original wheel handler, but do
// not frame-cap reports; fullscreen TUIs need the full wheel distance.
queueMicrotask(() => {
drainTerminalTuiWheelReports(state)
drainTerminalTuiWheelReports(state, terminal)
})
}
@ -174,7 +189,10 @@ export function attachTerminalMouseWheelMultiplier(
): void {
const replayState = createTerminalTuiMouseWheelReplayState()
terminal.attachCustomWheelEventHandler((event) => {
if (!shouldMultiplyTerminalMouseWheel(event, terminal.element)) {
if (
terminal.modes.mouseTrackingMode === 'none' ||
!shouldMultiplyTerminalMouseWheel(event, terminal.element)
) {
return true
}
@ -195,7 +213,7 @@ export function attachTerminalMouseWheelMultiplier(
rows: terminal.rows
}
)
queueTerminalTuiWheelReports(replayState, target, event, reportCount)
queueTerminalTuiWheelReports(replayState, terminal, target, event, reportCount)
return false
})