Skip to content

Commit fa8b6d5

Browse files
authored
feat(animation): expose duration and ease options (#28)
* update docs * improve typescript typings * check bundlesize * update default sizes * implement new options * bump peer dependency * refactor to newer standards * handy link
1 parent c602329 commit fa8b6d5

File tree

4 files changed

+156
-52
lines changed

4 files changed

+156
-52
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
- restore-cache: *restore-cache
3939
- *install
4040
- run: yarn build
41+
- run: npx bundlesize
4142
- save-cache: *save-cache
4243

4344
Semantic Release:

README.md

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
![smooth-scroll-into-view-if-needed](https://user-images.githubusercontent.com/81981/39496447-c1153942-4d9e-11e8-92c8-ad5ac0e406ac.png)
99

1010
This is an addon to [`scroll-into-view-if-needed`](https://www.npmjs.com/package/scroll-into-view-if-needed) that [ponyfills](https://ponyfill.com) smooth scrolling.
11+
And while `scroll-into-view-if-needed` use the same default options as browsers and the spec does, this library is a bit more opinionated and include bonus features that help you build great UIs.
1112

1213
## [Demo](https://scroll-into-view-if-needed.netlify.com/)
1314

@@ -23,18 +24,17 @@ yarn add smooth-scroll-into-view-if-needed scroll-into-view-if-needed
2324
import scrollIntoView from 'smooth-scroll-into-view-if-needed'
2425
const node = document.getElementById('hero')
2526

26-
// If all you want is for all your users to have stuff smooth scroll into view
27-
scrollIntoView(node, { behavior: 'smooth' })
27+
// `options.behavior` is set to `smooth` by default so you don't have to pass options like in `scroll-into-view-if-needed`
28+
scrollIntoView(node)
2829

29-
// combine it with any of the other options
30+
// combine it with any of the other options from 'scroll-into-view-if-needed'
3031
scrollIntoView(node, {
31-
behavior: 'smooth',
3232
scrollMode: 'if-needed',
3333
block: 'nearest',
3434
inline: 'nearest',
3535
})
3636

37-
// It returns a promise that is resolved when the animation is finished
37+
// a promise is always returned to help reduce boilerplate
3838
const sequence = async () => {
3939
const slide = document.getElementById('slide-3')
4040
// First smooth scroll to hero
@@ -44,6 +44,65 @@ const sequence = async () => {
4444
}
4545
```
4646

47+
## Polyfills
48+
49+
This library rely on `Promise` and `requestAnimationFrame`. This library does not ship with polyfills for these to keep bundlesizes as low as possible.
50+
51+
## API
52+
53+
Check the full API in [`scroll-into-view-if-needed`](https://github.com/stipsan/scroll-into-view-if-needed#api).
54+
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+
59+
### scrollIntoView(target, [options]) => Promise
60+
61+
`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.
62+
63+
> The ability to cancel animations will be added in a future version.
64+
65+
### options
66+
67+
Type: `Object`
68+
69+
#### behavior
70+
71+
Type: `'auto' | 'smooth' | 'instant' | Function`<br> Default: `'smooth'`
72+
73+
This option deviates from `scroll-into-view-if-needed` in two ways.
74+
75+
* The default value is `smooth` instead of `auto`
76+
* Using `smooth` adds it to browsers that miss it, and overrides the native smooth scrolling in the browsers that have it to ensure the scrolling is consistent in any browser.
77+
78+
#### duration
79+
80+
Type: `number`<br> Default: `300`
81+
82+
> Introduced in `v1.1.0`
83+
84+
This setting is not a hard limit.
85+
The duration of a scroll differs depending on how many elements is scrolled, and the capabilities of the browser.
86+
On mobile the browser might pause or throttle the animation if the user switches to another tab.
87+
And there might be nothing to scroll.
88+
No matter the scenario a Promise is returned so you can await on it.
89+
90+
#### ease
91+
92+
Type: `Function`
93+
94+
> Introduced in `v1.1.0`
95+
96+
The default easing is implemented like this, with `t` being in the range of `0` => `1`:
97+
98+
```typescript
99+
scrollIntoView(node, {
100+
ease: t => 0.5 * (1 - Math.cos(Math.PI * t)),
101+
})
102+
```
103+
104+
Here's more examples, like easeInCubic etc: https://gist.github.com/gre/1650294#file-easing-js
105+
47106
## Credits
48107

49108
* [smoothscroll](https://github.com/iamdustan/smoothscroll) for the reference implementation of smooth scrolling.

package.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"version": "1.0.1-alpha.3",
1414
"main": "index.js",
15+
"module": "es/index.js",
1516
"files": ["es", "typings", "umd"],
1617
"scripts": {
1718
"prebuild": "yarn clean",
@@ -31,7 +32,7 @@
3132
"typecheck": "tsc --noEmit"
3233
},
3334
"peerDependencies": {
34-
"scroll-into-view-if-needed": "^2.1.1"
35+
"scroll-into-view-if-needed": ">=2.1.6"
3536
},
3637
"devDependencies": {
3738
"@babel/cli": "7.0.0-beta.46",
@@ -79,6 +80,16 @@
7980
"browserify": {
8081
"transform": ["loose-envify"]
8182
},
83+
"bundlesize": [
84+
{
85+
"path": "./umd/smooth-scroll-into-view-if-needed.min.js",
86+
"maxSize": "2 kB"
87+
},
88+
{
89+
"path": "./umd/smooth-scroll-into-view-if-needed.js",
90+
"maxSize": "3.5 kB"
91+
}
92+
],
8293
"lint-staged": {
8394
"*.js": ["prettier --write", "git add"],
8495
"*.{ts,tsx}": ["prettier --write", "git add"],
@@ -88,7 +99,6 @@
8899
"**/package.json": ["prettier-package-json --write", "git add"],
89100
"**/.babelrc": ["prettier --write", "git add"]
90101
},
91-
"module": "es/index.js",
92102
"prettier": {
93103
"semi": false,
94104
"singleQuote": true,

src/index.ts

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,71 @@
1-
import scrollIntoView from 'scroll-into-view-if-needed'
2-
import { Options } from 'scroll-into-view-if-needed/compute'
1+
import scrollIntoView, {
2+
Options,
3+
StandardBehaviorOptions,
4+
CustomBehaviorOptions,
5+
} from 'scroll-into-view-if-needed'
6+
7+
export interface CustomEasing {
8+
(t: number): number
9+
}
10+
export interface SmoothBehaviorOptions extends Options {
11+
behavior?: 'smooth'
12+
duration?: number
13+
ease?: CustomEasing
14+
}
315

416
// Memoize so we're much more friendly to non-dom envs
517
let memoizedNow
6-
var now = () => {
18+
const now = () => {
719
if (!memoizedNow) {
820
memoizedNow =
921
'performance' in window ? performance.now.bind(performance) : Date.now
1022
}
1123
return memoizedNow()
1224
}
1325

14-
const SCROLL_TIME = 300
15-
16-
function ease(k) {
17-
return 0.5 * (1 - Math.cos(Math.PI * k))
18-
}
19-
2026
function step(context) {
21-
var time = now()
22-
var value
23-
var currentX
24-
var currentY
25-
var elapsed = (time - context.startTime) / SCROLL_TIME
26-
27-
// avoid elapsed times higher than one
28-
elapsed = elapsed > 1 ? 1 : elapsed
27+
const time = now()
28+
const elapsed = Math.min((time - context.startTime) / context.duration, 1)
2929

3030
// apply easing to elapsed time
31-
value = ease(elapsed)
31+
const value = context.ease(elapsed)
3232

33-
currentX = context.startX + (context.x - context.startX) * value
34-
currentY = context.startY + (context.y - context.startY) * value
33+
const currentX = context.startX + (context.x - context.startX) * value
34+
const currentY = context.startY + (context.y - context.startY) * value
3535

36-
context.method.call(context.scrollable, currentX, currentY)
36+
context.method(currentX, currentY)
3737

3838
// scroll more if we have not reached our destination
3939
if (currentX !== context.x || currentY !== context.y) {
40-
requestAnimationFrame(step.bind(global, context))
40+
requestAnimationFrame(() => step(context))
4141
}
4242
}
4343

44-
function smoothScroll(el, x, y, cb) {
45-
var scrollable
46-
var startX
47-
var startY
48-
var method
49-
var startTime = now()
44+
function smoothScroll(
45+
el,
46+
x,
47+
y,
48+
duration = 300,
49+
ease = t => 0.5 * (1 - Math.cos(Math.PI * t)),
50+
cb
51+
) {
52+
let scrollable
53+
let startX
54+
let startY
55+
let method
5056

5157
// define scroll context
5258
if (el === document.documentElement) {
5359
scrollable = window
5460
startX = window.scrollX || window.pageXOffset
5561
startY = window.scrollY || window.pageYOffset
56-
method = window.scroll
62+
method = (x, y) => window.scroll(x, y)
5763
} else {
5864
scrollable = el
5965
startX = el.scrollLeft
6066
startY = el.scrollTop
6167
method = (x, y) => {
68+
// @TODO use Element.scroll if it exists, as it is potentially better performing
6269
el.scrollLeft = x
6370
el.scrollTop = y
6471
}
@@ -68,31 +75,58 @@ function smoothScroll(el, x, y, cb) {
6875
step({
6976
scrollable: scrollable,
7077
method: method,
71-
startTime: startTime,
78+
startTime: now(),
7279
startX: startX,
7380
startY: startY,
7481
x: x,
7582
y: y,
83+
duration,
84+
ease,
7685
cb,
7786
})
7887
}
7988

80-
export default (target, options: Options = {}) => {
81-
const { behavior = 'smooth' } = options
89+
const shouldSmoothScroll = <T>(options: any): options is T => {
90+
return (options && !options.behavior) || options.behavior === 'smooth'
91+
}
8292

83-
// @TODO detect if someone is using this library without smooth behavior and maybe warn
84-
if (behavior !== 'smooth') {
85-
return scrollIntoView(target, options)
93+
function scroll(target: Element, options?: SmoothBehaviorOptions): Promise<any>
94+
function scroll<T>(target: Element, options: CustomBehaviorOptions<T>): T
95+
function scroll(target: Element, options: StandardBehaviorOptions): void
96+
function scroll<T>(target, options) {
97+
if (shouldSmoothScroll<SmoothBehaviorOptions>(options)) {
98+
const overrides = options || {}
99+
// @TODO replace <any> in promise signatures with better information
100+
return scrollIntoView<Promise<any>>(target, {
101+
block: overrides.block,
102+
inline: overrides.inline,
103+
scrollMode: overrides.scrollMode,
104+
boundary: overrides.boundary,
105+
behavior: actions =>
106+
Promise.all(
107+
actions.map(
108+
({ el, left, top }) =>
109+
new Promise(resolve =>
110+
smoothScroll(
111+
el,
112+
left,
113+
top,
114+
overrides.duration,
115+
overrides.ease,
116+
() => resolve()
117+
)
118+
)
119+
)
120+
),
121+
})
86122
}
87123

88-
return scrollIntoView(target, {
89-
...options,
90-
behavior: actions =>
91-
Promise.all(
92-
actions.map(
93-
({ el, left, top }) =>
94-
new Promise(resolve => smoothScroll(el, left, top, () => resolve()))
95-
)
96-
),
97-
})
124+
// @TODO maybe warn when someone could be using this library this way unintentionally
125+
126+
return scrollIntoView<T>(target, options)
98127
}
128+
129+
// re-assign here makes the flowtype generation work
130+
const smoothScrollIntoView = scroll
131+
132+
export default smoothScrollIntoView

0 commit comments

Comments
 (0)