Skip to content

Commit 5ea1852

Browse files
authored
fix: adding hyperlink capabilities to nav items and nav tabs (#885)
* fix: adding hyperlink capabilities to nav items and nav tabs * refactor: remove unused code * refactor: fixing lint
1 parent cc56d63 commit 5ea1852

File tree

7 files changed

+178
-98
lines changed

7 files changed

+178
-98
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ActivatedRoute } from '@angular/router';
2+
import { IconType } from '@hypertrace/assets-library';
3+
import {
4+
FeatureState,
5+
FeatureStateResolver,
6+
MemoizeModule,
7+
NavigationParamsType,
8+
NavigationService
9+
} from '@hypertrace/common';
10+
import { BetaTagComponent, IconComponent, LinkComponent } from '@hypertrace/components';
11+
import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest';
12+
import { MockComponent } from 'ng-mocks';
13+
import { EMPTY, of } from 'rxjs';
14+
import { NavItemConfig, NavItemType } from '../navigation-list.component';
15+
import { FeatureConfigCheckModule } from './../../feature-check/feature-config-check.module';
16+
import { NavItemComponent } from './nav-item.component';
17+
18+
describe('Navigation Item Component', () => {
19+
let spectator: SpectatorHost<NavItemComponent>;
20+
const activatedRoute = {
21+
root: {}
22+
};
23+
const createHost = createHostFactory({
24+
shallow: true,
25+
component: NavItemComponent,
26+
declarations: [MockComponent(IconComponent), MockComponent(LinkComponent), MockComponent(BetaTagComponent)],
27+
imports: [MemoizeModule, FeatureConfigCheckModule],
28+
providers: [
29+
mockProvider(ActivatedRoute, activatedRoute),
30+
mockProvider(NavigationService, {
31+
navigation$: EMPTY,
32+
navigateWithinApp: jest.fn(),
33+
getCurrentActivatedRoute: jest.fn().mockReturnValue(of(activatedRoute))
34+
}),
35+
mockProvider(FeatureStateResolver, {
36+
getCombinedFeatureState: () => of(FeatureState.Enabled)
37+
})
38+
]
39+
});
40+
41+
test('should update layout when collapsed input is updated', () => {
42+
const navItem: NavItemConfig = {
43+
type: NavItemType.Link,
44+
icon: IconType.TriangleLeft,
45+
label: 'Foo Label',
46+
matchPaths: ['foo', 'bar']
47+
};
48+
spectator = createHost(`<ht-nav-item [config]="navItem"></ht-nav-item>`, {
49+
hostProps: { navItem: navItem }
50+
});
51+
52+
expect(spectator.query('.label')).not.toExist();
53+
expect(spectator.query(IconComponent)?.icon).toEqual(IconType.TriangleLeft);
54+
55+
spectator.setInput({
56+
collapsed: false
57+
});
58+
59+
spectator.detectChanges();
60+
expect(spectator.query('.label')).toExist();
61+
expect(spectator.query(IconComponent)?.icon).toEqual(IconType.TriangleLeft);
62+
});
63+
64+
test('should navigate to first match on click, relative to activated route', () => {
65+
const navItem: NavItemConfig = {
66+
type: NavItemType.Link,
67+
icon: 'icon',
68+
label: 'Foo Label',
69+
matchPaths: ['foo', 'bar']
70+
};
71+
spectator = createHost(`<ht-nav-item [config]="navItem"></ht-nav-item>`, {
72+
hostProps: { navItem: navItem }
73+
});
74+
75+
const link = spectator.query(LinkComponent);
76+
expect(link).toExist();
77+
expect(link?.paramsOrUrl).toEqual({
78+
navType: NavigationParamsType.InApp,
79+
path: 'foo',
80+
relativeTo: spectator.inject(ActivatedRoute),
81+
replaceCurrentHistory: undefined
82+
});
83+
});
84+
85+
test('should navigate to first match on click, relative to activated route with skip location change option', () => {
86+
const navItem: NavItemConfig = {
87+
type: NavItemType.Link,
88+
icon: 'icon',
89+
label: 'Foo Label',
90+
matchPaths: ['foo', 'bar'],
91+
replaceCurrentHistory: true
92+
};
93+
94+
spectator = createHost(`<ht-nav-item [config]="navItem"></ht-nav-item>`, {
95+
hostProps: { navItem: navItem }
96+
});
97+
98+
const link = spectator.query(LinkComponent);
99+
expect(link).toExist();
100+
expect(link?.paramsOrUrl).toEqual({
101+
navType: NavigationParamsType.InApp,
102+
path: 'foo',
103+
relativeTo: spectator.inject(ActivatedRoute),
104+
replaceCurrentHistory: true
105+
});
106+
});
107+
});
Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2-
import { FeatureState } from '@hypertrace/common';
2+
import { ActivatedRoute } from '@angular/router';
3+
import { FeatureState, NavigationParams, NavigationParamsType } from '@hypertrace/common';
34
import { IconSize } from '../../icon/icon-size';
45
import { NavItemLinkConfig } from '../navigation-list.component';
56

@@ -8,28 +9,30 @@ import { NavItemLinkConfig } from '../navigation-list.component';
89
styleUrls: ['./nav-item.component.scss'],
910
changeDetection: ChangeDetectionStrategy.OnPush,
1011
template: `
11-
<div
12-
*htIfFeature="this.config.features | htFeature as featureState"
13-
class="nav-item"
14-
[ngClass]="{ active: this.active }"
15-
>
16-
<ht-icon
17-
class="icon"
18-
[icon]="this.config.icon"
19-
size="${IconSize.Medium}"
20-
[label]="this.config.label"
21-
[showTooltip]="this.collapsed"
12+
<ht-link *ngIf="this.config" [paramsOrUrl]="buildNavigationParam | htMemoize: this.config">
13+
<div
14+
*htIfFeature="this.config.features | htFeature as featureState"
15+
class="nav-item"
16+
[ngClass]="{ active: this.active }"
2217
>
23-
</ht-icon>
18+
<ht-icon
19+
class="icon"
20+
[icon]="this.config.icon"
21+
size="${IconSize.Medium}"
22+
[label]="this.config.label"
23+
[showTooltip]="this.collapsed"
24+
>
25+
</ht-icon>
2426
25-
<div class="label-container" *ngIf="!this.collapsed">
26-
<span class="label">{{ this.config.label }}</span>
27-
<span *ngIf="featureState === '${FeatureState.Preview}'" class="soon-container">
28-
<span class="soon">SOON</span>
29-
</span>
30-
<ht-beta-tag *ngIf="config.isBeta" class="beta"></ht-beta-tag>
27+
<div class="label-container" *ngIf="!this.collapsed">
28+
<span class="label">{{ this.config.label }}</span>
29+
<span *ngIf="featureState === '${FeatureState.Preview}'" class="soon-container">
30+
<span class="soon">SOON</span>
31+
</span>
32+
<ht-beta-tag *ngIf="config.isBeta" class="beta"></ht-beta-tag>
33+
</div>
3134
</div>
32-
</div>
35+
</ht-link>
3336
`
3437
})
3538
export class NavItemComponent {
@@ -41,4 +44,13 @@ export class NavItemComponent {
4144

4245
@Input()
4346
public collapsed: boolean = true;
47+
48+
public buildNavigationParam = (item: NavItemLinkConfig): NavigationParams => ({
49+
navType: NavigationParamsType.InApp,
50+
path: item.matchPaths[0],
51+
relativeTo: this.activatedRoute,
52+
replaceCurrentHistory: item.replaceCurrentHistory
53+
});
54+
55+
public constructor(private readonly activatedRoute: ActivatedRoute) {}
4456
}

projects/components/src/navigation/navigation-list.component.test.ts

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { ElementRef } from '@angular/core';
21
import { ActivatedRoute } from '@angular/router';
32
import { IconType } from '@hypertrace/assets-library';
4-
import { NavigationParams, NavigationParamsType, NavigationService } from '@hypertrace/common';
3+
import { MemoizeModule, NavigationService } from '@hypertrace/common';
54
import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest';
65
import { MockComponent } from 'ng-mocks';
76
import { EMPTY, of } from 'rxjs';
87
import { IconComponent } from '../icon/icon.component';
98
import { LetAsyncModule } from '../let-async/let-async.module';
9+
import { LinkComponent } from './../link/link.component';
1010
import { NavItemComponent } from './nav-item/nav-item.component';
1111
import { FooterItemConfig, NavigationListComponent, NavItemConfig, NavItemType } from './navigation-list.component';
1212
describe('Navigation List Component', () => {
@@ -17,8 +17,8 @@ describe('Navigation List Component', () => {
1717
const createHost = createHostFactory({
1818
shallow: true,
1919
component: NavigationListComponent,
20-
declarations: [MockComponent(IconComponent), MockComponent(NavItemComponent)],
21-
imports: [LetAsyncModule],
20+
declarations: [MockComponent(IconComponent), MockComponent(NavItemComponent), MockComponent(LinkComponent)],
21+
imports: [LetAsyncModule, MemoizeModule],
2222
providers: [
2323
mockProvider(ActivatedRoute, activatedRoute),
2424
mockProvider(NavigationService, {
@@ -78,49 +78,4 @@ describe('Navigation List Component', () => {
7878
expect(spectator.query('.navigation-list')).not.toHaveClass('expanded');
7979
expect(spectator.query(IconComponent)?.icon).toEqual(IconType.TriangleRight);
8080
});
81-
82-
test('should navigate to first match on click, relative to activated route', () => {
83-
const navItems: NavItemConfig[] = [
84-
{
85-
type: NavItemType.Link,
86-
icon: 'icon',
87-
label: 'Foo Label',
88-
matchPaths: ['foo', 'bar']
89-
}
90-
];
91-
spectator = createHost(`<ht-navigation-list [navItems]="navItems"></ht-navigation-list>`, {
92-
hostProps: { navItems: navItems }
93-
});
94-
95-
spectator.click(spectator.query(NavItemComponent, { read: ElementRef })!);
96-
expect(spectator.inject(NavigationService).navigate).toHaveBeenCalledWith<NavigationParams[]>({
97-
navType: NavigationParamsType.InApp,
98-
path: 'foo',
99-
relativeTo: spectator.inject(ActivatedRoute),
100-
replaceCurrentHistory: undefined
101-
});
102-
});
103-
104-
test('should navigate to first match on click, relative to activated route with skip location change option', () => {
105-
const navItems: NavItemConfig[] = [
106-
{
107-
type: NavItemType.Link,
108-
icon: 'icon',
109-
label: 'Foo Label',
110-
matchPaths: ['foo', 'bar'],
111-
replaceCurrentHistory: true
112-
}
113-
];
114-
spectator = createHost(`<ht-navigation-list [navItems]="navItems"></ht-navigation-list>`, {
115-
hostProps: { navItems: navItems }
116-
});
117-
118-
spectator.click(spectator.query(NavItemComponent, { read: ElementRef })!);
119-
expect(spectator.inject(NavigationService).navigate).toHaveBeenCalledWith<NavigationParams[]>({
120-
navType: NavigationParamsType.InApp,
121-
path: 'foo',
122-
relativeTo: spectator.inject(ActivatedRoute),
123-
replaceCurrentHistory: true
124-
});
125-
});
12681
});

projects/components/src/navigation/navigation-list.component.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
22
import { ActivatedRoute } from '@angular/router';
33
import { IconType } from '@hypertrace/assets-library';
4-
import { NavigationParamsType, NavigationService } from '@hypertrace/common';
4+
import { NavigationService } from '@hypertrace/common';
55
import { Observable } from 'rxjs';
66
import { map, startWith } from 'rxjs/operators';
77
import { IconSize } from '../icon/icon-size';
@@ -25,13 +25,7 @@ import { IconSize } from '../icon/icon-size';
2525
<hr *ngSwitchCase="'${NavItemType.Divider}'" class="nav-divider" />
2626
2727
<ng-container *ngSwitchCase="'${NavItemType.Link}'">
28-
<ht-nav-item
29-
[config]="item"
30-
[active]="item === activeItem"
31-
[collapsed]="this.collapsed"
32-
(click)="this.navigate(item)"
33-
>
34-
</ht-nav-item>
28+
<ht-nav-item [config]="item" [active]="item === activeItem" [collapsed]="this.collapsed"> </ht-nav-item>
3529
</ng-container>
3630
</ng-container>
3731
</ng-container>
@@ -82,15 +76,6 @@ export class NavigationListComponent {
8276
);
8377
}
8478

85-
public navigate(item: NavItemLinkConfig): void {
86-
this.navigationService.navigate({
87-
navType: NavigationParamsType.InApp,
88-
path: item.matchPaths[0],
89-
relativeTo: this.activatedRoute,
90-
replaceCurrentHistory: item.replaceCurrentHistory
91-
});
92-
}
93-
9479
public toggleView(): void {
9580
if (this.resizable) {
9681
this.collapsed = !this.collapsed;

projects/components/src/navigation/navigation-list.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { RouterModule } from '@angular/router';
4+
import { MemoizeModule } from '@hypertrace/common';
45
import { BetaTagModule } from '../beta-tag/beta-tag.module';
56
import { ButtonModule } from '../button/button.module';
67
import { FeatureConfigCheckModule } from '../feature-check/feature-config-check.module';
@@ -23,7 +24,9 @@ import { NavigationListComponent } from './navigation-list.component';
2324
ButtonModule,
2425
LinkModule,
2526
LabelModule,
26-
BetaTagModule
27+
BetaTagModule,
28+
MemoizeModule,
29+
LinkModule
2730
],
2831
declarations: [NavigationListComponent, NavItemComponent],
2932
exports: [NavigationListComponent]

projects/components/src/tabs/navigable/navigable-tab-group.component.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, QueryList } from '@angular/core';
22
import { ActivatedRoute } from '@angular/router';
3-
import { FeatureState, NavigationService } from '@hypertrace/common';
3+
import { FeatureState, NavigationParams, NavigationParamsType, NavigationService } from '@hypertrace/common';
44
import { merge, Observable } from 'rxjs';
55
import { map, startWith } from 'rxjs/operators';
66
import { NavigableTabComponent } from './navigable-tab.component';
@@ -14,15 +14,17 @@ import { NavigableTabComponent } from './navigable-tab.component';
1414
<nav mat-tab-nav-bar *htLetAsync="this.activeTab$ as activeTab" disableRipple>
1515
<ng-container *ngFor="let tab of this.tabs">
1616
<ng-container *ngIf="!tab.hidden">
17-
<div class="tab-button" *htIfFeature="tab.featureFlags | htFeature as featureState">
18-
<a mat-tab-link (click)="this.onTabClick(tab)" class="tab-link" [active]="activeTab === tab">
19-
<ng-container *ngTemplateOutlet="tab.content"></ng-container>
20-
<span *ngIf="featureState === '${FeatureState.Preview}'" class="soon-container">
21-
<span class="soon">SOON</span>
22-
</span>
23-
</a>
24-
<div class="ink-bar" [ngClass]="{ active: activeTab === tab }"></div>
25-
</div>
17+
<ht-link [paramsOrUrl]="buildNavigationParam | htMemoize: tab">
18+
<div class="tab-button" *htIfFeature="tab.featureFlags | htFeature as featureState">
19+
<a mat-tab-link (click)="this.onTabClick(tab)" class="tab-link" [active]="activeTab === tab">
20+
<ng-container *ngTemplateOutlet="tab.content"></ng-container>
21+
<span *ngIf="featureState === '${FeatureState.Preview}'" class="soon-container">
22+
<span class="soon">SOON</span>
23+
</span>
24+
</a>
25+
<div class="ink-bar" [ngClass]="{ active: activeTab === tab }"></div>
26+
</div>
27+
</ht-link>
2628
</ng-container>
2729
</ng-container>
2830
</nav>
@@ -48,6 +50,12 @@ export class NavigableTabGroupComponent implements AfterContentInit {
4850
);
4951
}
5052

53+
public buildNavigationParam = (tab: NavigableTabComponent): NavigationParams => ({
54+
navType: NavigationParamsType.InApp,
55+
path: [tab.path],
56+
relativeTo: this.activatedRoute
57+
});
58+
5159
public onTabClick(tab: NavigableTabComponent): void {
5260
this.navigationService.navigateWithinApp([tab.path], this.activatedRoute);
5361
}

projects/components/src/tabs/navigable/navigable-tab.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { MatTabsModule } from '@angular/material/tabs';
44
import { RouterModule } from '@angular/router';
5+
import { MemoizeModule } from '@hypertrace/common';
56
import { FeatureConfigCheckModule } from '../../feature-check/feature-config-check.module';
67
import { LetAsyncModule } from '../../let-async/let-async.module';
8+
import { LinkModule } from '../../link/link.module';
79
import { NavigableTabGroupComponent } from './navigable-tab-group.component';
810
import { NavigableTabComponent } from './navigable-tab.component';
911

1012
@NgModule({
1113
declarations: [NavigableTabGroupComponent, NavigableTabComponent],
1214
exports: [NavigableTabGroupComponent, NavigableTabComponent],
13-
imports: [MatTabsModule, CommonModule, RouterModule, LetAsyncModule, FeatureConfigCheckModule]
15+
imports: [
16+
MatTabsModule,
17+
CommonModule,
18+
RouterModule,
19+
LetAsyncModule,
20+
FeatureConfigCheckModule,
21+
MemoizeModule,
22+
LinkModule
23+
]
1424
})
1525
export class NavigableTabModule {}

0 commit comments

Comments
 (0)