Skip to content

Commit cb3856a

Browse files
kmcfaulmcoker
andauthored
feat(Toolbar): dynamic sticky (#12375)
* feat(Toolbar): dynamic sticky * add id to example * update example description * Update packages/react-core/src/components/Toolbar/examples/ToolbarDynamicSticky.tsx --------- Co-authored-by: Michael Coker <35148959+mcoker@users.noreply.github.com>
1 parent 96c60fb commit cb3856a

4 files changed

Lines changed: 131 additions & 3 deletions

File tree

packages/react-core/src/components/Toolbar/Toolbar.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ export interface ToolbarProps extends React.HTMLProps<HTMLDivElement>, OUIAProps
3838
isFullHeight?: boolean;
3939
/** Flag indicating the toolbar is static */
4040
isStatic?: boolean;
41-
/** Flag indicating the toolbar should stick to the top of its container */
41+
/** Flag indicating the toolbar should stick to the top of its container. This property applies both the sticky position and styling. */
4242
isSticky?: boolean;
43+
/** @beta Flag indicating the toolbar should have sticky positioning to the top of its container */
44+
isStickyBase?: boolean;
45+
/** @beta Flag indicating the toolbar should have stuck styling, when the toolbar is not at the top of the scroll container */
46+
isStickyStuck?: boolean;
4347
/** @beta Flag indicating the toolbar has a vertical orientation */
4448
isVertical?: boolean;
4549
/** Insets at various breakpoints. */
@@ -144,6 +148,8 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
144148
children,
145149
isFullHeight,
146150
isStatic,
151+
isStickyBase,
152+
isStickyStuck,
147153
inset,
148154
isSticky,
149155
isVertical,
@@ -171,6 +177,8 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
171177
isFullHeight && styles.modifiers.fullHeight,
172178
isStatic && styles.modifiers.static,
173179
isSticky && styles.modifiers.sticky,
180+
isStickyBase && styles.modifiers.stickyBase,
181+
isStickyStuck && styles.modifiers.stickyStuck,
174182
isVertical && styles.modifiers.vertical,
175183
formatBreakpointMods(inset, styles, '', getBreakpoint(width)),
176184
colorVariant === 'primary' && styles.modifiers.primary,

packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ describe('Toolbar', () => {
220220
expect(screen.getByTestId('Toolbar-test-is-not-vertical')).not.toHaveClass(styles.modifiers.vertical);
221221
});
222222

223-
it('Renders with class ${styles.modifiers.vertical} when isVertical is true', () => {
223+
it(`Renders with class ${styles.modifiers.vertical} when isVertical is true`, () => {
224224
const items = (
225225
<Fragment>
226226
<ToolbarItem>Test</ToolbarItem>
@@ -238,4 +238,39 @@ describe('Toolbar', () => {
238238

239239
expect(screen.getByTestId('Toolbar-test-is-vertical')).toHaveClass(styles.modifiers.vertical);
240240
});
241+
242+
it(`Does not add ${styles.modifiers.stickyBase} and ${styles.modifiers.stickyStuck} classes by default`, () => {
243+
render(
244+
<Toolbar data-testid="toolbar-sticky-default">
245+
<ToolbarContent>
246+
<ToolbarItem>Test</ToolbarItem>
247+
</ToolbarContent>
248+
</Toolbar>
249+
);
250+
const el = screen.getByTestId('toolbar-sticky-default');
251+
expect(el).not.toHaveClass(styles.modifiers.stickyBase);
252+
expect(el).not.toHaveClass(styles.modifiers.stickyStuck);
253+
});
254+
255+
it(`Adds ${styles.modifiers.stickyBase} when isStickyBase is true`, () => {
256+
render(
257+
<Toolbar data-testid="toolbar-sticky-base" isStickyBase>
258+
<ToolbarContent>
259+
<ToolbarItem>Test</ToolbarItem>
260+
</ToolbarContent>
261+
</Toolbar>
262+
);
263+
expect(screen.getByTestId('toolbar-sticky-base')).toHaveClass(styles.modifiers.stickyBase);
264+
});
265+
266+
it(`Adds ${styles.modifiers.stickyStuck} when isStickyStuck is true`, () => {
267+
render(
268+
<Toolbar data-testid="toolbar-sticky-stuck" isStickyStuck>
269+
<ToolbarContent>
270+
<ToolbarItem>Test</ToolbarItem>
271+
</ToolbarContent>
272+
</Toolbar>
273+
);
274+
expect(screen.getByTestId('toolbar-sticky-stuck')).toHaveClass(styles.modifiers.stickyStuck);
275+
});
241276
});

packages/react-core/src/components/Toolbar/examples/Toolbar.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ propComponents: ['Toolbar', 'ToolbarContent', 'ToolbarGroup', 'ToolbarItem', 'To
55
section: components
66
---
77

8-
import { Fragment, useState } from 'react';
8+
import { Fragment, useState, useLayoutEffect, useRef } from 'react';
99

1010
import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon';
1111
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
4444

4545
```
4646

47+
### Dynamic sticky toolbar
48+
49+
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.
50+
51+
```ts file="./ToolbarDynamicSticky.tsx"
52+
53+
```
54+
4755
### With groups of items
4856

4957
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
114122
```
115123

116124
## Examples with spacers and wrapping
125+
117126
You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers.
118127

119128
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".
120129

121130
### Toolbar content wrapping
131+
122132
The toolbar content section will wrap by default, but you can set the `rowRap` property to `noWrap` to make it not wrap.
123133

124134
```ts file="./ToolbarContentWrap.tsx"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useLayoutEffect, useState, useRef } from 'react';
2+
import { Toolbar, ToolbarItem, ToolbarContent, SearchInput, Checkbox } from '@patternfly/react-core';
3+
4+
const useIsStuckFromScrollParent = ({
5+
shouldTrack,
6+
scrollParentRef
7+
}: {
8+
/** Indicates whether to track the scroll top position of the scroll parent element */
9+
shouldTrack: boolean;
10+
/** Reference to the scroll parent element */
11+
scrollParentRef: React.RefObject<any>;
12+
}): boolean => {
13+
const [isStuck, setIsStuck] = useState(false);
14+
15+
useLayoutEffect(() => {
16+
if (!shouldTrack) {
17+
setIsStuck(false);
18+
return;
19+
}
20+
21+
const scrollElement = scrollParentRef.current;
22+
if (!scrollElement) {
23+
setIsStuck(false);
24+
return;
25+
}
26+
27+
const syncFromScroll = () => {
28+
setIsStuck(scrollElement.scrollTop > 0);
29+
};
30+
syncFromScroll();
31+
scrollElement.addEventListener('scroll', syncFromScroll, { passive: true });
32+
return () => scrollElement.removeEventListener('scroll', syncFromScroll);
33+
}, [shouldTrack, scrollParentRef]);
34+
35+
return isStuck;
36+
};
37+
38+
export const ToolbarDynamicSticky = () => {
39+
const scrollParentRef = useRef<HTMLDivElement>(null);
40+
const isStickyStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef });
41+
const [showEvenOnly, setShowEvenOnly] = useState(true);
42+
const [searchValue, setSearchValue] = useState('');
43+
const array = Array.from(Array(30), (_, x) => x); // create array of numbers from 1-30 for demo purposes
44+
const numbers = showEvenOnly ? array.filter((number) => number % 2 === 0) : array;
45+
46+
return (
47+
<div id="dynamic-sticky-scroll-parent" ref={scrollParentRef} style={{ overflowY: 'scroll', height: '200px' }}>
48+
<Toolbar id="toolbar-dynamic-sticky" inset={{ default: 'insetNone' }} isStickyBase isStickyStuck={isStickyStuck}>
49+
<ToolbarContent>
50+
<ToolbarItem>
51+
<SearchInput
52+
aria-label="Sticky example search input"
53+
value={searchValue}
54+
onChange={(_event, value) => setSearchValue(value)}
55+
onClear={() => setSearchValue('')}
56+
/>
57+
</ToolbarItem>
58+
<ToolbarItem alignSelf="center">
59+
<Checkbox
60+
label="Show only even number items"
61+
isChecked={showEvenOnly}
62+
onChange={(_event, checked) => setShowEvenOnly(checked)}
63+
id="showOnlyEvenCheckbox"
64+
/>
65+
</ToolbarItem>
66+
</ToolbarContent>
67+
</Toolbar>
68+
<ul>
69+
{numbers.map((number) => (
70+
<li key={number}>{`item ${number}`}</li>
71+
))}
72+
</ul>
73+
</div>
74+
);
75+
};

0 commit comments

Comments
 (0)