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 31 commits into
base: main
Choose a base branch
from
Draft

(WIP) Autocomplete with menu #7181

wants to merge 31 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

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
Copy link
Member Author

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

Comment on lines 174 to 176
// 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]);
Copy link
Member Author

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

Comment on lines +19 to +20
// TODO: export the respective contexts here
export {Autocomplete} from './Autocomplete';
Copy link
Member Author

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)

Comment on lines +223 to +224
// 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
Copy link
Member Author

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

Comment on lines +210 to +212
useEffect(() => {
if (register) {
register((e: ReactKeyboardEvent) => {
Copy link
Member Author

@LFDanLu LFDanLu Oct 24, 2024

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?

@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

@rspbot
Copy link

rspbot commented Nov 7, 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 <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
+}

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.

3 participants