Skip to content

Commit 58ad7a2

Browse files
committed
Feat: Add support for orientation to KeyboardDelegate interface
1 parent e92c7db commit 58ad7a2

File tree

16 files changed

+138
-50
lines changed

16 files changed

+138
-50
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
107107
collection,
108108
disabledKeys,
109109
ref: listBoxRef,
110-
layoutDelegate
110+
layoutDelegate,
111+
orientation: 'vertical'
111112
})
112113
), [keyboardDelegate, layoutDelegate, collection, disabledKeys, listBoxRef]);
113114

@@ -380,6 +381,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
380381
shouldUseVirtualFocus: true,
381382
shouldSelectOnPressUp: true,
382383
shouldFocusOnHover: true,
384+
orientation: 'vertical' as const,
383385
linkBehavior: 'selection' as const
384386
}),
385387
descriptionProps,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
5050
this.focusMode = options.focusMode ?? 'row';
5151
}
5252

53+
getOrientation(): Orientation | null {
54+
return this.layoutDelegate.getOrientation?.() || 'vertical';
55+
}
56+
5357
protected isCell(node: Node<T>): boolean {
5458
return node.type === 'cell';
5559
}

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

Lines changed: 9 additions & 2 deletions
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, LayoutDelegate, RefObject} from '@react-types/shared';
14+
import {DOMAttributes, KeyboardDelegate, LayoutDelegate, Orientation, RefObject} 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';
@@ -55,7 +55,12 @@ export interface AriaListBoxOptions<T> extends Omit<AriaListBoxProps<T>, 'childr
5555
* - 'override': links override all other interactions (link items are not selectable).
5656
* @default 'override'
5757
*/
58-
linkBehavior?: 'action' | 'selection' | 'override'
58+
linkBehavior?: 'action' | 'selection' | 'override',
59+
60+
/**
61+
* The orientation of the listbox.
62+
*/
63+
orientation?: Orientation
5964
}
6065

6166
/**
@@ -68,6 +73,7 @@ export function useListBox<T>(props: AriaListBoxOptions<T>, state: ListState<T>,
6873
let domProps = filterDOMProps(props, {labelable: true});
6974
// Use props instead of state here. We don't want this to change due to long press.
7075
let selectionBehavior = props.selectionBehavior || 'toggle';
76+
let orientation = props.orientation || props.keyboardDelegate?.getOrientation?.();
7177
let linkBehavior = props.linkBehavior || (selectionBehavior === 'replace' ? 'action' : 'override');
7278
if (selectionBehavior === 'toggle' && linkBehavior === 'action') {
7379
// linkBehavior="action" does not work with selectionBehavior="toggle" because there is no way
@@ -117,6 +123,7 @@ export function useListBox<T>(props: AriaListBoxOptions<T>, state: ListState<T>,
117123
'aria-multiselectable': 'true'
118124
} : {}, {
119125
role: 'listbox',
126+
'aria-orientation': orientation === 'horizontal' ? orientation : undefined,
120127
...mergeProps(fieldProps, listProps)
121128
})
122129
};

packages/@react-aria/menu/src/useMenu.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function useMenu<T>(props: AriaMenuOptions<T>, state: TreeState<T>, ref:
6262
collection: state.collection,
6363
disabledKeys: state.disabledKeys,
6464
shouldFocusWrap,
65+
orientation: 'vertical',
6566
linkBehavior: 'override'
6667
});
6768

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,35 @@ import {Key, LayoutDelegate, Orientation, Rect, RefObject, Size} from '@react-ty
1515

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

2020
constructor(ref: RefObject<HTMLElement | null>, orientation?: Orientation) {
2121
this.ref = ref;
22-
this.orientation = orientation ?? 'vertical';
22+
this.orientation = orientation;
2323
}
2424

25-
getOrientation(): Orientation {
26-
return this.orientation;
25+
getOrientation(): Orientation | null {
26+
let container = this.ref.current;
27+
if (this.orientation) {
28+
return this.orientation;
29+
}
30+
31+
// https://w3c.github.io/aria/#aria-orientation
32+
switch (container?.role) {
33+
case 'menubar':
34+
case 'slider':
35+
case 'separator':
36+
case 'tablist':
37+
case 'toolbar':
38+
return 'horizontal';
39+
case 'listbox':
40+
case 'menu':
41+
case 'scrollbar':
42+
case 'tree':
43+
return 'vertical';
44+
default:
45+
return null;
46+
}
2747
}
2848

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

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
4747
this.collator = opts.collator;
4848
this.disabledKeys = opts.disabledKeys || new Set();
4949
this.disabledBehavior = opts.disabledBehavior || 'all';
50-
this.orientation = opts.orientation || 'vertical';
50+
this.orientation = opts.orientation;
5151
this.direction = opts.direction;
5252
this.layout = opts.layout || 'stack';
5353
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref, this.orientation);
@@ -57,17 +57,31 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
5757
this.ref = args[2];
5858
this.collator = args[3];
5959
this.layout = 'stack';
60-
this.orientation = 'vertical';
6160
this.disabledBehavior = 'all';
62-
this.layoutDelegate = new DOMLayoutDelegate(this.ref, this.orientation);
61+
this.layoutDelegate = new DOMLayoutDelegate(this.ref);
6362
}
6463

6564
// If this is a vertical stack, remove the left/right methods completely
66-
// so they aren't called by useDroppableCollection.
67-
if (this.layout === 'stack' && this.orientation === 'vertical') {
68-
this.getKeyLeftOf = undefined;
69-
this.getKeyRightOf = undefined;
70-
}
65+
// so they aren't called by useDroppableCollection or useAutocomplete.
66+
let getKeyRightOf = this.getKeyRightOf;
67+
let getKeyLeftOf = this.getKeyLeftOf;
68+
69+
Object.defineProperty(this, 'getKeyRightOf', {
70+
get() { return this.layout === 'stack' && this.getOrientation() === 'vertical' ? undefined : getKeyRightOf; },
71+
configurable: true,
72+
enumerable: false
73+
});
74+
75+
Object.defineProperty(this, 'getKeyLeftOf', {
76+
get() { return this.layout === 'stack' && this.getOrientation() === 'vertical' ? undefined : getKeyLeftOf; },
77+
configurable: true,
78+
enumerable: false
79+
});
80+
}
81+
82+
getOrientation(): Orientation {
83+
// TODO: Should we log a warning if keyboard and layout delegate mismatch in orientation?
84+
return this.orientation || this.layoutDelegate.getOrientation?.() || 'vertical';
7185
}
7286

7387
private isDisabled(item: Node<unknown>) {
@@ -133,15 +147,15 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
133147
}
134148

135149
getKeyBelow(key: Key): Key | null {
136-
if (this.layout === 'grid' && this.orientation === 'vertical') {
150+
if (this.layout === 'grid' && this.getOrientation() === 'vertical') {
137151
return this.findKey(key, (key) => this.getNextKey(key), this.isSameRow);
138152
} else {
139153
return this.getNextKey(key);
140154
}
141155
}
142156

143157
getKeyAbove(key: Key): Key | null {
144-
if (this.layout === 'grid' && this.orientation === 'vertical') {
158+
if (this.layout === 'grid' && this.getOrientation() === 'vertical') {
145159
return this.findKey(key, (key) => this.getPreviousKey(key), this.isSameRow);
146160
} else {
147161
return this.getPreviousKey(key);
@@ -162,12 +176,12 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
162176
}
163177

164178
if (this.layout === 'grid') {
165-
if (this.orientation === 'vertical') {
179+
if (this.getOrientation() === 'vertical') {
166180
return this.getNextColumn(key, this.direction === 'rtl');
167181
} else {
168182
return this.findKey(key, (key) => this.getNextColumn(key, this.direction === 'rtl'), this.isSameColumn);
169183
}
170-
} else if (this.orientation === 'horizontal') {
184+
} else if (this.getOrientation() === 'horizontal') {
171185
return this.getNextColumn(key, this.direction === 'rtl');
172186
}
173187

@@ -182,12 +196,12 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
182196
}
183197

184198
if (this.layout === 'grid') {
185-
if (this.orientation === 'vertical') {
199+
if (this.getOrientation() === 'vertical') {
186200
return this.getNextColumn(key, this.direction === 'ltr');
187201
} else {
188202
return this.findKey(key, (key) => this.getNextColumn(key, this.direction === 'ltr'), this.isSameColumn);
189203
}
190-
} else if (this.orientation === 'horizontal') {
204+
} else if (this.getOrientation() === 'horizontal') {
191205
return this.getNextColumn(key, this.direction === 'ltr');
192206
}
193207

@@ -216,7 +230,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
216230
}
217231

218232
let nextKey: Key | null = key;
219-
if (this.orientation === 'horizontal') {
233+
if (this.getOrientation() === 'horizontal') {
220234
let pageX = Math.max(0, itemRect.x + itemRect.width - this.layoutDelegate.getVisibleRect().width);
221235

222236
while (itemRect && itemRect.x > pageX && nextKey != null) {
@@ -247,7 +261,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
247261
}
248262

249263
let nextKey: Key | null = key;
250-
if (this.orientation === 'horizontal') {
264+
if (this.getOrientation() === 'horizontal') {
251265
let pageX = Math.min(this.layoutDelegate.getContentSize().width, itemRect.y - itemRect.width + this.layoutDelegate.getVisibleRect().width);
252266

253267
while (itemRect && itemRect.x < pageX && nextKey != null) {

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaSelectableCollectionOptions, useSelectableCollection} from './useSelectableCollection';
14-
import {Collection, DOMAttributes, Key, KeyboardDelegate, LayoutDelegate, Node} from '@react-types/shared';
14+
import {Collection, DOMAttributes, Key, KeyboardDelegate, LayoutDelegate, Node, Orientation} from '@react-types/shared';
1515
import {ListKeyboardDelegate} from './ListKeyboardDelegate';
1616
import {useCollator} from '@react-aria/i18n';
1717
import {useMemo} from 'react';
@@ -34,7 +34,12 @@ export interface AriaSelectableListOptions extends Omit<AriaSelectableCollection
3434
/**
3535
* The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
3636
*/
37-
disabledKeys: Set<Key>
37+
disabledKeys: Set<Key>,
38+
/**
39+
* The primary orientation of the items. Usually this is the
40+
* direction that the collection scrolls.
41+
*/
42+
orientation?: Orientation
3843
}
3944

4045
export interface SelectableListAria {
@@ -54,7 +59,8 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL
5459
disabledKeys,
5560
ref,
5661
keyboardDelegate,
57-
layoutDelegate
62+
layoutDelegate,
63+
orientation
5864
} = props;
5965

6066
// By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
@@ -68,9 +74,10 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL
6874
disabledBehavior,
6975
ref,
7076
collator,
71-
layoutDelegate
77+
layoutDelegate,
78+
orientation
7279
})
73-
), [keyboardDelegate, layoutDelegate, collection, disabledKeys, ref, collator, disabledBehavior]);
80+
), [keyboardDelegate, layoutDelegate, collection, disabledKeys, ref, collator, disabledBehavior, orientation]);
7481

7582
let {collectionProps} = useSelectableCollection({
7683
...props,

packages/@react-aria/tabs/src/TabsKeyboardDelegate.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
1616
private collection: Collection<Node<T>>;
1717
private flipDirection: boolean;
1818
private disabledKeys: Set<Key>;
19-
private tabDirection: boolean;
19+
private orientation: Orientation;
2020

2121
constructor(collection: Collection<Node<T>>, direction: Direction, orientation: Orientation, disabledKeys: Set<Key> = new Set()) {
2222
this.collection = collection;
2323
this.flipDirection = direction === 'rtl' && orientation === 'horizontal';
2424
this.disabledKeys = disabledKeys;
25-
this.tabDirection = orientation === 'horizontal';
25+
this.orientation = orientation;
26+
}
27+
28+
getOrientation(): Orientation {
29+
return this.orientation;
2630
}
2731

2832
getKeyLeftOf(key: Key): Key | null {
@@ -61,14 +65,14 @@ export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
6165
}
6266

6367
getKeyAbove(key: Key): Key | null {
64-
if (this.tabDirection) {
68+
if (this.getOrientation() === 'horizontal') {
6569
return null;
6670
}
6771
return this.getPreviousKey(key);
6872
}
6973

7074
getKeyBelow(key: Key): Key | null {
71-
if (this.tabDirection) {
75+
if (this.getOrientation() === 'horizontal') {
7276
return null;
7377
}
7478
return this.getNextKey(key);

packages/@react-aria/tabs/src/useTabList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function useTabList<T>(props: AriaTabListOptions<T>, state: TabListState<
6868
tabListProps: {
6969
...mergeProps(collectionProps, tabListLabelProps),
7070
role: 'tablist',
71-
'aria-orientation': orientation,
71+
'aria-orientation': orientation === 'vertical' ? orientation : undefined,
7272
tabIndex: undefined
7373
}
7474
};

0 commit comments

Comments
 (0)