From 4e5d3640f1603894900653ee01a8d7fa1bbd0546 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 b38a419fa761b2a12004ec34c658de358eab0030 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 24 Apr 2026 12:49:51 -0400 Subject: [PATCH 2/4] bump core to .77 --- packages/react-core/package.json | 2 +- packages/react-docs/package.json | 2 +- packages/react-icons/package.json | 2 +- packages/react-styles/package.json | 2 +- packages/react-tokens/package.json | 2 +- yarn.lock | 25 +++++++++---------------- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 66c3c96bad8..316af25fd83 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -54,7 +54,7 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@patternfly/patternfly": "6.5.0-prerelease.72", + "@patternfly/patternfly": "6.5.0-prerelease.77", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.3" diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index d0c59f47275..6163b12700d 100644 --- a/packages/react-docs/package.json +++ b/packages/react-docs/package.json @@ -23,7 +23,7 @@ "test:a11y": "patternfly-a11y --config patternfly-a11y.config" }, "dependencies": { - "@patternfly/patternfly": "6.5.0-prerelease.72", + "@patternfly/patternfly": "6.5.0-prerelease.77", "@patternfly/react-charts": "workspace:^", "@patternfly/react-code-editor": "workspace:^", "@patternfly/react-core": "workspace:^", diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 0c1b3776c46..5302b05f4ee 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -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.71", + "@patternfly/patternfly": "6.5.0-prerelease.77", "@rhds/icons": "^2.1.0", "fs-extra": "^11.3.3", "tslib": "^2.8.1" diff --git a/packages/react-styles/package.json b/packages/react-styles/package.json index 9a8d7f8b9d0..bc2161351fe 100644 --- a/packages/react-styles/package.json +++ b/packages/react-styles/package.json @@ -19,7 +19,7 @@ "clean": "rimraf dist css" }, "devDependencies": { - "@patternfly/patternfly": "6.5.0-prerelease.72", + "@patternfly/patternfly": "6.5.0-prerelease.77", "change-case": "^5.4.4", "fs-extra": "^11.3.3" }, diff --git a/packages/react-tokens/package.json b/packages/react-tokens/package.json index f42ba24a8a2..2979ed84bd4 100644 --- a/packages/react-tokens/package.json +++ b/packages/react-tokens/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@adobe/css-tools": "^4.4.4", - "@patternfly/patternfly": "6.5.0-prerelease.72", + "@patternfly/patternfly": "6.5.0-prerelease.77", "fs-extra": "^11.3.3" } } diff --git a/yarn.lock b/yarn.lock index 202af09e172..db67cae33af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5070,17 +5070,10 @@ __metadata: languageName: node linkType: hard -"@patternfly/patternfly@npm:6.5.0-prerelease.71": - version: 6.5.0-prerelease.71 - resolution: "@patternfly/patternfly@npm:6.5.0-prerelease.71" - checksum: 10c0/08ab7878527666704ae99f5250e1a0446959143f744d4f0f9508aeb7e2a143dd088b8dc6bb4b0ca6679e9923d5c7cd8068d176070bce7b0c7992c48a865864d9 - languageName: node - linkType: hard - -"@patternfly/patternfly@npm:6.5.0-prerelease.72": - version: 6.5.0-prerelease.72 - resolution: "@patternfly/patternfly@npm:6.5.0-prerelease.72" - checksum: 10c0/b8d92a11b287d06efad3f410eb356ad60cbbdac3fbcb1e58a99c792253f81e7fcf154f57b0e5a40fbb9d7361364dab02925ecf21db2877e643a23101b8b0745a +"@patternfly/patternfly@npm:6.5.0-prerelease.77": + version: 6.5.0-prerelease.77 + resolution: "@patternfly/patternfly@npm:6.5.0-prerelease.77" + checksum: 10c0/0311dff0addde8da28ba9245cbffb230bfa7571d38b4a5e3c6a366aed8009a6774f5097c33b453312722afbdddb31ddb0d11659d4a6f56c294912e81487d8fe2 languageName: node linkType: hard @@ -5178,7 +5171,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-core@workspace:packages/react-core" dependencies: - "@patternfly/patternfly": "npm:6.5.0-prerelease.72" + "@patternfly/patternfly": "npm:6.5.0-prerelease.77" "@patternfly/react-icons": "workspace:^" "@patternfly/react-styles": "workspace:^" "@patternfly/react-tokens": "workspace:^" @@ -5199,7 +5192,7 @@ __metadata: resolution: "@patternfly/react-docs@workspace:packages/react-docs" dependencies: "@patternfly/documentation-framework": "npm:^6.36.8" - "@patternfly/patternfly": "npm:6.5.0-prerelease.72" + "@patternfly/patternfly": "npm:6.5.0-prerelease.77" "@patternfly/patternfly-a11y": "npm:5.1.0" "@patternfly/react-charts": "workspace:^" "@patternfly/react-code-editor": "workspace:^" @@ -5239,7 +5232,7 @@ __metadata: "@fortawesome/free-brands-svg-icons": "npm:^5.15.4" "@fortawesome/free-regular-svg-icons": "npm:^5.15.4" "@fortawesome/free-solid-svg-icons": "npm:^5.15.4" - "@patternfly/patternfly": "npm:6.5.0-prerelease.71" + "@patternfly/patternfly": "npm:6.5.0-prerelease.77" "@rhds/icons": "npm:^2.1.0" fs-extra: "npm:^11.3.3" tslib: "npm:^2.8.1" @@ -5326,7 +5319,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-styles@workspace:packages/react-styles" dependencies: - "@patternfly/patternfly": "npm:6.5.0-prerelease.72" + "@patternfly/patternfly": "npm:6.5.0-prerelease.77" change-case: "npm:^5.4.4" fs-extra: "npm:^11.3.3" languageName: unknown @@ -5368,7 +5361,7 @@ __metadata: resolution: "@patternfly/react-tokens@workspace:packages/react-tokens" dependencies: "@adobe/css-tools": "npm:^4.4.4" - "@patternfly/patternfly": "npm:6.5.0-prerelease.72" + "@patternfly/patternfly": "npm:6.5.0-prerelease.77" fs-extra: "npm:^11.3.3" languageName: unknown linkType: soft From 5f9bd340363e1b1a47a6527b8644e1fe7307b66f Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 24 Apr 2026 12:52:34 -0400 Subject: [PATCH 3/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 57df9be232bb727e919edc66bded360df543422d Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 24 Apr 2026 12:54:01 -0400 Subject: [PATCH 4/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"