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
7 changes: 6 additions & 1 deletion packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ import {
SelectableCollectionContextValue
} from './Autocomplete';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {FocusScope} from 'react-aria/FocusScope';
import {
FocusEvents,
forwardRefType,
GlobalDOMAttributes,
HoverEvents,
Expand All @@ -77,6 +77,7 @@ import {
PressEvents,
RefObject
} from '@react-types/shared';
import {FocusScope} from 'react-aria/FocusScope';
import {
HeaderNode,
ItemNode,
Expand Down Expand Up @@ -109,6 +110,7 @@ import {SelectionIndicatorContext} from './SelectionIndicator';
import {SharedElementTransition} from './SharedElementTransition';
import {TextContext} from './Text';
import {useCollator} from 'react-aria/useCollator';
import {useFocus} from 'react-aria/useFocus';
import {useFocusRing} from 'react-aria/useFocusRing';
import {useHover} from 'react-aria/useHover';
import {useLocale} from 'react-aria/I18nProvider';
Expand Down Expand Up @@ -486,6 +488,7 @@ export interface GridListItemProps<T = object>
LinkDOMProps,
HoverEvents,
PressEvents,
FocusEvents<HTMLDivElement>,
Omit<GlobalDOMAttributes<HTMLDivElement>, 'onClick'> {
/**
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
Expand Down Expand Up @@ -556,6 +559,7 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
onHoverEnd: item.props.onHoverEnd
});

let {focusProps: focusEventProps} = useFocus(props);
let {isFocusVisible, focusProps} = useFocusRing();
let {isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps} = useFocusRing({
within: true
Expand Down Expand Up @@ -650,6 +654,7 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
DOMProps,
renderProps,
rowProps,
focusEventProps,
focusProps,
focusWithinProps,
hoverProps,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-aria-components/stories/GridList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ export const GridListExample: GridListStory = args => {
);
};

export const ItemFocusEvents: GridListStory = args => (
<div>
<p>
Tab into the list, use the arrow keys, or click an item. Focus events appear in the Actions
panel.
</p>
<GridList {...args} aria-label="Item focus events" className={styles.menu} style={{width: 300}}>
{['Cat', 'Dog', 'Kangaroo'].map(name => (
<GridListItem
key={name}
id={name}
onFocus={action(name + ': onFocus')}
onBlur={action(name + ': onBlur')}
onFocusChange={action(name + ': onFocusChange')}
className={({isFocusVisible}) => classNames(styles, 'item', {focused: isFocusVisible})}>
{name}
</GridListItem>
))}
</GridList>
<Button>After the list</Button>
</div>
);

ItemFocusEvents.args = {selectionMode: 'single'};

export const MyGridListItem = (props: GridListItemProps) => {
return (
<GridListItem
Expand Down
115 changes: 115 additions & 0 deletions packages/react-aria-components/test/GridList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1940,6 +1940,121 @@ describe('GridList', () => {
});
});

describe('item focus events', () => {
it.each(['keyboard', 'mouse'])(
'should report item focus transitions using %s',
async interactionType => {
let onFocus = jest.fn();
let onBlur = jest.fn();
let onFocusChange = jest.fn();
let itemProps = {
onFocus: e => onFocus(e.target),
onBlur: e => onBlur(e.target),
onFocusChange
};
let {getAllByRole, getByRole} = render(
<>
<button>Before</button>
<GridList aria-label="Test" selectionMode="single">
<GridListItem id="cat" {...itemProps}>
Cat
</GridListItem>
<GridListItem id="dog" {...itemProps}>
Dog
</GridListItem>
</GridList>
<button>After</button>
</>
);
let rows = getAllByRole('row');

if (interactionType === 'keyboard') {
await user.tab();
await user.tab();
} else {
await user.click(rows[0]);
}
expect(document.activeElement).toBe(rows[0]);
expect(rows[0]).toHaveAttribute('data-focused', 'true');
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenLastCalledWith(rows[0]);
expect(onBlur).not.toHaveBeenCalled();
expect(onFocusChange.mock.calls).toEqual([[true]]);

if (interactionType === 'keyboard') {
expect(rows[0]).toHaveAttribute('data-focus-visible', 'true');
await user.keyboard('{ArrowDown}');
} else {
expect(rows[0]).not.toHaveAttribute('data-focus-visible');
await user.click(rows[1]);
}
Comment thread
snowystinger marked this conversation as resolved.
expect(document.activeElement).toBe(rows[1]);
expect(onFocus).toHaveBeenCalledTimes(2);
expect(onFocus).toHaveBeenLastCalledWith(rows[1]);
expect(onBlur).toHaveBeenCalledTimes(1);
expect(onBlur).toHaveBeenLastCalledWith(rows[0]);
expect(onFocusChange.mock.calls).toEqual([[true], [false], [true]]);

if (interactionType === 'keyboard') {
await user.tab();
} else {
await user.click(getByRole('button', {name: 'After'}));
}
expect(document.activeElement).toBe(getByRole('button', {name: 'After'}));
expect(onBlur).toHaveBeenCalledTimes(2);
expect(onBlur).toHaveBeenLastCalledWith(rows[1]);
expect(onFocusChange.mock.calls).toEqual([[true], [false], [true], [false]]);
}
);

it('should not report focus changes when a focused item rerenders', async () => {
let onFocusChange = jest.fn();
let example = label => (
<GridList aria-label="Test">
<GridListItem id="cat" onFocusChange={onFocusChange}>
{label}
</GridListItem>
</GridList>
);
let {rerender, getByRole} = render(example('Cat'));
await user.tab();
expect(onFocusChange.mock.calls).toEqual([[true]]);
rerender(example('Updated cat'));
expect(document.activeElement).toBe(getByRole('row'));
expect(onFocusChange.mock.calls).toEqual([[true]]);
});

it('should not report descendant focus as item focus', async () => {
let onFocus = jest.fn();
let onBlur = jest.fn();
let onFocusChange = jest.fn();
let {getByRole} = render(
<GridList aria-label="Test">
<GridListItem
textValue="Cat"
onFocus={onFocus}
onBlur={onBlur}
onFocusChange={onFocusChange}>
Cat <Button>First action</Button>
<Button>Second action</Button>
</GridListItem>
</GridList>
);
await user.tab();
expect(document.activeElement).toBe(getByRole('row'));
expect(onFocusChange.mock.calls).toEqual([[true]]);
await user.keyboard('{ArrowRight}');
expect(document.activeElement).toBe(getByRole('button', {name: 'First action'}));
expect(onFocusChange.mock.calls).toEqual([[true], [false]]);
await user.keyboard('{ArrowRight}');
expect(document.activeElement).toBe(getByRole('button', {name: 'Second action'}));
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onBlur).toHaveBeenCalledTimes(1);
expect(onFocusChange.mock.calls).toEqual([[true], [false]]);
expect(getByRole('row')).toHaveAttribute('data-focus-visible-within', 'true');
});
});

describe('press events', () => {
it.each`
interactionType
Expand Down
Loading