Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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