Merged in feat/sw-3218-move-sidepeek-to-design-system (pull request #2598)

feat(SW-3218): Move SidePeek to design-system

* Remove SidePeekProvider dependency on Next

* Remove dependency on i18n in sidepeek

* Inline types

* Move SidePeek to design-system

* Fix align-items value


Approved-by: Bianca Widstam
This commit is contained in:
Anton Gunnarsson
2025-08-06 08:35:34 +00:00
parent 75ffd5d10b
commit 7fb082f712
21 changed files with 152 additions and 65 deletions

View File

@@ -5,9 +5,9 @@ import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import JsonToHtml from "@/components/JsonToHtml"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { trackOpenSidePeekOnDestinationPagesEvent } from "@/utils/tracking/destinationPage"
import type { DestinationCityPageData } from "@scandic-hotels/trpc/types/destinationCityPage"
@@ -56,6 +56,9 @@ export default function DestinationPageSidepeek({
isOpen={sidePeekIsOpen}
openInRoot
handleClose={() => setSidePeekIsOpen(false)}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<JsonToHtml
nodes={content.json.children}

View File

@@ -1,7 +1,7 @@
import { Divider } from "@scandic-hotels/design-system/Divider"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import ContactInformation from "./ContactInformation"
@@ -27,6 +27,9 @@ export default async function AboutTheHotelSidePeek({
title={intl.formatMessage({
defaultMessage: "About the hotel",
})}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<section className={styles.wrapper}>
<ContactInformation

View File

@@ -1,8 +1,8 @@
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Preamble from "@scandic-hotels/design-system/Preamble"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import Link from "@/components/TempDesignSystem/Link"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import styles from "./activities.module.css"
@@ -22,6 +22,9 @@ export default async function ActivitiesSidePeek({
title={intl.formatMessage({
defaultMessage: "Activities",
})}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<Preamble>{preamble}</Preamble>
<div className={styles.buttonContainer}>

View File

@@ -1,10 +1,11 @@
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import AccessibilityAccordionItem from "@/components/SidePeeks/AmenitiesSidepeekContent/Accordions/Accessibility"
import BreakfastAccordionItem from "@/components/SidePeeks/AmenitiesSidepeekContent/Accordions/Breakfast"
import CheckInCheckOutAccordionItem from "@/components/SidePeeks/AmenitiesSidepeekContent/Accordions/CheckInCheckOut"
import ParkingAccordionItem from "@/components/SidePeeks/AmenitiesSidepeekContent/Accordions/Parking"
import AdditionalAmenities from "@/components/SidePeeks/AmenitiesSidepeekContent/AdditionalAmenities"
import Accordion from "@/components/TempDesignSystem/Accordion"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { appendSlugToPathname } from "@/utils/appendSlugToPathname"
@@ -32,6 +33,9 @@ export default async function AmenitiesSidePeek({
title={intl.formatMessage({
defaultMessage: "Amenities",
})}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<Accordion>
<ParkingAccordionItem

View File

@@ -1,7 +1,7 @@
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ButtonLink from "@/components/ButtonLink"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { appendSlugToPathname } from "@/utils/appendSlugToPathname"
@@ -26,7 +26,13 @@ export default async function MeetingsAndConferencesSidePeek({
const meetingPageHref = await appendSlugToPathname(meetingPageUrl)
return (
<SidePeek contentKey={SidepeekSlugs.meetings} title={heading}>
<SidePeek
contentKey={SidepeekSlugs.meetings}
title={heading}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div className={styles.wrapper}>
<Typography variant="Title/Subtitle/lg">
<h3>

View File

@@ -1,4 +1,6 @@
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { getIntl } from "@/i18n"
import RestaurantBarItem from "./RestaurantBarItem"
@@ -7,12 +9,20 @@ import styles from "./restaurantBar.module.css"
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
import type { RestaurantBarSidePeekProps } from "@/types/components/hotelPage/sidepeek/restaurantBar"
export default function RestaurantBarSidePeek({
export default async function RestaurantBarSidePeek({
restaurants,
heading,
}: RestaurantBarSidePeekProps) {
const intl = await getIntl()
return (
<SidePeek contentKey={SidepeekSlugs.restaurant} title={heading}>
<SidePeek
contentKey={SidepeekSlugs.restaurant}
title={heading}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div className={styles.content}>
{restaurants.map((restaurant) => (
<div key={restaurant.id} className={styles.item}>

View File

@@ -3,10 +3,10 @@ import Link from "next/link"
import { selectRateWithParams } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { dt } from "@scandic-hotels/common/dt"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ImageGallery from "@/components/ImageGallery"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
@@ -43,7 +43,13 @@ export default async function RoomSidePeek({
const selectRateURL = selectRateWithParams(lang, hotelId, fromdate, todate)
return (
<SidePeek contentKey={`room-${getRoomNameAsParam(name)}`} title={name}>
<SidePeek
contentKey={`room-${getRoomNameAsParam(name)}`}
title={name}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div className={styles.content}>
<div className={styles.innerContent}>
<Typography variant="Body/Paragraph/mdRegular">

View File

@@ -1,7 +1,8 @@
import { notFound } from "next/navigation"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import Image from "@/components/Image"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
@@ -45,6 +46,9 @@ export default async function TripAdvisorSidePeek({
title={intl.formatMessage({
defaultMessage: "Ratings & reviews",
})}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<section className={styles.container}>
{hotelHasAwards ? (

View File

@@ -1,7 +1,8 @@
import { cx } from "class-variance-authority"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import ButtonLink from "@/components/ButtonLink"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { appendSlugToPathname } from "@/utils/appendSlugToPathname"
@@ -24,7 +25,13 @@ export default async function WellnessAndExerciseSidePeek({
)
return (
<SidePeek contentKey={SidepeekSlugs.wellness} title={heading}>
<SidePeek
contentKey={SidepeekSlugs.wellness}
title={heading}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div
className={cx(styles.wrapper, {
[styles.hasSpaPage]: spaPage,

View File

@@ -1,5 +1,8 @@
"use client"
import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import SidePeekProvider from "@scandic-hotels/design-system/SidePeek/SidePeekProvider"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
interface SidePeeksProps extends React.PropsWithChildren {
@@ -7,9 +10,27 @@ interface SidePeeksProps extends React.PropsWithChildren {
}
export default function SidePeeks({ hotelId, children }: SidePeeksProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
function handleOpen(sidePeek: string) {
trackOpenSidePeekEvent(sidePeek, hotelId)
}
return <SidePeekProvider onOpen={handleOpen}>{children}</SidePeekProvider>
function handleClose() {
const nextSearchParams = new URLSearchParams(searchParams.toString())
nextSearchParams.delete("s")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
}
return (
<SidePeekProvider
onOpen={handleOpen}
onClose={handleClose}
searchParams={searchParams}
>
{children}
</SidePeekProvider>
)
}

View File

@@ -2,13 +2,13 @@
import { useIntl } from "react-intl"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ButtonLink from "@/components/ButtonLink"
import Contact from "@/components/HotelReservation/Contact"
import AdditionalAmenities from "@/components/SidePeeks/AmenitiesSidepeekContent/AdditionalAmenities"
import Accordion from "@/components/TempDesignSystem/Accordion"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import AccessibilityAccordionItem from "../AmenitiesSidepeekContent/Accordions/Accessibility"
import BreakfastAccordionItem from "../AmenitiesSidepeekContent/Accordions/Breakfast"
@@ -34,6 +34,9 @@ export default function HotelSidePeek({
title={hotel.name}
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
handleClose={close}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div className={styles.content}>
<Typography variant="Title/Subtitle/lg">

View File

@@ -1,4 +1,6 @@
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { useIntl } from "react-intl"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { RoomSidePeekContent } from "./RoomSidePeekContent"
@@ -10,11 +12,16 @@ export default function RoomSidePeek({
activeSidePeek,
close,
}: RoomSidePeekProps) {
const intl = useIntl()
return (
<SidePeek
title={room.name}
isOpen={activeSidePeek === SidePeekEnum.roomDetails}
handleClose={close}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<RoomSidePeekContent room={room} />
</SidePeek>

View File

@@ -1,51 +0,0 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { createContext, useEffect, useState } from "react"
interface SidepeekProviderProps extends React.PropsWithChildren {
onOpen?: (sidePeek: string) => void
}
interface ISidePeekContext {
handleClose: (isOpen: boolean) => void
activeSidePeek: string | null
}
export const SidePeekContext = createContext<ISidePeekContext | null>(null)
export default function SidePeekProvider({
children,
onOpen,
}: SidepeekProviderProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [activeSidePeek, setActiveSidePeek] = useState<string | null>(null)
useEffect(() => {
const sidePeekParam = searchParams.get("s")
if (sidePeekParam !== activeSidePeek) {
setActiveSidePeek(sidePeekParam)
}
}, [searchParams, activeSidePeek])
useEffect(() => {
if (activeSidePeek && onOpen) {
onOpen(activeSidePeek)
}
}, [activeSidePeek, onOpen])
function handleClose(isOpen: boolean) {
if (!isOpen) {
const nextSearchParams = new URLSearchParams(searchParams.toString())
nextSearchParams.delete("s")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
setActiveSidePeek(null)
}
}
return (
<SidePeekContext.Provider value={{ handleClose, activeSidePeek }}>
{children}
</SidePeekContext.Provider>
)
}

View File

@@ -1,14 +1,14 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import JsonToHtml from "@/components/JsonToHtml"
import SidePeek from "../../SidePeek"
import styles from "./sidepeek.module.css"
import type { AlertSidepeekProps } from "./sidepeek"
@@ -17,6 +17,7 @@ export default function AlertSidepeek({
ctaText,
sidePeekContent,
}: AlertSidepeekProps) {
const intl = useIntl()
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content } = sidePeekContent
@@ -38,6 +39,9 @@ export default function AlertSidepeek({
title={heading}
isOpen={sidePeekIsOpen}
handleClose={() => setSidePeekIsOpen(false)}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<JsonToHtml
nodes={content.json.children}

View File

@@ -1,16 +0,0 @@
import type { SidePeekProps } from "./sidePeek"
// Sidepeeks generally have important content that should be indexed by search engines.
// The content is hidden behind a modal, but it is still important for SEO.
// This component is used to provide SEO information for the sidepeek content.
export default function SidePeekSEO({
title,
children,
}: React.PropsWithChildren<Pick<SidePeekProps, "title">>) {
return (
<div className="sr-only">
<h2>{title}</h2>
{children}
</div>
)
}

View File

@@ -1,81 +0,0 @@
"use client"
import { useContext, useRef } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SidePeekContext } from "@/components/SidePeeks/SidePeekProvider"
import SidePeekSEO from "./SidePeekSEO"
import styles from "./sidePeek.module.css"
import type { SidePeekProps } from "./sidePeek"
export default function SidePeek({
children,
title,
contentKey,
handleClose,
isOpen,
openInRoot = false,
}: React.PropsWithChildren<SidePeekProps>) {
const intl = useIntl()
const rootDiv = useRef<HTMLDivElement>(null)
const context = useContext(SidePeekContext)
function onClose() {
const closeHandler = handleClose || context?.handleClose
closeHandler && closeHandler(false)
}
return (
<>
<div ref={openInRoot ? null : rootDiv}>
<ModalOverlay
UNSTABLE_portalContainer={rootDiv.current || undefined}
className={styles.overlay}
isOpen={
isOpen || (!!contentKey && contentKey === context?.activeSidePeek)
}
onOpenChange={onClose}
isDismissable
>
<Modal className={styles.modal}>
<Dialog className={styles.dialog} aria-label={title}>
<aside className={styles.sidePeek}>
<header className={styles.header}>
{title ? (
<Typography variant="Title/md">
<h2 className={styles.heading}>{title}</h2>
</Typography>
) : null}
<Button
aria-label={intl.formatMessage({
defaultMessage: "Close",
})}
className={styles.closeButton}
intent="text"
onPress={onClose}
>
<MaterialIcon
icon="close"
color="Icon/Interactive/Default"
/>
</Button>
</header>
<div className={styles.sidePeekContent}>{children}</div>
</aside>
</Dialog>
</Modal>
</ModalOverlay>
</div>
<SidePeekSEO title={title}>{children}</SidePeekSEO>
</>
)
}

View File

@@ -1,91 +0,0 @@
.modal {
--sidepeek-desktop-width: 560px;
}
@keyframes slide-in {
from {
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
right: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: var(--sidepeek-z-index);
background-color: var(--UI-Opacity-Almost-Black-30);
}
.modal {
position: fixed;
top: 0;
right: auto;
bottom: 0;
width: 100%;
height: 100vh;
background-color: var(--Background-Primary);
z-index: var(--sidepeek-z-index);
outline: none;
}
.modal[data-entering] {
animation: slide-in 250ms;
}
.modal[data-exiting] {
animation: slide-in 250ms reverse;
}
.dialog {
height: 100%;
outline: none;
}
.sidePeek {
position: relative;
display: grid;
grid-template-rows: min-content auto;
height: 100dvh;
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid var(--Base-Border-Subtle);
align-items: start;
padding: var(--Spacing-x4);
}
.header:has(> h2) {
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
hyphens: auto;
}
.sidePeekContent {
padding: var(--Spacing-x4);
overflow-y: auto;
}
@media screen and (min-width: 1367px) {
.modal {
top: 0;
right: 0px;
width: var(--sidepeek-desktop-width);
height: 100vh;
}
}

View File

@@ -1,7 +0,0 @@
export interface SidePeekProps {
contentKey?: string
title: string
isOpen?: boolean
openInRoot?: boolean
handleClose?: (isOpen: boolean) => void
}

View File

@@ -1,15 +1,15 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import ButtonLink from "@/components/ButtonLink"
import JsonToHtml from "@/components/JsonToHtml"
import SidePeek from "../../SidePeek"
import styles from "./sidepeek.module.css"
import type { TeaserCardSidepeekProps } from "@/types/components/teaserCard"
@@ -18,6 +18,7 @@ export default function TeaserCardSidepeek({
button,
sidePeekContent,
}: TeaserCardSidepeekProps) {
const intl = useIntl()
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content, primary_button, secondary_button } = sidePeekContent
@@ -38,6 +39,9 @@ export default function TeaserCardSidepeek({
isOpen={sidePeekIsOpen}
handleClose={() => setSidePeekIsOpen(false)}
openInRoot
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
{content ? (
<JsonToHtml