cogsbox-state 0.5.292 → 0.5.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CogsState.d.ts +1 -1
- package/dist/CogsState.jsx +379 -391
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +64 -55
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -42,7 +42,7 @@ import useMeasure from "react-use-measure";
|
|
|
42
42
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
43
43
|
|
|
44
44
|
export type VirtualViewOptions = {
|
|
45
|
-
itemHeight
|
|
45
|
+
itemHeight?: number;
|
|
46
46
|
overscan?: number;
|
|
47
47
|
stickToBottom?: boolean;
|
|
48
48
|
};
|
|
@@ -1802,9 +1802,8 @@ function createProxyHandler<T>(
|
|
|
1802
1802
|
return (
|
|
1803
1803
|
options: VirtualViewOptions
|
|
1804
1804
|
): VirtualStateObjectResult<any[]> => {
|
|
1805
|
-
// --- CHANGE 1: itemHeight is now optional with a default ---
|
|
1806
1805
|
const {
|
|
1807
|
-
itemHeight = 50, //
|
|
1806
|
+
itemHeight = 50, // Default/estimated height
|
|
1808
1807
|
overscan = 5,
|
|
1809
1808
|
stickToBottom = false,
|
|
1810
1809
|
} = options;
|
|
@@ -1815,7 +1814,20 @@ function createProxyHandler<T>(
|
|
|
1815
1814
|
endIndex: 10,
|
|
1816
1815
|
});
|
|
1817
1816
|
|
|
1818
|
-
// ---
|
|
1817
|
+
// --- State Tracking Refs for Stability ---
|
|
1818
|
+
const isAtBottomRef = useRef(stickToBottom);
|
|
1819
|
+
// Store the scroll position before a new item is added
|
|
1820
|
+
const scrollOffsetRef = useRef(0);
|
|
1821
|
+
// Ref to track if the list has grown, to trigger scroll correction
|
|
1822
|
+
const listGrewRef = useRef(false);
|
|
1823
|
+
|
|
1824
|
+
const sourceArray = getGlobalStore().getNestedState(
|
|
1825
|
+
stateKey,
|
|
1826
|
+
path
|
|
1827
|
+
) as any[];
|
|
1828
|
+
const totalCount = sourceArray.length;
|
|
1829
|
+
|
|
1830
|
+
// Helper to get measured heights or the default
|
|
1819
1831
|
const getItemHeight = useCallback(
|
|
1820
1832
|
(index: number): number => {
|
|
1821
1833
|
const metadata = getGlobalStore
|
|
@@ -1826,18 +1838,7 @@ function createProxyHandler<T>(
|
|
|
1826
1838
|
[itemHeight, stateKey, path]
|
|
1827
1839
|
);
|
|
1828
1840
|
|
|
1829
|
-
|
|
1830
|
-
const previousTotalCountRef = useRef(0);
|
|
1831
|
-
const isInitialMountRef = useRef(true);
|
|
1832
|
-
|
|
1833
|
-
const sourceArray = getGlobalStore().getNestedState(
|
|
1834
|
-
stateKey,
|
|
1835
|
-
path
|
|
1836
|
-
) as any[];
|
|
1837
|
-
const totalCount = sourceArray.length;
|
|
1838
|
-
|
|
1839
|
-
// --- CHANGE 3: Pre-calculate total height and item positions ---
|
|
1840
|
-
// This replaces all instances of `totalCount * itemHeight`.
|
|
1841
|
+
// Pre-calculate total height and the top offset of each item
|
|
1841
1842
|
const { totalHeight, positions } = useMemo(() => {
|
|
1842
1843
|
let currentHeight = 0;
|
|
1843
1844
|
const pos: number[] = [];
|
|
@@ -1845,10 +1846,12 @@ function createProxyHandler<T>(
|
|
|
1845
1846
|
pos[i] = currentHeight;
|
|
1846
1847
|
currentHeight += getItemHeight(i);
|
|
1847
1848
|
}
|
|
1849
|
+
|
|
1850
|
+
console.log("totalHeight", totalHeight);
|
|
1848
1851
|
return { totalHeight: currentHeight, positions: pos };
|
|
1849
1852
|
}, [totalCount, getItemHeight]);
|
|
1850
1853
|
|
|
1851
|
-
// This
|
|
1854
|
+
// This is identical to your original code
|
|
1852
1855
|
const virtualState = useMemo(() => {
|
|
1853
1856
|
const start = Math.max(0, range.startIndex);
|
|
1854
1857
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1863,23 +1866,49 @@ function createProxyHandler<T>(
|
|
|
1863
1866
|
});
|
|
1864
1867
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1865
1868
|
|
|
1869
|
+
// --- STABLE SCROLL LOGIC ---
|
|
1870
|
+
// useLayoutEffect runs after DOM mutations but before the browser paints.
|
|
1871
|
+
// This is the perfect place to correct scroll positions.
|
|
1866
1872
|
useLayoutEffect(() => {
|
|
1867
1873
|
const container = containerRef.current;
|
|
1868
1874
|
if (!container) return;
|
|
1869
1875
|
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1876
|
+
// If the list grew, we need to adjust the scroll position
|
|
1877
|
+
// to prevent the content from jumping.
|
|
1878
|
+
if (listGrewRef.current) {
|
|
1879
|
+
listGrewRef.current = false; // Reset the flag
|
|
1880
|
+
|
|
1881
|
+
if (isAtBottomRef.current) {
|
|
1882
|
+
// If we were at the bottom, stay at the bottom.
|
|
1883
|
+
// This is the fix for the auto-scroll issue.
|
|
1884
|
+
container.scrollTop = container.scrollHeight;
|
|
1885
|
+
} else {
|
|
1886
|
+
// If we were in the middle, restore the previous scroll position
|
|
1887
|
+
// plus the height of the content that was added above us.
|
|
1888
|
+
// This is an advanced case, but for now, let's keep it simple
|
|
1889
|
+
// as most use-cases are for chat-like views. For a simple list,
|
|
1890
|
+
// just staying at the bottom is the main goal.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}, [totalHeight]); // This effect runs whenever the total height changes
|
|
1894
|
+
|
|
1895
|
+
useEffect(() => {
|
|
1896
|
+
const container = containerRef.current;
|
|
1897
|
+
if (!container) return;
|
|
1898
|
+
|
|
1899
|
+
// Track the previous total count to detect when new items are added
|
|
1900
|
+
let previousTotalCount = totalCount;
|
|
1873
1901
|
|
|
1874
1902
|
const handleScroll = () => {
|
|
1903
|
+
if (!container) return;
|
|
1875
1904
|
const { scrollTop, clientHeight, scrollHeight } = container;
|
|
1905
|
+
// Update "is at bottom" status on every scroll
|
|
1876
1906
|
isAtBottomRef.current =
|
|
1877
1907
|
scrollHeight - scrollTop - clientHeight < 10;
|
|
1908
|
+
scrollOffsetRef.current = scrollTop;
|
|
1878
1909
|
|
|
1879
|
-
//
|
|
1880
|
-
// This is the dynamic equivalent of `Math.floor(scrollTop / itemHeight)`.
|
|
1910
|
+
// Find start/end indices based on positions
|
|
1881
1911
|
let startIndex = 0;
|
|
1882
|
-
// A simple loop is robust and easy to understand.
|
|
1883
1912
|
for (let i = 0; i < positions.length; i++) {
|
|
1884
1913
|
if (positions[i]! >= scrollTop) {
|
|
1885
1914
|
startIndex = i;
|
|
@@ -1890,7 +1919,6 @@ function createProxyHandler<T>(
|
|
|
1890
1919
|
let endIndex = startIndex;
|
|
1891
1920
|
while (
|
|
1892
1921
|
endIndex < totalCount &&
|
|
1893
|
-
positions[endIndex] &&
|
|
1894
1922
|
positions[endIndex]! < scrollTop + clientHeight
|
|
1895
1923
|
) {
|
|
1896
1924
|
endIndex++;
|
|
@@ -1904,39 +1932,26 @@ function createProxyHandler<T>(
|
|
|
1904
1932
|
prevRange.startIndex !== startIndex ||
|
|
1905
1933
|
prevRange.endIndex !== endIndex
|
|
1906
1934
|
) {
|
|
1907
|
-
return { startIndex
|
|
1935
|
+
return { startIndex, endIndex };
|
|
1908
1936
|
}
|
|
1909
1937
|
return prevRange;
|
|
1910
1938
|
});
|
|
1911
1939
|
};
|
|
1912
1940
|
|
|
1941
|
+
// Check if the list has grown *before* the next render cycle
|
|
1942
|
+
if (totalCount > previousTotalCount) {
|
|
1943
|
+
listGrewRef.current = true;
|
|
1944
|
+
}
|
|
1945
|
+
previousTotalCount = totalCount;
|
|
1946
|
+
|
|
1913
1947
|
container.addEventListener("scroll", handleScroll, {
|
|
1914
1948
|
passive: true,
|
|
1915
1949
|
});
|
|
1916
|
-
|
|
1917
|
-
// This logic is IDENTICAL to your original code
|
|
1918
|
-
if (stickToBottom) {
|
|
1919
|
-
if (isInitialMountRef.current) {
|
|
1920
|
-
container.scrollTo({
|
|
1921
|
-
top: container.scrollHeight,
|
|
1922
|
-
behavior: "auto",
|
|
1923
|
-
});
|
|
1924
|
-
} else if (wasAtBottom && listGrew) {
|
|
1925
|
-
requestAnimationFrame(() => {
|
|
1926
|
-
container.scrollTo({
|
|
1927
|
-
top: container.scrollHeight,
|
|
1928
|
-
behavior: "smooth",
|
|
1929
|
-
});
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
isInitialMountRef.current = false;
|
|
1934
|
-
handleScroll();
|
|
1950
|
+
handleScroll(); // Initial calculation
|
|
1935
1951
|
|
|
1936
1952
|
return () =>
|
|
1937
1953
|
container.removeEventListener("scroll", handleScroll);
|
|
1938
|
-
|
|
1939
|
-
}, [totalCount, overscan, stickToBottom, positions]);
|
|
1954
|
+
}, [totalCount, overscan, positions]); // Depend on positions to re-run scroll logic
|
|
1940
1955
|
|
|
1941
1956
|
const scrollToBottom = useCallback(
|
|
1942
1957
|
(behavior: ScrollBehavior = "smooth") => {
|
|
@@ -1950,34 +1965,28 @@ function createProxyHandler<T>(
|
|
|
1950
1965
|
[]
|
|
1951
1966
|
);
|
|
1952
1967
|
|
|
1953
|
-
// --- CHANGE 5: Update scrollToIndex to use positions array ---
|
|
1954
1968
|
const scrollToIndex = useCallback(
|
|
1955
1969
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
1956
1970
|
if (containerRef.current && positions[index] !== undefined) {
|
|
1957
1971
|
containerRef.current.scrollTo({
|
|
1958
|
-
top: positions[index],
|
|
1972
|
+
top: positions[index],
|
|
1959
1973
|
behavior,
|
|
1960
1974
|
});
|
|
1961
1975
|
}
|
|
1962
1976
|
},
|
|
1963
|
-
[positions]
|
|
1977
|
+
[positions]
|
|
1964
1978
|
);
|
|
1965
1979
|
|
|
1966
|
-
// --- CHANGE 6: Update props to use dynamic totalHeight and offsets ---
|
|
1967
1980
|
const virtualizerProps = {
|
|
1968
1981
|
outer: {
|
|
1969
1982
|
ref: containerRef,
|
|
1970
1983
|
style: { overflowY: "auto", height: "100%" },
|
|
1971
1984
|
},
|
|
1972
1985
|
inner: {
|
|
1973
|
-
style: {
|
|
1974
|
-
height: `${totalHeight}px`, // Use calculated total height
|
|
1975
|
-
position: "relative",
|
|
1976
|
-
},
|
|
1986
|
+
style: { height: `${totalHeight}px`, position: "relative" },
|
|
1977
1987
|
},
|
|
1978
1988
|
list: {
|
|
1979
1989
|
style: {
|
|
1980
|
-
// Use the pre-calculated position of the first visible item
|
|
1981
1990
|
transform: `translateY(${positions[range.startIndex] || 0}px)`,
|
|
1982
1991
|
},
|
|
1983
1992
|
},
|