Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/great-peaches-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@cloudoperators/juno-ui-components": major
---

Feature: Enhanced `PageFooter` Component for Flexible Theming and Accessibility

- Removed hardcoded cloud illustration, allowing CSS-based branding customization.
- Added a `copyright` prop for optional right-side rendering.
- Updated `children` rendering to support single/multiple items with defined spacing.
- Integrated ARIA roles and attributes (role="group", aria-labelledby) for better semantic clarity.
- Implemented design tokens, accommodating `light` and `dark` modes.
- Added `PageFooter` to Example App.
7 changes: 4 additions & 3 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
import React, { useEffect, useMemo } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MessagesProvider } from "@cloudoperators/juno-messages-provider"
import { AppShell, AppShellProvider } from "@cloudoperators/juno-ui-components"
import { AppShell, AppShellProvider, PageFooter } from "@cloudoperators/juno-ui-components"

import useAuthStore from "./store/useAuthStore"
import AsyncWorker from "./components/AsyncWorker"
import Footer from "./components/app-shell/Footer"
import useConfigStore from "./store/useConfigStore"
import Content from "./components/app-shell/Content"
import Header from "./components/app-shell/header/Header"
Expand All @@ -32,13 +31,15 @@ const App: React.FC<AppProps> = ({ endpoint = "", id = "" }) => {
setEndpoint(endpoint)
}, [endpoint])

const COPYRIGHT_TEXT = "Copyright © 2024 SAP SE, SAP affiliates and Juno contributors"

return (
<QueryClientProvider client={queryClient}>
<AsyncWorker consumerId={id} />
<AppShell
pageHeader={<Header />}
sideNavigation={isUserAuthenticated ? <SideNavigationComponent /> : null}
pageFooter={<Footer />}
pageFooter={<PageFooter copyright={COPYRIGHT_TEXT} />}
>
<Content />
</AppShell>
Expand Down
23 changes: 0 additions & 23 deletions apps/example/src/components/app-shell/Footer.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,48 @@
*/

import React from "react"
import CCloudShape from "../../img/ccloud_shape.svg"
import "./page-footer.css"

const basePageFooter = `
jn:flex
const basePageFooterStyles = `
jn:shrink-0
jn:grow-0
jn:basis-auto
jn:relative
jn:bg-theme-global-bg
jn:min-h-[3.25rem]
jn:pl-6
jn:pr-24
jn:py-5
jn:z-50
jn:text-theme-pagefooter
jn:bg-theme-pagefooter
`

const logoStyles = `
jn:h-[2.625rem]
jn:absolute
jn:right-0
jn:bottom-0
`
export interface PageFooterProps extends React.HTMLAttributes<HTMLDivElement> {
/** Additional custom styling class name for the footer container */
className?: string
/** The content to render inside the footer, typically links or informational text
* Use a list structure e.g. `<ul>` with `<li>` for grouped content or links, as in examples.
* Available CSS classes for styling:
* - `.juno-pagefooter-title`: Style for a title element within a column.
* - `.juno-pagefooter-items`: Style for a list of items.
* - `.juno-pagefooter-items-inline`: Style for a single line list with pipe separators.
* - `.juno-pagefooter-item`: Style for individual list items.
*/
children?: React.ReactNode
/** Optional copyright notice to display within the footer */
copyright?: string
}

/**
* The page footer component renders a footer at the bottom of the website. Place as last child of AppBody.
* PageFooter component renders a footer at the bottom of the page.
* It consists of a flexible content area for children and an optional copyright section.
* Usage:
* The component can be used to add legal disclaimers, links, or other contextual information at the page's footer.
*/
export const PageFooter: React.FC<PageFooterProps> = ({ className = "", children, ...props }) => {
export const PageFooter: React.FC<PageFooterProps> = ({ className = "", children, copyright = "", ...props }) => {
return (
<div className={`juno-pagefooter ${basePageFooter} ${className}`} role="contentinfo" {...props}>
{children}
<CCloudShape className={logoStyles} alt="cloud shape" />
<div className={`juno-pagefooter ${basePageFooterStyles} ${className}`} role="contentinfo" {...props}>
<div className="juno-content">
<div className={"juno-pagefooter-children"}>{children}</div>
{copyright && <div className="juno-pagefooter-copyright">{copyright}</div>}
</div>
</div>
)
}

export interface PageFooterProps extends React.HTMLAttributes<HTMLDivElement> {
/** Add custom class name */
className?: string
children?: React.ReactNode
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import React from "react"

import { PageFooter, PageFooterProps } from "./index"

export default {
Expand All @@ -17,22 +16,167 @@ export default {
type: { summary: "ReactNode" },
},
},
copyright: {
control: "text",
table: {
type: { summary: "string" },
},
},
className: {
control: false,
},
},
}

const Template = (args: PageFooterProps) => <PageFooter {...args}></PageFooter>

export const Simple = {
export const WithCustomCopyright = {
render: Template,
parameters: {
docs: {
description: {
story: "PageFooter with a custom copyright notice.",
},
},
},
args: {
copyright: "© 2023 Custom Corporation. All rights reserved.",
},
}

export const InlineLinks = {
render: Template,
parameters: {
docs: {
description: {
story:
"The page footer component renders a footer at the bottom of the website. Place as last child of AppBody.",
story: "PageFooter rendering inline links, illustrating how children can be displayed within the footer.",
},
},
},
args: {
children: (
<ul className="juno-pagefooter-items-inline">
<li>
<a className="juno-pagefooter-item" href="#">
About
</a>
</li>
<li>
<a className="juno-pagefooter-item" href="#">
Imprint
</a>
</li>
<li>
<a className="juno-pagefooter-item" href="#">
Terms of Use
</a>
</li>
</ul>
),
},
}

args: {},
export const WithTwoColumns = {
render: Template,
parameters: {
docs: {
description: {
story: "An example showing two columns within the PageFooter, each with a title and list of items.",
},
},
},
args: {
children: (
<>
<div role="group" aria-labelledby="footer-col1-title">
<p className="juno-pagefooter-title" id="footer-col1-title">
Column 1
</p>
<ul className="juno-pagefooter-items">
<li>
<a className="juno-pagefooter-item" href="#">
About
</a>
</li>
<li>
<a className="juno-pagefooter-item" href="#">
Imprint
</a>
</li>
</ul>
</div>
<div role="group" aria-labelledby="footer-col2-title">
<p className="juno-pagefooter-title" id="footer-col2-title">
Column 2
</p>
<ul className="juno-pagefooter-items">
<li>
<a className="juno-pagefooter-item" href="#">
Privacy Policy
</a>
</li>
<li>
<a className="juno-pagefooter-item" href="#">
Contact
</a>
</li>
</ul>
</div>
</>
),
},
}

export const WithThreeColumns = {
render: Template,
parameters: {
docs: {
description: {
story: "An example showing three columns within the PageFooter, each with a title and list of items.",
},
},
},
args: {
copyright: "© 2023 Custom Corporation. All rights reserved.",
children: (
<>
<div role="group" aria-labelledby="footer-col1-title">
<p className="juno-pagefooter-title" id="footer-col1-title">
Column 1
</p>
<ul className="juno-pagefooter-items">
<li>
<a className="juno-pagefooter-item" href="#">
About
</a>
</li>
</ul>
</div>
<div role="group" aria-labelledby="footer-col2-title">
<p className="juno-pagefooter-title" id="footer-col2-title">
Column 2
</p>
<ul className="juno-pagefooter-items">
<li>
<a className="juno-pagefooter-item" href="#">
Privacy Policy
</a>
</li>
</ul>
</div>
<div role="group" aria-labelledby="footer-col3-title">
<p className="juno-pagefooter-title" id="footer-col3-title">
Column 3
</p>
<ul className="juno-pagefooter-items">
<li>
<a className="juno-pagefooter-item" href="#">
Contact
</a>
</li>
</ul>
</div>
</>
),
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,59 @@ import { PageFooter } from "./index"
describe("PageFooter", () => {
test("renders a simple Page Footer and has flexbox layout", () => {
render(<PageFooter />)
expect(screen.getByRole("contentinfo")).toBeInTheDocument()
expect(screen.getByRole("contentinfo")).toHaveClass("jn:flex")
const footer = screen.getByRole("contentinfo")
expect(footer).toBeInTheDocument()
})

test("renders a Page Footer to have the global bg color", () => {
test("renders a Page Footer with specific styling", () => {
render(<PageFooter />)
expect(screen.getByRole("contentinfo")).toBeInTheDocument()
expect(screen.getByRole("contentinfo")).toHaveClass("jn:bg-theme-global-bg")
const footer = screen.getByRole("contentinfo")
expect(footer).toBeInTheDocument()
expect(footer).toHaveClass("juno-pagefooter") // Confirm it has the main class
})

test("renders children as passed", () => {
render(
<PageFooter>
<button></button>
<button>Test Button</button>
</PageFooter>
)
expect(screen.getByRole("contentinfo")).toBeInTheDocument()
expect(screen.getByRole("button")).toBeInTheDocument()
const button = screen.getByRole("button")
expect(button).toBeInTheDocument()
expect(button).toHaveTextContent("Test Button")
})

test("renders a custom className", () => {
render(<PageFooter className="my-custom-classname" />)
expect(screen.getByRole("contentinfo")).toBeInTheDocument()
expect(screen.getByRole("contentinfo")).toHaveClass("my-custom-classname")
const footer = screen.getByRole("contentinfo")
expect(footer).toBeInTheDocument()
expect(footer).toHaveClass("my-custom-classname")
})

test("renders all props", () => {
render(<PageFooter data-lolol="some-prop" />)
expect(screen.getByRole("contentinfo")).toBeInTheDocument()
expect(screen.getByRole("contentinfo")).toHaveAttribute("data-lolol", "some-prop")
const footer = screen.getByRole("contentinfo")
expect(footer).toBeInTheDocument()
expect(footer).toHaveAttribute("data-lolol", "some-prop")
})

test("renders copyright section when provided", () => {
render(<PageFooter copyright="© 2023 Test" />)
const footer = screen.getByRole("contentinfo")
const copyrightSection = footer.querySelector(".juno-pagefooter-copyright")
expect(copyrightSection).toBeInTheDocument()
expect(copyrightSection).toHaveTextContent("© 2023 Test")
})

test("renders children section with flex layout", () => {
render(
<PageFooter>
<span>Child Item</span>
</PageFooter>
)
const childrenSection = screen.getByRole("contentinfo").querySelector(".juno-pagefooter-children")
expect(childrenSection).toBeInTheDocument()
expect(childrenSection).toHaveClass("juno-pagefooter-children")
expect(childrenSection).toHaveTextContent("Child Item")
})
})
Loading
Loading