Skip to content

Commit 55014f5

Browse files
feat(ui): update PageHeader styling, API and theming support (#1150)
* improve header * tidied header * changeset * tests
1 parent bc48ec5 commit 55014f5

6 files changed

Lines changed: 224 additions & 97 deletions

File tree

.changeset/chilly-sheep-poke.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudoperators/juno-ui-components": minor
3+
---
4+
5+
Updated `PageHeader` component's styling, API, theming support and documentation
6+
- Added `applicationName`, deprecated `heading`.
7+
- Added `href`, used if `onClick` not supplied.

packages/ui-components/src/components/PageHeader/PageHeader.component.tsx

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,95 +7,140 @@ import React, { ReactElement, ReactNode } from "react"
77
import DefaultLogo from "../../img/JunoUI_logo.svg"
88

99
const pageHeaderStyles = `
10-
jn:min-h-[3.25rem]
11-
jn:bg-juno-grey-blue-11
10+
jn:bg-theme-pageheader
11+
jn:border-theme-pageheader
12+
jn:border-b-[1px]
1213
jn:sticky
1314
jn:top-0
14-
jn:px-6
15-
jn:py-3
15+
jn:px-[1.25rem]
16+
jn:py-[0.25rem]
17+
jn:h-[3.25rem]
1618
jn:z-50
19+
jn:flex
20+
jn:items-center
21+
jn:justify-between
1722
`
1823

1924
const pageHeaderInnerStyles = `
20-
jn:grid
21-
jn:grid-cols-[minmax(0,max-content)_1fr]
22-
jn:gap-3
23-
jn:h-7
25+
jn:flex
26+
jn:flex-row
27+
jn:justify-between
28+
jn:items-center
2429
jn:w-full
30+
jn:h-full
31+
jn:full
32+
jn:gap-2
33+
`
34+
35+
const logoAndTitleContainerStyles = `
36+
jn:flex
37+
jn:flex-row
2538
jn:items-center
39+
jn:gap-2
2640
`
2741

2842
const logoContainerStyles = `
29-
jn:h-7
3043
jn:max-w-xs
3144
jn:*:w-min
3245
jn:*:max-w-xs
3346
jn:*:h-7
3447
jn:*:object-contain
3548
`
3649

37-
const contentContainerStyles = `
38-
jn:grid
39-
jn:grid-cols-[1fr_minmax(0,max-content)]
50+
const applicationNameStyles = `
51+
jn:text-theme-pageheader-appname-default
52+
jn:text-lg
53+
jn:text-theme-high
4054
`
4155

4256
const optionsStyles = `
4357
jn:flex
4458
jn:flex-row
4559
jn:items-center
46-
`
47-
48-
const headingStyles = `
49-
jn:text-lg
50-
jn:text-theme-high
60+
jn:gap-2
61+
jn:justify-end
5162
`
5263

5364
/**
54-
* The PageHeader component renders a header to the application.
55-
* In order to customize the header Logo, PageHeader accepts a `logo` prop that expects a custom component: `logo={<CustomLogo />}` Ideally, the custom logo component should return an `<img />` or an inline `<svg>` element. When using `svg`, make sure the file does not contain any unnecessary cruft. `Svgo` is a great tool to optimize `svg` files. Make sure the `viewBox` element is not removed when optimizing a file for usage a a header logo.
56-
* Pass as prop to AppShell so it gets slotted into the correct place in the layout. If building your layout manually without AppShell place as first child of AppBody.
65+
* The `PageHeader` component renders a header for an application with customisable `logo`, `title` and other options.
66+
* Ideally, the custom logo component should return an `<img />` or an inline `<svg>` element.
67+
* When using SVG, ensure the file is optimized to eliminate any superfluous elements or data that can contribute to increased file size and reduced performance. `Svgo` is a great tool to optimize `svg` files. Make sure the `viewBox` element is not removed when optimizing a file for usage a a header logo.
68+
* Pass as prop to `AppShell` so it gets slotted into the correct place in the layout. If building your layout manually without `AppShell` place as first child of AppBody.
5769
*/
5870

5971
export const PageHeader: React.FC<PageHeaderProps> = ({
60-
heading,
72+
heading = "",
73+
applicationName = "",
74+
href = "",
6175
className = "",
76+
logo = true,
6277
children,
63-
logo,
6478
onClick,
6579
...props
6680
}) => {
81+
const Logo =
82+
typeof logo === "function" || React.isValidElement(logo)
83+
? logo
84+
: logo && <DefaultLogo alt="" data-testid="default-logo" />
85+
86+
const renderLogoAndTitle = () => (
87+
<div
88+
onClick={onClick}
89+
className={`juno-pageheader-logo-title ${logoAndTitleContainerStyles} ${onClick ? "jn:cursor-pointer" : ""}`}
90+
role={onClick ? "button" : undefined}
91+
>
92+
<div className={`juno-pageheader-logo-container ${logoContainerStyles}`}>{Logo}</div>
93+
<div className={`juno-pageheader-application-name ${applicationNameStyles}`}>
94+
{applicationName.trim() || heading}
95+
</div>
96+
</div>
97+
)
98+
99+
const contentWrapper = (
100+
<>
101+
{renderLogoAndTitle()}
102+
<div className={`juno-pageheader-content-container`}>
103+
<div className={`juno-pageheader-options ${optionsStyles}`}>{children}</div>
104+
</div>
105+
</>
106+
)
107+
67108
return (
68109
<div className={`juno-pageheader theme-dark ${pageHeaderStyles} ${className}`} role="banner" {...props}>
69110
<div className={`juno-pageheader-inner ${pageHeaderInnerStyles}`}>
70-
<div className={`juno-pageheader-logo-container ${logoContainerStyles}`}>
71-
{
72-
typeof logo === "function" || // Render if logo is a function (component)
73-
(React.isValidElement(logo) && logo) || // Render if logo is a valid React element
74-
((logo === true || logo === undefined) && <DefaultLogo data-testid="default-logo" alt={""} />) // Render default logo if logo is true or undefined
75-
}
76-
</div>
77-
<div className={`juno-pageheader-content-container ${contentContainerStyles}`}>
78-
<div
79-
className={`juno-pageheader-heading ${headingStyles} ${typeof onClick === "function" ? "jn:cursor-pointer" : ""}`}
80-
onClick={onClick}
81-
>
82-
{heading && heading}
83-
</div>
84-
<div className={`juno-pageheader-options ${optionsStyles}`}>{children}</div>
85-
</div>
111+
{href ? <a href={href}>{contentWrapper}</a> : contentWrapper}
86112
</div>
87113
</div>
88114
)
89115
}
90116

91117
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
92-
/** Heading (typically the name of the application) */
118+
/**
119+
* Name of the application.
120+
*/
121+
applicationName?: string
122+
/**
123+
* DEPRECATED PROP - Replaced by `applicationName`. If `applicationName` is provided, it will take precedence.
124+
*/
93125
heading?: string | ReactElement
94-
/** Add custom class name */
126+
/**
127+
* Link to open when applicationName or logo is clicked. If `onClick` is provided, it will take precedence.
128+
*/
129+
href?: string
130+
/**
131+
* Custom class names.
132+
*/
95133
className?: string
134+
/**
135+
* Application logo.
136+
*/
96137
logo?: boolean | ReactElement
97-
/** Optional: onClick handler for brand logo/page title. To be used to navigate to the home page. */
138+
/**
139+
* Handler executed when `applicationName` or `logo` are clicked.
140+
*/
98141
onClick?: React.MouseEventHandler<HTMLDivElement>
99-
/** Children to render in the header such as user name, avatar, log-in/out button, etc. */
142+
/**
143+
* Children to render in the header such as user name, avatar, log-in/out button, etc.
144+
*/
100145
children?: ReactNode
101146
}

packages/ui-components/src/components/PageHeader/PageHeader.stories.tsx

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,6 @@ import CustomLogoSquarePNG from "./custom-logo-placeholders/custom-logo-square.p
1414
import { PageHeader } from "./index"
1515
import { Button } from "../Button/"
1616

17-
const CustomLogoImagePNGSquare = () => <img src={CustomLogoSquarePNG} alt="Custom Logo Square" />
18-
const CustomLogoImagePNGLandscape = () => <img src={CustomLogoLandscapePNG} alt="Custom Logo Landscape" />
19-
const CustomLogoImagePNGPortrait = () => <img src={CustomLogoPortraitPNG} alt="Custom Logo Portrait" />
20-
const HeaderOptions = () => {
21-
const userNameStyles = { marginRight: "1rem" }
22-
return (
23-
<>
24-
<span style={userNameStyles}>Jane Doe</span>
25-
<Button size="small">Log Out</Button>
26-
</>
27-
)
28-
}
29-
3017
type Story = StoryObj<typeof meta>
3118

3219
const meta: Meta<typeof PageHeader> = {
@@ -39,7 +26,7 @@ const meta: Meta<typeof PageHeader> = {
3926
type: { summary: "ReactNode" },
4027
},
4128
},
42-
heading: {
29+
applicationName: {
4330
control: "text",
4431
table: {
4532
type: { summary: "string | ReactElement" },
@@ -57,86 +44,121 @@ export default meta
5744

5845
export const Default: Story = {
5946
args: {
60-
heading: "My App",
61-
logo: true,
47+
applicationName: "My App",
48+
onClick: undefined,
6249
},
6350
}
6451

65-
export const WithHeading: Story = {
66-
parameters: {
67-
docs: {
68-
description: { story: "PageHeader with Heading." },
69-
},
70-
},
52+
export const WithapplicationName: Story = {
7153
args: {
72-
heading: "My Awesome App",
54+
applicationName: "My Awesome App",
7355
},
7456
}
7557

76-
export const WithHeadingAndChildren: Story = {
58+
export const WithapplicationNameAndChildren: Story = {
7759
args: {
78-
heading: "My Awesome App",
79-
children: <HeaderOptions />,
60+
applicationName: "My Awesome App",
61+
children: (
62+
<>
63+
<span>Jane Doe</span>
64+
<Button size="small">Log Out</Button>
65+
</>
66+
),
8067
},
8168
}
8269

83-
export const NoHeadingWithChildren: Story = {
70+
export const NoapplicationNameWithChildren: Story = {
8471
args: {
85-
children: <HeaderOptions />,
72+
children: (
73+
<>
74+
<span>Jane Doe</span>
75+
<Button size="small">Log Out</Button>
76+
</>
77+
),
8678
},
8779
}
8880

8981
export const NoLogo: Story = {
9082
args: {
9183
logo: false,
92-
heading: "My Awesome App",
93-
children: <HeaderOptions />,
84+
applicationName: "My Awesome App",
85+
children: (
86+
<>
87+
<span>Jane Doe</span>
88+
<Button size="small">Log Out</Button>
89+
</>
90+
),
9491
},
9592
}
9693

9794
export const WithCustomLogoSquareInline: Story = {
9895
args: {
9996
logo: <CustomLogoSquare alt={""} />,
100-
heading: "My Awesome App",
101-
children: <HeaderOptions />,
97+
applicationName: "My Awesome App",
98+
children: (
99+
<>
100+
<span>Jane Doe</span>
101+
<Button size="small">Log Out</Button>
102+
</>
103+
),
102104
},
103105
}
104106

105107
export const WithCustomLogoLandscapeInline: Story = {
106108
args: {
107109
logo: <CustomLogoLandscape alt={""} />,
108-
heading: "My Awesome App",
109-
children: <HeaderOptions />,
110+
applicationName: "My Awesome App",
111+
children: (
112+
<>
113+
<span>Jane Doe</span>
114+
<Button size="small">Log Out</Button>
115+
</>
116+
),
110117
},
111118
}
112119

113120
export const WithCustomLogoPortraitInline: Story = {
114121
args: {
115122
logo: <CustomLogoPortrait alt={""} />,
116-
heading: "My Awesome App",
123+
applicationName: "My Awesome App",
117124
},
118125
}
119126

120127
export const WithCustomLogoPNGSquare: Story = {
121128
args: {
122-
logo: <CustomLogoImagePNGSquare />,
123-
heading: "My Awesome App",
124-
children: <HeaderOptions />,
129+
logo: <img src={CustomLogoSquarePNG} alt="Custom Logo Square" />,
130+
applicationName: "My Awesome App",
131+
children: (
132+
<>
133+
<span>Jane Doe</span>
134+
<Button size="small">Log Out</Button>
135+
</>
136+
),
125137
},
126138
}
127139

128140
export const WithCustomLogoPNGLandscape: Story = {
129141
args: {
130-
logo: <CustomLogoImagePNGLandscape />,
131-
heading: "My Awesome App",
132-
children: <HeaderOptions />,
142+
logo: <img src={CustomLogoLandscapePNG} alt="Custom Logo Landscape" />,
143+
applicationName: "My Awesome App",
144+
children: (
145+
<>
146+
<span>Jane Doe</span>
147+
<Button size="small">Log Out</Button>
148+
</>
149+
),
133150
},
134151
}
135152

136153
export const WithCustomLogoPNGPortrait: Story = {
137154
args: {
138-
logo: <CustomLogoImagePNGPortrait />,
139-
heading: "My Awesome App",
140-
children: <HeaderOptions />,
155+
logo: <img src={CustomLogoPortraitPNG} alt="Custom Logo Portrait" />,
156+
applicationName: "My Awesome App",
157+
children: (
158+
<>
159+
<span>Jane Doe</span>
160+
<Button size="small">Log Out</Button>
161+
</>
162+
),
141163
},
142164
}

0 commit comments

Comments
 (0)