|
|
@ -24,6 +24,7 @@ type QuestionSnapListProps = { |
|
|
footer?: ReactNode; |
|
|
footer?: ReactNode; |
|
|
firstQuestionHint?: ReactNode; |
|
|
firstQuestionHint?: ReactNode; |
|
|
onQuestionExit?: (currentIndex: number, nextIndex: number) => void; |
|
|
onQuestionExit?: (currentIndex: number, nextIndex: number) => void; |
|
|
|
|
|
onQuestionTransition?: (currentIndex: number, nextIndex: number) => void; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
export function QuestionSnapList({ |
|
|
export function QuestionSnapList({ |
|
|
@ -32,12 +33,14 @@ export function QuestionSnapList({ |
|
|
footer, |
|
|
footer, |
|
|
firstQuestionHint, |
|
|
firstQuestionHint, |
|
|
onQuestionExit, |
|
|
onQuestionExit, |
|
|
|
|
|
onQuestionTransition, |
|
|
}: QuestionSnapListProps) { |
|
|
}: QuestionSnapListProps) { |
|
|
const questions = Children.toArray(children); |
|
|
const questions = Children.toArray(children); |
|
|
const wheelLockedRef = useRef(false); |
|
|
const wheelLockedRef = useRef(false); |
|
|
const wheelUnlockTimeoutRef = useRef<number | null>(null); |
|
|
const wheelUnlockTimeoutRef = useRef<number | null>(null); |
|
|
const touchStartYRef = useRef<number | null>(null); |
|
|
const touchStartYRef = useRef<number | null>(null); |
|
|
const questionRefs = useRef<Array<HTMLDivElement | null>>([]); |
|
|
const questionRefs = useRef<Array<HTMLDivElement | null>>([]); |
|
|
|
|
|
const previousActiveIndexRef = useRef<number | null>(null); |
|
|
const [activeIndex, setActiveIndex] = useState(0); |
|
|
const [activeIndex, setActiveIndex] = useState(0); |
|
|
|
|
|
|
|
|
const stepQuestion = useCallback( |
|
|
const stepQuestion = useCallback( |
|
|
@ -52,9 +55,12 @@ export function QuestionSnapList({ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
onQuestionExit?.(activeIndex, nextIndex); |
|
|
onQuestionExit?.(activeIndex, nextIndex); |
|
|
|
|
|
|
|
|
|
|
|
onQuestionTransition?.(activeIndex, nextIndex); |
|
|
|
|
|
|
|
|
setActiveIndex(nextIndex); |
|
|
setActiveIndex(nextIndex); |
|
|
}, |
|
|
}, |
|
|
[activeIndex, onQuestionExit, questions.length], |
|
|
|
|
|
|
|
|
[activeIndex, onQuestionExit, onQuestionTransition, questions.length], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const scheduleWheelUnlock = useCallback(() => { |
|
|
const scheduleWheelUnlock = useCallback(() => { |
|
|
@ -68,6 +74,13 @@ export function QuestionSnapList({ |
|
|
}, WHEEL_GESTURE_IDLE_MS); |
|
|
}, WHEEL_GESTURE_IDLE_MS); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const previousActiveIndex = previousActiveIndexRef.current; |
|
|
|
|
|
|
|
|
|
|
|
onQuestionTransition?.(previousActiveIndex ?? activeIndex, activeIndex); |
|
|
|
|
|
previousActiveIndexRef.current = activeIndex; |
|
|
|
|
|
}, [activeIndex, onQuestionTransition]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const activeQuestion = questionRefs.current[activeIndex]; |
|
|
const activeQuestion = questionRefs.current[activeIndex]; |
|
|
|
|
|
|
|
|
@ -205,7 +218,7 @@ export function QuestionSnapList({ |
|
|
aria-label="Questions" |
|
|
aria-label="Questions" |
|
|
className={[ |
|
|
className={[ |
|
|
"relative touch-none overflow-hidden focus-visible:outline-none", |
|
|
"relative touch-none overflow-hidden focus-visible:outline-none", |
|
|
"flex-1 min-h-0", |
|
|
|
|
|
|
|
|
"flex-1 min-h-0 pt-4 pb-4", |
|
|
className, |
|
|
className, |
|
|
] |
|
|
] |
|
|
.filter(Boolean) |
|
|
.filter(Boolean) |
|
|
@ -218,15 +231,29 @@ export function QuestionSnapList({ |
|
|
{questions.map((question, index) => { |
|
|
{questions.map((question, index) => { |
|
|
const offset = index - activeIndex; |
|
|
const offset = index - activeIndex; |
|
|
const isActive = offset === 0; |
|
|
const isActive = offset === 0; |
|
|
const isAdjacent = Math.abs(offset) === 1; |
|
|
|
|
|
const showFooter = Boolean( |
|
|
|
|
|
footer && isActive && index === questions.length - 1, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const isPrev = offset === -1; |
|
|
|
|
|
const isNext = offset === 1; |
|
|
const questionKey = |
|
|
const questionKey = |
|
|
isValidElement(question) && question.key !== null |
|
|
isValidElement(question) && question.key !== null |
|
|
? question.key |
|
|
? question.key |
|
|
: String(question); |
|
|
: String(question); |
|
|
|
|
|
|
|
|
|
|
|
let containerStyles = ""; |
|
|
|
|
|
let wrapperStyles = "w-full"; |
|
|
|
|
|
|
|
|
|
|
|
if (isActive) { |
|
|
|
|
|
containerStyles = "top-1/2 left-0 -translate-y-1/2 z-10 opacity-100 scale-100 pointer-events-auto"; |
|
|
|
|
|
} else if (isPrev) { |
|
|
|
|
|
containerStyles = "top-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]"; |
|
|
|
|
|
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none"; |
|
|
|
|
|
} else if (isNext) { |
|
|
|
|
|
containerStyles = "bottom-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]"; |
|
|
|
|
|
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none"; |
|
|
|
|
|
} else { |
|
|
|
|
|
containerStyles = "top-1/2 left-0 -translate-y-1/2 z-0 opacity-0 pointer-events-none scale-[0.95]"; |
|
|
|
|
|
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none"; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<div |
|
|
<div |
|
|
key={questionKey} |
|
|
key={questionKey} |
|
|
@ -237,22 +264,26 @@ export function QuestionSnapList({ |
|
|
aria-hidden={isActive ? undefined : true} |
|
|
aria-hidden={isActive ? undefined : true} |
|
|
inert={isActive ? undefined : true} |
|
|
inert={isActive ? undefined : true} |
|
|
className={[ |
|
|
className={[ |
|
|
"absolute inset-0 flex w-full items-center pb-[35svh] transition-all duration-300 ease-out", |
|
|
|
|
|
isActive |
|
|
|
|
|
? "pointer-events-auto z-10 scale-100 opacity-100" |
|
|
|
|
|
: "pointer-events-none z-0 scale-[0.96]", |
|
|
|
|
|
!isActive && isAdjacent ? "opacity-20" : "", |
|
|
|
|
|
!isActive && !isAdjacent ? "opacity-0" : "", |
|
|
|
|
|
|
|
|
"absolute flex w-full flex-col justify-center transition-all duration-300 ease-out px-[17px]", |
|
|
|
|
|
containerStyles, |
|
|
].join(" ")} |
|
|
].join(" ")} |
|
|
style={{ transform: `translateY(${offset * 50}%)` }} |
|
|
|
|
|
> |
|
|
> |
|
|
<div className="w-full"> |
|
|
|
|
|
|
|
|
<div className={wrapperStyles}> |
|
|
{question} |
|
|
{question} |
|
|
{showFooter ? <div className="mt-6">{footer}</div> : null} |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
); |
|
|
})} |
|
|
})} |
|
|
|
|
|
{footer && activeIndex === questions.length - 1 ? ( |
|
|
|
|
|
<div className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]"> |
|
|
|
|
|
<div |
|
|
|
|
|
style={{ paddingBottom: "calc(24px + var(--safe-bottom))" }} |
|
|
|
|
|
className="pointer-events-auto px-[17px] pt-3" |
|
|
|
|
|
> |
|
|
|
|
|
{footer} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
) : null} |
|
|
{firstQuestionHint ? ( |
|
|
{firstQuestionHint ? ( |
|
|
<div |
|
|
<div |
|
|
aria-hidden="true" |
|
|
aria-hidden="true" |
|
|
|