cogsbox-state 0.5.300 → 0.5.302

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.300",
3
+ "version": "0.5.302",
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
@@ -1803,7 +1803,7 @@ function createProxyHandler<T>(
1803
1803
  options: VirtualViewOptions
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
1805
  const {
1806
- itemHeight = 50, // Default/estimated height
1806
+ itemHeight = 50,
1807
1807
  overscan = 5,
1808
1808
  stickToBottom = false,
1809
1809
  } = options;
@@ -1820,27 +1820,21 @@ function createProxyHandler<T>(
1820
1820
  []
1821
1821
  );
1822
1822
 
1823
- // --- This useEffect now cleanly subscribes to height changes ---
1823
+ // Track scroll position
1824
+ const isAtBottomRef = useRef(stickToBottom);
1825
+ const previousTotalCountRef = useRef(0);
1826
+ const isInitialMountRef = useRef(true);
1827
+
1824
1828
  useEffect(() => {
1825
- // Subscribe to shadow state changes for this specific key.
1826
1829
  const unsubscribe = getGlobalStore
1827
1830
  .getState()
1828
1831
  .subscribeToShadowState(stateKey, forceRecalculate);
1829
-
1830
- // On initial mount, we still need to trigger one recalculation
1831
- // to capture heights from the very first render.
1832
1832
  const timer = setTimeout(forceRecalculate, 50);
1833
-
1834
- // Cleanup function to unsubscribe when the component unmounts.
1835
1833
  return () => {
1836
1834
  unsubscribe();
1837
1835
  clearTimeout(timer);
1838
1836
  };
1839
- }, [stateKey, forceRecalculate]); // Runs only once on mount.
1840
-
1841
- const isAtBottomRef = useRef(stickToBottom);
1842
- const previousTotalCountRef = useRef(0);
1843
- const isInitialMountRef = useRef(true);
1837
+ }, [stateKey, forceRecalculate]);
1844
1838
 
1845
1839
  const sourceArray = getGlobalStore().getNestedState(
1846
1840
  stateKey,
@@ -1875,52 +1869,46 @@ function createProxyHandler<T>(
1875
1869
  ...meta,
1876
1870
  validIndices,
1877
1871
  });
1878
- }, [range.startIndex, range.endIndex, sourceArray]);
1872
+ }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1873
+
1879
1874
  useLayoutEffect(() => {
1880
1875
  const container = containerRef.current;
1881
1876
  if (!container) return;
1882
1877
 
1878
+ const wasAtBottom = isAtBottomRef.current;
1883
1879
  const listGrew = totalCount > previousTotalCountRef.current;
1884
-
1885
- // --- THE FIX for BOTH initial load and new entries ---
1886
- // We capture the scroll position *before* we do anything else.
1887
- // We also check if we are VERY close to the bottom. This will be true
1888
- // on initial load (0 scrollHeight, 0 scrollTop) and when user is at the end.
1889
- const wasAtBottom =
1890
- container.scrollHeight -
1891
- container.scrollTop -
1892
- container.clientHeight <
1893
- 5;
1894
-
1895
- // Now we update the ref for the *next* render cycle.
1896
1880
  previousTotalCountRef.current = totalCount;
1897
1881
 
1898
1882
  const handleScroll = () => {
1899
- // ... (binary search logic to setRange is the same) ...
1900
1883
  const { scrollTop, clientHeight, scrollHeight } = container;
1901
- let search = (list: number[], value: number) => {
1902
- let low = 0;
1903
- let high = list.length - 1;
1904
- while (low <= high) {
1905
- const mid = Math.floor((low + high) / 2);
1906
- if (list[mid]! < value) {
1907
- low = mid + 1;
1908
- } else {
1909
- high = mid - 1;
1910
- }
1884
+ // Consider "at bottom" if within 10px
1885
+ isAtBottomRef.current =
1886
+ scrollHeight - scrollTop - clientHeight < 10;
1887
+
1888
+ // Binary search for start index
1889
+ let low = 0,
1890
+ high = totalCount - 1;
1891
+ while (low <= high) {
1892
+ const mid = Math.floor((low + high) / 2);
1893
+ if (positions[mid]! < scrollTop) {
1894
+ low = mid + 1;
1895
+ } else {
1896
+ high = mid - 1;
1911
1897
  }
1912
- return low;
1913
- };
1914
- let startIndex = search(positions, scrollTop);
1898
+ }
1899
+ const startIndex = Math.max(0, high - overscan);
1900
+
1901
+ // Find end index
1915
1902
  let endIndex = startIndex;
1903
+ const visibleEnd = scrollTop + clientHeight;
1916
1904
  while (
1917
1905
  endIndex < totalCount &&
1918
- positions[endIndex]! < scrollTop + clientHeight
1906
+ positions[endIndex]! < visibleEnd
1919
1907
  ) {
1920
1908
  endIndex++;
1921
1909
  }
1922
- startIndex = Math.max(0, startIndex - overscan);
1923
1910
  endIndex = Math.min(totalCount, endIndex + overscan);
1911
+
1924
1912
  setRange((prevRange) => {
1925
1913
  if (
1926
1914
  prevRange.startIndex !== startIndex ||
@@ -1935,28 +1923,35 @@ function createProxyHandler<T>(
1935
1923
  container.addEventListener("scroll", handleScroll, {
1936
1924
  passive: true,
1937
1925
  });
1938
- handleScroll();
1939
1926
 
1940
- // This single block now handles all cases.
1941
- if (
1942
- stickToBottom &&
1943
- (listGrew || isInitialMountRef.current) &&
1944
- wasAtBottom
1945
- ) {
1946
- container.scrollTo({
1947
- top: container.scrollHeight,
1948
- behavior: "auto",
1949
- });
1927
+ // Handle stick to bottom
1928
+ if (stickToBottom) {
1929
+ if (isInitialMountRef.current) {
1930
+ // First render - go to bottom instantly
1931
+ container.scrollTo({
1932
+ top: container.scrollHeight,
1933
+ behavior: "auto",
1934
+ });
1935
+ } else if (wasAtBottom && listGrew) {
1936
+ // New items added and we were at bottom - stay at bottom
1937
+ requestAnimationFrame(() => {
1938
+ container.scrollTo({
1939
+ top: container.scrollHeight,
1940
+ behavior: "smooth",
1941
+ });
1942
+ });
1943
+ }
1950
1944
  }
1951
1945
 
1952
- // We only set this to false after the first correct layout has been established.
1953
- if (positions.length > 0) {
1954
- isInitialMountRef.current = false;
1955
- }
1946
+ // Mark as no longer initial mount after first render
1947
+ isInitialMountRef.current = false;
1948
+
1949
+ // Run handleScroll once to set initial range
1950
+ handleScroll();
1956
1951
 
1957
1952
  return () =>
1958
1953
  container.removeEventListener("scroll", handleScroll);
1959
- }, [totalCount, overscan, stickToBottom, positions]);
1954
+ }, [totalCount, positions, overscan, stickToBottom]);
1960
1955
 
1961
1956
  const scrollToBottom = useCallback(
1962
1957
  (behavior: ScrollBehavior = "smooth") => {
@@ -1972,9 +1967,9 @@ function createProxyHandler<T>(
1972
1967
 
1973
1968
  const scrollToIndex = useCallback(
1974
1969
  (index: number, behavior: ScrollBehavior = "smooth") => {
1975
- if (containerRef.current) {
1970
+ if (containerRef.current && positions[index] !== undefined) {
1976
1971
  containerRef.current.scrollTo({
1977
- top: positions[index] || 0,
1972
+ top: positions[index],
1978
1973
  behavior,
1979
1974
  });
1980
1975
  }
@@ -1985,10 +1980,13 @@ function createProxyHandler<T>(
1985
1980
  const virtualizerProps = {
1986
1981
  outer: {
1987
1982
  ref: containerRef,
1988
- style: { overflowY: "auto", height: "100%" },
1983
+ style: { overflowY: "auto" as const, height: "100%" },
1989
1984
  },
1990
1985
  inner: {
1991
- style: { height: `${totalHeight}px`, position: "relative" },
1986
+ style: {
1987
+ height: `${totalHeight}px`,
1988
+ position: "relative" as const,
1989
+ },
1992
1990
  },
1993
1991
  list: {
1994
1992
  style: {
@@ -1999,7 +1997,7 @@ function createProxyHandler<T>(
1999
1997
 
2000
1998
  return {
2001
1999
  virtualState,
2002
- virtualizerProps: virtualizerProps as any,
2000
+ virtualizerProps,
2003
2001
  scrollToBottom,
2004
2002
  scrollToIndex,
2005
2003
  };