Skip to content
Merged
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
27 changes: 26 additions & 1 deletion packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
}

let focusedNodeId = queuedActiveDescendant.current;
if (focusedNodeId !== null && getOwnerDocument(inputRef.current).getElementById(focusedNodeId) == null) {
// if the focused id doesn't exist in document, then we need to clear the tracked focused node, otherwise
// we will be attempting to fire key events on a non-existing node instead of trying to focus the newly swapped wrapped collection.
// This can happen if you are swapping out the Autocomplete wrapped collection component like in the docs search.
queuedActiveDescendant.current = null;
focusedNodeId = null;
}

switch (e.key) {
case 'a':
if (isCtrlKeyPressed(e)) {
Expand Down Expand Up @@ -260,11 +268,28 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
case 'PageDown':
case 'PageUp':
case 'ArrowUp':
case 'ArrowDown': {
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft': {
if ((e.key === 'Home' || e.key === 'End') && focusedNodeId == null && e.shiftKey) {
return;
}

// If there is text within the input field, we'll want continue propagating events down
// to the wrapped collection if there is a focused node so that a user can continue moving the
// virtual focus. However, if the user doesn't have a focus in the collection, just move the text
// cursor instead. They can move focus down into the collection via down/up arrow if need be
if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && state.inputValue.length > 0) {
if (focusedNodeId == null) {
if (!e.isPropagationStopped()) {
e.stopPropagation();
}
return;
}

break;
}

// Prevent these keys from moving the text cursor in the input
e.preventDefault();
// Move virtual focus into the wrapped collection
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/selection/src/useSelectableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
}
case 'ArrowLeft': {
if (delegate.getKeyLeftOf) {
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : null;
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : delegate.getFirstKey?.();
if (nextKey == null && shouldFocusWrap) {
nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey);
}
Expand All @@ -214,7 +214,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
}
case 'ArrowRight': {
if (delegate.getKeyRightOf) {
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : null;
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : delegate.getFirstKey?.();
if (nextKey == null && shouldFocusWrap) {
nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/tag/src/useTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {useGridListItem} from '@react-aria/gridlist';
import {useLocalizedStringFormatter} from '@react-aria/i18n';


export interface TagAria extends Omit<SelectableItemStates, 'hasAction'> {
export interface TagAria extends SelectableItemStates {
/** Props for the tag row element. */
rowProps: DOMAttributes,
/** Props for the tag cell element. */
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export const Tag = /*#__PURE__*/ createLeafComponent(ItemNode, (props: TagProps,
let {rowProps, gridCellProps, removeButtonProps, ...states} = useTag({item}, state, ref);

let {hoverProps, isHovered} = useHover({
isDisabled: !states.allowsSelection,
isDisabled: !states.allowsSelection && !states.hasAction,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
Expand Down
36 changes: 36 additions & 0 deletions packages/react-aria-components/stories/Autocomplete.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1208,3 +1208,39 @@ export const AutocompleteUserCustomFiltering: AutocompleteStory = {
}
}
};

export function AutocompleteGrid() {
return (
<AutocompleteWrapper>
<div>
<TextField autoFocus data-testid="autocomplete-example">
<Label style={{display: 'block'}}>Test</Label>
<Input />
<Text style={{display: 'block'}} slot="description">Please select an option below.</Text>
</TextField>
<ListBox
className={styles.menu}
aria-label="test listbox"
layout="grid"
orientation="vertical"
style={{
width: 300,
height: 300,
display: 'grid',
gridTemplate: 'repeat(3, 1fr) / repeat(3, 1fr)',
gridAutoFlow: 'row'
}}>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>1,1</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>1,2</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>1,3</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>2,1</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>2,2</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>2,3</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>3,1</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>3,2</MyListBoxItem>
<MyListBoxItem style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>3,3</MyListBoxItem>
</ListBox>
</div>
</AutocompleteWrapper>
);
};
25 changes: 24 additions & 1 deletion packages/react-aria-components/test/TagGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ describe('TagGroup', () => {
expect(onHoverEnd).not.toHaveBeenCalled();
});

it('should show hover state when tag has an href even without selectionMode', async () => {
let onHoverStart = jest.fn();
let onHoverChange = jest.fn();
let onHoverEnd = jest.fn();
let {getAllByRole} = renderTagGroup({}, {}, {href: '/', className: ({isHovered}) => isHovered ? 'hover' : '', onHoverStart, onHoverChange, onHoverEnd});
let row = getAllByRole('row')[0];

expect(row).not.toHaveAttribute('data-hovered');
expect(row).not.toHaveClass('hover');

await user.hover(row);
expect(row).toHaveAttribute('data-hovered', 'true');
expect(row).toHaveClass('hover');
expect(onHoverStart).toHaveBeenCalledTimes(1);
expect(onHoverChange).toHaveBeenCalledTimes(1);

await user.unhover(row);
expect(row).not.toHaveAttribute('data-hovered');
expect(row).not.toHaveClass('hover');
expect(onHoverEnd).toHaveBeenCalledTimes(1);
expect(onHoverChange).toHaveBeenCalledTimes(2);
});

it('should support focus ring', async () => {
let onFocus = jest.fn();
let onFocusChange = jest.fn();
Expand Down Expand Up @@ -588,7 +611,7 @@ describe('TagGroup', () => {
});

describe('press events', () => {
it.only.each`
it.each`
interactionType
${'mouse'}
${'keyboard'}
Expand Down
Loading