1111 */
1212
1313import { AriaLabelingProps , BaseEvent , DOMProps , RefObject } from '@react-types/shared' ;
14+ import { AriaTextFieldProps } from '@react-aria/textfield' ;
1415import { AutocompleteProps , AutocompleteState } from '@react-stately/autocomplete' ;
15- import { ChangeEvent , InputHTMLAttributes , KeyboardEvent as ReactKeyboardEvent , useCallback , useEffect , useMemo , useRef } from 'react' ;
1616import { CLEAR_FOCUS_EVENT , FOCUS_EVENT , isCtrlKeyPressed , mergeProps , mergeRefs , UPDATE_ACTIVEDESCENDANT , useEffectEvent , useId , useLabels , useObjectRef } from '@react-aria/utils' ;
1717// @ts -ignore
1818import intlMessages from '../intl/*.json' ;
19- import { useKeyboard } from '@ react-aria/interactions ' ;
19+ import { KeyboardEvent as ReactKeyboardEvent , useCallback , useEffect , useMemo , useRef } from 'react' ;
2020import { useLocalizedStringFormatter } from '@react-aria/i18n' ;
2121
2222export interface CollectionOptions extends DOMProps , AriaLabelingProps {
@@ -35,14 +35,12 @@ export interface AriaAutocompleteProps extends AutocompleteProps {
3535
3636export interface AriaAutocompleteOptions extends Omit < AriaAutocompleteProps , 'children' > {
3737 /** The ref for the wrapped collection element. */
38- collectionRef : RefObject < HTMLElement | null > ,
39- /** The ref for the wrapped input element. */
40- inputRef : RefObject < HTMLInputElement | null >
38+ collectionRef : RefObject < HTMLElement | null >
4139}
4240
4341export interface AutocompleteAria {
44- /** Props for the autocomplete input element. */
45- inputProps : InputHTMLAttributes < HTMLInputElement > ,
42+ /** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively . */
43+ textFieldProps : AriaTextFieldProps ,
4644 /** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */
4745 collectionProps : CollectionOptions ,
4846 /** Ref to attach to the wrapped collection. */
@@ -60,8 +58,7 @@ export interface AutocompleteAria {
6058export function UNSTABLE_useAutocomplete ( props : AriaAutocompleteOptions , state : AutocompleteState ) : AutocompleteAria {
6159 let {
6260 collectionRef,
63- filter,
64- inputRef
61+ filter
6562 } = props ;
6663
6764 let collectionId = useId ( ) ;
@@ -138,20 +135,22 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
138135 } ) ;
139136
140137 // TODO: update to see if we can tell what kind of event (paste vs backspace vs typing) is happening instead
141- let onChange = ( e : ChangeEvent < HTMLInputElement > ) => {
138+ let onChange = ( value : string ) => {
142139 // Tell wrapped collection to focus the first element in the list when typing forward and to clear focused key when deleting text
143140 // for screen reader announcements
144- if ( state . inputValue !== e . target . value && state . inputValue . length <= e . target . value . length ) {
141+ if ( state . inputValue !== value && state . inputValue . length <= value . length ) {
145142 focusFirstItem ( ) ;
146143 } else {
147144 clearVirtualFocus ( ) ;
148145 }
149146
150- state . setInputValue ( e . target . value ) ;
147+ state . setInputValue ( value ) ;
151148 } ;
152149
150+ let keyDownTarget = useRef < Element | null > ( null ) ;
153151 // For textfield specific keydown operations
154152 let onKeyDown = ( e : BaseEvent < ReactKeyboardEvent < any > > ) => {
153+ keyDownTarget . current = e . target as Element ;
155154 if ( e . nativeEvent . isComposing ) {
156155 return ;
157156 }
@@ -166,12 +165,7 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
166165 // Early return for Escape here so it doesn't leak the Escape event from the simulated collection event below and
167166 // close the dialog prematurely. Ideally that should be up to the discretion of the input element hence the check
168167 // for isPropagationStopped
169- // Also set the inputValue to '' to cover Firefox case where Esc doesn't actually clear searchfields. Normally we already
170- // handle this in useSearchField, but we are directly setting the inputValue on the input element in RAC Autocomplete instead of
171- // passing it to the SearchField via props. This means that a controlled value set on the Autocomplete isn't synced up with the
172- // SearchField until the user makes a change to the field's value via typing
173- if ( e . isPropagationStopped ( ) ) {
174- state . setInputValue ( '' ) ;
168+ if ( e . isDefaultPrevented ( ) ) {
175169 return ;
176170 }
177171 break ;
@@ -210,7 +204,13 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
210204 }
211205
212206 // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter
213- // or moving focus from one item to another
207+ // or moving focus from one item to another. Stop propagation on the input event if it isn't already stopped so it doesn't leak out. For events
208+ // like ESC, the dispatched event below will bubble out of the collection and be stopped if handled by useSelectableCollection, otherwise will bubble
209+ // as expected
210+ if ( ! e . isPropagationStopped ( ) ) {
211+ e . stopPropagation ( ) ;
212+ }
213+
214214 if ( state . focusedNodeId == null ) {
215215 collectionRef . current ?. dispatchEvent (
216216 new KeyboardEvent ( e . nativeEvent . type , e . nativeEvent )
@@ -227,7 +227,7 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
227227 // Dispatch simulated key up events for things like triggering links in listbox
228228 // Make sure to stop the propagation of the input keyup event so that the simulated keyup/down pair
229229 // is detected by usePress instead of the original keyup originating from the input
230- if ( e . target === inputRef . current ) {
230+ if ( e . target === keyDownTarget . current ) {
231231 e . stopImmediatePropagation ( ) ;
232232 if ( state . focusedNodeId == null ) {
233233 collectionRef . current ?. dispatchEvent (
@@ -247,9 +247,7 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
247247 return ( ) => {
248248 document . removeEventListener ( 'keyup' , onKeyUpCapture , true ) ;
249249 } ;
250- } , [ inputRef , onKeyUpCapture ] ) ;
251-
252- let { keyboardProps} = useKeyboard ( { onKeyDown} ) ;
250+ } , [ onKeyUpCapture ] ) ;
253251
254252 let stringFormatter = useLocalizedStringFormatter ( intlMessages , '@react-aria/autocomplete' ) ;
255253 let collectionProps = useLabels ( {
@@ -266,10 +264,10 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
266264 } , [ state . inputValue , filter ] ) ;
267265
268266 return {
269- inputProps : {
267+ textFieldProps : {
270268 value : state . inputValue ,
271269 onChange,
272- ... keyboardProps ,
270+ onKeyDown ,
273271 autoComplete : 'off' ,
274272 'aria-haspopup' : 'listbox' ,
275273 'aria-controls' : collectionId ,
@@ -284,10 +282,7 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
284282 collectionProps : mergeProps ( collectionProps , {
285283 // TODO: shouldFocusOnHover? shouldFocusWrap? Should it be up to the wrapped collection?
286284 shouldUseVirtualFocus : true ,
287- disallowTypeAhead : true ,
288- // Prevent the emulated keyboard events that were dispatched on the wrapped collection from propagating outside of the autocomplete since techincally
289- // they've been handled by the input already
290- onKeyDown : ( e ) => e . stopPropagation ( )
285+ disallowTypeAhead : true
291286 } ) ,
292287 collectionRef : mergedCollectionRef ,
293288 filterFn : filter != null ? filterFn : undefined
0 commit comments