-
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
lastSize.current = optionCount; | ||
}, [announced, setAnnounced, optionCount, stringFormatter, state.selectionManager.focusedKey]); | ||
|
||
// TODO: Omitted the custom announcement for selection because we expect to only trigger onActions for Autocomplete, selected key isn't a thing |
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.
For now we've decided to only handle the case where pressing on a menu item triggers an action but technically you could have a multi selection menu wrapped in the Autocomplete with which this announcement would need to be brought back
// TODO: Since menu only has `items` and not `defaultItems`, this means the user can't have completly controlled items like in ComboBox, | ||
// we always perform the filtering for them. | ||
let filteredCollection = useMemo(() => filterFn ? collection.filter(filterFn) : collection, [collection, filterFn]); |
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.
One difference to note, but the user can always pass their own defaultFilterFn
that returns true always if they want the items
they provide to be fully controlled (aka if they do their own filtering outside the Autocomplete and thus don't want our default filter fn to also be applied). It does feel a bit confusing though, but we'd have to add defaultItems
to Menu if we want to make items
fully controlled...
// TODO: export the respective contexts here | ||
export {Autocomplete} from './Autocomplete'; |
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.
Waiting for the api to settle. Right now there are three contexts in Autocomplete: AutocompleteContext (standard one for users to pass stuff from an external component to the Autocomplete component), AutocompleteStateContext (also standard, for accessing the state from within the component), InternalAutocompleteContext (for accessing the stuff Autocomplete wants to pass to its wrapped collection components)
// TODO: how would we handle this for a DialogTrigger? I guess we could automatically grab the overlaytrigger state from context and send a onAction that gets chained by the one provided | ||
// to Menu |
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.
Would the onus be on the user to access the Dialog's close
from the render function and call it in the onAction they provide to the Menu
useEffect(() => { | ||
if (register) { | ||
register((e: ReactKeyboardEvent) => { |
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.
The register and clearVirtualFocus that happen here feel like they should be in the menu hooks, would we be comfortable adding register
and inputValue
(or something along those lines) as something the hooks accept?
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
## 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 <T> {
+ 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
+ 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 <T> {
+ descriptionProps: DOMAttributes
+ inputProps: InputHTMLAttributes<HTMLInputElement>
+ labelProps: DOMAttributes
+ menuProps: AriaMenuOptions<T>
+ register: ((KeyboardEvent) => string | null) => void
+} @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
+ onHoverStart?: (HoverEvent) => 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-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
+} |
Closes
#7248 has a version that always persists focus, to test on Monday
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project:
RSP