Skip to content
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

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft

(WIP) Autocomplete with menu #7181

wants to merge 34 commits into from

Conversation

LFDanLu
Copy link
Member

@LFDanLu LFDanLu commented Oct 11, 2024

Closes

#7248 has a version that always persists focus, to test on Monday

✅ 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:

🧢 Your Project:

RSP

@rspbot
Copy link

rspbot commented Oct 11, 2024

@adobe adobe deleted a comment from rspbot Oct 22, 2024
@rspbot
Copy link

rspbot commented Oct 23, 2024

@rspbot
Copy link

rspbot commented Oct 24, 2024

Comment on lines 76 to 78
// TODO: how best to trigger the focused element's action? Currently having the registered callback handle dispatching a
// keyboard event
switch (e.key) {
Copy link
Member Author

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

@rspbot
Copy link

rspbot commented Oct 24, 2024

Comment on lines +125 to +129
// 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',
Copy link
Member Author

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

Comment on lines 137 to 141
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);
}
Copy link
Member Author

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

Comment on lines +212 to +216
// 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> {
Copy link
Member Author

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

@rspbot
Copy link

rspbot commented Oct 28, 2024

@romansndlr
Copy link

Is there maybe some document explaining the motivation for this new component and what it's going to be?

@LFDanLu
Copy link
Member Author

LFDanLu commented Oct 29, 2024

@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
@rspbot
Copy link

rspbot commented Nov 1, 2024

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
@rspbot
Copy link

rspbot commented Nov 6, 2024

@rspbot
Copy link

rspbot commented Nov 7, 2024

@adobe adobe deleted a comment from rspbot Nov 7, 2024
@rspbot
Copy link

rspbot commented Nov 7, 2024

@cheema-corellian
Copy link

cheema-corellian commented Nov 17, 2024

@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?

@LFDanLu
Copy link
Member Author

LFDanLu commented Nov 18, 2024

@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)
@rspbot
Copy link

rspbot commented Nov 20, 2024

@rspbot
Copy link

rspbot commented Nov 21, 2024

@rspbot
Copy link

rspbot commented Nov 21, 2024

## 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
+}

@cheema-corellian
Copy link

@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.

@LFDanLu Thanks for providing this update. It is very helpful. I look forward to trying this component in S2 when it becomes available.

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

Successfully merging this pull request may close these issues.

4 participants