Skip to content

Commit 8c13146

Browse files
authored
Add Virtualizer to React Aria Components (#6518)
1 parent 44b1dd0 commit 8c13146

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1350
-806
lines changed

packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {AriaButtonProps} from '@react-types/button';
1414
import {AriaListBoxOptions} from '@react-aria/listbox';
1515
import {AriaSearchAutocompleteProps} from '@react-types/autocomplete';
1616
import {ComboBoxState} from '@react-stately/combobox';
17-
import {DOMAttributes, KeyboardDelegate, ValidationResult} from '@react-types/shared';
17+
import {DOMAttributes, KeyboardDelegate, LayoutDelegate, ValidationResult} from '@react-types/shared';
1818
import {InputHTMLAttributes, RefObject} from 'react';
1919
import {mergeProps} from '@react-aria/utils';
2020
import {useComboBox} from '@react-aria/combobox';
@@ -43,7 +43,13 @@ export interface AriaSearchAutocompleteOptions<T> extends AriaSearchAutocomplete
4343
/** The ref for the list box. */
4444
listBoxRef: RefObject<HTMLElement | null>,
4545
/** An optional keyboard delegate implementation, to override the default. */
46-
keyboardDelegate?: KeyboardDelegate
46+
keyboardDelegate?: KeyboardDelegate,
47+
/**
48+
* A delegate object that provides layout information for items in the collection.
49+
* By default this uses the DOM, but this can be overridden to implement things like
50+
* virtualized scrolling.
51+
*/
52+
layoutDelegate?: LayoutDelegate
4753
}
4854

4955
/**
@@ -58,6 +64,7 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
5864
inputRef,
5965
listBoxRef,
6066
keyboardDelegate,
67+
layoutDelegate,
6168
onSubmit = () => {},
6269
onClear,
6370
onKeyDown,
@@ -98,6 +105,7 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
98105
{
99106
...otherProps,
100107
keyboardDelegate,
108+
layoutDelegate,
101109
popoverRef,
102110
listBoxRef,
103111
inputRef,

packages/@react-aria/combobox/src/useComboBox.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {AriaButtonProps} from '@react-types/button';
1515
import {AriaComboBoxProps} from '@react-types/combobox';
1616
import {ariaHideOutside} from '@react-aria/overlays';
1717
import {AriaListBoxOptions, getItemId, listData} from '@react-aria/listbox';
18-
import {BaseEvent, DOMAttributes, KeyboardDelegate, PressEvent, RouterOptions, ValidationResult} from '@react-types/shared';
18+
import {BaseEvent, DOMAttributes, KeyboardDelegate, LayoutDelegate, PressEvent, RouterOptions, ValidationResult} from '@react-types/shared';
1919
import {chain, isAppleDevice, mergeProps, useLabels, useRouter} from '@react-aria/utils';
2020
import {ComboBoxState} from '@react-stately/combobox';
2121
import {FocusEvent, InputHTMLAttributes, KeyboardEvent, RefObject, TouchEvent, useEffect, useMemo, useRef} from 'react';
@@ -38,7 +38,13 @@ export interface AriaComboBoxOptions<T> extends Omit<AriaComboBoxProps<T>, 'chil
3838
/** The ref for the optional list box popup trigger button. */
3939
buttonRef?: RefObject<Element | null>,
4040
/** An optional keyboard delegate implementation, to override the default. */
41-
keyboardDelegate?: KeyboardDelegate
41+
keyboardDelegate?: KeyboardDelegate,
42+
/**
43+
* A delegate object that provides layout information for items in the collection.
44+
* By default this uses the DOM, but this can be overridden to implement things like
45+
* virtualized scrolling.
46+
*/
47+
layoutDelegate?: LayoutDelegate
4248
}
4349

4450
export interface ComboBoxAria<T> extends ValidationResult {
@@ -69,6 +75,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
6975
inputRef,
7076
listBoxRef,
7177
keyboardDelegate,
78+
layoutDelegate,
7279
// completionMode = 'suggest',
7380
shouldFocusWrap,
7481
isReadOnly,
@@ -90,10 +97,16 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
9097

9198
// By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
9299
// When virtualized, the layout object will be passed in as a prop and override this.
93-
let delegate = useMemo(() =>
94-
keyboardDelegate ||
95-
new ListKeyboardDelegate(state.collection, state.disabledKeys, listBoxRef)
96-
, [keyboardDelegate, state.collection, state.disabledKeys, listBoxRef]);
100+
let {collection} = state;
101+
let {disabledKeys} = state.selectionManager;
102+
let delegate = useMemo(() => (
103+
keyboardDelegate || new ListKeyboardDelegate({
104+
collection,
105+
disabledKeys,
106+
ref: listBoxRef,
107+
layoutDelegate
108+
})
109+
), [keyboardDelegate, layoutDelegate, collection, disabledKeys, listBoxRef]);
97110

98111
// Use useSelectableCollection to get the keyboard handlers to apply to the textfield
99112
let {collectionProps} = useSelectableCollection({

packages/@react-aria/dnd/src/ListDropTargetDelegate.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Collection, Direction, DropTarget, DropTargetDelegate, Node, Orientation} from '@react-types/shared';
1+
import {Direction, DropTarget, DropTargetDelegate, Node, Orientation} from '@react-types/shared';
22
import {RefObject} from 'react';
33

44
interface ListDropTargetDelegateOptions {
@@ -30,13 +30,13 @@ interface ListDropTargetDelegateOptions {
3030
// direction. For grids, it is the secondary direction.
3131

3232
export class ListDropTargetDelegate implements DropTargetDelegate {
33-
private collection: Collection<Node<unknown>>;
33+
private collection: Iterable<Node<unknown>>;
3434
private ref: RefObject<HTMLElement | null>;
3535
private layout: 'stack' | 'grid';
3636
private orientation: Orientation;
3737
private direction: Direction;
3838

39-
constructor(collection: Collection<Node<unknown>>, ref: RefObject<HTMLElement | null>, options?: ListDropTargetDelegateOptions) {
39+
constructor(collection: Iterable<Node<unknown>>, ref: RefObject<HTMLElement | null>, options?: ListDropTargetDelegateOptions) {
4040
this.collection = collection;
4141
this.ref = ref;
4242
this.layout = options?.layout || 'stack';
@@ -73,7 +73,7 @@ export class ListDropTargetDelegate implements DropTargetDelegate {
7373
}
7474

7575
getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget {
76-
if (this.collection.size === 0) {
76+
if (this.collection[Symbol.iterator]().next().done) {
7777
return {type: 'root'};
7878
}
7979

packages/@react-aria/dnd/stories/VirtualizedListBox.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import {DroppableCollectionDropEvent} from '@react-types/shared';
1919
import {FocusRing} from '@react-aria/focus';
2020
import Folder from '@spectrum-icons/workflow/Folder';
2121
import {Item} from '@react-stately/collections';
22+
import {ListKeyboardDelegate} from '@react-aria/selection';
2223
import {ListLayout} from '@react-stately/layout';
2324
import React from 'react';
24-
import {useCollator} from '@react-aria/i18n';
2525
import {useDropIndicator, useDroppableCollection, useDroppableItem} from '..';
2626
import {useDroppableCollectionState} from '@react-stately/dnd';
2727
import {useListBox, useOption} from '@react-aria/listbox';
@@ -135,21 +135,21 @@ export const VirtualizedListBox = React.forwardRef(function (props: any, ref) {
135135
onDrop
136136
});
137137

138-
let collator = useCollator({usage: 'search', sensitivity: 'base'});
139138
let layout = React.useMemo(() =>
140139
new ListLayout<unknown>({
141140
estimatedRowHeight: 32,
142141
padding: 8,
143142
loaderHeight: 40,
144-
placeholderHeight: 32,
145-
collator
143+
placeholderHeight: 32
146144
})
147-
, [collator]);
148-
149-
layout.collection = state.collection;
145+
, []);
150146

151147
let {collectionProps} = useDroppableCollection({
152-
keyboardDelegate: layout,
148+
keyboardDelegate: new ListKeyboardDelegate({
149+
collection: state.collection,
150+
ref: domRef,
151+
layoutDelegate: layout
152+
}),
153153
dropTargetDelegate: layout,
154154
onDropActivate: chain(action('onDropActivate'), console.log),
155155
onDrop
@@ -158,7 +158,7 @@ export const VirtualizedListBox = React.forwardRef(function (props: any, ref) {
158158
let {listBoxProps} = useListBox({
159159
...props,
160160
'aria-label': 'List',
161-
keyboardDelegate: layout,
161+
layoutDelegate: layout,
162162
isVirtualized: true
163163
}, state, domRef);
164164
let isDropTarget = dropState.isDropTarget({type: 'root'});

packages/@react-aria/grid/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"@react-stately/collections": "^3.10.7",
3232
"@react-stately/grid": "^3.8.7",
3333
"@react-stately/selection": "^3.15.1",
34-
"@react-stately/virtualizer": "^3.7.1",
3534
"@react-types/checkbox": "^3.8.1",
3635
"@react-types/grid": "^3.2.6",
3736
"@react-types/shared": "^3.23.1",

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

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

13-
import {Direction, DisabledBehavior, Key, KeyboardDelegate, Node} from '@react-types/shared';
13+
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node} from '@react-types/shared';
14+
import {DOMLayoutDelegate} from '@react-aria/selection';
1415
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
1516
import {GridCollection} from '@react-types/grid';
16-
import {Layout, Rect} from '@react-stately/virtualizer';
1717
import {RefObject} from 'react';
1818

19-
export interface GridKeyboardDelegateOptions<T, C> {
19+
export interface GridKeyboardDelegateOptions<C> {
2020
collection: C,
2121
disabledKeys: Set<Key>,
2222
disabledBehavior?: DisabledBehavior,
2323
ref?: RefObject<HTMLElement | null>,
2424
direction: Direction,
2525
collator?: Intl.Collator,
26-
layout?: Layout<Node<T>>,
26+
layoutDelegate?: LayoutDelegate,
2727
focusMode?: 'row' | 'cell'
2828
}
2929

3030
export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements KeyboardDelegate {
3131
collection: C;
3232
protected disabledKeys: Set<Key>;
3333
protected disabledBehavior: DisabledBehavior;
34-
protected ref: RefObject<HTMLElement | null>;
3534
protected direction: Direction;
3635
protected collator: Intl.Collator;
37-
protected layout: Layout<Node<T>>;
36+
protected layoutDelegate: LayoutDelegate;
3837
protected focusMode;
3938

40-
constructor(options: GridKeyboardDelegateOptions<T, C>) {
39+
constructor(options: GridKeyboardDelegateOptions<C>) {
4140
this.collection = options.collection;
4241
this.disabledKeys = options.disabledKeys;
4342
this.disabledBehavior = options.disabledBehavior || 'all';
44-
this.ref = options.ref;
4543
this.direction = options.direction;
4644
this.collator = options.collator;
47-
this.layout = options.layout;
45+
this.layoutDelegate = options.layoutDelegate || new DOMLayoutDelegate(options.ref);
4846
this.focusMode = options.focusMode || 'row';
4947
}
5048

@@ -276,66 +274,35 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
276274
return key;
277275
}
278276

279-
private getItem(key: Key): HTMLElement {
280-
return this.ref.current.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
281-
}
282-
283-
private getItemRect(key: Key): Rect {
284-
if (this.layout) {
285-
return this.layout.getLayoutInfo(key)?.rect;
286-
}
287-
288-
let item = this.getItem(key);
289-
if (item) {
290-
return new Rect(item.offsetLeft, item.offsetTop, item.offsetWidth, item.offsetHeight);
291-
}
292-
}
293-
294-
private getPageHeight(): number {
295-
if (this.layout) {
296-
return this.layout.virtualizer?.visibleRect.height;
297-
}
298-
299-
return this.ref?.current?.offsetHeight;
300-
}
301-
302-
private getContentHeight(): number {
303-
if (this.layout) {
304-
return this.layout.getContentSize().height;
305-
}
306-
307-
return this.ref?.current?.scrollHeight;
308-
}
309-
310277
getKeyPageAbove(key: Key) {
311-
let itemRect = this.getItemRect(key);
278+
let itemRect = this.layoutDelegate.getItemRect(key);
312279
if (!itemRect) {
313280
return null;
314281
}
315282

316-
let pageY = Math.max(0, itemRect.maxY - this.getPageHeight());
283+
let pageY = Math.max(0, itemRect.y + itemRect.height - this.layoutDelegate.getVisibleRect().height);
317284

318285
while (itemRect && itemRect.y > pageY) {
319286
key = this.getKeyAbove(key);
320-
itemRect = this.getItemRect(key);
287+
itemRect = this.layoutDelegate.getItemRect(key);
321288
}
322289

323290
return key;
324291
}
325292

326293
getKeyPageBelow(key: Key) {
327-
let itemRect = this.getItemRect(key);
294+
let itemRect = this.layoutDelegate.getItemRect(key);
328295

329296
if (!itemRect) {
330297
return null;
331298
}
332299

333-
let pageHeight = this.getPageHeight();
334-
let pageY = Math.min(this.getContentHeight(), itemRect.y + pageHeight);
300+
let pageHeight = this.layoutDelegate.getVisibleRect().height;
301+
let pageY = Math.min(this.layoutDelegate.getContentSize().height, itemRect.y + pageHeight);
335302

336-
while (itemRect && itemRect.maxY < pageY) {
303+
while (itemRect && (itemRect.y + itemRect.height) < pageY) {
337304
let nextKey = this.getKeyBelow(key);
338-
itemRect = this.getItemRect(nextKey);
305+
itemRect = this.layoutDelegate.getItemRect(nextKey);
339306

340307
// Guard against case where maxY of the last key is barely less than pageY due to rounding
341308
// and thus it attempts to set key to null

packages/@react-aria/gridlist/src/useGridList.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
DOMProps,
1919
Key,
2020
KeyboardDelegate,
21+
LayoutDelegate,
2122
MultipleSelection
2223
} from '@react-types/shared';
2324
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils';
@@ -55,6 +56,12 @@ export interface AriaGridListOptions<T> extends Omit<AriaGridListProps<T>, 'chil
5556
* to override the default.
5657
*/
5758
keyboardDelegate?: KeyboardDelegate,
59+
/**
60+
* A delegate object that provides layout information for items in the collection.
61+
* By default this uses the DOM, but this can be overridden to implement things like
62+
* virtualized scrolling.
63+
*/
64+
layoutDelegate?: LayoutDelegate,
5865
/**
5966
* Whether focus should wrap around when the end/start is reached.
6067
* @default false
@@ -86,6 +93,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
8693
let {
8794
isVirtualized,
8895
keyboardDelegate,
96+
layoutDelegate,
8997
onAction,
9098
linkBehavior = 'action',
9199
keyboardNavigationBehavior = 'arrow'
@@ -100,7 +108,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
100108
collection: state.collection,
101109
disabledKeys: state.disabledKeys,
102110
ref,
103-
keyboardDelegate: keyboardDelegate,
111+
keyboardDelegate,
112+
layoutDelegate,
104113
isVirtualized,
105114
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
106115
shouldFocusWrap: props.shouldFocusWrap,

packages/@react-aria/listbox/src/useListBox.ts

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

1313
import {AriaListBoxProps} from '@react-types/listbox';
14-
import {DOMAttributes, KeyboardDelegate} from '@react-types/shared';
14+
import {DOMAttributes, KeyboardDelegate, LayoutDelegate} from '@react-types/shared';
1515
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils';
1616
import {listData} from './utils';
1717
import {ListState} from '@react-stately/list';
@@ -37,6 +37,13 @@ export interface AriaListBoxOptions<T> extends Omit<AriaListBoxProps<T>, 'childr
3737
*/
3838
keyboardDelegate?: KeyboardDelegate,
3939

40+
/**
41+
* A delegate object that provides layout information for items in the collection.
42+
* By default this uses the DOM, but this can be overridden to implement things like
43+
* virtualized scrolling.
44+
*/
45+
layoutDelegate?: LayoutDelegate,
46+
4047
/**
4148
* Whether the listbox items should use virtual focus instead of being focused directly.
4249
*/

0 commit comments

Comments
 (0)