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
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c33ac96
scaffolding, copied Combobox and renamed to Autocomplete
LFDanLu Oct 9, 2024
a185473
get listbox rendering by default without open state
LFDanLu Oct 9, 2024
e6fa7c1
update intl file and more clean up to get rid of combobox stuff
LFDanLu Oct 10, 2024
61aab83
fix lint
LFDanLu Oct 10, 2024
66fecc8
rough working version of Menu instead of Listbox in autocomplete
LFDanLu Oct 11, 2024
779309b
fix submenu
LFDanLu Oct 11, 2024
78de7f8
Update autocomplete to have the wrapped menu filter itself
LFDanLu Oct 19, 2024
e0f098a
fix keyboard interactions, and clean up
LFDanLu Oct 22, 2024
5914d11
fix menu, add more stories, fix strict
LFDanLu Oct 22, 2024
12480fd
add announcements to menu and various clean up
LFDanLu Oct 22, 2024
ad8942a
update yarn.lock
LFDanLu Oct 22, 2024
70c2d0f
Merge branch 'main' of github.com:adobe/react-spectrum into autocomplete
LFDanLu Oct 22, 2024
9812c3c
get rid of dom node in Autocomplete and fix readOnly bugs
LFDanLu Oct 23, 2024
8478937
fix build failure
LFDanLu Oct 23, 2024
3fbd35f
test against popover experience
LFDanLu Oct 23, 2024
e15d999
fix popover story
LFDanLu Oct 24, 2024
368a677
properly clear aria-activedecendant
LFDanLu Oct 24, 2024
82b3120
cleanup
LFDanLu Oct 24, 2024
423b73c
fix build
LFDanLu Oct 24, 2024
6c498f1
Merge branch 'main' into autocomplete
LFDanLu Oct 28, 2024
fe5bfbe
properly focus trap the autocomplete popover
LFDanLu Oct 29, 2024
477ca7f
update interaction pattern as per discussion
LFDanLu Nov 1, 2024
9a58613
update yarn.lock
LFDanLu Nov 1, 2024
574bd67
dont autofocus if user hasnt typed in the field yet
LFDanLu Nov 5, 2024
ae7a00f
add delay for now to make NVDA announcement better
LFDanLu Nov 5, 2024
7b11c5d
fix lint and scrap custom announcements
LFDanLu Nov 5, 2024
04e8777
intial tests
LFDanLu Nov 6, 2024
79c9064
more tests and fixes to BaseCollection and keyboard interactions from…
LFDanLu Nov 6, 2024
b20b626
fix lint and add RAC test
LFDanLu Nov 7, 2024
28afe21
Merge branch 'main' of github.com:adobe/react-spectrum into autocomplete
LFDanLu Nov 7, 2024
2d0a15f
use MenuSection
LFDanLu Nov 7, 2024
c87a507
(WIP) Refactor autocomplete logic to use custom events to update virt…
LFDanLu Nov 20, 2024
682357f
Merge branch 'main' of github.com:adobe/react-spectrum into autocomplete
LFDanLu Nov 20, 2024
3871e7b
fix lint and test with wrapping Listbox
LFDanLu Nov 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/ar-AE.json
LFDanLu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "مقترحات"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/bg-BG.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Предложения"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/cs-CZ.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Návrhy"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/da-DK.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Forslag"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/de-DE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Empfehlungen"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/el-GR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Προτάσεις"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Suggestions"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/es-ES.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Sugerencias"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/et-EE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Soovitused"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/fi-FI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Ehdotukset"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/fr-FR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Suggestions"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/he-IL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "הצעות"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/hr-HR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Prijedlozi"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/hu-HU.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Javaslatok"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/it-IT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Suggerimenti"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/ja-JP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "候補"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/ko-KR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "제안"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/lt-LT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Pasiūlymai"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/lv-LV.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Ieteikumi"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/nb-NO.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Forslag"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/nl-NL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Suggesties"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/pl-PL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Sugestie"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/pt-BR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Sugestões"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/pt-PT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Sugestões"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/ro-RO.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Sugestii"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/ru-RU.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Предложения"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/sk-SK.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Návrhy"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/sl-SI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Predlogi"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/sr-SP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Predlozi"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/sv-SE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Förslag"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/tr-TR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Öneriler"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/uk-UA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "Пропозиції"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "建议"
}
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/intl/zh-TW.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"menuLabel": "建議"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/autocomplete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@
},
"dependencies": {
"@react-aria/combobox": "^3.10.5",
"@react-aria/focus": "^3.18.3",
"@react-aria/i18n": "^3.12.3",
"@react-aria/listbox": "^3.13.5",
"@react-aria/searchfield": "^3.7.10",
"@react-aria/textfield": "^3.14.9",
"@react-aria/utils": "^3.25.3",
"@react-stately/autocomplete": "3.0.0-alpha.1",
"@react-stately/combobox": "^3.10.0",
"@react-types/autocomplete": "3.0.0-alpha.26",
"@react-types/button": "^3.10.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-aria/autocomplete/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
* governing permissions and limitations under the License.
*/
export {useSearchAutocomplete} from './useSearchAutocomplete';
export {useAutocomplete} from './useAutocomplete';

export type {AriaSearchAutocompleteOptions, SearchAutocompleteAria} from './useSearchAutocomplete';
export type {AriaSearchAutocompleteProps} from '@react-types/autocomplete';
export type {AriaAutocompleteProps, AriaAutocompleteOptions, AutocompleteAria} from './useAutocomplete';
146 changes: 146 additions & 0 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {AriaLabelingProps, BaseEvent, DOMAttributes, DOMProps, InputDOMProps, RefObject} from '@react-types/shared';
import type {AriaMenuOptions} from '@react-aria/menu';
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
import {chain, mergeProps, useId, useLabels} from '@react-aria/utils';
import {InputHTMLAttributes, KeyboardEvent, ReactNode, useRef} from 'react';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useTextField} from '@react-aria/textfield';

export interface AriaAutocompleteProps extends AutocompleteProps, DOMProps, InputDOMProps, AriaLabelingProps {
children: ReactNode
}

// TODO: all of this is menu specific but will need to eventually be agnostic to what collection element is inside
// Update all instances of "menu" then
export interface AriaAutocompleteOptions extends Omit<AriaAutocompleteProps, 'children'> {
/** The ref for the input element. */
inputRef: RefObject<HTMLInputElement | null>
}
export interface AutocompleteAria<T> {
/** Props for the label element. */
labelProps: DOMAttributes,
/** Props for the autocomplete input element. */
inputProps: InputHTMLAttributes<HTMLInputElement>,
/** Props for the menu, to be passed to [useMenu](useMenu.html). */
menuProps: AriaMenuOptions<T>,
/** Props for the autocomplete description element, if any. */
descriptionProps: DOMAttributes,
// TODO: fairly non-standard thing to return from a hook, discuss how best to share this with hook only users
// This is for the user to register a callback that upon recieving a keyboard event key returns the expected virtually focused node id
/** Register function that expects a callback function that returns the newlly virtually focused menu option when provided with the keyboard action that occurs in the input field. */
register: (callback: (e: KeyboardEvent) => string | null) => void
}

/**
* Provides the behavior and accessibility implementation for a autocomplete component.
* A autocomplete combines a text input with a menu, allowing users to filter a list of options to items matching a query.
* @param props - Props for the autocomplete.
* @param state - State for the autocomplete, as returned by `useAutocompleteState`.
*/
export function useAutocomplete<T>(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria<T> {
let {
inputRef,
isReadOnly
} = props;

let menuId = useId();

// TODO: may need to move this into Autocomplete? Kinda odd to return this from the hook? Maybe the callback should be considered
// external to the hook, and the onus is on the user to pass in a onKeydown to this hook that updates state.focusedNodeId in response to a key
// stroke
let callbackRef = useRef<(e: KeyboardEvent) => string>(null);
let register = (callback) => {
callbackRef.current = callback;
};

// For textfield specific keydown operations
let onKeyDown = (e: BaseEvent<KeyboardEvent<any>>) => {
if (e.nativeEvent.isComposing) {
return;
}

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

case 'Escape':
if (state.inputValue !== '' && !isReadOnly) {
state.setInputValue('');
} else {
e.continuePropagation();
}

break;
case 'Home':
case 'End':
case 'ArrowUp':
case 'ArrowDown':
// Prevent these keys from moving the text cursor in the input
e.preventDefault();
break;
}

if (callbackRef.current) {
let focusedNodeId = callbackRef.current(e);
state.setFocusedNodeId(focusedNodeId);
}
};

let {labelProps, inputProps, descriptionProps} = useTextField({
...props as any,
onChange: state.setInputValue,
onKeyDown: chain(onKeyDown, props.onKeyDown),
value: state.inputValue,
autoComplete: 'off'
}, inputRef);

let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete');
let menuProps = useLabels({
id: menuId,
'aria-label': stringFormatter.format('menuLabel'),
'aria-labelledby': props['aria-labelledby'] || labelProps.id
});

return {
labelProps,
inputProps: mergeProps(inputProps, {
'aria-haspopup': 'listbox',
'aria-controls': menuId,
// TODO: readd proper logic for completionMode = complete (aria-autocomplete: both)
'aria-autocomplete': 'list',
'aria-activedescendant': state.focusedNodeId ?? undefined,
// 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',
Comment on lines +223 to +227
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

// This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions.
autoCorrect: 'off',
// This disable's the macOS Safari spell check auto corrections.
spellCheck: 'false'
}),
menuProps: mergeProps(menuProps, {
shouldUseVirtualFocus: true,
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

}),
descriptionProps,
register
};
}
Loading