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
12 changes: 10 additions & 2 deletions packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {AriaButtonProps} from '@react-types/button';
import {AriaListBoxOptions} from '@react-aria/listbox';
import {AriaSearchAutocompleteProps} from '@react-types/autocomplete';
import {ComboBoxState} from '@react-stately/combobox';
import {DOMAttributes, KeyboardDelegate, ValidationResult} from '@react-types/shared';
import {DOMAttributes, KeyboardDelegate, LayoutDelegate, ValidationResult} from '@react-types/shared';
import {InputHTMLAttributes, RefObject} from 'react';
import {mergeProps} from '@react-aria/utils';
import {useComboBox} from '@react-aria/combobox';
Expand Down Expand Up @@ -43,7 +43,13 @@ export interface AriaSearchAutocompleteOptions<T> extends AriaSearchAutocomplete
/** The ref for the list box. */
listBoxRef: RefObject<HTMLElement | null>,
/** An optional keyboard delegate implementation, to override the default. */
keyboardDelegate?: KeyboardDelegate
keyboardDelegate?: KeyboardDelegate,
/**
* A delegate object that provides layout information for items in the collection.
* By default this uses the DOM, but this can be overridden to implement things like
* virtualized scrolling.
*/
layoutDelegate?: LayoutDelegate
}

/**
Expand All @@ -58,6 +64,7 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
inputRef,
listBoxRef,
keyboardDelegate,
layoutDelegate,
onSubmit = () => {},
onClear,
onKeyDown,
Expand Down Expand Up @@ -98,6 +105,7 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
{
...otherProps,
keyboardDelegate,
layoutDelegate,
popoverRef,
listBoxRef,
inputRef,
Expand Down
25 changes: 19 additions & 6 deletions packages/@react-aria/combobox/src/useComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {AriaButtonProps} from '@react-types/button';
import {AriaComboBoxProps} from '@react-types/combobox';
import {ariaHideOutside} from '@react-aria/overlays';
import {AriaListBoxOptions, getItemId, listData} from '@react-aria/listbox';
import {BaseEvent, DOMAttributes, KeyboardDelegate, PressEvent, RouterOptions, ValidationResult} from '@react-types/shared';
import {BaseEvent, DOMAttributes, KeyboardDelegate, LayoutDelegate, PressEvent, RouterOptions, ValidationResult} from '@react-types/shared';
import {chain, isAppleDevice, mergeProps, useLabels, useRouter} from '@react-aria/utils';
import {ComboBoxState} from '@react-stately/combobox';
import {FocusEvent, InputHTMLAttributes, KeyboardEvent, RefObject, TouchEvent, useEffect, useMemo, useRef} from 'react';
Expand All @@ -38,7 +38,13 @@ export interface AriaComboBoxOptions<T> extends Omit<AriaComboBoxProps<T>, 'chil
/** The ref for the optional list box popup trigger button. */
buttonRef?: RefObject<Element | null>,
/** An optional keyboard delegate implementation, to override the default. */
keyboardDelegate?: KeyboardDelegate
keyboardDelegate?: KeyboardDelegate,
/**
* A delegate object that provides layout information for items in the collection.
* By default this uses the DOM, but this can be overridden to implement things like
* virtualized scrolling.
*/
layoutDelegate?: LayoutDelegate
}

export interface ComboBoxAria<T> extends ValidationResult {
Expand Down Expand Up @@ -69,6 +75,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
inputRef,
listBoxRef,
keyboardDelegate,
layoutDelegate,
// completionMode = 'suggest',
shouldFocusWrap,
isReadOnly,
Expand All @@ -90,10 +97,16 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta

// By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
// When virtualized, the layout object will be passed in as a prop and override this.
let delegate = useMemo(() =>
keyboardDelegate ||
new ListKeyboardDelegate(state.collection, state.disabledKeys, listBoxRef)
, [keyboardDelegate, state.collection, state.disabledKeys, listBoxRef]);
let {collection} = state;
let {disabledKeys} = state.selectionManager;
let delegate = useMemo(() => (
keyboardDelegate || new ListKeyboardDelegate({
collection,
disabledKeys,
ref: listBoxRef,
layoutDelegate
})
), [keyboardDelegate, layoutDelegate, collection, disabledKeys, listBoxRef]);

// Use useSelectableCollection to get the keyboard handlers to apply to the textfield
let {collectionProps} = useSelectableCollection({
Expand Down
8 changes: 4 additions & 4 deletions packages/@react-aria/dnd/src/ListDropTargetDelegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Collection, Direction, DropTarget, DropTargetDelegate, Node, Orientation} from '@react-types/shared';
import {Direction, DropTarget, DropTargetDelegate, Node, Orientation} from '@react-types/shared';
import {RefObject} from 'react';

interface ListDropTargetDelegateOptions {
Expand Down Expand Up @@ -30,13 +30,13 @@ interface ListDropTargetDelegateOptions {
// direction. For grids, it is the secondary direction.

export class ListDropTargetDelegate implements DropTargetDelegate {
private collection: Collection<Node<unknown>>;
private collection: Iterable<Node<unknown>>;
private ref: RefObject<HTMLElement | null>;
private layout: 'stack' | 'grid';
private orientation: Orientation;
private direction: Direction;

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

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

Expand Down
18 changes: 9 additions & 9 deletions packages/@react-aria/dnd/stories/VirtualizedListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {DroppableCollectionDropEvent} from '@react-types/shared';
import {FocusRing} from '@react-aria/focus';
import Folder from '@spectrum-icons/workflow/Folder';
import {Item} from '@react-stately/collections';
import {ListKeyboardDelegate} from '@react-aria/selection';
import {ListLayout} from '@react-stately/layout';
import React from 'react';
import {useCollator} from '@react-aria/i18n';
import {useDropIndicator, useDroppableCollection, useDroppableItem} from '..';
import {useDroppableCollectionState} from '@react-stately/dnd';
import {useListBox, useOption} from '@react-aria/listbox';
Expand Down Expand Up @@ -135,21 +135,21 @@ export const VirtualizedListBox = React.forwardRef(function (props: any, ref) {
onDrop
});

let collator = useCollator({usage: 'search', sensitivity: 'base'});
let layout = React.useMemo(() =>
new ListLayout<unknown>({
estimatedRowHeight: 32,
padding: 8,
loaderHeight: 40,
placeholderHeight: 32,
collator
placeholderHeight: 32
})
, [collator]);

layout.collection = state.collection;
, []);

let {collectionProps} = useDroppableCollection({
keyboardDelegate: layout,
keyboardDelegate: new ListKeyboardDelegate({
collection: state.collection,
ref: domRef,
layoutDelegate: layout
}),
dropTargetDelegate: layout,
onDropActivate: chain(action('onDropActivate'), console.log),
onDrop
Expand All @@ -158,7 +158,7 @@ export const VirtualizedListBox = React.forwardRef(function (props: any, ref) {
let {listBoxProps} = useListBox({
...props,
'aria-label': 'List',
keyboardDelegate: layout,
layoutDelegate: layout,
isVirtualized: true
}, state, domRef);
let isDropTarget = dropState.isDropTarget({type: 'root'});
Expand Down
1 change: 0 additions & 1 deletion packages/@react-aria/grid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@react-stately/collections": "^3.10.7",
"@react-stately/grid": "^3.8.7",
"@react-stately/selection": "^3.15.1",
"@react-stately/virtualizer": "^3.7.1",
"@react-types/checkbox": "^3.8.1",
"@react-types/grid": "^3.2.6",
"@react-types/shared": "^3.23.1",
Expand Down
63 changes: 15 additions & 48 deletions packages/@react-aria/grid/src/GridKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,39 @@
* governing permissions and limitations under the License.
*/

import {Direction, DisabledBehavior, Key, KeyboardDelegate, Node} from '@react-types/shared';
import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node} 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';
import {Layout, Rect} from '@react-stately/virtualizer';
import {RefObject} from 'react';

export interface GridKeyboardDelegateOptions<T, C> {
export interface GridKeyboardDelegateOptions<C> {
collection: C,
disabledKeys: Set<Key>,
disabledBehavior?: DisabledBehavior,
ref?: RefObject<HTMLElement | null>,
direction: Direction,
collator?: Intl.Collator,
layout?: Layout<Node<T>>,
layoutDelegate?: LayoutDelegate,
focusMode?: 'row' | 'cell'
}

export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements KeyboardDelegate {
collection: C;
protected disabledKeys: Set<Key>;
protected disabledBehavior: DisabledBehavior;
protected ref: RefObject<HTMLElement | null>;
protected direction: Direction;
protected collator: Intl.Collator;
protected layout: Layout<Node<T>>;
protected layoutDelegate: LayoutDelegate;
protected focusMode;

constructor(options: GridKeyboardDelegateOptions<T, C>) {
constructor(options: GridKeyboardDelegateOptions<C>) {
this.collection = options.collection;
this.disabledKeys = options.disabledKeys;
this.disabledBehavior = options.disabledBehavior || 'all';
this.ref = options.ref;
this.direction = options.direction;
this.collator = options.collator;
this.layout = options.layout;
this.layoutDelegate = options.layoutDelegate || new DOMLayoutDelegate(options.ref);
this.focusMode = options.focusMode || 'row';
}

Expand Down Expand Up @@ -276,66 +274,35 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
return key;
}

private getItem(key: Key): HTMLElement {
return this.ref.current.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
}

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

let item = this.getItem(key);
if (item) {
return new Rect(item.offsetLeft, item.offsetTop, item.offsetWidth, item.offsetHeight);
}
}

private getPageHeight(): number {
if (this.layout) {
return this.layout.virtualizer?.visibleRect.height;
}

return this.ref?.current?.offsetHeight;
}

private getContentHeight(): number {
if (this.layout) {
return this.layout.getContentSize().height;
}

return this.ref?.current?.scrollHeight;
}

getKeyPageAbove(key: Key) {
let itemRect = this.getItemRect(key);
let itemRect = this.layoutDelegate.getItemRect(key);
if (!itemRect) {
return null;
}

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

while (itemRect && itemRect.y > pageY) {
key = this.getKeyAbove(key);
itemRect = this.getItemRect(key);
itemRect = this.layoutDelegate.getItemRect(key);
}

return key;
}

getKeyPageBelow(key: Key) {
let itemRect = this.getItemRect(key);
let itemRect = this.layoutDelegate.getItemRect(key);

if (!itemRect) {
return null;
}

let pageHeight = this.getPageHeight();
let pageY = Math.min(this.getContentHeight(), itemRect.y + pageHeight);
let pageHeight = this.layoutDelegate.getVisibleRect().height;
let pageY = Math.min(this.layoutDelegate.getContentSize().height, itemRect.y + pageHeight);

while (itemRect && itemRect.maxY < pageY) {
while (itemRect && (itemRect.y + itemRect.height) < pageY) {
let nextKey = this.getKeyBelow(key);
itemRect = this.getItemRect(nextKey);
itemRect = this.layoutDelegate.getItemRect(nextKey);

// Guard against case where maxY of the last key is barely less than pageY due to rounding
// and thus it attempts to set key to null
Expand Down
11 changes: 10 additions & 1 deletion packages/@react-aria/gridlist/src/useGridList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DOMProps,
Key,
KeyboardDelegate,
LayoutDelegate,
MultipleSelection
} from '@react-types/shared';
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils';
Expand Down Expand Up @@ -55,6 +56,12 @@ export interface AriaGridListOptions<T> extends Omit<AriaGridListProps<T>, 'chil
* to override the default.
*/
keyboardDelegate?: KeyboardDelegate,
/**
* A delegate object that provides layout information for items in the collection.
* By default this uses the DOM, but this can be overridden to implement things like
* virtualized scrolling.
*/
layoutDelegate?: LayoutDelegate,
/**
* Whether focus should wrap around when the end/start is reached.
* @default false
Expand Down Expand Up @@ -86,6 +93,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
let {
isVirtualized,
keyboardDelegate,
layoutDelegate,
onAction,
linkBehavior = 'action',
keyboardNavigationBehavior = 'arrow'
Expand All @@ -100,7 +108,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
collection: state.collection,
disabledKeys: state.disabledKeys,
ref,
keyboardDelegate: keyboardDelegate,
keyboardDelegate,
layoutDelegate,
isVirtualized,
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
shouldFocusWrap: props.shouldFocusWrap,
Expand Down
9 changes: 8 additions & 1 deletion packages/@react-aria/listbox/src/useListBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

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

/**
* A delegate object that provides layout information for items in the collection.
* By default this uses the DOM, but this can be overridden to implement things like
* virtualized scrolling.
*/
layoutDelegate?: LayoutDelegate,

/**
* Whether the listbox items should use virtual focus instead of being focused directly.
*/
Expand Down
Loading