Skip to content

Commit 7a88537

Browse files
committed
fix so that Tab from combobox and toolbar propagate upwards so that you tab out of gridlist
1 parent e872ea8 commit 7a88537

5 files changed

Lines changed: 110 additions & 27 deletions

File tree

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

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,31 @@ export const GridListWithTextfield: GridListStory = args => {
10081008
</TextField>{' '}
10091009
<Button>Go</Button>
10101010
</MyGridListItem>
1011+
<MyGridListItem textValue="Combobox">
1012+
ComboBox
1013+
<ComboBox aria-label="combobox" allowsEmptyCollection>
1014+
<div style={{display: 'flex'}}>
1015+
<Input />
1016+
<Button>
1017+
<span aria-hidden="true" style={{padding: '0 2px'}}>
1018+
1019+
</span>
1020+
</Button>
1021+
</div>
1022+
<Popover>
1023+
<ListBox
1024+
renderEmptyState={comboboxEmptyState}
1025+
data-testid="combo-box-list-box"
1026+
className={styles.menu}
1027+
style={{width: 'var(--trigger-width)'}}>
1028+
<MyListBoxItem>Foo</MyListBoxItem>
1029+
<MyListBoxItem>Bar</MyListBoxItem>
1030+
<MyListBoxItem>Baz</MyListBoxItem>
1031+
<MyListBoxItem href="http://google.com">Google</MyListBoxItem>
1032+
</ListBox>
1033+
</Popover>
1034+
</ComboBox>
1035+
</MyGridListItem>
10111036
<MyGridListItem textValue="Toolbar">
10121037
Toolbar
10131038
<Toolbar aria-label="Text formatting" style={{gap: 4}}>
@@ -1077,31 +1102,6 @@ export const GridListWithTextfield: GridListStory = args => {
10771102
</Checkbox>
10781103
</CheckboxGroup>
10791104
</MyGridListItem>
1080-
<MyGridListItem textValue="Combobox">
1081-
ComboBox
1082-
<ComboBox aria-label="combobox" allowsEmptyCollection>
1083-
<div style={{display: 'flex'}}>
1084-
<Input />
1085-
<Button>
1086-
<span aria-hidden="true" style={{padding: '0 2px'}}>
1087-
1088-
</span>
1089-
</Button>
1090-
</div>
1091-
<Popover>
1092-
<ListBox
1093-
renderEmptyState={comboboxEmptyState}
1094-
data-testid="combo-box-list-box"
1095-
className={styles.menu}
1096-
style={{width: 'var(--trigger-width)'}}>
1097-
<MyListBoxItem>Foo</MyListBoxItem>
1098-
<MyListBoxItem>Bar</MyListBoxItem>
1099-
<MyListBoxItem>Baz</MyListBoxItem>
1100-
<MyListBoxItem href="http://google.com">Google</MyListBoxItem>
1101-
</ListBox>
1102-
</Popover>
1103-
</ComboBox>
1104-
</MyGridListItem>
11051105
</GridList>
11061106
<input aria-label="input after gridlist" />
11071107
</div>

packages/react-aria-components/test/GridList.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {Checkbox as AriaCheckbox, CheckboxButton, CheckboxField} from '../src/Checkbox';
2323
import {Button} from '../src/Button';
2424
import {Collection} from 'react-aria/Collection';
25+
import {ComboBox} from '../src/ComboBox';
2526
import {Dialog, DialogTrigger} from '../src/Dialog';
2627
import {DropIndicator, useDragAndDrop} from '../src/useDragAndDrop';
2728
import {getFocusableTreeWalker} from 'react-aria/private/focus/FocusScope';
@@ -33,13 +34,17 @@ import {
3334
GridListSection
3435
} from '../src/GridList';
3536
import {GridListLoadMoreItem} from '../src/GridList';
37+
import {Input} from '../src/Input';
3638
import {installPointerEvent, User} from '@react-aria/test-utils';
3739
import {Label} from '../src/Label';
40+
import {ListBox, ListBoxItem} from '../src/ListBox';
3841
import {ListLayout} from 'react-stately/useVirtualizerState';
3942
import {Modal} from '../src/Modal';
43+
import {Popover} from '../src/Popover';
4044
import React from 'react';
4145
import {RouterProvider} from 'react-aria/private/utils/openLink';
4246
import {Tag, TagGroup, TagList} from '../src/TagGroup';
47+
import {Toolbar} from '../src/Toolbar';
4348
import userEvent from '@testing-library/user-event';
4449
import {Virtualizer} from '../src/Virtualizer';
4550

@@ -2002,5 +2007,79 @@ describe('GridList', () => {
20022007
expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['item1']));
20032008
}
20042009
);
2010+
2011+
it.each([
2012+
['keyboardNavigationBehavior="tab"', {keyboardNavigationBehavior: 'tab'}],
2013+
['layout="grid"', {layout: 'grid'}]
2014+
])(
2015+
'should exit the grid when tabbing from a combobox or toolbar, not focus the next row (%s)',
2016+
async (_, listProps) => {
2017+
let {getByRole} = render(
2018+
<div>
2019+
<input aria-label="before" />
2020+
<GridList aria-label="Test" {...listProps}>
2021+
<GridListItem id="1" textValue="combobox">
2022+
<ComboBox aria-label="combobox">
2023+
<Input />
2024+
<Button></Button>
2025+
<Popover>
2026+
<ListBox>
2027+
<ListBoxItem>Foo</ListBoxItem>
2028+
<ListBoxItem>Bar</ListBoxItem>
2029+
</ListBox>
2030+
</Popover>
2031+
</ComboBox>
2032+
</GridListItem>
2033+
<GridListItem id="2" textValue="formatting">
2034+
<Toolbar aria-label="formatting">
2035+
<Button>Bold</Button>
2036+
<Button>Italic</Button>
2037+
</Toolbar>
2038+
</GridListItem>
2039+
<GridListItem id="3" textValue="plain input row">
2040+
<input aria-label="row 2 input" />
2041+
</GridListItem>
2042+
</GridList>
2043+
<input aria-label="after" />
2044+
</div>
2045+
);
2046+
2047+
let combobox = getByRole('combobox');
2048+
let afterInput = getByRole('textbox', {name: 'after'});
2049+
let boldButton = getByRole('button', {name: 'Bold'});
2050+
let row2Input = getByRole('textbox', {name: 'row 2 input'});
2051+
2052+
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
2053+
let rows = gridListTester.getRows();
2054+
2055+
await user.tab();
2056+
await user.tab();
2057+
await user.tab();
2058+
expect(document.activeElement).toBe(combobox);
2059+
2060+
await user.tab();
2061+
expect(document.activeElement).toBe(afterInput);
2062+
2063+
// note that shift tabbing move focus back to gridlist item not the combobox itself,
2064+
// will need to look into this later
2065+
await user.tab({shift: true});
2066+
expect(document.activeElement).toBe(rows[0]);
2067+
await user.keyboard(listProps.layout === 'grid' ? '{ArrowRight}' : '{ArrowDown}');
2068+
expect(document.activeElement).toBe(rows[1]);
2069+
await user.tab();
2070+
expect(document.activeElement).toBe(boldButton);
2071+
await user.tab();
2072+
expect(document.activeElement).toBe(afterInput);
2073+
2074+
await user.tab({shift: true});
2075+
expect(document.activeElement).toBe(rows[1]);
2076+
await user.keyboard(listProps.layout === 'grid' ? '{ArrowRight}' : '{ArrowDown}');
2077+
expect(document.activeElement).toBe(rows[2]);
2078+
await user.tab();
2079+
expect(document.activeElement).toBe(row2Input);
2080+
await user.tab();
2081+
expect(document.activeElement).toBe(afterInput);
2082+
}
2083+
);
20052084
});
20062085
});

packages/react-aria/src/combobox/useComboBox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(
223223
if (state.isOpen) {
224224
state.commit();
225225
}
226-
return {shouldPreventDefault: false};
226+
return {shouldPreventDefault: false, shouldContinuePropagation: true};
227227
},
228228
Escape: () => {
229229
let shouldContinuePropagation = false;

packages/react-aria/src/interactions/createEventHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export function createEventHandler<T extends SyntheticEvent>(
4545
},
4646
continuePropagation() {
4747
shouldStopPropagation = false;
48+
// nested createEventHandler might have set continue propagation so we should continue
49+
// propagation on wrappers
50+
if (typeof (e as any).continuePropagation === 'function') {
51+
(e as any).continuePropagation();
52+
}
4853
},
4954
isPropagationStopped() {
5055
return shouldStopPropagation;

packages/react-aria/src/toolbar/useToolbar.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ export function useToolbar(
8989
// out of the entire toolbar. To do this, move focus
9090
// to the first or last focusable child, and let the
9191
// browser handle the Tab key as usual from there.
92-
e.stopPropagation();
9392
lastFocused.current = getActiveElement() as HTMLElement;
9493
if (e.shiftKey) {
9594
focusManager.focusFirst();

0 commit comments

Comments
 (0)