Skip to content

Commit ca2d897

Browse files
authored
feat: focus loading indicator in rac tree (#8270)
1 parent 58122b1 commit ca2d897

File tree

5 files changed

+37
-25
lines changed

5 files changed

+37
-25
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
7878
let nextKey = key;
7979
while (nextKey != null) {
8080
let item = this.collection.getItem(nextKey);
81-
if (item?.type === 'item' && !this.isDisabled(item)) {
81+
if (item?.type === 'loader' || (item?.type === 'item' && !this.isDisabled(item))) {
8282
return nextKey;
8383
}
8484

packages/react-aria-components/example/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ html {
2323
}
2424
}
2525

26+
.tree-loader,
2627
.tree-item {
2728
padding: 4px 5px;
2829
outline: none;

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', <T extends o
699699
);
700700
});
701701

702-
export interface UNSTABLE_TreeLoadingIndicatorRenderProps {
702+
export interface UNSTABLE_TreeLoadingIndicatorRenderProps extends Pick<TreeItemRenderProps, 'isFocused' | 'isFocusVisible'> {
703703
/**
704704
* What level the tree item has within the tree.
705705
* @selector [data-level]
@@ -711,36 +711,44 @@ export interface TreeLoaderProps extends RenderProps<UNSTABLE_TreeLoadingIndicat
711711

712712
export const UNSTABLE_TreeLoadingIndicator = createLeafComponent('loader', function TreeLoader<T extends object>(props: TreeLoaderProps, ref: ForwardedRef<HTMLDivElement>, item: Node<T>) {
713713
let state = useContext(TreeStateContext)!;
714-
// This loader row is is non-interactable, but we want the same aria props calculated as a typical row
715-
// @ts-ignore
716-
let {rowProps} = useTreeItem({node: item}, state, ref);
714+
ref = useObjectRef<HTMLDivElement>(ref);
715+
let {rowProps, gridCellProps, ...states} = useTreeItem({node: item}, state, ref);
717716
let level = rowProps['aria-level'] || 1;
718717

719718
let ariaProps = {
719+
role: 'row',
720720
'aria-level': rowProps['aria-level'],
721721
'aria-posinset': rowProps['aria-posinset'],
722-
'aria-setsize': rowProps['aria-setsize']
722+
'aria-setsize': rowProps['aria-setsize'],
723+
tabIndex: rowProps.tabIndex
723724
};
724725

726+
let {isFocusVisible, focusProps} = useFocusRing();
727+
725728
let renderProps = useRenderProps({
726729
...props,
727730
id: undefined,
728731
children: item.rendered,
729732
defaultClassName: 'react-aria-TreeLoader',
730733
values: {
731-
level
734+
level,
735+
isFocused: states.isFocused,
736+
isFocusVisible
732737
}
733738
});
734739

735740
return (
736741
<>
737742
<div
738-
role="row"
739743
ref={ref}
740-
{...mergeProps(filterDOMProps(props as any), ariaProps)}
744+
{...mergeProps(filterDOMProps(props as any), ariaProps, focusProps)}
741745
{...renderProps}
746+
data-key={rowProps['data-key']}
747+
data-collection={rowProps['data-collection']}
748+
data-focused={states.isFocused || undefined}
749+
data-focus-visible={isFocusVisible || undefined}
742750
data-level={level}>
743-
<div role="gridcell" aria-colindex={1}>
751+
<div {...gridCellProps}>
744752
{renderProps.children}
745753
</div>
746754
</div>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ let rows = [
208208

209209
const MyTreeLoader = () => {
210210
return (
211-
<UNSTABLE_TreeLoadingIndicator>
211+
<UNSTABLE_TreeLoadingIndicator
212+
className={({isFocused, isFocusVisible}) => classNames(styles, 'tree-loader', {
213+
focused: isFocused,
214+
'focus-visible': isFocusVisible
215+
})}>
212216
{({level}) => {
213217
let message = `Level ${level} loading spinner`;
214218
if (level === 1) {

packages/react-aria-components/test/Tree.test.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ describe('Tree', () => {
11241124
expect(cell).toHaveAttribute('aria-colindex', '1');
11251125
});
11261126

1127-
it('should not focus the load more row when using ArrowDown/ArrowUp', async () => {
1127+
it('should focus the load more row when using ArrowDown/ArrowUp', async () => {
11281128
let {getAllByRole} = render(<LoadingMoreTree isLoading />);
11291129

11301130
let rows = getAllByRole('row');
@@ -1133,19 +1133,18 @@ describe('Tree', () => {
11331133

11341134
await user.tab();
11351135
expect(document.activeElement).toBe(rows[0]);
1136-
for (let i = 0; i < 5; i++) {
1136+
for (let i = 1; i < 8; i++) {
11371137
await user.keyboard('{ArrowDown}');
1138+
expect(document.activeElement).toBe(rows[i]);
11381139
}
1139-
expect(document.activeElement).toBe(rows[5]);
11401140

1141-
await user.keyboard('{ArrowDown}');
1142-
expect(document.activeElement).toBe(rows[7]);
1143-
1144-
await user.keyboard('{ArrowUp}');
1145-
expect(document.activeElement).toBe(rows[5]);
1141+
for (let i = 6; i >= 0; i--) {
1142+
await user.keyboard('{ArrowUp}');
1143+
expect(document.activeElement).toBe(rows[i]);
1144+
}
11461145
});
11471146

1148-
it('should not focus the load more row when using End', async () => {
1147+
it('should focus the load more row when using End', async () => {
11491148
let {getAllByRole} = render(<LoadingMoreTree isLoading />);
11501149

11511150
let rows = getAllByRole('row');
@@ -1155,14 +1154,14 @@ describe('Tree', () => {
11551154
await user.tab();
11561155
expect(document.activeElement).toBe(rows[0]);
11571156
await user.keyboard('{End}');
1158-
expect(document.activeElement).toBe(rows[20]);
1157+
expect(document.activeElement).toBe(rows[21]);
11591158

11601159
// Check that it didn't shift the focusedkey to the loader key even if DOM focus didn't shift to the loader
11611160
await user.keyboard('{ArrowUp}');
1162-
expect(document.activeElement).toBe(rows[19]);
1161+
expect(document.activeElement).toBe(rows[20]);
11631162
});
11641163

1165-
it('should not focus the load more row when using PageDown', async () => {
1164+
it('should focus the load more row when using PageDown', async () => {
11661165
let {getAllByRole} = render(<LoadingMoreTree isLoading />);
11671166

11681167
let rows = getAllByRole('row');
@@ -1172,11 +1171,11 @@ describe('Tree', () => {
11721171
await user.tab();
11731172
expect(document.activeElement).toBe(rows[0]);
11741173
await user.keyboard('{PageDown}');
1175-
expect(document.activeElement).toBe(rows[20]);
1174+
expect(document.activeElement).toBe(rows[21]);
11761175

11771176
// Check that it didn't shift the focusedkey to the loader key even if DOM focus didn't shift to the loader
11781177
await user.keyboard('{ArrowUp}');
1179-
expect(document.activeElement).toBe(rows[19]);
1178+
expect(document.activeElement).toBe(rows[20]);
11801179
});
11811180

11821181
it('should not render no results state and the loader at the same time', () => {

0 commit comments

Comments
 (0)