Skip to content

Commit e15e50d

Browse files
committed
feat: support some state return from hook
1 parent 54bbc99 commit e15e50d

File tree

4 files changed

+176
-56
lines changed

4 files changed

+176
-56
lines changed

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
This is a hook to control your scrollbar in react component!
44

5-
**Live demo is <a target="_blank" href="https://codesandbox.io/s/react-smooth-scroll-hook-vhudw?file=/src/App.js" >Here</a>.**
6-
75
Basically; `useSmoothScroll` hook checks the `HTMLElement`'s API `scrollTo`, otherwise, use `requestAnimationFrame` to finish smooth scroll behaviour!
86

97
If you want to control the `speed` of scroll behaviour, it defaults to use `requestAnimationFrame` mode.
108

9+
**Examples are <a target="_blank" href="https://ron0115.best/react-smooth-scroll-hook/?path=/docs/hooks-usesmoothscroll--direction-x#basic" >Here</a>.**(Storybook)
10+
11+
**Live demo is <a target="_blank" href="https://codesandbox.io/s/react-smooth-scroll-hook-vhudw?file=/src/App.js" >Here</a>.**(Codesandbox)
12+
1113
## Feature
1214

13-
- You don't need to warn about compatibility, it use rAF api on low version browsers.
14-
- Provide `speed` on your demand, otherwise it would use the native API `element.scrollTo`.
15-
- Provide `direction` option ,you can set `x` for vertical, `y` for horizontal.
15+
- 🚀 You don't need to warn about compatibility, it use `requsetAnimationFrame` api to finish smooth scroll behaviour.
16+
17+
- 👉 Provide `direction` option ,you can set `x` for vertical, `y` for horizontal.
18+
19+
- 💧 No Third Party dependencies, light and pure.
1620

1721
## Installation
1822

@@ -65,7 +69,18 @@ export const Demo = () => {
6569

6670
### Returns of Hook
6771

68-
- `scrollTo(string | number)`
72+
- **scrollTo** `(string|number) => void`
6973

7074
- Pass `number`: the distance to scroll, e.g. `scrollTo(400)`
7175
- Pass `string`: the element seletor you want to scrollTo, passing to `document.querySelector`, e.g. `scrollTo('#your-dom-id')`
76+
77+
- **reachTop** `boolean`: Whether it is reach top of scrollContainer
78+
79+
- **reachBottom** `boolean`: Whether it is reach bottom of scrollContainer
80+
81+
- **scrollToPage** `(number) => void`: Pass page(`number`), which scroll to a distance as multiples of container size(`offsetWidth`/`offsetHeight`)
82+
.e.g `scrollToPage(1)`,`scrollToPage(-1)`
83+
84+
- **refreshState** `() => void`: Manually refresh the state of `reachTop` and `reachBottom`, just an API as you need, and possibly useful in some situation.
85+
86+
- **refreshSize** `() => void`: Manually refresh the size of ref container, just an API as you need, and possibly useful in some situation.

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
"engines": {
1616
"node": ">=10"
1717
},
18-
"homepage": "https://ron0115.best/react-smooth-scroll-hook/",
19-
"repository": {
20-
"type": "git",
21-
"url": "https://github.com/ron0115/react-smooth-scroll-hook.git"
22-
},
2318
"scripts": {
2419
"start": "tsdx watch",
2520
"build": "tsdx build",

src/index.tsx

Lines changed: 136 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
2+
import { useEffect } from 'react';
3+
4+
function debounce(cb: Function, delay = 100) {
5+
let timer: NodeJS.Timeout;
6+
return function(...args: any) {
7+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
8+
// @ts-ignore
9+
// eslint-disable-next-line @typescript-eslint/no-this-alias
10+
const _this = this;
11+
if (timer) clearTimeout(timer);
12+
timer = setTimeout(() => {
13+
cb.apply(_this, args);
14+
}, delay);
15+
};
16+
}
17+
218
export enum Direction {
319
X = 'x',
420
Y = 'y',
@@ -24,7 +40,7 @@ export type AttrMapType = {
2440
};
2541
// get the relative distance from destination
2642
const getRelativeDistance = (
27-
target: number | string,
43+
target: number | string | undefined,
2844
parent: HTMLElement,
2945
attrMap: AttrMapType
3046
) => {
@@ -45,7 +61,7 @@ const getRelativeDistance = (
4561

4662
export const useSmoothScroll = ({
4763
ref,
48-
speed,
64+
speed = 100,
4965
direction = Direction.Y,
5066
threshold = 1,
5167
}: UseSmoothScrollType) => {
@@ -61,7 +77,41 @@ export const useSmoothScroll = ({
6177
Direction.X === direction ? 'clientWidth' : 'clientHeight',
6278
};
6379

64-
const scrollTo = (target: number | string) => {
80+
const [reachTop, setReachTop] = useState(true);
81+
const [reachBottom, setReachBottom] = useState(true);
82+
const [size, setSize] = useState(0);
83+
84+
const isTopEdge = () => {
85+
const elm = ref.current;
86+
if (!elm) return false;
87+
return elm[attrMap.scrollLeftTop] === 0;
88+
};
89+
90+
const isBottomEdge = () => {
91+
const elm = ref.current;
92+
if (!elm) return false;
93+
return (
94+
Math.abs(
95+
elm[attrMap.scrollLeftTop] +
96+
elm[attrMap.clientWidthHeight] -
97+
elm[attrMap.scrollWidthHeight]
98+
) < threshold
99+
);
100+
};
101+
102+
const refreshSize = debounce(() => {
103+
if (ref.current) {
104+
const size = ref.current[attrMap.clientWidthHeight];
105+
setSize(size);
106+
}
107+
});
108+
109+
const refreshState = debounce((_evt: Event) => {
110+
isTopEdge() ? setReachTop(true) : setReachTop(false);
111+
isBottomEdge() ? setReachBottom(true) : setReachBottom(false);
112+
});
113+
114+
const scrollTo = (target?: number | string) => {
65115
if (!ref || !ref.current) {
66116
console.warn(
67117
'Please pass `ref` property for your scroll container! \n Get more info at https://github.com/ron0115/react-smooth-scroll-hook'
@@ -70,55 +120,103 @@ export const useSmoothScroll = ({
70120
}
71121
const elm = ref.current;
72122
if (!elm) return;
73-
if (!target) {
123+
if (!target && typeof target !== 'number') {
74124
console.warn(
75125
'Please pass a valid property for `scrollTo()`! \n Get more info at https://github.com/ron0115/react-smooth-scroll-hook'
76126
);
77127
}
78128
const initScrollLeftTop = elm[attrMap.scrollLeftTop];
79129
const distance = getRelativeDistance(target, elm, attrMap);
80130

81-
if (distance === 0) return;
82-
83-
if (elm.scrollTo && !speed) {
84-
elm.scrollTo({
85-
[attrMap.leftTop]: elm[attrMap.scrollLeftTop] + distance,
86-
behavior: 'smooth',
87-
});
88-
} else {
89-
let _speed = speed || 100;
90-
const cb = () => {
91-
// scroll to edge
92-
if (
93-
distance > 0
94-
? elm[attrMap.scrollLeftTop] + elm[attrMap.clientWidthHeight] >=
95-
elm[attrMap.scrollWidthHeight]
96-
: elm[attrMap.scrollLeftTop] === 0
97-
) {
98-
return;
99-
}
100-
const gone = () =>
101-
Math.abs(elm[attrMap.scrollLeftTop] - initScrollLeftTop);
131+
let _speed = speed;
132+
const cb = () => {
133+
refreshState();
102134

103-
if (Math.abs(distance) - gone() < _speed) {
104-
_speed = Math.abs(distance) - gone();
105-
}
135+
if (distance === 0) return;
106136

107-
// distance to run every frame,always 1/60s
108-
elm[attrMap.scrollLeftTop] += _speed * (distance > 0 ? 1 : -1);
137+
if ((isBottomEdge() && distance > 9) || (distance < 0 && isTopEdge()))
138+
return;
109139

110-
// reach destination, threshold defaults to 1
111-
if (Math.abs(gone() - Math.abs(distance)) < threshold) {
112-
return;
113-
}
140+
const gone = () =>
141+
Math.abs(elm[attrMap.scrollLeftTop] - initScrollLeftTop);
142+
143+
if (Math.abs(distance) - gone() < _speed) {
144+
_speed = Math.abs(distance) - gone();
145+
}
146+
147+
// distance to run every frame,always 1/60s
148+
elm[attrMap.scrollLeftTop] += _speed * (distance > 0 ? 1 : -1);
149+
150+
// reach destination, threshold defaults to 1
151+
if (Math.abs(gone() - Math.abs(distance)) < threshold) {
152+
return;
153+
}
114154

115-
requestAnimationFrame(cb);
116-
};
117155
requestAnimationFrame(cb);
118-
}
156+
};
157+
requestAnimationFrame(cb);
119158
};
159+
160+
// detect dom changes
161+
useEffect(() => {
162+
if (!ref.current) return;
163+
refreshState();
164+
refreshSize();
165+
const observer = new MutationObserver((mutationsList, _observer) => {
166+
// Use traditional 'for loops' for IE 11
167+
for (const mutation of mutationsList) {
168+
if (
169+
mutation.type === 'attributes' &&
170+
mutation.target instanceof Element
171+
) {
172+
refreshSize();
173+
}
174+
}
175+
});
176+
observer.observe(ref.current, {
177+
attributes: true,
178+
});
179+
window.addEventListener('resize', refreshSize);
180+
return () => {
181+
observer.disconnect();
182+
window.removeEventListener('resize', refreshSize);
183+
};
184+
}, [ref]);
185+
186+
// detect scrollbar changes
187+
useEffect(() => {
188+
if (!ref.current) return;
189+
const observer = new MutationObserver((mutationsList, _observer) => {
190+
// Use traditional 'for loops' for IE 11
191+
for (const mutation of mutationsList) {
192+
if (
193+
mutation.type === 'childList' &&
194+
mutation.target instanceof Element
195+
) {
196+
refreshState();
197+
}
198+
}
199+
});
200+
observer.observe(ref.current, {
201+
childList: true,
202+
subtree: true,
203+
});
204+
ref.current.addEventListener('scroll', refreshState);
205+
return () => {
206+
observer.disconnect();
207+
window.removeEventListener('scroll', refreshState);
208+
};
209+
}, [ref]);
210+
120211
return {
212+
reachTop,
213+
reachBottom,
121214
scrollTo,
215+
scrollToPage: (page: number) => {
216+
scrollTo(page * size);
217+
},
218+
refreshState,
219+
refreshSize,
122220
};
123221
};
124222

stories/Demo.stories.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Description } from '@storybook/addon-docs/dist/blocks';
44
import './index.css';
55

66
export default {
7-
title: '@Hooks/useSmoothScroll',
7+
title: 'useSmoothScroll',
88
component: useSmoothScroll,
99
};
1010

@@ -51,7 +51,7 @@ Demo.storyName = 'Basic';
5151
export const DirectionX = () => {
5252
const [speed, setSpeed] = useState(50);
5353
const ref = useRef<HTMLDivElement>(null);
54-
const { scrollTo } = useSmoothScroll({
54+
const { scrollTo, reachTop, reachBottom, scrollToPage } = useSmoothScroll({
5555
ref,
5656
direction: 'x',
5757
speed,
@@ -61,8 +61,8 @@ export const DirectionX = () => {
6161
};
6262
return (
6363
<>
64-
<button onClick={() => scrollTo('#x-item-40')}>
65-
scrollTo('#x-item-40')
64+
<button onClick={() => scrollTo('#x-item-20')}>
65+
scrollTo('#x-item-20')
6666
</button>
6767
<button onClick={() => scrollTo(Infinity)}>
6868
scrollTo Edge - scrollTo(Infinity)
@@ -72,18 +72,30 @@ export const DirectionX = () => {
7272
</button>
7373
<button onClick={() => scrollTo(100)}>scrollTo(100)</button>
7474
<button onClick={() => scrollTo(-100)}>scrollTo(-100)</button>
75+
7576
<br />
7677
<div>
77-
speed:
78+
speed:{speed}
79+
<br />
7880
<input
7981
value={speed}
8082
onChange={onChange}
8183
type="range"
82-
min={50}
84+
min={100}
8385
max={500}
8486
/>
87+
<br />
88+
reachTop: {String(reachTop)}
89+
<br />
90+
reachBottom: {String(reachBottom)}
8591
</div>
8692
<br />
93+
<button disabled={reachBottom} onClick={() => scrollToPage(1)}>
94+
scrollToPage(1)
95+
</button>
96+
<button disabled={reachTop} onClick={() => scrollToPage(-1)}>
97+
scrollToPage(-1)
98+
</button>
8799

88100
<div
89101
ref={ref}
@@ -97,7 +109,7 @@ export const DirectionX = () => {
97109
padding: '10px',
98110
}}
99111
>
100-
{Array(100)
112+
{Array(50)
101113
.fill(null)
102114
.map((_item, i) => (
103115
<div

0 commit comments

Comments
 (0)