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

View File

@@ -1,6 +1,6 @@
"use client"
import Link from "next/link"
import { Link } from "react-aria-components"
import { useIntl } from "react-intl"
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"
export function StayCard({ stay }: StayCardProps) {
export function StayCard({ stay, ref }: StayCardProps) {
const { bookingUrl, isWebAppOrigin: shouldLinkToMyStay } = stay.attributes
if (!shouldLinkToMyStay) {
@@ -24,13 +24,13 @@ export function StayCard({ stay }: StayCardProps) {
}
return (
<Link href={bookingUrl} className={styles.link}>
<Link className={styles.link} href={bookingUrl} ref={ref}>
<CardContent stay={stay} />
</Link>
)
}
function CardContent({ stay }: StayCardProps) {
function CardContent({ stay }: { stay: StayCardProps["stay"] }) {
const lang = useLang()
const intl = useIntl()

View File

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

View File

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