diff --git a/.all-contributorsrc b/.all-contributorsrc
index 7bab9bc8a..3fac8c0a0 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -872,6 +872,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "stipsan",
+ "name": "Cody Olsen",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/81981?v=4",
+ "profile": "http://twitter.com/stipsan",
+ "contributions": [
+ "code"
+ ]
}
]
}
diff --git a/README.md b/README.md
index 303479fac..b41c72039 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ autocomplete/dropdown/select/combobox components
[![downloads][downloads-badge]][npmcharts] [![version][version-badge]][package]
[![MIT License][license-badge]][license]
-[![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg?style=flat-square)](#contributors)
+[![All Contributors](https://img.shields.io/badge/all_contributors-89-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]][prs] [![Chat][chat-badge]][chat]
[![Code of Conduct][coc-badge]][coc]
[![Join the community on Spectrum][spectrum-badge]][spectrum]
@@ -1020,7 +1020,7 @@ Thanks goes to these people ([emoji key][emojis]):
| [![](https://avatars0.githubusercontent.com/u/29493001?v=4)
Austin Tackaberry](http://austintackaberry.co)
[π¬](#question-austintackaberry "Answering Questions") [π»](https://github.com/paypal/downshift/commits?author=austintackaberry "Code") [π](https://github.com/paypal/downshift/commits?author=austintackaberry "Documentation") [π](https://github.com/paypal/downshift/issues?q=author%3Aaustintackaberry "Bug reports") [π‘](#example-austintackaberry "Examples") [π€](#ideas-austintackaberry "Ideas, Planning, & Feedback") [π](#review-austintackaberry "Reviewed Pull Requests") [β οΈ](https://github.com/paypal/downshift/commits?author=austintackaberry "Tests") | [![](https://avatars3.githubusercontent.com/u/4168055?v=4)
Jean Duthon](https://github.com/jduthon)
[π](https://github.com/paypal/downshift/issues?q=author%3Ajduthon "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=jduthon "Code") | [![](https://avatars3.githubusercontent.com/u/3889580?v=4)
Anton Telesh](http://antontelesh.github.io)
[π](https://github.com/paypal/downshift/issues?q=author%3AAntontelesh "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=Antontelesh "Code") | [![](https://avatars3.githubusercontent.com/u/1060669?v=4)
Eric Edem](https://github.com/ericedem)
[π»](https://github.com/paypal/downshift/commits?author=ericedem "Code") [π](https://github.com/paypal/downshift/commits?author=ericedem "Documentation") [π€](#ideas-ericedem "Ideas, Planning, & Feedback") [β οΈ](https://github.com/paypal/downshift/commits?author=ericedem "Tests") | [![](https://avatars3.githubusercontent.com/u/3409645?v=4)
Austin Wood](https://github.com/indiesquidge)
[π¬](#question-indiesquidge "Answering Questions") [π](https://github.com/paypal/downshift/commits?author=indiesquidge "Documentation") [π](#review-indiesquidge "Reviewed Pull Requests") | [![](https://avatars3.githubusercontent.com/u/14275790?v=4)
Mark Murray](https://github.com/mmmurray)
[π](#infra-mmmurray "Infrastructure (Hosting, Build-Tools, etc)") | [![](https://avatars0.githubusercontent.com/u/1862172?v=4)
Gianmarco](https://github.com/gsimone)
[π](https://github.com/paypal/downshift/issues?q=author%3Agsimone "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=gsimone "Code") |
| [![](https://avatars2.githubusercontent.com/u/6838136?v=4)
Emmanuel Pastor](https://github.com/pastr)
[π‘](#example-pastr "Examples") | [![](https://avatars2.githubusercontent.com/u/10345034?v=4)
dalehurwitz](https://github.com/dalehurwitz)
[π»](https://github.com/paypal/downshift/commits?author=dalehurwitz "Code") | [![](https://avatars1.githubusercontent.com/u/4813007?v=4)
Bogdan Lobor](https://github.com/blobor)
[π](https://github.com/paypal/downshift/issues?q=author%3Ablobor "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=blobor "Code") | [![](https://avatars0.githubusercontent.com/u/1127238?v=4)
Luke Herrington](https://github.com/infiniteluke)
[π‘](#example-infiniteluke "Examples") | [![](https://avatars2.githubusercontent.com/u/6361167?v=4)
Brandon Clemons](https://github.com/drobannx)
[π»](https://github.com/paypal/downshift/commits?author=drobannx "Code") | [![](https://avatars0.githubusercontent.com/u/10591587?v=4)
Kieran](https://github.com/aMollusk)
[π»](https://github.com/paypal/downshift/commits?author=aMollusk "Code") | [![](https://avatars3.githubusercontent.com/u/11570627?v=4)
Brushedoctopus](https://github.com/Brushedoctopus)
[π](https://github.com/paypal/downshift/issues?q=author%3ABrushedoctopus "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=Brushedoctopus "Code") |
| [![](https://avatars3.githubusercontent.com/u/5456216?v=4)
Cameron Edwards](http://cameronpedwards.com)
[π»](https://github.com/paypal/downshift/commits?author=cameronprattedwards "Code") [β οΈ](https://github.com/paypal/downshift/commits?author=cameronprattedwards "Tests") | [![](https://avatars2.githubusercontent.com/u/179534?v=4)
stereobooster](https://github.com/stereobooster)
[π»](https://github.com/paypal/downshift/commits?author=stereobooster "Code") [β οΈ](https://github.com/paypal/downshift/commits?author=stereobooster "Tests") | [![](https://avatars0.githubusercontent.com/u/934879?v=4)
Trevor Pierce](https://github.com/1Copenut)
[π](#review-1Copenut "Reviewed Pull Requests") | [![](https://avatars1.githubusercontent.com/u/1334982?v=4)
Xuefei Li](http://xuefei-frank.com)
[π»](https://github.com/paypal/downshift/commits?author=franklixuefei "Code") | [![](https://avatars0.githubusercontent.com/u/7252803?v=4)
Alfred Ringstad](https://hyperlab.se)
[π»](https://github.com/paypal/downshift/commits?author=alfredringstad "Code") | [![](https://avatars0.githubusercontent.com/u/6895497?v=4)
D[oa]vid Weisz](https://github.com/dovidweisz)
[π»](https://github.com/paypal/downshift/commits?author=dovidweisz "Code") | [![](https://avatars0.githubusercontent.com/u/19773?v=4)
Royston Shufflebotham](https://github.com/RoystonS)
[π](https://github.com/paypal/downshift/issues?q=author%3ARoystonS "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=RoystonS "Code") |
-| [![](https://avatars3.githubusercontent.com/u/6643991?v=4)
MichaΓ«l De Boey](http://michaeldeboey.be)
[π»](https://github.com/paypal/downshift/commits?author=MichaelDeBoey "Code") | [![](https://avatars3.githubusercontent.com/u/4412771?v=4)
Henry](https://github.com/EricHenry)
[π»](https://github.com/paypal/downshift/commits?author=EricHenry "Code") | [![](https://avatars3.githubusercontent.com/u/2180127?v=4)
Andrew Walton](http://www.greenarrow.me)
[π](https://github.com/paypal/downshift/issues?q=author%3Agreen-arrow "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=green-arrow "Code") [β οΈ](https://github.com/paypal/downshift/commits?author=green-arrow "Tests") | [![](https://avatars0.githubusercontent.com/u/13774309?v=4)
Arthur Denner](https://github.com/arthurdenner)
[π»](https://github.com/paypal/downshift/commits?author=arthurdenner "Code") |
+| [![](https://avatars3.githubusercontent.com/u/6643991?v=4)
MichaΓ«l De Boey](http://michaeldeboey.be)
[π»](https://github.com/paypal/downshift/commits?author=MichaelDeBoey "Code") | [![](https://avatars3.githubusercontent.com/u/4412771?v=4)
Henry](https://github.com/EricHenry)
[π»](https://github.com/paypal/downshift/commits?author=EricHenry "Code") | [![](https://avatars3.githubusercontent.com/u/2180127?v=4)
Andrew Walton](http://www.greenarrow.me)
[π](https://github.com/paypal/downshift/issues?q=author%3Agreen-arrow "Bug reports") [π»](https://github.com/paypal/downshift/commits?author=green-arrow "Code") [β οΈ](https://github.com/paypal/downshift/commits?author=green-arrow "Tests") | [![](https://avatars0.githubusercontent.com/u/13774309?v=4)
Arthur Denner](https://github.com/arthurdenner)
[π»](https://github.com/paypal/downshift/commits?author=arthurdenner "Code") | [![](https://avatars2.githubusercontent.com/u/81981?v=4)
Cody Olsen](http://twitter.com/stipsan)
[π»](https://github.com/paypal/downshift/commits?author=stipsan "Code") |
diff --git a/package.json b/package.json
index 70f5d43a1..b0f926cb9 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"react": ">=0.14.9"
},
"dependencies": {
+ "compute-scroll-into-view": "^1.0.2",
"prop-types": "^15.6.0"
},
"devDependencies": {
diff --git a/src/__tests__/utils.findParent.js b/src/__tests__/utils.findParent.js
deleted file mode 100644
index 91a93b2bd..000000000
--- a/src/__tests__/utils.findParent.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import {findParent} from '../utils'
-
-test('return documentElement when founded node is document.body and scrollTop is 0', () => {
- const node = document.createElement('div')
- const parentNode = document.body
- parentNode.appendChild(node)
- const scrollParent = findParent(el => el === parentNode, node, parentNode)
- expect(scrollParent).toBe(document.documentElement)
-})
diff --git a/src/__tests__/utils.scroll-into-view.js b/src/__tests__/utils.scroll-into-view.js
index ac7b73070..dc89dc310 100644
--- a/src/__tests__/utils.scroll-into-view.js
+++ b/src/__tests__/utils.scroll-into-view.js
@@ -49,28 +49,6 @@ test('aligns to top when the node is above the scrollable parent', () => {
expect(scrollableNode.scrollTop).toBe(5)
})
-test('aligns to top when the node is above view area', () => {
- const node = getNode({height: 40, top: -15})
- const scrollableNode = getScrollableNode({
- top: -50,
- scrollTop: 100,
- children: [node],
- })
- scrollIntoView(node, scrollableNode)
- expect(scrollableNode.scrollTop).toBe(85)
-})
-
-test('aligns to top of view area when the node is above view area and scrollable parent top', () => {
- const node = getNode({height: 40, top: -75})
- const scrollableNode = getScrollableNode({
- top: -50,
- scrollTop: 100,
- children: [node],
- })
- scrollIntoView(node, scrollableNode)
- expect(scrollableNode.scrollTop).toBe(25)
-})
-
test('aligns to top of scrollable parent when the node is above view area', () => {
const node = getNode({height: 40, top: -50})
const scrollableNode = getScrollableNode({
@@ -82,18 +60,6 @@ test('aligns to top of scrollable parent when the node is above view area', () =
expect(scrollableNode.scrollTop).toBe(0)
})
-test('aligns to bottom when the node is below the scrollable parent and parent top above view area', () => {
- const node = getNode({height: 40, top: 280})
- const scrollableNode = getScrollableNode({
- top: -60,
- height: 360,
- scrollTop: 28,
- children: [node],
- })
- scrollIntoView(node, scrollableNode)
- expect(scrollableNode.scrollTop).toBe(48)
-})
-
test('aligns to bottom when the node is below the scrollable parent', () => {
const nodeTop = 115
const node = getNode({height: 10, top: nodeTop})
@@ -105,25 +71,11 @@ test('aligns to bottom when the node is below the scrollable parent', () => {
expect(scrollableNode.scrollTop).toBe(25)
})
-test('aligns to bottom when the the node is below the scrollable parent and scrollable parent has a border', () => {
- const nodeTop = 115
- const node = getNode({height: 10, top: nodeTop})
- const scrollableNode = getScrollableNode({
- height: 100,
- children: [node],
- borderBottomWidth: '2px',
- borderTopWidth: '2px',
- })
- scrollIntoView(node, scrollableNode)
- expect(scrollableNode.scrollTop).toBe(27)
-})
-
function getScrollableNode(overrides = {}) {
return getNode({
height: 100,
top: 0,
scrollTop: 0,
- clientHeight: 50,
scrollHeight: 150,
...overrides,
})
@@ -154,8 +106,10 @@ function getNode({
Object.defineProperties(div, {
clientHeight: {value: clientHeight},
+ offsetHeight: {value: clientHeight},
scrollHeight: {value: scrollHeight},
})
children.forEach(child => div.appendChild(child))
+ document.documentElement.appendChild(div)
return div
}
diff --git a/src/utils.js b/src/utils.js
index 5e35549e7..4eeffb659 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,3 +1,5 @@
+import computeScrollIntoView from 'compute-scroll-into-view'
+
let idCounter = 0
/**
@@ -13,98 +15,25 @@ function cbToCb(cb) {
}
function noop() {}
-function findParent(finder, node, rootNode) {
- if (node !== null && node !== rootNode.parentNode) {
- if (finder(node)) {
- if (node === document.body && node.scrollTop === 0) {
- // in chrome body.scrollTop always return 0
- return document.documentElement
- }
- return node
- } else {
- return findParent(finder, node.parentNode, rootNode)
- }
- } else {
- return null
- }
-}
-
-/**
- * Get the closest element that scrolls
- * @param {HTMLElement} node - the child element to start searching for scroll parent at
- * @param {HTMLElement} rootNode - the root element of the component
- * @return {HTMLElement} the closest parentNode that scrolls
- */
-const getClosestScrollParent = findParent.bind(
- null,
- node => node.scrollHeight > node.clientHeight,
-)
-
/**
* Scroll node into view if necessary
* @param {HTMLElement} node - the element that should scroll into view
* @param {HTMLElement} rootNode - the root element of the component
- * @param {Boolean} alignToTop - align element to the top of the visible area of the scrollable ancestor
*/
-// eslint-disable-next-line complexity
function scrollIntoView(node, rootNode) {
- const scrollParent = getClosestScrollParent(node, rootNode)
- if (scrollParent === null) {
+ if (node === null) {
return
}
- const scrollParentStyles = getComputedStyle(scrollParent)
- const scrollParentRect = scrollParent.getBoundingClientRect()
- const scrollParentBorderTopWidth = parseInt(
- scrollParentStyles.borderTopWidth,
- 10,
- )
- const scrollParentBorderBottomWidth = parseInt(
- scrollParentStyles.borderBottomWidth,
- 10,
- )
- const bordersWidth =
- scrollParentBorderTopWidth + scrollParentBorderBottomWidth
- const scrollParentTop = scrollParentRect.top + scrollParentBorderTopWidth
- const nodeRect = node.getBoundingClientRect()
- if (nodeRect.top < 0 && scrollParentRect.top < 0) {
- scrollParent.scrollTop += nodeRect.top
- return
- }
-
- if (nodeRect.top < 0) {
- // the item is above the viewport and the parent is not above the viewport
- scrollParent.scrollTop += nodeRect.top - scrollParentTop
- return
- }
-
- if (nodeRect.top > 0 && scrollParentRect.top < 0) {
- if (
- scrollParentRect.bottom > 0 &&
- nodeRect.bottom + bordersWidth > scrollParentRect.bottom
- ) {
- // the item is below scrollable area
- scrollParent.scrollTop +=
- nodeRect.bottom - scrollParentRect.bottom + bordersWidth
- }
- // item and parent top are on different sides of view top border (do nothing)
- return
- }
-
- const nodeOffsetTop = nodeRect.top + scrollParent.scrollTop
- const nodeTop = nodeOffsetTop - scrollParentTop
- if (nodeTop < scrollParent.scrollTop) {
- // the item is above the scrollable area
- scrollParent.scrollTop = nodeTop
- } else if (
- nodeTop + nodeRect.height + bordersWidth >
- scrollParent.scrollTop + scrollParentRect.height
- ) {
- // the item is below the scrollable area
- scrollParent.scrollTop =
- nodeTop + nodeRect.height - scrollParentRect.height + bordersWidth
- }
- // the item is within the scrollable area (do nothing)
+ const actions = computeScrollIntoView(node, {
+ boundary: rootNode,
+ block: 'nearest',
+ scrollMode: 'if-needed',
+ })
+ actions.forEach(({el, top, left}) => {
+ el.scrollTop = top
+ el.scrollLeft = left
+ })
}
/**
@@ -313,7 +242,6 @@ export {
callAll,
debounce,
scrollIntoView,
- findParent,
generateId,
getA11yStatusMessage,
unwrapArray,