feat(LOY-522): Move focus to the newly loaded stay in sidepeek for upcoming and previous stay * feat(LOY-522): Moved focus to the newly loaded stay in sidepeek for upcoming and previous stay Approved-by: Anton Gunnarsson
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useRef } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop"
|
|
import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
|
|
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
|
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
import { trpc } from "@scandic-hotels/trpc/client"
|
|
|
|
import useLang from "@/hooks/useLang"
|
|
|
|
import ShowMoreButton from "../../ShowMoreButton"
|
|
import { StayCard } from "../../StayCard"
|
|
import { groupStaysByYear } from "../../utils/groupStaysByYear"
|
|
|
|
import styles from "./upcomingStaysSidePeek.module.css"
|
|
|
|
import type { UpcomingStaysNonNullResponseObject } from "@/types/components/myPages/stays/upcoming"
|
|
|
|
interface UpcomingStaysSidePeekProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export function UpcomingStaysSidePeek({
|
|
isOpen,
|
|
onClose,
|
|
}: UpcomingStaysSidePeekProps) {
|
|
const intl = useIntl()
|
|
const lang = useLang()
|
|
const shouldFocusNextItem = useRef(false)
|
|
|
|
const { scrollContainerRef, showBackToTop, scrollToTop } =
|
|
useSidePeekScrollToTop()
|
|
|
|
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
|
trpc.user.stays.upcoming.useInfiniteQuery(
|
|
{
|
|
limit: 10,
|
|
lang,
|
|
includeFirstStay: true,
|
|
},
|
|
{
|
|
getNextPageParam: (lastPage) => {
|
|
return lastPage?.nextCursor
|
|
},
|
|
enabled: isOpen,
|
|
}
|
|
)
|
|
|
|
function loadMoreData() {
|
|
if (hasNextPage) {
|
|
shouldFocusNextItem.current = true
|
|
fetchNextPage()
|
|
}
|
|
}
|
|
|
|
const stays = data?.pages
|
|
.filter((page): page is UpcomingStaysNonNullResponseObject => !!page?.data)
|
|
.flatMap((page) => page.data)
|
|
|
|
const staysByYear = stays ? groupStaysByYear(stays, "asc") : []
|
|
|
|
const lastPage = data?.pages?.[data.pages.length - 1]
|
|
const firstNewTransaction = lastPage?.data?.[0]
|
|
|
|
const focusRefCallback = useCallback((node: HTMLAnchorElement | null) => {
|
|
if (!node || !shouldFocusNextItem.current) return
|
|
node.focus()
|
|
shouldFocusNextItem.current = false
|
|
}, [])
|
|
|
|
return (
|
|
<SidePeekSelfControlled
|
|
title={intl.formatMessage({
|
|
id: "stays.upcoming.title",
|
|
defaultMessage: "Upcoming stays",
|
|
})}
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
sidePeekSEO={false}
|
|
>
|
|
<div ref={scrollContainerRef} className={styles.content}>
|
|
{isLoading ? (
|
|
<div className={styles.loadingContainer}>
|
|
<LoadingSpinner />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{staysByYear.map(({ year, stays }) => (
|
|
<section key={year} className={styles.yearSection}>
|
|
<div className={styles.yearHeader}>
|
|
<Typography variant="Title/Overline/sm">
|
|
<span className={styles.yearText}>{year}</span>
|
|
</Typography>
|
|
</div>
|
|
<div className={styles.staysList}>
|
|
{stays.map((stay) => {
|
|
const isFirstNewItem = stay === firstNewTransaction
|
|
return (
|
|
<StayCard
|
|
key={stay.attributes.confirmationNumber}
|
|
stay={stay}
|
|
ref={isFirstNewItem ? focusRefCallback : null}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</section>
|
|
))}
|
|
{hasNextPage && (
|
|
<ShowMoreButton
|
|
disabled={isFetching}
|
|
loadMoreData={loadMoreData}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
{showBackToTop && !isLoading && (
|
|
<BackToTopButton
|
|
label={intl.formatMessage({
|
|
id: "common.backToTop",
|
|
defaultMessage: "Back to top",
|
|
})}
|
|
onPress={scrollToTop}
|
|
position="right"
|
|
/>
|
|
)}
|
|
</div>
|
|
</SidePeekSelfControlled>
|
|
)
|
|
}
|