@@ -13,15 +13,14 @@ import {
13
13
OnChanges ,
14
14
SimpleChanges ,
15
15
} from '@angular/core' ;
16
- import { Content } from '@ngneat/overview' ;
17
16
import { UntilDestroy , untilDestroyed } from '@ngneat/until-destroy' ;
18
- import { first } from 'rxjs' ;
19
17
import { CmdkService } from '../../cmdk.service' ;
20
18
import { EmptyDirective } from '../../directives/empty/empty.directive' ;
21
19
import { ItemDirective } from '../../directives/item/item.directive' ;
22
20
import { CmdkCommandProps } from '../../types' ;
23
21
import { GroupComponent } from '../group/group.component' ;
24
22
import { SeparatorComponent } from '../separator/separator.component' ;
23
+ import { ActiveDescendantKeyManager , FocusKeyManager } from '@angular/cdk/a11y' ;
25
24
26
25
let commandId = 0 ;
27
26
@UntilDestroy ( )
@@ -36,19 +35,15 @@ export class CommandComponent
36
35
implements CmdkCommandProps , AfterViewInit , OnChanges
37
36
{
38
37
@Output ( ) valueChanged = new EventEmitter < string > ( ) ;
39
- @Input ( ) value ?: string ;
40
- @Input ( ) label ?: Content ;
38
+ @Input ( ) value : string | undefined ;
41
39
@Input ( ) ariaLabel ?: string ;
42
40
@Input ( ) filter : ( ( value : string , search : string ) => boolean ) | null = (
43
41
value ,
44
42
search
45
- ) => {
46
- const searchValue = search . toLowerCase ( ) ;
47
- return value . toLowerCase ( ) . includes ( searchValue ) ;
48
- } ;
43
+ ) => value . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) ;
49
44
50
45
@ContentChildren ( ItemDirective , { descendants : true } )
51
- items : QueryList < ItemDirective > | undefined ;
46
+ items ! : QueryList < ItemDirective > ;
52
47
@ContentChildren ( GroupComponent , { descendants : true } )
53
48
groups : QueryList < GroupComponent > | undefined ;
54
49
@ContentChildren ( SeparatorComponent , { descendants : true } )
@@ -59,37 +54,51 @@ export class CommandComponent
59
54
60
55
private cmdkService = inject ( CmdkService ) ;
61
56
57
+ private keyManager ! : ActiveDescendantKeyManager < ItemDirective > ;
58
+ private focusKeyManager ! : FocusKeyManager < ItemDirective > ;
59
+
62
60
ngOnChanges ( changes : SimpleChanges ) {
63
- if (
64
- changes [ 'value' ] &&
65
- changes [ 'value' ] . previousValue !== changes [ 'value' ] . currentValue &&
66
- this . value
67
- ) {
61
+ if ( changes [ 'value' ] ) {
68
62
this . setValue ( this . value ) ;
69
63
}
70
64
}
71
65
72
66
ngAfterViewInit ( ) {
67
+ // create key and focus managers
68
+ this . keyManager = new ActiveDescendantKeyManager ( this . items )
69
+ . withWrap ( )
70
+ . skipPredicate ( ( item ) => item . disabled ) ;
71
+ this . focusKeyManager = new FocusKeyManager ( this . items )
72
+ . withWrap ( )
73
+ . skipPredicate ( ( item ) => item . disabled ) ;
73
74
if ( this . filter ) {
74
75
this . cmdkService . search$
75
76
. pipe ( untilDestroyed ( this ) )
76
77
. subscribe ( ( s ) => this . handleSearch ( s ) ) ;
77
78
}
78
79
79
- if ( ! this . value ) {
80
+ // if value is given, make that item active, else make first item active
81
+ if ( this . value ) {
82
+ this . setValue ( this . value ) ;
83
+ } else {
80
84
this . makeFirstItemActive ( ) ;
81
- this . makeFirstGroupActive ( ) ;
82
85
}
83
86
84
- this . cmdkService . value$
87
+ // emit value on item clicks
88
+ this . cmdkService . itemClicked$
85
89
. pipe ( untilDestroyed ( this ) )
86
- . subscribe ( ( value ) => this . valueChanged . emit ( value ) ) ;
87
-
88
- this . cmdkService . activeItem$
89
- . pipe ( untilDestroyed ( this ) )
90
- . subscribe ( ( itemId ) => {
91
- this . setActiveGroupForActiveItem ( itemId ) ;
90
+ . subscribe ( ( value ) => {
91
+ this . setValue ( value ) ;
92
+ this . valueChanged . emit ( value ) ;
92
93
} ) ;
94
+
95
+ // set active group on active item change
96
+ this . keyManager . change . pipe ( untilDestroyed ( this ) ) . subscribe ( ( ) => {
97
+ const activeItem = this . keyManager . activeItem ;
98
+ if ( activeItem ) {
99
+ this . setActiveGroupForActiveItem ( activeItem . itemId ) ;
100
+ }
101
+ } ) ;
93
102
}
94
103
95
104
get filteredItems ( ) {
@@ -129,74 +138,41 @@ export class CommandComponent
129
138
130
139
@HostListener ( 'keyup' , [ '$event' ] )
131
140
onKeyUp ( ev : KeyboardEvent ) {
132
- if ( ev . key === 'ArrowDown' ) {
133
- this . makeNextItemActive ( ) ;
134
- } else if ( ev . key === 'ArrowUp' ) {
135
- this . makePreviousItemActive ( ) ;
141
+ if ( ev . key === 'Enter' && this . keyManager . activeItem ) {
142
+ this . valueChanged . emit ( this . keyManager . activeItem . value ) ;
143
+ } else {
144
+ this . keyManager . onKeydown ( ev ) ;
145
+ this . focusKeyManager . onKeydown ( ev ) ;
136
146
}
137
147
}
138
148
139
149
private makeFirstItemActive ( ) {
140
150
setTimeout ( ( ) => {
141
151
const firstItem = this . filteredItems ?. [ 0 ] ;
142
152
if ( firstItem ) {
143
- this . cmdkService . setActiveItem ( firstItem . itemId ) ;
153
+ this . keyManager . setFirstItemActive ( ) ;
154
+ this . focusKeyManager . setFirstItemActive ( ) ;
144
155
}
145
156
} ) ;
146
157
}
147
158
148
- private makeFirstGroupActive ( ) {
149
- setTimeout ( ( ) => {
150
- const firstGroup = this . filteredGroups ?. [ 0 ] ;
151
- if ( firstGroup ) {
152
- this . cmdkService . setActiveGroup ( firstGroup . groupId ) ;
153
- }
159
+ private setActiveGroupForActiveItem ( nextActiveItemId : string ) {
160
+ this . filteredGroups ?. forEach ( ( group ) => {
161
+ group . active = group . filteredItems . some (
162
+ ( item ) => item . itemId === nextActiveItemId
163
+ ) ;
154
164
} ) ;
155
165
}
156
- private makePreviousItemActive ( ) {
157
- this . cmdkService . activeItem$
158
- . pipe ( first ( ) , untilDestroyed ( this ) )
159
- . subscribe ( ( activeItemId ) => {
160
- if ( this . filteredItems ?. length ) {
161
- const activeItemIndex = this . filteredItems . findIndex (
162
- ( item ) => item . itemId === activeItemId
163
- ) ;
164
- const nextActiveItem = this . filteredItems [ activeItemIndex - 1 ] ;
165
- if ( nextActiveItem ) {
166
- const nextActiveItemId = nextActiveItem . itemId ;
167
- this . cmdkService . setActiveItem ( nextActiveItemId ) ;
168
- }
169
- }
170
- } ) ;
171
- }
172
166
173
- private setActiveGroupForActiveItem ( nextActiveItemId : string ) {
174
- const nextActiveGroupId = this . filteredGroups ?. find ( ( group ) =>
175
- group . filteredItems . some ( ( item ) => item . itemId === nextActiveItemId )
176
- ) ?. groupId ;
177
- if ( nextActiveGroupId ) {
178
- this . cmdkService . setActiveGroup ( nextActiveGroupId ) ;
167
+ private setValue ( value : string | undefined ) {
168
+ if ( value !== undefined ) {
169
+ const valueItem = this . filteredItems ?. find (
170
+ ( item ) => item . value === value
171
+ ) ;
172
+ if ( valueItem ) {
173
+ this . keyManager . setActiveItem ( valueItem ) ;
174
+ this . focusKeyManager . setActiveItem ( valueItem ) ;
175
+ }
179
176
}
180
177
}
181
-
182
- private makeNextItemActive ( ) {
183
- this . cmdkService . activeItem$
184
- . pipe ( first ( ) , untilDestroyed ( this ) )
185
- . subscribe ( ( activeItemId ) => {
186
- if ( this . filteredItems ?. length ) {
187
- const activeItemIndex = this . filteredItems . findIndex (
188
- ( item ) => item . itemId === activeItemId
189
- ) ;
190
- const nextActiveItem = this . filteredItems [ activeItemIndex + 1 ] ;
191
- if ( nextActiveItem ) {
192
- const nextActiveItemId = nextActiveItem . itemId ;
193
- this . cmdkService . setActiveItem ( nextActiveItemId ) ;
194
- }
195
- }
196
- } ) ;
197
- }
198
-
199
- private setValue ( value : string ) {
200
- this . cmdkService . setValue ( value ) ;
201
- }
202
178
}
0 commit comments