Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@if (firstItem(); as first) {
<li class="tedi-sidenav-dropdown-group__parent-wrapper">
<div
class="tedi-sidenav-dropdown-item tedi-sidenav-dropdown-group__parent"
[class.tedi-sidenav-dropdown-item--selected]="first.selected()"
>
@if (first.href()) {
<a [href]="first.href()" class="tedi-sidenav-dropdown-item__trigger">
{{ first.textContent() }}
</a>
} @else if (first.route()) {
<a [routerLink]="first.route()" class="tedi-sidenav-dropdown-item__trigger">
{{ first.textContent() }}
</a>
} @else {
<span class="tedi-sidenav-dropdown-item__trigger">
{{ first.textContent() }}
</span>
}
</div>

@if (restItems().length > 0) {
<ul class="tedi-sidenav-dropdown-group__list">
@for (item of restItems(); track item) {
<li
class="tedi-sidenav-dropdown-item tedi-sidenav-dropdown-group__item"
[class.tedi-sidenav-dropdown-item--selected]="item.selected()"
>
@if (item.href()) {
<a [href]="item.href()" class="tedi-sidenav-dropdown-item__trigger">
{{ item.textContent() }}
</a>
} @else if (item.route()) {
<a [routerLink]="item.route()" class="tedi-sidenav-dropdown-item__trigger">
{{ item.textContent() }}
</a>
} @else {
<span class="tedi-sidenav-dropdown-item__trigger">
{{ item.textContent() }}
</span>
}
</li>
}
</ul>
}
</li>
}

<!-- Hidden container for @ContentChildren query -->
<div style="display: none" aria-hidden="true">
<ng-content></ng-content>
</div>
Original file line number Diff line number Diff line change
@@ -1,25 +1,62 @@
.tedi-sidenav-dropdown-group {
.tedi-sidenav-dropdown-item {
&:first-of-type {
&::before {
top: 50%;
width: var(--_sidenav-tree-bullet-size);
height: var(--_sidenav-tree-bullet-size);
border-radius: 50%;
transform: translateY(-50%) translateX(calc(-50% + var(--borders-01)));
}
--_group-padding-left: var(--navigation-vertical-item-padding-left-level-2);

&::after {
width: 0;
}
&__parent-wrapper {
position: relative;
display: block;
list-style: none;

&::before {
position: absolute;
top: 0;
left: calc(
var(--_group-padding-left) + var(--_sidenav-tree-left-padding)
);
width: var(--_sidenav-tree-trunk-width);
height: calc(var(--_sidenav-dropdown-item-height) / 2);
content: "";
background-color: var(--navigation-vertical-tree-brand-default);
}
}

&__list {
padding: 0;
margin: 0;
list-style: none;
}

&:not(:only-child) {
&::after {
top: 50%;
width: var(--_sidenav-tree-trunk-width);
height: 50%;
transform: translateY(0);
}
.tedi-sidenav-dropdown-item.tedi-sidenav-dropdown-group__parent {
position: relative;

&::before {
position: absolute;
top: 50%;
left: calc(var(--_padding-left) + var(--_sidenav-tree-left-padding));
width: var(--_sidenav-tree-bullet-size);
height: var(--_sidenav-tree-bullet-size);
content: "";
background-color: var(--navigation-vertical-tree-brand-default);
border-radius: 50%;
transform: translateY(-50%)
translateX(calc(-50% + calc(var(--_sidenav-tree-trunk-width) / 2)));
}

&::after {
position: absolute;
top: 0;
left: calc(var(--_padding-left) + var(--_sidenav-tree-left-padding));
width: var(--_sidenav-tree-trunk-width);
height: 100%;
content: "";
background-color: var(--navigation-vertical-tree-brand-default);
transform: translateY(50%);
}
}

&__item {
&:last-of-type {
&::before {
height: 50%;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { QueryList } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { SideNavDropdownGroupComponent } from "./sidenav-dropdown-group.component";
import { SideNavDropdownItemComponent } from "../sidenav-dropdown-item/sidenav-dropdown-item.component";

describe("SideNavDropdownGroupComponent", () => {
let fixture: ComponentFixture<SideNavDropdownGroupComponent>;
Expand All @@ -22,4 +24,40 @@ describe("SideNavDropdownGroupComponent", () => {
it("should have the base CSS class", () => {
expect(groupEl.classList.contains("tedi-sidenav-dropdown-group")).toBe(true);
});

it("should set itemsArray from ContentChildren in ngAfterContentInit", () => {
const mockItems = {
toArray: () => [{ id: 1 }, { id: 2 }],
changes: { subscribe: jest.fn() },
} as unknown as QueryList<SideNavDropdownItemComponent>;

fixture.componentInstance.items = mockItems;
fixture.componentInstance.ngAfterContentInit();

expect(fixture.componentInstance.firstItem()).toEqual({ id: 1 });
expect(fixture.componentInstance.restItems()).toEqual([{ id: 2 }]);
});

it("should update itemsArray when items.changes emits", () => {
let changeCallback: () => void = () => { };
const mockItems = {
toArray: jest.fn().mockReturnValue([{ id: 1 }]),
changes: {
subscribe: (cb: () => void) => {
changeCallback = cb;
},
},
} as unknown as QueryList<SideNavDropdownItemComponent>;

fixture.componentInstance.items = mockItems;
fixture.componentInstance.ngAfterContentInit();

expect(fixture.componentInstance.firstItem()).toEqual({ id: 1 });

mockItems.toArray = jest.fn().mockReturnValue([{ id: 1 }, { id: 2 }, { id: 3 }]);
changeCallback();

expect(fixture.componentInstance.firstItem()).toEqual({ id: 1 });
expect(fixture.componentInstance.restItems()).toEqual([{ id: 2 }, { id: 3 }]);
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
computed,
ContentChildren,
QueryList,
signal,
ViewEncapsulation,
} from "@angular/core";
import { RouterLink } from "@angular/router";
import { SideNavDropdownItemComponent } from "../sidenav-dropdown-item/sidenav-dropdown-item.component";

@Component({
selector: "tedi-sidenav-dropdown-group",
standalone: true,
template: "<ng-content />",
templateUrl: "./sidenav-dropdown-group.component.html",
styleUrl: "./sidenav-dropdown-group.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
imports: [RouterLink],
host: {
"class": "tedi-sidenav-dropdown-group",
"role": "presentation",
"style": "display: contents",
},
})
export class SideNavDropdownGroupComponent {}
export class SideNavDropdownGroupComponent implements AfterContentInit {
@ContentChildren(SideNavDropdownItemComponent)
items!: QueryList<SideNavDropdownItemComponent>;

private itemsArray = signal<SideNavDropdownItemComponent[]>([]);

firstItem = computed(() => this.itemsArray()[0]);
restItems = computed(() => this.itemsArray().slice(1));

// to keep same component composition structure but rearrange dom elements inside the group for correct html semantics
ngAfterContentInit(): void {
this.itemsArray.set(this.items.toArray());

this.items.changes.subscribe(() => {
this.itemsArray.set(this.items.toArray());
});
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
@if (href()) {
<a [href]="href()">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else if (route()) {
<a [routerLink]="route()">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else {
<ng-container *ngTemplateOutlet="content"></ng-container>
}
<li [class]="classes()">
@if (href()) {
<a [href]="href()" class="tedi-sidenav-dropdown-item__trigger">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else if (route()) {
<a [routerLink]="route()" class="tedi-sidenav-dropdown-item__trigger">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else {
<div class="tedi-sidenav-dropdown-item__trigger">
<ng-container *ngTemplateOutlet="content"></ng-container>
</div>
}
</li>

<ng-template #content>
<ng-content></ng-content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
position: relative;
display: block;
min-height: var(--_sidenav-dropdown-item-height);
padding: var(--_sidenav-item-padding-y) var(--_sidenav-item-padding-right)
var(--_sidenav-item-padding-y)
calc(var(--_padding-left) + var(--_sidenav-tree-container) + var(--_gap));
color: var(--navigation-vertical-item-text);
cursor: pointer;

&:hover {
background: var(--navigation-vertical-item-background-hover);
Expand All @@ -20,6 +16,22 @@
background: var(--navigation-vertical-item-background-active);
}

&__trigger {
display: block;
padding: var(--_sidenav-item-padding-y) var(--_sidenav-item-padding-right)
var(--_sidenav-item-padding-y)
calc(var(--_padding-left) + var(--_sidenav-tree-container) + var(--_gap));
color: inherit;
text-decoration: none;
cursor: pointer;

&:focus-visible {
outline: none;
background: var(--navigation-vertical-item-background-focus);
box-shadow: var(--_sidenav-focus-ring);
}
}

&::before {
position: absolute;
top: 0;
Expand All @@ -42,22 +54,19 @@
}

&--parent {
padding-left: var(--dropdown-item-padding-x);
.tedi-sidenav-dropdown-item__trigger {
padding-left: var(--dropdown-item-padding-x);
}

&::before,
&::after {
display: none;
}
}
}

&:last-of-type {
&::before {
height: 50%;
}
}

a {
color: inherit;
text-decoration: none;
}
tedi-sidenav-dropdown-item:last-child > .tedi-sidenav-dropdown-item::before,
tedi-sidenav-dropdown-item:has(+ tedi-sidenav-dropdown-group)
> .tedi-sidenav-dropdown-item::before {
height: 50%;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,38 @@ import { SideNavDropdownItemComponent } from "./sidenav-dropdown-item.component"

describe("SideNavDropdownItemComponent", () => {
let fixture: ComponentFixture<SideNavDropdownItemComponent>;
let dropdownItemEl: HTMLElement;
let liElement: HTMLLIElement;

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

fixture = TestBed.createComponent(SideNavDropdownItemComponent);
dropdownItemEl = fixture.nativeElement;
fixture.detectChanges();
liElement = fixture.nativeElement.querySelector("li");
});

it("should create the component", () => {
expect(fixture.componentInstance).toBeTruthy();
});

it("should have the base CSS class", () => {
expect(dropdownItemEl.classList.contains("tedi-sidenav-dropdown-item")).toBe(true);
it("should have the base CSS class on li element", () => {
expect(liElement.classList.contains("tedi-sidenav-dropdown-item")).toBe(true);
});

it("should add selected class when `selected` input is true", () => {
fixture.componentRef.setInput("selected", true);
fixture.detectChanges();
expect(dropdownItemEl.classList.contains("tedi-sidenav-dropdown-item--selected")).toBe(true);
expect(liElement.classList.contains("tedi-sidenav-dropdown-item--selected")).toBe(true);
});

it("should set textContent value in ngAfterViewInit when text exists", () => {
fixture.nativeElement.textContent = "Test Item Text";

fixture.componentInstance.ngAfterViewInit();

expect(fixture.componentInstance.textContent()).toBe("Test Item Text");
});

});
Loading