Skip to content

Commit

Permalink
feat(design): create DaffBreadcrumbComponent (#3028)
Browse files Browse the repository at this point in the history
  • Loading branch information
xelaint authored Sep 10, 2024
1 parent 8b278ae commit aa5ec26
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/design-land/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {

import { ACCORDION_EXAMPLES } from '@daffodil/design/accordion/examples';
import { ARTICLE_EXAMPLES } from '@daffodil/design/article/examples';
import { BREADCRUMB_EXAMPLES } from '@daffodil/design/breadcrumb/examples';
import { BUTTON_EXAMPLES } from '@daffodil/design/button/examples';
import { CALLOUT_EXAMPLES } from '@daffodil/design/callout/examples';
import { CARD_EXAMPLES } from '@daffodil/design/card/examples';
Expand Down Expand Up @@ -44,6 +45,7 @@ export class DesignLandAppComponent {
[
...ARTICLE_EXAMPLES,
...ACCORDION_EXAMPLES,
...BREADCRUMB_EXAMPLES,
...BUTTON_EXAMPLES,
...RADIO_EXAMPLES,
...CARD_EXAMPLES,
Expand Down
52 changes: 52 additions & 0 deletions libs/design/breadcrumb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Breadcrumb
Breadcrumbs are a secondary navigation that displays a user's location within a website or application.

## Overview
Breadcrumbs are a visual representation of the site's navigational hierarchy. They indicate the current page's location and allows users to easily move up to a parent level. It's required for breadcrumbs to be used with the native HTML `<ol>` element, and for each item to be an `<li>`. This offers additional context for assistive technology.

## Basic Breadcrumb
<design-land-example-viewer-container example="basic-breadcrumb"></design-land-example-viewer-container>

## Usage

## Within a standalone component
To use breadcrumb in a standalone component, import it directly into your custom component:

```ts
@Component({
selector: 'custom-component',
templateUrl: './custom-component.component.html',
standalone: true,
imports: [
DAFF_BREADCRUMB_COMPONENTS,
],
})
export class CustomComponent {}
```

## Within a module (deprecated)
To use breadcrumb in a module, import `DaffBreadcrumbModule` into your custom module:

```ts
import { NgModule } from '@angular/core';

import { DaffBreadcrumbModule } from '@daffodil/design/breadcrumb';

@NgModule({
declarations: [
CustomComponent,
],
exports: [
CustomComponent,
],
imports: [
DaffBreadcrumbModule,
],
})
export class CustomComponentModule { }
```

> This method is deprecated. It's recommended to update all custom components to standalone.
## Accessibility
Breadcrumbs should be wrapped in a native HTML `<nav>` element so that assistive technologies can present the breadcrumbs as a navigational element on the page. Use `aria-label="Breadcrumbs"` on the `nav` element to provide more context. `aria-current="page"` is added to a breadcrumb item when it's the current page, and `aria-current="false"` on all other items.
7 changes: 7 additions & 0 deletions libs/design/breadcrumb/examples/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../../scss"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<nav aria-label="Breadcrumb">
<ol daff-breadcrumb>
<li daffBreadcrumbItem>
<a routerLink="/link">Link</a>
</li>
<li daffBreadcrumbItem [active]="true">Active Link</li>
</ol>
</nav>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';

import { DAFF_BREADCRUMB_COMPONENTS } from '@daffodil/design/breadcrumb';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'basic-breadcrumb',
templateUrl: './basic-breadcrumb.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
DAFF_BREADCRUMB_COMPONENTS,
],
})
export class BasicBreadcrumbComponent {}
1 change: 1 addition & 0 deletions libs/design/breadcrumb/examples/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
5 changes: 5 additions & 0 deletions libs/design/breadcrumb/examples/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BasicBreadcrumbComponent } from './basic-breadcrumb/basic-breadcrumb.component';

export const BREADCRUMB_EXAMPLES = [
BasicBreadcrumbComponent,
];
7 changes: 7 additions & 0 deletions libs/design/breadcrumb/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../src/scss"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Component,
DebugElement,
} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffBreadcrumbItemDirective } from './breadcrumb-item.directive';

@Component({
template: `<li daffBreadcrumbItem [active]="active">Breadcrumb Item</li>`,
standalone: true,
imports: [
DaffBreadcrumbItemDirective,
],
})
class WrapperComponent {
active = false;
}

describe('@daffodil/design/breadcrumb | DaffBreadcrumbItemDirective', () => {
let wrapper: WrapperComponent;
let de: DebugElement;
let fixture: ComponentFixture<WrapperComponent>;
let component: DaffBreadcrumbItemDirective;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
WrapperComponent,
],
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;
de = fixture.debugElement.query(By.css('[daffBreadcrumbItem]'));
component = de.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(wrapper).toBeTruthy();
});

describe('li[daffBreadcrumbItem]', () => {
it('should add a class of "daff-breadcrumb__item" to the host element', () => {
expect(de.classes).toEqual(jasmine.objectContaining({
'daff-breadcrumb__item': true,
}));
});
});

it('should set the `aria-current` to page if it`s the active breadcrumb', () => {
wrapper.active = true;
fixture.detectChanges();

expect(de.nativeElement.ariaCurrent).toEqual('page');
});

it('should set the `aria-current` to false if it`s not the active breadcrumb', () => {
wrapper.active = false;
fixture.detectChanges();

expect(de.nativeElement.ariaCurrent).toEqual('false');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Directive,
HostBinding,
Input,
} from '@angular/core';

@Directive({
selector: 'li[daffBreadcrumbItem]',
standalone: true,
})
export class DaffBreadcrumbItemDirective {
@HostBinding('class.daff-breadcrumb__item') class = true;

@HostBinding('attr.aria-current') get ariaCurrent() {
return this.active ? 'page' : 'false';
}

/** Whether or not the breadcrumb item is active */
@Input() @HostBinding('class.active') active = false;

}
13 changes: 13 additions & 0 deletions libs/design/breadcrumb/src/breadcrumb-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use 'sass:map';
@use '../../scss/core';
@use '../../scss/theming';

@mixin daff-breadcrumb-theme($theme) {
$base: core.daff-map-deep-get($theme, 'core.base');
$base-contrast: core.daff-map-deep-get($theme, 'core.base-contrast');
$neutral: core.daff-map-deep-get($theme, 'core.neutral');

.daff-breadcrumb__item {
color: theming.daff-illuminate($base-contrast, $neutral, 4);
}
}
17 changes: 17 additions & 0 deletions libs/design/breadcrumb/src/breadcrumb.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';

import { DaffBreadcrumbComponent } from './breadcrumb/breadcrumb.component';
import { DaffBreadcrumbItemDirective } from './breadcrumb-item/breadcrumb-item.directive';

/** @deprecated in favor of {@link DAFF_BREADCRUMB_COMPONENTS} */
@NgModule({
imports: [
DaffBreadcrumbComponent,
DaffBreadcrumbItemDirective,
],
exports: [
DaffBreadcrumbComponent,
DaffBreadcrumbItemDirective,
],
})
export class DaffBreadcrumbModule { }
7 changes: 7 additions & 0 deletions libs/design/breadcrumb/src/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DaffBreadcrumbComponent } from './breadcrumb/breadcrumb.component';
import { DaffBreadcrumbItemDirective } from './breadcrumb-item/breadcrumb-item.directive';

export const DAFF_BREADCRUMB_COMPONENTS = <const> [
DaffBreadcrumbComponent,
DaffBreadcrumbItemDirective,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<ng-content select="[daffBreadcrumbItem]"></ng-content>
50 changes: 50 additions & 0 deletions libs/design/breadcrumb/src/breadcrumb/breadcrumb.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@use '../../../scss/typography' as t;
@use '../../../scss/state';

.daff-breadcrumb {
$root: &;
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;

&__item {
font-size: 1rem;

a {
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

&.active {
font-weight: 500;
}

&:not(:last-child) {
&::after {
content: '/';
color: currentColor;
font-weight: normal;
margin: 0 0.5rem;
}
}
}

&.daff-skeleton {
@include state.skeleton-screen(100%, 24px);
max-width: 290px;
width: 100%;

#{$root}__item {
visibility: hidden;

&::before {
content: unset;
}
}
}
}
66 changes: 66 additions & 0 deletions libs/design/breadcrumb/src/breadcrumb/breadcrumb.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Component,
DebugElement,
} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffBreadcrumbComponent } from './breadcrumb.component';

@Component({
template: `<ol daff-breadcrumb [skeleton]="skeleton"></ol>`,
standalone: true,
imports: [
DaffBreadcrumbComponent,
],
})

class WrapperComponent {
skeleton: boolean;
}

describe('@daffodil/design/breadcrumb | DaffBreadcrumbComponent', () => {
let wrapper: WrapperComponent;
let component: DaffBreadcrumbComponent;
let de: DebugElement;
let fixture: ComponentFixture<WrapperComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
WrapperComponent,
],
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;
de = fixture.debugElement.query(By.css('ol[daff-breadcrumb]'));
component = de.componentInstance;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should add a class of "daff-breadcrumb" to the host element', () => {
expect(de.classes).toEqual(jasmine.objectContaining({
'daff-breadcrumb': true,
}));
});

it('should take skeleton as an input', () => {
wrapper.skeleton = true;
fixture.detectChanges();

expect(de.nativeElement.classList.contains('daff-skeleton')).toEqual(true);
});
});
Loading

0 comments on commit aa5ec26

Please sign in to comment.