Skip to content

Commit 50bfc76

Browse files
committed
rough support for virtualfocus gridlist
1 parent d49f6b2 commit 50bfc76

File tree

6 files changed

+44
-29
lines changed

6 files changed

+44
-29
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
237237
// Prevent these keys from moving the text cursor in the input
238238
// TODO: special case ArrowLeft/Right so they still do move the text cursor
239239
// However, this should really depend on the primary wrapped component's layout orientation (aka maybe shouldn't happen if TagGroup?)
240-
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
240+
if (!(e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
241241
e.preventDefault();
242242
}
243243

packages/@react-aria/button/src/useButton.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
106106
focusableProps.tabIndex = isDisabled ? -1 : focusableProps.tabIndex;
107107
}
108108
let buttonProps = mergeProps(focusableProps, pressProps, filterDOMProps(props, {labelable: true}));
109-
109+
// TODO: will need to support virtual focus on the button
110110
return {
111111
isPressed, // Used to indicate press state for visual
112112
buttonProps: mergeProps(additionalProps, buttonProps, {

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {chain, getScrollParent, mergeProps, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
1414
import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
15-
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
15+
import {focusSafely, getFocusableTreeWalker, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
1616
import {getRowId, listMap} from './utils';
1717
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
1818
import {isFocusVisible} from '@react-aria/interactions';
@@ -131,13 +131,21 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
131131
shouldUseVirtualFocus
132132
});
133133

134+
let focusElement = (element: FocusableElement) => {
135+
if (!shouldUseVirtualFocus) {
136+
focusSafely(element);
137+
} else {
138+
moveVirtualFocus(element);
139+
}
140+
};
141+
134142
let onKeyDownCapture = (e: ReactKeyboardEvent) => {
135143
if (!e.currentTarget.contains(e.target as Element) || !ref.current || !document.activeElement) {
136144
return;
137145
}
138146

139147
let walker = getFocusableTreeWalker(ref.current);
140-
walker.currentNode = document.activeElement;
148+
walker.currentNode = shouldUseVirtualFocus ? getVirtuallyFocusedElement(document) as FocusableElement : document.activeElement;
141149

142150
if ('expandedKeys' in state && document.activeElement === ref.current) {
143151
if ((e.key === EXPANSION_KEYS['expand'][direction]) && state.selectionManager.focusedKey === node.key && hasChildRows && !state.expandedKeys.has(node.key)) {
@@ -162,20 +170,20 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
162170
if (focusable) {
163171
e.preventDefault();
164172
e.stopPropagation();
165-
focusSafely(focusable);
173+
focusElement(focusable);
166174
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
167175
} else {
168176
// If there is no next focusable child, then return focus back to the row
169177
e.preventDefault();
170178
e.stopPropagation();
171179
if (direction === 'rtl') {
172-
focusSafely(ref.current);
180+
focusElement(ref.current);
173181
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
174182
} else {
175183
walker.currentNode = ref.current;
176184
let lastElement = last(walker);
177185
if (lastElement) {
178-
focusSafely(lastElement);
186+
focusElement(lastElement);
179187
scrollIntoViewport(lastElement, {containingElement: getScrollParent(ref.current)});
180188
}
181189
}
@@ -192,19 +200,19 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
192200
if (focusable) {
193201
e.preventDefault();
194202
e.stopPropagation();
195-
focusSafely(focusable);
203+
focusElement(focusable);
196204
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
197205
} else {
198206
e.preventDefault();
199207
e.stopPropagation();
200208
if (direction === 'ltr') {
201-
focusSafely(ref.current);
209+
focusElement(ref.current);
202210
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
203211
} else {
204212
walker.currentNode = ref.current;
205213
let lastElement = last(walker);
206214
if (lastElement) {
207-
focusSafely(lastElement);
215+
focusElement(lastElement);
208216
scrollIntoViewport(lastElement, {containingElement: getScrollParent(ref.current)});
209217
}
210218
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {chain, isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1414
import {DOMAttributes, DOMProps, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
15-
import {focusSafely, PressHookProps, useLongPress, usePress} from '@react-aria/interactions';
15+
import {focusSafely, isFocusVisible, PressHookProps, useLongPress, usePress} from '@react-aria/interactions';
1616
import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
1717
import {moveVirtualFocus} from '@react-aria/focus';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
@@ -401,6 +401,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
401401
isPressed,
402402
isSelected: manager.isSelected(key),
403403
isFocused: manager.isFocused && manager.focusedKey === key,
404+
isFocusVisible: manager.isFocused && manager.focusedKey === key && isFocusVisible(),
404405
isDisabled,
405406
allowsSelection,
406407
hasAction

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps
1818
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
1919
import {DragAndDropHooks} from './useDragAndDrop';
2020
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately';
21-
import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
21+
import {filterDOMProps, inertValue, LoadMoreSentinelProps, mergeRefs, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
2222
import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, 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';
@@ -104,11 +104,9 @@ interface GridListInnerProps<T extends object> {
104104
}
105105

106106
function GridListInner<T extends object>({props, collection, gridListRef: ref}: GridListInnerProps<T>) {
107-
// TODO: for now, don't grab collection ref and collectionProps from the autocomplete, rely on the user tabbing to the gridlist
108-
// figure out if we want to support virtual focus for grids when wrapped in an autocomplete
109-
let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
110-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
111-
let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {};
107+
let {filter, collectionProps, collectionRef} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
108+
// Memoed so that useAutocomplete callback ref is properly only called once on mount and not everytime a rerender happens
109+
ref = useObjectRef(useMemo(() => mergeRefs(ref, collectionRef !== undefined ? collectionRef as RefObject<HTMLDivElement> : null), [collectionRef, ref]));
112110
let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props;
113111
let {CollectionRoot, isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate} = useContext(CollectionRendererContext);
114112
let gridlistState = useListState({
@@ -137,7 +135,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
137135

138136
let {gridProps} = useGridList({
139137
...props,
140-
...DOMCollectionProps,
138+
...collectionProps,
141139
keyboardDelegate,
142140
// Only tab navigation is supported in grid layout.
143141
keyboardNavigationBehavior: layout === 'grid' ? 'tab' : keyboardNavigationBehavior,
@@ -345,7 +343,8 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(GridListNode, func
345343
values: {
346344
...states,
347345
isHovered,
348-
isFocusVisible,
346+
// TODO: check if I can get rid of useFocusRing
347+
isFocusVisible: isFocusVisible || states.isFocusVisible,
349348
selectionMode: state.selectionManager.selectionMode,
350349
selectionBehavior: state.selectionManager.selectionBehavior,
351350
allowsDragging: !!dragState,
@@ -388,7 +387,7 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(GridListNode, func
388387
data-disabled={states.isDisabled || undefined}
389388
data-hovered={isHovered || undefined}
390389
data-focused={states.isFocused || undefined}
391-
data-focus-visible={isFocusVisible || undefined}
390+
data-focus-visible={isFocusVisible || states.isFocusVisible || undefined}
392391
data-pressed={states.isPressed || undefined}
393392
data-allows-dragging={!!dragState || undefined}
394393
data-dragging={isDragging || undefined}

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,12 @@ AutocompleteWithAsyncListBox.story = {
959959
}
960960
};
961961

962+
function MyButton() {
963+
return (
964+
<Button onPress={action('button press')}>Actions</Button>
965+
);
966+
}
967+
962968
export const AutocompleteWithGridList = () => {
963969
return (
964970
<AutocompleteWrapper>
@@ -968,18 +974,19 @@ export const AutocompleteWithGridList = () => {
968974
<Input />
969975
</TextField>
970976
<GridList
977+
onAction={action('onAction')}
971978
className={styles.menu}
972979
style={{height: 200, width: 200}}
973980
aria-label="test gridlist">
974-
<MyGridListItem textValue="Foo">Foo <Button>Actions</Button></MyGridListItem>
975-
<MyGridListItem textValue="Bar">Bar <Button>Actions</Button></MyGridListItem>
976-
<MyGridListItem textValue="Baz">Baz <Button>Actions</Button></MyGridListItem>
977-
<MyGridListItem textValue="Charizard">Charizard<Button>Actions</Button></MyGridListItem>
978-
<MyGridListItem textValue="Blastoise">Blastoise <Button>Actions</Button></MyGridListItem>
979-
<MyGridListItem textValue="Pikachu">Pikachu <Button>Actions</Button></MyGridListItem>
980-
<MyGridListItem textValue="Venusaur">Venusaur<Button>Actions</Button></MyGridListItem>
981-
<MyGridListItem textValue="text value check">textValue is "text value check" <Button>Actions</Button></MyGridListItem>
982-
<MyGridListItem textValue="Blah">Blah <Button>Actions</Button></MyGridListItem>
981+
<MyGridListItem textValue="Foo">Foo <MyButton /></MyGridListItem>
982+
<MyGridListItem textValue="Bar">Bar <MyButton /></MyGridListItem>
983+
<MyGridListItem textValue="Baz">Baz <MyButton /></MyGridListItem>
984+
<MyGridListItem textValue="Charizard">Charizard<MyButton /></MyGridListItem>
985+
<MyGridListItem textValue="Blastoise">Blastoise <MyButton /></MyGridListItem>
986+
<MyGridListItem textValue="Pikachu">Pikachu <MyButton /></MyGridListItem>
987+
<MyGridListItem textValue="Venusaur">Venusaur<MyButton /></MyGridListItem>
988+
<MyGridListItem textValue="text value check">textValue is "text value check" <MyButton /></MyGridListItem>
989+
<MyGridListItem textValue="Blah">Blah <MyButton /></MyGridListItem>
983990
</GridList>
984991
</div>
985992
</AutocompleteWrapper>

0 commit comments

Comments
 (0)