From e1ed6fe7e426ca2effaa7df4ad766f8c1134588e Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2025 15:15:41 +1100 Subject: [PATCH 1/5] fix: Autocomplete 'tab' key forwarding --- .../autocomplete/src/useAutocomplete.ts | 11 ++ .../stories/Autocomplete.stories.tsx | 83 ++++++++++- .../test/Autocomplete.test.tsx | 132 +++++++++++++++++- 3 files changed, 224 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index 2ebc52d200c..df26ebaa624 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -201,6 +201,17 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state: // But what about wrapped grids where ArrowLeft and ArrowRight should navigate left/right clearVirtualFocus(); break; + case 'Tab': + // Moving forward will always go into the collection which will handle Tab itself, prevent the browser's default tab behavior so both don't happen + if (!e.shiftKey) { + e.preventDefault(); + } + // Moving backwards shouldn't only be handled by the browser's default tab behavior + if (e.shiftKey) { + e.continuePropagation(); + return; + } + break; } // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter diff --git a/packages/react-aria-components/stories/Autocomplete.stories.tsx b/packages/react-aria-components/stories/Autocomplete.stories.tsx index 4307c4c014c..3dacd279071 100644 --- a/packages/react-aria-components/stories/Autocomplete.stories.tsx +++ b/packages/react-aria-components/stories/Autocomplete.stories.tsx @@ -11,7 +11,7 @@ */ import {action} from '@storybook/addon-actions'; -import {UNSTABLE_Autocomplete as Autocomplete, Button, Collection, Dialog, DialogTrigger, Header, Input, Keyboard, Label, ListBox, ListBoxSection, UNSTABLE_ListLayout as ListLayout, Menu, MenuSection, MenuTrigger, Popover, SearchField, Separator, Text, TextField, UNSTABLE_Virtualizer as Virtualizer} from 'react-aria-components'; +import {UNSTABLE_Autocomplete as Autocomplete, Button, Collection, Dialog, DialogTrigger, Header, Input, Keyboard, Label, ListBox, ListBoxSection, UNSTABLE_ListLayout as ListLayout, Menu, MenuItem, MenuSection, MenuTrigger, Popover, SearchField, Separator, Text, TextField, UNSTABLE_Virtualizer as Virtualizer} from 'react-aria-components'; import {MyListBoxItem, MyMenuItem} from './utils'; import React, {useMemo} from 'react'; import styles from '../example/index.css'; @@ -517,3 +517,84 @@ export const AutocompleteInPopoverDialogTrigger = { } } }; + +const MyMenu = () => { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + + + + + + + + + + + + console.log('open')}>Open + console.log('rename')}> + Rename… + + console.log('duplicate')}> + Duplicate + + console.log('share')}>Share… + console.log('delete')}> + Delete… + + + + + + + ); +}; + +const MyMenu2 = () => { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + + + + + + + + + + console.log('open')}>Open + console.log('rename')}> + Rename… + + console.log('duplicate')}> + Duplicate + + console.log('share')}>Share… + console.log('delete')}> + Delete… + + + + + + + + + ); +}; + +export function App() { + return ( +
+ +
+ + +
+ +
+ ); +} diff --git a/packages/react-aria-components/test/Autocomplete.test.tsx b/packages/react-aria-components/test/Autocomplete.test.tsx index c3ff2092c26..3ed788c59e5 100644 --- a/packages/react-aria-components/test/Autocomplete.test.tsx +++ b/packages/react-aria-components/test/Autocomplete.test.tsx @@ -11,7 +11,7 @@ */ import {AriaAutocompleteTests} from './AriaAutocomplete.test-util'; -import {Button, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Menu, MenuItem, MenuSection, SearchField, Separator, Text, UNSTABLE_Autocomplete} from '..'; +import {Button, Dialog, DialogTrigger, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Menu, MenuItem, MenuSection, Popover, SearchField, Separator, Text, TextField, UNSTABLE_Autocomplete} from '..'; import {pointerMap, render, within} from '@react-spectrum/test-utils-internal'; import React, {ReactNode} from 'react'; import {useAsyncList} from 'react-stately'; @@ -244,6 +244,136 @@ describe('Autocomplete', () => { expect(options[1]).toHaveAttribute('data-focused'); expect(options[1]).not.toHaveAttribute('data-focus-visible'); }); + + it('should be able to tab inside a focus scope that contains', async () => { + const MyMenu = () => { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + + + + + + + + + + + + Open + + Rename… + + + Duplicate + + + + + + + ); + }; + + function App() { + return ( +
+ +
+ +
+ +
+ ); + } + + let {getByRole} = render(); + let trigger = getByRole('button', {name: 'Menu'}); + await user.click(trigger); + let firstButton = getByRole('button', {name: 'First'}); + let secondButton = getByRole('button', {name: 'Second'}); + let input = getByRole('textbox'); + + expect(document.activeElement).toBe(input); + + await user.tab(); + + expect(document.activeElement).toBe(firstButton); + + await user.tab({shift: true}); + + expect(document.activeElement).toBe(input); + + await user.tab({shift: true}); + + expect(document.activeElement).toBe(secondButton); + }); + + it('should be able to tab inside a focus scope that contains', async () => { + const MyMenu = () => { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + + + + + + + + + + Open + + Rename… + + + Duplicate + + + + + + + + + ); + }; + + function App() { + return ( +
+ +
+ +
+ +
+ ); + } + + let {getByRole} = render(); + let trigger = getByRole('button', {name: 'Menu'}); + await user.click(trigger); + let firstButton = getByRole('button', {name: 'First'}); + let secondButton = getByRole('button', {name: 'Second'}); + let input = getByRole('textbox'); + + expect(document.activeElement).toBe(input); + + await user.tab(); + + expect(document.activeElement).toBe(firstButton); + + await user.tab({shift: true}); + + expect(document.activeElement).toBe(input); + + await user.tab({shift: true}); + + expect(document.activeElement).toBe(secondButton); + }); }); AriaAutocompleteTests({ From d754eaf7d3e203b0a892d800f85e9a0521aa5952 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2025 15:52:48 +1100 Subject: [PATCH 2/5] fix react 16 compatibility --- packages/@react-aria/autocomplete/src/useAutocomplete.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index df26ebaa624..ddf086560a2 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -204,6 +204,7 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state: case 'Tab': // Moving forward will always go into the collection which will handle Tab itself, prevent the browser's default tab behavior so both don't happen if (!e.shiftKey) { + e.nativeEvent.stopImmediatePropagation(); // react 16 compat due to where it listens to the events e.preventDefault(); } // Moving backwards shouldn't only be handled by the browser's default tab behavior From 0e943c3395e0240498d916ffb4d0367cd257926b Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 5 Feb 2025 15:55:53 +1100 Subject: [PATCH 3/5] make better story name --- packages/react-aria-components/stories/Autocomplete.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/stories/Autocomplete.stories.tsx b/packages/react-aria-components/stories/Autocomplete.stories.tsx index 3dacd279071..fa9dc36c769 100644 --- a/packages/react-aria-components/stories/Autocomplete.stories.tsx +++ b/packages/react-aria-components/stories/Autocomplete.stories.tsx @@ -586,7 +586,7 @@ const MyMenu2 = () => { ); }; -export function App() { +export function AutocompleteWithExtraButtons() { return (
From b73d35d53b190e3172adb1465ab2915250ef842d Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 7 Feb 2025 11:24:57 +1100 Subject: [PATCH 4/5] use daniel's fix --- .../autocomplete/src/useAutocomplete.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index ddf086560a2..3df6edcfb84 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -171,7 +171,13 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state: break; case ' ': // Space shouldn't trigger onAction so early return. - + return; + case 'Tab': + // Don't propogate Tab down to the collection, otherwise we will try to focus the collection via useSelectableCollection's Tab handler (aka shift tab logic) + // We want FocusScope to handle Tab if one exists (aka sub dialog), so special casepropogate + if ('continuePropagation' in e) { + e.continuePropagation(); + } return; case 'Home': case 'End': @@ -201,18 +207,6 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state: // But what about wrapped grids where ArrowLeft and ArrowRight should navigate left/right clearVirtualFocus(); break; - case 'Tab': - // Moving forward will always go into the collection which will handle Tab itself, prevent the browser's default tab behavior so both don't happen - if (!e.shiftKey) { - e.nativeEvent.stopImmediatePropagation(); // react 16 compat due to where it listens to the events - e.preventDefault(); - } - // Moving backwards shouldn't only be handled by the browser's default tab behavior - if (e.shiftKey) { - e.continuePropagation(); - return; - } - break; } // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter From 4d5eaec3b860a3d88320504c04489591f84abbae Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 11 Feb 2025 07:59:56 +1100 Subject: [PATCH 5/5] Update packages/react-aria-components/test/Autocomplete.test.tsx --- packages/react-aria-components/test/Autocomplete.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/test/Autocomplete.test.tsx b/packages/react-aria-components/test/Autocomplete.test.tsx index 3ed788c59e5..3dd5858706a 100644 --- a/packages/react-aria-components/test/Autocomplete.test.tsx +++ b/packages/react-aria-components/test/Autocomplete.test.tsx @@ -310,7 +310,7 @@ describe('Autocomplete', () => { expect(document.activeElement).toBe(secondButton); }); - it('should be able to tab inside a focus scope that contains', async () => { + it('should be able to tab inside a focus scope that contains with buttons after the autocomplete', async () => { const MyMenu = () => { let {contains} = useFilter({sensitivity: 'base'});