Skip to content

Commit 5d86732

Browse files
devongovettLFDanLu
andauthored
Support onAction and isDisabled on item elements (#5874)
* Support onAction and isDisabled on item elements * strict * account for disabledBehavior --------- Co-authored-by: Daniel Lu <dl1644@gmail.com>
1 parent 02f8cf7 commit 5d86732

File tree

25 files changed

+319
-48
lines changed

25 files changed

+319
-48
lines changed

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

Lines changed: 10 additions & 3 deletions
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, Key, KeyboardDelegate, Node} from '@react-types/shared';
13+
import {Direction, DisabledBehavior, Key, KeyboardDelegate, Node} from '@react-types/shared';
1414
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
1515
import {GridCollection} from '@react-types/grid';
1616
import {Layout, Rect} from '@react-stately/virtualizer';
@@ -19,6 +19,7 @@ import {RefObject} from 'react';
1919
export interface GridKeyboardDelegateOptions<T, C> {
2020
collection: C,
2121
disabledKeys: Set<Key>,
22+
disabledBehavior?: DisabledBehavior,
2223
ref?: RefObject<HTMLElement>,
2324
direction: Direction,
2425
collator?: Intl.Collator,
@@ -29,6 +30,7 @@ export interface GridKeyboardDelegateOptions<T, C> {
2930
export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements KeyboardDelegate {
3031
collection: C;
3132
protected disabledKeys: Set<Key>;
33+
protected disabledBehavior: DisabledBehavior;
3234
protected ref: RefObject<HTMLElement>;
3335
protected direction: Direction;
3436
protected collator: Intl.Collator;
@@ -38,6 +40,7 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
3840
constructor(options: GridKeyboardDelegateOptions<T, C>) {
3941
this.collection = options.collection;
4042
this.disabledKeys = options.disabledKeys;
43+
this.disabledBehavior = options.disabledBehavior || 'all';
4144
this.ref = options.ref;
4245
this.direction = options.direction;
4346
this.collator = options.collator;
@@ -53,14 +56,18 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
5356
return node.type === 'row' || node.type === 'item';
5457
}
5558

59+
private isDisabled(item: Node<unknown>) {
60+
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key));
61+
}
62+
5663
protected findPreviousKey(fromKey?: Key, pred?: (item: Node<T>) => boolean) {
5764
let key = fromKey != null
5865
? this.collection.getKeyBefore(fromKey)
5966
: this.collection.getLastKey();
6067

6168
while (key != null) {
6269
let item = this.collection.getItem(key);
63-
if (!this.disabledKeys.has(key) && (!pred || pred(item))) {
70+
if (!this.isDisabled(item) && (!pred || pred(item))) {
6471
return key;
6572
}
6673

@@ -75,7 +82,7 @@ export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements Key
7582

7683
while (key != null) {
7784
let item = this.collection.getItem(key);
78-
if (!this.disabledKeys.has(key) && (!pred || pred(item))) {
85+
if (!this.isDisabled(item) && (!pred || pred(item))) {
7986
return key;
8087
}
8188

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
8686
let disabledBehavior = state.selectionManager.disabledBehavior;
8787
let delegate = useMemo(() => keyboardDelegate || new GridKeyboardDelegate({
8888
collection: state.collection,
89-
disabledKeys: disabledBehavior === 'selection' ? new Set() : state.disabledKeys,
89+
disabledKeys: state.disabledKeys,
90+
disabledBehavior,
9091
ref,
9192
direction,
9293
collator,

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

Lines changed: 4 additions & 2 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 {chain} from '@react-aria/utils';
1314
import {DOMAttributes, FocusableElement} from '@react-types/shared';
1415
import {GridCollection, GridNode} from '@react-types/grid';
1516
import {gridMap} from './utils';
@@ -52,14 +53,15 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
5253
onAction
5354
} = props;
5455

55-
let {actions: {onRowAction}} = gridMap.get(state);
56+
let {actions} = gridMap.get(state);
57+
let onRowAction = actions.onRowAction ? () => actions.onRowAction(node.key) : onAction;
5658
let {itemProps, ...states} = useSelectableItem({
5759
selectionManager: state.selectionManager,
5860
key: node.key,
5961
ref,
6062
isVirtualized,
6163
shouldSelectOnPressUp,
62-
onAction: onRowAction ? () => onRowAction(node.key) : onAction,
64+
onAction: onRowAction || node?.props?.onAction ? chain(node?.props?.onAction, onRowAction) : undefined,
6365
isDisabled: state.collection.size === 0
6466
});
6567

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

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

13+
import {chain, getScrollParent, getSyntheticLinkProps, mergeProps, scrollIntoViewport, useSlotId} from '@react-aria/utils';
1314
import {DOMAttributes, FocusableElement, Node as RSNode} from '@react-types/shared';
1415
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
1516
import {getLastItem} from '@react-stately/collections';
1617
import {getRowId, listMap} from './utils';
17-
import {getScrollParent, getSyntheticLinkProps, mergeProps, scrollIntoViewport, useSlotId} from '@react-aria/utils';
1818
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, RefObject, useRef} from 'react';
1919
import {isFocusVisible} from '@react-aria/interactions';
2020
import type {ListState} from '@react-stately/list';
@@ -111,7 +111,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
111111
ref,
112112
isVirtualized,
113113
shouldSelectOnPressUp,
114-
onAction: onAction ? () => onAction(node.key) : undefined,
114+
onAction: onAction || node.props?.onAction ? chain(node.props?.onAction, onAction ? () => onAction(node.key) : undefined) : undefined,
115115
focus,
116116
linkBehavior
117117
});

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

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

13+
import {chain, filterDOMProps, isMac, isWebKit, mergeProps, useSlotId} from '@react-aria/utils';
1314
import {DOMAttributes, FocusableElement, Key} from '@react-types/shared';
14-
import {filterDOMProps, isMac, isWebKit, mergeProps, useSlotId} from '@react-aria/utils';
1515
import {getItemCount} from '@react-stately/collections';
1616
import {getItemId, listData} from './utils';
1717
import {isFocusVisible, useHover} from '@react-aria/interactions';
@@ -93,7 +93,7 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
9393

9494
let data = listData.get(state);
9595

96-
let isDisabled = props.isDisabled ?? state.disabledKeys.has(key);
96+
let isDisabled = props.isDisabled ?? state.selectionManager.isDisabled(key);
9797
let isSelected = props.isSelected ?? state.selectionManager.isSelected(key);
9898
let shouldSelectOnPressUp = props.shouldSelectOnPressUp ?? data?.shouldSelectOnPressUp;
9999
let shouldFocusOnHover = props.shouldFocusOnHover ?? data?.shouldFocusOnHover;
@@ -125,6 +125,7 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
125125
optionProps['aria-setsize'] = getItemCount(state.collection);
126126
}
127127

128+
let onAction = data?.onAction ? () => data?.onAction?.(key) : undefined;
128129
let {itemProps, isPressed, isFocused, hasAction, allowsSelection} = useSelectableItem({
129130
selectionManager: state.selectionManager,
130131
key,
@@ -134,7 +135,7 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
134135
isVirtualized,
135136
shouldUseVirtualFocus,
136137
isDisabled,
137-
onAction: data?.onAction ? () => data?.onAction?.(key) : undefined,
138+
onAction: onAction || item?.props?.onAction ? chain(item?.props?.onAction, onAction) : undefined,
138139
linkBehavior: data?.linkBehavior
139140
});
140141

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,25 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
120120
} = props;
121121

122122
let isTrigger = !!hasPopup;
123-
let isDisabled = props.isDisabled ?? state.disabledKeys.has(key);
123+
let isDisabled = props.isDisabled ?? state.selectionManager.isDisabled(key);
124124
let isSelected = props.isSelected ?? state.selectionManager.isSelected(key);
125125
let data = menuData.get(state);
126+
let item = state.collection.getItem(key);
126127
let onClose = props.onClose || data.onClose;
127-
let onAction = isTrigger ? () => {} : props.onAction || data.onAction;
128128
let router = useRouter();
129129
let performAction = (e: PressEvent) => {
130-
if (onAction) {
131-
onAction(key);
130+
if (isTrigger) {
131+
return;
132+
}
133+
134+
if (item?.props?.onAction) {
135+
item.props.onAction();
136+
}
137+
138+
if (props.onAction) {
139+
props.onAction(key);
140+
} else if (data.onAction) {
141+
data.onAction(key);
132142
}
133143

134144
if (e.target instanceof HTMLAnchorElement) {
@@ -164,7 +174,6 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
164174
ariaProps['aria-checked'] = isSelected;
165175
}
166176

167-
let item = state.collection.getItem(key);
168177
if (isVirtualized) {
169178
ariaProps['aria-posinset'] = item?.index;
170179
ariaProps['aria-setsize'] = getItemCount(state.collection);

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

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

13-
import {Collection, Direction, Key, KeyboardDelegate, Node, Orientation} from '@react-types/shared';
13+
import {Collection, Direction, DisabledBehavior, Key, KeyboardDelegate, Node, Orientation} from '@react-types/shared';
1414
import {isScrollable} from '@react-aria/utils';
1515
import {RefObject} from 'react';
1616

@@ -21,12 +21,14 @@ interface ListKeyboardDelegateOptions<T> {
2121
layout?: 'stack' | 'grid',
2222
orientation?: Orientation,
2323
direction?: Direction,
24-
disabledKeys?: Set<Key>
24+
disabledKeys?: Set<Key>,
25+
disabledBehavior?: DisabledBehavior
2526
}
2627

2728
export class ListKeyboardDelegate<T> implements KeyboardDelegate {
2829
private collection: Collection<Node<T>>;
2930
private disabledKeys: Set<Key>;
31+
private disabledBehavior: DisabledBehavior;
3032
private ref: RefObject<HTMLElement>;
3133
private collator: Intl.Collator | undefined;
3234
private layout: 'stack' | 'grid';
@@ -42,6 +44,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
4244
this.ref = opts.ref;
4345
this.collator = opts.collator;
4446
this.disabledKeys = opts.disabledKeys || new Set();
47+
this.disabledBehavior = opts.disabledBehavior || 'all';
4548
this.orientation = opts.orientation;
4649
this.direction = opts.direction;
4750
this.layout = opts.layout || 'stack';
@@ -52,6 +55,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
5255
this.collator = args[3];
5356
this.layout = 'stack';
5457
this.orientation = 'vertical';
58+
this.disabledBehavior = 'all';
5559
}
5660

5761
// If this is a vertical stack, remove the left/right methods completely
@@ -62,11 +66,15 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
6266
}
6367
}
6468

69+
private isDisabled(item: Node<unknown>) {
70+
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key));
71+
}
72+
6573
getNextKey(key: Key) {
6674
key = this.collection.getKeyAfter(key);
6775
while (key != null) {
6876
let item = this.collection.getItem(key);
69-
if (item.type === 'item' && !this.disabledKeys.has(key)) {
77+
if (item.type === 'item' && !this.isDisabled(item)) {
7078
return key;
7179
}
7280

@@ -80,7 +88,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
8088
key = this.collection.getKeyBefore(key);
8189
while (key != null) {
8290
let item = this.collection.getItem(key);
83-
if (item.type === 'item' && !this.disabledKeys.has(key)) {
91+
if (item.type === 'item' && !this.isDisabled(item)) {
8492
return key;
8593
}
8694

@@ -170,7 +178,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
170178
let key = this.collection.getFirstKey();
171179
while (key != null) {
172180
let item = this.collection.getItem(key);
173-
if (item?.type === 'item' && !this.disabledKeys.has(key)) {
181+
if (item?.type === 'item' && !this.isDisabled(item)) {
174182
return key;
175183
}
176184

@@ -184,7 +192,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
184192
let key = this.collection.getLastKey();
185193
while (key != null) {
186194
let item = this.collection.getItem(key);
187-
if (item.type === 'item' && !this.disabledKeys.has(key)) {
195+
if (item.type === 'item' && !this.isDisabled(item)) {
188196
return key;
189197
}
190198

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL
5555
let collator = useCollator({usage: 'search', sensitivity: 'base'});
5656
let disabledBehavior = selectionManager.disabledBehavior;
5757
let delegate = useMemo(() => (
58-
keyboardDelegate || new ListKeyboardDelegate(collection, disabledBehavior === 'selection' ? new Set() : disabledKeys, ref, collator)
58+
keyboardDelegate || new ListKeyboardDelegate({
59+
collection,
60+
disabledKeys,
61+
disabledBehavior,
62+
ref,
63+
collator
64+
})
5965
), [keyboardDelegate, collection, disabledKeys, ref, collator, disabledBehavior]);
6066

6167
let {collectionProps} = useSelectableCollection({

packages/@react-aria/table/src/useTable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export function useTable<T>(props: AriaTableProps<T>, state: TableState<T> | Tre
5151
let disabledBehavior = state.selectionManager.disabledBehavior;
5252
let delegate = useMemo(() => keyboardDelegate || new TableKeyboardDelegate({
5353
collection: state.collection,
54-
disabledKeys: disabledBehavior === 'selection' ? new Set() : state.disabledKeys,
54+
disabledKeys: state.disabledKeys,
55+
disabledBehavior,
5556
ref,
5657
direction,
5758
collator,

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

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

13-
import {Collection, Direction, Key, KeyboardDelegate, Orientation} from '@react-types/shared';
13+
import {Collection, Direction, Key, KeyboardDelegate, Node, Orientation} from '@react-types/shared';
1414

1515
export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
16-
private collection: Collection<T>;
16+
private collection: Collection<Node<T>>;
1717
private flipDirection: boolean;
1818
private disabledKeys: Set<Key>;
1919

20-
constructor(collection: Collection<T>, direction: Direction, orientation: Orientation, disabledKeys: Set<Key> = new Set()) {
20+
constructor(collection: Collection<Node<T>>, direction: Direction, orientation: Orientation, disabledKeys: Set<Key> = new Set()) {
2121
this.collection = collection;
2222
this.flipDirection = direction === 'rtl' && orientation === 'horizontal';
2323
this.disabledKeys = disabledKeys;
@@ -45,17 +45,21 @@ export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
4545
return this.getNextKey(key);
4646
}
4747

48+
private isDisabled(key: Key) {
49+
return this.disabledKeys.has(key) || !!this.collection.getItem(key)?.props?.isDisabled;
50+
}
51+
4852
getFirstKey() {
4953
let key = this.collection.getFirstKey();
50-
if (key != null && this.disabledKeys.has(key)) {
54+
if (key != null && this.isDisabled(key)) {
5155
key = this.getNextKey(key);
5256
}
5357
return key;
5458
}
5559

5660
getLastKey() {
5761
let key = this.collection.getLastKey();
58-
if (key != null && this.disabledKeys.has(key)) {
62+
if (key != null && this.isDisabled(key)) {
5963
key = this.getPreviousKey(key);
6064
}
6165
return key;
@@ -67,7 +71,7 @@ export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
6771
if (key == null) {
6872
key = this.collection.getFirstKey();
6973
}
70-
} while (this.disabledKeys.has(key));
74+
} while (this.isDisabled(key));
7175
return key;
7276
}
7377

@@ -77,7 +81,7 @@ export class TabsKeyboardDelegate<T> implements KeyboardDelegate {
7781
if (key == null) {
7882
key = this.collection.getLastKey();
7983
}
80-
} while (this.disabledKeys.has(key));
84+
} while (this.isDisabled(key));
8185
return key;
8286
}
8387
}

0 commit comments

Comments
 (0)