-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix Autocomplete focus styles when clicking the input after virtual focus #9767
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, m | |
| import {getInteractionModality, getPointerType} from '@react-aria/interactions'; | ||
| // @ts-ignore | ||
| import intlMessages from '../intl/*.json'; | ||
| import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {useLocalizedStringFormatter} from '@react-aria/i18n'; | ||
|
|
||
| export interface CollectionOptions extends DOMProps, AriaLabelingProps { | ||
|
|
@@ -420,6 +420,19 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut | |
| } | ||
| }; | ||
|
|
||
| // Clicking an already-focused input won't emit a new focus event, so clear virtual focus | ||
| // on pointer down to restore the input's focused styling before the click completes. | ||
| // Touch is excluded because touch interactions should not move focus back to the input. | ||
| let onPointerDown = (e: ReactPointerEvent) => { | ||
| if (e.button !== 0 || e.pointerType === 'touch' || queuedActiveDescendant.current == null || inputRef.current == null) { | ||
| return; | ||
| } | ||
|
|
||
| if (getEventTarget(e) === inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) === inputRef.current) { | ||
|
||
| clearVirtualFocus(); | ||
| } | ||
| }; | ||
|
|
||
| // Only apply the autocomplete specific behaviors if the collection component wrapped by it is actually | ||
| // being filtered/allows filtering by the Autocomplete. | ||
| let inputProps = { | ||
|
|
@@ -431,7 +444,8 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut | |
| onKeyDown, | ||
| 'aria-activedescendant': state.focusedNodeId ?? undefined, | ||
| onBlur, | ||
| onFocus | ||
| onFocus, | ||
| onPointerDown | ||
| }; | ||
|
|
||
| if (hasCollection) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -496,6 +496,68 @@ describe('Autocomplete', () => { | |||
| expect(input).toHaveAttribute('data-focus-visible'); | ||||
| }); | ||||
|
|
||||
| it('should restore focused styles to the input when clicking it after hovering an option', async () => { | ||||
| let {getByRole} = render( | ||||
| <AutocompleteWrapper> | ||||
| <StaticMenu /> | ||||
| </AutocompleteWrapper> | ||||
| ); | ||||
|
|
||||
| let input = getByRole('searchbox'); | ||||
| await user.click(input); | ||||
| expect(document.activeElement).toBe(input); | ||||
| expect(input).toHaveAttribute('data-focused'); | ||||
|
|
||||
| let menu = getByRole('menu'); | ||||
| let options = within(menu).getAllByRole('menuitem'); | ||||
| await user.hover(options[1]); | ||||
| options = within(menu).getAllByRole('menuitem'); | ||||
|
||||
| options = within(menu).getAllByRole('menuitem'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also removed that extra re-fetch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is touch excluded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. I kept it excluded because removing that guard changes touch behavior too - re-tapping would start clearing virtual focus on touch as well, and I wasn't sure that was intended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into making the return-to-input behavior consistently clear, but it affects more than this PR's original scope (virtual focus restoration, submenu/subdialog cases, etc).
Would it make sense to keep this PR focused on the original focus recovery / styling issue, and tackle the broader preserve-vs-clear question separately? I can follow up in either direction once the intended behavior is clear.