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 | |
} |
what does
findScrollParent
do?
Looks like I forgot to rename the scrollParent()
helper - it should be named findScrollParent()
to the naming collision with the variable.
I've updated the gist now!
Hi, can you show how to use it with window as parentRef
?
Hi, can you show how to use it with window as
parentRef
?
Hi @thanhlmm! You should be able to use it the same way you use useVirtual()
from react-virtual
:
function ScrolledComponent() {
const items = Array.apply(null, {length: 10000}).map((_, i) => i+1) // the item array
const virtualizer = useVirtualScrollParent({
parentRef,
size: items.length,
estimateSize: React.useCallback(() => 40, []), // should return height of each item
})
return <div ref={parentRef} style={{ position: 'relative', height: virtualizer.totalSize + 'px' }}>
{virtualizer.virtualItems.map((row, offset) => {
const style = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}
return <div style={style}>{item[virtualRow.index]}</div>
})}
</div>
}
target
undefined?
target
undefined?
Good catch, I've updated the code now so that it should be correct (target
=> scrollParent
)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
what does
findScrollParent
do?