Skip to content

fix: Clear Autocomplete virtualFocus upon paste/undo/redo and other focus fixes #8438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

LFDanLu
Copy link
Member

@LFDanLu LFDanLu commented Jun 24, 2025

Closes RSP Component Milestones (view)

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

Go to the RAC autocomplete stories and try typing/backspacing/cut/paste/redo/undo operations. Only typing forward should autofocus the first item in the list, the rest should clear virtual focus. Sanity check announcements, most should not be interrupted since we clear virtual focus so you should be able to hear what you've just typed/pasted/etc

🧢 Your Project:

RSP

@rspbot
Copy link

rspbot commented Jun 24, 2025

@rspbot
Copy link

rspbot commented Jun 25, 2025

@@ -419,7 +418,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions

// If no focusable items exist in the list, make sure to clear any activedescendant that may still exist
if (keyToFocus == null) {
let previousActiveElement = getActiveElement();
// TODO: Bit gross, but we need the first moveVirtualFocus to clear the previous aria-activeDescendant, then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't quite follow, what's going on here? and what are the repercussions across other collections?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, I'll add a more illustrative comment once I confirm this is the only way to do this. What this is handling is the case where the user types into the autocomplete input -> tells the filtered collection to try to virtually focus its first key -> the filtered collection filters itself and ends up in a state where there are no focusable items (aka only disabled items/no matching items at all) -> it needs to then tell the autocomplete input to clear its old aria-activeDescendant and that the input should be focused instead.

I was hoping to remove moveVirtualFocus(ref.current) in favor of the dispatchVirtualFocus below since it feels weird to do two separate virtual focus move operation but the autocomplete input will only clear its focused node when it detects that a focusin on the collection itself

This shouldn't have an repercussions for other standalone collections because it only runs when FOCUS_EVENT is fired from useAutocomplete

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very helpful, thank you for explaining again <3

@LFDanLu LFDanLu changed the title fix: Clear Autocomplete virtualFocus upon paste/undo/redo fix: Clear Autocomplete virtualFocus upon paste/undo/redo and other focus fixes Jun 26, 2025
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only has cut/paste, undo and redo didn't seem to fire properly

@rspbot
Copy link

rspbot commented Jun 26, 2025

Comment on lines +502 to +503
// If 500ms hasn't elapsed the aria-activedecendant hasn't been updated
act(() => jest.advanceTimersByTime(300));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500 v 300?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, if I advance more than 500ms, the aria-activedescendant will be set which is the typical flow tested by the other tests via runAllTimers. The buggy flow was if the user then backspaces before that 500ms finishes resulting in a state where the input "blurred" away from itself, but then interrupts updating the activedescendant, resulting in a state where getVirtuallyFocusedElement doesn't return a value and moveVirtualFocus will think that virtual focus didn't move away from the input.

LFDanLu and others added 2 commits June 27, 2025 15:24
@rspbot
Copy link

rspbot commented Jun 27, 2025

@rspbot
Copy link

rspbot commented Jun 27, 2025

## API Changes

react-aria-components

/react-aria-components:useContextProps

 useContextProps <E extends Element, T, U extends SlotProps> {
   props: (T & SlotProps)
-  ref: ForwardedRef<E> | undefined
+  ref: ForwardedRef<E>
   context: Context<ContextValue<U, E>>
   returnVal: undefined
 }

/react-aria-components:TabPanel

 TabPanel {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ChildrenOrFunction<TabPanelRenderProps>
   className?: ClassNameOrFunction<TabPanelRenderProps>
-  id?: Key
+  id?: string
   shouldForceMount?: boolean = false
   style?: StyleOrFunction<TabPanelRenderProps>
 }

/react-aria-components:TabPanelProps

 TabPanelProps {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ChildrenOrFunction<TabPanelRenderProps>
   className?: ClassNameOrFunction<TabPanelRenderProps>
-  id?: Key
+  id?: string
   shouldForceMount?: boolean = false
   style?: StyleOrFunction<TabPanelRenderProps>
 }

@react-aria/tabs

/@react-aria/tabs:AriaTabPanelProps

 AriaTabPanelProps {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
-  id?: Key
+  id?: string
 }

@react-spectrum/s2

/@react-spectrum/s2:TabPanel

 TabPanel {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children: ReactNode
-  id?: Key
+  id?: string
   shouldForceMount?: boolean = false
   styles?: StylesPropWithHeight
 }

/@react-spectrum/s2:TabPanelProps

 TabPanelProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children: ReactNode
-  id?: Key
+  id?: string
   shouldForceMount?: boolean = false
   styles?: StylesPropWithHeight
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants