cogsbox-state 0.5.301 → 0.5.303

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.301",
3
+ "version": "0.5.303",
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,6 +1820,12 @@ function createProxyHandler<T>(
1820
1820
  []
1821
1821
  );
1822
1822
 
1823
+ // Track scroll position
1824
+ const isAtBottomRef = useRef(stickToBottom);
1825
+ const previousTotalCountRef = useRef(0);
1826
+ const isInitialMountRef = useRef(true);
1827
+ const hasScrolledToBottomRef = useRef(false); // Track if we've done initial scroll
1828
+
1823
1829
  useEffect(() => {
1824
1830
  const unsubscribe = getGlobalStore
1825
1831
  .getState()
@@ -1831,30 +1837,38 @@ function createProxyHandler<T>(
1831
1837
  };
1832
1838
  }, [stateKey, forceRecalculate]);
1833
1839
 
1834
- const isAtBottomRef = useRef(stickToBottom);
1835
- const isInitialMountRef = useRef(true);
1836
- const previousTotalCountRef = useRef(0);
1837
-
1838
1840
  const sourceArray = getGlobalStore().getNestedState(
1839
1841
  stateKey,
1840
1842
  path
1841
1843
  ) as any[];
1842
1844
  const totalCount = sourceArray.length;
1843
1845
 
1844
- const { totalHeight, positions } = useMemo(() => {
1845
- const shadowArray =
1846
- getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1847
- [];
1848
- let height = 0;
1849
- const pos: number[] = [];
1850
- for (let i = 0; i < totalCount; i++) {
1851
- pos[i] = height;
1852
- const measuredHeight =
1853
- shadowArray[i]?.virtualizer?.itemHeight;
1854
- height += measuredHeight || itemHeight;
1855
- }
1856
- return { totalHeight: height, positions: pos };
1857
- }, [totalCount, stateKey, path, itemHeight, heightsVersion]);
1846
+ const { totalHeight, positions, allItemsMeasured } =
1847
+ useMemo(() => {
1848
+ const shadowArray =
1849
+ getGlobalStore
1850
+ .getState()
1851
+ .getShadowMetadata(stateKey, path) || [];
1852
+ let height = 0;
1853
+ const pos: number[] = [];
1854
+ let measured = true;
1855
+
1856
+ for (let i = 0; i < totalCount; i++) {
1857
+ pos[i] = height;
1858
+ const measuredHeight =
1859
+ shadowArray[i]?.virtualizer?.itemHeight;
1860
+ if (!measuredHeight && totalCount > 0) {
1861
+ measured = false;
1862
+ }
1863
+ height += measuredHeight || itemHeight;
1864
+ }
1865
+
1866
+ return {
1867
+ totalHeight: height,
1868
+ positions: pos,
1869
+ allItemsMeasured: measured,
1870
+ };
1871
+ }, [totalCount, stateKey, path, itemHeight, heightsVersion]);
1858
1872
 
1859
1873
  const virtualState = useMemo(() => {
1860
1874
  const start = Math.max(0, range.startIndex);
@@ -1868,46 +1882,46 @@ function createProxyHandler<T>(
1868
1882
  ...meta,
1869
1883
  validIndices,
1870
1884
  });
1871
- }, [range.startIndex, range.endIndex, sourceArray]);
1885
+ }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1872
1886
 
1873
1887
  useLayoutEffect(() => {
1874
1888
  const container = containerRef.current;
1875
1889
  if (!container) return;
1876
1890
 
1891
+ const wasAtBottom = isAtBottomRef.current;
1877
1892
  const listGrew = totalCount > previousTotalCountRef.current;
1878
1893
  previousTotalCountRef.current = totalCount;
1879
1894
 
1880
- const wasAtBottom = isAtBottomRef.current;
1881
-
1882
1895
  const handleScroll = () => {
1883
1896
  const { scrollTop, clientHeight, scrollHeight } = container;
1897
+ // Consider "at bottom" if within 10px
1884
1898
  isAtBottomRef.current =
1885
- scrollHeight - scrollTop - clientHeight < 5;
1886
-
1887
- let search = (list: number[], value: number) => {
1888
- let low = 0,
1889
- high = list.length - 1;
1890
- while (low <= high) {
1891
- const mid = Math.floor((low + high) / 2);
1892
- const midValue = list[mid]!;
1893
- if (midValue < value) {
1894
- low = mid + 1;
1895
- } else {
1896
- high = mid - 1;
1897
- }
1899
+ scrollHeight - scrollTop - clientHeight < 10;
1900
+
1901
+ // Binary search for start index
1902
+ let low = 0,
1903
+ high = totalCount - 1;
1904
+ while (low <= high) {
1905
+ const mid = Math.floor((low + high) / 2);
1906
+ if (positions[mid]! < scrollTop) {
1907
+ low = mid + 1;
1908
+ } else {
1909
+ high = mid - 1;
1898
1910
  }
1899
- return low;
1900
- };
1901
- let startIndex = search(positions, scrollTop);
1911
+ }
1912
+ const startIndex = Math.max(0, high - overscan);
1913
+
1914
+ // Find end index
1902
1915
  let endIndex = startIndex;
1916
+ const visibleEnd = scrollTop + clientHeight;
1903
1917
  while (
1904
1918
  endIndex < totalCount &&
1905
- positions[endIndex]! < scrollTop + clientHeight
1919
+ positions[endIndex]! < visibleEnd
1906
1920
  ) {
1907
1921
  endIndex++;
1908
1922
  }
1909
- startIndex = Math.max(0, startIndex - overscan);
1910
1923
  endIndex = Math.min(totalCount, endIndex + overscan);
1924
+
1911
1925
  setRange((prevRange) => {
1912
1926
  if (
1913
1927
  prevRange.startIndex !== startIndex ||
@@ -1922,29 +1936,64 @@ function createProxyHandler<T>(
1922
1936
  container.addEventListener("scroll", handleScroll, {
1923
1937
  passive: true,
1924
1938
  });
1925
- handleScroll();
1926
1939
 
1940
+ // Handle stick to bottom
1927
1941
  if (stickToBottom) {
1928
- if (isInitialMountRef.current) {
1929
- container.scrollTo({
1930
- top: container.scrollHeight,
1931
- behavior: "auto",
1932
- });
1933
- } else if (listGrew && wasAtBottom) {
1934
- container.scrollTo({
1935
- top: container.scrollHeight,
1936
- behavior: "auto",
1942
+ if (
1943
+ isInitialMountRef.current &&
1944
+ !hasScrolledToBottomRef.current
1945
+ ) {
1946
+ // For initial mount, wait for items to be measured
1947
+ if (allItemsMeasured && totalCount > 0) {
1948
+ container.scrollTo({
1949
+ top: container.scrollHeight,
1950
+ behavior: "auto",
1951
+ });
1952
+ hasScrolledToBottomRef.current = true;
1953
+ isInitialMountRef.current = false;
1954
+ } else if (totalCount > 0) {
1955
+ // If not all measured yet, try again soon
1956
+ const retryTimer = setTimeout(() => {
1957
+ if (containerRef.current && isInitialMountRef.current) {
1958
+ containerRef.current.scrollTo({
1959
+ top: containerRef.current.scrollHeight,
1960
+ behavior: "auto",
1961
+ });
1962
+ hasScrolledToBottomRef.current = true;
1963
+ isInitialMountRef.current = false;
1964
+ }
1965
+ }, 100);
1966
+ return () => clearTimeout(retryTimer);
1967
+ }
1968
+ } else if (
1969
+ !isInitialMountRef.current &&
1970
+ wasAtBottom &&
1971
+ listGrew
1972
+ ) {
1973
+ // New items added and we were at bottom - stay at bottom
1974
+ requestAnimationFrame(() => {
1975
+ container.scrollTo({
1976
+ top: container.scrollHeight,
1977
+ behavior: "smooth",
1978
+ });
1937
1979
  });
1938
1980
  }
1939
- }
1940
-
1941
- if (totalCount > 0) {
1981
+ } else {
1942
1982
  isInitialMountRef.current = false;
1943
1983
  }
1944
1984
 
1985
+ // Run handleScroll once to set initial range
1986
+ handleScroll();
1987
+
1945
1988
  return () =>
1946
1989
  container.removeEventListener("scroll", handleScroll);
1947
- }, [totalCount, overscan, stickToBottom, positions]);
1990
+ }, [
1991
+ totalCount,
1992
+ positions,
1993
+ overscan,
1994
+ stickToBottom,
1995
+ allItemsMeasured,
1996
+ ]);
1948
1997
 
1949
1998
  const scrollToBottom = useCallback(
1950
1999
  (behavior: ScrollBehavior = "smooth") => {
@@ -1960,9 +2009,9 @@ function createProxyHandler<T>(
1960
2009
 
1961
2010
  const scrollToIndex = useCallback(
1962
2011
  (index: number, behavior: ScrollBehavior = "smooth") => {
1963
- if (containerRef.current) {
2012
+ if (containerRef.current && positions[index] !== undefined) {
1964
2013
  containerRef.current.scrollTo({
1965
- top: positions[index] || 0,
2014
+ top: positions[index],
1966
2015
  behavior,
1967
2016
  });
1968
2017
  }
@@ -1973,10 +2022,13 @@ function createProxyHandler<T>(
1973
2022
  const virtualizerProps = {
1974
2023
  outer: {
1975
2024
  ref: containerRef,
1976
- style: { overflowY: "auto", height: "100%" },
2025
+ style: { overflowY: "auto" as const, height: "100%" },
1977
2026
  },
1978
2027
  inner: {
1979
- style: { height: `${totalHeight}px`, position: "relative" },
2028
+ style: {
2029
+ height: `${totalHeight}px`,
2030
+ position: "relative" as const,
2031
+ },
1980
2032
  },
1981
2033
  list: {
1982
2034
  style: {
@@ -1987,7 +2039,7 @@ function createProxyHandler<T>(
1987
2039
 
1988
2040
  return {
1989
2041
  virtualState,
1990
- virtualizerProps: virtualizerProps as any,
2042
+ virtualizerProps,
1991
2043
  scrollToBottom,
1992
2044
  scrollToIndex,
1993
2045
  };