Skip to content

Commit 4b97449

Browse files
committed
feat: use angular/cdk key manager
1 parent 7958784 commit 4b97449

File tree

10 files changed

+155
-141
lines changed

10 files changed

+155
-141
lines changed

package-lock.json

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"private": true,
2323
"dependencies": {
2424
"@angular/animations": "^15.1.0",
25+
"@angular/cdk": "^15.2.0",
2526
"@angular/common": "^15.1.0",
2627
"@angular/compiler": "^15.1.0",
2728
"@angular/core": "^15.1.0",

projects/ngneat/cmdk/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"peerDependencies": {
55
"@angular/common": ">=13.3.11 <=15.1.0",
66
"@angular/core": ">=13.3.11 <=15.1.0",
7-
"@ngneat/overview": ">=3.0.4",
8-
"@ngneat/until-destroy": ">=9.2.3"
7+
"@ngneat/overview": ">=4.1.0",
8+
"@ngneat/until-destroy": ">=9.2.3",
9+
"@angular/cdk": ">=15.2.0"
910
},
1011
"dependencies": {
1112
"tslib": "^2.3.0"

projects/ngneat/cmdk/schematics/ng-add/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export const ngAdd = (options: Schema): Rule => (tree: Tree) => {
3737
const addPackageJsonDependencies = (): Rule => (host: Tree, context: SchematicContext) => {
3838
const dependencies: { name: string; version: string }[] = [
3939
{ name: '@ngneat/overview', version: '4.1.0' },
40-
{ name: '@ngneat//until-destroy', version: '9.2.3' },
40+
{ name: '@ngneat/until-destroy', version: '9.2.3' },
41+
{ name: '@angular/cdk', version: '15.2.0' },
4142
];
4243

4344
dependencies.forEach((dependency) => {

projects/ngneat/cmdk/src/lib/cmdk.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { GroupComponent } from './components/group/group.component';
77
import { SeparatorComponent } from './components/separator/separator.component';
88
import { CommonModule } from '@angular/common';
99
import { ItemDirective } from './directives/item/item.directive';
10+
import { A11yModule } from '@angular/cdk/a11y';
1011

1112
const ComponentsAndDirectives = [
1213
CommandComponent,
@@ -18,7 +19,7 @@ const ComponentsAndDirectives = [
1819
];
1920
@NgModule({
2021
declarations: ComponentsAndDirectives,
21-
imports: [CommonModule, DynamicViewModule],
22+
imports: [CommonModule, DynamicViewModule, A11yModule],
2223
exports: ComponentsAndDirectives,
2324
})
2425
export class CmdkModule {}
Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
11
import { Injectable } from '@angular/core';
2-
import { ReplaySubject, Subject } from 'rxjs';
2+
import { Subject } from 'rxjs';
33

44
@Injectable()
55
export class CmdkService {
66
private _searchSub = new Subject<string>();
77
search$ = this._searchSub.asObservable();
8-
private _valueSub = new ReplaySubject<string>(1);
9-
value$ = this._valueSub.asObservable();
10-
private _activeItemSub = new ReplaySubject<string>(1);
11-
activeItem$ = this._activeItemSub.asObservable();
12-
private _activeGroupSub = new ReplaySubject<string>(1);
13-
activeGroup$ = this._activeGroupSub.asObservable();
8+
private _itemClickedSub = new Subject<string>();
9+
itemClicked$ = this._itemClickedSub.asObservable();
1410

1511
setSearch(value: string) {
1612
this._searchSub.next(value);
1713
}
1814

19-
setValue(value: string) {
20-
this._valueSub.next(value);
21-
}
22-
23-
setActiveItem(itemId: string) {
24-
this._activeItemSub.next(itemId);
25-
}
26-
27-
setActiveGroup(groupId: string) {
28-
this._activeGroupSub.next(groupId);
15+
itemClicked(value: string) {
16+
this._itemClickedSub.next(value);
2917
}
3018
}

projects/ngneat/cmdk/src/lib/components/command/command.component.ts

Lines changed: 53 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ import {
1313
OnChanges,
1414
SimpleChanges,
1515
} from '@angular/core';
16-
import { Content } from '@ngneat/overview';
1716
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
18-
import { first } from 'rxjs';
1917
import { CmdkService } from '../../cmdk.service';
2018
import { EmptyDirective } from '../../directives/empty/empty.directive';
2119
import { ItemDirective } from '../../directives/item/item.directive';
2220
import { CmdkCommandProps } from '../../types';
2321
import { GroupComponent } from '../group/group.component';
2422
import { SeparatorComponent } from '../separator/separator.component';
23+
import { ActiveDescendantKeyManager, FocusKeyManager } from '@angular/cdk/a11y';
2524

2625
let commandId = 0;
2726
@UntilDestroy()
@@ -36,19 +35,15 @@ export class CommandComponent
3635
implements CmdkCommandProps, AfterViewInit, OnChanges
3736
{
3837
@Output() valueChanged = new EventEmitter<string>();
39-
@Input() value?: string;
40-
@Input() label?: Content;
38+
@Input() value: string | undefined;
4139
@Input() ariaLabel?: string;
4240
@Input() filter: ((value: string, search: string) => boolean) | null = (
4341
value,
4442
search
45-
) => {
46-
const searchValue = search.toLowerCase();
47-
return value.toLowerCase().includes(searchValue);
48-
};
43+
) => value.toLowerCase().includes(search.toLowerCase());
4944

5045
@ContentChildren(ItemDirective, { descendants: true })
51-
items: QueryList<ItemDirective> | undefined;
46+
items!: QueryList<ItemDirective>;
5247
@ContentChildren(GroupComponent, { descendants: true })
5348
groups: QueryList<GroupComponent> | undefined;
5449
@ContentChildren(SeparatorComponent, { descendants: true })
@@ -59,37 +54,51 @@ export class CommandComponent
5954

6055
private cmdkService = inject(CmdkService);
6156

57+
private keyManager!: ActiveDescendantKeyManager<ItemDirective>;
58+
private focusKeyManager!: FocusKeyManager<ItemDirective>;
59+
6260
ngOnChanges(changes: SimpleChanges) {
63-
if (
64-
changes['value'] &&
65-
changes['value'].previousValue !== changes['value'].currentValue &&
66-
this.value
67-
) {
61+
if (changes['value']) {
6862
this.setValue(this.value);
6963
}
7064
}
7165

7266
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);
7374
if (this.filter) {
7475
this.cmdkService.search$
7576
.pipe(untilDestroyed(this))
7677
.subscribe((s) => this.handleSearch(s));
7778
}
7879

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 {
8084
this.makeFirstItemActive();
81-
this.makeFirstGroupActive();
8285
}
8386

84-
this.cmdkService.value$
87+
// emit value on item clicks
88+
this.cmdkService.itemClicked$
8589
.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);
9293
});
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+
});
93102
}
94103

95104
get filteredItems() {
@@ -129,74 +138,41 @@ export class CommandComponent
129138

130139
@HostListener('keyup', ['$event'])
131140
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);
136146
}
137147
}
138148

139149
private makeFirstItemActive() {
140150
setTimeout(() => {
141151
const firstItem = this.filteredItems?.[0];
142152
if (firstItem) {
143-
this.cmdkService.setActiveItem(firstItem.itemId);
153+
this.keyManager.setFirstItemActive();
154+
this.focusKeyManager.setFirstItemActive();
144155
}
145156
});
146157
}
147158

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+
);
154164
});
155165
}
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-
}
172166

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+
}
179176
}
180177
}
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-
}
202178
}

0 commit comments

Comments
 (0)