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
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.78",
"@patternfly/patternfly": "6.5.0-prerelease.80",
"case-anything": "^3.1.2",
"css": "^3.0.0",
"fs-extra": "^11.3.3"
Expand Down
10 changes: 10 additions & 0 deletions packages/react-core/src/components/Page/PageGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface PageGroupProps extends React.HTMLProps<HTMLDivElement> {
xl?: 'top' | 'bottom';
'2xl'?: 'top' | 'bottom';
};
/** @beta Applies the base sticky positioning to the top or bottom of the scroll parent container. */
stickyBase?: 'top' | 'bottom';
/** @beta Flag indicating if the group has stuck styling, applied when the group is not at the edge of the scroll parent container. */
isStickyStuck?: boolean;
/** Enables the page group to fill the available vertical space if true, or disable filling if false. */
isFilled?: boolean;
/** Modifier indicating if PageGroup should have a shadow at the top */
Expand All @@ -37,6 +41,8 @@ export const PageGroup = ({
className = '',
children,
stickyOnBreakpoint,
stickyBase,
isStickyStuck = false,
isFilled,
hasShadowTop = false,
hasShadowBottom = false,
Expand All @@ -61,6 +67,10 @@ export const PageGroup = ({
className={css(
styles.pageMainGroup,
formatBreakpointMods(stickyOnBreakpoint, styles, 'sticky-', getVerticalBreakpoint(height), true),
stickyBase === 'top' && styles.modifiers.stickyTopBase,
stickyBase === 'bottom' && styles.modifiers.stickyBottomBase,
isStickyStuck && stickyBase === 'top' && styles.modifiers.stickyTopStuck,
isStickyStuck && stickyBase === 'bottom' && styles.modifiers.stickyBottomStuck,
isFilled === false && styles.modifiers.noFill,
isFilled === true && styles.modifiers.fill,
hasShadowTop && styles.modifiers.shadowTop,
Expand Down
10 changes: 10 additions & 0 deletions packages/react-core/src/components/Page/PageSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export interface PageSectionProps extends React.HTMLProps<HTMLDivElement> {
xl?: 'top' | 'bottom';
'2xl'?: 'top' | 'bottom';
};
/** @beta Applies the base sticky positioning to the top or bottom of the scroll parent container. */
stickyBase?: 'top' | 'bottom';
/** @beta Flag indicating if the section has stuck styling, applied when the section is not at the edge of the scroll parent container. */
isStickyStuck?: boolean;
/** Modifier indicating if PageSection should have a shadow at the top */
hasShadowTop?: boolean;
/** Modifier indicating if PageSection should have a shadow at the bottom */
Expand Down Expand Up @@ -96,6 +100,8 @@ export const PageSection: React.FunctionComponent<PageSectionProps> = ({
isWidthLimited = false,
isCenterAligned = false,
stickyOnBreakpoint,
stickyBase,
isStickyStuck = false,
hasShadowTop = false,
hasShadowBottom = false,
hasOverflowScroll = false,
Expand Down Expand Up @@ -124,6 +130,10 @@ export const PageSection: React.FunctionComponent<PageSectionProps> = ({
variantType[type],
formatBreakpointMods(padding, styles),
formatBreakpointMods(stickyOnBreakpoint, styles, 'sticky-', getVerticalBreakpoint(height), true),
stickyBase === 'top' && styles.modifiers.stickyTopBase,
stickyBase === 'bottom' && styles.modifiers.stickyBottomBase,
isStickyStuck && stickyBase === 'top' && styles.modifiers.stickyTopStuck,
isStickyStuck && stickyBase === 'bottom' && styles.modifiers.stickyBottomStuck,
type === PageSectionTypes.default && variantStyle[variant],
isFilled === false && styles.modifiers.noFill,
isFilled === true && styles.modifiers.fill,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,55 @@ test(`Renders with ${styles.modifiers.noPlainOnGlass} class when isNoPlainOnGlas

expect(screen.getByText('test')).toHaveClass(styles.modifiers.noPlainOnGlass);
});

test(`Does not add sticky base or sticky stuck classes by default`, () => {
render(<PageGroup>test</PageGroup>);
const group = screen.getByText('test');
expect(group).not.toHaveClass(styles.modifiers.stickyTopBase);
expect(group).not.toHaveClass(styles.modifiers.stickyBottomBase);
expect(group).not.toHaveClass(styles.modifiers.stickyTopStuck);
expect(group).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Adds ${styles.modifiers.stickyTopBase} without stuck class when stickyBase="top"`, () => {
render(<PageGroup stickyBase="top">test</PageGroup>);
const group = screen.getByText('test');
expect(group).toHaveClass(styles.modifiers.stickyTopBase);
expect(group).not.toHaveClass(styles.modifiers.stickyTopStuck);
});

test(`Adds ${styles.modifiers.stickyBottomBase} without stuck class when stickyBase="bottom"`, () => {
render(<PageGroup stickyBase="bottom">test</PageGroup>);
const group = screen.getByText('test');
expect(group).toHaveClass(styles.modifiers.stickyBottomBase);
expect(group).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Adds ${styles.modifiers.stickyTopStuck} when stickyBase="top" and isStickyStuck`, () => {
render(
<PageGroup stickyBase="top" isStickyStuck>
test
</PageGroup>
);
const group = screen.getByText('test');
expect(group).toHaveClass(styles.modifiers.stickyTopBase);
expect(group).toHaveClass(styles.modifiers.stickyTopStuck);
});

test(`Adds ${styles.modifiers.stickyBottomStuck} when stickyBase="bottom" and isStickyStuck`, () => {
render(
<PageGroup stickyBase="bottom" isStickyStuck>
test
</PageGroup>
);
const group = screen.getByText('test');
expect(group).toHaveClass(styles.modifiers.stickyBottomBase);
expect(group).toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Does not add stuck class when isStickyStuck is true but stickyBase is not set`, () => {
render(<PageGroup isStickyStuck>test</PageGroup>);
const group = screen.getByText('test');
expect(group).not.toHaveClass(styles.modifiers.stickyTopStuck);
expect(group).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,67 @@ test(`Renders with ${styles.modifiers.noPlainOnGlass} class when isNoPlainOnGlas

expect(screen.getByText('test')).toHaveClass(styles.modifiers.noPlainOnGlass);
});

test(`Does not add sticky base or sticky stuck classes by default`, () => {
render(<PageSection component="main">test</PageSection>);
const section = screen.getByRole('main');
expect(section).not.toHaveClass(styles.modifiers.stickyTopBase);
expect(section).not.toHaveClass(styles.modifiers.stickyBottomBase);
expect(section).not.toHaveClass(styles.modifiers.stickyTopStuck);
expect(section).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Adds ${styles.modifiers.stickyTopBase} without stuck class when stickyBase="top"`, () => {
render(
<PageSection component="main" stickyBase="top">
test
</PageSection>
);
const section = screen.getByRole('main');
expect(section).toHaveClass(styles.modifiers.stickyTopBase);
expect(section).not.toHaveClass(styles.modifiers.stickyTopStuck);
});

test(`Adds ${styles.modifiers.stickyBottomBase} without stuck class when stickyBase="bottom"`, () => {
render(
<PageSection component="main" stickyBase="bottom">
test
</PageSection>
);
const section = screen.getByRole('main');
expect(section).toHaveClass(styles.modifiers.stickyBottomBase);
expect(section).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Adds ${styles.modifiers.stickyTopStuck} when stickyBase="top" and isStickyStuck`, () => {
render(
<PageSection component="main" stickyBase="top" isStickyStuck>
test
</PageSection>
);
const section = screen.getByRole('main');
expect(section).toHaveClass(styles.modifiers.stickyTopBase);
expect(section).toHaveClass(styles.modifiers.stickyTopStuck);
});

test(`Adds ${styles.modifiers.stickyBottomStuck} when stickyBase="bottom" and isStickyStuck`, () => {
render(
<PageSection component="main" stickyBase="bottom" isStickyStuck>
test
</PageSection>
);
const section = screen.getByRole('main');
expect(section).toHaveClass(styles.modifiers.stickyBottomBase);
expect(section).toHaveClass(styles.modifiers.stickyBottomStuck);
});

test(`Does not add stuck class when isStickyStuck is true but stickyBase is not set`, () => {
render(
<PageSection component="main" isStickyStuck>
test
</PageSection>
);
const section = screen.getByRole('main');
expect(section).not.toHaveClass(styles.modifiers.stickyTopStuck);
expect(section).not.toHaveClass(styles.modifiers.stickyBottomStuck);
});
12 changes: 11 additions & 1 deletion packages/react-core/src/components/Page/examples/Page.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ propComponents:
['Page', 'PageSidebar', 'PageSidebarBody', 'PageSection', 'PageGroup', 'PageBreadcrumb', 'PageToggleButton']
---

import { useState } from 'react';
import { useState, useLayoutEffect, useRef } from 'react';
import BarsIcon from '@patternfly/react-icons/dist/js/icons/bars-icon';
import pageSectionWidthLimitMaxWidth from '@patternfly/react-tokens/dist/esm/c_page_section_m_limit_width_MaxWidth';

Expand Down Expand Up @@ -131,3 +131,13 @@ To remove the default background color from a page section or group, use the `is
```ts file="./PagePlainSections.tsx"

```

### Dynamic sticky section

A page section may be made sticky with separate control of its sticky positioning and stuck styling using the `stickyBase` and `isStickyStuck` properties. The `stickyBase` property accepts a value of `"top"` or `"bottom"` and applies the base sticky positioning in the given direction. The `isStickyStuck` property applies visual "stuck" styling such as a background, box shadow, and border, and should be toggled based on the scroll position of the scroll parent container.

In this example, a scroll event listener on the scroll parent container toggles `isStickyStuck` when `scrollTop > 0`, so the stuck styling appears only when the content is scrolled.

```ts file="./PageDynamicStickySection.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useLayoutEffect, useState, useRef } from 'react';
import {
Page,
Masthead,
MastheadMain,
MastheadBrand,
MastheadLogo,
MastheadContent,
PageSection,
Toolbar,
ToolbarContent,
ToolbarItem,
Breadcrumb,
BreadcrumbItem,
Content
} from '@patternfly/react-core';

const useIsStuckFromScrollParent = ({
shouldTrack,
scrollParentRef
}: {
shouldTrack: boolean;
scrollParentRef: React.RefObject<any>;
}): boolean => {
const [isStuck, setIsStuck] = useState(false);

useLayoutEffect(() => {
if (!shouldTrack) {
setIsStuck(false);
return;
}

const scrollElement = scrollParentRef.current;
if (!scrollElement) {
setIsStuck(false);
return;
}

const syncFromScroll = () => {
setIsStuck(scrollElement.scrollTop > 0);
};
syncFromScroll();
scrollElement.addEventListener('scroll', syncFromScroll, { passive: true });
return () => scrollElement.removeEventListener('scroll', syncFromScroll);
}, [shouldTrack, scrollParentRef]);

return isStuck;
};

export const PageDynamicStickySection: React.FunctionComponent = () => {
const scrollParentRef = useRef<HTMLDivElement>(null);
const isStickyStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef });

const headerToolbar = (
<Toolbar id="dynamic-sticky-toolbar">
<ToolbarContent>
<ToolbarItem>header-tools</ToolbarItem>
</ToolbarContent>
</Toolbar>
);

const masthead = (
<Masthead>
<MastheadMain>
<MastheadBrand>
<MastheadLogo href="https://patternfly.org" target="_blank">
Logo
</MastheadLogo>
</MastheadBrand>
</MastheadMain>
<MastheadContent>{headerToolbar}</MastheadContent>
</Masthead>
);

return (
<Page masthead={masthead}>
<div ref={scrollParentRef} style={{ overflowY: 'auto', height: '100%' }}>
<PageSection type="breadcrumb" stickyBase="top" isStickyStuck={isStickyStuck}>
<Breadcrumb>
<BreadcrumbItem>Section home</BreadcrumbItem>
<BreadcrumbItem to="#">Section title</BreadcrumbItem>
<BreadcrumbItem to="#" isActive>
Section landing
</BreadcrumbItem>
</Breadcrumb>
</PageSection>
<PageSection>
<Content>
<h1>Main title</h1>
<p>
Scroll the container to see the breadcrumb section above dynamically apply its stuck styling. The section
uses <code>stickyBase=&quot;top&quot;</code> to remain fixed at the top of the scroll parent, and{' '}
<code>isStickyStuck</code> is toggled via a scroll event listener to apply visual styling when the section
is no longer at the top edge.
</p>
</Content>
</PageSection>
{Array.from({ length: 30 }, (_, i) => (
<PageSection key={i} variant={i % 2 === 0 ? 'default' : 'secondary'}>
<Content>
<p>{`Section ${i + 1} content`}</p>
</Content>
</PageSection>
))}
</div>
</Page>
);
};
2 changes: 1 addition & 1 deletion packages/react-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
},
"dependencies": {
"@patternfly/patternfly": "6.5.0-prerelease.78",
"@patternfly/patternfly": "6.5.0-prerelease.80",
"@patternfly/react-charts": "workspace:^",
"@patternfly/react-code-editor": "workspace:^",
"@patternfly/react-core": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@patternfly/patternfly": "6.5.0-prerelease.78",
"@patternfly/patternfly": "6.5.0-prerelease.80",
"@rhds/icons": "^2.2.0",
"fs-extra": "^11.3.3",
"tslib": "^2.8.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/react-styles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"clean": "rimraf dist css"
},
"devDependencies": {
"@patternfly/patternfly": "6.5.0-prerelease.78",
"@patternfly/patternfly": "6.5.0-prerelease.80",
"change-case": "^5.4.4",
"fs-extra": "^11.3.3"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"devDependencies": {
"@adobe/css-tools": "^4.4.4",
"@patternfly/patternfly": "6.5.0-prerelease.78",
"@patternfly/patternfly": "6.5.0-prerelease.80",
"fs-extra": "^11.3.3"
}
}
Loading
Loading