Skip to content

Commit

Permalink
feat(ui): adds HeaderContainer component (#607)
Browse files Browse the repository at this point in the history
* feat(ui): adds HeaderContainer component

* fix(ui): corrections afer review

* Update .changeset/giant-news-flash.md

Co-authored-by: Guoda <[email protected]>

* Update packages/ui-components/src/components/AppShell/AppShell.component.tsx

Co-authored-by: Guoda <[email protected]>

* Update packages/ui-components/src/components/AppShell/AppShell.component.tsx

Co-authored-by: Guoda <[email protected]>

* fix(ui): fixes based on Guodas review commentary

* fix(ui): code and logic improvements. Thankfully provided by Wowa

---------

Co-authored-by: Guoda <[email protected]>
Co-authored-by: Wowa Barsukov <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent ea90738 commit 385ebcf
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 13 deletions.
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}
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
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"]
) => {
if (!pageHeader && !topNavigation) {
return null
}
return (
<HeaderContainer fullWidth={fullWidthContent === true}>
{pageHeader && typeof pageHeader === "string" ? <PageHeader heading={pageHeader} /> : pageHeader}
{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>}
<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={`
juno-header-container
${!fullWidth ? "jn-w-full 2xl:jn-container 2xl:jn-mx-auto" : ""}
${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: {
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"
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

0 comments on commit 385ebcf

Please sign in to comment.