cogsbox-state 0.5.319 → 0.5.321

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.319",
3
+ "version": "0.5.321",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/CogsState.tsx CHANGED
@@ -1813,12 +1813,15 @@ function createProxyHandler<T>(
1813
1813
  startIndex: 0,
1814
1814
  endIndex: 10,
1815
1815
  });
1816
-
1817
- // This ref tracks if the user is locked to the bottom.
1818
1816
  const isLockedToBottomRef = useRef(stickToBottom);
1819
-
1820
- // This state triggers a re-render when item heights change.
1821
1817
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1818
+ const hasScrolledToBottomRef = useRef(false);
1819
+
1820
+ const sourceArray = getGlobalStore().getNestedState(
1821
+ stateKey,
1822
+ path
1823
+ ) as any[];
1824
+ const totalCount = sourceArray.length;
1822
1825
 
1823
1826
  useEffect(() => {
1824
1827
  const unsubscribe = getGlobalStore
@@ -1829,35 +1832,52 @@ function createProxyHandler<T>(
1829
1832
  return unsubscribe;
1830
1833
  }, [stateKey]);
1831
1834
 
1832
- const sourceArray = getGlobalStore().getNestedState(
1833
- stateKey,
1834
- path
1835
- ) as any[];
1836
- const totalCount = sourceArray.length;
1835
+ const { totalHeight, positions, bottomItemsMeasured } =
1836
+ useMemo(() => {
1837
+ const shadowArray =
1838
+ getGlobalStore
1839
+ .getState()
1840
+ .getShadowMetadata(stateKey, path) || [];
1841
+ let height = 0;
1842
+ const pos: number[] = [];
1843
+ let bottomMeasuredCount = 0;
1844
+
1845
+ // Check how many of the last 20 items are measured
1846
+ const checkFromIndex = Math.max(0, totalCount - 20);
1847
+
1848
+ for (let i = 0; i < totalCount; i++) {
1849
+ pos[i] = height;
1850
+ const measuredHeight =
1851
+ shadowArray[i]?.virtualizer?.itemHeight;
1852
+
1853
+ if (measuredHeight) {
1854
+ height += measuredHeight;
1855
+ if (i >= checkFromIndex) {
1856
+ bottomMeasuredCount++;
1857
+ }
1858
+ } else {
1859
+ height += itemHeight;
1860
+ }
1861
+ }
1837
1862
 
1838
- // Calculate heights from shadow state. This runs when data or measurements change.
1839
- const { totalHeight, positions } = useMemo(() => {
1840
- const shadowArray =
1841
- getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1842
- [];
1843
- let height = 0;
1844
- const pos: number[] = [];
1845
- for (let i = 0; i < totalCount; i++) {
1846
- pos[i] = height;
1847
- const measuredHeight =
1848
- shadowArray[i]?.virtualizer?.itemHeight;
1849
- height += measuredHeight || itemHeight;
1850
- }
1851
- return { totalHeight: height, positions: pos };
1852
- }, [
1853
- totalCount,
1854
- stateKey,
1855
- path.join("."),
1856
- itemHeight,
1857
- shadowUpdateTrigger,
1858
- ]);
1863
+ // Bottom items are measured if we have measurements for the last 20 items
1864
+ const bottomReady =
1865
+ bottomMeasuredCount >=
1866
+ Math.min(20, totalCount - checkFromIndex);
1867
+
1868
+ return {
1869
+ totalHeight: height,
1870
+ positions: pos,
1871
+ bottomItemsMeasured: bottomReady,
1872
+ };
1873
+ }, [
1874
+ totalCount,
1875
+ stateKey,
1876
+ path.join("."),
1877
+ itemHeight,
1878
+ shadowUpdateTrigger,
1879
+ ]);
1859
1880
 
1860
- // Memoize the virtualized slice of data.
1861
1881
  const virtualState = useMemo(() => {
1862
1882
  const start = Math.max(0, range.startIndex);
1863
1883
  const end = Math.min(totalCount, range.endIndex);
@@ -1872,14 +1892,10 @@ function createProxyHandler<T>(
1872
1892
  });
1873
1893
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1874
1894
 
1875
- // This is the main effect that handles all scrolling and updates.
1876
1895
  useLayoutEffect(() => {
1877
1896
  const container = containerRef.current;
1878
1897
  if (!container) return;
1879
1898
 
1880
- let scrollTimeoutId: NodeJS.Timeout;
1881
-
1882
- // This function determines what's visible in the viewport.
1883
1899
  const updateVirtualRange = () => {
1884
1900
  if (!container) return;
1885
1901
  const { scrollTop } = container;
@@ -1903,7 +1919,6 @@ function createProxyHandler<T>(
1903
1919
  setRange({ startIndex, endIndex });
1904
1920
  };
1905
1921
 
1906
- // This function handles ONLY user-initiated scrolls.
1907
1922
  const handleUserScroll = () => {
1908
1923
  isLockedToBottomRef.current =
1909
1924
  container.scrollHeight -
@@ -1917,28 +1932,39 @@ function createProxyHandler<T>(
1917
1932
  passive: true,
1918
1933
  });
1919
1934
 
1920
- // In useLayoutEffect, replace this section:
1921
- if (stickToBottom) {
1922
- scrollTimeoutId = setTimeout(() => {
1923
- if (isLockedToBottomRef.current) {
1924
- container.scrollTo({
1925
- top: container.scrollHeight + 1000, // ADD A BUFFER HERE
1926
- behavior: "auto",
1927
- });
1928
- }
1929
- }, 200);
1935
+ // STICK TO BOTTOM LOGIC
1936
+ if (
1937
+ stickToBottom &&
1938
+ !hasScrolledToBottomRef.current &&
1939
+ totalCount > 0
1940
+ ) {
1941
+ if (!bottomItemsMeasured) {
1942
+ // Step 1: Jump to near bottom to trigger rendering of bottom items
1943
+ console.log(
1944
+ "[VirtualView] Jumping to near bottom to trigger measurements"
1945
+ );
1946
+ const jumpPosition = Math.max(
1947
+ 0,
1948
+ (totalCount - 30) * itemHeight
1949
+ );
1950
+ container.scrollTop = jumpPosition;
1951
+ } else {
1952
+ // Step 2: Bottom items are measured, now scroll to actual bottom
1953
+ console.log(
1954
+ "[VirtualView] Bottom items measured, scrolling to true bottom"
1955
+ );
1956
+ hasScrolledToBottomRef.current = true;
1957
+ container.scrollTop = container.scrollHeight;
1958
+ isLockedToBottomRef.current = true;
1959
+ }
1930
1960
  }
1931
1961
 
1932
- // Update the visible range on initial load.
1933
1962
  updateVirtualRange();
1934
1963
 
1935
- // Cleanup function is vital to prevent memory leaks.
1936
1964
  return () => {
1937
- clearTimeout(scrollTimeoutId);
1938
1965
  container.removeEventListener("scroll", handleUserScroll);
1939
1966
  };
1940
- // This effect re-runs whenever the list size or item heights change.
1941
- }, [totalCount, positions, stickToBottom]);
1967
+ }, [totalCount, positions, stickToBottom, bottomItemsMeasured]);
1942
1968
 
1943
1969
  const scrollToBottom = useCallback(
1944
1970
  (behavior: ScrollBehavior = "smooth") => {
@@ -1969,7 +1995,13 @@ function createProxyHandler<T>(
1969
1995
  const virtualizerProps = {
1970
1996
  outer: {
1971
1997
  ref: containerRef,
1972
- style: { overflowY: "auto" as const, height: "100%" },
1998
+ style: {
1999
+ overflowY: "auto" as const,
2000
+ height: "100%",
2001
+ overflowAnchor: stickToBottom
2002
+ ? ("auto" as const)
2003
+ : ("none" as const),
2004
+ },
1973
2005
  },
1974
2006
  inner: {
1975
2007
  style: {