Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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