|
|
@ -15,8 +15,8 @@ import { |
|
|
} from "./question-viewport-coordinator"; |
|
|
} from "./question-viewport-coordinator"; |
|
|
|
|
|
|
|
|
const WHEEL_GESTURE_IDLE_MS = 320; |
|
|
const WHEEL_GESTURE_IDLE_MS = 320; |
|
|
const TOUCH_MIN_DISTANCE = 8; |
|
|
|
|
|
const DRAG_ENGAGE_DISTANCE = 10; |
|
|
|
|
|
|
|
|
const TOUCH_MIN_DISTANCE = 40; |
|
|
|
|
|
const DRAG_ENGAGE_DISTANCE = 16; |
|
|
const DRAG_COMMIT_RATIO = 0.3; |
|
|
const DRAG_COMMIT_RATIO = 0.3; |
|
|
const DRAG_FLICK_VELOCITY = 0.55; |
|
|
const DRAG_FLICK_VELOCITY = 0.55; |
|
|
const SNAP_ANIMATION_MS = 340; |
|
|
const SNAP_ANIMATION_MS = 340; |
|
|
@ -33,6 +33,9 @@ const DRAG_IGNORE_SELECTOR = [ |
|
|
'[role="option"]', |
|
|
'[role="option"]', |
|
|
'[role="checkbox"]', |
|
|
'[role="checkbox"]', |
|
|
'[role="radio"]', |
|
|
'[role="radio"]', |
|
|
|
|
|
'[role="switch"]', |
|
|
|
|
|
'[role="combobox"]', |
|
|
|
|
|
'[role="listbox"]', |
|
|
'[contenteditable="true"]', |
|
|
'[contenteditable="true"]', |
|
|
"[data-snap-drag-ignore]", |
|
|
"[data-snap-drag-ignore]", |
|
|
].join(", "); |
|
|
].join(", "); |
|
|
@ -353,7 +356,19 @@ export function QuestionSnapList({ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const target = event.target as HTMLElement | null; |
|
|
const target = event.target as HTMLElement | null; |
|
|
drag.ignored = Boolean(target?.closest?.(DRAG_IGNORE_SELECTOR)); |
|
|
|
|
|
|
|
|
const isIgnored = Boolean(target?.closest?.(DRAG_IGNORE_SELECTOR)); |
|
|
|
|
|
drag.ignored = isIgnored; |
|
|
|
|
|
|
|
|
|
|
|
if (isIgnored) { |
|
|
|
|
|
// When touching an interactive element (input, textarea, button, etc.),
|
|
|
|
|
|
// completely isolate it from the snap/drag gesture engine so native focus & clicks work on first tap.
|
|
|
|
|
|
drag.pointerDown = false; |
|
|
|
|
|
drag.engaged = false; |
|
|
|
|
|
drag.baseOffset = 0; |
|
|
|
|
|
drag.offset = 0; |
|
|
|
|
|
touchStartYRef.current = null; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
drag.height = |
|
|
drag.height = |
|
|
containerRef.current?.getBoundingClientRect().height ?? |
|
|
containerRef.current?.getBoundingClientRect().height ?? |
|
|
@ -395,11 +410,7 @@ export function QuestionSnapList({ |
|
|
|
|
|
|
|
|
const drag = dragRef.current; |
|
|
const drag = dragRef.current; |
|
|
|
|
|
|
|
|
if (!drag.pointerDown) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (drag.ignored) { |
|
|
|
|
|
|
|
|
if (drag.ignored || !drag.pointerDown) { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -451,7 +462,10 @@ export function QuestionSnapList({ |
|
|
const finishDrag = useCallback( |
|
|
const finishDrag = useCallback( |
|
|
(cancelled: boolean) => { |
|
|
(cancelled: boolean) => { |
|
|
const drag = dragRef.current; |
|
|
const drag = dragRef.current; |
|
|
if (!drag.pointerDown) { |
|
|
|
|
|
|
|
|
if (drag.ignored || !drag.pointerDown) { |
|
|
|
|
|
drag.pointerDown = false; |
|
|
|
|
|
drag.engaged = false; |
|
|
|
|
|
touchStartYRef.current = null; |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
drag.pointerDown = false; |
|
|
drag.pointerDown = false; |
|
|
@ -501,6 +515,13 @@ export function QuestionSnapList({ |
|
|
|
|
|
|
|
|
const drag = dragRef.current; |
|
|
const drag = dragRef.current; |
|
|
|
|
|
|
|
|
|
|
|
if (drag.ignored) { |
|
|
|
|
|
drag.pointerDown = false; |
|
|
|
|
|
drag.engaged = false; |
|
|
|
|
|
touchStartYRef.current = null; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (drag.engaged) { |
|
|
if (drag.engaged) { |
|
|
finishDrag(false); |
|
|
finishDrag(false); |
|
|
return; |
|
|
return; |
|
|
@ -511,8 +532,7 @@ export function QuestionSnapList({ |
|
|
} |
|
|
} |
|
|
drag.pointerDown = false; |
|
|
drag.pointerDown = false; |
|
|
|
|
|
|
|
|
// Fallback for very fast flicks whose touchmove never engaged the
|
|
|
|
|
|
// drag layer: fall back to the distance-based step.
|
|
|
|
|
|
|
|
|
// Fallback for fast flicks whose touchmove never engaged continuous drag
|
|
|
const startY = touchStartYRef.current; |
|
|
const startY = touchStartYRef.current; |
|
|
const endY = event.changedTouches[0]?.clientY; |
|
|
const endY = event.changedTouches[0]?.clientY; |
|
|
touchStartYRef.current = null; |
|
|
touchStartYRef.current = null; |
|
|
@ -533,6 +553,13 @@ export function QuestionSnapList({ |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const handleTouchCancel = useCallback(() => { |
|
|
const handleTouchCancel = useCallback(() => { |
|
|
|
|
|
const drag = dragRef.current; |
|
|
|
|
|
if (drag.ignored) { |
|
|
|
|
|
drag.pointerDown = false; |
|
|
|
|
|
drag.engaged = false; |
|
|
|
|
|
touchStartYRef.current = null; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
finishDrag(true); |
|
|
finishDrag(true); |
|
|
}, [finishDrag]); |
|
|
}, [finishDrag]); |
|
|
|
|
|
|
|
|
@ -545,7 +572,7 @@ export function QuestionSnapList({ |
|
|
ref={containerRef} |
|
|
ref={containerRef} |
|
|
aria-label="Questions" |
|
|
aria-label="Questions" |
|
|
className={[ |
|
|
className={[ |
|
|
"question-snap-list relative touch-none overflow-hidden focus-visible:outline-none", |
|
|
|
|
|
|
|
|
"question-snap-list relative touch-pan-y overflow-hidden focus-visible:outline-none", |
|
|
"flex-1 min-h-0 pt-4 pb-4", |
|
|
"flex-1 min-h-0 pt-4 pb-4", |
|
|
className, |
|
|
className, |
|
|
] |
|
|
] |
|
|
|