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]): | [
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") | [
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") | [
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") | [
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") | [
Austin Wood](https://github.com/indiesquidge)
[πŸ’¬](#question-indiesquidge "Answering Questions") [πŸ“–](https://github.com/paypal/downshift/commits?author=indiesquidge "Documentation") [πŸ‘€](#review-indiesquidge "Reviewed Pull Requests") | [
Mark Murray](https://github.com/mmmurray)
[πŸš‡](#infra-mmmurray "Infrastructure (Hosting, Build-Tools, etc)") | [
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") | | [
Emmanuel Pastor](https://github.com/pastr)
[πŸ’‘](#example-pastr "Examples") | [
dalehurwitz](https://github.com/dalehurwitz)
[πŸ’»](https://github.com/paypal/downshift/commits?author=dalehurwitz "Code") | [
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") | [
Luke Herrington](https://github.com/infiniteluke)
[πŸ’‘](#example-infiniteluke "Examples") | [
Brandon Clemons](https://github.com/drobannx)
[πŸ’»](https://github.com/paypal/downshift/commits?author=drobannx "Code") | [
Kieran](https://github.com/aMollusk)
[πŸ’»](https://github.com/paypal/downshift/commits?author=aMollusk "Code") | [
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") | | [
Cameron Edwards](http://cameronpedwards.com)
[πŸ’»](https://github.com/paypal/downshift/commits?author=cameronprattedwards "Code") [⚠️](https://github.com/paypal/downshift/commits?author=cameronprattedwards "Tests") | [
stereobooster](https://github.com/stereobooster)
[πŸ’»](https://github.com/paypal/downshift/commits?author=stereobooster "Code") [⚠️](https://github.com/paypal/downshift/commits?author=stereobooster "Tests") | [
Trevor Pierce](https://github.com/1Copenut)
[πŸ‘€](#review-1Copenut "Reviewed Pull Requests") | [
Xuefei Li](http://xuefei-frank.com)
[πŸ’»](https://github.com/paypal/downshift/commits?author=franklixuefei "Code") | [
Alfred Ringstad](https://hyperlab.se)
[πŸ’»](https://github.com/paypal/downshift/commits?author=alfredringstad "Code") | [
D[oa]vid Weisz](https://github.com/dovidweisz)
[πŸ’»](https://github.com/paypal/downshift/commits?author=dovidweisz "Code") | [
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") | -| [
MichaΓ«l De Boey](http://michaeldeboey.be)
[πŸ’»](https://github.com/paypal/downshift/commits?author=MichaelDeBoey "Code") | [
Henry](https://github.com/EricHenry)
[πŸ’»](https://github.com/paypal/downshift/commits?author=EricHenry "Code") | [
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") | [
Arthur Denner](https://github.com/arthurdenner)
[πŸ’»](https://github.com/paypal/downshift/commits?author=arthurdenner "Code") | +| [
MichaΓ«l De Boey](http://michaeldeboey.be)
[πŸ’»](https://github.com/paypal/downshift/commits?author=MichaelDeBoey "Code") | [
Henry](https://github.com/EricHenry)
[πŸ’»](https://github.com/paypal/downshift/commits?author=EricHenry "Code") | [
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") | [
Arthur Denner](https://github.com/arthurdenner)
[πŸ’»](https://github.com/paypal/downshift/commits?author=arthurdenner "Code") | [
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,