Merged in fix/SW-2118-breadcrumbs (pull request #1721)

fix(SW-2118): changed variants for breadcrumbs to handle different background-colors and widths

* fix(SW-2118): changed variants for breadcrumbs to handle different background-colors and widths


Approved-by: Christian Andolf
Approved-by: Linus Flood
This commit is contained in:
Erik Tiekstra
2025-04-07 14:02:39 +00:00
committed by Linus Flood
parent a9c6901752
commit 85a90baa12
16 changed files with 141 additions and 105 deletions

View File

@@ -4,12 +4,11 @@ import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default function AllBreadcrumbs({}: PageArgs<LangParams>) { export default function AllBreadcrumbs({}: PageArgs<LangParams>) {
return ( return (
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant={PageContentTypeEnum.accountPage} /> <Breadcrumbs />
</Suspense> </Suspense>
) )
} }

View File

@@ -5,14 +5,31 @@ import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/Bread
import type { ContentTypeParams, LangParams, PageArgs } from "@/types/params" import type { ContentTypeParams, LangParams, PageArgs } from "@/types/params"
import { PageContentTypeEnum } from "@/types/requests/contentType" import { PageContentTypeEnum } from "@/types/requests/contentType"
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
// This is a temporary solution to avoid showing breadcrumbs on certain content types.
// This should be refactored in the future to handle content types differently, similar to `destination_overview_page`.
const IGNORED_CONTENT_TYPES = [ const IGNORED_CONTENT_TYPES = [
PageContentTypeEnum.hotelPage, PageContentTypeEnum.hotelPage,
PageContentTypeEnum.contentPage, PageContentTypeEnum.contentPage,
PageContentTypeEnum.destinationCityPage, PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage, PageContentTypeEnum.destinationCountryPage,
PageContentTypeEnum.destinationOverviewPage,
PageContentTypeEnum.startPage,
] ]
// This function is temporary until the content types are refactored and handled differently, similar to `destination_overview_page`.
function getBreadcrumbsVariantsByContentType(
contentType: PageContentTypeEnum
): Pick<BreadcrumbsProps, "color" | "size"> | null {
switch (contentType) {
case PageContentTypeEnum.collectionPage:
return { color: "Surface/Secondary/Default", size: "contentWidth" }
default:
return null
}
}
export default function PageBreadcrumbs({ export default function PageBreadcrumbs({
params, params,
}: PageArgs<LangParams & ContentTypeParams>) { }: PageArgs<LangParams & ContentTypeParams>) {
@@ -20,9 +37,11 @@ export default function PageBreadcrumbs({
return null return null
} }
const variants = getBreadcrumbsVariantsByContentType(params.contentType)
return ( return (
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton {...variants} />}>
<Breadcrumbs variant={params.contentType} /> <Breadcrumbs {...variants} />
</Suspense> </Suspense>
) )
} }

View File

@@ -5,11 +5,15 @@ import { generateBreadcrumbsSchema } from "@/utils/jsonSchemas"
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs" import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
interface Props extends Pick<BreadcrumbsProps, "variant"> { interface Props extends Pick<BreadcrumbsProps, "color" | "size"> {
subpageTitle?: string subpageTitle?: string
} }
export default async function Breadcrumbs({ variant, subpageTitle }: Props) { export default async function Breadcrumbs({
color,
size,
subpageTitle,
}: Props) {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get() const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
if (!breadcrumbs?.length) { if (!breadcrumbs?.length) {
return null return null
@@ -29,7 +33,7 @@ export default async function Breadcrumbs({ variant, subpageTitle }: Props) {
__html: JSON.stringify(jsonSchema.jsonLd), __html: JSON.stringify(jsonSchema.jsonLd),
}} }}
/> />
<BreadcrumbsComp breadcrumbs={breadcrumbs} variant={variant} /> <BreadcrumbsComp breadcrumbs={breadcrumbs} color={color} size={size} />
</> </>
) )
} }

View File

@@ -21,8 +21,6 @@ import DestinationCityPageSkeleton from "./DestinationCityPageSkeleton"
import styles from "./destinationCityPage.module.css" import styles from "./destinationCityPage.module.css"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function DestinationCityPage() { export default async function DestinationCityPage() {
const pageData = await getDestinationCityPage() const pageData = await getDestinationCityPage()
@@ -51,9 +49,7 @@ export default async function DestinationCityPage() {
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
<header className={styles.header}> <header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs <Breadcrumbs />
variant={PageContentTypeEnum.destinationCityPage}
/>
</Suspense> </Suspense>
{images?.length ? ( {images?.length ? (
<TopImages images={images} destinationName={city.name} /> <TopImages images={images} destinationName={city.name} />

View File

@@ -18,8 +18,6 @@ import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
import styles from "./destinationCountryPage.module.css" import styles from "./destinationCountryPage.module.css"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function DestinationCountryPage() { export default async function DestinationCountryPage() {
const pageData = await getDestinationCountryPage() const pageData = await getDestinationCountryPage()
@@ -48,9 +46,7 @@ export default async function DestinationCountryPage() {
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
<header className={styles.header}> <header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs <Breadcrumbs />
variant={PageContentTypeEnum.destinationCityPage}
/>
</Suspense> </Suspense>
{images?.length ? ( {images?.length ? (
<TopImages <TopImages

View File

@@ -43,7 +43,6 @@ import styles from "./hotelPage.module.css"
import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage" import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function HotelPage({ hotelId }: HotelPageProps) { export default async function HotelPage({ hotelId }: HotelPageProps) {
const lang = getLang() const lang = getLang()
@@ -147,8 +146,8 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
}} }}
/> />
<header className={styles.header}> <header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton size="headerWidth" />}>
<Breadcrumbs variant={PageContentTypeEnum.hotelPage} /> <Breadcrumbs size="headerWidth" />
</Suspense> </Suspense>
{images?.length ? ( {images?.length ? (
<PreviewImages images={images} hotelName={name} /> <PreviewImages images={images} hotelName={name} />

View File

@@ -4,7 +4,6 @@
.header { .header {
display: grid; display: grid;
gap: var(--Spacing-x4);
background-color: var(--Base-Surface-Subtle-Normal); background-color: var(--Base-Surface-Subtle-Normal);
padding-bottom: var(--Spacing-x4); padding-bottom: var(--Spacing-x4);
} }

View File

@@ -77,11 +77,8 @@ export default async function HotelSubpage({
<StickyMeetingPackageWidget destination={meetingPackageDestination} /> <StickyMeetingPackageWidget destination={meetingPackageDestination} />
)} )}
<div className={styles.header}> <div className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton size="contentWidth" />}>
<Breadcrumbs <Breadcrumbs subpageTitle={pageData.heading} size="contentWidth" />
variant="hotelSubpage"
subpageTitle={pageData.heading}
/>
</Suspense> </Suspense>
{pageData.heroImage ? ( {pageData.heroImage ? (

View File

@@ -8,8 +8,6 @@ import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/Bread
import StaticPage from ".." import StaticPage from ".."
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function ContentPage() { export default async function ContentPage() {
const contentPageRes = await serverClient().contentstack.contentPage.get() const contentPageRes = await serverClient().contentstack.contentPage.get()
@@ -26,8 +24,15 @@ export default async function ContentPage() {
destination={contentPage.meeting_package.location} destination={contentPage.meeting_package.location}
/> />
)} )}
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense
<Breadcrumbs variant={PageContentTypeEnum.contentPage} /> fallback={
<BreadcrumbsSkeleton
color="Surface/Secondary/Default"
size="contentWidth"
/>
}
>
<Breadcrumbs color="Surface/Secondary/Default" size="contentWidth" />
</Suspense> </Suspense>
<StaticPage <StaticPage
content={contentPage} content={contentPage}

View File

@@ -4,7 +4,7 @@
.header { .header {
background-color: var(--Base-Surface-Subtle-Normal); background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) 0; padding-bottom: var(--Spacing-x4);
} }
.headerContent { .headerContent {
@@ -32,7 +32,6 @@
} }
.contentContainer { .contentContainer {
padding-top: var(--Spacing-x4);
width: 100%; width: 100%;
padding: var(--Spacing-x4) var(--Spacing-x2) 0; padding: var(--Spacing-x4) var(--Spacing-x2) 0;
} }
@@ -83,21 +82,9 @@
.headerIntro { .headerIntro {
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
} }
.header {
padding: var(--Spacing-x4) 0;
}
} }
@media (min-width: 1367px) { @media (min-width: 1367px) {
.heroContainer {
padding: var(--Spacing-x4) 0;
}
.contentContainer {
padding: var(--Spacing-x4) 0 0;
}
.content .contentContainer { .content .contentContainer {
grid-template-areas: "main sidebar"; grid-template-areas: "main sidebar";
grid-template-columns: var(--max-width-text-block) 1fr; grid-template-columns: var(--max-width-text-block) 1fr;

View File

@@ -1,9 +1,9 @@
import { cx } from "class-variance-authority" import { cx } from "class-variance-authority"
import Link from "next/link"
import { Breadcrumb as AriaBreadcrumb } from "react-aria-components" import { Breadcrumb as AriaBreadcrumb } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import Link from "@/components/TempDesignSystem/Link"
import styles from "./breadcrumbs.module.css" import styles from "./breadcrumbs.module.css"
@@ -19,9 +19,11 @@ export function Breadcrumb({
<AriaBreadcrumb className={cx(styles.listItem, className)} {...props}> <AriaBreadcrumb className={cx(styles.listItem, className)} {...props}>
{href ? ( {href ? (
<> <>
<Link color="peach80" href={href} variant="breadcrumb"> <Typography variant="Label/xsRegular">
<Link className={styles.link} href={href}>
{children} {children}
</Link> </Link>
</Typography>
<MaterialIcon <MaterialIcon
icon="chevron_right" icon="chevron_right"
size={20} size={20}

View File

@@ -1,17 +1,25 @@
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import SkeletonShimmer from "@/components/SkeletonShimmer" import SkeletonShimmer from "@/components/SkeletonShimmer"
import styles from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css" import styles from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
export default function BreadcrumbsSkeleton() { import { breadcrumbsVariants } from "../variants"
import type { BreadcrumbsProps } from "../breadcrumbs"
export default function BreadcrumbsSkeleton({
color,
size,
}: Pick<BreadcrumbsProps, "color" | "size">) {
const classNames = breadcrumbsVariants({ color, size })
return ( return (
<nav className={styles.breadcrumbs}> <nav className={classNames}>
<ul className={styles.list}> <ul className={styles.list}>
<li className={styles.listItem}> <li className={styles.listItem}>
<MaterialIcon <MaterialIcon
icon="home" icon="home"
size={16} size={20}
color="Icon/Interactive/Secondary" color="Icon/Interactive/Secondary"
/> />
<MaterialIcon <MaterialIcon
@@ -23,9 +31,9 @@ export default function BreadcrumbsSkeleton() {
</li> </li>
<li className={styles.listItem}> <li className={styles.listItem}>
<Footnote color="burgundy" type="bold"> <Typography variant="Label/xsBold">
<SkeletonShimmer width={"12ch"} /> <SkeletonShimmer width="20ch" />
</Footnote> </Typography>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@@ -2,30 +2,33 @@
padding: var(--Spacing-x4) 0 var(--Spacing-x3); padding: var(--Spacing-x4) 0 var(--Spacing-x3);
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
max-width: var(--max-width-page);
} }
.breadcrumbs.fullWidth { .breadcrumbs.transparent {
max-width: var(--max-width-page); background-color: transparent;
}
.breadcrumbs.surfaceSecondaryDefault {
background-color: var(--Surface-Secondary-Default);
}
.breadcrumbs.surfacePrimaryOnSurfaceDefault {
background-color: var(--Surface-Primary-On-Surface-Default);
} }
.breadcrumbs.contentWidth { .breadcrumbs .list {
max-width: var(--max-width-content);
}
.breadcrumbs.contentWidth {
background-color: var(--Base-Surface-Subtle-Normal);
padding-bottom: 0;
}
.breadcrumbs.headerWidth {
max-width: min(var(--max-width-page), calc(100% - var(--max-width-spacing)));
}
.list {
display: flex; display: flex;
gap: var(--Spacing-x-quarter); gap: var(--Spacing-x-quarter);
padding-inline-start: 0; padding-inline-start: 0;
margin: 0 auto;
}
.breadcrumbs.contentWidth .list {
max-width: var(--max-width-content);
}
.breadcrumbs.headerWidth .list {
max-width: min(var(--max-width-page), calc(100% - var(--max-width-spacing)));
}
.breadcrumbs.pageWidth .list {
max-width: var(--max-width-page);
} }
.list .listItem:last-of-type { .list .listItem:last-of-type {
@@ -58,6 +61,16 @@
/* this increases the width of the button for tapping */ /* this increases the width of the button for tapping */
padding: 0 5px; padding: 0 5px;
margin: 0 -5px; margin: 0 -5px;
color: var(--Base-Text-High-contrast);
}
.link {
color: var(--Text-Interactive-Secondary);
}
.link:hover,
.button:hover {
color: var(--Text-Interactive-Hover-Secondary);
} }
.dialog { .dialog {
@@ -76,6 +89,7 @@
display: block; display: block;
border-radius: var(--Corner-radius-Medium); border-radius: var(--Corner-radius-Medium);
padding: var(--Spacing-x1); padding: var(--Spacing-x1);
color: var(--Text-Default);
} }
.dialogLink:focus, .dialogLink:focus,

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { cx } from "class-variance-authority" import { cx } from "class-variance-authority"
import Link from "next/link"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { import {
Breadcrumbs as AriaBreadcrumbs, Breadcrumbs as AriaBreadcrumbs,
@@ -16,9 +17,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { debounce } from "@/utils/debounce" import { debounce } from "@/utils/debounce"
import Link from "../Link"
import { Arrow } from "../Popover/Arrow" import { Arrow } from "../Popover/Arrow"
import Footnote from "../Text/Footnote"
import { Breadcrumb } from "./Breadcrumb" import { Breadcrumb } from "./Breadcrumb"
import { splitBreadcrumbs } from "./utils" import { splitBreadcrumbs } from "./utils"
import { breadcrumbsVariants } from "./variants" import { breadcrumbsVariants } from "./variants"
@@ -29,7 +28,8 @@ import type { BreadcrumbsProps } from "./breadcrumbs"
export default function Breadcrumbs({ export default function Breadcrumbs({
breadcrumbs, breadcrumbs,
variant, color,
size,
}: BreadcrumbsProps) { }: BreadcrumbsProps) {
// using a ref instead to detect when the element is available and forcing a render // using a ref instead to detect when the element is available and forcing a render
const [element, attachRef] = useState<HTMLButtonElement | null>(null) const [element, attachRef] = useState<HTMLButtonElement | null>(null)
@@ -59,7 +59,8 @@ export default function Breadcrumbs({
} }
const classNames = breadcrumbsVariants({ const classNames = breadcrumbsVariants({
variant, color,
size,
}) })
const [homeBreadcrumb, remainingBreadcrumbs, lastBreadcrumb] = const [homeBreadcrumb, remainingBreadcrumbs, lastBreadcrumb] =
@@ -72,9 +73,14 @@ export default function Breadcrumbs({
href={homeBreadcrumb.href} href={homeBreadcrumb.href}
aria-label={homeBreadcrumb.title} aria-label={homeBreadcrumb.title}
> >
<MaterialIcon icon="home" size={16} color="CurrentColor" /> <MaterialIcon
icon="home"
size={20}
color="Icon/Interactive/Secondary"
/>
</Breadcrumb> </Breadcrumb>
{/* These breadcrumbs are visible on mobile only */}
{remainingBreadcrumbs.length >= 3 ? ( {remainingBreadcrumbs.length >= 3 ? (
<> <>
<Breadcrumb <Breadcrumb
@@ -85,20 +91,24 @@ export default function Breadcrumbs({
</Breadcrumb> </Breadcrumb>
<Breadcrumb className={styles.mobile}> <Breadcrumb className={styles.mobile}>
<DialogTrigger> <DialogTrigger>
<Footnote color="burgundy" type="bold" asChild> <Typography variant="Label/xsRegular">
<Button className={styles.button}></Button> <Button className={styles.button}></Button>
</Footnote> </Typography>
<Popover> <Popover>
<Dialog className={styles.dialog}> <Dialog className={styles.dialog}>
{remainingBreadcrumbs.slice(1).map((breadcrumb) => ( {remainingBreadcrumbs.slice(1).map((breadcrumb) => (
<Typography
key={breadcrumb.uid}
variant="Label/xsRegular"
className={styles.dialogItem}
>
<Link <Link
href={breadcrumb.href!} href={breadcrumb.href!}
size="tiny"
key={breadcrumb.uid}
className={styles.dialogLink} className={styles.dialogLink}
> >
{breadcrumb.title} {breadcrumb.title}
</Link> </Link>
</Typography>
))} ))}
</Dialog> </Dialog>
</Popover> </Popover>
@@ -122,6 +132,8 @@ export default function Breadcrumbs({
</Breadcrumb> </Breadcrumb>
)) ))
)} )}
{/* These breadcrumbs are visible on desktop only */}
{remainingBreadcrumbs.map((breadcrumb) => ( {remainingBreadcrumbs.map((breadcrumb) => (
<Breadcrumb <Breadcrumb
key={breadcrumb.uid} key={breadcrumb.uid}
@@ -131,9 +143,11 @@ export default function Breadcrumbs({
{breadcrumb.title} {breadcrumb.title}
</Breadcrumb> </Breadcrumb>
))} ))}
{/* Current page breadcrumb */}
<Breadcrumb> <Breadcrumb>
<DialogTrigger> <DialogTrigger>
<Footnote color="burgundy" type="bold" asChild> <Typography variant="Label/xsBold">
<Button <Button
className={cx(styles.button, styles.tooltipTrigger)} className={cx(styles.button, styles.tooltipTrigger)}
ref={attachRef} ref={attachRef}
@@ -141,7 +155,7 @@ export default function Breadcrumbs({
> >
{lastBreadcrumb.title} {lastBreadcrumb.title}
</Button> </Button>
</Footnote> </Typography>
<Popover placement="bottom" offset={16}> <Popover placement="bottom" offset={16}>
<Dialog className={styles.tooltip}> <Dialog className={styles.tooltip}>
<OverlayArrow> <OverlayArrow>

View File

@@ -1,7 +1,5 @@
import type { Breadcrumb } from "./breadcrumbs" import type { Breadcrumb } from "./breadcrumbs"
export { splitBreadcrumbs }
function splitBreadcrumbs( function splitBreadcrumbs(
breadcrumbs: Breadcrumb[] breadcrumbs: Breadcrumb[]
): [Breadcrumb, Breadcrumb[], Breadcrumb] { ): [Breadcrumb, Breadcrumb[], Breadcrumb] {
@@ -10,3 +8,5 @@ function splitBreadcrumbs(
const last = copy.pop()! const last = copy.pop()!
return [first, copy, last] return [first, copy, last]
} }
export { splitBreadcrumbs }

View File

@@ -2,25 +2,22 @@ import { cva } from "class-variance-authority"
import styles from "./breadcrumbs.module.css" import styles from "./breadcrumbs.module.css"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export const breadcrumbsVariants = cva(styles.breadcrumbs, { export const breadcrumbsVariants = cva(styles.breadcrumbs, {
variants: { variants: {
variant: { color: {
[PageContentTypeEnum.accountPage]: styles.fullWidth, transparent: styles.transparent,
[PageContentTypeEnum.contentPage]: styles.contentWidth, "Surface/Secondary/Default": styles.surfaceSecondaryDefault,
[PageContentTypeEnum.collectionPage]: styles.contentWidth, "Surface/Primary/OnSurface/Default":
[PageContentTypeEnum.destinationOverviewPage]: styles.fullWidth, styles.surfacePrimaryOnSurfaceDefault,
[PageContentTypeEnum.destinationCountryPage]: styles.fullWidth, },
[PageContentTypeEnum.destinationCityPage]: styles.fullWidth, size: {
[PageContentTypeEnum.hotelPage]: styles.headerWidth, pageWidth: styles.pageWidth,
[PageContentTypeEnum.loyaltyPage]: styles.fullWidth, headerWidth: styles.headerWidth,
[PageContentTypeEnum.startPage]: styles.contentWidth, contentWidth: styles.contentWidth,
hotelSubpage: styles.contentWidth,
default: styles.fullWidth,
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", color: "transparent",
size: "pageWidth",
}, },
}) })