Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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: 5 additions & 1 deletion 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, Rect, RefObject, Size} from '@react-types/shared';
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node, Orientation, Rect, RefObject, Size} from '@react-types/shared';
import {DOMLayoutDelegate} from '@react-aria/selection';
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
import {GridCollection, GridNode} from '@react-types/grid';
Expand Down Expand Up @@ -470,6 +470,10 @@ class DeprecatedLayoutDelegate implements LayoutDelegate {
this.layout = layout;
}

getOrientation(): Orientation {
return 'vertical';
}

getContentSize(): Size {
return this.layout.getContentSize();
}
Expand Down
10 changes: 8 additions & 2 deletions packages/@react-aria/selection/src/DOMLayoutDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
*/

import {getItemElement} from './utils';
import {Key, LayoutDelegate, Rect, RefObject, Size} from '@react-types/shared';
import {Key, LayoutDelegate, Orientation, Rect, RefObject, Size} from '@react-types/shared';

export class DOMLayoutDelegate implements LayoutDelegate {
private ref: RefObject<HTMLElement | null>;
private orientation: Orientation;

constructor(ref: RefObject<HTMLElement | null>) {
constructor(ref: RefObject<HTMLElement | null>, orientation?: Orientation) {
this.ref = ref;
this.orientation = orientation ?? 'vertical';
}

getOrientation(): Orientation {
return this.orientation;
}

getItemRect(key: Key): Rect | null {
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/selection/src/ListKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
this.orientation = opts.orientation || 'vertical';
this.direction = opts.direction;
this.layout = opts.layout || 'stack';
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref);
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref, this.orientation);
} else {
this.collection = args[0];
this.disabledKeys = args[1];
Expand All @@ -59,7 +59,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
this.layout = 'stack';
this.orientation = 'vertical';
this.disabledBehavior = 'all';
this.layoutDelegate = new DOMLayoutDelegate(this.ref);
this.layoutDelegate = new DOMLayoutDelegate(this.ref, this.orientation);
}

// If this is a vertical stack, remove the left/right methods completely
Expand Down
179 changes: 90 additions & 89 deletions packages/@react-stately/layout/src/ListLayout.ts

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion packages/@react-stately/virtualizer/src/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
*/

import {InvalidationContext} from './types';
import {ItemDropTarget, Key, LayoutDelegate, Node} from '@react-types/shared';
import {ItemDropTarget, Key, LayoutDelegate, Node, Orientation} from '@react-types/shared';
import {LayoutInfo} from './LayoutInfo';
import {Rect} from './Rect';
import {Size} from './Size';
import {Virtualizer} from './Virtualizer';

export interface LayoutOptions {
orientation?: Orientation
}

/**
* Virtualizer supports arbitrary layout objects, which compute what items are visible, and how
* to position and style them. However, layouts do not render items directly. Instead,
Expand All @@ -28,6 +32,8 @@ import {Virtualizer} from './Virtualizer';
* `getLayoutInfo`, and `getContentSize` methods. All other methods can be optionally overridden to implement custom behavior.
*/
export abstract class Layout<T extends object = Node<any>, O = any> implements LayoutDelegate {
protected orientation: Orientation;

/** The Virtualizer the layout is currently attached to. */
virtualizer: Virtualizer<T, any> | null = null;

Expand All @@ -50,6 +56,17 @@ export abstract class Layout<T extends object = Node<any>, O = any> implements L
*/
abstract getContentSize(): Size;

constructor(options: LayoutOptions = {}) {
this.orientation = options.orientation ?? 'vertical';
}

/**
* Returns the orientation of the layout.
*/
getOrientation(): Orientation {
return this.orientation;
}

/**
* Returns whether the layout should invalidate in response to
* visible rectangle changes. By default, it only invalidates
Expand Down
15 changes: 9 additions & 6 deletions packages/@react-stately/virtualizer/src/OverscanManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/

import {Orientation} from '@react-types/shared';
import {Point} from './Point';
import {Rect} from './Rect';

Expand All @@ -34,16 +35,18 @@ export class OverscanManager {
this.visibleRect = rect;
}

getOverscannedRect(): Rect {
getOverscannedRect(orientation: Orientation): Rect {
let overscanned = this.visibleRect.copy();

let overscanY = this.visibleRect.height / 3;
overscanned.height += overscanY;
if (this.velocity.y < 0) {
overscanned.y -= overscanY;
if (orientation === 'vertical' || this.velocity.y !== 0) {
let overscanY = this.visibleRect.height / 3;
overscanned.height += overscanY;
if (this.velocity.y < 0) {
overscanned.y -= overscanY;
}
}

if (this.velocity.x !== 0) {
if (orientation === 'horizontal' || this.velocity.x !== 0) {
let overscanX = this.visibleRect.width / 3;
overscanned.width += overscanX;
if (this.velocity.x < 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/virtualizer/src/Virtualizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export class Virtualizer<T extends object, V> {
if (isTestEnv && !(isClientWidthMocked && isClientHeightMocked)) {
rect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
} else {
rect = this._overscanManager.getOverscannedRect();
rect = this._overscanManager.getOverscannedRect(this.layout.getOrientation());
}
let layoutInfos = this.layout.getVisibleLayoutInfos(rect);
let map = new Map;
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-types/shared/src/collections.d.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 {Key} from '@react-types/shared';
import {Key, Orientation} from '@react-types/shared';
import {LinkDOMProps} from './dom';
import {ReactElement, ReactNode} from 'react';

Expand Down Expand Up @@ -137,6 +137,8 @@ export interface Size {

/** A LayoutDelegate provides layout information for collection items. */
export interface LayoutDelegate {
/** Returns the orientation of the layout. */
getOrientation(): Orientation,
/** Returns a rectangle for the item with the given key. */
getItemRect(key: Key): Rect | null,
/** Returns the visible rectangle of the collection. */
Expand Down
13 changes: 10 additions & 3 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPers
import {DragAndDropHooks} from './useDragAndDrop';
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared';
import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, Orientation, PressEvents, RefObject} from '@react-types/shared';
import {ListStateContext} from './ListBox';
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
import {TextContext} from './Text';
Expand Down Expand Up @@ -75,7 +75,13 @@ export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>
* Whether the items are arranged in a stack or grid.
* @default 'stack'
*/
layout?: 'stack' | 'grid'
layout?: 'stack' | 'grid',
/**
* The primary orientation of the items. Usually this is the
* direction that the collection scrolls.
* @default 'vertical'
*/
orientation?: Orientation
}


Expand Down Expand Up @@ -103,7 +109,7 @@ interface GridListInnerProps<T extends object> {
}

function GridListInner<T extends object>({props, collection, gridListRef: ref}: GridListInnerProps<T>) {
let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props;
let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack', orientation = 'vertical'} = props;
let {CollectionRoot, isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate} = useContext(CollectionRendererContext);
let state = useListState({
...props,
Expand Down Expand Up @@ -183,6 +189,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:

let keyboardDelegate = new ListKeyboardDelegate({
collection,
orientation,
disabledKeys: selectionManager.disabledKeys,
disabledBehavior: selectionManager.disabledBehavior,
ref
Expand Down
34 changes: 29 additions & 5 deletions packages/react-aria-components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {action} from '@storybook/addon-actions';
import {Collection, DropIndicator, GridLayout, Header, ListBox, ListBoxItem, ListBoxProps, ListBoxSection, ListLayout, Separator, Text, useDragAndDrop, Virtualizer, WaterfallLayout} from 'react-aria-components';
import {ListBoxLoadMoreItem} from '../src/ListBox';
import {LoadingSpinner, MyListBoxItem} from './utils';
import {LoadingSpinner, MyHeader, MyListBoxItem} from './utils';
import React from 'react';
import {Size} from '@react-stately/virtualizer';
import styles from '../example/index.css';
Expand Down Expand Up @@ -394,6 +394,8 @@ function generateRandomString(minLength: number, maxLength: number): string {
}

export function VirtualizedListBox(args) {
let heightProperty = args.orientation === 'horizontal' ? 'width' : 'height';
let widthProperty = args.orientation === 'horizontal' ? 'height' : 'width';
let sections: {id: string, name: string, children: {id: string, name: string}[]}[] = [];
for (let s = 0; s < 10; s++) {
let items: {id: string, name: string}[] = [];
Expand All @@ -407,15 +409,16 @@ export function VirtualizedListBox(args) {
return (
<Virtualizer
layout={new ListLayout({
orientation: args.orientation,
estimatedRowHeight: 25,
estimatedHeadingHeight: 26,
loaderHeight: 30
})}>
<ListBox className={styles.menu} style={{height: 400}} aria-label="virtualized listbox">
<ListBox orientation={args.orientation} className={styles.menu} style={{[heightProperty]: 400, [widthProperty]: 200}} aria-label="virtualized listbox">
<Collection items={sections}>
{section => (
<ListBoxSection className={styles.group}>
<Header style={{fontSize: '1.2em'}}>{section.name}</Header>
<MyHeader style={{fontSize: '1.2em'}}>{section.name}</MyHeader>
<Collection items={section.children}>
{item => <MyListBoxItem>{item.name}</MyListBoxItem>}
</Collection>
Expand All @@ -430,8 +433,15 @@ export function VirtualizedListBox(args) {

VirtualizedListBox.story = {
args: {
orientation: 'vertical',
variableHeight: false,
isLoading: false
},
argTypes: {
orientation: {
control: 'radio',
options: ['vertical', 'horizontal']
}
}
};

Expand All @@ -450,7 +460,7 @@ export function VirtualizedListBoxEmpty() {
);
}

export function VirtualizedListBoxDnd() {
export function VirtualizedListBoxDnd(args) {
let items: {id: number, name: string}[] = [];
for (let i = 0; i < 10000; i++) {
items.push({id: i, name: `Item ${i}`});
Expand Down Expand Up @@ -481,13 +491,15 @@ export function VirtualizedListBoxDnd() {
<Virtualizer
layout={ListLayout}
layoutOptions={{
rowHeight: 25,
orientation: args.orientation,
rowHeight: args.orientation === 'horizontal' ? 45 : 25,
gap: 8
}}>
<ListBox
className={styles.menu}
selectionMode="multiple"
selectionBehavior="replace"
orientation={args.orientation}
style={{width: '100%', height: '100%'}}
aria-label="virtualized listbox"
items={list.items}
Expand All @@ -499,6 +511,18 @@ export function VirtualizedListBoxDnd() {
);
}

VirtualizedListBoxDnd.story = {
args: {
orientation: 'vertical'
},
argTypes: {
orientation: {
control: 'radio',
options: ['vertical', 'horizontal']
}
}
};

function VirtualizedListBoxGridExample({minSize = 80, maxSize = 100, preserveAspectRatio = false}) {
let items: {id: number, name: string}[] = [];
for (let i = 0; i < 10000; i++) {
Expand Down
10 changes: 7 additions & 3 deletions packages/react-aria-components/stories/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import {classNames} from '@react-spectrum/utils';
import {ListBoxItem, ListBoxItemProps, MenuItem, MenuItemProps, ProgressBar} from 'react-aria-components';
import React from 'react';
import {Header, ListBoxItem, ListBoxItemProps, MenuItem, MenuItemProps, ProgressBar} from 'react-aria-components';
import React, {HTMLAttributes} from 'react';
import styles from '../example/index.css';

export const MyHeader = (props: HTMLAttributes<HTMLElement>) => {
return <Header {...props} style={{width: 'max-content', ...props.style}} />;
};

export const MyListBoxItem = (props: ListBoxItemProps) => {
return (
<ListBoxItem
{...props}
style={{wordBreak: 'break-word', ...props.style}}
style={{wordBreak: 'break-word', width: 'max-content', ...props.style}}
className={({isFocused, isSelected, isHovered, isFocusVisible}) => classNames(styles, 'item', {
focused: isFocused,
selected: isSelected,
Expand Down