Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export class S2TableLayout<T> extends TableLayout<T> {
// we want the body to be sticky and only as wide as the table so it is always in view if loading/empty
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect.width = this.virtualizer!.visibleRect.width - 80;
layoutInfo.rect.width = this.virtualizer!.size.width - 80;
}

return [
Expand All @@ -228,7 +228,7 @@ export class S2TableLayout<T> extends TableLayout<T> {
let layoutNode = super.buildLoader(node, x, y);
let {layoutInfo} = layoutNode;
layoutInfo.allowOverflow = true;
layoutInfo.rect.width = this.virtualizer!.visibleRect.width;
layoutInfo.rect.width = this.virtualizer!.size.width;
// If performing first load or empty, the body will be sticky so we don't want to apply sticky to the loader, otherwise it will
// affect the positioning of the empty state renderer
let collection = this.virtualizer!.collection;
Expand All @@ -246,7 +246,7 @@ export class S2TableLayout<T> extends TableLayout<T> {
// If loading or empty, we'll want the body to be sticky and centered
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect = new Rect(40, 40, this.virtualizer!.visibleRect.width - 80, this.virtualizer!.visibleRect.height - 80);
layoutInfo.rect = new Rect(40, 40, this.virtualizer!.size.width - 80, this.virtualizer!.size.height - 80);
layoutInfo.isSticky = true;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/react-aria-components/src/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicat
let {layout, layoutOptions} = useContext(LayoutContext)!;
let layoutOptions2 = layout.useLayoutOptions?.();
let state = useVirtualizerState({
allowsWindowScrolling: true,
layout,
collection,
renderView: (type, item) => {
Expand All @@ -98,9 +99,11 @@ function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicat

let {contentProps} = useScrollView({
onVisibleRectChange: state.setVisibleRect,
onSizeChange: state.setSize,
contentSize: state.contentSize,
onScrollStart: state.startScrolling,
onScrollEnd: state.endScrolling
onScrollEnd: state.endScrolling,
allowsWindowScrolling: true
}, scrollRef!);

return (
Expand Down
26 changes: 17 additions & 9 deletions packages/react-aria/src/virtualizer/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ import {useResizeObserver} from '../utils/useResizeObserver';
interface ScrollViewProps extends Omit<HTMLAttributes<HTMLElement>, 'onScroll'> {
contentSize: Size,
onVisibleRectChange: (rect: Rect) => void,
onSizeChange?: (size: Size) => void,
children?: ReactNode,
innerStyle?: CSSProperties,
onScrollStart?: () => void,
onScrollEnd?: () => void,
scrollDirection?: 'horizontal' | 'vertical' | 'both',
onScroll?: (e: Event) => void
onScroll?: (e: Event) => void,
allowsWindowScrolling?: boolean
}

function ScrollView(props: ScrollViewProps, ref: ForwardedRef<HTMLDivElement | null>) {
Expand Down Expand Up @@ -71,11 +73,13 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
let {
contentSize,
onVisibleRectChange,
onSizeChange,
innerStyle,
onScrollStart,
onScrollEnd,
scrollDirection = 'both',
onScroll: onScrollProp,
allowsWindowScrolling,
...otherProps
} = props;

Expand All @@ -101,14 +105,16 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
// their sizes into account for performance reasons. Their scroll positions are accounted for in viewportOffset
// though (due to getBoundingClientRect). This may result in more rows than absolutely necessary being rendered,
// but no more than the entire height of the viewport which is good enough for virtualization use cases.
let visibleRect = new Rect(
state.viewportOffset.x + state.scrollPosition.x,
state.viewportOffset.y + state.scrollPosition.y,
Math.max(0, Math.min(state.size.width - state.viewportOffset.x, state.viewportSize.width)),
Math.max(0, Math.min(state.size.height - state.viewportOffset.y, state.viewportSize.height))
);
let visibleRect = allowsWindowScrolling
? new Rect(
state.viewportOffset.x + state.scrollPosition.x,
state.viewportOffset.y + state.scrollPosition.y,
Math.max(0, Math.min(state.size.width - state.viewportOffset.x, state.viewportSize.width)),
Math.max(0, Math.min(state.size.height - state.viewportOffset.y, state.viewportSize.height))
)
: new Rect(state.scrollPosition.x, state.scrollPosition.y, state.size.width, state.size.height);
onVisibleRectChange(visibleRect);
}, [state, onVisibleRectChange]);
}, [state, allowsWindowScrolling, onVisibleRectChange]);

let [isScrolling, setScrolling] = useState(false);

Expand Down Expand Up @@ -232,6 +238,7 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
state.size = new Size(w, h);
flush(() => {
updateVisibleRect();
onSizeChange?.(state.size);
});

// If the clientWidth or clientHeight changed, scrollbars appeared or disappeared as
Expand All @@ -243,12 +250,13 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
state.size = new Size(dom.clientWidth, dom.clientHeight);
flush(() => {
updateVisibleRect();
onSizeChange?.(state.size);
});
}
}

isUpdatingSize.current = false;
}, [ref, state, updateVisibleRect]);
}, [ref, state, updateVisibleRect, onSizeChange]);
let updateSizeEvent = useEffectEvent(updateSize);

// Track the size of the entire window viewport, which is used to bound the size of the virtualizer's visible rectangle.
Expand Down
18 changes: 9 additions & 9 deletions packages/react-stately/src/layout/GridLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,22 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
} = invalidationContext.layoutOptions || {};
this.dropIndicatorThickness = dropIndicatorThickness;

let visibleWidth = this.virtualizer!.visibleRect.width;
let virtualizerWidth = this.virtualizer!.size.width;

// The max item width is always the entire viewport.
// If the max item height is infinity, scale in proportion to the max width.
let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
let maxItemWidth = Math.min(maxItemSize.width, virtualizerWidth);
let maxItemHeight = Number.isFinite(maxItemSize.height)
? maxItemSize.height
: Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);

// Compute the number of rows and columns needed to display the content
let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
let columns = Math.floor(virtualizerWidth / (minItemSize.width + minSpace.width));
let numColumns = Math.max(1, Math.min(maxColumns, columns));
this.numColumns = numColumns;

// Compute the available width (minus the space between items)
let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
let width = virtualizerWidth - (minSpace.width * Math.max(0, numColumns));

// Compute the item width based on the space available
let itemWidth = Math.floor(width / numColumns);
Expand All @@ -139,9 +139,9 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));

// Compute the horizontal spacing, content height and horizontal margin
let horizontalSpacing = Math.min(Math.max(maxHorizontalSpace, minSpace.width), Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1)));
let horizontalSpacing = Math.min(Math.max(maxHorizontalSpace, minSpace.width), Math.floor((virtualizerWidth - numColumns * itemWidth) / (numColumns + 1)));
this.gap = new Size(horizontalSpacing, minSpace.height);
this.margin = Math.floor((visibleWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2);
this.margin = Math.floor((virtualizerWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2);

// If there is a skeleton loader within the last 2 items in the collection, increment the collection size
// so that an additional row is added for the skeletons.
Expand Down Expand Up @@ -214,7 +214,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
y += maxHeight + minSpace.height;

// Keep adding skeleton rows until we fill the viewport
if (skeleton && row === rows - 1 && y < this.virtualizer!.visibleRect.height) {
if (skeleton && row === rows - 1 && y < this.virtualizer!.size.height) {
rows++;
}
}
Expand All @@ -225,7 +225,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
if (skeletonCount > 0 || !lastNode.props.isLoading) {
loaderHeight = 0;
}
const loaderWidth = visibleWidth - horizontalSpacing * 2;
const loaderWidth = virtualizerWidth - horizontalSpacing * 2;
// Note that if the user provides isLoading to their sentinel during a case where they only want to render the emptyState, this will reserve
// room for the loader alongside rendering the emptyState
let rect = new Rect(horizontalSpacing, y, loaderWidth, loaderHeight);
Expand All @@ -235,7 +235,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
}

this.layoutInfos = newLayoutInfos;
this.contentSize = new Size(this.virtualizer!.visibleRect.width, y);
this.contentSize = new Size(this.virtualizer!.size.width, y);
}

getLayoutInfo(key: Key): LayoutInfo | null {
Expand Down
10 changes: 5 additions & 5 deletions packages/react-stately/src/layout/ListLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte

offset = Math.max(offset - this.gap, 0);
offset += isEmptyOrLoading ? 0 : this.padding;
this.contentSize = this.orientation === 'horizontal' ? new Size(offset, this.virtualizer!.visibleRect.height) : new Size(this.virtualizer!.visibleRect.width, offset);
this.contentSize = this.orientation === 'horizontal' ? new Size(offset, this.virtualizer!.size.height) : new Size(this.virtualizer!.size.width, offset);
return nodes;
}

Expand Down Expand Up @@ -445,8 +445,8 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte

protected buildSection(node: Node<T>, x: number, y: number): LayoutNode {
let collection = this.virtualizer!.collection;
let width = this.virtualizer!.visibleRect.width - this.padding - x;
let height = this.virtualizer!.visibleRect.height - this.padding - y;
let width = this.virtualizer!.size.width - this.padding - x;
let height = this.virtualizer!.size.height - this.padding - y;
let rect = this.orientation === 'horizontal' ? new Rect(x, y, 0, height) : new Rect(x, y, width, 0);
let layoutInfo = new LayoutInfo(node.type, node.key, rect);

Expand Down Expand Up @@ -497,7 +497,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
protected buildSectionHeader(node: Node<T>, x: number, y: number): LayoutNode {
let widthProperty = this.orientation === 'horizontal' ? 'height' : 'width';
let heightProperty = this.orientation === 'horizontal' ? 'width' : 'height';
let width = this.virtualizer!.visibleRect[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let width = this.virtualizer!.size[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let rectHeight = this.headingSize;
let isEstimated = false;

Expand Down Expand Up @@ -538,7 +538,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
let widthProperty = this.orientation === 'horizontal' ? 'height' : 'width';
let heightProperty = this.orientation === 'horizontal' ? 'width' : 'height';

let width = this.virtualizer!.visibleRect[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let width = this.virtualizer!.size[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let rectHeight = this.rowSize;
let isEstimated = false;

Expand Down
4 changes: 2 additions & 2 deletions packages/react-stately/src/layout/TableLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
}
} else if (invalidationContext.sizeChanged || this.columnsChanged(newCollection, this.lastCollection)) {
let columnLayout = new TableColumnLayout({});
this.columnWidths = columnLayout.buildColumnWidths(this.virtualizer!.visibleRect.width - this.padding * 2, newCollection, new Map());
this.columnWidths = columnLayout.buildColumnWidths(this.virtualizer!.size.width - this.padding * 2, newCollection, new Map());
invalidationContext.sizeChanged = true;
}

Expand Down Expand Up @@ -345,7 +345,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
// Make sure that the table body gets a height if empty or performing initial load
let isEmptyOrLoading = collection?.size === 0;
if (isEmptyOrLoading) {
y = this.virtualizer!.visibleRect.maxY;
y = this.virtualizer!.size.height;
} else {
y -= this.gap;
}
Expand Down
18 changes: 9 additions & 9 deletions packages/react-stately/src/layout/WaterfallLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,21 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
loaderHeight = DEFAULT_OPTIONS.loaderHeight
} = invalidationContext.layoutOptions || {};
this.dropIndicatorThickness = dropIndicatorThickness;
let visibleWidth = this.virtualizer!.visibleRect.width;
let virtualizerWidth = this.virtualizer!.size.width;

// The max item width is always the entire viewport.
// If the max item height is infinity, scale in proportion to the max width.
let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
let maxItemWidth = Math.min(maxItemSize.width, virtualizerWidth);
let maxItemHeight = Number.isFinite(maxItemSize.height)
? maxItemSize.height
: Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);

// Compute the number of rows and columns needed to display the content
let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
let columns = Math.floor(virtualizerWidth / (minItemSize.width + minSpace.width));
let numColumns = Math.max(1, Math.min(maxColumns, columns));

// Compute the available width (minus the space between items)
let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
let width = virtualizerWidth - (minSpace.width * Math.max(0, numColumns));

// Compute the item width based on the space available
let itemWidth = Math.floor(width / numColumns);
Expand All @@ -133,8 +133,8 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));

// Compute the horizontal spacing, content height and horizontal margin
let horizontalSpacing = Math.min(Math.max(maxHorizontalSpace, minSpace.width), Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1)));
this.margin = Math.floor((visibleWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2);
let horizontalSpacing = Math.min(Math.max(maxHorizontalSpace, minSpace.width), Math.floor((virtualizerWidth - numColumns * itemWidth) / (numColumns + 1)));
this.margin = Math.floor((virtualizerWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2);

// Setup an array of column heights
let columnHeights = Array(numColumns).fill(minSpace.height);
Expand Down Expand Up @@ -176,7 +176,7 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
let startingHeights = [...columnHeights];
while (
!columnHeights.every((h, i) => h !== startingHeights[i]) ||
Math.min(...columnHeights) < this.virtualizer!.visibleRect.height
Math.min(...columnHeights) < this.virtualizer!.size.height
) {
let key = `${node.key}-${skeletonCount++}`;
let content = this.layoutInfos.get(key)?.content || {...node};
Expand All @@ -200,7 +200,7 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
if (skeletonCount > 0 || !lastNode.props.isLoading) {
loaderHeight = 0;
}
const loaderWidth = visibleWidth - horizontalSpacing * 2;
const loaderWidth = virtualizerWidth - horizontalSpacing * 2;
// Note that if the user provides isLoading to their sentinel during a case where they only want to render the emptyState, this will reserve
// room for the loader alongside rendering the emptyState
let rect = new Rect(horizontalSpacing, maxHeight, loaderWidth, loaderHeight);
Expand All @@ -209,7 +209,7 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
maxHeight = layoutInfo.rect.maxY;
}

this.contentSize = new Size(this.virtualizer!.visibleRect.width, maxHeight);
this.contentSize = new Size(this.virtualizer!.size.width, maxHeight);
this.layoutInfos = newLayoutInfos;
this.numColumns = numColumns;
}
Expand Down
15 changes: 12 additions & 3 deletions packages/react-stately/src/virtualizer/Virtualizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export class Virtualizer<T extends object, V> {
readonly contentSize: Size;
/** The currently visible rectangle. */
readonly visibleRect: Rect;
/** The size of the virtualizer scroll view. */
readonly size: Size;
/** The set of persisted keys that are always present in the DOM, even if not currently in view. */
readonly persistedKeys: Set<Key>;

Expand All @@ -74,6 +76,7 @@ export class Virtualizer<T extends object, V> {
this.layout = options.layout;
this.contentSize = new Size;
this.visibleRect = new Rect;
this.size = new Size;
this.persistedKeys = new Set();
this._visibleViews = new Map();
this._renderedContent = new WeakMap();
Expand Down Expand Up @@ -288,19 +291,25 @@ export class Virtualizer<T extends object, V> {
needsUpdate = true;
}

if (!this.visibleRect.equals(opts.visibleRect)) {
if (!this.visibleRect.equals(opts.visibleRect) || !this.size.equals(opts.size)) {
this._overscanManager.setVisibleRect(opts.visibleRect);
let shouldInvalidate = this.layout.shouldInvalidate(opts.visibleRect, this.visibleRect);

// Create a rectangle using the scroll position and layout size of the scroll view. This is not the same
// as the visibleRect, whose width and height may change during window scrolling.
let oldRect = new Rect(this.visibleRect.x, this.visibleRect.y, this.size.width, this.size.height);
let newRect = new Rect(opts.visibleRect.x, opts.visibleRect.y, opts.size.width, opts.size.height);
let shouldInvalidate = this.layout.shouldInvalidate(newRect, oldRect);

if (shouldInvalidate) {
offsetChanged = !opts.visibleRect.pointEquals(this.visibleRect);
sizeChanged = !opts.visibleRect.sizeEquals(this.visibleRect);
sizeChanged = !this.size.equals(opts.size);
needsLayout = true;
} else {
needsUpdate = true;
}

mutableThis.visibleRect = opts.visibleRect;
mutableThis.size = opts.size;
}

if (opts.invalidationContext !== this._invalidationContext) {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-stately/src/virtualizer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import {Collection, Key} from '@react-types/shared';
import {Layout} from './Layout';
import {Rect} from './Rect';
import {Size} from './Size';

export interface InvalidationContext<O = any> {
contentChanged?: boolean,
Expand All @@ -34,6 +35,7 @@ export interface VirtualizerRenderOptions<T extends object, O = any> {
collection: Collection<T>,
persistedKeys?: Set<Key> | null,
visibleRect: Rect,
size: Size,
invalidationContext: InvalidationContext,
isScrolling: boolean,
layoutOptions?: O
Expand Down
Loading
Loading