Skip to content

Commit

Permalink
feat(description): add first iteration of collapsible
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlj95 committed Sep 19, 2023
1 parent 1a8e88a commit 281ef5a
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="item" (click)="onClick()" [tabindex]="isCollapsable ? 0 : -1" (keydown)="onKeydown($event)">
<span class="symbol material-symbols-outlined">{{ line.symbol }}</span>
<span class="content" [innerHtml]="sanitizer.bypassSecurityTrustHtml(line.html)"></span>
<div class="collapsable-buttons" *ngIf="isCollapsable">
<button type="button" *ngIf="!collapsed" class="collapse material-symbols-outlined">{{ Icon.ExpandLess }}</button>
<button type="button" *ngIf="collapsed" class="expand material-symbols-outlined">{{ Icon.ExpandMore }}</button>
</div>
</div>
<ul *ngIf="line.lines && line.lines.length && !collapsed">
<li *ngFor="let subLine of line.lines">
<app-description-line [line]="subLine" [indentLevel]="indentLevel+1"></app-description-line>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@use "margins";

app-description-line {
div.item {
display: flex;
align-items: center;
word-break: break-word;
font-size: 1.5rem;

a {
text-decoration-line: underline;
text-decoration-thickness: 1px;
}

.symbol {
margin-right: margins.$s;
}

.material-symbols-outlined {
font-size: 1em;
font-variation-settings: 'FILL' 1,
'wght' 700,
'GRAD' 0,
'opsz' 24
}

button {
background: transparent;
color: inherit;
border: none;
cursor: pointer;
vertical-align: -3px;
}
}

ul > li {
display: flex;
flex-direction: column;
gap: margins.$m;

> app-description-line {
margin-left: margins.$l;
}
}

&:has(> ul):not([data-indent-level="0"]) {
> .item {
cursor: pointer;
}

> button {
cursor: pointer;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MATERIAL_SYMBOLS_CLASS } from '../../../../test/constants';
import { getComponentSelector } from '../../../../test/helpers/component-testers';
import { Build, Code, History } from '../../../material-symbols';
import { DescriptionLineComponent } from './description-line.component';

describe('DescriptionLineComponent', () => {
let component: DescriptionLineComponent;
let fixture: ComponentFixture<DescriptionLineComponent>;
const fakeChildrenLines = [
{symbol: History, html: 'Line 1.1 HTML', text: 'Line 1.1 Text'},
{symbol: Code, html: 'Line 1.2 HTML', text: 'Line 1.2 Text'},
]
const fakeDescriptionLine =
{symbol: Build, html: 'Line 1 HTML', text: 'Line 1 Text', lines: fakeChildrenLines}

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DescriptionLineComponent],
});
fixture = TestBed.createComponent(DescriptionLineComponent);
component = fixture.componentInstance;
component.line = fakeDescriptionLine;
fixture.detectChanges();
});

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

it('should display symbol', () => {
const symbolElement = fixture.debugElement.query(By.css('.symbol'))

expect(symbolElement.classes[MATERIAL_SYMBOLS_CLASS]).toBeTrue()
expect(symbolElement.nativeElement.textContent).toEqual(fakeDescriptionLine.symbol)
})

it('should display html content', () => {
const contentElement = fixture.debugElement.query(By.css('.content'))

expect(contentElement.nativeElement.innerHTML).toEqual(fakeDescriptionLine.html)
})


describe('when no children lines present', () => {
beforeEach(() => {
component.line = {
...fakeDescriptionLine,
lines: undefined,
}
fixture.detectChanges()
})
it('should not include any list', () => {
const listElement = fixture.debugElement.query(By.css('ul'))

expect(listElement).toBeNull()
})
})

describe('when children lines empty', () => {
beforeEach(() => {
component.line = {
...fakeDescriptionLine,
lines: [],
}
fixture.detectChanges()
})
it('should not include any list', () => {
const listElement = fixture.debugElement.query(By.css('ul'))

expect(listElement).toBeNull()
})
})

describe('when children lines exist', () => {
let fakeIndentLevel: number;
beforeEach(() => {
fakeIndentLevel = 4;
component.line = {
...fakeDescriptionLine,
lines: fakeChildrenLines,
}
component.indentLevel = fakeIndentLevel;
fixture.detectChanges()
})
it('should render them using self component and augmenting indentation level', () => {
const childrenLineElements = fixture.debugElement.queryAll(
By.css(getComponentSelector(DescriptionLineComponent)),
)

expect(childrenLineElements.length).toBe(fakeChildrenLines.length)
childrenLineElements.forEach((childLineElement, index) => {
const childLine = fakeChildrenLines[index];
expect(childLineElement.componentInstance.line).withContext(`child line ${index}`).toEqual(childLine)
expect(childLineElement.componentInstance.indentLevel).withContext(`child line ${index} indent`).toEqual(fakeIndentLevel+1)
})
})
})
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, HostBinding, Input, ViewEncapsulation } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { ExpandLess, ExpandMore } from '../../../material-symbols';
import { DescriptionLine } from '../../../metadata';

@Component({
selector: 'app-description-line',
templateUrl: './description-line.component.html',
styleUrls: ['./description-line.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class DescriptionLineComponent {
@Input({required: true}) public line!: DescriptionLine;
@Input() @HostBinding('attr.data-indent-level') public indentLevel = 0;
@Input() @HostBinding('attr.data-collapsed') public collapsed: boolean = false;

protected readonly Icon = {
ExpandMore: ExpandMore,
ExpandLess: ExpandLess,
}

constructor(
protected sanitizer: DomSanitizer,
) {
}

protected get isCollapsable() {
return this.indentLevel > 0 && this.line.lines && this.line.lines.length
}

protected onKeydown($event: KeyboardEvent) {
if($event.key == "Enter" || $event.key == " ") {
this.toggleCollapse();
}
}

protected onClick() {
this.toggleCollapse();
}

private toggleCollapse() {
if(!this.isCollapsable) return;

this.collapsed = !this.collapsed;
}
}
25 changes: 3 additions & 22 deletions src/app/about/description/description.component.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
<ul>
<ng-container *ngFor="let descriptionLine of descriptionLines"
[ngTemplateOutlet]="lineTemplate"
[ngTemplateOutletContext]="{line:descriptionLine}"
>
</ng-container>
<ng-template #lineTemplate let-line='line'>

<li>
<div class="item">
<span class="material-symbols-outlined">{{ line.symbol }}</span>
<span [innerHtml]="line.html">></span>
</div>
<ul *ngIf="line.lines">
<!--suppress TypeScriptValidateJSTypes -->
<ng-container *ngFor="let subLine of line.lines"
[ngTemplateOutlet]="lineTemplate"
[ngTemplateOutletContext]="{line:subLine}"
>
</ng-container>
</ul>
</li>
</ng-template>
<li *ngFor="let descriptionLine of metadata.descriptionLines">
<app-description-line [line]="descriptionLine"></app-description-line>
</li>
</ul>
44 changes: 7 additions & 37 deletions src/app/about/description/description.component.scss
Original file line number Diff line number Diff line change
@@ -1,55 +1,25 @@
@use "margins";
@use "paddings";

:host {
app-description {
display: flex;
flex-direction: column;
justify-content: center;
padding: paddings.$m;

ul {
ul, app-description-line {
display: flex;
flex-direction: column;
gap: margins.$m;
}

div.item {
display: flex;
align-items: center;
column-gap: margins.$s;
row-gap: margins.$m;
word-break: break-word;

::ng-deep a {
text-decoration-line: underline;
text-decoration-thickness: 1px;
}
::ng-deep a.craft {
text-decoration-line: underline;
text-decoration-color: currentColor;
}
}

ul > li {
font-size: 1.5rem;
display: flex;
flex-direction: column;
gap: margins.$m;

> ul {
margin-left: margins.$l;
}
// Content-specific customizations
a.craft {
text-decoration-line: underline;
text-decoration-color: currentColor;
}

> ul > li > ul > li > .item {
app-description-line[ng-reflect-indent-level="1"] > .item {
font-weight: bold;
}

.material-symbols-outlined {
font-size: 1em;
font-variation-settings: 'FILL' 1,
'wght' 700,
'GRAD' 0,
'opsz' 24
}
}
37 changes: 16 additions & 21 deletions src/app/about/description/description.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MockProvider } from 'ng-mocks';
import { MATERIAL_SYMBOLS_CLASS_SELECTOR } from '../../../test/constants';
import { MockComponent, MockProvider } from 'ng-mocks';
import { getComponentSelector } from '../../../test/helpers/component-testers';
import { METADATA } from '../../common/injection-tokens';
import { Build, Code, History } from '../../material-symbols';
import { DescriptionLine, Metadata } from '../../metadata';
import { DescriptionLineComponent } from './description-line/description-line.component';

import { DescriptionComponent } from './description.component';

describe('DescriptionComponent', () => {
let component: DescriptionComponent;
let fixture: ComponentFixture<DescriptionComponent>;
type FakeHtmlDescriptionLine =
Omit<DescriptionLine, 'text' | 'lines'>
& { lines?: ReadonlyArray<FakeHtmlDescriptionLine> }
const fakeDescriptionLines: ReadonlyArray<FakeHtmlDescriptionLine> = [
{symbol: Code, html: 'Foo bar'},
const fakeDescriptionLines: ReadonlyArray<DescriptionLine> = [
{symbol: Code, html: 'Line 1 HTML', text: 'Line 1 Text'},
{
symbol: Build, html: 'Bar foo', lines: [{symbol: History, html: 'Foo bar foo bar'}],
symbol: Build, html: 'Line 2 HTML', text: 'Line 2 Text', lines: [
{symbol: History, html: 'Line 2.1 HTML', text: 'Line 2.1 Text'},
],
},
]
const allFakeDescriptionLines = fakeDescriptionLines
.flatMap((line) => line.lines && line.lines.length ? [line, ...line.lines] : line)
const fakeMetadata: Metadata = ({
descriptionLines: fakeDescriptionLines,
} as Pick<Metadata, 'descriptionLines'>) as Metadata;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DescriptionComponent],
declarations: [
DescriptionComponent,
MockComponent(DescriptionLineComponent),
],
providers: [
MockProvider(METADATA, fakeMetadata),
],
Expand All @@ -42,17 +43,11 @@ describe('DescriptionComponent', () => {
expect(component).toBeTruthy();
});

it('should contain all description lines with its symbol and text', () => {
const lineElements = fixture.debugElement.queryAll(By.css('.item'));
expect(lineElements.length).toBe(allFakeDescriptionLines.length);
it('should render all parent description lines with the component', () => {
const lineElements = fixture.debugElement.queryAll(By.css(getComponentSelector(DescriptionLineComponent)));
expect(lineElements.length).toBe(fakeDescriptionLines.length);
lineElements.forEach((lineElement, index) => {
const descriptionLine= allFakeDescriptionLines[index];

const materialSymbolSpan = lineElement.query(MATERIAL_SYMBOLS_CLASS_SELECTOR)
expect(materialSymbolSpan.nativeElement.textContent).withContext(`item ${index} symbol`).toEqual(descriptionLine.symbol)

const textSpan = lineElement.query(By.css('span:nth-child(2)'))
expect(textSpan.nativeElement.innerHTML).withContext(`item ${index} text`).toEqual(descriptionLine.html)
expect(lineElement.componentInstance.line).withContext(`line ${index}`).toEqual(fakeDescriptionLines[index])
});
})
});
Loading

0 comments on commit 281ef5a

Please sign in to comment.