diff --git a/web/components/answers/compatibility-questions-display.tsx b/web/components/answers/compatibility-questions-display.tsx index 75752ce3..06b988aa 100644 --- a/web/components/answers/compatibility-questions-display.tsx +++ b/web/components/answers/compatibility-questions-display.tsx @@ -13,7 +13,7 @@ import {User} from 'common/user' import {shortenNumber} from 'common/util/format' import {keyBy, sortBy} from 'lodash' import {PinIcon} from 'lucide-react' -import {useCallback, useEffect, useMemo, useState} from 'react' +import {RefObject, useCallback, useEffect, useMemo, useRef, useState} from 'react' import toast from 'react-hot-toast' import {AddCompatibilityQuestionButton} from 'web/components/answers/add-compatibility-question-button' import DropdownMenu from 'web/components/comments/dropdown-menu' @@ -62,6 +62,25 @@ import {PinQuestionButton} from './pin-question-button' const NUM_QUESTIONS_TO_SHOW = 8 const NUM_PINNED_QUESTIONS_TO_SHOW = 4 +// Matches the `scroll-mt-24` on the paginated lists (6rem), which is what keeps a scrolled-to first prompt +// clear of the sticky header. +const LIST_SCROLL_OFFSET = 96 + +/** + * Changing page is a request for new content to read, and reading starts at the first prompt — so put the + * top of that list back in view instead of leaving the reader parked at the tail of the new page. The + * paginator itself is not lost: it sits where they left it, one scroll down past the prompts they came for. + * + * Only scrolls when the top has actually drifted out of reach (short list, or the reader is already up + * there) — a jump that changes nothing reads as a glitch. + */ +const scrollListIntoView = (ref: RefObject) => { + const el = ref.current + if (!el) return + if (el.getBoundingClientRect().top >= LIST_SCROLL_OFFSET) return + el.scrollIntoView({block: 'start', behavior: 'smooth'}) +} + export function CompatibilityQuestionsDisplay(props: { isCurrentUser: boolean user: User @@ -145,7 +164,16 @@ export function CompatibilityQuestionsDisplay(props: { ) }, [answers, compatibilityQuestions, comparedAnswers, searchTerm, sort]) + // Each list scrolls to its own top, not to a shared one — paging the pinned list must not yank the + // reader down to the main list, or vice versa. + const listRef = useRef(null) + const pinnedListRef = useRef(null) + const [page, setPage] = useState(0) + const goToPage = (p: number) => { + setPage(p) + scrollListIntoView(listRef) + } const currentSlice = page * NUM_QUESTIONS_TO_SHOW const shownAnswers = sortedAndFilteredAnswers.slice( currentSlice, @@ -160,6 +188,10 @@ export function CompatibilityQuestionsDisplay(props: { }, [answers, pinnedQuestionIds]) const [pinnedPage, setPinnedPage] = useState(0) + const goToPinnedPage = (p: number) => { + setPinnedPage(p) + scrollListIntoView(pinnedListRef) + } const pinnedCurrentSlice = pinnedPage * NUM_PINNED_QUESTIONS_TO_SHOW const shownPinnedAnswers = pinnedAnswers.slice( pinnedCurrentSlice, @@ -177,7 +209,7 @@ export function CompatibilityQuestionsDisplay(props: { {pinnedAnswers.length > 0 && ( - + {shownPinnedAnswers.map((answer) => ( )}
@@ -266,7 +298,7 @@ export function CompatibilityQuestionsDisplay(props: { ) : ( <> - + {shownAnswers.map((answer) => { return ( )} {isCurrentUser && !fromProfilePage && ( @@ -450,7 +482,7 @@ export function CompatibilityAnswerBlock(props: { // `font-figtree` is the body face: globals.css sets every h1–h6 to Newsreader at weight 700, // so a heading element opts out of the serif explicitly or it does not opt out at all. className="font-figtree text-ink-600 min-w-0 font-normal" - style={{fontSize: '16px', lineHeight: '1.45'}} + style={{fontSize: '18px', lineHeight: '1.45'}} data-testid="profile-compatibility-question" > {question.question}