Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/components/ChatView/__tests__/useScrollMode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ test('only scrolling keys claim reader ownership', () => {
assert.equal(readerInputMayScroll('touchmove'), true)
})

test('a disclosure tap does not mistake its collapse clamp for a reader swipe', () => {
const disclosureTarget = {
closest(selector) {
assert.match(selector, /chat__activity-header/)
return { className: 'chat__activity-header' }
},
}
const ordinaryTarget = { closest: () => null }

assert.equal(readerInputMayScroll('pointerdown', '', disclosureTarget), false)
assert.equal(readerInputMayScroll('touchstart', '', disclosureTarget), false)
assert.equal(readerInputMayScroll('touchmove', '', disclosureTarget), true,
'a real drag starting on the disclosure still claims reader ownership')
assert.equal(readerInputMayScroll('pointerdown', '', ordinaryTarget), true)
})

test('inputs without an end event get a next-frame no-scroll release', () => {
assert.equal(readerInputNeedsFrameRelease('wheel'), true)
assert.equal(readerInputNeedsFrameRelease('keydown'), true)
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/components/ChatView/useScrollMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,10 +534,21 @@ export function gestureLayoutRetryDelay(gestureWindowUntil, now) {
}


/** Only keys whose default action can move the chat begin reader ownership.
/** Only input whose default action can move the chat begins reader ownership.
* Text entry and activating controls inside a message must not freeze layout
* until the no-scroll dead-man expires. */
export function readerInputMayScroll(type, key = '') {
* until the no-scroll dead-man expires. A disclosure tap is especially
* important here: collapsing its body can clamp scrollTop and emit a synthetic
* scroll event. Treating that clamp as the start of a swipe holds the smaller
* spacer for the 250ms momentum window, so the viewport moves once on collapse
* and again when layout ownership returns. A real drag that starts on a
* disclosure still produces touchmove, which claims reader ownership below. */
export function readerInputMayScroll(type, key = '', target = null) {
if ((type === 'pointerdown' || type === 'touchstart')
&& target?.closest?.(
'.chat__activity-header, .chat__tool-header, .chat__marker-header',
)) {
return false
}
if (type !== 'keydown') return true
return [
'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', 'Tab', ' ',
Expand Down Expand Up @@ -1430,7 +1441,7 @@ export default function useScrollMode({
})
}
const onUserInput = (event) => {
if (!readerInputMayScroll(event?.type, event?.key)) return
if (!readerInputMayScroll(event?.type, event?.key, event?.target)) return
// Input and its first scroll event are ordered, but not guaranteed to be
// less than 250ms apart under a busy renderer. Keep layout ownership
// suspended until that first event actually lands; after it, the normal
Expand Down
Loading