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
5 changes: 1 addition & 4 deletions packages/@react-aria/dnd/stories/VirtualizedListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,7 @@ export const VirtualizedListBox = React.forwardRef(function (props: any, ref) {

let layout = React.useMemo(() =>
new ListLayout<unknown>({
estimatedRowHeight: 32,
padding: 8,
loaderHeight: 40,
placeholderHeight: 32
estimatedRowHeight: 32
})
, []);

Expand Down
41 changes: 39 additions & 2 deletions packages/@react-aria/grid/src/GridKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node} from '@react-types/shared';
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node, Rect, Size} from '@react-types/shared';
import {DOMLayoutDelegate} from '@react-aria/selection';
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
import {GridCollection} from '@react-types/grid';
Expand All @@ -24,6 +24,8 @@ export interface GridKeyboardDelegateOptions<C> {
direction: Direction,
collator?: Intl.Collator,
layoutDelegate?: LayoutDelegate,
/** @deprecated - Use layoutDelegate instead. */
layout?: DeprecatedLayout,
focusMode?: 'row' | 'cell'
}

Expand All @@ -42,7 +44,7 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
this.disabledBehavior = options.disabledBehavior || 'all';
this.direction = options.direction;
this.collator = options.collator;
this.layoutDelegate = options.layoutDelegate || new DOMLayoutDelegate(options.ref);
this.layoutDelegate = options.layoutDelegate || (options.layout ? new DeprecatedLayoutDelegate(options.layout) : new DOMLayoutDelegate(options.ref));
this.focusMode = options.focusMode || 'row';
}

Expand Down Expand Up @@ -356,3 +358,38 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
return null;
}
}

/* Backward compatibility for old Virtualizer Layout interface. */
interface DeprecatedLayout {
getLayoutInfo(key: Key): DeprecatedLayoutInfo,
getContentSize(): Size,
virtualizer: DeprecatedVirtualizer
}

interface DeprecatedLayoutInfo {
rect: Rect
}

interface DeprecatedVirtualizer {
visibleRect: Rect
}

class DeprecatedLayoutDelegate implements LayoutDelegate {
layout: DeprecatedLayout;

constructor(layout: DeprecatedLayout) {
this.layout = layout;
}

getContentSize(): Size {
return this.layout.getContentSize();
}

getItemRect(key: Key): Rect | null {
return this.layout.getLayoutInfo(key)?.rect || null;
}

getVisibleRect(): Rect {
return this.layout.virtualizer.visibleRect;
}
}
28 changes: 23 additions & 5 deletions packages/@react-aria/table/src/useTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {GridAria, GridProps, useGrid} from '@react-aria/grid';
import {gridIds} from './utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {LayoutDelegate} from '@react-types/shared';
import {Key, LayoutDelegate, Rect, Size} from '@react-types/shared';
import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/utils';
import {RefObject, useMemo} from 'react';
import {TableKeyboardDelegate} from './TableKeyboardDelegate';
Expand All @@ -25,7 +25,23 @@ import {useCollator, useLocale, useLocalizedStringFormatter} from '@react-aria/i

export interface AriaTableProps extends GridProps {
/** The layout object for the table. Computes what content is visible and how to position and style them. */
layoutDelegate?: LayoutDelegate
layoutDelegate?: LayoutDelegate,
/** @deprecated - Use layoutDelegate instead. */
layout?: DeprecatedLayout
}

interface DeprecatedLayout {
getLayoutInfo(key: Key): DeprecatedLayoutInfo,
getContentSize(): Size,
virtualizer: DeprecatedVirtualizer
}

interface DeprecatedLayoutInfo {
rect: Rect
}

interface DeprecatedVirtualizer {
visibleRect: Rect
}

/**
Expand All @@ -40,7 +56,8 @@ export function useTable<T>(props: AriaTableProps, state: TableState<T> | TreeGr
let {
keyboardDelegate,
isVirtualized,
layoutDelegate
layoutDelegate,
layout
} = props;

// By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
Expand All @@ -55,8 +72,9 @@ export function useTable<T>(props: AriaTableProps, state: TableState<T> | TreeGr
ref,
direction,
collator,
layoutDelegate
}), [keyboardDelegate, state.collection, state.disabledKeys, disabledBehavior, ref, direction, collator, layoutDelegate]);
layoutDelegate,
layout
}), [keyboardDelegate, state.collection, state.disabledKeys, disabledBehavior, ref, direction, collator, layoutDelegate, layout]);
let id = useId(props.id);
gridIds.set(state, id);

Expand Down
4 changes: 1 addition & 3 deletions packages/@react-aria/virtualizer/src/VirtualizerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ export function layoutInfoToStyle(layoutInfo: LayoutInfo, dir: Direction, parent
position: layoutInfo.isSticky ? 'sticky' : 'absolute',
// Sticky elements are positioned in normal document flow. Display inline-block so that they don't push other sticky columns onto the following rows.
display: layoutInfo.isSticky ? 'inline-block' : undefined,
// Use clip instead of hidden to avoid creating an implicit generic container in the accessibility tree in Firefox.
// Hidden still allows programmatic scrolling whereas clip does not.
overflow: layoutInfo.allowOverflow ? 'visible' : 'clip',
overflow: layoutInfo.allowOverflow ? 'visible' : 'hidden',
opacity: layoutInfo.opacity,
zIndex: layoutInfo.zIndex,
transform: layoutInfo.transform,
Expand Down
1 change: 1 addition & 0 deletions packages/@react-spectrum/list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@react-stately/collections": "^3.10.7",
"@react-stately/layout": "^3.13.9",
"@react-stately/list": "^3.10.5",
"@react-stately/virtualizer": "^3.7.1",
"@react-types/grid": "^3.2.6",
"@react-types/shared": "^3.23.1",
"@spectrum-icons/ui": "^3.6.7",
Expand Down
14 changes: 5 additions & 9 deletions packages/@react-spectrum/list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import InsertionIndicator from './InsertionIndicator';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {ListKeyboardDelegate} from '@react-aria/selection';
import {ListLayout} from '@react-stately/layout';
import {ListState, useListState} from '@react-stately/list';
import listStyles from './styles.css';
import {ListViewItem} from './ListViewItem';
import {ListViewLayout} from './ListViewLayout';
import {ProgressCircle} from '@react-spectrum/progress';
import React, {JSX, ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import RootDropIndicator from './RootDropIndicator';
Expand Down Expand Up @@ -70,7 +70,7 @@ interface ListViewContextValue<T> {
onAction:(key: Key) => void,
isListDraggable: boolean,
isListDroppable: boolean,
layout: ListLayout<T>,
layout: ListViewLayout<T>,
loadingState: LoadingState,
renderEmptyState?: () => JSX.Element
}
Expand All @@ -94,16 +94,12 @@ const ROW_HEIGHTS = {

function useListLayout<T>(state: ListState<T>, density: SpectrumListViewProps<T>['density'], overflowMode: SpectrumListViewProps<T>['overflowMode']) {
let {scale} = useProvider();
let isEmpty = state.collection.size === 0;
let layout = useMemo(() =>
new ListLayout<T>({
estimatedRowHeight: ROW_HEIGHTS[density][scale],
padding: 0,
loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale],
enableEmptyState: true
new ListViewLayout<T>({
estimatedRowHeight: ROW_HEIGHTS[density][scale]
})
// eslint-disable-next-line react-hooks/exhaustive-deps
, [scale, density, isEmpty, overflowMode]);
, [scale, density, overflowMode]);

return layout;
}
Expand Down
55 changes: 55 additions & 0 deletions packages/@react-spectrum/list/src/ListViewLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {InvalidationContext, LayoutInfo, Rect} from '@react-stately/virtualizer';
import {LayoutNode, ListLayout} from '@react-stately/layout';
import {Node} from '@react-types/shared';

interface ListViewLayoutProps {
isLoading?: boolean
}

export class ListViewLayout<T> extends ListLayout<T, ListViewLayoutProps> {
private isLoading: boolean = false;

validate(invalidationContext: InvalidationContext<ListViewLayoutProps>): void {
this.isLoading = invalidationContext.layoutOptions?.isLoading || false;
super.validate(invalidationContext);
}

protected buildCollection(): LayoutNode[] {
let nodes = super.buildCollection();
let y = this.contentSize.height;

if (this.isLoading) {
let rect = new Rect(0, y, this.virtualizer.visibleRect.width, nodes.length === 0 ? this.virtualizer.visibleRect.height : this.estimatedRowHeight);
let loader = new LayoutInfo('loader', 'loader', rect);
let node = {
layoutInfo: loader,
validRect: loader.rect
};
nodes.push(node);
this.layoutNodes.set(loader.key, node);
y = loader.rect.maxY;
}

if (nodes.length === 0) {
let rect = new Rect(0, y, this.virtualizer.visibleRect.width, this.virtualizer.visibleRect.height);
let placeholder = new LayoutInfo('placeholder', 'placeholder', rect);
let node = {
layoutInfo: placeholder,
validRect: placeholder.rect
};
nodes.push(node);
this.layoutNodes.set(placeholder.key, node);
y = placeholder.rect.maxY;
}

this.contentSize.height = y;
return nodes;
}

protected buildItem(node: Node<T>, x: number, y: number): LayoutNode {
let res = super.buildItem(node, x, y);
// allow overflow so the focus ring/selection ring can extend outside to overlap with the adjacent items borders
res.layoutInfo.allowOverflow = true;
return res;
}
}
13 changes: 5 additions & 8 deletions packages/@react-spectrum/listbox/src/ListBoxBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {FocusScope} from '@react-aria/focus';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {ListBoxContext} from './ListBoxContext';
import {ListBoxLayout} from './ListBoxLayout';
import {ListBoxOption} from './ListBoxOption';
import {ListBoxSection} from './ListBoxSection';
import {ListLayout} from '@react-stately/layout';
import {ListState} from '@react-stately/list';
import {mergeProps} from '@react-aria/utils';
import {ProgressCircle} from '@react-spectrum/progress';
Expand All @@ -31,7 +31,7 @@ import {useProvider} from '@react-spectrum/provider';
import {Virtualizer, VirtualizerItem} from '@react-aria/virtualizer';

interface ListBoxBaseProps<T> extends AriaListBoxOptions<T>, DOMProps, AriaLabelingProps, StyleProps {
layout: ListLayout<T>,
layout: ListBoxLayout<T>,
state: ListState<T>,
autoFocus?: boolean | FocusStrategy,
shouldFocusWrap?: boolean,
Expand All @@ -48,17 +48,14 @@ interface ListBoxBaseProps<T> extends AriaListBoxOptions<T>, DOMProps, AriaLabel
}

/** @private */
export function useListBoxLayout<T>(): ListLayout<T> {
export function useListBoxLayout<T>(): ListBoxLayout<T> {
let {scale} = useProvider();
let layout = useMemo(() =>
new ListLayout<T>({
new ListBoxLayout<T>({
estimatedRowHeight: scale === 'large' ? 48 : 32,
estimatedHeadingHeight: scale === 'large' ? 33 : 26,
padding: scale === 'large' ? 5 : 4, // TODO: get from DNA
loaderHeight: 40,
placeholderHeight: scale === 'large' ? 48 : 32,
forceSectionHeaders: true,
enableEmptyState: true
placeholderHeight: scale === 'large' ? 48 : 32
})
, [scale]);

Expand Down
87 changes: 87 additions & 0 deletions packages/@react-spectrum/listbox/src/ListBoxLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {InvalidationContext, LayoutInfo, Rect} from '@react-stately/virtualizer';
import {LayoutNode, ListLayout, ListLayoutOptions} from '@react-stately/layout';
import {Node} from '@react-types/shared';

interface ListBoxLayoutProps {
isLoading?: boolean
}

interface ListBoxLayoutOptions extends ListLayoutOptions {
placeholderHeight: number,
padding: number
}

export class ListBoxLayout<T> extends ListLayout<T, ListBoxLayoutProps> {
private isLoading: boolean = false;
private placeholderHeight: number;
private padding: number;

constructor(opts: ListBoxLayoutOptions) {
super(opts);
this.placeholderHeight = opts.placeholderHeight;
this.padding = opts.padding;
}

validate(invalidationContext: InvalidationContext<ListBoxLayoutProps>): void {
this.isLoading = invalidationContext.layoutOptions?.isLoading || false;
super.validate(invalidationContext);
}

protected buildCollection(): LayoutNode[] {
let nodes = super.buildCollection(this.padding);
let y = this.contentSize.height;

if (this.isLoading) {
let rect = new Rect(0, y, this.virtualizer.visibleRect.width, 40);
let loader = new LayoutInfo('loader', 'loader', rect);
let node = {
layoutInfo: loader,
validRect: loader.rect
};
nodes.push(node);
this.layoutNodes.set(loader.key, node);
y = loader.rect.maxY;
}

if (nodes.length === 0) {
let rect = new Rect(0, y, this.virtualizer.visibleRect.width, this.placeholderHeight ?? this.virtualizer.visibleRect.height);
let placeholder = new LayoutInfo('placeholder', 'placeholder', rect);
let node = {
layoutInfo: placeholder,
validRect: placeholder.rect
};
nodes.push(node);
this.layoutNodes.set(placeholder.key, node);
y = placeholder.rect.maxY;
}

this.contentSize.height = y + this.padding;
return nodes;
}

protected buildSection(node: Node<T>, x: number, y: number): LayoutNode {
// Synthesize a collection node for the header.
let headerNode = {
type: 'header',
key: node.key + ':header',
parentKey: node.key,
value: null,
level: node.level,
hasChildNodes: false,
childNodes: [],
rendered: node.rendered,
textValue: node.textValue
};

// Build layout node for it and adjust y offset of section children.
let header = this.buildSectionHeader(headerNode, x, y);
header.node = headerNode;
header.layoutInfo.parentKey = node.key;
this.layoutNodes.set(headerNode.key, header);
y += header.layoutInfo.rect.height;

let section = super.buildSection(node, x, y);
section.children.unshift(header);
return section;
}
}
Loading