diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.test.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.test.tsx index 55c0606839..842d586e2d 100644 --- a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.test.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.test.tsx @@ -101,17 +101,43 @@ const setupApis = ( describe("ChannelPage", () => { it("Displays the channel title, banner, and avatar", async () => { - const { channel } = setupApis() + const { channel } = setupApis({ + search_filter: "offered_by=ocw", + channel_type: "unit", + }) renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) - - const title = await screen.findAllByText(channel.title) - const header = title[0].closest("header") + const findTitle = (titles: HTMLElement[]) => { + return titles[ + titles.findIndex( + (title: HTMLElement) => + title.textContent === channel.title || + title.textContent === channel.configuration.heading, + ) + ] + } + await waitFor(() => { + const titles = screen.getAllByRole("heading") + const title = findTitle(titles) + expect(title).toBeInTheDocument() + }) + const titles = screen.getAllByRole("heading") + const title = findTitle(titles) + expect(title).toBeInTheDocument() + const header = title.closest("header") assertInstanceOf(header, HTMLElement) const images = within(header).getAllByRole("img") as HTMLImageElement[] const headerStyles = getComputedStyle(header) - expect(headerStyles.backgroundImage).toContain( - channel.configuration.banner_background, - ) + if (channel.channel_type !== "unit") { + /* + * unit channels are filtered out from this assertion + * because they wrap the background image in a linear-gradient, + * which causes react testing library to not render the background-image + * property at all + */ + expect(headerStyles.backgroundImage).toContain( + channel.configuration.banner_background, + ) + } expect(images[0].src).toContain(channel.configuration.logo) }) it("Displays a featured carousel if the channel type is 'unit'", async () => { @@ -158,7 +184,7 @@ describe("ChannelPage", () => { "platform=ocw&platform=mitxonline&department=8&department=9", }) renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) - await screen.findByText(channel.title) + await screen.findAllByText(channel.title) const expectedProps = expect.objectContaining({ constantSearchParams: { platform: ["ocw", "mitxonline"], @@ -176,7 +202,7 @@ describe("ChannelPage", () => { const { channel } = setupApis() channel.search_filter = undefined renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) - await screen.findByText(channel.title) + await screen.findAllByText(channel.title) expect(mockedChannelSearch).toHaveBeenCalledTimes(0) }) @@ -185,17 +211,17 @@ describe("ChannelPage", () => { const { channel } = setupApis() channel.search_filter = undefined renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) - await screen.findByText(channel.title) + await screen.findAllByText(channel.title) await waitFor(() => { - expect( - screen.getByText(channel.configuration.sub_heading), - ).toBeInTheDocument() + screen.getAllByText(channel.configuration.sub_heading).forEach((el) => { + expect(el).toBeInTheDocument() + }) }) await waitFor(() => { - expect( - screen.getByText(channel.configuration.heading), - ).toBeInTheDocument() + screen.getAllByText(channel.configuration.heading).forEach((el) => { + expect(el).toBeInTheDocument() + }) }) }) diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx index 602382ce62..953f6f4329 100644 --- a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx @@ -1,12 +1,12 @@ import React from "react" import { useParams } from "react-router" -import ChannelPageSkeleton from "./ChannelPageSkeleton" +import { ChannelPageTemplate } from "./ChannelPageTemplate" import { useChannelDetail } from "api/hooks/channels" import FieldSearch from "./ChannelSearch" -import type { - Facets, - FacetKey, - BooleanFacets, +import { + type Facets, + type FacetKey, + type BooleanFacets, } from "@mitodl/course-search-utils" import { ChannelTypeEnum } from "api/v0" import TestimonialDisplay from "@/page-components/TestimonialDisplay/TestimonialDisplay" @@ -41,7 +41,7 @@ const ChannelPage: React.FC = () => { return ( name && channelType && ( - +

{channelQuery.data?.public_description}

{channelType === "unit" ? ( @@ -52,7 +52,7 @@ const ChannelPage: React.FC = () => { channelType={channelType} /> )} -
+ ) ) } diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelPageSkeleton.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelPageSkeleton.tsx deleted file mode 100644 index 2971c50264..0000000000 --- a/frontends/mit-open/src/pages/ChannelPage/ChannelPageSkeleton.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React, { useMemo } from "react" -import { Link } from "react-router-dom" -import * as routes from "../../common/urls" -import { - BannerPage, - styled, - Container, - Typography, - Box, - Breadcrumbs, -} from "ol-components" -import { MetaTags } from "ol-utilities" -import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle" -import { ChannelDetails } from "@/page-components/ChannelDetails/ChannelDetails" -import { useChannelDetail } from "api/hooks/channels" -import ChannelMenu from "@/components/ChannelMenu/ChannelMenu" -import ChannelAvatar from "@/components/ChannelAvatar/ChannelAvatar" -import ResourceCarousel, { - ResourceCarouselProps, -} from "@/page-components/ResourceCarousel/ResourceCarousel" -import { SourceTypeEnum } from "api" -import { getSearchParamMap } from "@/common/utils" -import { DEPARTMENTS, HOME, TOPICS, UNITS } from "../../common/urls" - -export const ChannelTitleRow = styled.div` - display: flex; - flex-direction: row; - align-items: center; - - h1 a { - &:hover { - text-decoration: none; - } - } -` - -const FeaturedCoursesCarousel = styled(ResourceCarousel)(({ theme }) => ({ - margin: "80px 0", - [theme.breakpoints.down("sm")]: { - marginTop: "32px", - marginBottom: "32px", - }, -})) -export const ChannelControls = styled.div` - position: relative; - min-height: 38px; - display: flex; -` - -interface ChannelSkeletonProps { - children: React.ReactNode - channelType: string - name: string -} -const NAV_PATH: { [key: string]: { href: string; label: string } } = { - topic: { - href: TOPICS, - label: "Browse by Topic", - }, - department: { - href: DEPARTMENTS, - label: "Browse by Academic Department", - }, - unit: { - href: UNITS, - label: "MIT Units", - }, - pathway: { - href: "", - label: "Pathways", - }, -} - -/** - * Common structure for channel-oriented pages. - * - * Renders the channel title and avatar in a banner. - */ -const ChannelSkeletonProps: React.FC = ({ - children, - channelType, - name, -}) => { - const channel = useChannelDetail(String(channelType), String(name)) - const urlParams = new URLSearchParams(channel.data?.search_filter) - const displayConfiguration = channel.data?.configuration - - const urlParamMap: Record = useMemo(() => { - const urlParams = new URLSearchParams(channel.data?.search_filter) - return getSearchParamMap(urlParams) - }, [channel]) - - const FEATURED_RESOURCES_CAROUSEL: ResourceCarouselProps["config"] = [ - { - cardProps: { size: "medium" }, - data: { - type: "lr_featured", - params: { limit: 12, ...urlParamMap }, - }, - label: undefined, - }, - ] - - return ( - <> - - - - - {channel.data && ( - - - ({ - flexGrow: 1, - flexShrink: 0, - order: 1, - py: "24px", - - [theme.breakpoints.down("md")]: { - py: 0, - pb: "8px", - }, - [theme.breakpoints.down("sm")]: { - width: "100%", - }, - })} - > - {displayConfiguration?.logo ? ( - - ) : ( - - - {channel.data.title} - - - )} - - {displayConfiguration.heading ? ( - - - {displayConfiguration.heading} - - - ) : ( - <> - )} - - - {displayConfiguration.sub_heading} - - - {channelType === "unit" ? ( - - - {channel.data?.search_filter ? ( - - ) : null} - {channel.data?.is_moderator ? ( - - ) : null} - - - ) : null} - - {channelType === "unit" ? ( - - - - ) : ( - - - {channel.data?.search_filter ? ( - - ) : null} - {channel.data?.is_moderator ? ( - - ) : null} - - - )} - - )} - - - } - > - {channelType === "unit" ? ( - - - - ) : null} - {children} - - - ) -} - -export default ChannelSkeletonProps diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelPageTemplate.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelPageTemplate.tsx new file mode 100644 index 0000000000..5d9a10eedb --- /dev/null +++ b/frontends/mit-open/src/pages/ChannelPage/ChannelPageTemplate.tsx @@ -0,0 +1,95 @@ +import React from "react" +import UnitChannelSkeleton from "./UnitChannelTemplate" +import DefaultChannelSkeleton from "./DefaultChannelTemplate" +import { ChannelTypeEnum } from "api/v0" +import { + DEPARTMENTS as DEPARTMENTS_URL, + TOPICS as TOPICS_URL, + UNITS as UNITS_URL, +} from "@/common/urls" +import { styled } from "ol-components" + +const TOPICS_LABEL = "Browse by Topic" +const DEPARTMENTS_LABEL = "Browse by Academic Department" +const UNITS_LABEL = "MIT Units" +const PATHWAYS_LABEL = "Pathways" + +const CHANNEL_TYPE_BREADCRUMB_TARGETS: { + [key: string]: { href: string; label: string } +} = { + topic: { + href: TOPICS_URL, + label: TOPICS_LABEL, + }, + department: { + href: DEPARTMENTS_URL, + label: DEPARTMENTS_LABEL, + }, + unit: { + href: UNITS_URL, + label: UNITS_LABEL, + }, + pathway: { + href: "", + label: PATHWAYS_LABEL, + }, +} + +const ChannelTitleRow = styled.div({ + display: "flex", + flexDirection: "row", + alignItems: "center", + + h1: { + a: { + "&:hover": { + textDecoration: "none", + }, + }, + }, +}) + +const ChannelControls = styled.div({ + position: "relative", + minHeight: "38px", + display: "flex", +}) + +interface ChannelSkeletonProps { + children: React.ReactNode + channelType: string + name: string +} + +/** + * Common structure for channel-oriented pages. + * + * Renders the channel title and avatar in a banner. + */ +const ChannelPageTemplate: React.FC = ({ + children, + channelType, + name, +}) => { + const ChannelTemplate = + channelType === ChannelTypeEnum.Unit + ? UnitChannelSkeleton + : DefaultChannelSkeleton + + return ( + + {children} + + ) +} + +export { + ChannelPageTemplate, + ChannelTitleRow, + ChannelControls, + CHANNEL_TYPE_BREADCRUMB_TARGETS, + UNITS_LABEL, + TOPICS_LABEL, + DEPARTMENTS_LABEL, + PATHWAYS_LABEL, +} diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelSearch.test.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelSearch.test.tsx index 2556c72227..80c8fc2f00 100644 --- a/frontends/mit-open/src/pages/ChannelPage/ChannelSearch.test.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/ChannelSearch.test.tsx @@ -116,7 +116,7 @@ describe("ChannelSearch", () => { }) setMockResponse.get(urls.userMe.get(), {}) renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) - await screen.findByText(channel.title) + await screen.findAllByText(channel.title) const tabpanel = await screen.findByRole("tabpanel") for (const resource of resources) { await within(tabpanel).findByText(resource.title) diff --git a/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx b/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx new file mode 100644 index 0000000000..a5614219dc --- /dev/null +++ b/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx @@ -0,0 +1,165 @@ +import React from "react" +import { + styled, + Container, + Typography, + Breadcrumbs, + Banner, +} from "ol-components" +import { MetaTags } from "ol-utilities" +import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle" +import { useChannelDetail } from "api/hooks/channels" +import ChannelMenu from "@/components/ChannelMenu/ChannelMenu" +import ChannelAvatar from "@/components/ChannelAvatar/ChannelAvatar" +import { SourceTypeEnum } from "api" +import { HOME as HOME_URL } from "../../common/urls" +import { + CHANNEL_TYPE_BREADCRUMB_TARGETS, + ChannelControls, + ChannelTitleRow, +} from "./ChannelPageTemplate" + +const HeadingTextContainer = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "row", + alignItems: "center", + flexGrow: 0, + flexShrink: 0, + order: 2, + my: 1, + [theme.breakpoints.down("sm")]: { + width: "100%", + }, + [theme.breakpoints.up("md")]: { + width: "80%", + }, +})) + +const ChannelControlsContainer = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "row", + alignItems: "end", + flexGrow: 0, + flexShrink: 0, + order: 2, + [theme.breakpoints.down("xs")]: { + width: "100%", + }, + [theme.breakpoints.down("sm")]: { + mt: "8px", + mb: "48px", + }, + [theme.breakpoints.up("md")]: { + mt: "0px", + mb: "48px", + width: "15%", + }, +})) + +interface DefaultChannelTemplateProps { + children: React.ReactNode + channelType: string + name: string +} + +/** + * Common structure for channel-oriented pages. + * + * Renders the channel title and avatar in a banner. + */ +const DefaultChannelTemplate: React.FC = ({ + children, + channelType, + name, +}) => { + const channel = useChannelDetail(String(channelType), String(name)) + const urlParams = new URLSearchParams(channel.data?.search_filter) + const displayConfiguration = channel.data?.configuration + + return ( + <> + + + } + avatar={ + displayConfiguration?.logo && + channel.data && ( + + ) + } + header={channel.data?.title} + subheader={displayConfiguration?.heading} + extraHeader={displayConfiguration?.sub_heading} + backgroundUrl={ + displayConfiguration?.banner_background ?? + "/static/images/background_steps.jpeg" + } + extraRight={ + + + {channel.data?.search_filter ? ( + + ) : null} + {channel.data?.is_moderator ? ( + + ) : null} + + + } + /> + + + {displayConfiguration?.heading ? ( + + + {displayConfiguration.heading} + + + ) : ( + <> + )} + {displayConfiguration?.sub_heading ? ( + + + {displayConfiguration.sub_heading} + + + ) : ( + <> + )} + + + {children} + + ) +} + +export default DefaultChannelTemplate diff --git a/frontends/mit-open/src/pages/ChannelPage/EditChannelPage.tsx b/frontends/mit-open/src/pages/ChannelPage/EditChannelPage.tsx index d5581ee8bc..82eae36dfd 100644 --- a/frontends/mit-open/src/pages/ChannelPage/EditChannelPage.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/EditChannelPage.tsx @@ -8,7 +8,7 @@ import { MetaTags } from "ol-utilities" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" import { useChannelDetail } from "api/hooks/channels" import EditChannelAppearanceForm from "./EditChannelAppearanceForm" -import ChannelPageSkeleton from "./ChannelPageSkeleton" +import { ChannelPageTemplate } from "./ChannelPageTemplate" type RouteParams = { channelType: string name: string @@ -34,7 +34,7 @@ const EditChannelPage: React.FC = () => { ) return channel.data ? ( - @@ -89,7 +89,7 @@ const EditChannelPage: React.FC = () => { )} - + ) : null } diff --git a/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx b/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx new file mode 100644 index 0000000000..7e200a0dd9 --- /dev/null +++ b/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx @@ -0,0 +1,184 @@ +import React, { useMemo } from "react" +import { styled, Container, Box, Breadcrumbs, Banner } from "ol-components" +import { MetaTags } from "ol-utilities" +import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle" +import { ChannelDetails } from "@/page-components/ChannelDetails/ChannelDetails" +import { useChannelDetail } from "api/hooks/channels" +import ChannelMenu from "@/components/ChannelMenu/ChannelMenu" +import ChannelAvatar from "@/components/ChannelAvatar/ChannelAvatar" +import ResourceCarousel, { + ResourceCarouselProps, +} from "@/page-components/ResourceCarousel/ResourceCarousel" +import { SourceTypeEnum } from "api" +import { getSearchParamMap } from "@/common/utils" +import { HOME as HOME_URL, UNITS as UNITS_URL } from "../../common/urls" +import { ChannelTypeEnum } from "api/v0" +import { ChannelControls, UNITS_LABEL } from "./ChannelPageTemplate" + +const FeaturedCoursesCarousel = styled(ResourceCarousel)(({ theme }) => ({ + margin: "80px 0", + [theme.breakpoints.down("sm")]: { + marginTop: "32px", + marginBottom: "32px", + }, +})) + +interface UnitChannelTemplateProps { + children: React.ReactNode + name: string +} + +/** + * Common structure for channel-oriented pages. + * + * Renders the channel title and avatar in a banner. + */ +const UnitChannelTemplate: React.FC = ({ + children, + name, +}) => { + const channel = useChannelDetail(ChannelTypeEnum.Unit, String(name)) + const urlParams = new URLSearchParams(channel.data?.search_filter) + const displayConfiguration = channel.data?.configuration + + const urlParamMap: Record = useMemo(() => { + const urlParams = new URLSearchParams(channel.data?.search_filter) + return getSearchParamMap(urlParams) + }, [channel]) + + const FEATURED_RESOURCES_CAROUSEL: ResourceCarouselProps["config"] = [ + { + cardProps: { size: "medium" }, + data: { + type: "lr_featured", + params: { limit: 12, ...urlParamMap }, + }, + label: undefined, + }, + ] + + const headerStyles = { + width: { md: "80%", sm: "100%" }, + my: 1, + } + + return ( + <> + + + } + avatar={ + displayConfiguration?.logo && channel.data ? ( + ({ + flexGrow: 1, + flexShrink: 0, + order: 1, + py: "24px", + + [theme.breakpoints.down("md")]: { + py: 0, + pb: "8px", + }, + [theme.breakpoints.down("sm")]: { + pt: "32px", + width: "100%", + }, + })} + > + + + ) : null + } + header={displayConfiguration?.heading} + headerTypography={{ xs: "h5", md: "h4" }} + headerStyles={headerStyles} + subheader={displayConfiguration?.sub_heading} + subheaderStyles={headerStyles} + subheaderTypography={{ xs: "body2", md: "body1" }} + extraHeader={ + + + {channel.data?.search_filter ? ( + + ) : null} + {channel.data?.is_moderator ? ( + + ) : null} + + + } + extraRight={ + channel.data && ( + + + + ) + } + /> + + + {children} + + + ) +} + +export default UnitChannelTemplate diff --git a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx index f40a37f645..cbdbd7d653 100644 --- a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx +++ b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx @@ -217,8 +217,8 @@ const DepartmentListingPage: React.FC = () => { { current="Topics" /> } - title="Browse by Topic" - description="" + header="Browse by Topic" + subheader="" backgroundUrl={TOPICS_BANNER_IMAGE} /> diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx index b6b0e556ed..ff376a8258 100644 --- a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx @@ -414,8 +414,8 @@ const UnitsListingPage: React.FC = () => { current="MIT Units" /> } - title="Academic & Professional Learning" - description="Non-degree learning resources tailored to the needs of students and working professionals." + header="Academic & Professional Learning" + subheader="Non-degree learning resources tailored to the needs of students and working professionals." backgroundUrl={UNITS_BANNER_IMAGE} /> diff --git a/frontends/ol-components/src/components/Banner/Banner.stories.tsx b/frontends/ol-components/src/components/Banner/Banner.stories.tsx index f145eae1be..59e2922190 100644 --- a/frontends/ol-components/src/components/Banner/Banner.stories.tsx +++ b/frontends/ol-components/src/components/Banner/Banner.stories.tsx @@ -1,21 +1,94 @@ +import React from "react" import type { Meta, StoryObj } from "@storybook/react" +import { withRouter } from "storybook-addon-react-router-v6" import { Banner } from "./Banner" +import { Breadcrumbs } from "../Breadcrumbs/Breadcrumbs" +import { Button } from "../Button/Button" +import Typography from "@mui/material/Typography" const lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo" -const meta: Meta = { +const meta: Meta = { title: "smoot-design/Banner", component: Banner, args: { backgroundUrl: "https://images.pexels.com/photos/1851188/pexels-photo-1851188.jpeg?auto=compress&cs=tinysrgb&w=800", - title: "Banner Title", - description: lipsum, - navText: "Home / Nav / Text", + navText: ( + + ), + header: "Banner Title", + subheader: lipsum, }, } export default meta type Story = StoryObj -export const story: Story = {} + +export const basicBanner: Story = { + decorators: [withRouter], + render: (args) => , +} + +export const logoBanner: Story = { + decorators: [withRouter], + render: (args) => { + return ( + + } + {...args} + /> + ) + }, +} + +export const logoBannerWithExtras: Story = { + decorators: [withRouter], + render: (args) => { + return ( + + } + extraHeader={ + + } + extraRight={ +
+ Extra Content +
+ +
+
+ +
+
+ } + {...args} + /> + ) + }, +} diff --git a/frontends/ol-components/src/components/Banner/Banner.tsx b/frontends/ol-components/src/components/Banner/Banner.tsx index 29a0556378..06668ab6ce 100644 --- a/frontends/ol-components/src/components/Banner/Banner.tsx +++ b/frontends/ol-components/src/components/Banner/Banner.tsx @@ -2,45 +2,84 @@ import React from "react" import styled from "@emotion/styled" import Typography from "@mui/material/Typography" import Container from "@mui/material/Container" +import { ResponsiveStyleValue, SxProps } from "@mui/system" +import { Theme } from "../ThemeProvider/ThemeProvider" -const Description = styled(Typography)(({ theme }) => ({ +const SubHeader = styled(Typography)({ maxWidth: "700px", marginTop: "8px", - [theme.breakpoints.down("sm")]: { - marginTop: "16px", - }, -})) +}) type BannerWrapperProps = { backgroundUrl: string + backgroundSize?: string + backgroundDim?: number + containerPadding?: string } /** * This is a full-width banner component that takes a background image URL. */ -const BannerWrapper = styled.div( - ({ theme, backgroundUrl }) => ({ - backgroundImage: `url(${backgroundUrl})`, - backgroundSize: "cover", +const BannerWrapper = styled.header( + ({ + theme, + backgroundUrl, + backgroundSize = "cover", + backgroundDim = 0, + containerPadding = "48px 0 48px 0", + }) => ({ + backgroundAttachment: "fixed", + backgroundImage: backgroundDim + ? `linear-gradient(rgba(0 0 0 / ${backgroundDim}%), rgba(0 0 0 / ${backgroundDim}%)), url('${backgroundUrl}')` + : `url(${backgroundUrl})`, + backgroundSize: backgroundSize, + backgroundRepeat: "no-repeat", color: theme.custom.colors.white, - paddingTop: "48px", - paddingBottom: "48px", + padding: containerPadding, [theme.breakpoints.down("sm")]: { - paddingTop: "32px", - paddingBottom: "32px", + padding: "32px 0 32px 0", }, }), ) -const NavText = styled(Typography)(({ theme }) => ({ - color: theme.custom.colors.lightGray2, - marginBottom: "4px", +const InnerContainer = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "row", + alignItems: "flex-start", + justifyContent: "space-between", + [theme.breakpoints.down("md")]: { + flexDirection: "column", + }, +})) + +const HeaderContainer = styled.div({ + display: "flex", + flexDirection: "column", +}) + +const RightContainer = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "row", + [theme.breakpoints.down("md")]: { + width: "100%", + }, })) type BannerProps = BannerWrapperProps & { - description: React.ReactNode - title: React.ReactNode + backgroundUrl: string + backgroundSize?: string + backgroundDim?: number + containerPadding?: string navText: React.ReactNode + avatar?: React.ReactNode + header: React.ReactNode + headerTypography?: ResponsiveStyleValue + headerStyles?: SxProps + subheader?: React.ReactNode + subheaderTypography?: ResponsiveStyleValue + subheaderStyles?: SxProps + extraHeader?: React.ReactNode + extraRight?: React.ReactNode } /** @@ -49,20 +88,52 @@ type BannerProps = BannerWrapperProps & { */ const Banner = ({ backgroundUrl, - description, - title, + backgroundSize = "cover", + backgroundDim = 0, + containerPadding = "48px 0 48px 0", navText, + avatar, + header, + headerTypography, + headerStyles, + subheader, + subheaderTypography, + subheaderStyles, + extraHeader, + extraRight, }: BannerProps) => { + const defaultHeaderTypography = { xs: "h2", md: "h1" } + const defaultSubHeaderTypography = { xs: "body2", md: "body1" } return ( - + - {navText} - - {title} - - - {description} - + {navText} + + + {avatar ?
{avatar}
: null} + + {header} + + + {subheader} + +
{extraHeader}
+
+ {extraRight} +
) diff --git a/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.test.tsx index f2cbfcc1f6..44b8c28159 100644 --- a/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.test.tsx +++ b/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.test.tsx @@ -46,7 +46,7 @@ describe("Breadcrumbs", () => { ) expect(screen.getAllByRole("link")).toHaveLength(totalAncestors) expectedLabels.forEach((label, index) => { - const link = screen.getByText(label) + const link = screen.getByText(label).parentElement expect(link).toBeInTheDocument() expect(link).toHaveAttribute("href", expectedHrefs[index]) }) diff --git a/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.tsx b/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.tsx index 8389b5fb61..60c7f1ec7b 100644 --- a/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/frontends/ol-components/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -8,11 +8,26 @@ const BreadcrumbsContainer = styled.span({ display: "inline-flex", paddingBottom: "16px", alignItems: "flex-start", + overflow: "hidden", + width: "100%", }) const Breadcrumb = styled.span({ display: "flex", alignItems: "center", + overflow: "hidden", +}) + +const BreadcrumbLink = styled(Link)({ + overflow: "hidden", + textOverflow: "ellipsis", +}) + +const BreadcrumbText = styled.span({ + textWrap: "nowrap", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", }) const Separator = styled(RiArrowRightSLine)({ @@ -28,7 +43,7 @@ const DarkSeparator = styled(Separator)({ color: theme.custom.colors.silverGray, }) -const Current = styled.span({ +const Current = styled(BreadcrumbText)({ ...theme.typography.body3, }) @@ -57,14 +72,14 @@ const Breadcrumbs: React.FC = (props) => { const isLast = index === ancestors.length return ( - - {ancestor.label} - + {ancestor.label} + {!isLast && <_Separator data-testid="breadcrumb-separator" />} )