Skip to content

Commit 849db17

Browse files
author
m2rt
committed
feat(tag): new tedi-ready component #297
also added ariaLabel input to closing-button, refactored spinner svg rendering
1 parent bf37a35 commit 849db17

11 files changed

Lines changed: 549 additions & 20 deletions

File tree

tedi/components/buttons/closing-button/closing-button.component.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export type ClosingButtonIconSize = 18 | 24;
1919
templateUrl: "./closing-button.component.html",
2020
styleUrl: "./closing-button.component.scss",
2121
host: {
22-
"[title]": "title()",
23-
"[attr.aria-label]": "title()",
22+
"[title]": "ariaLabel() || _defaultLabel()",
23+
"[attr.aria-label]": "ariaLabel() || _defaultLabel()",
2424
"[class.tedi-closing-button]": "true",
2525
"[class.tedi-closing-button--small]": "size() === 'small'",
2626
},
@@ -41,6 +41,11 @@ export class ClosingButtonComponent {
4141
*/
4242
iconSize = input<ClosingButtonIconSize>(24);
4343

44+
/**
45+
* ARIA label to override default label "close"
46+
*/
47+
readonly ariaLabel = input<string | undefined>();
48+
4449
private translationService = inject(TediTranslationService);
45-
title = this.translationService.track("close");
50+
private readonly _defaultLabel = this.translationService.track("close");
4651
}

tedi/components/buttons/closing-button/closing-button.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,12 @@ describe("ClosingButtonComponent", () => {
4747
expect(buttonElement.getAttribute("title")).toBe("Sulge");
4848
expect(buttonElement.getAttribute("aria-label")).toBe("Sulge");
4949
});
50+
51+
it("should override default aria-label when provided", () => {
52+
fixture.componentRef.setInput("ariaLabel", "Eemalda");
53+
fixture.detectChanges();
54+
55+
expect(buttonElement.getAttribute("title")).toBe("Eemalda");
56+
expect(buttonElement.getAttribute("aria-label")).toBe("Eemalda");
57+
});
5058
});

tedi/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./loader";
88
export * from "./navigation";
99
export * from "./overlay";
1010
export * from "./notifications";
11+
export * from "./tags";
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
<svg viewBox="22 22 44 44" aria-hidden="true">
1+
<svg viewBox="0 0 44 44" aria-hidden="true">
22
<circle
33
class="tedi-spinner--inner"
4-
cx="44"
5-
cy="44"
6-
r="20"
4+
cx="22"
5+
cy="22"
76
fill="none"
87
></circle>
98
</svg>

tedi/components/loader/spinner/spinner.component.scss

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@ $spinner-colors: (
22
"primary": "loader-spinner-color-primary",
33
"secondary": "loader-spinner-color-secondary",
44
);
5-
$spinner-sizes: (10 16 48);
5+
$spinner-viewbox: 44;
6+
$spinner-sizes: (
7+
10: 1,
8+
16: 2,
9+
48: 4,
10+
);
611

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

1118
&--inner {
12-
stroke-width: 4px;
13-
stroke-dasharray: 80px, 200px;
19+
// Convert stroke size in px to viewBox units: stroke px × (viewbox / size px)
20+
stroke-width: calc(var(--tedi-spinner-stroke, 2) * #{$spinner-viewbox} / var(--tedi-spinner-size, 16));
21+
r: calc(#{$spinner-viewbox / 2} - var(--tedi-spinner-stroke, 2) * #{$spinner-viewbox} / var(--tedi-spinner-size, 16) / 2);
22+
stroke-dasharray: 80, 200;
1423
stroke-dashoffset: 0;
15-
animation: 1.4s ease-in-out 0s infinite normal none running
16-
tedi-spinner-inner;
24+
animation: 1.4s ease-in-out 0s infinite normal none running tedi-spinner-inner;
1725

1826
@media (prefers-reduced-motion: reduce) {
1927
animation: none;
@@ -26,10 +34,10 @@ $spinner-sizes: (10 16 48);
2634
}
2735
}
2836

29-
@each $size in $spinner-sizes {
37+
@each $size, $stroke in $spinner-sizes {
3038
&--size-#{$size} {
31-
width: #{$size}px;
32-
height: #{$size}px;
39+
--tedi-spinner-size: #{$size};
40+
--tedi-spinner-stroke: #{$stroke};
3341
}
3442
}
3543

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

5159
@keyframes tedi-spinner-inner {
5260
0% {
53-
stroke-dasharray: 1px, 200px;
61+
stroke-dasharray: 1, 200;
5462
stroke-dashoffset: 0;
5563
}
5664

5765
50% {
58-
stroke-dasharray: 100px, 200px;
59-
stroke-dashoffset: -15px;
66+
stroke-dasharray: 100, 200;
67+
stroke-dashoffset: -15;
6068
}
6169

6270
100% {
63-
stroke-dasharray: 100px, 200px;
64-
stroke-dashoffset: -125px;
71+
stroke-dasharray: 100, 200;
72+
stroke-dashoffset: -125;
6573
}
6674
}

tedi/components/tags/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./tag/tag.component";
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@if (type() === "danger") {
2+
<span class="tedi-tag__icon-wrapper">
3+
<tedi-icon name="error" color="danger" [size]="16" />
4+
</span>
5+
}
6+
7+
<span class="tedi-tag__content">
8+
<ng-content />
9+
</span>
10+
11+
@if (loading()) {
12+
<span class="tedi-tag__spinner-wrapper">
13+
<tedi-spinner [size]="48" />
14+
</span>
15+
} @else if (closable()) {
16+
<button
17+
type="button"
18+
tedi-closing-button
19+
size="small"
20+
[iconSize]="18"
21+
(click)="handleClose($event)"
22+
>
23+
</button>
24+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.tedi-tag {
2+
--_background-color: var(--tag-primary-background);
3+
--_border-color: var(--tag-primary-border);
4+
--_border-width: 1px;
5+
--_line-height: var(--body-small-regular-line-height);
6+
7+
display: inline-flex;
8+
gap: var(--tag-default-padding-x);
9+
align-items: flex-start;
10+
padding: 0 var(--tag-default-padding-x);
11+
overflow: hidden;
12+
font-family: var(--family-default);
13+
font-size: var(--body-small-regular-size);
14+
font-weight: var(--body-small-regular-weight);
15+
line-height: var(--_line-height);
16+
color: var(--general-text-primary);
17+
background-color: var(--_background-color);
18+
border: var(--_border-width) solid;
19+
border-color: var(--_border-color);
20+
border-radius: var(--tag-default-radius);
21+
22+
23+
&__content {
24+
padding: calc(var(--tag-default-padding-y) - var(--_border-width)) 0;
25+
}
26+
27+
&__icon-wrapper {
28+
line-height: var(--_line-height);
29+
}
30+
31+
&__spinner-wrapper {
32+
display: flex;
33+
align-items: center;
34+
height: calc(var(--_line-height) + var(--tag-default-padding-y));
35+
36+
tedi-spinner {
37+
--tedi-spinner-size: 12;
38+
--tedi-spinner-stroke: 2;
39+
}
40+
}
41+
42+
&--closable:not(.tedi-tag--loading) {
43+
padding-right: 0;
44+
45+
.tedi-closing-button {
46+
margin-block: -1px;
47+
margin-right: -1px;
48+
}
49+
}
50+
51+
&--secondary {
52+
--_background-color: var(--tag-secondary-background);
53+
--_border-color: var(--tag-secondary-border);
54+
}
55+
56+
&--danger {
57+
--_background-color: var(--tag-danger-background);
58+
--_border-color: var(--tag-danger-border);
59+
}
60+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { TagComponent, TagType } from "./tag.component";
3+
import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token";
4+
5+
describe("TagComponent", () => {
6+
let component: TagComponent;
7+
let fixture: ComponentFixture<TagComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [TagComponent],
12+
providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }],
13+
}).compileComponents();
14+
15+
fixture = TestBed.createComponent(TagComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it("should create", () => {
21+
expect(component).toBeTruthy();
22+
});
23+
24+
it("should have default values", () => {
25+
expect(component.loading()).toBe(false);
26+
expect(component.closable()).toBe(false);
27+
expect(component.type()).toBe("primary");
28+
});
29+
30+
it("should have tedi-tag class", () => {
31+
expect(fixture.nativeElement.classList).toContain("tedi-tag");
32+
});
33+
34+
it("should apply correct type class", () => {
35+
const types: TagType[] = ["primary", "secondary", "danger"];
36+
37+
for (const type of types) {
38+
fixture.componentRef.setInput("type", type);
39+
fixture.detectChanges();
40+
41+
expect(fixture.nativeElement.classList).toContain(`tedi-tag--${type}`);
42+
}
43+
});
44+
45+
it("should show danger icon when type is danger", () => {
46+
fixture.componentRef.setInput("type", "danger");
47+
fixture.detectChanges();
48+
49+
const iconElement = fixture.nativeElement.querySelector("tedi-icon");
50+
expect(iconElement).toBeTruthy();
51+
expect(iconElement.textContent).toBe("error");
52+
});
53+
54+
it("should not show danger icon when type is not danger", () => {
55+
fixture.componentRef.setInput("type", "primary");
56+
fixture.detectChanges();
57+
58+
const iconWrapper = fixture.nativeElement.querySelector(".tedi-tag__icon-wrapper");
59+
expect(iconWrapper).toBeFalsy();
60+
});
61+
62+
it("should show spinner when loading is true", () => {
63+
fixture.componentRef.setInput("loading", true);
64+
fixture.detectChanges();
65+
66+
const spinner = fixture.nativeElement.querySelector("tedi-spinner");
67+
expect(spinner).toBeTruthy();
68+
expect(fixture.nativeElement.classList).toContain("tedi-tag--loading");
69+
});
70+
71+
it("should not show spinner when loading is false", () => {
72+
fixture.componentRef.setInput("loading", false);
73+
fixture.detectChanges();
74+
75+
const spinner = fixture.nativeElement.querySelector("tedi-spinner");
76+
expect(spinner).toBeFalsy();
77+
});
78+
79+
it("should show close button when closable is true", () => {
80+
fixture.componentRef.setInput("closable", true);
81+
fixture.detectChanges();
82+
83+
const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
84+
expect(closeButton).toBeTruthy();
85+
expect(fixture.nativeElement.classList).toContain("tedi-tag--closable");
86+
});
87+
88+
it("should not show close button when closable is false", () => {
89+
fixture.componentRef.setInput("closable", false);
90+
fixture.detectChanges();
91+
92+
const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
93+
expect(closeButton).toBeFalsy();
94+
});
95+
96+
it("should not show close button when loading is true even if closable is true", () => {
97+
fixture.componentRef.setInput("closable", true);
98+
fixture.componentRef.setInput("loading", true);
99+
fixture.detectChanges();
100+
101+
const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
102+
const spinner = fixture.nativeElement.querySelector("tedi-spinner");
103+
104+
expect(closeButton).toBeFalsy();
105+
expect(spinner).toBeTruthy();
106+
});
107+
108+
it("should emit closed event when close button is clicked", () => {
109+
fixture.componentRef.setInput("closable", true);
110+
fixture.detectChanges();
111+
112+
const closedSpy = jest.fn();
113+
component.closed.subscribe(closedSpy);
114+
115+
const closeButton = fixture.nativeElement.querySelector("[tedi-closing-button]");
116+
closeButton.click();
117+
fixture.detectChanges();
118+
119+
expect(closedSpy).toHaveBeenCalled();
120+
});
121+
122+
it("should render content", () => {
123+
const contentElement = fixture.nativeElement.querySelector(".tedi-tag__content");
124+
expect(contentElement).toBeTruthy();
125+
});
126+
});

0 commit comments

Comments
 (0)