Skip to content

Commit b07954a

Browse files
committed
feat: add orientation to layout
1 parent 94f0575 commit b07954a

File tree

11 files changed

+180
-113
lines changed

11 files changed

+180
-113
lines changed

packages/@react-aria/grid/src/GridKeyboardDelegate.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node, Rect, RefObject, Size} from '@react-types/shared';
13+
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node, Orientation, Rect, RefObject, Size} from '@react-types/shared';
1414
import {DOMLayoutDelegate} from '@react-aria/selection';
1515
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
1616
import {GridCollection, GridNode} from '@react-types/grid';
@@ -470,6 +470,10 @@ class DeprecatedLayoutDelegate implements LayoutDelegate {
470470
this.layout = layout;
471471
}
472472

473+
getOrientation(): Orientation {
474+
return 'vertical';
475+
}
476+
473477
getContentSize(): Size {
474478
return this.layout.getContentSize();
475479
}

packages/@react-aria/selection/src/DOMLayoutDelegate.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@
1111
*/
1212

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

1616
export class DOMLayoutDelegate implements LayoutDelegate {
1717
private ref: RefObject<HTMLElement | null>;
18+
private orientation: Orientation;
1819

19-
constructor(ref: RefObject<HTMLElement | null>) {
20+
constructor(ref: RefObject<HTMLElement | null>, orientation?: Orientation) {
2021
this.ref = ref;
22+
this.orientation = orientation ?? 'vertical';
23+
}
24+
25+
getOrientation(): Orientation {
26+
return this.orientation;
2127
}
2228

2329
getItemRect(key: Key): Rect | null {

packages/@react-aria/selection/src/ListKeyboardDelegate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
5050
this.orientation = opts.orientation || 'vertical';
5151
this.direction = opts.direction;
5252
this.layout = opts.layout || 'stack';
53-
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref);
53+
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref, this.orientation);
5454
} else {
5555
this.collection = args[0];
5656
this.disabledKeys = args[1];
@@ -59,7 +59,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
5959
this.layout = 'stack';
6060
this.orientation = 'vertical';
6161
this.disabledBehavior = 'all';
62-
this.layoutDelegate = new DOMLayoutDelegate(this.ref);
62+
this.layoutDelegate = new DOMLayoutDelegate(this.ref, this.orientation);
6363
}
6464

6565
// If this is a vertical stack, remove the left/right methods completely

packages/@react-stately/layout/src/ListLayout.ts

Lines changed: 90 additions & 89 deletions
Large diffs are not rendered by default.

packages/@react-stately/virtualizer/src/Layout.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
*/
1212

1313
import {InvalidationContext} from './types';
14-
import {ItemDropTarget, Key, LayoutDelegate, Node} from '@react-types/shared';
14+
import {ItemDropTarget, Key, LayoutDelegate, Node, Orientation} from '@react-types/shared';
1515
import {LayoutInfo} from './LayoutInfo';
1616
import {Rect} from './Rect';
1717
import {Size} from './Size';
1818
import {Virtualizer} from './Virtualizer';
1919

20+
export interface LayoutOptions {
21+
orientation?: Orientation
22+
}
23+
2024
/**
2125
* Virtualizer supports arbitrary layout objects, which compute what items are visible, and how
2226
* to position and style them. However, layouts do not render items directly. Instead,
@@ -28,6 +32,8 @@ import {Virtualizer} from './Virtualizer';
2832
* `getLayoutInfo`, and `getContentSize` methods. All other methods can be optionally overridden to implement custom behavior.
2933
*/
3034
export abstract class Layout<T extends object = Node<any>, O = any> implements LayoutDelegate {
35+
protected orientation: Orientation;
36+
3137
/** The Virtualizer the layout is currently attached to. */
3238
virtualizer: Virtualizer<T, any> | null = null;
3339

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

59+
constructor(options: LayoutOptions = {}) {
60+
this.orientation = options.orientation ?? 'vertical';
61+
}
62+
63+
/**
64+
* Returns the orientation of the layout.
65+
*/
66+
getOrientation(): Orientation {
67+
return this.orientation;
68+
}
69+
5370
/**
5471
* Returns whether the layout should invalidate in response to
5572
* visible rectangle changes. By default, it only invalidates

packages/@react-stately/virtualizer/src/OverscanManager.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {Orientation} from '@react-types/shared';
1314
import {Point} from './Point';
1415
import {Rect} from './Rect';
1516

@@ -34,16 +35,18 @@ export class OverscanManager {
3435
this.visibleRect = rect;
3536
}
3637

37-
getOverscannedRect(): Rect {
38+
getOverscannedRect(orientation: Orientation): Rect {
3839
let overscanned = this.visibleRect.copy();
3940

40-
let overscanY = this.visibleRect.height / 3;
41-
overscanned.height += overscanY;
42-
if (this.velocity.y < 0) {
43-
overscanned.y -= overscanY;
41+
if (orientation === 'vertical' || this.velocity.y !== 0) {
42+
let overscanY = this.visibleRect.height / 3;
43+
overscanned.height += overscanY;
44+
if (this.velocity.y < 0) {
45+
overscanned.y -= overscanY;
46+
}
4447
}
4548

46-
if (this.velocity.x !== 0) {
49+
if (orientation === 'horizontal' || this.velocity.x !== 0) {
4750
let overscanX = this.visibleRect.width / 3;
4851
overscanned.width += overscanX;
4952
if (this.velocity.x < 0) {

packages/@react-stately/virtualizer/src/Virtualizer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class Virtualizer<T extends object, V> {
191191
if (isTestEnv && !(isClientWidthMocked && isClientHeightMocked)) {
192192
rect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
193193
} else {
194-
rect = this._overscanManager.getOverscannedRect();
194+
rect = this._overscanManager.getOverscannedRect(this.layout.getOrientation());
195195
}
196196
let layoutInfos = this.layout.getVisibleLayoutInfos(rect);
197197
let map = new Map;

packages/@react-types/shared/src/collections.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Key} from '@react-types/shared';
13+
import {Key, Orientation} from '@react-types/shared';
1414
import {LinkDOMProps} from './dom';
1515
import {ReactElement, ReactNode} from 'react';
1616

@@ -137,6 +137,8 @@ export interface Size {
137137

138138
/** A LayoutDelegate provides layout information for collection items. */
139139
export interface LayoutDelegate {
140+
/** Returns the orientation of the layout. */
141+
getOrientation(): Orientation,
140142
/** Returns a rectangle for the item with the given key. */
141143
getItemRect(key: Key): Rect | null,
142144
/** Returns the visible rectangle of the collection. */

packages/react-aria-components/src/GridList.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPers
1919
import {DragAndDropHooks} from './useDragAndDrop';
2020
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
2121
import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
22-
import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared';
22+
import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, Orientation, PressEvents, RefObject} from '@react-types/shared';
2323
import {ListStateContext} from './ListBox';
2424
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
2525
import {TextContext} from './Text';
@@ -75,7 +75,13 @@ export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>
7575
* Whether the items are arranged in a stack or grid.
7676
* @default 'stack'
7777
*/
78-
layout?: 'stack' | 'grid'
78+
layout?: 'stack' | 'grid',
79+
/**
80+
* The primary orientation of the items. Usually this is the
81+
* direction that the collection scrolls.
82+
* @default 'vertical'
83+
*/
84+
orientation?: Orientation
7985
}
8086

8187

packages/react-aria-components/stories/ListBox.stories.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {action} from '@storybook/addon-actions';
1414
import {Collection, DropIndicator, GridLayout, Header, ListBox, ListBoxItem, ListBoxProps, ListBoxSection, ListLayout, Separator, Text, useDragAndDrop, Virtualizer, WaterfallLayout} from 'react-aria-components';
1515
import {ListBoxLoadMoreItem} from '../src/ListBox';
16-
import {LoadingSpinner, MyListBoxItem} from './utils';
16+
import {LoadingSpinner, MyHeader, MyListBoxItem} from './utils';
1717
import React from 'react';
1818
import {Size} from '@react-stately/virtualizer';
1919
import styles from '../example/index.css';
@@ -394,6 +394,8 @@ function generateRandomString(minLength: number, maxLength: number): string {
394394
}
395395

396396
export function VirtualizedListBox(args) {
397+
let heightProperty = args.orientation === 'horizontal' ? 'width' : 'height';
398+
let widthProperty = args.orientation === 'horizontal' ? 'height' : 'width';
397399
let sections: {id: string, name: string, children: {id: string, name: string}[]}[] = [];
398400
for (let s = 0; s < 10; s++) {
399401
let items: {id: string, name: string}[] = [];
@@ -407,15 +409,16 @@ export function VirtualizedListBox(args) {
407409
return (
408410
<Virtualizer
409411
layout={new ListLayout({
412+
orientation: args.orientation,
410413
estimatedRowHeight: 25,
411414
estimatedHeadingHeight: 26,
412415
loaderHeight: 30
413416
})}>
414-
<ListBox className={styles.menu} style={{height: 400}} aria-label="virtualized listbox">
417+
<ListBox orientation={args.orientation} className={styles.menu} style={{[heightProperty]: 400, [widthProperty]: 200}} aria-label="virtualized listbox">
415418
<Collection items={sections}>
416419
{section => (
417420
<ListBoxSection className={styles.group}>
418-
<Header style={{fontSize: '1.2em'}}>{section.name}</Header>
421+
<MyHeader style={{fontSize: '1.2em'}}>{section.name}</MyHeader>
419422
<Collection items={section.children}>
420423
{item => <MyListBoxItem>{item.name}</MyListBoxItem>}
421424
</Collection>
@@ -430,8 +433,15 @@ export function VirtualizedListBox(args) {
430433

431434
VirtualizedListBox.story = {
432435
args: {
436+
orientation: 'vertical',
433437
variableHeight: false,
434438
isLoading: false
439+
},
440+
argTypes: {
441+
orientation: {
442+
control: 'radio',
443+
options: ['vertical', 'horizontal']
444+
}
435445
}
436446
};
437447

@@ -450,7 +460,7 @@ export function VirtualizedListBoxEmpty() {
450460
);
451461
}
452462

453-
export function VirtualizedListBoxDnd() {
463+
export function VirtualizedListBoxDnd(args) {
454464
let items: {id: number, name: string}[] = [];
455465
for (let i = 0; i < 10000; i++) {
456466
items.push({id: i, name: `Item ${i}`});
@@ -481,13 +491,15 @@ export function VirtualizedListBoxDnd() {
481491
<Virtualizer
482492
layout={ListLayout}
483493
layoutOptions={{
484-
rowHeight: 25,
494+
orientation: args.orientation,
495+
rowHeight: args.orientation === 'horizontal' ? 45 : 25,
485496
gap: 8
486497
}}>
487498
<ListBox
488499
className={styles.menu}
489500
selectionMode="multiple"
490501
selectionBehavior="replace"
502+
orientation={args.orientation}
491503
style={{width: '100%', height: '100%'}}
492504
aria-label="virtualized listbox"
493505
items={list.items}
@@ -499,6 +511,18 @@ export function VirtualizedListBoxDnd() {
499511
);
500512
}
501513

514+
VirtualizedListBoxDnd.story = {
515+
args: {
516+
orientation: 'vertical'
517+
},
518+
argTypes: {
519+
orientation: {
520+
control: 'radio',
521+
options: ['vertical', 'horizontal']
522+
}
523+
}
524+
};
525+
502526
function VirtualizedListBoxGridExample({minSize = 80, maxSize = 100, preserveAspectRatio = false}) {
503527
let items: {id: number, name: string}[] = [];
504528
for (let i = 0; i < 10000; i++) {

0 commit comments

Comments
 (0)