Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"tslib": "^2.8.1"
},
"devDependencies": {
"@patternfly/patternfly": "6.5.0-prerelease.19",
"@patternfly/patternfly": "6.5.0-prerelease.21",
"case-anything": "^3.1.2",
"css": "^3.0.0",
"fs-extra": "^11.3.0"
Expand Down
81 changes: 6 additions & 75 deletions packages/react-core/src/components/Compass/CompassHero.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,18 @@
import styles from '@patternfly/react-styles/css/components/Compass/compass';
import { css } from '@patternfly/react-styles';

import compassHeroBackgroundImageLight from '@patternfly/react-tokens/dist/esm/c_compass__hero_BackgroundImage_light';
import compassHeroBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_compass__hero_BackgroundImage_dark';
import compassHeroGradientStop1Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_1_light';
import compassHeroGradientStop2Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_2_light';
import compassHeroGradientStop3Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_3_light';
import compassHeroGradientStop1Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_1_dark';
import compassHeroGradientStop2Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_2_dark';
import compassHeroGradientStop3Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_3_dark';

/** A wrapper component to pass a PatternFly Hero component into. */
interface CompassHeroProps extends Omit<React.HTMLProps<HTMLDivElement>, 'content'> {
/** Content of the hero */
children?: React.ReactNode;
/** Additional classes added to the hero */
className?: string;
/** Light theme background image path of the hero */
backgroundSrcLight?: string;
/** Dark theme background image path of the hero */
backgroundSrcDark?: string;
/** Light theme gradient of the hero */
gradientLight?: {
stop1?: string;
stop2?: string;
stop3?: string;
};
/** Dark theme gradient of the hero */
gradientDark?: {
stop1?: string;
stop2?: string;
stop3?: string;
};
}

export const CompassHero: React.FunctionComponent<CompassHeroProps> = ({
className,
children,
backgroundSrcLight,
backgroundSrcDark,
gradientLight,
gradientDark,
...props
}) => {
const backgroundImageStyles: { [key: string]: string } = {};
if (backgroundSrcLight) {
backgroundImageStyles[compassHeroBackgroundImageLight.name] = `url(${backgroundSrcLight})`;
}
if (backgroundSrcDark) {
backgroundImageStyles[compassHeroBackgroundImageDark.name] = `url(${backgroundSrcDark})`;
}

if (gradientLight) {
if (gradientLight.stop1) {
backgroundImageStyles[compassHeroGradientStop1Light.name] = gradientLight.stop1;
}
if (gradientLight.stop2) {
backgroundImageStyles[compassHeroGradientStop2Light.name] = gradientLight.stop2;
}
if (gradientLight.stop3) {
backgroundImageStyles[compassHeroGradientStop3Light.name] = gradientLight.stop3;
}
}
if (gradientDark) {
if (gradientDark.stop1) {
backgroundImageStyles[compassHeroGradientStop1Dark.name] = gradientDark.stop1;
}
if (gradientDark.stop2) {
backgroundImageStyles[compassHeroGradientStop2Dark.name] = gradientDark.stop2;
}
if (gradientDark.stop3) {
backgroundImageStyles[compassHeroGradientStop3Dark.name] = gradientDark.stop3;
}
}

return (
<div
className={css(styles.compassPanel, styles.compassHero, className)}
style={{ ...props.style, ...backgroundImageStyles }}
{...props}
>
<div className={css(styles.compassHeroBody)}>{children}</div>
</div>
);
};
export const CompassHero: React.FunctionComponent<CompassHeroProps> = ({ className, children, ...props }) => (
<div className={css(`${styles.compass}__hero`, className)} {...props}>
{children}
</div>
);

CompassHero.displayName = 'CompassHero';
Original file line number Diff line number Diff line change
Expand Up @@ -16,135 +16,23 @@ test('Renders with children', () => {
expect(screen.getByText('Test content')).toBeVisible();
});

test('Renders with custom class name when className prop is provided', () => {
render(<CompassHero className="custom-class">Test</CompassHero>);
expect(screen.getByText('Test').parentElement).toHaveClass('custom-class');
});

test(`Renders with default ${styles.compassPanel} and ${styles.compassHero} classes on the hero and ${styles.compassHeroBody} class on the hero body`, () => {
test(`Renders with ${styles.compass}__hero class by defaulty`, () => {
render(<CompassHero>Test</CompassHero>);
const heroBodyElement = screen.getByText('Test');
expect(heroBodyElement).toHaveClass(styles.compassHeroBody);

const heroElement = heroBodyElement.parentElement;
expect(heroElement).toHaveClass(styles.compassPanel);
expect(heroElement).toHaveClass(styles.compassHero);
});

test('Renders with light background image style when backgroundSrcLight is provided', () => {
const backgroundSrc = 'light-bg.jpg';
render(<CompassHero backgroundSrcLight={backgroundSrc}>Test</CompassHero>);
expect(screen.getByText('Test').parentElement).toHaveStyle(
`--pf-v6-c-compass__hero--BackgroundImage--light: url(${backgroundSrc})`
);
});

test('Renders with dark background image style when backgroundSrcDark is provided', () => {
const backgroundSrc = 'dark-bg.jpg';
render(<CompassHero backgroundSrcDark={backgroundSrc}>Test</CompassHero>);
expect(screen.getByText('Test').parentElement).toHaveStyle(
`--pf-v6-c-compass__hero--BackgroundImage--dark: url(${backgroundSrc})`
);
});

test('Renders with both light and dark background image styles when both are provided', () => {
const lightSrc = 'light-bg.jpg';
const darkSrc = 'dark-bg.jpg';
render(
<CompassHero backgroundSrcLight={lightSrc} backgroundSrcDark={darkSrc}>
Test
</CompassHero>
);
const heroElement = screen.getByText('Test').parentElement;
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--light: url(${lightSrc})`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--dark: url(${darkSrc})`);
});

test('Renders with light gradient styles when gradientLight is provided', () => {
const gradient = {
stop1: '#ff0000',
stop2: '#00ff00',
stop3: '#0000ff'
};
render(<CompassHero gradientLight={gradient}>Test</CompassHero>);
const heroElement = screen.getByText('Test').parentElement;
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${gradient.stop1}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-2--light: ${gradient.stop2}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-3--light: ${gradient.stop3}`);
});

test('Renders with dark gradient styles when gradientDark is provided', () => {
const gradient = {
stop1: '#ff0000',
stop2: '#00ff00',
stop3: '#0000ff'
};
render(<CompassHero gradientDark={gradient}>Test</CompassHero>);
const heroElement = screen.getByText('Test').parentElement;
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${gradient.stop1}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-2--dark: ${gradient.stop2}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-3--dark: ${gradient.stop3}`);
});

test('Renders with both light and dark gradient styles when both are provided', () => {
const lightGradient = {
stop1: '#ff0000',
stop2: '#00ff00',
stop3: '#0000ff'
};
const darkGradient = {
stop1: '#000000',
stop2: '#ffffff',
stop3: '#808080'
};
render(
<CompassHero gradientLight={lightGradient} gradientDark={darkGradient}>
Test
</CompassHero>
);
const heroElement = screen.getByText('Test').parentElement;
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${lightGradient.stop1}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${darkGradient.stop1}`);
expect(screen.getByText('Test')).toHaveClass(`${styles.compass}__hero`, { exact: true });
});

test('Renders with both background images and gradient styles when both are provided', () => {
const lightSrc = 'light-bg.jpg';
const darkSrc = 'dark-bg.jpg';
const lightGradient = { stop1: '#ff0000' };
const darkGradient = { stop1: '#000000' };

render(
<CompassHero
backgroundSrcLight={lightSrc}
backgroundSrcDark={darkSrc}
gradientLight={lightGradient}
gradientDark={darkGradient}
>
Test
</CompassHero>
);
const heroElement = screen.getByText('Test').parentElement;
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--light: url(${lightSrc})`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--dark: url(${darkSrc})`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${lightGradient.stop1}`);
expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${darkGradient.stop1}`);
test('Renders with custom class name when className prop is provided', () => {
render(<CompassHero className="custom-class">Test</CompassHero>);
expect(screen.getByText('Test')).toHaveClass('custom-class');
});

test('Renders with additional props spread to the component', () => {
render(<CompassHero aria-label="Test label">Test</CompassHero>);
expect(screen.getByText('Test').parentElement).toHaveAccessibleName('Test label');
expect(screen.getByText('Test')).toHaveAccessibleName('Test label');
});

test('Matches the snapshot', () => {
const { asFragment } = render(
<CompassHero
backgroundSrcLight="light.jpg"
backgroundSrcDark="dark.jpg"
gradientLight={{ stop1: '#ff0000', stop2: '#00ff00', stop3: '#0000ff' }}
gradientDark={{ stop1: '#000000', stop2: '#ffffff', stop3: '#808080' }}
>
<div>Hero content</div>
</CompassHero>
);
const { asFragment } = render(<CompassHero>Hero content</CompassHero>);
expect(asFragment()).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,9 @@
exports[`Matches the snapshot 1`] = `
<DocumentFragment>
<div
class="pf-v6-c-compass__panel pf-v6-c-compass__hero"
style="--pf-v6-c-compass__hero--BackgroundImage--light: url(light.jpg); --pf-v6-c-compass__hero--BackgroundImage--dark: url(dark.jpg); --pf-v6-c-compass__hero--gradient--stop-1--light: #ff0000; --pf-v6-c-compass__hero--gradient--stop-2--light: #00ff00; --pf-v6-c-compass__hero--gradient--stop-3--light: #0000ff; --pf-v6-c-compass__hero--gradient--stop-1--dark: #000000; --pf-v6-c-compass__hero--gradient--stop-2--dark: #ffffff; --pf-v6-c-compass__hero--gradient--stop-3--dark: #808080;"
class="pf-v6-c-compass__hero"
>
<div
class="pf-v6-c-compass__hero-body"
>
<div>
Hero content
</div>
</div>
Hero content
</div>
</DocumentFragment>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CompassMainHeader,
CompassPanel,
CompassMessageBar,
Hero,
Tabs,
TabsComponent,
Tab,
Expand Down Expand Up @@ -119,8 +120,8 @@ export const CompassBasic: React.FunctionComponent = () => {
const sidebarStartContent = sidebarContent;
const mainContent = (
<>
<CompassHero gradientDark={{ stop1: '#000', stop2: '#1b0d33', stop3: '#3d2785' }}>
<div>Hero</div>
<CompassHero>
<Hero gradientDark={{ stop1: '#000', stop2: '#1b0d33', stop3: '#3d2785' }}>Hero</Hero>
</CompassHero>
<CompassMainHeader title={<Title headingLevel="h1">Content title</Title>} />
<CompassContent>
Expand Down
102 changes: 102 additions & 0 deletions packages/react-core/src/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import styles from '@patternfly/react-styles/css/components/Hero/hero';
import { css } from '@patternfly/react-styles';
import heroBackgroundImageLight from '@patternfly/react-tokens/dist/esm/c_hero_BackgroundImage_light';
import heroBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_hero_BackgroundImage_dark';
import heroGradientStop1Light from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_1_light';
import heroGradientStop2Light from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_2_light';
import heroGradientStop3Light from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_3_light';
import heroGradientStop1Dark from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_1_dark';
import heroGradientStop2Dark from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_2_dark';
import heroGradientStop3Dark from '@patternfly/react-tokens/dist/esm/c_hero_gradient_stop_3_dark';
import heroBodyWidth from '@patternfly/react-tokens/dist/esm/c_hero__body_Width';
import heroBodyMaxWidth from '@patternfly/react-tokens/dist/esm/c_hero__body_MaxWidth';

/** The main Hero component that allows adjusting of its background images and gradients in different color modes (such as light and dark). */

export interface HeroProps extends Omit<React.HTMLProps<HTMLDivElement>, 'content'> {
/** Content of the hero */
children?: React.ReactNode;
/** Additional classes added to the hero */
className?: string;
/** Light theme background image path of the hero */
backgroundSrcLight?: string;
/** Dark theme background image path of the hero */
backgroundSrcDark?: string;
/** Light theme gradient of the hero, taking any valid CSS color values for each stop property. */
gradientLight?: {
stop1?: string;
stop2?: string;
stop3?: string;
};
/** Dark theme gradient of the hero, taking any valid CSS color values for each stop property. */
gradientDark?: {
stop1?: string;
stop2?: string;
stop3?: string;
};
/** Flag indicating whether glass styles are removed from the hero when a glass theme is applied. */
hasNoGlass?: boolean;
/** Modifies the width of the hero body. */
bodyWidth?: string;
/** Modifies the max-width of the hero body. */
bodyMaxWidth?: string;
}

export const Hero: React.FunctionComponent<HeroProps> = ({
className,
children,
backgroundSrcLight,
backgroundSrcDark,
gradientLight,
gradientDark,
hasNoGlass = false,
bodyWidth,
bodyMaxWidth,
...props
}) => {
const customStyles: { [key: string]: string } = {};
if (backgroundSrcLight) {
customStyles[heroBackgroundImageLight.name] = `url(${backgroundSrcLight})`;
}
if (backgroundSrcDark) {
customStyles[heroBackgroundImageDark.name] = `url(${backgroundSrcDark})`;
}

if (gradientLight?.stop1) {
customStyles[heroGradientStop1Light.name] = gradientLight.stop1;
}
if (gradientLight?.stop2) {
customStyles[heroGradientStop2Light.name] = gradientLight.stop2;
}
if (gradientLight?.stop3) {
customStyles[heroGradientStop3Light.name] = gradientLight.stop3;
}
if (gradientDark?.stop1) {
customStyles[heroGradientStop1Dark.name] = gradientDark.stop1;
}
if (gradientDark?.stop2) {
customStyles[heroGradientStop2Dark.name] = gradientDark.stop2;
}
if (gradientDark?.stop3) {
customStyles[heroGradientStop3Dark.name] = gradientDark.stop3;
}

if (bodyWidth) {
customStyles[heroBodyWidth.name] = bodyWidth;
}
if (bodyMaxWidth) {
customStyles[heroBodyMaxWidth.name] = bodyMaxWidth;
}

return (
<div
className={css(styles.hero, hasNoGlass && styles.modifiers.noGlass, className)}
style={{ ...props.style, ...customStyles }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @mcoker Do we need to set the two hero body widths on the hero body element or is it fine to set them all on the hero element? Just wanted to check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way. I would probably put them on the hero wrapper since that's where we suggest consumers put their customizations*.

* To expand on that, this is the hero component CSS var block. It renders this in the CSS:

Screenshot 2025-11-11 at 12 55 19 PM

The way we want consumers to customize components is the same way. In their CSS that loads after patternfly CSS (after so that it overrides ours), they should write something like:

.pf-v6-c-hero {
  --pf-v6-c-hero--BackgroundColor: lemonchiffon;
  --pf-v6-c-hero--BorderStyle: dotted;
  --pf-v6-c-hero__body--MaxWidth: 42 parsecs;
  ... and so on
}

So when we have a component prop that customizes some CSS var for that component, we're effectively just doing that customization for them, so you'd do it the same way.

You don't have to though if it's difficult - like a nested thing where it isn't easy to pass a style object back up to a parent wrapper. Another good use case for adding the vars directly to a hero body would be - if we went back to having a HeroBody component and we supported multiple HeroBodys inside of Hero, then we/users would set the --Width/MaxWidth vars on each HeroBody to customize them individually. If you wanted them all to be the same, you'd put the customization at the component wrapper level.

{...props}
>
<div className={css(styles.heroBody)}>{children}</div>
</div>
);
};

Hero.displayName = 'Hero';
Loading
Loading