Skip to content

feat: introduce InfiniteScroll component #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 24, 2023
Merged
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
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
} as Meta;

export const Playground: Story<ComponentProps> = (args) => <Component {...args} />;
Playground.storyName = 'Component';
Playground.storyName = 'Components/Component';
File renamed without changes.
12 changes: 12 additions & 0 deletions src/components/InfiniteScroll/InfiniteScroll.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use '../variables';

$block: '.#{variables.$ns}infinite-scroll';

#{$block} {
&__loader {
width: 100%;
padding: 60px 0 20px;
display: flex;
justify-content: center;
}
}
70 changes: 70 additions & 0 deletions src/components/InfiniteScroll/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, {useState, useRef, useEffect} from 'react';
import {Loader} from '@gravity-ui/uikit';

import {block} from '../utils/cn';
import {useIntersection} from './hooks/useIntersection';

import './InfiniteScroll.scss';

export interface InfiniteScrollProps {
/** When called shows loader and wait till the promise is fulfilled to hide loader */
onActivate: () => Promise<void>;
/** Turn off activation */
disabled: boolean;
children: React.ReactNode;
/** Custom loader */
loader?: React.ReactNode;
}

const b = block('infinite-scroll');

export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
onActivate,
disabled,
children,
loader,
}) => {
const [isActive, setIsActive] = useState(false);
const [bottomRef, setBottomRef] = useState<HTMLDivElement | null>(null);
const mounted = useRef(false);
const isBottomVisible = useIntersection(bottomRef);

useEffect(() => {
mounted.current = true;

return () => {
mounted.current = false;
};
}, []);

useEffect(() => {
const handleFetchData = async () => {
setIsActive(true);
await onActivate();

if (mounted.current) {
setIsActive(false);
}
};

if (isBottomVisible && !isActive && !disabled) {
handleFetchData();
}
}, [isBottomVisible, onActivate, isActive, disabled]);

const renderedLoader = loader || (
<div className={b('loader')}>
<Loader size="l" />
</div>
);

return (
<>
{children}

{isActive && renderedLoader}

<div className={b('intersector')} ref={setBottomRef} />
</>
);
};
24 changes: 24 additions & 0 deletions src/components/InfiniteScroll/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## InfiniteScroll

The component is useful for creating infinite lists like Social Network Feed, or events history. It renders its children. If you scroll it to the bottom, it will show a loader and call an `onActivate` callback. When all the data is loaded and there is no need to load more, pass the `disabled={true}` property.

### InfiniteScroll PropTypes

| Property | Type | Required | Default | Description |
| :--------- | :-------------------- | :------: | :-------------------- | :----------------------------------------------------------------------------- |
| onActivate | `() => Promise<void>` | `true` | | When called shows loader and wait till the promise is fulfilled to hide loader |
| disabled | `Boolean` | `true` | | Turn off activation |
| loader | `ReactNode` | `false` | `<Loader size="l" />` | Custom loader component |

### Example

```jsx
const {data, fetchNextPage} = useFeedQuery();
const isAllPostsShown = data.posts.length === data.total;

<InfiniteScroll onActivate={fetchNextPage} disabled={isAllPostsShown}>
{data.posts.map((post) => (
<Post key={post.id} post={post} />
))}
</InfiniteScroll>;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import {InfiniteScroll} from '../InfiniteScroll';
import {InfiniteScrollShowcase} from './InfiniteScrollShowcase';
import type {Meta, Story} from '@storybook/react';

export default {
title: 'Components/InfiniteScroll',
component: InfiniteScroll,
} as Meta;

export const Playground: Story = () => <InfiniteScrollShowcase />;
Playground.storyName = 'InfiniteScroll';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

import {InfiniteScroll} from '../InfiniteScroll';
import {emailsList, delay} from './utils';

export function InfiniteScrollShowcase() {
const [emails, setEmails] = React.useState<string[]>([]);
const [page, setPage] = React.useState<number>(0);
const isAllEmailsShown = emails.length >= emailsList.length;

const fetchData = async () => {
await delay(3000);
setPage(page + 1);
setEmails(emailsList.slice(0, (page + 1) * 10));
};

return (
<InfiniteScroll onActivate={fetchData} disabled={isAllEmailsShown}>
{emails.map((email, index) => (
<li key={index} style={{lineHeight: '90px', fontSize: '54px'}}>
{email}
</li>
))}
</InfiniteScroll>
);
}
39 changes: 39 additions & 0 deletions src/components/InfiniteScroll/__stories__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export const emailsList = [
'bdbrn@msn.com',
'chlim@yahoo.ca',
'tjensen@gmail.com',
'morain@mac.com',
'arachne@aol.com',
'cfhsoft@att.net',
'empathy@att.net',
'goldberg@outlook.com',
'jsnover@yahoo.ca',
'carreras@gmail.com',
'tubajon@att.net',
'gbacon@gmail.com',
'dialworld@verizon.net',
'fglock@sbcglobal.net',
'jane.smith@testfed.com',
'terjesa@att.net',
'tsuruta@optonline.net',
'pkplex@yahoo.ca',
'storerm@verizon.net',
'overbom@aol.com',
'rande@yahoo.ca',
'crowemojo@msn.com',
'tristan@yahoo.com',
'ducasse@comcast.net',
'tsuruta@mac.com',
'kostas@mac.com',
'rcwil@outlook.com',
'msloan@aol.com',
'sarahs@icloud.com',
'srour@aol.com',
'rnelson@me.com',
'mcnihil@live.com',
'treeves@me.com',
];
19 changes: 19 additions & 0 deletions src/components/InfiniteScroll/hooks/useIntersection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useState, useEffect} from 'react';

export const useIntersection = (element: Element | null, options?: IntersectionObserverInit) => {
const [isVisible, setState] = useState(false);

useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setState(entry.isIntersecting);
}, options);

if (element) {
observer.observe(element);
}

return () => (element === null ? undefined : observer.unobserve(element));
}, [element, options]);

return isVisible;
};
1 change: 1 addition & 0 deletions src/components/InfiniteScroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './InfiniteScroll';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AdaptiveTabs';
export * from './InfiniteScroll';

export {Lang, configure} from './utils/configure';