Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ index 39a24b8f2ccdc52739d130480ab18975073616cb..0c3f5199401c15b90230c25a02de364e
}
UI.clearInitialValue(el);
}
diff --git a/dist/cjs/event/behavior/keydown.js b/dist/cjs/event/behavior/keydown.js
index 55027cb256f66b808d17280dc01bc55a796a1032..993d5de5a838a711d7ae009344354772a42ed0c1 100644
--- a/dist/cjs/event/behavior/keydown.js
+++ b/dist/cjs/event/behavior/keydown.js
@@ -110,7 +110,7 @@ const keydownBehavior = {
},
Tab: (event, target, instance)=>{
return ()=>{
- const dest = getTabDestination.getTabDestination(target, instance.system.keyboard.modifiers.Shift);
+ const dest = getTabDestination.getTabDestination(document.activeElement, instance.system.keyboard.modifiers.Shift);
focus.focusElement(dest);
if (selection.hasOwnSelection(dest)) {
UI.setUISelection(dest, {
diff --git a/dist/cjs/utils/focus/getActiveElement.js b/dist/cjs/utils/focus/getActiveElement.js
index d25f3a8ef67e856e43614559f73012899c0b53d7..4ed9ee45565ed438ee9284d8d3043c0bd50463eb 100644
--- a/dist/cjs/utils/focus/getActiveElement.js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@ describe('useSearchAutocomplete', function () {

let {result} = renderHook((props) => useSearchAutocomplete(props, state.current), {initialProps: props});
let {inputProps} = result.current;
inputProps.onKeyDown(event({key: 'ArrowDown'}));
let input = document.createElement('input');
inputProps.onKeyDown(event({key: 'ArrowDown', target: input}));
expect(openSpy).toHaveBeenCalledTimes(1);
expect(openSpy).toHaveBeenLastCalledWith('first', 'manual');
expect(toggleSpy).toHaveBeenCalledTimes(0);
inputProps.onKeyDown(event({key: 'ArrowUp'}));
inputProps.onKeyDown(event({key: 'ArrowUp', target: input}));
expect(openSpy).toHaveBeenCalledTimes(2);
expect(openSpy).toHaveBeenLastCalledWith('last', 'manual');
expect(toggleSpy).toHaveBeenCalledTimes(0);
Expand Down
5 changes: 3 additions & 2 deletions packages/@react-aria/combobox/test/useComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ describe('useComboBox', function () {

let {result} = renderHook((props) => useComboBox(props, state.current), {initialProps: props});
let {inputProps, buttonProps} = result.current;
inputProps.onKeyDown(event({key: 'ArrowDown'}));
let input = document.createElement('input');
inputProps.onKeyDown(event({key: 'ArrowDown', target: input}));
expect(openSpy).toHaveBeenCalledTimes(1);
expect(openSpy).toHaveBeenLastCalledWith('first', 'manual');
expect(toggleSpy).toHaveBeenCalledTimes(0);
inputProps.onKeyDown(event({key: 'ArrowUp'}));
inputProps.onKeyDown(event({key: 'ArrowUp', target: input}));
expect(openSpy).toHaveBeenCalledTimes(2);
expect(openSpy).toHaveBeenLastCalledWith('last', 'manual');
expect(toggleSpy).toHaveBeenCalledTimes(0);
Expand Down
25 changes: 23 additions & 2 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,32 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
isDisabled: state.collection.size === 0
});

let onKeyDownCapture = (e: ReactKeyboardEvent) => {
let onKeyDown = (e: ReactKeyboardEvent) => {
if (!e.currentTarget.contains(e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !document.activeElement) {
return;
}

// TODO: keyboard handler should only stop propagation on keys we intend to handle, not ALL keys, that way we don't have to call continue then call stop else where but only *sometimes* depending on the order
// if (e.target !== ref.current && e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
// if (e.key === 'Tab' && ref.current.contains(e.target as Element)) {
// let cellWalker = getFocusableTreeWalker(ref.current, {tabbable: true});
// if (e.shiftKey) {
// cellWalker.currentNode = ref.current;
// let isFirstFocusable = cellWalker.firstChild() === e.target;
// if (!isFirstFocusable) {
// e.stopPropagation();
// }
// } else {
// cellWalker.currentNode = ref.current;
// let isLastFocusable = cellWalker.lastChild() === e.target;
// if (!isLastFocusable) {
// e.stopPropagation();
// }
// }
// }
// return;
// }

let walker = getFocusableTreeWalker(ref.current);
walker.currentNode = document.activeElement;

Expand Down Expand Up @@ -252,7 +273,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps

let gridCellProps: DOMAttributes = mergeProps(itemProps, {
role: 'gridcell',
onKeyDownCapture,
onKeyDown,
'aria-colspan': node.colSpan,
'aria-colindex': node.colIndex != null ? node.colIndex + 1 : undefined, // aria-colindex is 1-based
colSpan: isVirtualized ? undefined : node.colSpan,
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-aria/interactions/src/createEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export function createEventHandler<T extends SyntheticEvent>(handler?: (e: BaseE

let shouldStopPropagation = true;
return (e: T) => {
if ('continuePropagation' in e) {
handler(e as any);
return undefined;
}
let event: BaseEvent<T> = {
...e,
preventDefault() {
Expand All @@ -48,7 +52,7 @@ export function createEventHandler<T extends SyntheticEvent>(handler?: (e: BaseE

handler(event);

if (shouldStopPropagation) {
if (shouldStopPropagation && ('isPropagationStopped' in e && !e.isPropagationStopped())) {
e.stopPropagation();
}
};
Expand Down
6 changes: 6 additions & 0 deletions packages/@react-aria/menu/src/useMenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTrigge
}
// fallthrough
case 'ArrowDown':
if (e.key === 'ArrowDown' && !e.altKey && e.target.closest('[role="gridcell"]')) {
return;
}
// Stop propagation, unless it would already be handled by useKeyboard.
if (!('continuePropagation' in e)) {
e.stopPropagation();
Expand All @@ -85,6 +88,9 @@ export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTrigge
state.toggle('first');
break;
case 'ArrowUp':
if (e.key === 'ArrowUp' && !e.altKey && e.target.closest('[role="gridcell"]')) {
return;
}
if (!('continuePropagation' in e)) {
e.stopPropagation();
}
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-aria/searchfield/src/useSearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export function useSearchField(
e.continuePropagation();
} else {
e.preventDefault();
// by default textfield will continue this one because it doesn't do anything there, so we have to explicitly stop it here
e.stopPropagation();
state.setValue('');
if (onClear) {
onClear();
Expand Down
13 changes: 7 additions & 6 deletions packages/@react-aria/select/src/useSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {FocusEvent, useMemo} from 'react';
import {HiddenSelectProps} from './HiddenSelect';
import {ListKeyboardDelegate, useTypeSelect} from '@react-aria/selection';
import {SelectState} from '@react-stately/select';
import {setInteractionModality} from '@react-aria/interactions';
import {setInteractionModality, useKeyboard} from '@react-aria/interactions';
import {useCollator} from '@react-aria/i18n';
import {useField} from '@react-aria/label';
import {useMenuTrigger} from '@react-aria/menu';
Expand Down Expand Up @@ -136,9 +136,6 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>,
errorMessage: props.errorMessage || validationErrors
});

typeSelectProps.onKeyDown = typeSelectProps.onKeyDownCapture;
delete typeSelectProps.onKeyDownCapture;

let domProps = filterDOMProps(props, {labelable: true});
let triggerProps = mergeProps(typeSelectProps, menuTriggerProps, fieldProps);

Expand All @@ -152,6 +149,11 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>,
validationBehavior
});

let {keyboardProps} = useKeyboard({
onKeyDown: chain(triggerProps.onKeyDown, onKeyDown, props.onKeyDown),
onKeyUp: props.onKeyUp
});

return {
labelProps: {
...labelProps,
Expand All @@ -166,9 +168,8 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>,
},
triggerProps: mergeProps(domProps, {
...triggerProps,
...keyboardProps,
isDisabled,
onKeyDown: chain(triggerProps.onKeyDown, onKeyDown, props.onKeyDown),
onKeyUp: props.onKeyUp,
'aria-labelledby': [
valueId,
triggerProps['aria-labelledby'],
Expand Down
22 changes: 17 additions & 5 deletions packages/@react-aria/selection/src/useSelectableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
* governing permissions and limitations under the License.
*/

import {BaseEvent, DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, getActiveElement, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils';
import {dispatchVirtualFocus, getFocusableTreeWalker, moveVirtualFocus} from '@react-aria/focus';
import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared';
import {flushSync} from 'react-dom';
import {FocusEvent, KeyboardEvent, useEffect, useRef} from 'react';
import {focusSafely, getInteractionModality} from '@react-aria/interactions';
import {focusSafely, getInteractionModality, useKeyboard} from '@react-aria/interactions';
import {getItemElement, isNonContiguousSelectionModifier, useCollectionId} from './utils';
import {MultipleSelectionManager} from '@react-stately/selection';
import {useLocale} from '@react-aria/i18n';
Expand Down Expand Up @@ -126,7 +126,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
let {direction} = useLocale();
let router = useRouter();

let onKeyDown = (e: KeyboardEvent) => {
let onKeyDown = (e: BaseEvent<KeyboardEvent<any>>) => {
// Prevent option + tab from doing anything since it doesn't move focus to the cells, only buttons/checkboxes
if (e.altKey && e.key === 'Tab') {
e.preventDefault();
Expand All @@ -135,6 +135,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
// Keyboard events bubble through portals. Don't handle keyboard events
// for elements outside the collection (e.g. menus).
if (!ref.current?.contains(e.target as Element)) {
e.continuePropagation();
return;
}

Expand Down Expand Up @@ -286,9 +287,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
break;
case 'Escape':
if (escapeKeyBehavior === 'clearSelection' && !disallowEmptySelection && manager.selectedKeys.size !== 0) {
e.stopPropagation();
e.preventDefault();
manager.clearSelection();
} else {
e.continuePropagation();
}
break;
case 'Tab': {
Expand All @@ -314,11 +316,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions

if (next && !next.contains(document.activeElement)) {
focusWithoutScrolling(next);
} else {
e.continuePropagation();
}
}
break;
} else {
e.continuePropagation();
}
}
default:
e.continuePropagation();
}
};

Expand Down Expand Up @@ -560,7 +568,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
});

let handlers = {
onKeyDown,
onFocus,
onBlur,
onMouseDown(e) {
Expand All @@ -577,6 +584,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
selectionManager: manager
});

let {keyboardProps} = useKeyboard({
onKeyDown
});
handlers = mergeProps(handlers, keyboardProps);

if (!disallowTypeAhead) {
handlers = mergeProps(typeSelectProps, handlers);
}
Expand Down
57 changes: 44 additions & 13 deletions packages/@react-aria/selection/src/useTypeSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {DOMAttributes, Key, KeyboardDelegate} from '@react-types/shared';
import {KeyboardEvent, useRef} from 'react';
import {KeyboardEvent, useEffect, useRef} from 'react';
import {MultipleSelectionManager} from '@react-stately/selection';

/**
Expand Down Expand Up @@ -51,21 +51,45 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
timeout: undefined
}).current;

let onKeyDown = (e: KeyboardEvent) => {
let character = getStringForKey(e.key);
if (!character || e.ctrlKey || e.metaKey || !e.currentTarget.contains(e.target as HTMLElement) || (state.search.length === 0 && character === ' ')) {
return;
}

// Do not propagate the Spacebar event if it's meant to be part of the search.
// When we time out, the search term becomes empty, hence the check on length.
// Trimming is to account for the case of pressing the Spacebar more than once,
// which should cycle through the selection/deselection of the focused item.
if (character === ' ' && state.search.trim().length > 0) {
let onKeyDownCapture = (e: KeyboardEvent) => {
// if we're in the middle of a search, then a spacebar should be treated as a search and we should not propagate the event
// since we handle this one in a capture phase, we should ignore it in the bubble phase
if (state.search.length > 0 && e.key === ' ') {
e.preventDefault();
if (!('continuePropagation' in e)) {
e.stopPropagation();
}
state.search += ' ';

if (keyboardDelegate.getKeyForSearch != null) {
// Use the delegate to find a key to focus.
// Prioritize items after the currently focused item, falling back to searching the whole list.
let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);

// If no key found, search from the top.
if (key == null) {
key = keyboardDelegate.getKeyForSearch(state.search);
}

if (key != null) {
selectionManager.setFocusedKey(key);
if (onTypeSelect) {
onTypeSelect(key);
}
}
}
}

clearTimeout(state.timeout);
state.timeout = setTimeout(() => {
state.search = '';
}, TYPEAHEAD_DEBOUNCE_WAIT_MS);
};

let onKeyDown = (e: KeyboardEvent) => {
let character = getStringForKey(e.key);
if (!character || e.ctrlKey || e.metaKey || character === ' ' || !e.currentTarget.contains(e.target as HTMLElement)) {
return;
}

state.search += character;
Expand Down Expand Up @@ -94,11 +118,18 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
}, TYPEAHEAD_DEBOUNCE_WAIT_MS);
};

useEffect(() => {
return () => {
clearTimeout(state.timeout);
};
}, []);

return {
typeSelectProps: {
// Using a capturing listener to catch the keydown event before
// other hooks in order to handle the Spacebar event.
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDownCapture : undefined,
onKeyDown: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined
}
};
}
Expand Down
Loading