Skip to content

Commit fd5748a

Browse files
authored
fix(promise): return promises consistently, and only smooth scroll when needed (#136)
* update readme * better easing examples * one more example * expand test cases * add test case for fallbacks * Always return a promise * resolve with the actions that got performed * Add umd instructions to readme * add test case for no-op * only smooth scroll when needed
1 parent 152c3c7 commit fd5748a

File tree

4 files changed

+110
-20
lines changed

4 files changed

+110
-20
lines changed

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ And while `scroll-into-view-if-needed` use the same default options as browsers
1818
yarn add smooth-scroll-into-view-if-needed
1919
```
2020

21+
The UMD build is also available on [unpkg](https://unpkg.com/smooth-scroll-into-view-if-needed/umd/):
22+
23+
<script src="https://unpkg.com/smooth-scroll-into-view-if-needed/umd/smooth-scroll-into-view-if-needed.min.js"></script>
24+
25+
You can find the library on `window.scrollIntoView`.
26+
2127
## Usage
2228

2329
```js
@@ -52,10 +58,6 @@ This library rely on `Promise` and `requestAnimationFrame`. This library does no
5258

5359
Check the full API in [`scroll-into-view-if-needed`](https://github.com/stipsan/scroll-into-view-if-needed#api).
5460

55-
This library differs from the API in `scroll-into-view-if-needed` in the following ways:
56-
57-
- the second argument can't be a boolean, it must be either undefined or an object.
58-
5961
### scrollIntoView(target, [options]) => Promise
6062

6163
`scroll-into-view-if-needed` does not return anything, while this library will return a Promise that is resolved when all of the scrolling boxes are finished scrolling.
@@ -95,15 +97,32 @@ Type: `Function`
9597

9698
> Introduced in `v1.1.0`
9799
98-
The default easing is implemented like this, with `t` being in the range of `0` => `1`:
100+
The default easing is `easeOutQuint` based on these algorithms: https://gist.github.com/gre/1650294#file-easing-js
101+
102+
Linear example:
99103

100104
```typescript
101105
scrollIntoView(node, {
102-
ease: t => 0.5 * (1 - Math.cos(Math.PI * t)),
106+
ease: t => t,
103107
})
104108
```
105109

106-
Here's more examples, like easeInCubic etc: https://gist.github.com/gre/1650294#file-easing-js
110+
Acceleration until halfway, then deceleration:
111+
112+
```typescript
113+
scrollIntoView(node, {
114+
ease: t =>
115+
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
116+
})
117+
```
118+
119+
Sine easing in and out:
120+
121+
```typescript
122+
scrollIntoView(node, {
123+
ease: t => (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2,
124+
})
125+
```
107126

108127
## Credits
109128

src/index.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ type Context = {
3838
function step(context: Context) {
3939
const time = now()
4040
const elapsed = Math.min((time - context.startTime) / context.duration, 1)
41-
4241
// apply easing to elapsed time
4342
const value = context.ease(elapsed)
4443

@@ -79,7 +78,7 @@ function smoothScroll(
7978
el.scrollTop = y
8079
}
8180

82-
// scroll looping over a frame
81+
// scroll looping over a frame if needed
8382
step({
8483
scrollable: scrollable,
8584
method: method,
@@ -112,26 +111,37 @@ function scroll<T>(target: Element, options?: any) {
112111
boundary: overrides.boundary,
113112
behavior: actions =>
114113
Promise.all(
115-
actions.map(
116-
({ el, left, top }) =>
117-
new Promise(resolve =>
118-
smoothScroll(
114+
actions.reduce((results: Promise<any>[], { el, left, top }) => {
115+
const startLeft = el.scrollLeft
116+
const startTop = el.scrollTop
117+
if (startLeft === left && startTop === top) {
118+
return results
119+
}
120+
121+
return [
122+
...results,
123+
new Promise(resolve => {
124+
return smoothScroll(
119125
el,
120126
left,
121127
top,
122128
overrides.duration,
123129
overrides.ease,
124-
() => resolve()
130+
() =>
131+
resolve({
132+
el,
133+
left: [startLeft, left],
134+
top: [startTop, top],
135+
})
125136
)
126-
)
127-
)
137+
}),
138+
]
139+
}, [])
128140
),
129141
})
130142
}
131143

132-
// @TODO maybe warn when someone could be using this library this way unintentionally
133-
134-
return scrollIntoView<T>(target, options)
144+
return Promise.resolve(scrollIntoView<T>(target, options))
135145
}
136146

137147
// re-assign here makes the flowtype generation work

tests/interface.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
import scrollIntoView from '../src'
22

3+
// This mock supports two scenarios:
4+
// 1. Default smooth scrolling, return empty array to ensure no scrolling is attempted.
5+
// 2. Falling back to scroll-into-view-if-needed, simulate an array over scroll actions
6+
// This enables tests to know which of the two cases is in effect, and verify if it's correct.
37
jest.mock(
48
'scroll-into-view-if-needed',
5-
() => (_target: Element, options: any) => options.behavior([])
9+
() => (_target: Element, options: any) =>
10+
typeof options.behavior == 'function'
11+
? options.behavior([])
12+
: [{ el: {}, top: 0, left: 0 }]
613
)
714

815
test('options is optional', () => {
916
expect.assertions(1)
1017
return expect(scrollIntoView(document.body)).resolves.toEqual([])
1118
})
19+
20+
test('behavior option is optional', () => {
21+
expect.assertions(1)
22+
return expect(
23+
scrollIntoView(document.body, { scrollMode: 'if-needed' })
24+
).resolves.toEqual([])
25+
})
26+
27+
test('behavior option set to smooth works correctly', () => {
28+
expect.assertions(1)
29+
return expect(
30+
scrollIntoView(document.body, { behavior: 'smooth' })
31+
).resolves.toEqual([])
32+
})
33+
34+
test('other behavior options still returns a promise', () => {
35+
expect.assertions(1)
36+
return expect(
37+
scrollIntoView(document.body, { behavior: 'auto' })
38+
).resolves.toEqual([{ el: {}, top: 0, left: 0 }])
39+
})

tests/scroll.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
beforeEach(() => {
2+
jest.resetModules()
3+
})
4+
5+
test('resolves after scroll is completed', () => {
6+
jest.doMock(
7+
'scroll-into-view-if-needed',
8+
() => (_target: Element, options: any) =>
9+
options.behavior([
10+
{ el: { scrollTop: 0, scrollLeft: 0 }, top: 60, left: 60 },
11+
])
12+
)
13+
const scrollIntoView = require('../src').default
14+
expect.assertions(1)
15+
return expect(scrollIntoView(document.body)).resolves.toEqual([
16+
{ el: { scrollTop: 60, scrollLeft: 60 }, top: [0, 60], left: [0, 60] },
17+
])
18+
})
19+
20+
test('resolves right away if there is nothign to scroll', () => {
21+
jest.doMock(
22+
'scroll-into-view-if-needed',
23+
() => (_target: Element, options: any) =>
24+
options.behavior([
25+
{ el: { scrollTop: 0, scrollLeft: 0 }, top: 0, left: 0 },
26+
])
27+
)
28+
const scrollIntoView = require('../src').default
29+
expect.assertions(1)
30+
return expect(
31+
scrollIntoView(document.body, { duration: Number.MAX_VALUE })
32+
).resolves.toEqual([])
33+
})

0 commit comments

Comments
 (0)