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:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user