Skip to content

Commit 7c4f9df

Browse files
feat(Pagination): added dynamic sticky and plain (#12388)
* feat(Pagination): added dynamic sticky and plain * Made util out of scroll helper in examples * Updated scroll logic, removed plain * Yarn lock * Made util helper back to inline code * Verbiage updates
1 parent b679641 commit 7c4f9df

12 files changed

Lines changed: 290 additions & 69 deletions

File tree

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.5.0-prerelease.78",
57+
"@patternfly/patternfly": "6.5.0-prerelease.79",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.3"

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,14 @@ export interface PaginationProps extends React.HTMLProps<HTMLDivElement>, OUIAPr
125125
isCompact?: boolean;
126126
/** Flag indicating if pagination should not be sticky on mobile. */
127127
isStatic?: boolean;
128-
/** Flag indicating if pagination should stick to its position (based on variant). */
128+
/** Flag indicating if pagination should stick to its position (based on variant). For dynamic sticky control, use isStickyBase
129+
* and isStickyStuck instead.
130+
*/
129131
isSticky?: boolean;
132+
/** @beta Flag indicating the pagination should have sticky positioning relative to its container. */
133+
isStickyBase?: boolean;
134+
/** @beta Flag indicating the pagination should have stuck styling, when the pagination is not at the top (for top variant) or bottom (for bottom variant) of the scroll container. */
135+
isStickyStuck?: boolean;
130136
/** Total number of items. */
131137
itemCount?: number;
132138
/** Last index of items on current page. */
@@ -191,8 +197,10 @@ export const Pagination: React.FunctionComponent<PaginationProps> = ({
191197
variant = PaginationVariant.top,
192198
isDisabled = false,
193199
isCompact = false,
194-
isSticky = false,
195200
isStatic = false,
201+
isSticky = false,
202+
isStickyBase = false,
203+
isStickyStuck = false,
196204
dropDirection: dropDirectionProp,
197205
toggleTemplate,
198206
perPage = defaultPerPageOptions[0].value,
@@ -290,7 +298,9 @@ export const Pagination: React.FunctionComponent<PaginationProps> = ({
290298
usePageInsets && styles.modifiers.pageInsets,
291299
formatBreakpointMods(inset, styles),
292300
isStatic && styles.modifiers.static,
293-
isSticky && styles.modifiers.sticky,
301+
isSticky && !isStickyBase && !isStickyStuck && styles.modifiers.sticky,
302+
isStickyBase && styles.modifiers.stickyBase,
303+
isStickyStuck && styles.modifiers.stickyStuck,
294304
className
295305
)}
296306
{...(widgetId && { id: `${widgetId}-${variant}-pagination` })}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
33

44
import { Pagination, PaginationVariant } from '../index';
55
import { KeyTypes } from '../../../helpers';
6+
import styles from '@patternfly/react-styles/css/components/Pagination/pagination';
67

78
describe('Pagination', () => {
89
describe('component render', () => {
@@ -117,6 +118,26 @@ describe('Pagination', () => {
117118
render(<Pagination data-testid="pagination-insets" usePageInsets />);
118119
expect(screen.getByTestId('pagination-insets')).toHaveClass('pf-m-page-insets');
119120
});
121+
122+
test(`should not render ${styles.modifiers.stickyBase} class by default`, () => {
123+
render(<Pagination data-testid="pagination-sticky-base-default" itemCount={20} />);
124+
expect(screen.getByTestId('pagination-sticky-base-default')).not.toHaveClass(styles.modifiers.stickyBase);
125+
});
126+
127+
test(`should render ${styles.modifiers.stickyBase} class when isStickyBase is true`, () => {
128+
render(<Pagination data-testid="pagination-sticky-base" itemCount={20} isStickyBase />);
129+
expect(screen.getByTestId('pagination-sticky-base')).toHaveClass(styles.modifiers.stickyBase);
130+
});
131+
132+
test(`should not render ${styles.modifiers.stickyStuck} class by default`, () => {
133+
render(<Pagination data-testid="pagination-sticky-stuck-default" itemCount={20} />);
134+
expect(screen.getByTestId('pagination-sticky-stuck-default')).not.toHaveClass(styles.modifiers.stickyStuck);
135+
});
136+
137+
test(`should render ${styles.modifiers.stickyStuck} class when isStickyStuck is true`, () => {
138+
render(<Pagination data-testid="pagination-sticky-stuck" itemCount={20} isStickyStuck />);
139+
expect(screen.getByTestId('pagination-sticky-stuck')).toHaveClass(styles.modifiers.stickyStuck);
140+
});
120141
});
121142

122143
describe('API', () => {

packages/react-core/src/components/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap

Lines changed: 42 additions & 42 deletions
Large diffs are not rendered by default.

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,84 @@ propComponents: ['Pagination', PaginationTitles, PerPageOptions, PaginationToggl
66
ouia: true
77
---
88

9-
import { Fragment, useState } from 'react';
9+
import { Fragment, useState, useRef, useLayoutEffect } from 'react';
1010

1111
## Examples
1212

1313
### Top
1414

1515
```ts file="./PaginationTop.tsx"
16+
1617
```
1718

1819
### Bottom
1920

2021
```ts file="./PaginationBottom.tsx"
22+
2123
```
2224

2325
### Indeterminate
2426

2527
By not passing `itemCount` and passing `toggleTemplate` you can customize the toggle with text.
2628

2729
```ts file="./PaginationIndeterminate.tsx"
30+
2831
```
2932

3033
### Disabled
3134

3235
```ts file="./PaginationDisabled.tsx"
36+
3337
```
3438

3539
### No items
3640

3741
```ts file="./PaginationNoItems.tsx"
42+
3843
```
3944

4045
### One page
4146

4247
```ts file="./PaginationOnePage.tsx"
48+
4349
```
4450

4551
### Compact
4652

4753
```ts file="./PaginationCompact.tsx"
54+
4855
```
4956

5057
### Offset
5158

5259
```ts file="./PaginationOffset.tsx"
60+
5361
```
5462

5563
### Sticky
5664

5765
```ts isFullscreen file="./PaginationSticky.tsx"
66+
67+
```
68+
69+
### Dynamic sticky top
70+
71+
Pagination can be made sticky via two properties: `isStickyBase`, which allows separate control of the sticky position, and `isStickyStuck`, which applies the sticky styling. In this example, `isStickyStuck` is only applied when the pagination is not at the top of the scroll parent container.
72+
73+
```ts isFullscreen file="./PaginationDynamicStickyTop.tsx"
74+
75+
```
76+
77+
### Dynamic sticky bottom
78+
79+
Bottom pagination can be made sticky via two properties: `isStickyBase`, which allows separate control of the sticky position, and `isStickyStuck`, which applies the sticky styling. In this example, `isStickyStuck` is only applied when the pagination is not at the bottom of the scroll parent container.
80+
81+
```ts isFullscreen file="./PaginationDynamicStickyBottom.tsx"
82+
5883
```
5984

6085
### Inset
6186

6287
```ts file="./PaginationInset.tsx"
88+
6389
```
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useLayoutEffect, useState, useRef } from 'react';
2+
import { Pagination, PaginationVariant, Gallery, GalleryItem, Card, CardBody } 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 + scrollElement.clientHeight < scrollElement.scrollHeight);
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 PaginationDynamicStickyBottom: React.FunctionComponent = () => {
39+
const scrollParentRef = useRef<HTMLDivElement>(null);
40+
const isStickyStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef });
41+
const [page, setPage] = useState(1);
42+
const [perPage, setPerPage] = useState(50);
43+
const itemCount = 523;
44+
45+
const onSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => {
46+
setPage(newPage);
47+
};
48+
49+
const onPerPageSelect = (
50+
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
51+
newPerPage: number,
52+
newPage: number
53+
) => {
54+
setPerPage(newPerPage);
55+
setPage(newPage);
56+
};
57+
58+
const buildCards = () => {
59+
const numberOfCards = (page - 1) * perPage + perPage - 1 >= itemCount ? itemCount - (page - 1) * perPage : perPage;
60+
61+
return Array.from({ length: numberOfCards }).map((_value, index) => (
62+
<GalleryItem key={index}>
63+
<Card>
64+
<CardBody>This is card {(page - 1) * perPage + index + 1}</CardBody>
65+
</Card>
66+
</GalleryItem>
67+
));
68+
};
69+
70+
return (
71+
<div ref={scrollParentRef} style={{ overflowY: 'scroll', height: '400px' }}>
72+
<Gallery hasGutter>{buildCards()}</Gallery>
73+
<Pagination
74+
itemCount={itemCount}
75+
perPage={perPage}
76+
page={page}
77+
onSetPage={onSetPage}
78+
widgetId="dynamic-sticky-bottom-example"
79+
onPerPageSelect={onPerPageSelect}
80+
variant={PaginationVariant.bottom}
81+
isStickyBase
82+
isStickyStuck={isStickyStuck}
83+
/>
84+
</div>
85+
);
86+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useLayoutEffect, useState, useRef } from 'react';
2+
import { Pagination, Gallery, GalleryItem, Card, CardBody } 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 PaginationDynamicStickyTop: React.FunctionComponent = () => {
39+
const scrollParentRef = useRef<HTMLDivElement>(null);
40+
const isStickyStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef });
41+
const [page, setPage] = useState(1);
42+
const [perPage, setPerPage] = useState(50);
43+
const itemCount = 523;
44+
45+
const onSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => {
46+
setPage(newPage);
47+
};
48+
49+
const onPerPageSelect = (
50+
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
51+
newPerPage: number,
52+
newPage: number
53+
) => {
54+
setPerPage(newPerPage);
55+
setPage(newPage);
56+
};
57+
58+
const buildCards = () => {
59+
const numberOfCards = (page - 1) * perPage + perPage - 1 >= itemCount ? itemCount - (page - 1) * perPage : perPage;
60+
61+
return Array.from({ length: numberOfCards }).map((_value, index) => (
62+
<GalleryItem key={index}>
63+
<Card>
64+
<CardBody>This is card {(page - 1) * perPage + index + 1}</CardBody>
65+
</Card>
66+
</GalleryItem>
67+
));
68+
};
69+
70+
return (
71+
<div ref={scrollParentRef} style={{ overflowY: 'scroll', height: '400px' }}>
72+
<Pagination
73+
itemCount={itemCount}
74+
perPage={perPage}
75+
page={page}
76+
onSetPage={onSetPage}
77+
widgetId="dynamic-sticky-top-example"
78+
onPerPageSelect={onPerPageSelect}
79+
isStickyBase
80+
isStickyStuck={isStickyStuck}
81+
/>
82+
<Gallery hasGutter>{buildCards()}</Gallery>
83+
</div>
84+
);
85+
};

packages/react-docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
2424
},
2525
"dependencies": {
26-
"@patternfly/patternfly": "6.5.0-prerelease.78",
26+
"@patternfly/patternfly": "6.5.0-prerelease.79",
2727
"@patternfly/react-charts": "workspace:^",
2828
"@patternfly/react-code-editor": "workspace:^",
2929
"@patternfly/react-core": "workspace:^",

packages/react-icons/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
"@fortawesome/free-brands-svg-icons": "^5.15.4",
3636
"@fortawesome/free-regular-svg-icons": "^5.15.4",
3737
"@fortawesome/free-solid-svg-icons": "^5.15.4",
38-
"@patternfly/patternfly": "6.5.0-prerelease.78",
39-
"@rhds/icons": "^2.2.0",
38+
"@patternfly/patternfly": "6.5.0-prerelease.79",
39+
"@rhds/icons": "^2.1.0",
4040
"fs-extra": "^11.3.3",
4141
"tslib": "^2.8.1"
4242
},

packages/react-styles/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"clean": "rimraf dist css"
2020
},
2121
"devDependencies": {
22-
"@patternfly/patternfly": "6.5.0-prerelease.78",
22+
"@patternfly/patternfly": "6.5.0-prerelease.79",
2323
"change-case": "^5.4.4",
2424
"fs-extra": "^11.3.3"
2525
},

0 commit comments

Comments
 (0)