Skip to content
Open
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
3 changes: 3 additions & 0 deletions community/components/tags/tag/tag.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {

export type TagType = "primary" | "secondary" | "danger";

/**
* @deprecated Use Tag from TEDI-ready instead. This component will be removed from future versions.
*/
@Component({
selector: "tedi-tag",
imports: [SpinnerComponent, IconComponent, TediTranslationPipe],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export type ClosingButtonIconSize = 18 | 24;
templateUrl: "./closing-button.component.html",
styleUrl: "./closing-button.component.scss",
host: {
"[title]": "title()",
"[attr.aria-label]": "title()",
"[title]": "ariaLabel() || _defaultLabel()",
"[attr.aria-label]": "ariaLabel() || _defaultLabel()",
"[class.tedi-closing-button]": "true",
"[class.tedi-closing-button--small]": "size() === 'small'",
},
Expand All @@ -41,6 +41,11 @@ export class ClosingButtonComponent {
*/
iconSize = input<ClosingButtonIconSize>(24);

/**
* ARIA label to override default label "close"
*/
readonly ariaLabel = input<string | undefined>();

private translationService = inject(TediTranslationService);
title = this.translationService.track("close");
private readonly _defaultLabel = this.translationService.track("close");
}
8 changes: 8 additions & 0 deletions tedi/components/buttons/closing-button/closing-button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ describe("ClosingButtonComponent", () => {
expect(buttonElement.getAttribute("title")).toBe("Sulge");
expect(buttonElement.getAttribute("aria-label")).toBe("Sulge");
});

it("should override default aria-label when provided", () => {
fixture.componentRef.setInput("ariaLabel", "Eemalda");
fixture.detectChanges();

expect(buttonElement.getAttribute("title")).toBe("Eemalda");
expect(buttonElement.getAttribute("aria-label")).toBe("Eemalda");
});
});
1 change: 1 addition & 0 deletions tedi/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./loader";
export * from "./navigation";
export * from "./overlay";
export * from "./notifications";
export * from "./tags";
7 changes: 3 additions & 4 deletions tedi/components/loader/spinner/spinner.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<svg viewBox="22 22 44 44" aria-hidden="true">
<svg viewBox="0 0 44 44" aria-hidden="true">
<circle
class="tedi-spinner--inner"
cx="44"
cy="44"
r="20"
cx="22"
cy="22"
fill="none"
></circle>
</svg>
34 changes: 21 additions & 13 deletions tedi/components/loader/spinner/spinner.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ $spinner-colors: (
"primary": "loader-spinner-color-primary",
"secondary": "loader-spinner-color-secondary",
);
$spinner-sizes: (10 16 48);
$spinner-viewbox: 44;
$spinner-sizes: (
10: 1,
16: 2,
48: 4,
);

.tedi-spinner {
display: flex;
width: calc(var(--tedi-spinner-size, 16) * 1rem / 16);
height: calc(var(--tedi-spinner-size, 16) * 1rem / 16);
animation: 1.4s linear 0s infinite normal none running tedi-spinner-outer;

&--inner {
stroke-width: 4px;
stroke-dasharray: 80px, 200px;
// Convert stroke size in px to viewBox units: stroke px × (viewbox / size px)
stroke-width: calc(var(--tedi-spinner-stroke, 2) * #{$spinner-viewbox} / var(--tedi-spinner-size, 16));
r: calc(#{$spinner-viewbox / 2} - var(--tedi-spinner-stroke, 2) * #{$spinner-viewbox} / var(--tedi-spinner-size, 16) / 2);
stroke-dasharray: 80, 200;
stroke-dashoffset: 0;
animation: 1.4s ease-in-out 0s infinite normal none running
tedi-spinner-inner;
animation: 1.4s ease-in-out 0s infinite normal none running tedi-spinner-inner;

@media (prefers-reduced-motion: reduce) {
animation: none;
Expand All @@ -26,10 +34,10 @@ $spinner-sizes: (10 16 48);
}
}

@each $size in $spinner-sizes {
@each $size, $stroke in $spinner-sizes {
&--size-#{$size} {
width: #{$size}px;
height: #{$size}px;
--tedi-spinner-size: #{$size};
--tedi-spinner-stroke: #{$stroke};
}
}

Expand All @@ -50,17 +58,17 @@ $spinner-sizes: (10 16 48);

@keyframes tedi-spinner-inner {
0% {
stroke-dasharray: 1px, 200px;
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}

50% {
stroke-dasharray: 100px, 200px;
stroke-dashoffset: -15px;
stroke-dasharray: 100, 200;
stroke-dashoffset: -15;
}

100% {
stroke-dasharray: 100px, 200px;
stroke-dashoffset: -125px;
stroke-dasharray: 100, 200;
stroke-dashoffset: -125;
}
}
1 change: 1 addition & 0 deletions tedi/components/tags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tag/tag.component";
26 changes: 26 additions & 0 deletions tedi/components/tags/tag/tag.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@if (type() === "danger") {
<span class="tedi-tag__icon-wrapper">
<tedi-icon name="error" color="danger" [size]="16" />
</span>
}

<span class="tedi-tag__content" [attr.id]="uniqueId">
<ng-content />
</span>

@if (loading()) {
<span class="tedi-tag__spinner-wrapper">
<tedi-spinner [size]="48" />
</span>
} @else if (closable()) {
<button
type="button"
tedi-closing-button
size="small"
[iconSize]="18"
(click)="handleClose($event)"
[attr.aria-describedby]="uniqueId"
[ariaLabel]="'remove' | tediTranslate"
>
</button>
}
55 changes: 55 additions & 0 deletions tedi/components/tags/tag/tag.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.tedi-tag {
--_background-color: var(--tag-primary-background);
--_border-color: var(--tag-primary-border);
--_line-height: var(--body-small-regular-line-height);

display: inline-flex;
gap: var(--tag-default-padding-x);
align-items: flex-start;
padding: 0 var(--tag-default-padding-x);
font-family: var(--family-default);
font-size: var(--body-small-regular-size);
font-weight: var(--body-small-regular-weight);
line-height: var(--_line-height);
color: var(--general-text-primary);
background-color: var(--_background-color);
border-radius: var(--tag-default-radius);
box-shadow: inset 0 0 0 1px var(--_border-color);

&__content {
padding: var(--tag-default-padding-y) 0;
}

&__icon-wrapper {
line-height: var(--_line-height);
}

&__spinner-wrapper {
display: flex;
align-items: center;
height: calc(var(--_line-height) + var(--tag-default-padding-y));

tedi-spinner {
--tedi-spinner-size: 12;
--tedi-spinner-stroke: 2;
}
}

&--closable:not(.tedi-tag--loading) {
padding-right: 0;

.tedi-closing-button {
border-radius: 0 var(--button-radius-sm) var(--button-radius-sm) 0;
}
}

&--secondary {
--_background-color: var(--tag-secondary-background);
--_border-color: var(--tag-secondary-border);
}

&--danger {
--_background-color: var(--tag-danger-background);
--_border-color: var(--tag-danger-border);
}
}
126 changes: 126 additions & 0 deletions tedi/components/tags/tag/tag.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { TagComponent, TagType } from "./tag.component";
import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token";

describe("TagComponent", () => {
let component: TagComponent;
let fixture: ComponentFixture<TagComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TagComponent],
providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }],
}).compileComponents();

fixture = TestBed.createComponent(TagComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

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

it("should have default values", () => {
expect(component.loading()).toBe(false);
expect(component.closable()).toBe(false);
expect(component.type()).toBe("primary");
});

it("should have tedi-tag class", () => {
expect(fixture.nativeElement.classList).toContain("tedi-tag");
});

it("should apply correct type class", () => {
const types: TagType[] = ["primary", "secondary", "danger"];

for (const type of types) {
fixture.componentRef.setInput("type", type);
fixture.detectChanges();

expect(fixture.nativeElement.classList).toContain(`tedi-tag--${type}`);
}
});

it("should show danger icon when type is danger", () => {
fixture.componentRef.setInput("type", "danger");
fixture.detectChanges();

const iconElement = fixture.nativeElement.querySelector("tedi-icon");
expect(iconElement).toBeTruthy();
expect(iconElement.textContent).toBe("error");
});

it("should not show danger icon when type is not danger", () => {
fixture.componentRef.setInput("type", "primary");
fixture.detectChanges();

const iconWrapper = fixture.nativeElement.querySelector(".tedi-tag__icon-wrapper");
expect(iconWrapper).toBeFalsy();
});

it("should show spinner when loading is true", () => {
fixture.componentRef.setInput("loading", true);
fixture.detectChanges();

const spinner = fixture.nativeElement.querySelector("tedi-spinner");
expect(spinner).toBeTruthy();
expect(fixture.nativeElement.classList).toContain("tedi-tag--loading");
});

it("should not show spinner when loading is false", () => {
fixture.componentRef.setInput("loading", false);
fixture.detectChanges();

const spinner = fixture.nativeElement.querySelector("tedi-spinner");
expect(spinner).toBeFalsy();
});

it("should show close button when closable is true", () => {
fixture.componentRef.setInput("closable", true);
fixture.detectChanges();

const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
expect(closeButton).toBeTruthy();
expect(fixture.nativeElement.classList).toContain("tedi-tag--closable");
});

it("should not show close button when closable is false", () => {
fixture.componentRef.setInput("closable", false);
fixture.detectChanges();

const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
expect(closeButton).toBeFalsy();
});

it("should not show close button when loading is true even if closable is true", () => {
fixture.componentRef.setInput("closable", true);
fixture.componentRef.setInput("loading", true);
fixture.detectChanges();

const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
const spinner = fixture.nativeElement.querySelector("tedi-spinner");

expect(closeButton).toBeFalsy();
expect(spinner).toBeTruthy();
});

it("should emit closed event when close button is clicked", () => {
fixture.componentRef.setInput("closable", true);
fixture.detectChanges();

const closedSpy = jest.fn();
component.closed.subscribe(closedSpy);

const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
closeButton.click();
fixture.detectChanges();

expect(closedSpy).toHaveBeenCalled();
});

it("should render content", () => {
const contentElement = fixture.nativeElement.querySelector(".tedi-tag__content");
expect(contentElement).toBeTruthy();
});
});
Loading