1- import { useInteractionContext } from "@/interactionContext" ;
2- import { useCallback , useEffect , useId , useLayoutEffect , useMemo , useRef , useState } from "react" ;
1+ import {
2+ ACCEPT ,
3+ CANCEL ,
4+ CURSOR_DOWN ,
5+ CURSOR_END ,
6+ CURSOR_HOME ,
7+ CURSOR_PAGE_DOWN ,
8+ CURSOR_PAGE_UP ,
9+ CURSOR_UP ,
10+ } from "@/features/commands/commandIds" ;
11+ import { useCommandRegistry } from "@/features/commands/commands" ;
12+ import { useFocusContext , useManagedFocusLayer } from "@/focusContext" ;
13+ import { useEffect , useId , useLayoutEffect , useMemo , useRef , useState } from "react" ;
314import styles from "./AutocompleteInput.module.css" ;
415
516export interface AutocompleteOption {
@@ -46,11 +57,11 @@ export function AutocompleteInput({
4657 enterKeyHint,
4758 keepOpenOnSelect = false ,
4859} : AutocompleteInputProps ) {
49- const interactionContext = useInteractionContext ( ) ;
60+ const commandRegistry = useCommandRegistry ( ) ;
61+ const focusContext = useFocusContext ( ) ;
5062 const localInputRef = useRef < HTMLInputElement > ( null ) ;
5163 const mergedInputRef = inputRef ?? localInputRef ;
5264 const [ open , setOpen ] = useState ( false ) ;
53- const [ focused , setFocused ] = useState ( false ) ;
5465 const [ selectedIndex , setSelectedIndex ] = useState < number | null > ( null ) ;
5566 const pointerDownRef = useRef ( false ) ;
5667 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
@@ -69,20 +80,45 @@ export function AutocompleteInput({
6980 [ groups ] ,
7081 ) ;
7182 const dropdownOpen = open && flattened . length > 0 ;
83+ useManagedFocusLayer ( "autocomplete" , dropdownOpen ) ;
7284 const flattenedRef = useRef ( flattened ) ;
7385 const selectedIndexRef = useRef ( selectedIndex ) ;
7486 const dropdownOpenRef = useRef ( dropdownOpen ) ;
7587 flattenedRef . current = flattened ;
7688 selectedIndexRef . current = selectedIndex ;
7789 dropdownOpenRef . current = dropdownOpen ;
7890
79- const moveInputCursor = useCallback ( ( position : "start" | "end" ) => {
80- const input = mergedInputRef . current ;
81- if ( ! input ) return ;
82- const nextPosition = position === "start" ? 0 : input . value . length ;
83- input . focus ( ) ;
84- input . setSelectionRange ( nextPosition , nextPosition ) ;
85- } , [ mergedInputRef ] ) ;
91+ useEffect ( ( ) => {
92+ return focusContext . registerAdapter ( "autocomplete" , {
93+ focus ( ) {
94+ mergedInputRef . current ?. focus ( ) ;
95+ } ,
96+ contains ( node ) {
97+ return node instanceof Node
98+ ? mergedInputRef . current ?. contains ( node ) === true || dropdownRef . current ?. contains ( node ) === true
99+ : false ;
100+ } ,
101+ isEditableTarget ( node ) {
102+ return mergedInputRef . current ?. contains ( node as Node ) === true ;
103+ } ,
104+ allowCommandRouting ( event ) {
105+ if ( ! dropdownOpenRef . current ) return false ;
106+ switch ( event . key ) {
107+ case "ArrowUp" :
108+ case "ArrowDown" :
109+ case "PageUp" :
110+ case "PageDown" :
111+ case "Home" :
112+ case "End" :
113+ case "Enter" :
114+ case "Escape" :
115+ return true ;
116+ default :
117+ return false ;
118+ }
119+ } ,
120+ } ) ;
121+ } , [ focusContext , mergedInputRef ] ) ;
86122
87123 useEffect ( ( ) => {
88124 setSelectedIndex ( ( current ) => {
@@ -104,74 +140,73 @@ export function AutocompleteInput({
104140 commitSelectionRef . current = commitSelection ;
105141
106142 useEffect ( ( ) => {
107- return interactionContext . registerController ( {
108- contains ( node ) {
109- const input = mergedInputRef . current ;
110- const dropdown = dropdownRef . current ;
111- if ( ! ( node instanceof Node ) ) return false ;
112- return Boolean ( ( input && input . contains ( node ) ) || ( dropdown && dropdown . contains ( node ) ) ) ;
113- } ,
114- isActive ( ) {
115- return focused ;
116- } ,
117- handleIntent ( intent ) {
118- switch ( intent ) {
119- case "cancel" :
120- if ( ! dropdownOpenRef . current ) return false ;
121- setOpen ( false ) ;
122- return true ;
123- case "cursorDown" :
124- if ( ! dropdownOpenRef . current ) return false ;
125- setSelectedIndex ( ( current ) =>
126- current === null ? 0 : Math . min ( flattenedRef . current . length - 1 , current + 1 ) ,
127- ) ;
128- return true ;
129- case "cursorUp" :
130- if ( ! dropdownOpenRef . current ) return false ;
131- setSelectedIndex ( ( current ) =>
132- current === null ? Math . max ( 0 , flattenedRef . current . length - 1 ) : Math . max ( 0 , current - 1 ) ,
133- ) ;
134- return true ;
135- case "cursorPageDown" :
136- if ( ! dropdownOpenRef . current ) return false ;
137- setSelectedIndex ( ( current ) =>
138- current === null ? 0 : Math . min ( flattenedRef . current . length - 1 , current + PAGE_STEP ) ,
139- ) ;
140- return true ;
141- case "cursorPageUp" :
142- if ( ! dropdownOpenRef . current ) return false ;
143- setSelectedIndex ( ( current ) => ( current === null ? 0 : Math . max ( 0 , current - PAGE_STEP ) ) ) ;
144- return true ;
145- case "cursorHome" :
146- if ( ! dropdownOpenRef . current ) {
147- moveInputCursor ( "start" ) ;
148- return true ;
149- }
150- if ( flattenedRef . current . length === 0 ) return false ;
151- setSelectedIndex ( 0 ) ;
152- return true ;
153- case "cursorEnd" :
154- if ( ! dropdownOpenRef . current ) {
155- moveInputCursor ( "end" ) ;
156- return true ;
157- }
158- if ( flattenedRef . current . length === 0 ) return false ;
159- setSelectedIndex ( flattenedRef . current . length - 1 ) ;
160- return true ;
161- case "accept" : {
162- if ( ! dropdownOpenRef . current ) return false ;
163- if ( selectedIndexRef . current === null ) return false ;
164- const selected = flattenedRef . current [ selectedIndexRef . current ] ;
165- if ( ! selected ) return false ;
166- commitSelectionRef . current ( selected . option . value ) ;
167- return true ;
168- }
169- default :
170- return false ;
171- }
172- } ,
173- } ) ;
174- } , [ focused , interactionContext , mergedInputRef , moveInputCursor ] ) ;
143+ if ( ! dropdownOpen ) return ;
144+ const disposables = [
145+ commandRegistry . registerCommand (
146+ CANCEL ,
147+ ( ) => {
148+ setOpen ( false ) ;
149+ setSelectedIndex ( null ) ;
150+ } ,
151+ ) ,
152+ commandRegistry . registerCommand (
153+ CURSOR_DOWN ,
154+ ( ) => {
155+ setSelectedIndex ( ( current ) =>
156+ current === null ? 0 : Math . min ( flattenedRef . current . length - 1 , current + 1 ) ,
157+ ) ;
158+ } ,
159+ ) ,
160+ commandRegistry . registerCommand (
161+ CURSOR_UP ,
162+ ( ) => {
163+ setSelectedIndex ( ( current ) =>
164+ current === null ? Math . max ( 0 , flattenedRef . current . length - 1 ) : Math . max ( 0 , current - 1 ) ,
165+ ) ;
166+ } ,
167+ ) ,
168+ commandRegistry . registerCommand (
169+ CURSOR_PAGE_DOWN ,
170+ ( ) => {
171+ setSelectedIndex ( ( current ) =>
172+ current === null ? 0 : Math . min ( flattenedRef . current . length - 1 , current + PAGE_STEP ) ,
173+ ) ;
174+ } ,
175+ ) ,
176+ commandRegistry . registerCommand (
177+ CURSOR_PAGE_UP ,
178+ ( ) => {
179+ setSelectedIndex ( ( current ) => ( current === null ? 0 : Math . max ( 0 , current - PAGE_STEP ) ) ) ;
180+ } ,
181+ ) ,
182+ commandRegistry . registerCommand (
183+ CURSOR_HOME ,
184+ ( ) => {
185+ if ( flattenedRef . current . length === 0 ) return ;
186+ setSelectedIndex ( 0 ) ;
187+ } ,
188+ ) ,
189+ commandRegistry . registerCommand (
190+ CURSOR_END ,
191+ ( ) => {
192+ if ( flattenedRef . current . length === 0 ) return ;
193+ setSelectedIndex ( flattenedRef . current . length - 1 ) ;
194+ } ,
195+ ) ,
196+ commandRegistry . registerCommand (
197+ ACCEPT ,
198+ ( ) => {
199+ if ( selectedIndexRef . current === null ) return ;
200+ const selected = flattenedRef . current [ selectedIndexRef . current ] ;
201+ if ( ! selected ) return ;
202+ commitSelectionRef . current ( selected . option . value ) ;
203+ } ,
204+ ) ,
205+ ] ;
206+ return ( ) => {
207+ disposables . forEach ( ( dispose ) => dispose ( ) ) ;
208+ } ;
209+ } , [ commandRegistry , dropdownOpen ] ) ;
175210
176211 useEffect ( ( ) => {
177212 const dropdown = dropdownRef . current ;
@@ -238,11 +273,7 @@ export function AutocompleteInput({
238273 setOpen ( true ) ;
239274 setSelectedIndex ( null ) ;
240275 } }
241- onFocus = { ( ) => {
242- setFocused ( true ) ;
243- } }
244276 onBlur = { ( ) => {
245- setFocused ( false ) ;
246277 if ( pointerDownRef . current ) return ;
247278 setOpen ( false ) ;
248279 setSelectedIndex ( null ) ;
0 commit comments