Skip to content

Commit 0ab7635

Browse files
authored
Merge pull request streamich#35 from streamich/infinite-scroll
Infinite scroll
2 parents 81f9bf6 + bfe400f commit 0ab7635

File tree

12 files changed

+172
-27
lines changed

12 files changed

+172
-27
lines changed

.storybook/webpack.config.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const path = require('path');
2+
const {compilerOptions} = require('../tsconfig.json');
23

34
const SRC_PATH = path.join(__dirname, '../src');
45

@@ -10,7 +11,14 @@ module.exports = {
1011
loader: 'ts-loader',
1112
include: [
1213
SRC_PATH,
13-
]
14+
],
15+
options: {
16+
transpileOnly: true, // use transpileOnly mode to speed-up compilation
17+
compilerOptions: {
18+
...compilerOptions,
19+
declaration: false,
20+
},
21+
},
1422
}
1523
]
1624
},

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# libreact
44

5-
[![][npm-badge]][npm-url] [![][travis-badge]][travis-url] [![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface) [![Backers on Open Collective](https://opencollective.com/libreact/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/libreact/sponsors/badge.svg)](#sponsors)
5+
[![][npm-badge]][npm-url] [![][travis-badge]][travis-url] [![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface) [![Backers on Open Collective](https://opencollective.com/libreact/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/libreact/sponsors/badge.svg)](#sponsors)
66

77
React standard library—must-have toolbox for any React project.
88

@@ -90,6 +90,7 @@ const MyComponent = mock();
9090
- [`<Slider>`](./docs/en/Slider.md)
9191
- [`<DropArea>`](./docs/en/DropArea.md)
9292
- [`<Group>`](./docs/en/Group.md)
93+
- [`<InfiniteScroll>`](./docs/en/InfiniteScroll.md)
9394
- [`<OutsideClick>`](./docs/en/OutsideClick.md)
9495
- [`<Ripple>`](./docs/en/Ripple.md) and [`withRipple()`](./docs/en/Ripple.md#withripple) &mdash; [**example**](https://codesandbox.io/s/983q7jr80o)
9596
- [`<Img>`](./docs/en/Img.md)

docs/en/InfiniteScroll.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# `<InfiniteScroll>`
2+
3+
Fires `loadMore` prop when end of content becomes visible.
4+
5+
6+
## Usage
7+
8+
```jsx
9+
import {InfiniteScroll} from 'libreact/lib/InfiniteScroll';
10+
11+
<InfiniteScroll
12+
hasMore={true || false}
13+
cursor={cursor}
14+
loadMore={() => {/* ... */}}
15+
>
16+
{items}
17+
</InfiniteScroll>
18+
```
19+
20+
21+
## Props
22+
23+
- `loadMore` &mdash; required, function that is called when user scrolls to the bottom of the component.
24+
- `cursor` &mdash; required, unique identifier of current page, `loadMore` is called only once for each adjacent unique value of `cursor`.
25+
- `hasMore` &mdash; optional, boolean, whether there are more items to load, if set to `false`, `loadMore` will not be called.
26+
- `sentinel` &mdash; optional, React element to render at the bottom of the component, when this element becomes visible it triggers `loadMore` function, defaults to empty `<div>` pixel.
27+
- `margin` &mdash; optional, number, invisible margin before `sentinel` when to already call `loadMore` before `sentinel` is visible, defaults to `100`.

docs/en/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
- [`<Slider>`](./Slider.md)
5757
- [`<DropArea>`](./DropArea.md)
5858
- [`<Group>`](./Group.md)
59+
- [`<InfiniteScroll>`](./InfiniteScroll.md)
5960
- [`<OutsideClick>`](./OutsideClick.md)
6061
- [`<Ripple>`](./Ripple.md) and [`withRipple()`](./Ripple.md#withripple) &mdash; [**example**](https://codesandbox.io/s/983q7jr80o)
6162
- [`<Img>`](./Img.md)

docs/en/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
* [Ripple](Ripple.md)
7070
* [Img](Img.md)
7171
* [Group](Group.md)
72+
* [InfiniteScroll](InfiniteScroll.md)
7273
* [WidthQuery](WidthQuery.md)
7374
* [View](View.md)
7475
* [WindowWidthQuery](WindowWidthQuery.md)
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {InfiniteScroll} from '..';
4+
import ShowDocs from '../../ShowDocs';
5+
6+
const h = React.createElement;
7+
8+
const Block = () => {
9+
return <div style={{
10+
width: 100,
11+
height: 100,
12+
margin: 20,
13+
background: 'red',
14+
}}></div>
15+
};
16+
17+
class Demo extends React.Component {
18+
state = {
19+
items: [
20+
<Block key={0} />,
21+
<Block key={1} />,
22+
<Block key={2} />,
23+
<Block key={3} />,
24+
<Block key={4} />,
25+
],
26+
cursor: 1,
27+
};
28+
29+
constructor (props) {
30+
super(props);
31+
}
32+
33+
load = (cnt = 5) => {
34+
console.log('loading for cursor: ' + this.state.cursor);
35+
const items = [...this.state.items];
36+
for (let i = 0; i < cnt; i++) {
37+
items.push(<Block key={items.length} />);
38+
}
39+
this.setState({
40+
items,
41+
cursor: this.state.cursor + 1,
42+
});
43+
};
44+
45+
render () {
46+
return (
47+
<InfiniteScroll hasMore={this.state.cursor < 5} loadMore={this.load} cursor={this.state.cursor}>
48+
{this.state.items}
49+
</InfiniteScroll>
50+
);
51+
}
52+
}
53+
54+
storiesOf('UI/InfiniteScroll', module)
55+
.add('Documentation', () => h(ShowDocs, {md: require('../../../docs/en/InfiniteScroll.md')}))
56+
.add('Example', () => <Demo />)

src/InfiniteScroll/index.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from 'react';
2+
import {ViewportScrollSensor} from '../ViewportScrollSensor';
3+
4+
const h = React.createElement;
5+
const defaultSentinel = h('div', {style: {width: 1, height: 1}});
6+
7+
export interface InfiniteScrollProps {
8+
cursor?: number | string;
9+
sentinel?: React.ReactElement<any>;
10+
hasMore?: boolean;
11+
margin?: number;
12+
loadMore: () => void;
13+
}
14+
15+
export interface InfiniteScrollState {
16+
}
17+
18+
export class InfiniteScroll extends React.Component<InfiniteScrollProps, InfiniteScrollProps> {
19+
static defaultProps = {
20+
sentinel: defaultSentinel,
21+
hasMore: true,
22+
margin: 100,
23+
};
24+
25+
lastLoadMoreCursor: number | string | null = null;
26+
27+
onViewportChange = ({visible}) => {
28+
if (visible) {
29+
if (this.lastLoadMoreCursor !== this.props.cursor) {
30+
this.lastLoadMoreCursor = this.props.cursor;
31+
this.props.loadMore();
32+
}
33+
}
34+
};
35+
36+
render () {
37+
const {props} = this;
38+
const {children, hasMore, sentinel, margin} = props;
39+
return h(React.Fragment, null,
40+
children,
41+
hasMore &&
42+
h(ViewportScrollSensor, {margin: [0, 0, margin, 0], onChange: this.onViewportChange}, sentinel),
43+
);
44+
}
45+
}

src/ViewportObserverSensor/__story__/story.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import StoryViewportSensorConf from './StoryViewportSensorConf';
88

99
storiesOf('Sensors/ViewportSensor/ViewportObserverSensor', module)
1010
.add('Basic example', () =>
11-
<StoryViewportSensorBasic sensor={ViewportObserverSensor} onChange={action('onChange')} />)
11+
<StoryViewportSensorBasic sensor={ViewportObserverSensor} onChange={console.log} />)
1212
.add('Horizontal', () =>
13-
<StoryViewportSensorHorizontal sensor={ViewportObserverSensor} onChange={action('onChange')} />)
13+
<StoryViewportSensorHorizontal sensor={ViewportObserverSensor} onChange={console.log} />)
1414
.add('Threshold 0%', () =>
15-
<StoryViewportSensorConf threshold={0} sensor={ViewportObserverSensor} onChange={action('onChange')} />)
15+
<StoryViewportSensorConf threshold={0} sensor={ViewportObserverSensor} onChange={console.log} />)
1616
.add('Threshold 25%', () =>
17-
<StoryViewportSensorConf threshold={0.25} sensor={ViewportObserverSensor} onChange={action('onChange')} />)
17+
<StoryViewportSensorConf threshold={0.25} sensor={ViewportObserverSensor} onChange={console.log} />)
1818
.add('Threshold 75%', () =>
19-
<StoryViewportSensorConf threshold={0.75} sensor={ViewportObserverSensor} onChange={action('onChange')} />)
19+
<StoryViewportSensorConf threshold={0.75} sensor={ViewportObserverSensor} onChange={console.log} />)
2020
.add('Threshold 100%', () =>
21-
<StoryViewportSensorConf threshold={1} sensor={ViewportObserverSensor} onChange={action('onChange')} />)
21+
<StoryViewportSensorConf threshold={1} sensor={ViewportObserverSensor} onChange={console.log} />)
2222
.add('Threshold 100%, margin 100px', () =>
2323
<StoryViewportSensorConf threshold={1} margin={[100, 100, 100, -100]} sensor={ViewportObserverSensor} onChange={action('onChange')} />);

src/ViewportObserverSensor/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ export class ViewportObserverSensor extends Component<IViewportObserverSensorPro
5555
const entry = entries[0];
5656
const {intersectionRatio} = entry;
5757
const {threshold, onChange} = this.props;
58-
58+
const noThresholdAndTinyBitIsVisible = !!(!threshold && intersectionRatio);
59+
const visibleMoreOrEqualToThreshold = threshold
60+
? intersectionRatio >= threshold
61+
: intersectionRatio > threshold;
5962
const state = {
60-
visible: !!((!threshold && intersectionRatio) || (intersectionRatio >= threshold))
63+
visible: noThresholdAndTinyBitIsVisible || visibleMoreOrEqualToThreshold,
6164
};
6265

6366
this.setState(state);

src/ViewportScrollSensor/__story__/story.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import StoryViewportSensorConf from '../../ViewportObserverSensor/__story__/Stor
88

99
storiesOf('Sensors/ViewportSensor/ViewportScrollSensor', module)
1010
.add('Basic example', () =>
11-
<StoryViewportSensorBasic sensor={ViewportScrollSensor} onChange={action('onChange')} />)
11+
<StoryViewportSensorBasic sensor={ViewportScrollSensor} onChange={console.log} />)
1212
.add('Horizontal', () =>
13-
<StoryViewportSensorHorizontal sensor={ViewportScrollSensor} onChange={action('onChange')} />)
13+
<StoryViewportSensorHorizontal sensor={ViewportScrollSensor} onChange={console.log} />)
1414
.add('Threshold 0%', () =>
15-
<StoryViewportSensorConf threshold={0} sensor={ViewportScrollSensor} onChange={action('onChange')} />)
15+
<StoryViewportSensorConf threshold={0} sensor={ViewportScrollSensor} onChange={console.log} />)
1616
.add('Threshold 25%', () =>
17-
<StoryViewportSensorConf threshold={0.25} sensor={ViewportScrollSensor} onChange={action('onChange')} />)
17+
<StoryViewportSensorConf threshold={0.25} sensor={ViewportScrollSensor} onChange={console.log} />)
1818
.add('Threshold 75%', () =>
19-
<StoryViewportSensorConf threshold={0.75} sensor={ViewportScrollSensor} onChange={action('onChange')} />)
19+
<StoryViewportSensorConf threshold={0.75} sensor={ViewportScrollSensor} onChange={console.log} />)
2020
.add('Threshold 100%', () =>
21-
<StoryViewportSensorConf threshold={1} sensor={ViewportScrollSensor} onChange={action('onChange')} />)
21+
<StoryViewportSensorConf threshold={1} sensor={ViewportScrollSensor} onChange={console.log} />)
2222
.add('Threshold 100%, margin 100px', () =>
2323
<StoryViewportSensorConf threshold={1} margin={[100, 100, 100, 100]} sensor={ViewportScrollSensor} onChange={action('onChange')} />);

src/ViewportScrollSensor/index.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Component, cloneElement, SFCElement} from 'react';
1+
import * as React from 'react';
22
import {on, off, noop} from '../util';
33
import {throttle} from 'throttle-debounce';
44
import renderProp from '../util/renderProp';
@@ -56,19 +56,19 @@ export interface IViewportScrollSensorState {
5656
visible: boolean;
5757
}
5858

59-
export class ViewportScrollSensor<TProps extends IViewportScrollSensorProps, TState extends IViewportScrollSensorState> extends Component<TProps, TState> {
59+
export class ViewportScrollSensor extends React.Component<IViewportScrollSensorProps, IViewportScrollSensorState> {
6060
static defaultProps = {
6161
threshold: 0,
6262
throttle: 50,
6363
margin: [0, 0, 0, 0]
64-
};
64+
} as any;
6565

6666
mounted: boolean = false;
6767
el: HTMLElement;
6868

69-
state: TState = {
69+
state: IViewportScrollSensorState = {
7070
visible: false
71-
} as TState;
71+
};
7272

7373
ref = (originalRef) => (el) => {
7474
this.el = el;
@@ -79,13 +79,15 @@ export class ViewportScrollSensor<TProps extends IViewportScrollSensorProps, TSt
7979
this.mounted = true;
8080

8181
on(document, 'scroll', this.onScroll);
82+
on(window, 'resize', this.onScroll);
8283
this.onScroll();
8384
}
8485

8586
componentWillUnmount () {
8687
this.mounted = false;
8788

8889
off(document, 'scroll', this.onScroll);
90+
off(window, 'resize', this.onScroll);
8991
}
9092

9193
onCalculation (visible, rectRoot: TRect, rectEl: TRect, rectIntersection: TRect) {
@@ -134,7 +136,7 @@ export class ViewportScrollSensor<TProps extends IViewportScrollSensorProps, TSt
134136
}
135137
}
136138

137-
return cloneElement(element, {
139+
return React.cloneElement(element, {
138140
ref: this.ref(element.ref)
139141
});
140142
}

src/ViewportSensor/index.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import faccToHoc, {divWrapper} from '../util/faccToHoc';
55

66
let Sensor: any = ViewportObserverSensor;
77

8-
if (!(window as any).IntersectionObserver) {
9-
const loader = () => import('../ViewportScrollSensor').then((module) => module.ViewportScrollSensor);
10-
11-
Sensor = loadable({loader});
12-
Sensor.load();
8+
if (typeof window === 'object') {
9+
if (!(window as any).IntersectionObserver) {
10+
const loader = () => import('../ViewportScrollSensor').then((module) => module.ViewportScrollSensor);
11+
Sensor = loadable({loader});
12+
Sensor.load();
13+
}
1314
}
1415

1516
export const ViewportSensor: React.StatelessComponent<IViewportObserverSensorProps> = (props) => {

0 commit comments

Comments
 (0)