Skip to content

Commit 3bc8eea

Browse files
authored
fix: Select typeahead spaces (#8497)
1 parent 7bee51d commit 3bc8eea

File tree

4 files changed

+116
-7
lines changed

4 files changed

+116
-7
lines changed

packages/@react-aria/menu/src/useMenuTrigger.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTrigge
6868
switch (e.key) {
6969
case 'Enter':
7070
case ' ':
71-
if (trigger === 'longPress') {
71+
// React puts listeners on the same root, so even if propagation was stopped, immediate propagation is still possible.
72+
// useTypeSelect will handle the spacebar first if it's running, so we don't want to open if it's handled it already.
73+
// We use isDefaultPrevented() instead of isPropagationStopped() because createEventHandler stops propagation by default.
74+
// And default prevented means that the event was handled by something else (typeahead), so we don't want to open the menu.
75+
if (trigger === 'longPress' || e.isDefaultPrevented()) {
7276
return;
7377
}
7478
// fallthrough

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
5353

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

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,68 @@ export const SelectRenderProps = () => (
6666

6767
let manyItems = [...Array(100)].map((_, i) => ({id: i, name: `Item ${i}`}));
6868

69+
const usStateOptions = [
70+
{id: 'AL', name: 'Alabama'},
71+
{id: 'AK', name: 'Alaska'},
72+
{id: 'AS', name: 'American Samoa'},
73+
{id: 'AZ', name: 'Arizona'},
74+
{id: 'AR', name: 'Arkansas'},
75+
{id: 'CA', name: 'California'},
76+
{id: 'CO', name: 'Colorado'},
77+
{id: 'CT', name: 'Connecticut'},
78+
{id: 'DE', name: 'Delaware'},
79+
{id: 'DC', name: 'District Of Columbia'},
80+
{id: 'FM', name: 'Federated States Of Micronesia'},
81+
{id: 'FL', name: 'Florida'},
82+
{id: 'GA', name: 'Georgia'},
83+
{id: 'GU', name: 'Guam'},
84+
{id: 'HI', name: 'Hawaii'},
85+
{id: 'ID', name: 'Idaho'},
86+
{id: 'IL', name: 'Illinois'},
87+
{id: 'IN', name: 'Indiana'},
88+
{id: 'IA', name: 'Iowa'},
89+
{id: 'KS', name: 'Kansas'},
90+
{id: 'KY', name: 'Kentucky'},
91+
{id: 'LA', name: 'Louisiana'},
92+
{id: 'ME', name: 'Maine'},
93+
{id: 'MH', name: 'Marshall Islands'},
94+
{id: 'MD', name: 'Maryland'},
95+
{id: 'MA', name: 'Massachusetts'},
96+
{id: 'MI', name: 'Michigan'},
97+
{id: 'MN', name: 'Minnesota'},
98+
{id: 'MS', name: 'Mississippi'},
99+
{id: 'MO', name: 'Missouri'},
100+
{id: 'MT', name: 'Montana'},
101+
{id: 'NE', name: 'Nebraska'},
102+
{id: 'NV', name: 'Nevada'},
103+
{id: 'NH', name: 'New Hampshire'},
104+
{id: 'NJ', name: 'New Jersey'},
105+
{id: 'NM', name: 'New Mexico'},
106+
{id: 'NY', name: 'New York'},
107+
{id: 'NC', name: 'North Carolina'},
108+
{id: 'ND', name: 'North Dakota'},
109+
{id: 'MP', name: 'Northern Mariana Islands'},
110+
{id: 'OH', name: 'Ohio'},
111+
{id: 'OK', name: 'Oklahoma'},
112+
{id: 'OR', name: 'Oregon'},
113+
{id: 'PW', name: 'Palau'},
114+
{id: 'PA', name: 'Pennsylvania'},
115+
{id: 'PR', name: 'Puerto Rico'},
116+
{id: 'RI', name: 'Rhode Island'},
117+
{id: 'SC', name: 'South Carolina'},
118+
{id: 'SD', name: 'South Dakota'},
119+
{id: 'TN', name: 'Tennessee'},
120+
{id: 'TX', name: 'Texas'},
121+
{id: 'UT', name: 'Utah'},
122+
{id: 'VT', name: 'Vermont'},
123+
{id: 'VI', name: 'Virgin Islands'},
124+
{id: 'VA', name: 'Virginia'},
125+
{id: 'WA', name: 'Washington'},
126+
{id: 'WV', name: 'West Virginia'},
127+
{id: 'WI', name: 'Wisconsin'},
128+
{id: 'WY', name: 'Wyoming'}
129+
];
130+
69131
export const SelectManyItems = () => (
70132
<Select style={{position: 'relative'}}>
71133
<Label style={{display: 'block'}}>Test</Label>
@@ -77,7 +139,7 @@ export const SelectManyItems = () => (
77139
<OverlayArrow>
78140
<svg width={12} height={12}><path d="M0 0,L6 6,L12 0" /></svg>
79141
</OverlayArrow>
80-
<ListBox items={manyItems} className={styles.menu}>
142+
<ListBox items={usStateOptions} className={styles.menu}>
81143
{item => <MyListBoxItem>{item.name}</MyListBoxItem>}
82144
</ListBox>
83145
</Popover>

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

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,50 @@ describe('Select', () => {
368368
await selectTester.selectOption({option: 'Kangaroo'});
369369
expect(trigger).toHaveTextContent('Kangaroo');
370370
});
371-
371+
372+
describe('typeahead', () => {
373+
beforeEach(() => {
374+
jest.useFakeTimers();
375+
});
376+
377+
afterEach(() => {
378+
jest.useRealTimers();
379+
});
380+
381+
it('can select an option via typeahead', async function () {
382+
let {getByTestId} = render(
383+
<Select data-testid="select">
384+
<Label>Favorite Animal</Label>
385+
<Button>
386+
<SelectValue />
387+
</Button>
388+
<Text slot="description">Description</Text>
389+
<Text slot="errorMessage">Error</Text>
390+
<Popover>
391+
<ListBox>
392+
<ListBoxItem>Australian Capital Territory</ListBoxItem>
393+
<ListBoxItem>New South Wales</ListBoxItem>
394+
<ListBoxItem>Northern Territory</ListBoxItem>
395+
<ListBoxItem>Queensland</ListBoxItem>
396+
<ListBoxItem>South Australia</ListBoxItem>
397+
<ListBoxItem>Tasmania</ListBoxItem>
398+
<ListBoxItem>Victoria</ListBoxItem>
399+
<ListBoxItem>Western Australia</ListBoxItem>
400+
</ListBox>
401+
</Popover>
402+
</Select>
403+
);
404+
405+
let wrapper = getByTestId('select');
406+
await user.tab();
407+
await user.keyboard('Northern Terr');
408+
let selectTester = testUtilUser.createTester('Select', {root: wrapper, interactionType: 'keyboard'});
409+
let trigger = selectTester.trigger;
410+
expect(trigger).toHaveTextContent('Northern Territory');
411+
expect(trigger).not.toHaveAttribute('data-pressed');
412+
});
413+
});
414+
372415
it('should support autoFocus', () => {
373416
let {getByTestId} = render(<TestSelect autoFocus />);
374417
let selectTester = testUtilUser.createTester('Select', {
@@ -425,7 +468,7 @@ describe('Select', () => {
425468

426469
it('should not submit if required and selectedKey is null', async () => {
427470
const onSubmit = jest.fn().mockImplementation(e => e.preventDefault());
428-
471+
429472
function Test() {
430473
const [selectedKey, setSelectedKey] = React.useState(null);
431474
return (
@@ -444,13 +487,13 @@ describe('Select', () => {
444487
</Form>
445488
);
446489
}
447-
490+
448491
const {getByTestId} = render(<Test />);
449492
const wrapper = getByTestId('select');
450493
const selectTester = testUtilUser.createTester('Select', {root: wrapper});
451494
const trigger = selectTester.trigger;
452495
const submit = getByTestId('submit');
453-
496+
454497
expect(trigger).toHaveTextContent('Select an item');
455498
await selectTester.selectOption({option: 'Cat'});
456499
expect(trigger).toHaveTextContent('Cat');

0 commit comments

Comments
 (0)