aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
508 lines (505 loc) • 15.7 kB
JavaScript
'use client';
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { useEnhancedPerformance } from './useEnhancedPerformance.js';
/**
* High-performance virtualization hook for large datasets
*/
function useVirtualization(items, options) {
const {
itemHeight,
containerHeight,
overscan = 3,
enableHorizontal = false,
itemWidth,
containerWidth = 0,
threshold = 100,
onScroll
} = options;
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [measuredSizes, setMeasuredSizes] = useState(new Map());
const {
performanceMode
} = useEnhancedPerformance();
const containerRef = useRef(null);
const isScrollingRef = useRef(false);
const scrollTimeoutRef = useRef();
// Calculate item dimensions
const getItemHeight = useCallback(index => {
if (typeof itemHeight === 'function') {
return itemHeight(index);
}
return measuredSizes.get(index)?.height || itemHeight;
}, [itemHeight, measuredSizes]);
const getItemWidth = useCallback(index => {
if (!enableHorizontal || !itemWidth) return containerWidth;
if (typeof itemWidth === 'function') {
return itemWidth(index);
}
return measuredSizes.get(index)?.width || itemWidth;
}, [enableHorizontal, itemWidth, containerWidth, measuredSizes]);
// Calculate total dimensions
const totalHeight = useMemo(() => {
if (typeof itemHeight === 'number') {
return (items?.length || 0) * itemHeight;
}
let total = 0;
for (let i = 0; i < (items?.length || 0); i++) {
total += getItemHeight(i);
}
return total;
}, [items.length, itemHeight, getItemHeight]);
const totalWidth = useMemo(() => {
if (!enableHorizontal || typeof itemWidth === 'number') {
return containerWidth;
}
let total = 0;
for (let i = 0; i < (items?.length || 0); i++) {
total += getItemWidth(i);
}
return total;
}, [enableHorizontal, itemWidth, containerWidth, items.length, getItemWidth]);
// Calculate visible range
const visibleRange = useMemo(() => {
let startIndex = 0;
let endIndex = items.length - 1;
if (typeof itemHeight === 'number') {
// Fixed height optimization
startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
endIndex = Math.min((items?.length || 1) - 1, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan);
} else {
// Variable height calculation
let currentOffset = 0;
// Find start index
for (let i = 0; i < (items?.length || 0); i++) {
const height = getItemHeight(i);
if (currentOffset + height > scrollTop) {
startIndex = Math.max(0, i - overscan);
break;
}
currentOffset += height;
}
// Find end index
currentOffset = 0;
for (let i = 0; i < startIndex; i++) {
currentOffset += getItemHeight(i);
}
for (let i = startIndex; i < items.length; i++) {
const height = getItemHeight(i);
if (currentOffset > scrollTop + containerHeight) {
endIndex = Math.min(items.length - 1, i + overscan);
break;
}
currentOffset += height;
}
}
return {
startIndex,
endIndex
};
}, [items.length, itemHeight, scrollTop, containerHeight, overscan, getItemHeight]);
// Calculate visible items with positions
const visibleItems = useMemo(() => {
const result = [];
let currentOffset = 0;
// Calculate offset to start index
for (let i = 0; i < visibleRange.startIndex; i++) {
currentOffset += getItemHeight(i);
}
// Generate visible items
for (let i = visibleRange.startIndex; i <= visibleRange.endIndex && i < items.length; i++) {
const height = getItemHeight(i);
const width = getItemWidth(i);
result.push({
index: i,
data: items[i],
style: {
position: 'absolute',
top: currentOffset,
left: enableHorizontal ? 0 : undefined,
width: enableHorizontal ? width : '100%',
height,
contain: 'layout style paint'
}
});
currentOffset += height;
}
return result;
}, [visibleRange, items, getItemHeight, getItemWidth, enableHorizontal]);
// Measure element size
const measureElement = useCallback((index, element) => {
if (!element) return;
const rect = element.getBoundingClientRect();
setMeasuredSizes(prev => new Map(prev.set(index, {
width: rect.width,
height: rect.height
})));
}, []);
// Optimized scroll handler
const handleScroll = useCallback(e => {
const target = e.currentTarget;
const newScrollTop = target.scrollTop;
const newScrollLeft = target.scrollLeft;
setScrollTop(newScrollTop);
setScrollLeft(newScrollLeft);
isScrollingRef.current = true;
// Clear existing timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// Set timeout to detect end of scrolling
scrollTimeoutRef.current = setTimeout(() => {
isScrollingRef.current = false;
}, 150);
onScroll?.(newScrollTop, newScrollLeft);
}, [onScroll]);
// Scroll to index
const scrollToIndex = useCallback((index, align = 'start') => {
if (!containerRef.current) return;
let offset = 0;
for (let i = 0; i < index; i++) {
offset += getItemHeight(i);
}
const itemHeight = getItemHeight(index);
switch (align) {
case 'center':
offset -= (containerHeight - itemHeight) / 2;
break;
case 'end':
offset -= containerHeight - itemHeight;
break;
}
containerRef.current.scrollTo({
top: Math.max(0, offset),
behavior: 'smooth'
});
}, [getItemHeight, containerHeight]);
// Performance optimization: disable virtualization for small lists
const shouldVirtualize = useMemo(() => {
if (performanceMode === 'high') return (items?.length || 0) > threshold * 2;
if (performanceMode === 'low') return (items?.length || 0) > threshold / 2;
return (items?.length || 0) > threshold;
}, [items?.length, threshold, performanceMode]);
// Container props
const containerProps = {
ref: containerRef,
onScroll: handleScroll,
style: {
height: containerHeight,
width: enableHorizontal ? containerWidth : '100%',
overflow: 'auto',
contain: 'strict',
WebkitOverflowScrolling: 'touch'
}
};
// Scroll element props (inner container)
const scrollElementProps = {
style: {
height: totalHeight,
width: enableHorizontal ? totalWidth : '100%',
position: 'relative',
contain: 'layout'
}
};
// Return all items if virtualization is disabled
const finalVisibleItems = shouldVirtualize ? visibleItems : items.map((data, index) => ({
index,
data,
style: {
position: 'relative',
contain: 'layout style paint'
}
}));
return {
startIndex: shouldVirtualize ? visibleRange.startIndex : 0,
endIndex: shouldVirtualize ? visibleRange.endIndex : items.length - 1,
visibleItems: finalVisibleItems,
totalHeight,
totalWidth,
scrollTop,
scrollLeft,
containerProps,
scrollElementProps,
measureElement,
scrollToIndex
};
}
/**
* Hook for grid virtualization
*/
function useGridVirtualization(items, options) {
const {
itemHeight,
itemWidth,
containerHeight,
containerWidth,
columns,
gap = 0,
overscan = 3
} = options;
const [scrollTop, setScrollTop] = useState(0);
const rows = Math.ceil(items.length / columns);
const totalHeight = rows * (itemHeight + gap) - gap;
const startRow = Math.max(0, Math.floor(scrollTop / (itemHeight + gap)) - overscan);
const endRow = Math.min(rows - 1, Math.ceil((scrollTop + containerHeight) / (itemHeight + gap)) + overscan);
const visibleItems = useMemo(() => {
const result = [];
for (let row = startRow; row <= endRow; row++) {
for (let col = 0; col < columns; col++) {
const index = row * columns + col;
if (index >= (items?.length || 0)) break;
result.push({
index,
data: items[index],
row,
col,
style: {
position: 'absolute',
top: row * (itemHeight + gap),
left: col * (itemWidth + gap),
width: itemWidth,
height: itemHeight,
contain: 'layout style paint'
}
});
}
}
return result;
}, [items, startRow, endRow, columns, itemHeight, itemWidth, gap]);
const handleScroll = useCallback(e => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
return {
visibleItems,
totalHeight,
containerProps: {
onScroll: handleScroll,
style: {
height: containerHeight,
width: containerWidth,
overflow: 'auto',
contain: 'strict'
}
},
scrollElementProps: {
style: {
height: totalHeight,
width: containerWidth,
position: 'relative',
contain: 'layout'
}
}
};
}
/**
* Hook for infinite scroll with virtualization
*/
function useInfiniteVirtualization(items, options) {
const {
loadMore,
hasMore,
isLoading,
...virtualizationOptions
} = options;
const [allItems, setAllItems] = useState(items);
const loadingRef = useRef(false);
const virtualization = useVirtualization(allItems, virtualizationOptions);
// Load more items when scrolled near bottom
const handleLoadMore = useCallback(async () => {
if (loadingRef.current || !hasMore || isLoading) return;
const {
containerHeight
} = virtualizationOptions;
const {
scrollTop,
totalHeight
} = virtualization;
const scrollPercentage = (scrollTop + (containerHeight || 0)) / totalHeight;
if (scrollPercentage > 0.8) {
loadingRef.current = true;
try {
const newItems = await loadMore();
setAllItems(prev => [...prev, ...newItems]);
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('Failed to load more items:', error);
}
} finally {
loadingRef.current = false;
}
}
}, [virtualization, hasMore, isLoading, loadMore]);
// Monitor scroll for infinite loading
useEffect(() => {
handleLoadMore();
}, [handleLoadMore]);
return {
...virtualization,
isLoadingMore: loadingRef.current,
totalItems: allItems?.length || 0
};
}
/**
* Hook for window virtualization (entire viewport)
*/
function useWindowVirtualization(items, itemHeight, overscan = 5) {
const [scrollY, setScrollY] = useState(0);
const [windowHeight, setWindowHeight] = useState(typeof window !== 'undefined' ? window.innerHeight : 600);
// Update scroll position
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
const handleResize = () => {
setWindowHeight(window.innerHeight);
};
window.addEventListener('scroll', handleScroll, {
passive: true
});
window.addEventListener('resize', handleResize, {
passive: true
});
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
};
}, []);
const visibleRange = useMemo(() => {
if (typeof itemHeight === 'number') {
const startIndex = Math.max(0, Math.floor(scrollY / itemHeight) - overscan);
const endIndex = Math.min((items?.length || 1) - 1, Math.ceil((scrollY + windowHeight) / itemHeight) + overscan);
return {
startIndex,
endIndex
};
}
// Variable height calculation
let currentOffset = 0;
let startIndex = 0;
let endIndex = items.length - 1;
// Find start index
for (let i = 0; i < (items?.length || 0); i++) {
const height = typeof itemHeight === 'function' ? itemHeight(i) : itemHeight;
if (currentOffset + height > scrollY) {
startIndex = Math.max(0, i - overscan);
break;
}
currentOffset += height;
}
// Find end index
for (let i = startIndex; i < items.length; i++) {
const height = typeof itemHeight === 'function' ? itemHeight(i) : itemHeight;
if (currentOffset > scrollY + windowHeight) {
endIndex = Math.min(items.length - 1, i + overscan);
break;
}
currentOffset += height;
}
return {
startIndex,
endIndex
};
}, [scrollY, windowHeight, items.length, itemHeight, overscan]);
const visibleItems = useMemo(() => {
const result = [];
let currentOffset = 0;
// Calculate offset to start index
for (let i = 0; i < visibleRange.startIndex; i++) {
const height = typeof itemHeight === 'function' ? itemHeight(i) : itemHeight;
currentOffset += height;
}
// Generate visible items
for (let i = visibleRange.startIndex; i <= visibleRange.endIndex && i < items.length; i++) {
const height = typeof itemHeight === 'function' ? itemHeight(i) : itemHeight;
result.push({
index: i,
data: items[i],
style: {
position: 'absolute',
top: currentOffset,
width: '100%',
height,
contain: 'layout style paint'
}
});
currentOffset += height;
}
return result;
}, [visibleRange, items, itemHeight]);
return {
startIndex: visibleRange.startIndex,
endIndex: visibleRange.endIndex,
visibleItems,
scrollY,
windowHeight
};
}
/**
* Hook for table virtualization with fixed headers
*/
function useTableVirtualization(data, options) {
const {
rowHeight,
headerHeight,
containerHeight,
columns,
overscan = 3
} = options;
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const availableHeight = containerHeight - headerHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const endIndex = Math.min(data.length - 1, Math.ceil((scrollTop + availableHeight) / rowHeight) + overscan);
const visibleRows = useMemo(() => {
const result = [];
for (let i = startIndex; i <= endIndex && i < data.length; i++) {
result.push({
index: i,
data: data[i],
columns,
style: {
position: 'absolute',
top: headerHeight + i * rowHeight,
left: 0,
width: '100%',
height: rowHeight,
display: 'flex',
contain: 'layout style paint'
}
});
}
return result;
}, [startIndex, endIndex, data, columns, headerHeight, rowHeight]);
const handleScroll = useCallback(e => {
setScrollTop(e.currentTarget.scrollTop);
setScrollLeft(e.currentTarget.scrollLeft);
}, []);
const totalHeight = headerHeight + data.length * rowHeight;
const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);
return {
visibleRows,
totalHeight,
totalWidth,
scrollTop,
scrollLeft,
headerHeight,
containerProps: {
onScroll: handleScroll,
style: {
height: containerHeight,
overflow: 'auto',
contain: 'strict'
}
},
scrollElementProps: {
style: {
height: totalHeight,
width: totalWidth,
position: 'relative',
contain: 'layout'
}
}
};
}
export { useGridVirtualization, useInfiniteVirtualization, useTableVirtualization, useVirtualization, useWindowVirtualization };
//# sourceMappingURL=useVirtualization.js.map