diff --git a/src/core/scroller.ts b/src/core/scroller.ts index a219a7e4e..1ee845273 100644 --- a/src/core/scroller.ts +++ b/src/core/scroller.ts @@ -33,12 +33,39 @@ const normalizeOffset = (offset: number, isHorizontal: boolean): number => { } }; +const calcOffsetToViewport = ( + node: HTMLElement, + viewport: HTMLElement, + isHorizontal: boolean, + offset: number = 0 +): number => { + // TODO calc offset only when it changes (maybe impossible) + const offsetSum = + offset + + (isHorizontal && isRTLDocument() + ? viewport.offsetWidth - node.offsetLeft - node.offsetWidth + : node[isHorizontal ? "offsetLeft" : "offsetTop"]); + + const parent = node.offsetParent; + if (node === viewport || !parent) { + return offsetSum; + } + + return calcOffsetToViewport( + parent as HTMLElement, + viewport, + isHorizontal, + offsetSum + ); +}; + const createScrollObserver = ( store: VirtualStore, viewport: HTMLElement | Window, isHorizontal: boolean, getScrollOffset: () => number, - updateScrollOffset: (value: number, isMomentumScrolling: boolean) => void + updateScrollOffset: (value: number, isMomentumScrolling: boolean) => void, + calcStartOffset: (() => number) | undefined ) => { const now = Date.now; @@ -69,7 +96,12 @@ const createScrollObserver = ( stillMomentumScrolling = true; } - store._update(ACTION_SCROLL, getScrollOffset()); + let offset = getScrollOffset(); + if (calcStartOffset) { + offset -= calcStartOffset(); + } + + store._update(ACTION_SCROLL, offset); onScrollEnd(); }; @@ -140,7 +172,10 @@ type ScrollObserver = ReturnType; * @internal */ export type Scroller = { - _observe: (viewportElement: HTMLElement) => void; + _observe: ( + viewportElement: HTMLElement, + containerElement: HTMLElement + ) => void; _dispose(): void; _scrollTo: (offset: number) => void; _scrollBy: (offset: number) => void; @@ -153,7 +188,8 @@ export type Scroller = { */ export const createScroller = ( store: VirtualStore, - isHorizontal: boolean + isHorizontal: boolean, + unbound?: boolean ): Scroller => { let viewportElement: HTMLElement | undefined; let scrollObserver: ScrollObserver | undefined; @@ -245,7 +281,7 @@ export const createScroller = ( }; return { - _observe(viewport) { + _observe(viewport, container) { viewportElement = viewport; scrollObserver = createScrollObserver( @@ -268,7 +304,10 @@ export const createScroller = ( } viewport[scrollOffsetKey] += normalizeOffset(jump, isHorizontal); - } + }, + unbound + ? () => calcOffsetToViewport(container, viewport, isHorizontal) + : undefined ); }, _dispose() { @@ -352,47 +391,19 @@ export const createWindowScroller = ( const window = getCurrentWindow(document); const documentBody = document.body; - const calcOffsetToViewport = ( - node: HTMLElement, - viewport: HTMLElement, - isHorizontal: boolean, - offset: number = 0 - ): number => { - // TODO calc offset only when it changes (maybe impossible) - const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop"; - const offsetSum = - offset + - (isHorizontal && isRTLDocument() - ? window.innerWidth - node[offsetKey] - node.offsetWidth - : node[offsetKey]); - - const parent = node.offsetParent; - if (node === viewport || !parent) { - return offsetSum; - } - - return calcOffsetToViewport( - parent as HTMLElement, - viewport, - isHorizontal, - offsetSum - ); - }; - scrollObserver = createScrollObserver( store, window, isHorizontal, - () => - normalizeOffset(window[scrollOffsetKey], isHorizontal) - - calcOffsetToViewport(container, documentBody, isHorizontal), + () => normalizeOffset(window[scrollOffsetKey], isHorizontal), (jump) => { // TODO support case two window scrollers exist in the same view window.scrollBy( isHorizontal ? normalizeOffset(jump, isHorizontal) : 0, isHorizontal ? 0 : jump ); - } + }, + () => calcOffsetToViewport(container, documentBody, isHorizontal) ); }, _dispose() { diff --git a/src/react/VGrid.spec.tsx b/src/react/VGrid.spec.tsx index 568b0ce29..6c79fa0e4 100644 --- a/src/react/VGrid.spec.tsx +++ b/src/react/VGrid.spec.tsx @@ -63,30 +63,6 @@ it("should pass attributes to element", () => { expect(asFragment()).toMatchSnapshot(); }); -// it("should change components", () => { -// const UlList = forwardRef( -// ({ children, attrs, scrollSize }, ref) => { -// return ( -//
-//
    -// {children} -//
-//
-// ); -// } -// ); -// const { asFragment } = render( -// -//
0
-//
1
-//
2
-//
3
-//
4
-//
-// ); -// expect(asFragment()).toMatchSnapshot(); -// }); - describe("grid", () => { it("should render 1 children", () => { const { asFragment } = render( diff --git a/src/react/VGrid.tsx b/src/react/VGrid.tsx index 1bd56f3ed..3f27fac53 100644 --- a/src/react/VGrid.tsx +++ b/src/react/VGrid.tsx @@ -27,6 +27,7 @@ import { ViewportComponentAttributes } from "./types"; import { flushSync } from "react-dom"; import { isRTLDocument } from "../core/environment"; import { useRerender } from "./useRerender"; + const genKey = (i: number, j: number) => `${i}-${j}`; /** @@ -241,9 +242,11 @@ export const VGrid = forwardRef( const height = getScrollSize(vStore); const width = getScrollSize(hStore); const rootRef = useRef(null); + const containerRef = useRef(null); useIsomorphicLayoutEffect(() => { const root = rootRef[refKey]!; + const container = containerRef[refKey]!; // store must be subscribed first because others may dispatch update on init depending on implementation const unsubscribeVStore = vStore._subscribe( UPDATE_SCROLL_STATE + UPDATE_SIZE_STATE, @@ -266,8 +269,8 @@ export const VGrid = forwardRef( } ); resizer._observeRoot(root); - vScroller._observe(root); - hScroller._observe(root); + vScroller._observe(root, container); + hScroller._observe(root, container); return () => { unsubscribeVStore(); unsubscribeHStore(); diff --git a/src/react/Virtualizer.tsx b/src/react/Virtualizer.tsx index 96428ebac..58dadacb5 100644 --- a/src/react/Virtualizer.tsx +++ b/src/react/Virtualizer.tsx @@ -114,6 +114,10 @@ export interface VirtualizerProps { * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link VirtualizerHandle.cache}. */ cache?: CacheSnapshot; + /** + * TODO + */ + unbound?: boolean; /** * If you put an element before virtualizer, you have to define its height with this prop. */ @@ -178,6 +182,7 @@ export const Virtualizer = forwardRef( horizontal: horizontalProp, reverse, cache, + unbound, startMargin, endMargin, ssrCount, @@ -215,7 +220,7 @@ export const Virtualizer = forwardRef( return [ _store, createResizer(_store, _isHorizontal), - createScroller(_store, _isHorizontal), + createScroller(_store, _isHorizontal, unbound), _isHorizontal, ]; }); @@ -261,15 +266,16 @@ export const Virtualizer = forwardRef( onScrollEnd[refKey] && onScrollEnd[refKey](); } ); + const container = containerRef[refKey]!; const assignScrollableElement = (e: HTMLElement) => { resizer._observeRoot(e); - scroller._observe(e); + scroller._observe(e, container); }; if (scrollRef) { // parent's ref doesn't exist when useLayoutEffect is called microtask(() => assignScrollableElement(scrollRef[refKey]!)); } else { - assignScrollableElement(containerRef[refKey]!.parentElement!); + assignScrollableElement(container.parentElement!); } return () => { diff --git a/src/solid/Virtualizer.tsx b/src/solid/Virtualizer.tsx index 7eeca3173..b52929e88 100644 --- a/src/solid/Virtualizer.tsx +++ b/src/solid/Virtualizer.tsx @@ -230,7 +230,7 @@ export const Virtualizer = (props: VirtualizerProps): JSX.Element => { const scrollable = containerRef!.parentElement!; resizer._observeRoot(scrollable); - scroller._observe(scrollable); + scroller._observe(scrollable, containerRef!); onCleanup(() => { if (props.ref) { diff --git a/src/vue/Virtualizer.tsx b/src/vue/Virtualizer.tsx index 82e5a6fd0..535d3f8e6 100644 --- a/src/vue/Virtualizer.tsx +++ b/src/vue/Virtualizer.tsx @@ -123,10 +123,11 @@ export const Virtualizer = /*#__PURE__*/ defineComponent({ ); onMounted(() => { - const scrollable = containerRef.value!.parentElement; + const container = containerRef.value!; + const scrollable = container!.parentElement; if (!scrollable) return; resizer._observeRoot(scrollable); - scroller._observe(scrollable); + scroller._observe(scrollable, container); }); onUnmounted(() => { unsubscribeStore(); diff --git a/stories/react/basics/Virtualizer.stories.tsx b/stories/react/basics/Virtualizer.stories.tsx index fa2a8316c..7f2ff1280 100644 --- a/stories/react/basics/Virtualizer.stories.tsx +++ b/stories/react/basics/Virtualizer.stories.tsx @@ -38,8 +38,6 @@ const createRows = (num: number) => { export const HeaderAndFooter: StoryObj = { render: () => { - const headerHeight = 400; - const footerHeight = 600; return (
-
- header -
- - {createRows(1000)} - -
- footer +
header
+ {createRows(1000)} +
footer
+
+ ); + }, +}; + +const createColumns = (num: number) => { + return Array.from({ length: num }).map((_, i) => { + return ( +
+ Column {i} +
+ ); + }); +}; + +export const HeaderAndFooterHorizontal: StoryObj = { + render: () => { + const ref = useRef(null); + return ( +
+
+
+ header +
+ + {createColumns(1000)} + +
+ footer +
); @@ -66,8 +105,6 @@ export const HeaderAndFooter: StoryObj = { export const StickyHeaderAndFooter: StoryObj = { render: () => { - const headerHeight = 40; - const footerHeight = 60; return (
header
- - {createRows(1000)} - + {createRows(1000)}
@@ -107,11 +142,40 @@ export const StickyHeaderAndFooter: StoryObj = { }, }; +export const Padding: StoryObj = { + render: () => { + const ref = useRef(null); + + return ( +
+
+ + {createRows(1000)} + +
+
+ ); + }, +}; + export const Nested: StoryObj = { render: () => { const ref = useRef(null); - const outerPadding = 40; - const innerPadding = 60; + return (
-
-
- +
+
+ {createRows(1000)}
@@ -188,8 +248,6 @@ export const BiDirectionalInfiniteScrolling: StoryObj = { ready.current = true; }, []); - const spinnerHeight = 100; - return (
- + { if (!ready.current) return; if (end + THRESHOLD > count && endFetchedCountRef.current < count) { @@ -229,10 +283,7 @@ export const BiDirectionalInfiniteScrolling: StoryObj = { > {items} - +
); }, @@ -359,12 +410,7 @@ export const TableElement: StoryObj = { overflow: "auto", }} > - + {(i) => ( {COLUMN_WIDTHS.map((width, j) => ( diff --git a/stories/react/common.tsx b/stories/react/common.tsx index 320fe7ab2..3a60e2fb1 100644 --- a/stories/react/common.tsx +++ b/stories/react/common.tsx @@ -8,17 +8,15 @@ export const delay = (ms: number) => export const Spinner = ({ style, - height = 100, }: { style?: CSSProperties; - height?: number; }) => { return ( <>