Merged in feat/LOY-522-move-focus-to-newly-loaded-item (pull request #3452)

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
This commit is contained in:
Emma Zettervall
2026-01-20 08:10:42 +00:00
parent ba42690261
commit 8b56fa84e7
5 changed files with 55 additions and 18 deletions

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useCallback, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop" import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop"
@@ -31,6 +32,8 @@ export function PreviousStaysSidePeek({
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const shouldFocusNextItem = useRef(false)
const { scrollContainerRef, showBackToTop, scrollToTop } = const { scrollContainerRef, showBackToTop, scrollToTop } =
useSidePeekScrollToTop() useSidePeekScrollToTop()
@@ -50,6 +53,7 @@ export function PreviousStaysSidePeek({
function loadMoreData() { function loadMoreData() {
if (hasNextPage) { if (hasNextPage) {
shouldFocusNextItem.current = true
fetchNextPage() fetchNextPage()
} }
} }
@@ -60,6 +64,15 @@ export function PreviousStaysSidePeek({
const staysByYear = stays ? groupStaysByYear(stays) : [] const staysByYear = stays ? groupStaysByYear(stays) : []
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 ( return (
<SidePeekSelfControlled <SidePeekSelfControlled
title={intl.formatMessage({ title={intl.formatMessage({
@@ -68,6 +81,7 @@ export function PreviousStaysSidePeek({
})} })}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
sidePeekSEO={false}
> >
<div ref={scrollContainerRef} className={styles.content}> <div ref={scrollContainerRef} className={styles.content}>
{isLoading ? ( {isLoading ? (
@@ -84,12 +98,16 @@ export function PreviousStaysSidePeek({
</Typography> </Typography>
</div> </div>
<div className={styles.staysList}> <div className={styles.staysList}>
{stays.map((stay) => ( {stays.map((stay) => {
<StayCard const isFirstNewItem = stay === firstNewTransaction
key={stay.attributes.confirmationNumber} return (
stay={stay} <StayCard
/> key={stay.attributes.confirmationNumber}
))} stay={stay}
ref={isFirstNewItem ? focusRefCallback : null}
/>
)
})}
</div> </div>
</section> </section>
))} ))}

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import Link from "next/link" import { Link } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
@@ -16,7 +16,7 @@ import styles from "./stayCard.module.css"
import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" import type { StayCardProps } from "@/types/components/myPages/stays/stayCard"
export function StayCard({ stay }: StayCardProps) { export function StayCard({ stay, ref }: StayCardProps) {
const { bookingUrl, isWebAppOrigin: shouldLinkToMyStay } = stay.attributes const { bookingUrl, isWebAppOrigin: shouldLinkToMyStay } = stay.attributes
if (!shouldLinkToMyStay) { if (!shouldLinkToMyStay) {
@@ -24,13 +24,13 @@ export function StayCard({ stay }: StayCardProps) {
} }
return ( return (
<Link href={bookingUrl} className={styles.link}> <Link className={styles.link} href={bookingUrl} ref={ref}>
<CardContent stay={stay} /> <CardContent stay={stay} />
</Link> </Link>
) )
} }
function CardContent({ stay }: StayCardProps) { function CardContent({ stay }: { stay: StayCardProps["stay"] }) {
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useCallback, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop" import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop"
@@ -30,6 +31,7 @@ export function UpcomingStaysSidePeek({
}: UpcomingStaysSidePeekProps) { }: UpcomingStaysSidePeekProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const shouldFocusNextItem = useRef(false)
const { scrollContainerRef, showBackToTop, scrollToTop } = const { scrollContainerRef, showBackToTop, scrollToTop } =
useSidePeekScrollToTop() useSidePeekScrollToTop()
@@ -51,6 +53,7 @@ export function UpcomingStaysSidePeek({
function loadMoreData() { function loadMoreData() {
if (hasNextPage) { if (hasNextPage) {
shouldFocusNextItem.current = true
fetchNextPage() fetchNextPage()
} }
} }
@@ -61,6 +64,15 @@ export function UpcomingStaysSidePeek({
const staysByYear = stays ? groupStaysByYear(stays, "asc") : [] 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 ( return (
<SidePeekSelfControlled <SidePeekSelfControlled
title={intl.formatMessage({ title={intl.formatMessage({
@@ -69,6 +81,7 @@ export function UpcomingStaysSidePeek({
})} })}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
sidePeekSEO={false}
> >
<div ref={scrollContainerRef} className={styles.content}> <div ref={scrollContainerRef} className={styles.content}>
{isLoading ? ( {isLoading ? (
@@ -85,12 +98,16 @@ export function UpcomingStaysSidePeek({
</Typography> </Typography>
</div> </div>
<div className={styles.staysList}> <div className={styles.staysList}>
{stays.map((stay) => ( {stays.map((stay) => {
<StayCard const isFirstNewItem = stay === firstNewTransaction
key={stay.attributes.confirmationNumber} return (
stay={stay} <StayCard
/> key={stay.attributes.confirmationNumber}
))} stay={stay}
ref={isFirstNewItem ? focusRefCallback : null}
/>
)
})}
</div> </div>
</section> </section>
))} ))}

View File

@@ -2,4 +2,5 @@ import type { Stay } from "@scandic-hotels/trpc/routers/user/output"
export type StayCardProps = { export type StayCardProps = {
stay: Stay stay: Stay
ref?: React.Ref<HTMLAnchorElement>
} }

View File

@@ -18,6 +18,7 @@ interface SidePeekSelfControlledProps extends React.PropsWithChildren {
title: string title: string
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
sidePeekSEO?: boolean
} }
export default function SidePeekSelfControlled({ export default function SidePeekSelfControlled({
@@ -25,6 +26,7 @@ export default function SidePeekSelfControlled({
isOpen, isOpen,
onClose, onClose,
title, title,
sidePeekSEO = true,
}: SidePeekSelfControlledProps) { }: SidePeekSelfControlledProps) {
const intl = useIntl() const intl = useIntl()
@@ -81,8 +83,7 @@ export default function SidePeekSelfControlled({
</Dialog> </Dialog>
</Modal> </Modal>
</ModalOverlay> </ModalOverlay>
{sidePeekSEO && <SidePeekSEO title={title}>{children}</SidePeekSEO>}
<SidePeekSEO title={title}>{children}</SidePeekSEO>
</> </>
) )
} }