Last active
July 13, 2022 21:43
-
-
Save mogelbrod/7029848de62016bf0f0b38fb56f8da3e to your computer and use it in GitHub Desktop.
useVirtualScrollParent() hook using useVirtual() from react-virtual
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const html = document.documentElement | |
const body = document.body | |
/** | |
Pseudo HTML element which behaves like a regular element in terms of scrolling. | |
*/ | |
const documentElement = { | |
set scrollLeft(x) { html.scrollLeft = x }, | |
get scrollLeft() { return window.scrollX || window.pageXOffset }, | |
set scrollTop(x) { html.scrollTop = x }, | |
get scrollTop() { return window.scrollY || window.pageYOffset }, | |
get scrollWidth() { | |
return Math.max( | |
body.scrollWidth, html.scrollWidth, | |
body.offsetWidth, html.offsetWidth, | |
body.clientWidth, html.clientWidth, | |
) | |
}, | |
get scrollHeight() { | |
return Math.max( | |
body.scrollHeight, html.scrollHeight, | |
body.offsetHeight, html.offsetHeight, | |
body.clientHeight, html.clientHeight, | |
) | |
}, | |
get clientWidth() { return html.clientWidth }, | |
get clientHeight() { return html.clientHeight }, | |
getBoundingClientRect() { | |
return { | |
x: 0, | |
y: 0, | |
top: 0, | |
left: 0, | |
width: this.clientWidth, | |
height: this.clientHeight, | |
} | |
}, | |
addEventListener(event, ...args) { | |
const target = event === 'scroll' ? window : html | |
return target.addEventListener(event, ...args) | |
}, | |
removeEventListener(event, ...args) { | |
const target = event === 'scroll' ? window : html | |
return target.removeEventListener(event, ...args) | |
}, | |
} | |
export default documentElement |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { useVirtual } from 'react-virtual' | |
import documentElement from './document-element' | |
export default function useVirtualScrollParent(options) { | |
const sizeKey = options.horizontal ? 'width' : 'height' | |
const { parentRef } = options | |
const [rowSize, setRowSize] = React.useState(options.estimateSize(0)) | |
// Mock the API surface currently used through parentRef | |
const mockedParentRef = React.useRef(null) | |
React.useLayoutEffect(() => { | |
const scrolled = parentRef.current | |
if (!scrolled) { return } | |
let scrollParent = findScrollParent(scrolled) | |
if (scrollParent === document.documentElement) { | |
scrollParent = documentElement | |
} | |
let originalScrollListener = null | |
const scrollListener = (originalEvent) => { | |
// Compensate for potential <body> offset (due to disabled scrolling for example) | |
const bodyOffset = scrollParent === documentElement | |
? parseInt(document.body.style.top || '0', 10) | |
: 0 | |
const target = { | |
scrollLeft: scrollParent.scrollLeft - scrolled.offsetLeft, | |
scrollTop: scrollParent.scrollTop - scrolled.offsetTop - bodyOffset, | |
} | |
originalScrollListener({ target }) | |
} | |
mockedParentRef.current = { | |
get scrollLeft() { | |
return scrollParent.scrollLeft - scrolled.offsetLeft | |
}, | |
set scrollLeft(x) { | |
scrollParent.scrollLeft = x + scrolled.offsetLeft | |
}, | |
get scrollTop() { | |
return scrollParent.scrollTop - scrolled.offsetTop | |
}, | |
set scrollTop(x) { | |
scrollParent.scrollTop = x + scrolled.offsetTop | |
}, | |
getBoundingClientRect: () => ({ | |
width: scrollParent.clientWidth, | |
height: scrollParent.clientHeight, | |
}), | |
addEventListener: (type, listener, ...args) => { | |
// Only proxy 'scroll' event listeners | |
if (type === 'scroll') { | |
originalScrollListener = listener | |
listener = scrollListener | |
args = [] // has to be reset for IE11 to be able to later remove it | |
listener() | |
} | |
return scrollParent.addEventListener(type, listener, ...args) | |
}, | |
removeEventListener: (type, listener, ...args) => { | |
if (type === 'scroll') { | |
listener = scrollListener | |
args = [] | |
} | |
return scrollParent.removeEventListener(type, listener, ...args) | |
} | |
} | |
}, [parentRef]) | |
const rowVirtualizer = useVirtual({ | |
...options, | |
parentRef: mockedParentRef, | |
estimateSize: React.useCallback(() => rowSize, [rowSize]), | |
}) | |
// Calculate row size from first item only | |
const sizedElementRef = React.useRef() | |
const estimateSizeRef = React.useCallback(element => { | |
sizedElementRef.current = element | |
if (!element) { return } | |
setRowSize(element.getBoundingClientRect()[sizeKey] + 1) | |
}, [setRowSize, sizeKey]) | |
// Re-calculate row size on window resize | |
React.useLayoutEffect(() => { | |
const onResize = () => { | |
if (!sizedElementRef.current) { return } | |
setRowSize(sizedElementRef.current.getBoundingClientRect()[sizeKey] + 1) | |
} | |
onResize() | |
window.addEventListener('resize', onResize) | |
return () => { window.removeEventListener('resize', onResize) } | |
}, [sizeKey, estimateSizeRef]) | |
return { | |
...rowVirtualizer, | |
estimateSizeRef, | |
} | |
} | |
function findScrollParent(node) { | |
const scrollingElement = document.scrollingElement || document.documentElement | |
if (!(node instanceof HTMLElement || node instanceof SVGElement)) { | |
return scrollingElement | |
} | |
let style = getComputedStyle(node) | |
if (style.position === 'fixed') { | |
return scrollingElement | |
} | |
const excludeStaticParent = style.position === 'absolute' | |
const overflowRegex = /(auto|scroll|overlay)/ | |
let parent = node.parentElement | |
while (parent) { | |
style = getComputedStyle(parent) | |
if (excludeStaticParent && style.position === 'static') { | |
continue | |
} | |
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) { | |
return parent === document.body ? scrollingElement : parent | |
} | |
parent = parent.parentElement | |
} | |
return scrollingElement | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Good catch, I've updated the code now so that it should be correct (
target
=>scrollParent
)