Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): adds HeaderContainer component #607

Merged
merged 10 commits into from
Nov 25, 2024
5 changes: 5 additions & 0 deletions .changeset/giant-news-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudoperators/juno-ui-components": patch
---

Created HeaderContainer component to make PageHeader and TopNavigation sticky when scrolling the content. AppShell is also affected by this change.
17 changes: 15 additions & 2 deletions apps/example/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import styles from "./styles.scss?inline"

import MonorepoChecker from "./components/MonorepoChecker"

import { AppShellProvider, AppShell, PageHeader, Container } from "@cloudoperators/juno-ui-components"
import {
AppShellProvider,
AppShell,
PageHeader,
Container,
TopNavigation,
TopNavigationItem,
} from "@cloudoperators/juno-ui-components"
import { mockedSession } from "@cloudoperators/juno-oauth"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import AppContent from "./components/AppContent"
Expand Down Expand Up @@ -59,12 +66,18 @@ const App = (props = {}) => {
<MonorepoChecker></MonorepoChecker>
<AsyncWorker consumerId={props.id} mockAPI={true} />
<AppShell
embedded={props.embedded === "true" || props.embedded === true}
MartinS-git marked this conversation as resolved.
Show resolved Hide resolved
pageHeader={
<PageHeader heading="Converged Cloud | Example App">
<HeaderUser login={oidc.login} logout={oidc.logout} />
</PageHeader>
}
embedded={props.embedded === "true" || props.embedded === true}
topNavigation={
<TopNavigation>
<TopNavigationItem icon="home" label="Home" />
<TopNavigationItem active label="Navigation Item" />
</TopNavigation>
}
>
<Container py>
<AppContent props={props} />
Expand Down
barsukov marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MainContainer } from "../MainContainer/index"
import { MainContainerInner } from "../MainContainerInner/index"
import { ContentContainer } from "../ContentContainer/index"
import { PageFooter } from "../PageFooter/index"
import { HeaderContainer } from "../HeaderContainer/index"

/**
* Body of the app. Treat this like the body tag of an html page.
Expand All @@ -36,12 +37,28 @@ export const AppShell = ({
)
}

const renderHeaderContainer = (
pageHeader?: AppShellProps["pageHeader"],
topNavigation?: AppShellProps["topNavigation"]
guoda-puidokaite marked this conversation as resolved.
Show resolved Hide resolved
) => {
if (!pageHeader && !topNavigation) {
return null
}
return (
<HeaderContainer fullWidth={fullWidthContent === true}>
{pageHeader && typeof pageHeader === "string" ? <PageHeader heading={pageHeader} /> : pageHeader}
guoda-puidokaite marked this conversation as resolved.
Show resolved Hide resolved
{topNavigation}
{/* Wrap everything except page header and footer and navigations in a main container. Add top margin to MainContainerInner as we are not in embedded mode here. */}
</HeaderContainer>
)
}

return (
<AppBody className={className} {...props}>
{contentHeading || ""}
{embedded ? (
<>
{topNavigation && topNavigation}
{topNavigation && <HeaderContainer>{topNavigation}</HeaderContainer>}
barsukov marked this conversation as resolved.
Show resolved Hide resolved
<MainContainer>
<MainContainerInner
fullWidth={fullWidthContent === false ? false : true}
Expand All @@ -55,22 +72,15 @@ export const AppShell = ({
</>
) : (
<>
{pageHeader && (typeof pageHeader === "string" || pageHeader instanceof String) ? (
<PageHeader heading={pageHeader} />
) : (
pageHeader
)}
{topNavigation && topNavigation}
{/* Wrap everything except page header and footer and navigations in a main container. Add top margin to MainContainerInner as we are not in embedded mode here. */}
{renderHeaderContainer(pageHeader, topNavigation)}
<MainContainer>
<MainContainerInner
fullWidth={fullWidthContent === true ? true : false}
hasSideNav={sideNavigation ? true : false}
className="jn-mt-[3.875rem]"
>
{sideNavigation && sideNavigation}
{/* Content Container. This is the place to add the app's main content. Render left margin only if no SideNavigation is present. */}
<ContentContainer className={sideNavigation ? "" : "jn-ml-8"}>{children}</ContentContainer>
{sideNavigation}
<ContentContainer>{children}</ContentContainer>
</MainContainerInner>
</MainContainer>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"

const headerContainerStyles = `
jn-flex
jn-flex-col
jn-sticky
jn-top-0
jn-z-50
jn-shadow-lg
jn-bg-theme-global-bg
`

export const HeaderContainer: React.FC<HeaderContainerProps> = ({
fullWidth = false,
className = "",
children = null,
...props
}) => {
return (
<div
className={`
MartinS-git marked this conversation as resolved.
Show resolved Hide resolved
juno-header-container
${!fullWidth ? "jn-w-full 2xl:jn-container 2xl:jn-mx-auto" : ""}
MartinS-git marked this conversation as resolved.
Show resolved Hide resolved
${headerContainerStyles}
${className}`}
{...props}
>
{children}
</div>
)
}

export interface HeaderContainerProps extends React.HTMLAttributes<HTMLDivElement> {
/** Whether the page/view content will stretch over the full width of the viewport or not. Default is `false`. */
fullWidth?: boolean
/** Add custom class name */
className?: string
children?: React.ReactNode
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"

import { HeaderContainer, HeaderContainerProps } from "./index"

export default {
title: "Internal/HeaderContainer",
component: HeaderContainer,
argTypes: {
children: {
MartinS-git marked this conversation as resolved.
Show resolved Hide resolved
control: false,
table: {
type: { summary: "ReactNode" },
},
},
},
}

const Template = (args: HeaderContainerProps) => <HeaderContainer {...args}>Header content</HeaderContainer>

export const Main = {
render: Template,

parameters: {
docs: {
description: {
story:
"Only needed if you want to create the framework of your application manually. In most cases, it is better to use the AppShell component instead. The header container includes <PageHeader> and <TopNavigation>. When scrolling the page, the component sticks to the top and above the content.",
},
},
},

args: {},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import * as React from "react"
import { render, screen } from "@testing-library/react"
import { HeaderContainer } from "./index"

describe("HeaderContainer", () => {
test("renders a header container", () => {
render(<HeaderContainer data-testid="my-header-container" />)
expect(screen.getByTestId("my-header-container")).toBeInTheDocument()
expect(screen.getByTestId("my-header-container")).toHaveClass("juno-header-container")
})

test("renders a header container which has sticky class", () => {
render(<HeaderContainer data-testid="my-header-container" />)
expect(screen.getByTestId("my-header-container")).toBeInTheDocument()
expect(screen.getByTestId("my-header-container")).toHaveClass("jn-sticky")
})

test("renders a custom className as passed", () => {
render(<HeaderContainer className="my-class" data-testid="my-header-container" />)
expect(screen.getByTestId("my-header-container")).toBeInTheDocument()
expect(screen.getByTestId("my-header-container")).toHaveClass("my-class")
})

test("renders all props", () => {
render(<HeaderContainer data-lolol="some-prop" data-testid="my-header-container" />)
expect(screen.getByTestId("my-header-container")).toBeInTheDocument()
expect(screen.getByTestId("my-header-container")).toHaveAttribute("data-lolol", "some-prop")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { HeaderContainer, type HeaderContainerProps } from "./HeaderContainer.component"
barsukov marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/ui-components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { FormSection } from "./components/FormSection/FormSection.component"
export { Grid } from "./components/Grid/Grid.component"
export { GridRow } from "./components/GridRow/GridRow.component"
export { GridColumn } from "./components/GridColumn/GridColumn.component"
export { HeaderContainer } from "./components/HeaderContainer/HeaderContainer.component"
export { Icon } from "./components/Icon/index"
export { InputGroup } from "./components/InputGroup/index"
export { IntroBox } from "./components/IntroBox/index.js"
Expand Down
Loading