Skip to content

Commit a828cdc

Browse files
authored
feat(breadcrumb): Add breadcrumb
feat(breadcrumb): Add breadcrumb
2 parents 6a67a9a + 8e0519d commit a828cdc

File tree

7 files changed

+352
-0
lines changed

7 files changed

+352
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
Component,
3+
HostBinding,
4+
Input
5+
} from "@angular/core";
6+
7+
@Component({
8+
selector: "ibm-breadcrumb-item",
9+
template: `
10+
<a class="bx--link"
11+
href="{{href}}"
12+
*ngIf="href; else content">
13+
<ng-container *ngTemplateOutlet="content"></ng-container>
14+
</a>
15+
<ng-template #content>
16+
<ng-content></ng-content>
17+
</ng-template>`
18+
})
19+
export class BreadcrumbItemComponent {
20+
@Input() href: string;
21+
22+
@HostBinding("class.bx--breadcrumb-item") itemClass = true;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* A structure that represents a breadcrumb item.
3+
*
4+
* @export
5+
* @interface BreadcrumbItem
6+
*/
7+
export interface BreadcrumbItem {
8+
/**
9+
* Content to be displayed in the breadcrumb item.
10+
* @type {string}
11+
* @memberof BreadcrumbItem
12+
*/
13+
content: string;
14+
/**
15+
* Href for the breadcrumb item.
16+
* @type {string}
17+
* @memberof BreadcrumbItem
18+
*/
19+
href: string;
20+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Component } from "@angular/core";
2+
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
3+
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
4+
import { FormsModule } from "@angular/forms";
5+
import { By } from "@angular/platform-browser";
6+
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing";
7+
8+
import { Breadcrumb } from "./breadcrumb.component";
9+
import { BreadcrumbItemComponent } from "./breadcrumb-item.component";
10+
import { BreadcrumbItem } from "./breadcrumb-item.interface";
11+
import { OverflowMenu } from "../dialog/overflow-menu/overflow-menu.component";
12+
13+
@Component({
14+
selector: "test-breadcrumb",
15+
template: `
16+
<ibm-breadcrumb [noTrailingSlash]="noTrailingSlash">
17+
<ibm-breadcrumb-item href="#">
18+
Breadcrumb 1
19+
</ibm-breadcrumb-item>
20+
<ibm-breadcrumb-item href="#">
21+
Breadcrumb 2
22+
</ibm-breadcrumb-item>
23+
<ibm-breadcrumb-item href="#">
24+
Breadcrumb 3
25+
</ibm-breadcrumb-item>
26+
<ibm-breadcrumb-item href="#">
27+
Breadcrumb 4
28+
</ibm-breadcrumb-item>
29+
<ibm-breadcrumb-item href="#">
30+
Breadcrumb 5
31+
</ibm-breadcrumb-item>
32+
</ibm-breadcrumb>`,
33+
entryComponents: [Breadcrumb]
34+
})
35+
class TestBreadcrumb {
36+
noTrailingSlash = true;
37+
}
38+
39+
@Component({
40+
selector: "test-breadcrumb",
41+
template: `
42+
<ibm-breadcrumb
43+
[noTrailingSlash]="noTrailingSlash"
44+
[threshold]="threshold"
45+
[items]="items">
46+
</ibm-breadcrumb>`,
47+
entryComponents: [Breadcrumb]
48+
})
49+
class TestBreadcrumbModel {
50+
noTrailingSlash = true;
51+
threshold = 4;
52+
items = [];
53+
}
54+
55+
describe("Breadcrumb", () => {
56+
let component: Breadcrumb;
57+
let fixture: ComponentFixture<Breadcrumb>;
58+
59+
beforeEach(async(() => {
60+
TestBed.resetTestEnvironment();
61+
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
62+
63+
TestBed.configureTestingModule({
64+
imports: [FormsModule],
65+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
66+
declarations: [
67+
Breadcrumb,
68+
BreadcrumbItemComponent,
69+
TestBreadcrumb,
70+
TestBreadcrumbModel
71+
]
72+
}).compileComponents();
73+
}));
74+
75+
beforeEach(() => {
76+
fixture = TestBed.createComponent(Breadcrumb);
77+
component = fixture.componentInstance;
78+
fixture.detectChanges();
79+
});
80+
81+
it("should work", () => {
82+
expect(component).toBeTruthy();
83+
});
84+
85+
it("should create a breadcrumb with four items and no overflow menu", () => {
86+
const testFixture: ComponentFixture<TestBreadcrumb> =
87+
TestBed.createComponent(TestBreadcrumb);
88+
const testComponent = testFixture.componentInstance;
89+
testFixture.detectChanges();
90+
91+
const breadcrumbElement = testFixture.debugElement.query(By.directive(Breadcrumb));
92+
expect(breadcrumbElement).not.toBeNull();
93+
const breadcrumbItemElements = testFixture.debugElement.queryAll(By.directive(BreadcrumbItemComponent));
94+
expect(breadcrumbItemElements).not.toBeNull();
95+
expect(breadcrumbItemElements.length).toBe(5);
96+
const overflowMenuElement = testFixture.debugElement.query(By.directive(OverflowMenu));
97+
expect(overflowMenuElement).toBeNull();
98+
});
99+
100+
it("should create a breadcrumb with five items and an overflow menu in second position", () => {
101+
const testFixture: ComponentFixture<TestBreadcrumbModel> =
102+
TestBed.createComponent(TestBreadcrumbModel);
103+
const testComponent = testFixture.componentInstance;
104+
testComponent.threshold = 4;
105+
testComponent.items = [
106+
{ content: "Breadcrumb 1", href: "#1" },
107+
{ content: "Breadcrumb 2", href: "#2" },
108+
{ content: "Breadcrumb 3", href: "#3" },
109+
{ content: "Breadcrumb 4", href: "#4" },
110+
{ content: "Breadcrumb 5", href: "#5" }
111+
];
112+
testFixture.detectChanges();
113+
114+
const breadcrumbElement = testFixture.debugElement.query(By.directive(Breadcrumb));
115+
expect(breadcrumbElement).not.toBeNull();
116+
const breadcrumbItemElements = testFixture.debugElement.queryAll(By.directive(BreadcrumbItemComponent));
117+
expect(breadcrumbItemElements).not.toBeNull();
118+
expect(breadcrumbItemElements.length).toBe(4); // 4 because one is created for the overflow menu
119+
expect(breadcrumbItemElements[1].children[0].name).toEqual("ibm-overflow-menu");
120+
});
121+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
Component,
3+
Input
4+
} from "@angular/core";
5+
6+
import { BreadcrumbItem } from "./breadcrumb-item.interface";
7+
8+
const MINIMUM_OVERFLOW_THRESHOLD = 4;
9+
10+
@Component({
11+
selector: "ibm-breadcrumb",
12+
template: `
13+
<nav #nav class="bx--breadcrumb"
14+
[ngClass]="{
15+
'bx--breadcrumb--no-trailing-slash' : noTrailingSlash
16+
}"
17+
[attr.aria-label]="ariaLabel">
18+
<ng-template [ngIf]="shouldShowContent">
19+
<ng-content></ng-content>
20+
</ng-template>
21+
<ng-template [ngIf]="!shouldShowOverflow">
22+
<ibm-breadcrumb-item
23+
*ngFor="let item of items"
24+
[href]="item.href">
25+
{{item.content}}
26+
</ibm-breadcrumb-item>
27+
</ng-template>
28+
<ng-template [ngIf]="shouldShowOverflow">
29+
<ibm-breadcrumb-item [href]="first?.href">
30+
{{first?.content}}
31+
</ibm-breadcrumb-item>
32+
<ibm-breadcrumb-item>
33+
<ibm-overflow-menu>
34+
<li class="bx--overflow-menu-options__option"
35+
*ngFor="let item of overflowItems">
36+
<a class="bx--overflow-menu-options__btn"
37+
href="{{item?.href}}"
38+
style="text-decoration: none;">
39+
{{item?.content}}
40+
</a>
41+
</li>
42+
</ibm-overflow-menu>
43+
</ibm-breadcrumb-item>
44+
<ibm-breadcrumb-item [href]="secondLast?.href">
45+
{{secondLast?.content}}
46+
</ibm-breadcrumb-item>
47+
<ibm-breadcrumb-item [href]="last?.href">
48+
{{last?.content}}
49+
</ibm-breadcrumb-item>
50+
</ng-template>
51+
</nav>`
52+
})
53+
export class Breadcrumb {
54+
@Input() items: Array<BreadcrumbItem>;
55+
56+
@Input() noTrailingSlash = false;
57+
58+
@Input() ariaLabel: string;
59+
60+
protected _threshold: number;
61+
62+
@Input()
63+
set threshold(threshold: number) {
64+
this._threshold = threshold;
65+
if (isNaN(threshold) || threshold < MINIMUM_OVERFLOW_THRESHOLD) {
66+
this._threshold = MINIMUM_OVERFLOW_THRESHOLD;
67+
}
68+
}
69+
70+
get threshold(): number {
71+
return this._threshold;
72+
}
73+
74+
get shouldShowContent(): boolean {
75+
return !this.items;
76+
}
77+
78+
get shouldShowOverflow(): boolean {
79+
if (!this.items) {
80+
return false;
81+
}
82+
return this.items.length > this.threshold;
83+
}
84+
85+
get first(): BreadcrumbItem {
86+
return this.shouldShowOverflow ? this.items[0] : null;
87+
}
88+
89+
get overflowItems(): Array<BreadcrumbItem> {
90+
return this.shouldShowOverflow ? this.items.slice(1, this.items.length - 2) : [];
91+
}
92+
93+
get secondLast(): BreadcrumbItem {
94+
return this.shouldShowOverflow ? this.items[this.items.length - 2] : null;
95+
}
96+
97+
get last(): BreadcrumbItem {
98+
return this.shouldShowOverflow ? this.items[this.items.length - 1] : null;
99+
}
100+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NgModule } from "@angular/core";
2+
import { CommonModule } from "@angular/common";
3+
4+
import { DialogModule } from "../";
5+
6+
import { Breadcrumb } from "./breadcrumb.component";
7+
import { BreadcrumbItemComponent } from "./breadcrumb-item.component";
8+
9+
export { Breadcrumb } from "./breadcrumb.component";
10+
export { BreadcrumbItemComponent } from "./breadcrumb-item.component";
11+
export { BreadcrumbItem } from "./breadcrumb-item.interface";
12+
13+
@NgModule({
14+
declarations: [
15+
Breadcrumb,
16+
BreadcrumbItemComponent
17+
],
18+
exports: [
19+
Breadcrumb,
20+
BreadcrumbItemComponent
21+
],
22+
imports: [
23+
CommonModule,
24+
DialogModule
25+
]
26+
})
27+
export class BreadcrumbModule { }
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { storiesOf, moduleMetadata } from "@storybook/angular";
2+
import { withKnobs, boolean, number } from "@storybook/addon-knobs/angular";
3+
4+
import { BreadcrumbModule, DialogModule } from "../";
5+
import { BreadcrumbItem } from "../breadcrumb/breadcrumb-item.interface";
6+
7+
let breadcrumbItems;
8+
9+
const createBreadcrumbItems = (count: number): Array<BreadcrumbItem> => {
10+
if (breadcrumbItems && count === breadcrumbItems.length) {
11+
return breadcrumbItems;
12+
}
13+
breadcrumbItems = Array(count).fill(0).map((x, i) => ({ content: " Breadcrumb " + (i + 1), href: "#" + (i + 1) }));
14+
return breadcrumbItems;
15+
};
16+
17+
storiesOf("Breadcrumb", module)
18+
.addDecorator(
19+
moduleMetadata({
20+
imports: [
21+
BreadcrumbModule,
22+
DialogModule
23+
]
24+
})
25+
)
26+
.addDecorator(withKnobs)
27+
.add("Basic", () => ({
28+
template: `
29+
<ibm-breadcrumb [noTrailingSlash]="noTrailingSlash">
30+
<ibm-breadcrumb-item href="#1">
31+
Breadcrumb 1
32+
</ibm-breadcrumb-item>
33+
<ibm-breadcrumb-item href="#2">
34+
Breadcrumb 2
35+
</ibm-breadcrumb-item>
36+
<ibm-breadcrumb-item href="#3">
37+
Breadcrumb 3
38+
</ibm-breadcrumb-item>
39+
<ibm-breadcrumb-item href="#4">
40+
Breadcrumb 4
41+
</ibm-breadcrumb-item>
42+
</ibm-breadcrumb>`,
43+
props: {
44+
noTrailingSlash: boolean("noTrailingSlash", true)
45+
}
46+
}))
47+
.add("Model", () => ({
48+
template: `
49+
<ibm-breadcrumb
50+
[noTrailingSlash]="noTrailingSlash"
51+
[threshold]="threshold"
52+
[items]="items(itemCount)">
53+
</ibm-breadcrumb>`,
54+
props: {
55+
noTrailingSlash: boolean("noTrailingSlash", true),
56+
itemCount: number("itemCount", 10),
57+
threshold: number("threshold", 4),
58+
items: createBreadcrumbItems
59+
}
60+
}));

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ export * from "./switch/switch.module";
3030
export * from "./table/table.module";
3131
export * from "./tabs/tabs.module";
3232
export * from "./tiles/tiles.module";
33+
export * from "./breadcrumb/breadcrumb.module";

0 commit comments

Comments
 (0)