-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
(WIP) Autocomplete with menu #7181
base: main
Are you sure you want to change the base?
Conversation
issues outlined in various comments, basically boils down to ideally using the wrapped collection components state
ended up going with dispatching events to the menu/menuitem so we can piggyback off of useSelectableCollection and menus press handling for submenutriggers, onAction, and link handling
// TODO: how best to trigger the focused element's action? Currently having the registered callback handle dispatching a | ||
// keyboard event | ||
switch (e.key) { |
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.
At the moment the keyboard event dispatch handling happens in Menu directly, but it might be good to have those in a hook somewhere. Its a bit tough since we don't have access to the wrapped collection and collection ref at the top level, but potentially could just have the callbackRef expect to receive the collection and collection ref instead of just the id, open to opinions here
// TODO: note that the searchbox role causes newly typed letters to interrupt the announcement of the number of available options in Safari. | ||
// I tested on iPad/Android/etc and the combobox role doesn't seem to do that but it will announce that there is a listbox which isn't true | ||
// and it will say press Control Option Space to display a list of options which is also incorrect. To be fair though, our combobox doesn't open with | ||
// that combination of buttons | ||
role: 'searchbox', |
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.
something to note with the announcements, hopefully this won't be a problem as browsers/screenreaders begin to handle announcing of aria-activedescendant properly
onHoverStart: (e) => { | ||
// TODO: another thing to think about, what is the best way to past this to menu/wrapped collection component so that hovering on | ||
// a item also updates the focusedNode. Perhaps we should just pass down setFocusedNodeId instead | ||
state.setFocusedNodeId(e.target.id); | ||
} |
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.
Is this weird also? Focus on hover happens in the menu (and future other wrapped collections) and thus needs to communicate this change upwards. This felt better that having the menu hooks accept setFocusedNodeId
// TODO: this is pretty specific to menu, will need to check if it is generic enough | ||
// Will need to handle varying levels I assume but will revisit after I get searchable menu working for base menu | ||
// TODO: an alternative is to simply walk the collection and add all item nodes that match the filter and any sections/separators we encounter | ||
// to an array, then walk that new array and fix all the next/Prev keys while adding them to the new collection | ||
filter(filterFn: (nodeValue: string) => boolean): BaseCollection<T> { |
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.
Definitely specific to menu, will need to update when we handle other collection components. Collections is in alpha so that should shield us from any changes that may need to happen to this function
Is there maybe some document explaining the motivation for this new component and what it's going to be? |
@romansndlr nothing that I can quite share at the moment, but essentially this will support the ability to filter a menu in the same fashion that a combobox does (virtual focus so the user remains in the input), but without needing the menu to be within a separate popover (like Spotlight search on macOS). Still early stages for it but we hope that the wrapping Autocomplete will eventually be able to take any kind of collection component (e.g. gridlist, table, etc) rather than just a menu. |
for now we will have first item auto focus, no custom announcements, and clear focus on backspace or arrow left/right. See https://docs.google.com/spreadsheets/d/12M--aeeNK4Kruzg8GnO7L-_DUsQE29_bWJzu4yI4-UA/edit?gid=1690380375#gid=1690380375 for a testing matrix
complications making the custom announcements for safari have a delay for when typing ahead but not when using the arrow keys to move through the options, so stashed it for now
@LFDanLu Is it correct to assume that since this component appears to use combobox as the base, much of the combobox functionality will be retained? Of particular interest is the ability to categorize the search results as described in this discussion topic: #7308 Also, is there a very rough estimate on when this feature might make it into a release? And is it targeting S1, S2 or both? |
@cheema-corellian The search/filtering capabilities of combobox along with the same kind of "virtual" focus interactions will be part of autocomplete, yes. The experience in #7308 should be possible with this component I imagine, barring the additional "Add new employee" item at the bottom which will need some additional handling. I hope to have this feature out within the next month, at least in an alpha form in React Aria Components but additional work needs to be done for submenu support and supporting any generic collection rather than just menu. We will for sure target S2 once it stabilizes in React Aria Components, but not sure about S1 to be honest. |
…ual focus (#7378) * get clear and focus first working with custom events * fix delayed id update and update id on item hover * refactor logic to use custom events to move virtual focus instead of a callback ref * move logic out of menu and down into selection hooks * use useEvent didnt use it in autocomplete hook since the ref might not initially exist which is a problem for useEvent (it wont setup the listener)
## API Changes
react-aria-components/react-aria-components:Autocomplete+Autocomplete {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ autoFocus?: boolean
+ children?: ReactNode | ((AutocompleteRenderProps & {
+ defaultChildren: ReactNode | undefined
+})) => ReactNode
+ className?: string | ((AutocompleteRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultFilter?: (string, string) => boolean
+ defaultInputValue?: string
+ id?: string
+ inputValue?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ name?: string
+ onBlur?: (FocusEvent<HTMLInputElement>) => void
+ onFocus?: (FocusEvent<HTMLInputElement>) => void
+ onFocusChange?: (boolean) => void
+ onInputChange?: (string) => void
+ onKeyDown?: (KeyboardEvent) => void
+ onKeyUp?: (KeyboardEvent) => void
+ slot?: string | null
+ style?: CSSProperties | ((AutocompleteRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-aria/autocomplete/@react-aria/autocomplete:useAutocomplete+useAutocomplete {
+ props: AriaAutocompleteOptions
+ state: AutocompleteState
+ returnVal: undefined
+} /@react-aria/autocomplete:AriaAutocompleteProps+AriaAutocompleteProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ autoFocus?: boolean
+ children: ReactNode
+ defaultInputValue?: string
+ description?: ReactNode
+ errorMessage?: ReactNode | (ValidationResult) => ReactNode
+ id?: string
+ inputValue?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ label?: ReactNode
+ name?: string
+ onBlur?: (FocusEvent<HTMLInputElement>) => void
+ onFocus?: (FocusEvent<HTMLInputElement>) => void
+ onFocusChange?: (boolean) => void
+ onInputChange?: (string) => void
+ onKeyDown?: (KeyboardEvent) => void
+ onKeyUp?: (KeyboardEvent) => void
+ placeholder?: string
+} /@react-aria/autocomplete:AriaAutocompleteOptions+AriaAutocompleteOptions {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ autoFocus?: boolean
+ collectionRef: RefObject<HTMLElement | null>
+ defaultInputValue?: string
+ description?: ReactNode
+ errorMessage?: ReactNode | (ValidationResult) => ReactNode
+ id?: string
+ inputRef: RefObject<HTMLInputElement | null>
+ inputValue?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ label?: ReactNode
+ name?: string
+ onBlur?: (FocusEvent<HTMLInputElement>) => void
+ onFocus?: (FocusEvent<HTMLInputElement>) => void
+ onFocusChange?: (boolean) => void
+ onInputChange?: (string) => void
+ onKeyDown?: (KeyboardEvent) => void
+ onKeyUp?: (KeyboardEvent) => void
+ placeholder?: string
+} /@react-aria/autocomplete:AutocompleteAria+AutocompleteAria {
+ collectionProps: CollectionOptions
+ descriptionProps: DOMAttributes
+ inputProps: InputHTMLAttributes<HTMLInputElement>
+ labelProps: DOMAttributes
+} /@react-aria/autocomplete:CollectionOptions+CollectionOptions {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ disallowTypeAhead: boolean
+ id?: string
+ shouldUseVirtualFocus: boolean
+} @react-aria/collections/@react-aria/collections:BaseCollection BaseCollection <T> {
addNode: (CollectionNode<T>) => void
at: () => Node<T>
clone: () => this
commit: (Key | null, Key | null, any) => void
+ filter: ((string) => boolean) => BaseCollection<T>
getChildren: (Key) => Iterable<Node<T>>
getFirstKey: () => void
getItem: (Key) => Node<T> | null
getKeyAfter: (Key) => void
getKeys: () => void
getLastKey: () => void
removeNode: (Key) => void
size: any
undefined: () => void
} @react-aria/menu/@react-aria/menu:AriaMenuOptions AriaMenuOptions <T> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
defaultSelectedKeys?: 'all' | Iterable<Key>
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
id?: string
isVirtualized?: boolean
items?: Iterable<T>
keyboardDelegate?: KeyboardDelegate
onAction?: (Key) => void
onClose?: () => void
onKeyDown?: (KeyboardEvent) => void
onKeyUp?: (KeyboardEvent) => void
onSelectionChange?: (Selection) => void
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
shouldFocusWrap?: boolean
+ shouldUseVirtualFocus?: boolean
} /@react-aria/menu:getItemId+getItemId <T> {
+ state: TreeState<T>
+ itemKey: Key
+ returnVal: undefined
+} /@react-aria/menu:menuData+menuData {
+ UNTYPED
+} @react-aria/selection/@react-aria/selection:SelectableItemOptions SelectableItemOptions {
allowsDifferentPressOrigin?: boolean
focus?: () => void
+ id?: string
isDisabled?: boolean
isVirtualized?: boolean
key: Key
linkBehavior?: 'action' | 'selection' | 'override' | 'none' = 'action'
ref: RefObject<FocusableElement | null>
selectionManager: MultipleSelectionManager
shouldSelectOnPressUp?: boolean
shouldUseVirtualFocus?: boolean
} @react-aria/utils/@react-aria/utils:CLEAR_FOCUS_EVENT+CLEAR_FOCUS_EVENT {
+ UNTYPED
+} /@react-aria/utils:DELAY_UPDATE+DELAY_UPDATE {
+ UNTYPED
+} /@react-aria/utils:FOCUS_EVENT+FOCUS_EVENT {
+ UNTYPED
+} /@react-aria/utils:UPDATE_ACTIVEDESCENDANT+UPDATE_ACTIVEDESCENDANT {
+ UNTYPED
+} @react-stately/autocomplete/@react-stately/autocomplete:useAutocompleteState+useAutocompleteState {
+ props: AutocompleteStateOptions
+ returnVal: undefined
+} /@react-stately/autocomplete:AutocompleteProps+AutocompleteProps {
+ autoFocus?: boolean
+ defaultInputValue?: string
+ description?: ReactNode
+ errorMessage?: ReactNode | (ValidationResult) => ReactNode
+ inputValue?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ label?: ReactNode
+ onBlur?: (FocusEvent<HTMLInputElement>) => void
+ onFocus?: (FocusEvent<HTMLInputElement>) => void
+ onFocusChange?: (boolean) => void
+ onInputChange?: (string) => void
+ onKeyDown?: (KeyboardEvent) => void
+ onKeyUp?: (KeyboardEvent) => void
+ placeholder?: string
+} /@react-stately/autocomplete:AutocompleteStateOptions+AutocompleteStateOptions {
+ autoFocus?: boolean
+ defaultInputValue?: string
+ description?: ReactNode
+ errorMessage?: ReactNode | (ValidationResult) => ReactNode
+ inputValue?: string
+ isDisabled?: boolean
+ isReadOnly?: boolean
+ label?: ReactNode
+ onBlur?: (FocusEvent<HTMLInputElement>) => void
+ onFocus?: (FocusEvent<HTMLInputElement>) => void
+ onFocusChange?: (boolean) => void
+ onInputChange?: (string) => void
+ onKeyDown?: (KeyboardEvent) => void
+ onKeyUp?: (KeyboardEvent) => void
+ placeholder?: string
+} /@react-stately/autocomplete:AutocompleteState+AutocompleteState {
+ focusedNodeId: string | null
+ inputValue: string
+ setFocusedNodeId: (string | null) => void
+ setInputValue: (string) => void
+} |
@LFDanLu Thanks for providing this update. It is very helpful. I look forward to trying this component in S2 when it becomes available. |
Closes
#7248 has a version that always persists focus, to test on Monday
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project:
RSP