@ -60,6 +60,8 @@ type SnapDragState = {
hardIgnored : boolean ;
isTextInput : boolean ;
isOptionCard : boolean ;
isScrollableQuestion : boolean ;
scrollContainer : HTMLElement | null ;
engaged : boolean ;
didDrag : boolean ;
animating : boolean ;
@ -117,6 +119,8 @@ export function QuestionSnapList({
hardIgnored : false ,
isTextInput : false ,
isOptionCard : false ,
isScrollableQuestion : false ,
scrollContainer : null ,
engaged : false ,
didDrag : false ,
animating : false ,
@ -144,6 +148,17 @@ export function QuestionSnapList({
return ;
}
const currentEl = questionRefs . current [ activeIndexRef . current ] ;
const currentContent = currentEl ? . querySelector < HTMLElement > ( ".question-snap-content" ) ;
if ( currentContent ) {
currentContent . scrollTop = 0 ;
}
const nextEl = questionRefs . current [ nextIndex ] ;
const nextContent = nextEl ? . querySelector < HTMLElement > ( ".question-snap-content" ) ;
if ( nextContent ) {
nextContent . scrollTop = 0 ;
}
resetQuestionKeyboardState ( ) ;
onQuestionExit ? . ( activeIndexRef . current , nextIndex ) ;
setActiveIndex ( nextIndex ) ;
@ -424,6 +439,30 @@ export function QuestionSnapList({
return ;
}
const activeEl = questionRefs . current [ activeIndexRef . current ] ;
const contentEl = activeEl ? . querySelector < HTMLElement > ( ".question-snap-content" ) ;
const hasInlineOptions = Boolean (
activeEl ? . querySelector ( 'input[type="radio"], input[type="checkbox"]' )
) ;
const isOverflowing =
hasInlineOptions &&
contentEl != null &&
contentEl . scrollHeight > contentEl . clientHeight + 6 ;
if ( isOverflowing && contentEl ) {
const currentScroll = contentEl . scrollTop ;
const maxScroll = contentEl . scrollHeight - contentEl . clientHeight ;
const isScrollingDown = delta > 0 ;
const isScrollingUp = delta < 0 ;
if ( isScrollingDown && currentScroll < maxScroll - 4 ) {
return ;
}
if ( isScrollingUp && currentScroll > 4 ) {
return ;
}
}
event . preventDefault ( ) ;
if ( ! wheelLockedRef . current ) {
@ -539,6 +578,20 @@ export function QuestionSnapList({
drag . isOptionCard = isOptionCard ;
drag . didDrag = false ;
// 3. Detect overflowing inline-options question
const activeEl = questionRefs . current [ activeIndexRef . current ] ;
const contentEl = activeEl ? . querySelector < HTMLElement > ( ".question-snap-content" ) ;
const hasInlineOptions = Boolean (
activeEl ? . querySelector ( 'input[type="radio"], input[type="checkbox"]' )
) ;
const isOverflowing =
hasInlineOptions &&
contentEl != null &&
contentEl . scrollHeight > contentEl . clientHeight + 6 ;
drag . isScrollableQuestion = Boolean ( isOverflowing ) ;
drag . scrollContainer = isOverflowing ? contentEl : null ;
drag . height =
container . getBoundingClientRect ( ) . height ||
window . innerHeight ||
@ -570,7 +623,7 @@ export function QuestionSnapList({
drag . lastMoveTime = drag . startTime ;
touchStartYRef . current = drag . startY ;
console . log ( ` [Snap] TouchStart at ( ${ drag . startX . toFixed ( 0 ) } , ${ drag . startY . toFixed ( 0 ) } ) on < ${ target ? . tagName ? . toLowerCase ( ) } > (isOption: ${ isOptionCard } ) ` ) ;
console . log ( ` [Snap] TouchStart at ( ${ drag . startX . toFixed ( 0 ) } , ${ drag . startY . toFixed ( 0 ) } ) on < ${ target ? . tagName ? . toLowerCase ( ) } > (isOption: ${ isOptionCard } , isScrollable: ${ drag . isScrollableQuestion } ) ` ) ;
} ;
const onTouchMove = ( event : TouchEvent ) = > {
@ -610,6 +663,25 @@ export function QuestionSnapList({
return ;
}
// If this question has inline options that overflow the screen:
if ( drag . isScrollableQuestion && drag . scrollContainer ) {
const scrollEl = drag . scrollContainer ;
const currentScroll = scrollEl . scrollTop ;
const maxScroll = scrollEl . scrollHeight - scrollEl . clientHeight ;
const isDraggingUp = deltaY < 0 ; // Finger moving up -> scrolling down to view lower options
const isDraggingDown = deltaY > 0 ; // Finger moving down -> scrolling up towards top
if ( isDraggingUp && currentScroll < maxScroll - 4 ) {
// Not at the bottom yet! Allow internal scroll of options to continue without snapping page.
return ;
}
if ( isDraggingDown && currentScroll > 4 ) {
// Not at the top yet! Allow internal scroll back to top without snapping page.
return ;
}
}
// Pager claims the gesture!
drag . engaged = true ;
drag . didDrag = true ;
@ -624,8 +696,8 @@ export function QuestionSnapList({
console . log ( ` [Snap] Drag Engaged: deltaY= ${ deltaY . toFixed ( 0 ) } px, slop= ${ slop } px ` ) ;
}
// Non-passive preventDefault stops native pan and guarantees gesture ownership
if ( event . cancelable ) {
// Non-passive preventDefault stops native pan only when pager has claimed the gesture
if ( drag . engaged && event . cancelable ) {
event . preventDefault ( ) ;
}
@ -685,8 +757,8 @@ export function QuestionSnapList({
}
drag . pointerDown = false ;
// If it was a clean tap on an option card / button, leave it to native click !
if ( drag . isOptionCard ) {
// If it was a clean tap on an option card / button or an internal scroll on overflowing question, leave it !
if ( drag . isOptionCard || drag . isScrollableQuestion ) {
touchStartYRef . current = null ;
return ;
}
@ -813,7 +885,9 @@ export function QuestionSnapList({
containerStyles ,
] . join ( " " ) }
>
< div className = { ` question-snap-content ${ wrapperStyles } ` } >
< div
className = { ` question-snap-content ${ wrapperStyles } overflow-y-auto max-h-full overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ` }
>
{ question }
< / div >
{ isActive && (