Skip to content

Commit

Permalink
fix(typings): Improve TS typings (#223)
Browse files Browse the repository at this point in the history
* prepare to add flowtype tests

* improve the quality of the typescript libdefs

* Improve typescript declarations

* improve further

* update readme etc

* Update index.ts
  • Loading branch information
stipsan authored May 7, 2018
1 parent a486bd7 commit 07e6533
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 57 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,55 @@ scrollIntoView(target, {
})
```

# TypeScript support

When the library itself is built on TypeScript there's no excuse for not publishing great library definitions!

This goes beyond just checking if you misspelled `behavior: 'smoooth'` to the return type of a custom behavior:

```typescript
const scrolling = scrollIntoView(document.body, {
behavior: actions => {
return new Promise(
...
)
},
})
// jest understands that scrolling is a Promise, you can safely await on it
scrolling.then(() => console.log('done scrolling'))
```

You can optionally use a generic to ensure that `options.behavior` is the expected type.
It can be useful if the custom behavior is implemented in another module:

```typescript
const customBehavior = actions => {
return new Promise(
...
)
}

const scrolling = scrollIntoView<Promise<any>>(document.body, {
behavior: customBehavior
})
// throws if customBehavior does not return a promise
```

The options are available for you if you are wrapping this libary in another abstraction (like a React component):

```typescript
import scrollIntoView, { Options } from 'scroll-into-view-if-needed'

interface CustomOptions extends Options {
useBoundary?: boolean
}

function scrollToTarget(selector, options: Options = {}) {
const { useBoundary = false, ...scrollOptions } = options
return scrollIntoView(document.querySelector(selector), scrollOptions)
}
```

# Breaking API changes from v1

Since v1 ponyfilled Element.scrollIntoViewIfNeeded, while v2 ponyfills Element.scrollIntoView, there are breaking changes from the differences in their APIs.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"version": "2.0.0-dev",
"main": "index.js",
"module": "es/index.js",
"files": [
"compute.js",
"es",
Expand Down Expand Up @@ -45,6 +46,7 @@
"eslint-config-prettier": "2.9.0",
"eslint-plugin-import": "2.11.0",
"eslint-plugin-react": "7.7.0",
"flowgen": "1.2.1",
"husky": "0.14.3",
"lint-staged": "7.1.0",
"prettier": "1.12.1",
Expand Down Expand Up @@ -118,7 +120,6 @@
"git add"
]
},
"module": "es/index.js",
"prettier": {
"semi": false,
"singleQuote": true,
Expand Down
22 changes: 5 additions & 17 deletions src/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,14 @@
// add support for visualViewport object currently implemented in chrome
declare global {
interface Window {
visualViewport: {
visualViewport?: {
height: number
width: number
}
}
}

export interface checkBoundary {
(parent: Element): boolean
}
export interface Options extends ScrollIntoViewOptions {
// 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
scrollMode?: 'always' | 'if-needed'
// This option is not in any spec and specific to this library
boundary?: Element | checkBoundary
}
import { CustomScrollAction, Options } from './types'

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

return overflowValue !== 'visible' && overflowValue !== 'clip'
}
Expand Down Expand Up @@ -193,7 +185,7 @@ const alignNearest = (
export default (
target: Element,
options: Options = {}
): { el: Element; top: number; left: number }[] => {
): CustomScrollAction[] => {
const { scrollMode, block, inline, boundary } = {
scrollMode: 'always',
block: 'center',
Expand Down Expand Up @@ -269,11 +261,7 @@ export default (
let targetInline

// Collect new scroll positions
const computations = frames.map((frame): {
el: Element
top: number
left: number
} => {
const computations = frames.map((frame): CustomScrollAction => {
const frameRect = frame.getBoundingClientRect()
// @TODO fix hardcoding of block => top/Y

Expand Down
99 changes: 66 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,42 @@
import compute, { Options as ComputeOptions } from './compute'
import compute from './compute'
import {
ScrollBehavior,
CustomScrollBehaviorCallback,
CustomScrollAction,
Options as BaseOptions,
} from './types'

export interface Options {
behavior?: 'auto' | 'smooth' | 'instant' | Function
scrollMode?: ComputeOptions['scrollMode']
boundary?: ComputeOptions['boundary']
block?: ComputeOptions['block']
inline?: ComputeOptions['inline']
export interface StandardBehaviorOptions extends BaseOptions {
behavior?: ScrollBehavior
}
export interface CustomBehaviorOptions<T> extends BaseOptions {
behavior: CustomScrollBehaviorCallback<T>
}

export interface Options<T = any> extends BaseOptions {
behavior?: ScrollBehavior | CustomScrollBehaviorCallback<T>
}

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

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

const defaultBehavior = (
actions: CustomScrollAction[],
behavior: ScrollBehavior = 'auto'
) => {
if (supportsScrollBehavior === undefined) {
supportsScrollBehavior = 'scrollBehavior' in document.documentElement.style
}

// Handle alignToTop for legacy reasons, to be compatible with the spec
if (maybeOptions === true || maybeOptions === null) {
options = { block: 'start', inline: 'nearest' }
} else if (maybeOptions === false) {
options = { block: 'end', inline: 'nearest' }
} else if (maybeOptions === Object(maybeOptions)) {
// @TODO check if passing an empty object is handled like defined by the spec (for now it makes the web platform tests pass)
options =
Object.keys(maybeOptions).length === 0
? { block: 'start', inline: 'nearest' }
: { block: 'center', inline: 'nearest', ...maybeOptions }
}

const { behavior = 'auto', ...computeOptions } = options
const instructions = compute(target, computeOptions)

if (typeof behavior == 'function') {
return behavior(instructions)
}

instructions.forEach(({ el, top, left }) => {
actions.forEach(({ el, top, left }) => {
// browser implements the new Element.prototype.scroll API that supports `behavior`
// and guard window.scroll with supportsScrollBehavior
if (el.scroll && supportsScrollBehavior) {
Expand All @@ -57,3 +51,42 @@ export default (target: Element, maybeOptions: Options | boolean = true) => {
}
})
}

const getOptions = (options: any = true): StandardBehaviorOptions => {
// Handle alignToTop for legacy reasons, to be compatible with the spec
if (options === true || options === null) {
return { block: 'start', inline: 'nearest' }
} else if (options === false) {
return { block: 'end', inline: 'nearest' }
} else if (isOptionsObject<StandardBehaviorOptions>(options)) {
return { block: 'center', inline: 'nearest', ...options }
}

// if options = {}, based on w3c web platform test
return { block: 'start', inline: 'nearest' }
}

// Some people might use both "auto" and "ponyfill" modes in the same file, so we also provide a named export so
// that imports in userland code (like if they use native smooth scrolling on some browsers, and the ponyfill for everything else)
// the named export allows this `import {auto as autoScrollIntoView, ponyfill as smoothScrollIntoView} from ...`
function scrollIntoView<T>(
target: Element,
options: CustomBehaviorOptions<T>
): T
function scrollIntoView(target: Element, options?: Options | boolean): void
function scrollIntoView<T>(target, options: Options<T> | boolean = true) {
if (
isOptionsObject<CustomBehaviorOptions<T>>(options) &&
isFunction(options.behavior)
) {
return options.behavior(compute(target, options))
}

const computeOptions = getOptions(options)
return defaultBehavior(
compute(target, computeOptions),
computeOptions.behavior
)
}

export default scrollIntoView
22 changes: 22 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Standard, based on CSSOM View spec
export type ScrollBehavior = 'auto' | 'instant' | 'smooth'
export type ScrollLogicalPosition = 'start' | 'center' | 'end' | 'nearest'
// 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
export type ScrollMode = 'always' | 'if-needed'

export interface Options {
block?: ScrollLogicalPosition
inline?: ScrollLogicalPosition
scrollMode?: ScrollMode
boundary?: CustomScrollBoundary
}

// Custom behavior, not in any spec
export interface CustomScrollBoundaryCallback {
(parent: Element): boolean
}
export type CustomScrollBoundary = Element | CustomScrollBoundaryCallback
export type CustomScrollAction = { el: Element; top: number; left: number }
export interface CustomScrollBehaviorCallback<T> {
(actions: CustomScrollAction[]): T
}
9 changes: 9 additions & 0 deletions tests/flowtype/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"private": true,
"dependencies": {
"scroll-into-view-if-needed": "link:../.."
},
"devDependencies": {
"flow-bin": "0.71.0"
}
}
11 changes: 11 additions & 0 deletions tests/flowtype/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


[email protected]:
version "0.71.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.71.0.tgz#fd1b27a6458c3ebaa5cb811853182ed631918b70"

"scroll-into-view-if-needed@link:../..":
version "0.0.0"
uid ""
Loading

0 comments on commit 07e6533

Please sign in to comment.