Skip to content

Commit 423f1b6

Browse files
committed
progress with supporting virtual focus in table
still a bunch of bugs, see notes
1 parent 1d76088 commit 423f1b6

File tree

20 files changed

+278
-170
lines changed

20 files changed

+278
-170
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,15 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
231231
// Backspace shouldn't trigger tag deletion either
232232
return;
233233
case 'Tab':
234-
// Don't propogate Tab down to the collection, otherwise we will try to focus the collection via useSelectableCollection's Tab handler (aka shift tab logic)
235-
// We want FocusScope to handle Tab if one exists (aka sub dialog), so special casepropogate
236-
if ('continuePropagation' in e) {
234+
// Propagate Tab down to the collection so that tabbing foward will hit our special logic to treat the collection
235+
// as a single tab stop. We want FocusScope to handle Shift Tab if one exists (aka sub dialog), so special case propogate
236+
// Otherwise, we don't want useSeletableCollection to handle that anyways since focus is actually on an input outside the
237+
// wrapped collection and thus the browser can handle that for us
238+
if ('continuePropagation' in e && e.shiftKey) {
237239
e.continuePropagation();
240+
return;
238241
}
239-
return;
242+
break;
240243
case 'Home':
241244
case 'End':
242245
case 'PageDown':

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,15 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
105105
if (allowFocusWhenDisabled) {
106106
focusableProps.tabIndex = isDisabled ? -1 : focusableProps.tabIndex;
107107
}
108+
109+
// TODO: need to make the button (and really any elements within a virtual focus collection) non tabbable
110+
// if we don't do this then real focus can move inside the collection and breaks the virtual focus logic
111+
// (can get cases where multiple things get focus visible styles)
112+
// Ideally useSelectableCollection can do the same thing where it coerces focus past the entire collection
113+
// focusableProps.tabIndex = -1;
114+
108115
let buttonProps = mergeProps(focusableProps, pressProps, filterDOMProps(props, {labelable: true}));
109-
// TODO: will need to support virtual focus on the button
116+
110117
return {
111118
isPressed, // Used to indicate press state for visual
112119
buttonProps: mergeProps(additionalProps, buttonProps, {

packages/@react-aria/focus/src/useFocusRing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ export function useFocusRing(props: AriaFocusRingProps = {}): FocusRingAria {
5050
let updateState = useCallback(() => setFocusVisible(state.current.isFocused && state.current.isFocusVisible), []);
5151

5252
let onFocusChange = useCallback(isFocused => {
53+
// console.log('calling focus change', isFocused, state.current.isFocusVisible)
5354
state.current.isFocused = isFocused;
5455
setFocused(isFocused);
5556
updateState();
5657
}, [updateState]);
5758

5859
useFocusVisibleListener((isFocusVisible) => {
60+
// console.log('calling focus visible listern', isFocusVisible)
5961
state.current.isFocusVisible = isFocusVisible;
6062
updateState();
6163
}, [], {isTextInput});

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ export interface GridProps extends DOMProps, AriaLabelingProps {
6464
*/
6565
escapeKeyBehavior?: 'clearSelection' | 'none',
6666
/** Whether selection should occur on press up instead of press down. */
67-
shouldSelectOnPressUp?: boolean
67+
shouldSelectOnPressUp?: boolean,
68+
/**
69+
* Whether the table cells should use virtual focus instead of being focused directly.
70+
*/
71+
shouldUseVirtualFocus?: boolean
6872
}
6973

7074
export interface GridAria {
@@ -90,7 +94,8 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
9094
onRowAction,
9195
onCellAction,
9296
escapeKeyBehavior = 'clearSelection',
93-
shouldSelectOnPressUp
97+
shouldSelectOnPressUp,
98+
shouldUseVirtualFocus
9499
} = props;
95100
let {selectionManager: manager} = state;
96101

@@ -120,11 +125,12 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
120125
isVirtualized,
121126
scrollRef,
122127
disallowTypeAhead,
123-
escapeKeyBehavior
128+
escapeKeyBehavior,
129+
shouldUseVirtualFocus
124130
});
125131

126132
let id = useId(props.id);
127-
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}, shouldSelectOnPressUp});
133+
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}, shouldSelectOnPressUp, shouldUseVirtualFocus});
128134

129135
let descriptionProps = useHighlightSelectionDescription({
130136
selectionManager: manager,
@@ -169,6 +175,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
169175
'aria-multiselectable': manager.selectionMode === 'multiple' ? 'true' : undefined
170176
},
171177
state.isKeyboardNavigationDisabled ? navDisabledHandlers : collectionProps,
178+
// TODO: may need to change this tabIndex here for virtual focus
172179
// If collection is empty, make sure the grid is tabbable unless there is a child tabbable element.
173180
(state.collection.size === 0 && {tabIndex: hasTabbableChild ? -1 : 0}) || undefined,
174181
descriptionProps

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

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

1313
import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared';
1414
import {focusSafely, isFocusVisible} from '@react-aria/interactions';
15-
import {getFocusableTreeWalker} from '@react-aria/focus';
15+
import {getFocusableTreeWalker, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
1616
import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils';
1717
import {GridCollection, GridNode} from '@react-types/grid';
1818
import {gridMap} from './utils';
@@ -62,37 +62,40 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
6262
} = props;
6363

6464
let {direction} = useLocale();
65-
let {keyboardDelegate, actions: {onCellAction}} = gridMap.get(state)!;
65+
let {keyboardDelegate, actions: {onCellAction}, shouldUseVirtualFocus} = gridMap.get(state)!;
6666

6767
// We need to track the key of the item at the time it was last focused so that we force
6868
// focus to go to the item when the DOM node is reused for a different item in a virtualizer.
6969
let keyWhenFocused = useRef<Key | null>(null);
7070

7171
// Handles focusing the cell. If there is a focusable child,
7272
// it is focused, otherwise the cell itself is focused.
73+
// TODO: throughly test these changes
7374
let focus = () => {
7475
if (ref.current) {
7576
let treeWalker = getFocusableTreeWalker(ref.current);
77+
let activeElement = shouldUseVirtualFocus ? getVirtuallyFocusedElement(document) : document.activeElement;
7678
if (focusMode === 'child') {
7779
// If focus is already on a focusable child within the cell, early return so we don't shift focus
78-
if (ref.current.contains(document.activeElement) && ref.current !== document.activeElement) {
80+
if (ref.current.contains(activeElement) && ref.current !== activeElement) {
7981
return;
8082
}
8183

8284
let focusable = state.selectionManager.childFocusStrategy === 'last'
8385
? last(treeWalker)
8486
: treeWalker.firstChild() as FocusableElement;
87+
8588
if (focusable) {
86-
focusSafely(focusable);
89+
focusElement(focusable);
8790
return;
8891
}
8992
}
9093

9194
if (
9295
(keyWhenFocused.current != null && node.key !== keyWhenFocused.current) ||
93-
!ref.current.contains(document.activeElement)
96+
!ref.current.contains(activeElement)
9497
) {
95-
focusSafely(ref.current);
98+
focusElement(ref.current);
9699
}
97100
}
98101
};
@@ -105,16 +108,25 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
105108
focus,
106109
shouldSelectOnPressUp,
107110
onAction: onCellAction ? () => onCellAction(node.key) : onAction,
108-
isDisabled: state.collection.size === 0
111+
isDisabled: state.collection.size === 0,
112+
shouldUseVirtualFocus
109113
});
110114

115+
let focusElement = (element: FocusableElement) => {
116+
if (!shouldUseVirtualFocus) {
117+
focusSafely(element);
118+
} else {
119+
moveVirtualFocus(element);
120+
}
121+
};
122+
111123
let onKeyDownCapture = (e: ReactKeyboardEvent) => {
112124
if (!e.currentTarget.contains(e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !document.activeElement) {
113125
return;
114126
}
115127

116128
let walker = getFocusableTreeWalker(ref.current);
117-
walker.currentNode = document.activeElement;
129+
walker.currentNode = shouldUseVirtualFocus ? getVirtuallyFocusedElement(document) as FocusableElement : document.activeElement;
118130

119131
switch (e.key) {
120132
case 'ArrowLeft': {
@@ -131,7 +143,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
131143
e.preventDefault();
132144
e.stopPropagation();
133145
if (focusable) {
134-
focusSafely(focusable);
146+
focusElement(focusable);
135147
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
136148
} else {
137149
// If there is no next focusable child, then move to the next cell to the left of this one.
@@ -150,16 +162,17 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
150162
break;
151163
}
152164

165+
// TODO: may need the same handling as in GridList with the currentNode check
153166
if (focusMode === 'cell' && direction === 'rtl') {
154-
focusSafely(ref.current);
167+
focusElement(ref.current);
155168
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
156169
} else {
157170
walker.currentNode = ref.current;
158171
focusable = direction === 'rtl'
159172
? walker.firstChild() as FocusableElement
160173
: last(walker);
161174
if (focusable) {
162-
focusSafely(focusable);
175+
focusElement(focusable);
163176
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
164177
}
165178
}
@@ -178,7 +191,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
178191
e.preventDefault();
179192
e.stopPropagation();
180193
if (focusable) {
181-
focusSafely(focusable);
194+
focusElement(focusable);
182195
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
183196
} else {
184197
let next = keyboardDelegate.getKeyRightOf?.(node.key);
@@ -192,16 +205,17 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
192205
break;
193206
}
194207

208+
// TODO: may need the same handling as in GridList with the currentNode check
195209
if (focusMode === 'cell' && direction === 'ltr') {
196-
focusSafely(ref.current);
210+
focusElement(ref.current);
197211
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
198212
} else {
199213
walker.currentNode = ref.current;
200214
focusable = direction === 'rtl'
201215
? last(walker)
202216
: walker.firstChild() as FocusableElement;
203217
if (focusable) {
204-
focusSafely(focusable);
218+
focusElement(focusable);
205219
scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
206220
}
207221
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
5252
onAction
5353
} = props;
5454

55-
let {actions, shouldSelectOnPressUp: gridShouldSelectOnPressUp} = gridMap.get(state)!;
55+
let {actions, shouldSelectOnPressUp: gridShouldSelectOnPressUp, shouldUseVirtualFocus} = gridMap.get(state)!;
5656
let onRowAction = actions.onRowAction ? () => actions.onRowAction?.(node.key) : onAction;
5757
let {itemProps, ...states} = useSelectableItem({
5858
selectionManager: state.selectionManager,
@@ -61,7 +61,8 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
6161
isVirtualized,
6262
shouldSelectOnPressUp: gridShouldSelectOnPressUp || shouldSelectOnPressUp,
6363
onAction: onRowAction || node?.props?.onAction ? chain(node?.props?.onAction, onRowAction) : undefined,
64-
isDisabled: state.collection.size === 0
64+
isDisabled: state.collection.size === 0,
65+
shouldUseVirtualFocus
6566
});
6667

6768
let isSelected = state.selectionManager.isSelected(node.key);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ interface GridMapShared {
2020
onRowAction?: (key: Key) => void,
2121
onCellAction?: (key: Key) => void
2222
},
23-
shouldSelectOnPressUp?: boolean
23+
shouldSelectOnPressUp?: boolean,
24+
shouldUseVirtualFocus?: boolean
2425
}
2526

2627
// Used to share:

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,17 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
7373
// We need to track the key of the item at the time it was last focused so that we force
7474
// focus to go to the item when the DOM node is reused for a different item in a virtualizer.
7575
let keyWhenFocused = useRef<Key | null>(null);
76+
// TODO: need to update this to handle virtual focus
7677
let focus = () => {
7778
// Don't shift focus to the row if the active element is a element within the row already
7879
// (e.g. clicking on a row button)
80+
let activeElement = shouldUseVirtualFocus ? getVirtuallyFocusedElement(document) : document.activeElement;
7981
if (
8082
ref.current !== null &&
8183
((keyWhenFocused.current != null && node.key !== keyWhenFocused.current) ||
82-
!ref.current?.contains(document.activeElement))
84+
!ref.current?.contains(activeElement))
8385
) {
84-
focusSafely(ref.current);
86+
focusElement(ref.current);
8587
}
8688
};
8789

packages/@react-aria/interactions/src/focusSafely.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function focusSafely(element: FocusableElement): void {
3131
// from off the screen.
3232
const ownerDocument = getOwnerDocument(element);
3333
const activeElement = getActiveElement(ownerDocument);
34+
3435
if (getInteractionModality() === 'virtual') {
3536
let lastFocusedElement = activeElement;
3637
runAfterTransition(() => {

packages/@react-aria/interactions/src/useFocus.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import {DOMAttributes, FocusableElement, FocusEvents} from '@react-types/shared';
1919
import {FocusEvent, useCallback} from 'react';
2020
import {getActiveElement, getEventTarget, getOwnerDocument} from '@react-aria/utils';
21+
// TODO: circular dependency
22+
import {getVirtuallyFocusedElement} from '../../focus';
2123
import {useSyntheticBlurEvent} from './utils';
2224

2325
export interface FocusProps<Target = FocusableElement> extends FocusEvents<Target> {
@@ -63,9 +65,18 @@ export function useFocus<Target extends FocusableElement = FocusableElement>(pro
6365
// Double check that document.activeElement actually matches e.target in case a previously chained
6466
// focus handler already moved focus somewhere else.
6567

68+
// if (e.target.nodeName === 'BUTTON') {
69+
// console.log('focusing specific node', e.target)
70+
// console.trace()
71+
// }
6672
const ownerDocument = getOwnerDocument(e.target);
6773
const activeElement = ownerDocument ? getActiveElement(ownerDocument) : getActiveElement();
68-
if (e.target === e.currentTarget && activeElement === getEventTarget(e.nativeEvent)) {
74+
75+
// TODO the below check on getVirtuallyFocusedElement isn't accurate because at this point when focus is called, the virtually focused element
76+
// that we detect is still the old one and thus we cant really distinguish if the element recieveing focus here will be the newly virtually focused element
77+
// or if we are outside the component using virtual focus and focus is landing on something else? Maybe that doesn't matter since if we are landing on
78+
// an element with real focus outside the component using virtual focus, then activeElement === getEventTarget(e.nativeEvent) resolves as true
79+
if (e.target === e.currentTarget && (activeElement === getEventTarget(e.nativeEvent) || getVirtuallyFocusedElement(ownerDocument))) {
6980
if (onFocusProp) {
7081
onFocusProp(e);
7182
}

0 commit comments

Comments
 (0)