@@ -18,6 +18,7 @@ type CommandContextType = {
1818 registerItem : ( id : string ) => void
1919 unregisterItem : ( id : string ) => void
2020 selectItem : ( id : string ) => void
21+ handleKeyDown : ( e : React . KeyboardEvent ) => void
2122}
2223
2324const CommandContext = createContext < CommandContextType | undefined > ( undefined )
@@ -30,6 +31,11 @@ const useCommandContext = () => {
3031 return context
3132}
3233
34+ export const useCommandKeyDown = ( ) => {
35+ const context = useContext ( CommandContext )
36+ return context ?. handleKeyDown
37+ }
38+
3339interface CommandProps {
3440 children : ReactNode
3541 className ?: string
@@ -59,126 +65,130 @@ interface CommandSeparatorProps {
5965 className ?: string
6066}
6167
62- export const Command = React . forwardRef < HTMLDivElement , CommandProps > (
63- ( { children, className, filter, searchQuery : externalSearchQuery } , ref ) => {
64- const [ internalSearchQuery , setInternalSearchQuery ] = useState ( '' )
65- const [ activeIndex , setActiveIndex ] = useState ( - 1 )
66- const [ items , setItems ] = useState < string [ ] > ( [ ] )
67- const [ filteredItems , setFilteredItems ] = useState < string [ ] > ( [ ] )
68-
69- const searchQuery = externalSearchQuery ?? internalSearchQuery
70-
71- const registerItem = useCallback ( ( id : string ) => {
72- setItems ( ( prev ) => {
73- if ( prev . includes ( id ) ) return prev
74- return [ ...prev , id ]
75- } )
76- } , [ ] )
77-
78- const unregisterItem = useCallback ( ( id : string ) => {
79- setItems ( ( prev ) => prev . filter ( ( item ) => item !== id ) )
80- } , [ ] )
81-
82- const selectItem = useCallback (
83- ( id : string ) => {
84- const index = filteredItems . indexOf ( id )
85- if ( index >= 0 ) {
86- setActiveIndex ( index )
87- }
88- } ,
89- [ filteredItems ]
90- )
91-
92- useEffect ( ( ) => {
93- if ( ! searchQuery ) {
94- setFilteredItems ( items )
95- return
96- }
97-
98- const filtered = items
99- . map ( ( item ) => {
100- const score = filter ? filter ( item , searchQuery ) : defaultFilter ( item , searchQuery )
101- return { item, score }
102- } )
103- . filter ( ( item ) => item . score > 0 )
104- . sort ( ( a , b ) => b . score - a . score )
105- . map ( ( item ) => item . item )
106-
107- setFilteredItems ( filtered )
108- setActiveIndex ( filtered . length > 0 ? 0 : - 1 )
109- } , [ searchQuery , items , filter ] )
110-
111- useEffect ( ( ) => {
112- if ( activeIndex >= 0 && filteredItems [ activeIndex ] ) {
113- const activeElement = document . getElementById ( filteredItems [ activeIndex ] )
114- if ( activeElement ) {
115- activeElement . scrollIntoView ( {
116- behavior : 'smooth' ,
117- block : 'nearest' ,
118- } )
119- }
68+ export function Command ( {
69+ children,
70+ className,
71+ filter,
72+ searchQuery : externalSearchQuery ,
73+ } : CommandProps ) {
74+ const [ internalSearchQuery , setInternalSearchQuery ] = useState ( '' )
75+ const [ activeIndex , setActiveIndex ] = useState ( - 1 )
76+ const [ items , setItems ] = useState < string [ ] > ( [ ] )
77+ const [ filteredItems , setFilteredItems ] = useState < string [ ] > ( [ ] )
78+
79+ const searchQuery = externalSearchQuery ?? internalSearchQuery
80+
81+ const registerItem = useCallback ( ( id : string ) => {
82+ setItems ( ( prev ) => {
83+ if ( prev . includes ( id ) ) return prev
84+ return [ ...prev , id ]
85+ } )
86+ } , [ ] )
87+
88+ const unregisterItem = useCallback ( ( id : string ) => {
89+ setItems ( ( prev ) => prev . filter ( ( item ) => item !== id ) )
90+ } , [ ] )
91+
92+ const selectItem = useCallback (
93+ ( id : string ) => {
94+ const index = filteredItems . indexOf ( id )
95+ if ( index >= 0 ) {
96+ setActiveIndex ( index )
12097 }
121- } , [ activeIndex , filteredItems ] )
98+ } ,
99+ [ filteredItems ]
100+ )
122101
123- const defaultFilter = useCallback ( ( value : string , search : string ) : number => {
124- const normalizedValue = value . toLowerCase ( )
125- const normalizedSearch = search . toLowerCase ( )
102+ useEffect ( ( ) => {
103+ if ( ! searchQuery ) {
104+ setFilteredItems ( items )
105+ return
106+ }
126107
127- if ( normalizedValue === normalizedSearch ) return 1
128- if ( normalizedValue . startsWith ( normalizedSearch ) ) return 0.8
129- if ( normalizedValue . includes ( normalizedSearch ) ) return 0.6
130- return 0
131- } , [ ] )
108+ const filtered = items . filter ( ( item ) => {
109+ const score = filter ? filter ( item , searchQuery ) : defaultFilter ( item , searchQuery )
110+ return score > 0
111+ } )
132112
133- const handleKeyDown = useCallback (
134- ( e : React . KeyboardEvent ) => {
135- if ( filteredItems . length === 0 ) return
113+ setFilteredItems ( filtered )
114+ setActiveIndex ( filtered . length > 0 ? 0 : - 1 )
115+ } , [ searchQuery , items , filter ] )
136116
137- switch ( e . key ) {
138- case 'ArrowDown' :
139- e . preventDefault ( )
140- setActiveIndex ( ( prev ) => ( prev + 1 ) % filteredItems . length )
141- break
142- case 'ArrowUp' :
117+ useEffect ( ( ) => {
118+ if ( activeIndex >= 0 && filteredItems [ activeIndex ] ) {
119+ const activeElement = document . getElementById ( filteredItems [ activeIndex ] )
120+ if ( activeElement ) {
121+ activeElement . scrollIntoView ( {
122+ behavior : 'smooth' ,
123+ block : 'nearest' ,
124+ } )
125+ }
126+ }
127+ } , [ activeIndex , filteredItems ] )
128+
129+ const defaultFilter = useCallback ( ( value : string , search : string ) : number => {
130+ const normalizedValue = value . toLowerCase ( )
131+ const normalizedSearch = search . toLowerCase ( )
132+
133+ if ( normalizedValue === normalizedSearch ) return 1
134+ if ( normalizedValue . startsWith ( normalizedSearch ) ) return 0.8
135+ if ( normalizedValue . includes ( normalizedSearch ) ) return 0.6
136+ return 0
137+ } , [ ] )
138+
139+ const handleKeyDown = useCallback (
140+ ( e : React . KeyboardEvent ) => {
141+ if ( filteredItems . length === 0 ) return
142+
143+ switch ( e . key ) {
144+ case 'ArrowDown' :
145+ e . preventDefault ( )
146+ setActiveIndex ( ( prev ) => ( prev + 1 ) % filteredItems . length )
147+ break
148+ case 'ArrowUp' :
149+ e . preventDefault ( )
150+ setActiveIndex ( ( prev ) => ( prev - 1 + filteredItems . length ) % filteredItems . length )
151+ break
152+ case 'Enter' :
153+ if ( activeIndex >= 0 ) {
143154 e . preventDefault ( )
144- setActiveIndex ( ( prev ) => ( prev - 1 + filteredItems . length ) % filteredItems . length )
145- break
146- case 'Enter' :
147- if ( activeIndex >= 0 ) {
148- e . preventDefault ( )
149- document . getElementById ( filteredItems [ activeIndex ] ) ?. click ( )
150- }
151- break
152- }
153- } ,
154- [ filteredItems , activeIndex ]
155- )
156-
157- const contextValue = useMemo (
158- ( ) => ( {
159- searchQuery,
160- setSearchQuery : setInternalSearchQuery ,
161- activeIndex,
162- setActiveIndex,
163- filteredItems,
164- registerItem,
165- unregisterItem,
166- selectItem,
167- } ) ,
168- [ searchQuery , activeIndex , filteredItems , registerItem , unregisterItem , selectItem ]
169- )
170-
171- return (
172- < CommandContext . Provider value = { contextValue } >
173- < div ref = { ref } className = { cn ( 'flex w-full flex-col' , className ) } onKeyDown = { handleKeyDown } >
174- { children }
175- </ div >
176- </ CommandContext . Provider >
177- )
178- }
179- )
155+ document . getElementById ( filteredItems [ activeIndex ] ) ?. click ( )
156+ }
157+ break
158+ }
159+ } ,
160+ [ filteredItems , activeIndex ]
161+ )
162+
163+ const contextValue = useMemo (
164+ ( ) => ( {
165+ searchQuery,
166+ setSearchQuery : setInternalSearchQuery ,
167+ activeIndex,
168+ setActiveIndex,
169+ filteredItems,
170+ registerItem,
171+ unregisterItem,
172+ selectItem,
173+ handleKeyDown,
174+ } ) ,
175+ [
176+ searchQuery ,
177+ activeIndex ,
178+ filteredItems ,
179+ registerItem ,
180+ unregisterItem ,
181+ selectItem ,
182+ handleKeyDown ,
183+ ]
184+ )
180185
181- Command . displayName = 'Command'
186+ return (
187+ < CommandContext . Provider value = { contextValue } >
188+ < div className = { cn ( 'flex w-full flex-col' , className ) } > { children } </ div >
189+ </ CommandContext . Provider >
190+ )
191+ }
182192
183193export function CommandList ( { children, className } : CommandListProps ) {
184194 return < div className = { cn ( className ) } > { children } </ div >
0 commit comments