Skip to content

Commit 98f3075

Browse files
committed
feat(suggest): implement custom header slot
1 parent 6b93e08 commit 98f3075

File tree

3 files changed

+223
-30
lines changed

3 files changed

+223
-30
lines changed

projects/angular/components/ui-suggest/src/ui-suggest.component.html

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,59 @@
231231
<ng-container *ngTemplateOutlet="customValueTemplate; context: { $implicit: 'down' }">
232232
</ng-container>
233233

234+
<ng-container *ngFor="let item of headerItems; let index = index">
235+
<mat-list-item *ngLet="drillDown && !!item?.expandable as itemExpandable"
236+
[class.active]="index === activeIndex"
237+
[class.is-loading]="item.loading !== VirtualScrollItemStatus.loaded"
238+
[class.readonly]="item.loading !== VirtualScrollItemStatus.loaded"
239+
[class.selected]="!!item &&
240+
!multiple &&
241+
isItemSelected(item)"
242+
[class.is-expandable]="itemExpandable"
243+
[style.height.px]="!!item ? itemSize : 0"
244+
(click)="preventDefault($event);
245+
updateValue(item, !multiple, true);"
246+
[attr.role]="'option'"
247+
matRipple>
248+
<ng-container *ngIf="!!item">
249+
<ng-container *ngIf="itemTemplate; else defaultItem">
250+
<ng-container *ngTemplateOutlet="itemTemplate; context: {
251+
$implicit: item
252+
}">
253+
</ng-container>
254+
</ng-container>
255+
</ng-container>
256+
<ng-template #defaultItem>
257+
<div [matTooltip]="disableTooltip ? '' : intl.translateLabel(item.text)"
258+
[attr.data-item-id]="item.id"
259+
matTooltipPosition="right"
260+
class="ui-suggest-item">
261+
<mat-checkbox *ngIf="multiple"
262+
[checked]="isItemSelected(item)"
263+
[indeterminate]="false">
264+
</mat-checkbox>
265+
<div [attr.aria-label]="intl.translateLabel(item.text)"
266+
class="text-label text-ellipsis"
267+
tabindex="-1">
268+
<mat-icon *ngIf="!!item?.icon?.svgIcon"
269+
[svgIcon]="item!.icon!.svgIcon!"></mat-icon>
270+
<mat-icon *ngIf="!!item?.icon?.matIcon">{{item?.icon?.matIcon}}</mat-icon>
271+
<span class="text-label-rendered">{{ item?.icon?.iconOnly ? null :
272+
intl.translateLabel(item.text) }}</span>
273+
</div>
274+
<mat-icon *ngIf="itemExpandable"
275+
class="expand-icon">chevron_right</mat-icon>
276+
</div>
277+
</ng-template>
278+
</mat-list-item>
279+
</ng-container>
280+
281+
<mat-divider *ngIf="headerItems!.length"></mat-divider>
282+
234283
<ng-container
235284
*cdkVirtualFor="let item of renderItems; let index = index; trackBy: trackById; templateCacheSize: disableTooltip ? 20 : 0">
236285
<mat-list-item *ngLet="drillDown && !!item?.expandable as itemExpandable"
237-
[class.active]="index === activeIndex"
286+
[class.active]="computedItemsOffset(index) === activeIndex"
238287
[class.is-loading]="item.loading !== VirtualScrollItemStatus.loaded"
239288
[class.readonly]="item.loading !== VirtualScrollItemStatus.loaded"
240289
[class.selected]="!!item &&

projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class UiSuggestFixtureDirective {
8282
readonly?: boolean;
8383
enableCustomValue?: boolean;
8484
items?: ISuggestValue[];
85+
headerItems?: ISuggestValue[];
8586
displayTemplateValue?: boolean;
8687
direction: 'up' | 'down' = 'down';
8788
displayPriority: 'default' | 'selected' = 'default';
@@ -287,14 +288,14 @@ const sharedSpecifications = (
287288
expect(itemListEntries.length).toEqual(0);
288289
});
289290

290-
it('should render the list open and not close on selection if alwaysExpanded is true', (async () => {
291+
it('should render the list open and not close on selection if alwaysExpanded is true', (fakeAsync(() => {
291292
const items = generateSuggetionItemList(10);
292293

293294
component.alwaysExpanded = true;
294295
component.items = items;
295296

296297
fixture.detectChanges();
297-
await fixture.whenStable();
298+
tick(400);
298299

299300
const itemListEntries = fixture.debugElement.queryAll(By.css('.mat-list-item'));
300301

@@ -308,10 +309,11 @@ const sharedSpecifications = (
308309

309310
currentListItem.nativeElement.dispatchEvent(EventGenerator.click);
310311
fixture.detectChanges();
312+
tick(400);
311313

312314
expect(itemListEntries).not.toBeNull();
313315
expect(itemListEntries.length).toEqual(items.length);
314-
}));
316+
})));
315317

316318
it('should filter items if typed into', (done) => {
317319
let items = generateSuggetionItemList(40);
@@ -1321,6 +1323,66 @@ const sharedSpecifications = (
13211323
});
13221324
});
13231325

1326+
describe('Behavior: headerItems', () => {
1327+
it('should not work with custom value enabled', () => {
1328+
const error = new Error('enableCustomValue and headerItems are mutually exclusive options');
1329+
component.enableCustomValue = true;
1330+
1331+
expect(() => {
1332+
component.headerItems = generateSuggetionItemList(5);
1333+
fixture.detectChanges();
1334+
}).toThrow(error);
1335+
});
1336+
1337+
it('should display header items & fetched data in list', fakeAsync(() => {
1338+
const items = generateSuggetionItemList(10);
1339+
const headerItems = items.slice(0, 5);
1340+
const regularItems = items.slice(5, 10);
1341+
1342+
component.alwaysExpanded = true;
1343+
component.items = regularItems;
1344+
component.headerItems = headerItems;
1345+
1346+
fixture.detectChanges();
1347+
tick(1000);
1348+
1349+
const itemListEntries = fixture.debugElement.queryAll(By.css('.mat-list-item'));
1350+
1351+
expect(itemListEntries).not.toBeNull();
1352+
expect(itemListEntries.length).toEqual(items.length);
1353+
itemListEntries.forEach((entry, idx) => {
1354+
expect(entry.nativeElement.innerText).toBe(items[idx].text);
1355+
});
1356+
}));
1357+
1358+
it('should hide elements on search', fakeAsync(() => {
1359+
const items = generateSuggetionItemList(10, 'Item');
1360+
const headerItems = items.slice(0, 5);
1361+
const regularItems = items.slice(5, 10);
1362+
1363+
component.alwaysExpanded = true;
1364+
component.items = regularItems;
1365+
component.headerItems = headerItems;
1366+
component.searchable = true;
1367+
1368+
fixture.detectChanges();
1369+
tick(1000);
1370+
1371+
searchFor('Item', fixture);
1372+
1373+
fixture.detectChanges();
1374+
tick(1000);
1375+
1376+
const itemListEntries = fixture.debugElement.queryAll(By.css('.mat-list-item'));
1377+
1378+
expect(itemListEntries).not.toBeNull();
1379+
expect(itemListEntries.length).toEqual(regularItems.length);
1380+
itemListEntries.forEach((entry, idx) => {
1381+
expect(entry.nativeElement.innerText).toBe(regularItems[idx].text);
1382+
});
1383+
}));
1384+
});
1385+
13241386
describe('Selection: single value', () => {
13251387
it('should have one list item entry for each item provided', waitForAsync(async () => {
13261388
fixture.detectChanges();
@@ -2324,6 +2386,7 @@ describe('Component: UiSuggest', () => {
23242386
[searchable]="searchable"
23252387
[enableCustomValue]="enableCustomValue"
23262388
[alwaysExpanded]="alwaysExpanded"
2389+
[headerItems]="headerItems"
23272390
[items]="items"
23282391
[value]="value"
23292392
[direction]="direction"
@@ -2403,6 +2466,7 @@ describe('Component: UiSuggest', () => {
24032466
[clearable]="clearable"
24042467
[searchable]="searchable"
24052468
[enableCustomValue]="enableCustomValue"
2469+
[headerItems]="headerItems"
24062470
[alwaysExpanded]="alwaysExpanded"
24072471
[items]="items"
24082472
[direction]="direction"
@@ -2592,6 +2656,7 @@ describe('Component: UiSuggest', () => {
25922656
[searchable]="searchable"
25932657
[enableCustomValue]="enableCustomValue"
25942658
[alwaysExpanded]="alwaysExpanded"
2659+
[headerItems]="headerItems"
25952660
[items]="items"
25962661
[value]="value"
25972662
[direction]="direction"

0 commit comments

Comments
 (0)