Skip to content

Commit 07e6533

Browse files
authored
fix(typings): Improve TS typings (#223)
* prepare to add flowtype tests * improve the quality of the typescript libdefs * Improve typescript declarations * improve further * update readme etc * Update index.ts
1 parent a486bd7 commit 07e6533

File tree

8 files changed

+239
-57
lines changed

8 files changed

+239
-57
lines changed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,55 @@ scrollIntoView(target, {
250250
})
251251
```
252252

253+
# TypeScript support
254+
255+
When the library itself is built on TypeScript there's no excuse for not publishing great library definitions!
256+
257+
This goes beyond just checking if you misspelled `behavior: 'smoooth'` to the return type of a custom behavior:
258+
259+
```typescript
260+
const scrolling = scrollIntoView(document.body, {
261+
behavior: actions => {
262+
return new Promise(
263+
...
264+
)
265+
},
266+
})
267+
// jest understands that scrolling is a Promise, you can safely await on it
268+
scrolling.then(() => console.log('done scrolling'))
269+
```
270+
271+
You can optionally use a generic to ensure that `options.behavior` is the expected type.
272+
It can be useful if the custom behavior is implemented in another module:
273+
274+
```typescript
275+
const customBehavior = actions => {
276+
return new Promise(
277+
...
278+
)
279+
}
280+
281+
const scrolling = scrollIntoView<Promise<any>>(document.body, {
282+
behavior: customBehavior
283+
})
284+
// throws if customBehavior does not return a promise
285+
```
286+
287+
The options are available for you if you are wrapping this libary in another abstraction (like a React component):
288+
289+
```typescript
290+
import scrollIntoView, { Options } from 'scroll-into-view-if-needed'
291+
292+
interface CustomOptions extends Options {
293+
useBoundary?: boolean
294+
}
295+
296+
function scrollToTarget(selector, options: Options = {}) {
297+
const { useBoundary = false, ...scrollOptions } = options
298+
return scrollIntoView(document.querySelector(selector), scrollOptions)
299+
}
300+
```
301+
253302
# Breaking API changes from v1
254303

255304
Since v1 ponyfilled Element.scrollIntoViewIfNeeded, while v2 ponyfills Element.scrollIntoView, there are breaking changes from the differences in their APIs.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"version": "2.0.0-dev",
1212
"main": "index.js",
13+
"module": "es/index.js",
1314
"files": [
1415
"compute.js",
1516
"es",
@@ -45,6 +46,7 @@
4546
"eslint-config-prettier": "2.9.0",
4647
"eslint-plugin-import": "2.11.0",
4748
"eslint-plugin-react": "7.7.0",
49+
"flowgen": "1.2.1",
4850
"husky": "0.14.3",
4951
"lint-staged": "7.1.0",
5052
"prettier": "1.12.1",
@@ -118,7 +120,6 @@
118120
"git add"
119121
]
120122
},
121-
"module": "es/index.js",
122123
"prettier": {
123124
"semi": false,
124125
"singleQuote": true,

src/compute.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,14 @@
99
// add support for visualViewport object currently implemented in chrome
1010
declare global {
1111
interface Window {
12-
visualViewport: {
12+
visualViewport?: {
1313
height: number
1414
width: number
1515
}
1616
}
1717
}
1818

19-
export interface checkBoundary {
20-
(parent: Element): boolean
21-
}
22-
export interface Options extends ScrollIntoViewOptions {
23-
// This new option is tracked in this PR, which is the most likely candidate at the time: https://github.com/w3c/csswg-drafts/pull/1805
24-
scrollMode?: 'always' | 'if-needed'
25-
// This option is not in any spec and specific to this library
26-
boundary?: Element | checkBoundary
27-
}
19+
import { CustomScrollAction, Options } from './types'
2820

2921
const isElement = el => el != null && typeof el == 'object' && el.nodeType === 1
3022
const hasScrollableSpace = (el, axis: 'Y' | 'X') => {
@@ -39,7 +31,7 @@ const hasScrollableSpace = (el, axis: 'Y' | 'X') => {
3931
return false
4032
}
4133
const canOverflow = (el, axis: 'Y' | 'X') => {
42-
const overflowValue = window.getComputedStyle(el, null)['overflow' + axis]
34+
const overflowValue = getComputedStyle(el, null)['overflow' + axis]
4335

4436
return overflowValue !== 'visible' && overflowValue !== 'clip'
4537
}
@@ -193,7 +185,7 @@ const alignNearest = (
193185
export default (
194186
target: Element,
195187
options: Options = {}
196-
): { el: Element; top: number; left: number }[] => {
188+
): CustomScrollAction[] => {
197189
const { scrollMode, block, inline, boundary } = {
198190
scrollMode: 'always',
199191
block: 'center',
@@ -269,11 +261,7 @@ export default (
269261
let targetInline
270262

271263
// Collect new scroll positions
272-
const computations = frames.map((frame): {
273-
el: Element
274-
top: number
275-
left: number
276-
} => {
264+
const computations = frames.map((frame): CustomScrollAction => {
277265
const frameRect = frame.getBoundingClientRect()
278266
// @TODO fix hardcoding of block => top/Y
279267

src/index.ts

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,42 @@
1-
import compute, { Options as ComputeOptions } from './compute'
1+
import compute from './compute'
2+
import {
3+
ScrollBehavior,
4+
CustomScrollBehaviorCallback,
5+
CustomScrollAction,
6+
Options as BaseOptions,
7+
} from './types'
28

3-
export interface Options {
4-
behavior?: 'auto' | 'smooth' | 'instant' | Function
5-
scrollMode?: ComputeOptions['scrollMode']
6-
boundary?: ComputeOptions['boundary']
7-
block?: ComputeOptions['block']
8-
inline?: ComputeOptions['inline']
9+
export interface StandardBehaviorOptions extends BaseOptions {
10+
behavior?: ScrollBehavior
11+
}
12+
export interface CustomBehaviorOptions<T> extends BaseOptions {
13+
behavior: CustomScrollBehaviorCallback<T>
14+
}
15+
16+
export interface Options<T = any> extends BaseOptions {
17+
behavior?: ScrollBehavior | CustomScrollBehaviorCallback<T>
918
}
1019

1120
// Wait with checking if native smooth-scrolling exists until scrolling is invoked
1221
// This is much more friendly to server side rendering envs, and testing envs like jest
1322
let supportsScrollBehavior
1423

15-
// Some people might use both "auto" and "ponyfill" modes in the same file, so we also provide a named export so
16-
// that imports in userland code (like if they use native smooth scrolling on some browsers, and the ponyfill for everything else)
17-
// the named export allows this `import {auto as autoScrollIntoView, ponyfill as smoothScrollIntoView} from ...`
18-
export default (target: Element, maybeOptions: Options | boolean = true) => {
19-
let options: Options = {}
24+
const isFunction = (arg: any): arg is Function => {
25+
return typeof arg == 'function'
26+
}
27+
const isOptionsObject = <T>(options: any): options is T => {
28+
return options === Object(options) && Object.keys(options).length !== 0
29+
}
2030

31+
const defaultBehavior = (
32+
actions: CustomScrollAction[],
33+
behavior: ScrollBehavior = 'auto'
34+
) => {
2135
if (supportsScrollBehavior === undefined) {
2236
supportsScrollBehavior = 'scrollBehavior' in document.documentElement.style
2337
}
2438

25-
// Handle alignToTop for legacy reasons, to be compatible with the spec
26-
if (maybeOptions === true || maybeOptions === null) {
27-
options = { block: 'start', inline: 'nearest' }
28-
} else if (maybeOptions === false) {
29-
options = { block: 'end', inline: 'nearest' }
30-
} else if (maybeOptions === Object(maybeOptions)) {
31-
// @TODO check if passing an empty object is handled like defined by the spec (for now it makes the web platform tests pass)
32-
options =
33-
Object.keys(maybeOptions).length === 0
34-
? { block: 'start', inline: 'nearest' }
35-
: { block: 'center', inline: 'nearest', ...maybeOptions }
36-
}
37-
38-
const { behavior = 'auto', ...computeOptions } = options
39-
const instructions = compute(target, computeOptions)
40-
41-
if (typeof behavior == 'function') {
42-
return behavior(instructions)
43-
}
44-
45-
instructions.forEach(({ el, top, left }) => {
39+
actions.forEach(({ el, top, left }) => {
4640
// browser implements the new Element.prototype.scroll API that supports `behavior`
4741
// and guard window.scroll with supportsScrollBehavior
4842
if (el.scroll && supportsScrollBehavior) {
@@ -57,3 +51,42 @@ export default (target: Element, maybeOptions: Options | boolean = true) => {
5751
}
5852
})
5953
}
54+
55+
const getOptions = (options: any = true): StandardBehaviorOptions => {
56+
// Handle alignToTop for legacy reasons, to be compatible with the spec
57+
if (options === true || options === null) {
58+
return { block: 'start', inline: 'nearest' }
59+
} else if (options === false) {
60+
return { block: 'end', inline: 'nearest' }
61+
} else if (isOptionsObject<StandardBehaviorOptions>(options)) {
62+
return { block: 'center', inline: 'nearest', ...options }
63+
}
64+
65+
// if options = {}, based on w3c web platform test
66+
return { block: 'start', inline: 'nearest' }
67+
}
68+
69+
// Some people might use both "auto" and "ponyfill" modes in the same file, so we also provide a named export so
70+
// that imports in userland code (like if they use native smooth scrolling on some browsers, and the ponyfill for everything else)
71+
// the named export allows this `import {auto as autoScrollIntoView, ponyfill as smoothScrollIntoView} from ...`
72+
function scrollIntoView<T>(
73+
target: Element,
74+
options: CustomBehaviorOptions<T>
75+
): T
76+
function scrollIntoView(target: Element, options?: Options | boolean): void
77+
function scrollIntoView<T>(target, options: Options<T> | boolean = true) {
78+
if (
79+
isOptionsObject<CustomBehaviorOptions<T>>(options) &&
80+
isFunction(options.behavior)
81+
) {
82+
return options.behavior(compute(target, options))
83+
}
84+
85+
const computeOptions = getOptions(options)
86+
return defaultBehavior(
87+
compute(target, computeOptions),
88+
computeOptions.behavior
89+
)
90+
}
91+
92+
export default scrollIntoView

src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Standard, based on CSSOM View spec
2+
export type ScrollBehavior = 'auto' | 'instant' | 'smooth'
3+
export type ScrollLogicalPosition = 'start' | 'center' | 'end' | 'nearest'
4+
// This new option is tracked in this PR, which is the most likely candidate at the time: https://github.com/w3c/csswg-drafts/pull/1805
5+
export type ScrollMode = 'always' | 'if-needed'
6+
7+
export interface Options {
8+
block?: ScrollLogicalPosition
9+
inline?: ScrollLogicalPosition
10+
scrollMode?: ScrollMode
11+
boundary?: CustomScrollBoundary
12+
}
13+
14+
// Custom behavior, not in any spec
15+
export interface CustomScrollBoundaryCallback {
16+
(parent: Element): boolean
17+
}
18+
export type CustomScrollBoundary = Element | CustomScrollBoundaryCallback
19+
export type CustomScrollAction = { el: Element; top: number; left: number }
20+
export interface CustomScrollBehaviorCallback<T> {
21+
(actions: CustomScrollAction[]): T
22+
}

tests/flowtype/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"private": true,
3+
"dependencies": {
4+
"scroll-into-view-if-needed": "link:../.."
5+
},
6+
"devDependencies": {
7+
"flow-bin": "0.71.0"
8+
}
9+
}

tests/flowtype/yarn.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
6+
version "0.71.0"
7+
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.71.0.tgz#fd1b27a6458c3ebaa5cb811853182ed631918b70"
8+
9+
"scroll-into-view-if-needed@link:../..":
10+
version "0.0.0"
11+
uid ""

0 commit comments

Comments
 (0)