feat(SW-441): Implemented useScrollSpy hook
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
.cardContainer {
|
||||||
|
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||||
|
}
|
||||||
|
|
||||||
.spanOne {
|
.spanOne {
|
||||||
grid-column: span 1;
|
grid-column: span 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function FacilitiesCardGrid({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={imageCard.card.id}>
|
<section id={imageCard.card.id} className={styles.cardContainer}>
|
||||||
<Grids.Stackable className={styles.desktopGrid}>
|
<Grids.Stackable className={styles.desktopGrid}>
|
||||||
{facilitiesCardGrid.map((card: FacilityCardType) => (
|
{facilitiesCardGrid.map((card: FacilityCardType) => (
|
||||||
<Card {...card} key={card.id} className={getCardClassName(card)} />
|
<Card {...card} key={card.id} className={getCardClassName(card)} />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { RoomCard } from "./RoomCard"
|
|||||||
|
|
||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
|
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
import type { RoomsProps } from "./types"
|
import type { RoomsProps } from "./types"
|
||||||
|
|
||||||
export function Rooms({ rooms }: RoomsProps) {
|
export function Rooms({ rooms }: RoomsProps) {
|
||||||
@@ -50,8 +51,11 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer id="rooms-section">
|
<SectionContainer
|
||||||
<div ref={scrollRef}></div>
|
id={HotelHashValues.rooms}
|
||||||
|
className={styles.roomsContainer}
|
||||||
|
>
|
||||||
|
<div ref={scrollRef} className={styles.scrollRef}></div>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
textTransform="capitalize"
|
textTransform="capitalize"
|
||||||
title={intl.formatMessage({ id: "Rooms" })}
|
title={intl.formatMessage({ id: "Rooms" })}
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
.roomsContainer {
|
||||||
|
position: relative;
|
||||||
|
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollRef {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(-1 * var(--hotel-page-scroll-margin-top));
|
||||||
|
}
|
||||||
|
|
||||||
.ctaContainer {
|
.ctaContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import useHash from "@/hooks/useHash"
|
import useHash from "@/hooks/useHash"
|
||||||
|
import useScrollSpy from "@/hooks/useScrollSpy"
|
||||||
|
|
||||||
import styles from "./tabNavigation.module.css"
|
import styles from "./tabNavigation.module.css"
|
||||||
|
|
||||||
@@ -18,46 +20,58 @@ export default function TabNavigation({ restaurantTitle }: TabNavigationProps) {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// const [isObserverActive, setIsObserverActive] = useState<boolean>(true)
|
// const [isObserverActive, setIsObserverActive] = useState<boolean>(true)
|
||||||
|
|
||||||
const hotelTabLinks: { href: HotelHashValues | string; text: string }[] = [
|
const hotelTabLinks: { hash: HotelHashValues | string; text: string }[] = [
|
||||||
{
|
{
|
||||||
href: HotelHashValues.overview,
|
hash: HotelHashValues.overview,
|
||||||
text: intl.formatMessage({ id: "Overview" }),
|
text: intl.formatMessage({ id: "Overview" }),
|
||||||
},
|
},
|
||||||
{ href: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
|
{ hash: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
|
||||||
{
|
{
|
||||||
href: HotelHashValues.restaurant,
|
hash: HotelHashValues.restaurant,
|
||||||
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),
|
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: HotelHashValues.meetings,
|
hash: HotelHashValues.meetings,
|
||||||
text: intl.formatMessage({ id: "Meetings & Conferences" }),
|
text: intl.formatMessage({ id: "Meetings & Conferences" }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: HotelHashValues.wellness,
|
hash: HotelHashValues.wellness,
|
||||||
text: intl.formatMessage({ id: "Wellness & Exercise" }),
|
text: intl.formatMessage({ id: "Wellness & Exercise" }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: HotelHashValues.activities,
|
hash: HotelHashValues.activities,
|
||||||
text: intl.formatMessage({ id: "Activities" }),
|
text: intl.formatMessage({ id: "Activities" }),
|
||||||
},
|
},
|
||||||
{ href: HotelHashValues.faq, text: intl.formatMessage({ id: "FAQ" }) },
|
{ hash: HotelHashValues.faq, text: intl.formatMessage({ id: "FAQ" }) },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const { activeSectionId, pauseScrollSpy } = useScrollSpy(
|
||||||
|
hotelTabLinks.map(({ hash }) => hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSectionId) {
|
||||||
|
router.replace(`#${activeSectionId}`, { scroll: false })
|
||||||
|
}
|
||||||
|
}, [activeSectionId, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.stickyWrapper}>
|
<div className={styles.stickyWrapper}>
|
||||||
<nav className={styles.tabsContainer}>
|
<nav className={styles.tabsContainer}>
|
||||||
{hotelTabLinks.map((link) => {
|
{hotelTabLinks.map((link) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
hash === link.href ||
|
hash === link.hash ||
|
||||||
(hash === "" && link.href === HotelHashValues.overview)
|
(!hash && link.hash === HotelHashValues.overview)
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.hash}
|
||||||
href={link.href}
|
href={`#${link.hash}`}
|
||||||
active={isActive}
|
active={isActive}
|
||||||
variant="tab"
|
variant="tab"
|
||||||
color="burgundy"
|
color="burgundy"
|
||||||
textDecoration="none"
|
textDecoration="none"
|
||||||
|
scroll={true}
|
||||||
|
onClick={pauseScrollSpy}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: link.text })}
|
{intl.formatMessage({ id: link.text })}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
.pageContainer {
|
.pageContainer {
|
||||||
|
--hotel-page-navigation-height: 59px;
|
||||||
|
--hotel-page-scroll-margin-top: calc(
|
||||||
|
var(--hotel-page-navigation-height) + var(--Spacing-x2)
|
||||||
|
);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"hotelImages"
|
"hotelImages"
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
.introContainer {
|
.introContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x4);
|
gap: var(--Spacing-x4);
|
||||||
|
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import TabNavigation from "./TabNavigation"
|
|||||||
|
|
||||||
import styles from "./hotelPage.module.css"
|
import styles from "./hotelPage.module.css"
|
||||||
|
|
||||||
|
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
|
|
||||||
export default async function HotelPage() {
|
export default async function HotelPage() {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = getLang()
|
const lang = getLang()
|
||||||
@@ -63,7 +65,7 @@ export default async function HotelPage() {
|
|||||||
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
||||||
/>
|
/>
|
||||||
<main className={styles.mainSection}>
|
<main className={styles.mainSection}>
|
||||||
<div id="overview" className={styles.introContainer}>
|
<div id={HotelHashValues.overview} className={styles.introContainer}>
|
||||||
<IntroSection
|
<IntroSection
|
||||||
hotelName={hotelName}
|
hotelName={hotelName}
|
||||||
hotelDescription={hotelDescription}
|
hotelDescription={hotelDescription}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { useParams } from "next/navigation"
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
function getHash() {
|
function getHash() {
|
||||||
return typeof window !== "undefined" ? window.location.hash : undefined
|
if (typeof window === "undefined" || !window.location.hash) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return window.location.hash.split("#")[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHash() {
|
export default function useHash() {
|
||||||
const [isClient, setIsClient] = useState(false)
|
const [isClient, setIsClient] = useState(false)
|
||||||
const [hash, setHash] = useState(getHash())
|
const [hash, setHash] = useState(getHash())
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -19,5 +22,3 @@ function useHash() {
|
|||||||
|
|
||||||
return isClient ? hash : null
|
return isClient ? hash : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useHash
|
|
||||||
|
|||||||
70
hooks/useScrollSpy.ts
Normal file
70
hooks/useScrollSpy.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export default function useScrollSpy(
|
||||||
|
sectionIds: string[],
|
||||||
|
options: IntersectionObserverInit = {}
|
||||||
|
): {
|
||||||
|
activeSectionId: string
|
||||||
|
pauseScrollSpy: () => void
|
||||||
|
} {
|
||||||
|
const [activeSectionId, setActiveSectionId] = useState("")
|
||||||
|
const observerIsInactive = useRef(false)
|
||||||
|
|
||||||
|
const mergedOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: null,
|
||||||
|
// Make sure only to activate the section when it reaches the top of the viewport.
|
||||||
|
// A negative value for rootMargin shrinks the root bounding box inward,
|
||||||
|
// meaning elements will only be considered intersecting when they are further inside the viewport.
|
||||||
|
rootMargin: "-8% 0% -90% 0%",
|
||||||
|
threshold: 0,
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
|
[options]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleIntersection = useCallback(
|
||||||
|
(entries: IntersectionObserverEntry[]) => {
|
||||||
|
if (observerIsInactive.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersectingEntries: IntersectionObserverEntry[] = []
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
intersectingEntries.push(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (intersectingEntries.length) {
|
||||||
|
setActiveSectionId(intersectingEntries[0].target.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(handleIntersection, mergedOptions)
|
||||||
|
const elements = sectionIds
|
||||||
|
.map((id) => document.getElementById(id))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observer.observe(element)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => elements.forEach((el) => el && observer.unobserve(el))
|
||||||
|
}, [sectionIds, mergedOptions, handleIntersection])
|
||||||
|
|
||||||
|
const pauseScrollSpy = () => {
|
||||||
|
observerIsInactive.current = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observerIsInactive.current = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { activeSectionId, pauseScrollSpy }
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
export enum HotelHashValues { // Should these be translated?
|
export enum HotelHashValues {
|
||||||
overview = "#overview",
|
overview = "overview",
|
||||||
rooms = "#rooms-section",
|
rooms = "rooms-section",
|
||||||
restaurant = "#restaurant-and-bar",
|
restaurant = "restaurant-and-bar",
|
||||||
meetings = "#meetings-and-conferences",
|
meetings = "meetings-and-conferences",
|
||||||
wellness = "#wellness-and-exercise",
|
wellness = "wellness-and-exercise",
|
||||||
activities = "#activities",
|
activities = "activities",
|
||||||
faq = "#faq",
|
faq = "faq",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TabNavigationProps = {
|
export type TabNavigationProps = {
|
||||||
|
|||||||
Reference in New Issue
Block a user