diff --git a/examples/scroll-element-grid-scroller.tsx b/examples/scroll-element-grid-scroller.tsx new file mode 100644 index 000000000..701c9364b --- /dev/null +++ b/examples/scroll-element-grid-scroller.tsx @@ -0,0 +1,162 @@ +import * as React from 'react' +import { VirtuosoGrid, GridComponents } from '../src' +import styled from '@emotion/styled' + +const ItemContainer = styled.div` + box-sizing: border-box; + padding: 5px; + width: 25%; + background: #f5f5f5; + display: flex; + flex: none; + align-content: stretch; + @media (max-width: 1024px) { + width: 33%; + } + + @media (max-width: 768px) { + width: 50%; + } + + @media (max-width: 480px) { + width: 100%; + } +` + +const ItemWrapper = styled.div` + flex: 1; + text-align: center; + font-size: 80%; + padding: 20px; + box-shadow: 0 5px 6px -6px #777; + background: white; + } +` +const ListContainer = styled.div` + display: flex; + flex-wrap: wrap; +` as GridComponents['List'] + +export default function App() { + const [scrollElement, setScrollElement] = React.useState(null) + const [height, setHeight] = React.useState('25vh') + const toggleHeight = () => (height === '50vh' ? setHeight('25vh') : setHeight('50vh')) + + return ( +
+ +
+
    +
  1. + Click the change height button in the upper left corner to toggle the height. Items should appear when the height changes. +
  2. +
  3. Scroll down manually, new items should appear.
  4. +
  5. Scroll halfway down manually, then scroll the outer green container down, new items should appear.
  6. +
  7. + Scroll outer red container all the way down, then scroll items down, then scroll outer green container up and down, new items + should appear. +
  8. +
+
+ ( + + -- + + ), + }} + totalCount={1000} + itemContent={(index) => Item {index}} + /> +
+

Yes there is a bottom!

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse + consectetur quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; Curabitur ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices + vulputate. Nulla semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed + risus enim. Sed non ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis + magna. Nullam accumsan justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis + magna, nec rhoncus mi tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque + condimentum risus, vel fringilla tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo + sagittis vulputate vitae sed massa. Nam ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non + porta cursus, tortor ante venenatis ipsum, nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et + egestas felis suscipit. Etiam bibendum lacinia nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus + aliquet lacus. In hac habitasse platea dictumst. Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Ut ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus + euismod tortor. Duis facilisis eu quam ac bibendum. Morbi pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem + accumsan dignissim. Fusce vestibulum condimentum eros, in ornare augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit + interdum purus quis mattis. Nulla mollis ac diam id sagittis. Maecenas non nibh non arcu aliquet ultrices eget et diam. + Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit tincidunt placerat. Maecenas pulvinar ligula eu scelerisque + maximus. Nullam a magna ex. Aliquam elementum augue id malesuada scelerisque. Curabitur quis lectus augue. Morbi id blandit + nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor dui. Nam ac sodales purus. Sed rhoncus auctor urna, non + consectetur lorem condimentum sed. Suspendisse vel posuere erat. Maecenas viverra iaculis commodo. Maecenas et tellus quis sem + congue consequat. Sed eget feugiat eros. Suspendisse viverra augue et risus aliquam feugiat. Quisque consectetur vel dolor + quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae vulputate augue. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse + consectetur quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; Curabitur ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices + vulputate. Nulla semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed + risus enim. Sed non ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis + magna. Nullam accumsan justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis + magna, nec rhoncus mi tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque + condimentum risus, vel fringilla tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo + sagittis vulputate vitae sed massa. Nam ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non + porta cursus, tortor ante venenatis ipsum, nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et + egestas felis suscipit. Etiam bibendum lacinia nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus + aliquet lacus. In hac habitasse platea dictumst. Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Ut ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus + euismod tortor. Duis facilisis eu quam ac bibendum. Morbi pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem + accumsan dignissim. Fusce vestibulum condimentum eros, in ornare augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit + interdum purus quis mattis. Nulla mollis ac diam id sagittis. Maecenas non nibh non arcu aliquet ultrices eget et diam. + Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit tincidunt placerat. Maecenas pulvinar ligula eu scelerisque + maximus. Nullam a magna ex. Aliquam elementum augue id malesuada scelerisque. Curabitur quis lectus augue. Morbi id blandit + nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor dui. Nam ac sodales purus. Sed rhoncus auctor urna, non + consectetur lorem condimentum sed. Suspendisse vel posuere erat. Maecenas viverra iaculis commodo. Maecenas et tellus quis sem + congue consequat. Sed eget feugiat eros. Suspendisse viverra augue et risus aliquam feugiat. Quisque consectetur vel dolor + quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae vulputate augue. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse + consectetur quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; Curabitur ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices + vulputate. Nulla semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed + risus enim. Sed non ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis + magna. Nullam accumsan justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis + magna, nec rhoncus mi tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque + condimentum risus, vel fringilla tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo + sagittis vulputate vitae sed massa. Nam ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non + porta cursus, tortor ante venenatis ipsum, nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et + egestas felis suscipit. Etiam bibendum lacinia nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus + aliquet lacus. In hac habitasse platea dictumst. Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Ut ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus + euismod tortor. Duis facilisis eu quam ac bibendum. Morbi pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem + accumsan dignissim. Fusce vestibulum condimentum eros, in ornare augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit + interdum purus quis mattis. Nulla mollis ac diam id sagittis. Maecenas non nibh non arcu aliquet ultrices eget et diam. + Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit tincidunt placerat. Maecenas pulvinar ligula eu scelerisque + maximus. Nullam a magna ex. Aliquam elementum augue id malesuada scelerisque. Curabitur quis lectus augue. Morbi id blandit + nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor dui. Nam ac sodales purus. Sed rhoncus auctor urna, non + consectetur lorem condimentum sed. Suspendisse vel posuere erat. Maecenas viverra iaculis commodo. Maecenas et tellus quis sem + congue consequat. Sed eget feugiat eros. Suspendisse viverra augue et risus aliquam feugiat. Quisque consectetur vel dolor + quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae vulputate augue. +

+
+
+
+
+ ) +} diff --git a/examples/scroll-element-list-scroller.tsx b/examples/scroll-element-list-scroller.tsx new file mode 100644 index 000000000..4aa2d879e --- /dev/null +++ b/examples/scroll-element-list-scroller.tsx @@ -0,0 +1,102 @@ +import * as React from 'react' +import { Virtuoso } from '../src' + +export default function App() { + const [scrollElement, setScrollElement] = React.useState(null) + return ( +
+
+
    +
  1. Scroll down manually, new items should appear.
  2. +
  3. Scroll halfway down manually, then scroll the outer green container down, new items should appear.
  4. +
  5. + Scroll outer green container all the way down, then scroll items down, then scroll outer green container up and down, new items + should appear. +
  6. +
  7. Click on foo, then scroll upwards. the paragraphs should stay correctly.
  8. +
+ Go to foo +
Item {index}
} + style={{ border: '1px solid red' }} + /> +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse consectetur + quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Curabitur + ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices vulputate. Nulla + semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed risus enim. Sed non + ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis magna. Nullam accumsan + justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis magna, nec rhoncus mi + tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque condimentum risus, vel fringilla + tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo sagittis vulputate vitae sed massa. Nam + ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non porta cursus, tortor ante venenatis ipsum, + nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et egestas felis suscipit. Etiam bibendum lacinia + nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus aliquet lacus. In hac habitasse platea dictumst. + Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut + ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus euismod tortor. Duis facilisis eu quam ac bibendum. Morbi + pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem accumsan dignissim. Fusce vestibulum condimentum eros, in ornare + augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit interdum purus quis mattis. Nulla mollis ac diam id sagittis. + Maecenas non nibh non arcu aliquet ultrices eget et diam. Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit + tincidunt placerat. Maecenas pulvinar ligula eu scelerisque maximus. Nullam a magna ex. Aliquam elementum augue id malesuada + scelerisque. Curabitur quis lectus augue. Morbi id blandit nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor + dui. Nam ac sodales purus. Sed rhoncus auctor urna, non consectetur lorem condimentum sed. Suspendisse vel posuere erat. + Maecenas viverra iaculis commodo. Maecenas et tellus quis sem congue consequat. Sed eget feugiat eros. Suspendisse viverra augue + et risus aliquam feugiat. Quisque consectetur vel dolor quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae + vulputate augue. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse consectetur + quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Curabitur + ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices vulputate. Nulla + semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed risus enim. Sed non + ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis magna. Nullam accumsan + justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis magna, nec rhoncus mi + tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque condimentum risus, vel fringilla + tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo sagittis vulputate vitae sed massa. Nam + ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non porta cursus, tortor ante venenatis ipsum, + nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et egestas felis suscipit. Etiam bibendum lacinia + nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus aliquet lacus. In hac habitasse platea dictumst. + Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut + ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus euismod tortor. Duis facilisis eu quam ac bibendum. Morbi + pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem accumsan dignissim. Fusce vestibulum condimentum eros, in ornare + augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit interdum purus quis mattis. Nulla mollis ac diam id sagittis. + Maecenas non nibh non arcu aliquet ultrices eget et diam. Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit + tincidunt placerat. Maecenas pulvinar ligula eu scelerisque maximus. Nullam a magna ex. Aliquam elementum augue id malesuada + scelerisque. Curabitur quis lectus augue. Morbi id blandit nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor + dui. Nam ac sodales purus. Sed rhoncus auctor urna, non consectetur lorem condimentum sed. Suspendisse vel posuere erat. + Maecenas viverra iaculis commodo. Maecenas et tellus quis sem congue consequat. Sed eget feugiat eros. Suspendisse viverra augue + et risus aliquam feugiat. Quisque consectetur vel dolor quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae + vulputate augue. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris rhoncus magna nec interdum consectetur. Suspendisse consectetur + quis tortor at scelerisque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Curabitur + ultrices imperdiet quam vitae gravida. Aenean aliquam porttitor arcu. Ut volutpat velit at risus ultrices vulputate. Nulla + semper tortor ac purus rhoncus, ut fermentum orci feugiat. Quisque venenatis suscipit consectetur. Cras sed risus enim. Sed non + ex ultricies, malesuada neque quis, volutpat massa. Sed sit amet orci non ex feugiat porttitor vel quis magna. Nullam accumsan + justo nec ipsum maximus placerat. Integer et dui ut metus mattis vestibulum. Aliquam pretium mollis magna, nec rhoncus mi + tristique non. Sed vitae ligula augue. Donec rutrum, mi efficitur maximus volutpat, diam neque condimentum risus, vel fringilla + tortor lorem vel risus. Cras venenatis ipsum ac suscipit faucibus. Donec at arcu nec leo sagittis vulputate vitae sed massa. Nam + ullamcorper hendrerit nunc eu vestibulum. Sed et feugiat sem. Sed iaculis, augue non porta cursus, tortor ante venenatis ipsum, + nec sodales leo lectus et nunc. Nunc pharetra ipsum sit amet felis viverra, et egestas felis suscipit. Etiam bibendum lacinia + nisi semper finibus. Aliquam feugiat ultrices est eu viverra. Quisque luctus aliquet lacus. In hac habitasse platea dictumst. + Fusce volutpat tincidunt finibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut + ac mattis nunc. Integer lorem dui, facilisis eget lacus id, tempus euismod tortor. Duis facilisis eu quam ac bibendum. Morbi + pulvinar feugiat tortor id imperdiet. Fusce nec odio ut lorem accumsan dignissim. Fusce vestibulum condimentum eros, in ornare + augue sodales nec. Pellentesque ut lorem nibh. Nunc suscipit interdum purus quis mattis. Nulla mollis ac diam id sagittis. + Maecenas non nibh non arcu aliquet ultrices eget et diam. Quisque interdum nulla sed arcu eleifend posuere. Donec hendrerit + tincidunt placerat. Maecenas pulvinar ligula eu scelerisque maximus. Nullam a magna ex. Aliquam elementum augue id malesuada + scelerisque. Curabitur quis lectus augue. Morbi id blandit nunc. Donec tellus justo, volutpat ac enim non, tincidunt porttitor + dui. Nam ac sodales purus. Sed rhoncus auctor urna, non consectetur lorem condimentum sed. Suspendisse vel posuere erat. + Maecenas viverra iaculis commodo. Maecenas et tellus quis sem congue consequat. Sed eget feugiat eros. Suspendisse viverra augue + et risus aliquam feugiat. Quisque consectetur vel dolor quis auctor. Pellentesque commodo et nunc eu elementum. Nullam vitae + vulputate augue. +

+
+
+
+ ) +} diff --git a/src/Grid.tsx b/src/Grid.tsx index ad8629159..3cb578635 100644 --- a/src/Grid.tsx +++ b/src/Grid.tsx @@ -137,7 +137,8 @@ const Viewport: FC = ({ children }) => { const WindowViewport: FC = ({ children }) => { const windowViewportRect = usePublisher('windowViewportRect') - const viewportRef = useWindowViewportRectRef(windowViewportRect) + const customScrollParent = useEmitterValue('customScrollParent') + const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent) return (
@@ -148,8 +149,9 @@ const WindowViewport: FC = ({ children }) => { const GridRoot: FC = React.memo(function GridRoot({ ...props }) { const useWindowScroll = useEmitterValue('useWindowScroll') - const TheScroller = useWindowScroll ? WindowScroller : Scroller - const TheViewport = useWindowScroll ? WindowViewport : Viewport + const customScrollParent = useEmitterValue('customScrollParent') + const TheScroller = customScrollParent || useWindowScroll ? WindowScroller : Scroller + const TheViewport = customScrollParent || useWindowScroll ? WindowViewport : Viewport return ( @@ -174,6 +176,7 @@ const { Component: Grid, usePublisher, useEmitterValue, useEmitter } = systemToC listClassName: 'listClassName', itemClassName: 'itemClassName', useWindowScroll: 'useWindowScroll', + customScrollParent: 'customScrollParent', scrollerRef: 'scrollerRef', // deprecated diff --git a/src/List.tsx b/src/List.tsx index 42f2d80fd..9dd280fdf 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -170,16 +170,25 @@ export const Items = React.memo(function VirtuosoItems({ showTopList = false }: const deviation = useEmitterValue('deviation') const sizeRanges = usePublisher('sizeRanges') const useWindowScroll = useEmitterValue('useWindowScroll') + const customScrollParent = useEmitterValue('customScrollParent') const windowScrollContainerStateCallback = usePublisher('windowScrollContainerState') const _scrollContainerStateCallback = usePublisher('scrollContainerState') - const scrollContainerStateCallback = useWindowScroll ? windowScrollContainerStateCallback : _scrollContainerStateCallback + const scrollContainerStateCallback = + customScrollParent || useWindowScroll ? windowScrollContainerStateCallback : _scrollContainerStateCallback const itemContent = useEmitterValue('itemContent') const groupContent = useEmitterValue('groupContent') const trackItemSizes = useEmitterValue('trackItemSizes') const itemSize = useEmitterValue('itemSize') const log = useEmitterValue('log') - const ref = useChangedListContentsSizes(sizeRanges, itemSize, trackItemSizes, showTopList ? noop : scrollContainerStateCallback, log) + const ref = useChangedListContentsSizes( + sizeRanges, + itemSize, + trackItemSizes, + showTopList ? noop : scrollContainerStateCallback, + log, + customScrollParent + ) const EmptyPlaceholder = useEmitterValue('EmptyPlaceholder') const ScrollSeekPlaceholder = useEmitterValue('ScrollSeekPlaceholder') || DefaultScrollSeekPlaceholder const ListComponent = useEmitterValue('ListComponent')! @@ -337,19 +346,21 @@ export function buildWindowScroller({ usePublisher, useEmitter, useEmitterValue const smoothScrollTargetReached = usePublisher('smoothScrollTargetReached') const totalListHeight = useEmitterValue('totalListHeight') const deviation = useEmitterValue('deviation') + const customScrollParent = useEmitterValue('customScrollParent') const { scrollerRef, scrollByCallback, scrollToCallback } = useScrollTop( scrollContainerStateCallback, smoothScrollTargetReached, ScrollerComponent, - noop + noop, + customScrollParent ) useIsomorphicLayoutEffect(() => { - scrollerRef.current = window + scrollerRef.current = customScrollParent ? customScrollParent : window return () => { scrollerRef.current = null } - }, [scrollerRef]) + }, [scrollerRef, customScrollParent]) useEmitter('windowScrollTo', scrollToCallback) useEmitter('scrollBy', scrollByCallback) @@ -380,7 +391,8 @@ const Viewport: FC = ({ children }) => { const WindowViewport: FC = ({ children }) => { const windowViewportRect = usePublisher('windowViewportRect') - const viewportRef = useWindowViewportRectRef(windowViewportRect) + const customScrollParent = useEmitterValue('customScrollParent') + const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent) return (
@@ -399,8 +411,9 @@ const TopItemListContainer: FC = ({ children }) => { const ListRoot: FC = React.memo(function VirtuosoRoot(props) { const useWindowScroll = useEmitterValue('useWindowScroll') const showTopList = useEmitterValue('topItemsIndexes').length > 0 - const TheScroller = useWindowScroll ? WindowScroller : Scroller - const TheViewport = useWindowScroll ? WindowViewport : Viewport + const customScrollParent = useEmitterValue('customScrollParent') + const TheScroller = customScrollParent || useWindowScroll ? WindowScroller : Scroller + const TheViewport = customScrollParent || useWindowScroll ? WindowViewport : Viewport return ( @@ -447,6 +460,7 @@ export const { Component: List, usePublisher, useEmitterValue, useEmitter } = sy initialScrollTop: 'initialScrollTop', alignToBottom: 'alignToBottom', useWindowScroll: 'useWindowScroll', + customScrollParent: 'customScrollParent', scrollerRef: 'scrollerRef', logLevel: 'logLevel', diff --git a/src/Table.tsx b/src/Table.tsx index 500dc9f67..2973cae4a 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -68,15 +68,17 @@ export const Items = React.memo(function VirtuosoItems() { const deviation = useEmitterValue('deviation') const sizeRanges = usePublisher('sizeRanges') const useWindowScroll = useEmitterValue('useWindowScroll') + const customScrollParent = useEmitterValue('customScrollParent') const windowScrollContainerStateCallback = usePublisher('windowScrollContainerState') const _scrollContainerStateCallback = usePublisher('scrollContainerState') - const scrollContainerStateCallback = useWindowScroll ? windowScrollContainerStateCallback : _scrollContainerStateCallback + const scrollContainerStateCallback = + customScrollParent || useWindowScroll ? windowScrollContainerStateCallback : _scrollContainerStateCallback const itemContent = useEmitterValue('itemContent') const trackItemSizes = useEmitterValue('trackItemSizes') const itemSize = useEmitterValue('itemSize') const log = useEmitterValue('log') - const ref = useChangedListContentsSizes(sizeRanges, itemSize, trackItemSizes, scrollContainerStateCallback, log) + const ref = useChangedListContentsSizes(sizeRanges, itemSize, trackItemSizes, scrollContainerStateCallback, log, customScrollParent) const EmptyPlaceholder = useEmitterValue('EmptyPlaceholder') const ScrollSeekPlaceholder = useEmitterValue('ScrollSeekPlaceholder') || DefaultScrollSeekPlaceholder const TableBodyComponent = useEmitterValue('TableBodyComponent')! @@ -150,7 +152,8 @@ const Viewport: FC = ({ children }) => { const WindowViewport: FC = ({ children }) => { const windowViewportRect = usePublisher('windowViewportRect') - const viewportRef = useWindowViewportRectRef(windowViewportRect) + const customScrollParent = useEmitterValue('customScrollParent') + const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent) return (
@@ -161,11 +164,12 @@ const WindowViewport: FC = ({ children }) => { const TableRoot: FC = React.memo(function TableVirtuosoRoot(props) { const useWindowScroll = useEmitterValue('useWindowScroll') + const customScrollParent = useEmitterValue('customScrollParent') const fixedHeaderHeight = usePublisher('fixedHeaderHeight') const fixedHeaderContent = useEmitterValue('fixedHeaderContent') const theadRef = useSize(compose(fixedHeaderHeight, (el) => correctItemSize(el, 'height'))) - const TheScroller = useWindowScroll ? WindowScroller : Scroller - const TheViewport = useWindowScroll ? WindowViewport : Viewport + const TheScroller = customScrollParent || useWindowScroll ? WindowScroller : Scroller + const TheViewport = customScrollParent || useWindowScroll ? WindowViewport : Viewport const TheTable = useEmitterValue('TableComponent') const TheTHead = useEmitterValue('TableHeadComponent') @@ -215,6 +219,7 @@ export const { Component: Table, usePublisher, useEmitterValue, useEmitter } = s initialScrollTop: 'initialScrollTop', alignToBottom: 'alignToBottom', useWindowScroll: 'useWindowScroll', + customScrollParent: 'customScrollParent', scrollerRef: 'scrollerRef', logLevel: 'logLevel', }, diff --git a/src/components.tsx b/src/components.tsx index c849a16ce..1d5670e21 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -208,6 +208,11 @@ export interface VirtuosoProps extends Omit extends Omit, 'component */ useWindowScroll?: boolean + /** + * Pass a reference to a scrollable parent element, so that the table won't wrap in its own. + */ + customScrollParent?: HTMLElement + /** * Provides access to the root DOM element */ @@ -511,6 +521,11 @@ export interface VirtuosoGridProps extends GridProps { * Uses the document scroller rather than wrapping the grid in its own. */ useWindowScroll?: boolean + + /** + * Pass a reference to a scrollable parent element, so that the grid won't wrap in its own. + */ + customScrollParent?: HTMLElement } export interface VirtuosoHandle extends ListHandle { diff --git a/src/gridSystem.ts b/src/gridSystem.ts index de04dfb25..740fe16d1 100644 --- a/src/gridSystem.ts +++ b/src/gridSystem.ts @@ -66,7 +66,7 @@ export const gridSystem = u.system( stateFlags, scrollSeek, { propsReady, didMount }, - { windowViewportRect, windowScrollTo, useWindowScroll, windowScrollContainerState }, + { windowViewportRect, windowScrollTo, useWindowScroll, customScrollParent, windowScrollContainerState }, ]) => { const totalCount = u.statefulStream(0) const initialItemCount = u.statefulStream(0) @@ -258,6 +258,7 @@ export const gridSystem = u.system( windowViewportRect, windowScrollTo, useWindowScroll, + customScrollParent, windowScrollContainerState, deviation, scrollContainerState, diff --git a/src/hooks/useChangedChildSizes.ts b/src/hooks/useChangedChildSizes.ts index bd9d298a0..86bc83268 100644 --- a/src/hooks/useChangedChildSizes.ts +++ b/src/hooks/useChangedChildSizes.ts @@ -7,7 +7,8 @@ export default function useChangedListContentsSizes( itemSize: SizeFunction, enabled: boolean, scrollContainerStateCallback: (state: [number, number]) => void, - log: Log + log: Log, + customScrollParent?: HTMLElement ) { return useSize((el: HTMLElement) => { const ranges = getChangedChildSizes(el.children, itemSize, 'offsetHeight', log) @@ -17,13 +18,16 @@ export default function useChangedListContentsSizes( scrollableElement = scrollableElement.parentElement! } - const scrollTop = - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const scrollTop = customScrollParent + ? customScrollParent.scrollTop + : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (scrollableElement.firstElementChild! as HTMLDivElement).dataset['viewportType']! === 'window' - ? window.pageYOffset || document.documentElement.scrollTop - : scrollableElement.scrollTop + ? window.pageYOffset || document.documentElement.scrollTop + : scrollableElement.scrollTop - scrollContainerStateCallback([Math.max(scrollTop, 0), scrollableElement.scrollHeight]) + customScrollParent + ? scrollContainerStateCallback([Math.max(scrollTop, 0), customScrollParent.scrollHeight]) + : scrollContainerStateCallback([Math.max(scrollTop, 0), scrollableElement.scrollHeight]) if (ranges !== null) { callback(ranges) } diff --git a/src/hooks/useScrollTop.ts b/src/hooks/useScrollTop.ts index 0a709cd6a..2572ff57c 100644 --- a/src/hooks/useScrollTop.ts +++ b/src/hooks/useScrollTop.ts @@ -12,7 +12,8 @@ export default function useScrollTop( scrollContainerStateCallback: (state: [number, number]) => void, smoothScrollTargetReached: (yes: true) => void, scrollerElement: any, - scrollerRefCallback: (ref: ScrollerRef) => void = u.noop + scrollerRefCallback: (ref: ScrollerRef) => void = u.noop, + customScrollParent?: HTMLElement ) { const scrollerRef = useRef(null) const scrollTopTarget = useRef(null) @@ -42,9 +43,9 @@ export default function useScrollTop( ) useEffect(() => { - const localRef = scrollerRef.current! + const localRef = customScrollParent ? customScrollParent : scrollerRef.current! - scrollerRefCallback(scrollerRef.current) + scrollerRefCallback(customScrollParent ? customScrollParent : scrollerRef.current) handler(({ target: localRef } as unknown) as Event) localRef.addEventListener('scroll', handler, { passive: true }) @@ -52,7 +53,7 @@ export default function useScrollTop( scrollerRefCallback(null) localRef.removeEventListener('scroll', handler) } - }, [scrollerRef, handler, scrollerElement, scrollerRefCallback]) + }, [scrollerRef, handler, scrollerElement, scrollerRefCallback, customScrollParent]) function scrollToCallback(location: ScrollToOptions) { const scrollerElement = scrollerRef.current diff --git a/src/hooks/useSize.ts b/src/hooks/useSize.ts index 7bd10d52e..55ec4a837 100644 --- a/src/hooks/useSize.ts +++ b/src/hooks/useSize.ts @@ -12,19 +12,9 @@ export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled = t if (typeof ResizeObserver !== 'undefined') { const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { const element = entries[0].target as HTMLElement - // Revert the RAF below - it causes a blink in the upward scrolling fix - // See e2e/chat example - // Avoid Resize loop limit exceeded error - // https://github.com/edunad/react-virtuoso/commit/581d4558f2994adea375291b76fe59605556c08f - // requestAnimationFrame(() => { - // - // if display: none, the element won't have an offsetParent - // measuring it at this mode is not going to work - // https://stackoverflow.com/a/21696585/1009797 if (element.offsetParent !== null) { callback(element) } - // }) }) callbackRef = (elRef: CallbackRefParam) => { diff --git a/src/hooks/useWindowViewportRect.ts b/src/hooks/useWindowViewportRect.ts index 32083b252..f85cc49b9 100644 --- a/src/hooks/useWindowViewportRect.ts +++ b/src/hooks/useWindowViewportRect.ts @@ -2,7 +2,7 @@ import { useEffect, useRef, useCallback } from 'react' import { useSizeWithElRef } from './useSize' import { WindowViewportInfo } from '../interfaces' -export default function useWindowViewportRectRef(callback: (info: WindowViewportInfo) => void) { +export default function useWindowViewportRectRef(callback: (info: WindowViewportInfo) => void, customScrollParent?: HTMLElement) { const viewportInfo = useRef(null) const calculateInfo = useCallback( @@ -11,34 +11,55 @@ export default function useWindowViewportRectRef(callback: (info: WindowViewport return } const rect = element.getBoundingClientRect() - const visibleHeight = window.innerHeight - Math.max(0, rect.top) - const visibleWidth = rect.width - const offsetTop = rect.top + window.pageYOffset + let visibleHeight: number, offsetTop: number + + if (customScrollParent) { + const customScrollParentRect = customScrollParent.getBoundingClientRect() + const deltaTop = rect.top - customScrollParentRect.top + + visibleHeight = customScrollParentRect.height - Math.max(0, deltaTop) + offsetTop = deltaTop + customScrollParent.scrollTop + } else { + visibleHeight = window.innerHeight - Math.max(0, rect.top) + offsetTop = rect.top + window.pageYOffset + } + viewportInfo.current = { offsetTop, visibleHeight, visibleWidth, } + callback(viewportInfo.current) }, - [callback] + [callback, customScrollParent] ) const { callbackRef, ref } = useSizeWithElRef(calculateInfo) - const windowEH = useCallback(() => { + const scrollAndResizeEventHandler = useCallback(() => { calculateInfo(ref.current) }, [calculateInfo, ref]) useEffect(() => { - window.addEventListener('scroll', windowEH) - window.addEventListener('resize', windowEH) - return () => { - window.removeEventListener('scroll', windowEH) - window.removeEventListener('resize', windowEH) + if (customScrollParent) { + customScrollParent.addEventListener('scroll', scrollAndResizeEventHandler) + const observer = new ResizeObserver(scrollAndResizeEventHandler) + observer.observe(customScrollParent) + return () => { + customScrollParent.removeEventListener('scroll', scrollAndResizeEventHandler) + observer.unobserve(customScrollParent) + } + } else { + window.addEventListener('scroll', scrollAndResizeEventHandler) + window.addEventListener('resize', scrollAndResizeEventHandler) + return () => { + window.removeEventListener('scroll', scrollAndResizeEventHandler) + window.removeEventListener('resize', scrollAndResizeEventHandler) + } } - }, [windowEH]) + }, [scrollAndResizeEventHandler, customScrollParent]) return callbackRef } diff --git a/src/windowScrollerSystem.ts b/src/windowScrollerSystem.ts index 4efe92568..6051934b5 100644 --- a/src/windowScrollerSystem.ts +++ b/src/windowScrollerSystem.ts @@ -7,6 +7,7 @@ export const windowScrollerSystem = u.system(([{ scrollTo, scrollContainerState const windowViewportRect = u.stream() const windowScrollTo = u.stream() const useWindowScroll = u.statefulStream(false) + const customScrollParent = u.statefulStream(undefined) u.connect( u.pipe( @@ -35,6 +36,7 @@ export const windowScrollerSystem = u.system(([{ scrollTo, scrollContainerState return { // config useWindowScroll, + customScrollParent, // input windowScrollContainerState,