From 7fc5b7c562eb8f91445f8bfd8caebe0077b9c6a7 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Thu, 23 Apr 2026 10:33:15 -0400 Subject: [PATCH 1/4] feat(Toolbar): dynamic sticky --- .../src/components/Toolbar/Toolbar.tsx | 10 ++- .../Toolbar/__tests__/Toolbar.test.tsx | 37 ++++++++- .../components/Toolbar/examples/Toolbar.md | 12 ++- .../Toolbar/examples/ToolbarDynamicSticky.tsx | 75 +++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx diff --git a/packages/react-core/src/components/Toolbar/Toolbar.tsx b/packages/react-core/src/components/Toolbar/Toolbar.tsx index 78f21c60ce0..5a0cf48de75 100644 --- a/packages/react-core/src/components/Toolbar/Toolbar.tsx +++ b/packages/react-core/src/components/Toolbar/Toolbar.tsx @@ -38,8 +38,12 @@ export interface ToolbarProps extends React.HTMLProps, OUIAProps isFullHeight?: boolean; /** Flag indicating the toolbar is static */ isStatic?: boolean; - /** Flag indicating the toolbar should stick to the top of its container */ + /** Flag indicating the toolbar should stick to the top of its container. This property applies both the sticky position and styling. */ isSticky?: boolean; + /** @beta Flag indicating the toolbar should have sticky positioning to the top of its container */ + isStickyBase?: boolean; + /** @beta Flag indicating the toolbar should have stuck styling, when the toolbar is not at the top of the scroll container */ + isStickyStuck?: boolean; /** @beta Flag indicating the toolbar has a vertical orientation */ isVertical?: boolean; /** Insets at various breakpoints. */ @@ -144,6 +148,8 @@ class Toolbar extends Component { children, isFullHeight, isStatic, + isStickyBase, + isStickyStuck, inset, isSticky, isVertical, @@ -171,6 +177,8 @@ class Toolbar extends Component { isFullHeight && styles.modifiers.fullHeight, isStatic && styles.modifiers.static, isSticky && styles.modifiers.sticky, + isStickyBase && styles.modifiers.stickyBase, + isStickyStuck && styles.modifiers.stickyStuck, isVertical && styles.modifiers.vertical, formatBreakpointMods(inset, styles, '', getBreakpoint(width)), colorVariant === 'primary' && styles.modifiers.primary, diff --git a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx index fcd5c5b6219..b515b858436 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx @@ -220,7 +220,7 @@ describe('Toolbar', () => { expect(screen.getByTestId('Toolbar-test-is-not-vertical')).not.toHaveClass(styles.modifiers.vertical); }); - it('Renders with class ${styles.modifiers.vertical} when isVertical is true', () => { + it(`Renders with class ${styles.modifiers.vertical} when isVertical is true`, () => { const items = ( Test @@ -238,4 +238,39 @@ describe('Toolbar', () => { expect(screen.getByTestId('Toolbar-test-is-vertical')).toHaveClass(styles.modifiers.vertical); }); + + it(`Does not add ${styles.modifiers.stickyBase} and ${styles.modifiers.stickyStuck} classes by default`, () => { + render( + + + Test + + + ); + const el = screen.getByTestId('toolbar-sticky-default'); + expect(el).not.toHaveClass(styles.modifiers.stickyBase); + expect(el).not.toHaveClass(styles.modifiers.stickyStuck); + }); + + it(`Adds ${styles.modifiers.stickyBase} when isStickyBase is true`, () => { + render( + + + Test + + + ); + expect(screen.getByTestId('toolbar-sticky-base')).toHaveClass(styles.modifiers.stickyBase); + }); + + it(`Adds ${styles.modifiers.stickyStuck} when isStickyStuck is true`, () => { + render( + + + Test + + + ); + expect(screen.getByTestId('toolbar-sticky-stuck')).toHaveClass(styles.modifiers.stickyStuck); + }); }); diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 89639b516c7..18e6a527a20 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -5,7 +5,7 @@ propComponents: ['Toolbar', 'ToolbarContent', 'ToolbarGroup', 'ToolbarItem', 'To section: components --- -import { Fragment, useState } from 'react'; +import { Fragment, useState, useLayoutEffect, useRef } from 'react'; import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon'; import CloneIcon from '@patternfly/react-icons/dist/esm/icons/clone-icon'; @@ -44,6 +44,14 @@ In the following example, toggle the "is toolbar sticky" checkbox to see the dif ``` +### Dynamic sticky toolbar + +A toolbar may alternatively be made sticky with two properties: `isStickyBase` and `isStickyStuck` - which allows separate control of the sticky position and sticky styling respectively. + +```ts file="./ToolbarDynamicSticky.tsx" + +``` + ### With groups of items You can group similar items together to create desired associations and to enable items to respond to changes in viewport width together. @@ -114,11 +122,13 @@ When all of a toolbar's required elements cannot fit in a single line, you can s ``` ## Examples with spacers and wrapping + You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers. Items are spaced “16px” apart by default and can be modified by changing their or their parents' `gap`, `columnGap`, and `rowGap` properties. You can set the property values at multiple breakpoints, including "default", "md", "lg", "xl", and "2xl". ### Toolbar content wrapping + The toolbar content section will wrap by default, but you can set the `rowRap` property to `noWrap` to make it not wrap. ```ts file="./ToolbarContentWrap.tsx" diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx new file mode 100644 index 00000000000..46d268bd2ed --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx @@ -0,0 +1,75 @@ +import { useLayoutEffect, useState, useRef } from 'react'; +import { Toolbar, ToolbarItem, ToolbarContent, SearchInput, Checkbox } from '@patternfly/react-core'; + +const useIsStuckFromScrollParent = ({ + shouldTrack, + scrollParentRef +}: { + /** Indicates whether to track the scroll top position of the scroll parent element */ + shouldTrack: boolean; + /** Reference to the scroll parent element */ + scrollParentRef: React.RefObject; +}): 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 ToolbarDynamicSticky = () => { + const scrollParentRef = useRef(null); + const isStickyStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef }); + const [showEvenOnly, setShowEvenOnly] = useState(true); + const [searchValue, setSearchValue] = useState(''); + const array = Array.from(Array(30), (_, x) => x); // create array of numbers from 1-30 for demo purposes + const numbers = showEvenOnly ? array.filter((number) => number % 2 === 0) : array; + + return ( +
+ + + + setSearchValue(value)} + onClear={() => setSearchValue('')} + /> + + + setShowEvenOnly(checked)} + id="showOnlyEvenCheckbox" + /> + + + +
    + {numbers.map((number) => ( +
  • {`item ${number}`}
  • + ))} +
+
+ ); +}; From b1a90d4aa19018cbc7c7689741a0b366a802e886 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 24 Apr 2026 12:52:34 -0400 Subject: [PATCH 2/4] add id to example --- .../src/components/Toolbar/examples/ToolbarDynamicSticky.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx index 46d268bd2ed..89272966a43 100644 --- a/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx @@ -44,7 +44,7 @@ export const ToolbarDynamicSticky = () => { const numbers = showEvenOnly ? array.filter((number) => number % 2 === 0) : array; return ( -
+
From 514cf530bed54ae15074afa0264e4c015c11900d Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 24 Apr 2026 12:54:01 -0400 Subject: [PATCH 3/4] update example description --- packages/react-core/src/components/Toolbar/examples/Toolbar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 18e6a527a20..8554fcb83a2 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -46,7 +46,7 @@ In the following example, toggle the "is toolbar sticky" checkbox to see the dif ### Dynamic sticky toolbar -A toolbar may alternatively be made sticky with two properties: `isStickyBase` and `isStickyStuck` - which allows separate control of the sticky position and sticky styling respectively. +A toolbar may alternatively be made sticky with two properties: `isStickyBase` and `isStickyStuck` - which allows separate control of the sticky position and sticky styling respectively. In this example, `isStickyStuck` is only applied when the sticky element is not at the top of the scroll parent container. ```ts file="./ToolbarDynamicSticky.tsx" From 8007785e3f9e5009178e3197a54eefa3f380d650 Mon Sep 17 00:00:00 2001 From: Michael Coker <35148959+mcoker@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:53:21 -0500 Subject: [PATCH 4/4] Update packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx --- .../src/components/Toolbar/examples/ToolbarDynamicSticky.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx index 89272966a43..2bfb8240553 100644 --- a/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx @@ -45,7 +45,7 @@ export const ToolbarDynamicSticky = () => { return (
- +