From e558640377a836343b63faf4336e58989bd56879 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Tue, 10 Feb 2026 12:43:15 +0200 Subject: [PATCH 01/10] feat(accordion): add accordion tedi-ready component #262 --- .../accordion-item.component.html | 133 ++++ .../accordion-item.component.scss | 158 +++++ .../accordion-item.component.spec.ts | 133 ++++ .../accordion-item.component.ts | 126 ++++ .../cards/accordion/accordion.stories.ts | 568 ++++++++++++++++++ .../accordion/accordion.component.spec.ts | 99 +++ .../accordion/accordion.component.ts | 48 ++ tedi/components/cards/accordion/index.ts | 2 + 8 files changed, 1267 insertions(+) create mode 100644 tedi/components/cards/accordion/accordion-item/accordion-item.component.html create mode 100644 tedi/components/cards/accordion/accordion-item/accordion-item.component.scss create mode 100644 tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts create mode 100644 tedi/components/cards/accordion/accordion-item/accordion-item.component.ts create mode 100644 tedi/components/cards/accordion/accordion.stories.ts create mode 100644 tedi/components/cards/accordion/accordion/accordion.component.spec.ts create mode 100644 tedi/components/cards/accordion/accordion/accordion.component.ts create mode 100644 tedi/components/cards/accordion/index.ts diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html new file mode 100644 index 000000000..b7ab27aef --- /dev/null +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -0,0 +1,133 @@ +
+ @if (showIconCard()) { + + } + +
+ @if (withAction()) { +
+ +
+ } @else { + + } +
+ + @if (expanded()) { +
+ +
+ } +
+ + +
+ + + @if (showStartExpandIcon()) { + + } + + @if (withAction()) { + + } @else { + + {{ title() | tediTranslate }} + + } + + + + @if (descriptionPosition() !== "end") { + + + } +
+ +
+ @if (descriptionPosition() !== "start") { + + + } + + @if (withAction()) { + + } + + @if (showEndExpandIcon()) { + @if (showExpandLabel()) { + + } @else { + + } + } +
+
+ + + + + + + + + + @if (description()) { + + {{ description() ?? "" | tediTranslate }} + + } + diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss new file mode 100644 index 000000000..c75df4423 --- /dev/null +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss @@ -0,0 +1,158 @@ +.tedi-accordion__item { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto 1fr; + align-items: stretch; + + &--selected { + border: 1px solid var(--card-border-selected); + border-radius: var(--card-radius-rounded); + + .tedi-accordion__header, + .tedi-accordion__body, + [tedi-accordion-icon-card] { + border: transparent; + } + + .tedi-accordion__header, + .tedi-accordion__body { + &--with-icon-card { + border-left: 1px solid var(--card-border-primary); + } + } + + .tedi-accordion__header { + &--expanded { + border-bottom: 1px solid var(--card-border-primary); + } + } + } +} + +.tedi-accordion__header-row { + display: flex; + grid-row: 1; + grid-column: 2; + align-items: stretch; +} + +.tedi-accordion__header { + position: relative; + display: flex; + flex: 1; + gap: var(--layout-grid-gutters-16); + align-items: center; + align-self: stretch; + padding: var(--card-padding-md-default); + background: var(--card-background-primary); + border: 1px solid var(--card-border-primary); + border-radius: var(--card-radius-rounded); + + &:has([tedi-accordion-action]) { + padding: var(--card-padding-sm) var(--card-padding-md-default); + } + + &--expanded { + border-radius: var(--card-radius-rounded) var(--card-radius-rounded) 0 0; + } + + &--expanded.tedi-accordion__header--with-icon-card { + border-radius: 0 var(--card-radius-rounded) 0 0; + } + + &--with-icon-card { + border-radius: var(--card-radius-sharp) var(--card-radius-rounded) + var(--card-radius-rounded) var(--card-radius-sharp); + } + + &--hoverable { + cursor: pointer; + + &:hover { + background: var(--general-surface-hover); + } + + &:active { + background: var(--general-surface-brand-tertiary); + } + + &:focus-visible { + z-index: 1; + outline: none; + background: var(--card-background-primary); + border: 1px solid var(--card-border-primary); + box-shadow: + 0 0 0 1px var(--tedi-neutral-100), + 0 0 0 3px var(--tedi-primary-500); + } + } +} + +.tedi-accordion__body { + grid-row: 2; + grid-column: 2; + padding: var(--card-padding-md-default); + background: var(--card-background-primary); + border: 1px solid var(--card-border-primary); + border-top: none; + border-radius: 0 0 var(--card-radius-rounded) var(--card-radius-rounded); + + &--with-icon-card { + border-radius: 0 0 var(--card-radius-rounded) 0; + } +} + +.tedi-accordion__icon--expanded { + transform: rotate(180deg); +} + +.tedi-accordion__start, +.tedi-accordion__end { + display: flex; + align-items: center; +} + +.tedi-accordion__start { + flex: 1 0 0; + gap: var(--layout-grid-gutters-08); + min-width: 0; + + &--with-description, + &:has([tedi-accordion-description-start]) { + flex-direction: column; + gap: 0; + align-items: flex-start; + } +} + +.tedi-accordion__end { + gap: var(--layout-grid-gutters-16); +} + +[tedi-accordion-icon-card] { + display: inline-flex; + grid-row: 1 / span 2; + grid-column: 1; + gap: var(--layout-grid-gutters-08); + align-items: flex-start; + align-self: stretch; + justify-content: flex-end; + padding: var(--card-padding-md-default); + background: var(--card-background-secondary); + border: 1px solid var(--card-border-primary); + border-right: none; + border-radius: var(--card-radius-rounded) var(--card-radius-sharp) + var(--card-radius-sharp) var(--card-radius-rounded); +} + +.tedi-accordion__toggle-button { + display: flex; + padding: 0; + font-size: var(--body-regular-size); + background: transparent; + border: transparent; + + &-disabled { + pointer-events: none; + } +} diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts new file mode 100644 index 000000000..04039bf49 --- /dev/null +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts @@ -0,0 +1,133 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { AccordionItemComponent } from "./accordion-item.component"; +import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../../tokens/translation.token"; +import { By } from "@angular/platform-browser"; + +describe("AccordionItemComponent", () => { + let fixture: ComponentFixture; + let component: AccordionItemComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AccordionItemComponent], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }], + }).compileComponents(); + + fixture = TestBed.createComponent(AccordionItemComponent); + component = fixture.componentInstance; + }); + + it("should create component", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + it("should be collapsed by default", () => { + fixture.detectChanges(); + + const body = fixture.debugElement.query(By.css(".tedi-accordion__body")); + expect(body).toBeNull(); + }); + + it("should be expanded when defaultExpanded is true", () => { + fixture.componentRef.setInput("defaultExpanded", true); + + fixture.detectChanges(); + + expect(component.expanded()).toBe(true); + + const body = fixture.debugElement.query(By.css(".tedi-accordion__body")); + expect(body).not.toBeNull(); + }); + + it("should emit toggled when header button is clicked", () => { + fixture.detectChanges(); + + const spy = jest.fn(); + component.toggled.subscribe(spy); + + const button = fixture.debugElement.query( + By.css("button.tedi-accordion__header"), + ); + + button.triggerEventHandler("click"); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalled(); + expect(component.expanded()).toBe(false); + }); + + it("should not toggle expanded when withAction is true and header is clicked", () => { + fixture.componentRef.setInput("withAction", true); + fixture.detectChanges(); + + const header = fixture.debugElement.query( + By.css(".tedi-accordion__header"), + ); + + header.triggerEventHandler("click"); + fixture.detectChanges(); + + expect(component.expanded()).toBe(false); + }); + + it("should update expanded state when setExpanded is called", () => { + component.setExpanded(true); + expect(component.expanded()).toBe(true); + + component.setExpanded(false); + expect(component.expanded()).toBe(false); + }); + + it("should set aria-expanded on header button", () => { + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("button.tedi-accordion__header"), + )?.nativeElement as HTMLButtonElement; + + expect(button.getAttribute("aria-expanded")).toBe("false"); + + component.setExpanded(true); + fixture.detectChanges(); + + expect(button.getAttribute("aria-expanded")).toBe("true"); + }); + + it("should emit toggled selected state when action button is clicked", () => { + const spy = jest.fn(); + component.selectToggle.subscribe(spy); + + fixture.componentRef.setInput("selected", false); + + component.onSelectClick(new MouseEvent("click")); + + expect(spy).toHaveBeenCalledWith(true); + }); + + it("should show open label when collapsed and close label when expanded", () => { + fixture.componentRef.setInput("openLabel", "Open"); + fixture.componentRef.setInput("closeLabel", "Close"); + + component.setExpanded(false); + expect(component.expandLabel()).toBe("Open"); + + component.setExpanded(true); + expect(component.expandLabel()).toBe("Close"); + }); + + it("should show start expand icon only when position=start and no action", () => { + fixture.componentRef.setInput("expandIconPosition", "start"); + fixture.componentRef.setInput("withAction", false); + + expect(component.showStartExpandIcon()).toBe(true); + expect(component.showEndExpandIcon()).toBe(false); + }); + + it("should not show expand icons when withAction is true", () => { + fixture.componentRef.setInput("expandIconPosition", "start"); + fixture.componentRef.setInput("withAction", true); + + expect(component.showStartExpandIcon()).toBe(false); + expect(component.showEndExpandIcon()).toBe(false); + }); +}); diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts new file mode 100644 index 000000000..50a3fc9bb --- /dev/null +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -0,0 +1,126 @@ +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, + input, + output, + signal, + OnInit, + computed, +} from "@angular/core"; +import { + IconComponent, + TextComponent, + LinkComponent, + generateUUID, + TediTranslationPipe, +} from "@tedi-design-system/angular/tedi"; + +@Component({ + selector: "tedi-accordion-item", + standalone: true, + templateUrl: "./accordion-item.component.html", + styleUrl: "./accordion-item.component.scss", + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + IconComponent, + CommonModule, + TextComponent, + LinkComponent, + TediTranslationPipe, + ], +}) +export class AccordionItemComponent implements OnInit { + title = input(""); + + /** Optional description text shown in the header */ + description = input(undefined); + + /** + * Whether the accordion item is expanded initially. + * Does not control the expanded state after initialization. + */ + defaultExpanded = input(false); + + /** + * Marks the accordion item as selected. + * Used together with `withAction` to render selection UI. + */ + selected = input(false); + + /** + * Controls whether the expand/collapse label is shown. + */ + showExpandLabel = input(true); + + /** + * Uses the inverted color variant for the expand label. + */ + expandLabelInverted = input(false); + + /** Label shown when accordion is collapsed */ + openLabel = input("open"); + + /** Label shown when accordion is expanded */ + closeLabel = input("close"); + + /** + * Position of the expand icon relative to the header content. + * Has no effect when `withAction` is true. + */ + expandIconPosition = input<"start" | "end">("end"); + + /** + * Position of the description relative to the title. + */ + descriptionPosition = input<"start" | "end" | "both">("start"); + + /** + * Enables the icon-card layout variant. + */ + showIconCard = input(false); + + /** + * Disables header toggling and enables action slot usage. + */ + withAction = input(false); + + expanded = signal(false); + + toggled = output(); + selectToggle = output(); + + readonly bodyId = `tedi-accordion-body-${generateUUID()}`; + readonly headerId = `tedi-accordion-header-${generateUUID()}`; + + ngOnInit() { + this.expanded.set(this.defaultExpanded()); + } + + toggle() { + this.toggled.emit(); + } + + setExpanded(value: boolean) { + this.expanded.set(value); + } + + onSelectClick(event: MouseEvent) { + event.stopPropagation(); + this.selectToggle.emit(!this.selected()); + } + + expandLabel = computed(() => + this.expanded() ? this.closeLabel() : this.openLabel(), + ); + + showStartExpandIcon = computed( + () => !this.withAction() && this.expandIconPosition() === "start", + ); + + showEndExpandIcon = computed( + () => !this.withAction() && this.expandIconPosition() === "end", + ); +} diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts new file mode 100644 index 000000000..4f5dedb15 --- /dev/null +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -0,0 +1,568 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { AccordionComponent } from "./accordion/accordion.component"; +import { AccordionItemComponent } from "./accordion-item/accordion-item.component"; +import { IconComponent, TextComponent } from "tedi/components/base"; +import { ButtonComponent } from "tedi/components/buttons"; +import { StatusBadgeComponent } from "community/components/tags"; + +export default { + title: "TEDI-Ready/Components/Cards/Accordion", + decorators: [ + moduleMetadata({ + imports: [ + AccordionComponent, + AccordionItemComponent, + IconComponent, + TextComponent, + ButtonComponent, + StatusBadgeComponent, + ], + }), + ], + argTypes: { + multiple: { + control: "boolean", + description: "Whether multiple accordion items can be opened at once.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + title: { + control: "text", + description: "The title of the accordion item.", + table: { + type: { summary: "string" }, + defaultValue: { summary: "Title" }, + }, + }, + openLabel: { + control: "text", + description: "Label for the open action.", + table: { + type: { summary: "string" }, + defaultValue: { summary: "open" }, + }, + }, + closeLabel: { + control: "text", + description: "Label for the close action.", + table: { + type: { summary: "string" }, + defaultValue: { summary: "close" }, + }, + }, + showExpandLabel: { + control: "boolean", + description: "Whether to show the expand/collapse labels.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, + expandLabelInverted: { + control: "boolean", + description: "Whether the expand label should be inverted.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + expandIconPosition: { + control: "radio", + options: ["start", "end"], + description: "Position of the expand/collapse icon.", + table: { + type: { summary: "'start' | 'end'" }, + defaultValue: { summary: "end" }, + }, + }, + defaultExpanded: { + control: "boolean", + description: + "Whether the accordion item is initially expanded or collapsed.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + description: { + control: "text", + description: "The description text of the accordion item.", + table: { + type: { summary: "string" }, + defaultValue: { summary: "" }, + }, + }, + descriptionPosition: { + control: "radio", + options: ["start", "end", "both"], + description: "Position of the description text.", + table: { + type: { summary: "'start' | 'end' | 'both'" }, + defaultValue: { summary: "start" }, + }, + }, + showIconCard: { + control: "boolean", + description: "Whether to show the icon card in the accordion item.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + withAction: { + control: "boolean", + description: + "Whether the accordion header contains an additional action element for managing selection state.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + selected: { + control: "boolean", + description: "Whether the accordion item is selected.", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + }, +} as Meta; + +const contentExample = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt +ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco +laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in +voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat +non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`; + +const iconCardTemplate = ` + + + Töövõime + +`; + +const actionButtonTemplate = (selectedState: string, toggleFn: string) => ` + +`; + +export const Default: StoryObj = { + args: { + multiple: false, + title: "Title", + openLabel: "open", + closeLabel: "close", + showExpandLabel: true, + expandLabelInverted: false, + expandIconPosition: "end", + defaultExpanded: false, + description: "", + descriptionPosition: "start", + showIconCard: false, + withAction: false, + selected: false, + }, + render: (args) => ({ + props: { + ...args, + toggle(selected: boolean) { + this["selected"] = selected; + }, + }, + template: ` + + + ${` + @if (withAction) { + ${actionButtonTemplate("selected", "toggle")} + } + `} + ${iconCardTemplate} + ${contentExample} + + + ${contentExample} + + + `, + }), +}; + +export const Header: StoryObj = { + render: () => ({ + template: ` +
+ + + ${contentExample} + + + + + + Approved + ${contentExample} + + + + + + + ${contentExample} + + + + + + + ${contentExample} + + + + + + ${contentExample} + + + + + + ${contentExample} + + + + + + ${contentExample} + + + + + + ${contentExample} + + + + + + ${contentExample} + + + Description + + + + Another description + + + + + + + ${actionButtonTemplate("selectedA", "toggleA")} + ${contentExample} + + + + + + ${actionButtonTemplate("selectedB", "toggleB")} + ${contentExample} + + +
+ `, + props: { + selectedA: false, + selectedB: true, + toggleA(selected: boolean) { + this["selectedA"] = selected; + }, + toggleB(selected: boolean) { + this["selectedB"] = selected; + }, + }, + }), +}; + +export const HeaderWithBody: StoryObj = { + render: () => ({ + template: ` + +
+
+ + + ${contentExample} + + + + + ${contentExample} + + +
+ +
+ + + ${contentExample} + + + + + ${contentExample} + + +
+ +
+ + + ${contentExample} + ${actionButtonTemplate("selectedA", "toggleA")} + + + + + + ${contentExample} + ${actionButtonTemplate("selectedB", "toggleB")} + + +
+ +
+ + + ${contentExample} + ${actionButtonTemplate("selectedC", "toggleC")} + + + + + + ${contentExample} + ${actionButtonTemplate("selectedD", "toggleD")} + + +
+
+ `, + props: { + selectedA: false, + selectedB: false, + selectedC: true, + selectedD: true, + + toggleA(selected: boolean) { + this["selectedA"] = selected; + }, + toggleB(selected: boolean) { + this["selectedB"] = selected; + }, + toggleC(selected: boolean) { + this["selectedC"] = selected; + }, + toggleD(selected: boolean) { + this["selectedD"] = selected; + }, + }, + }), +}; + +export const AccordionWithIconCard: StoryObj = { + render: () => ({ + template: ` +
+
+ + + ${iconCardTemplate} + ${contentExample} + + + + + ${iconCardTemplate} + ${contentExample} + + +
+ +
+ + + ${iconCardTemplate} + ${contentExample} + + + + + ${iconCardTemplate} + ${contentExample} + + +
+ +
+ + + ${iconCardTemplate} + ${contentExample} + ${actionButtonTemplate("selectedA", "toggleA")} + + + + + + ${iconCardTemplate} + ${contentExample} + ${actionButtonTemplate("selectedB", "toggleB")} + + +
+ +
+ + + ${iconCardTemplate} + ${contentExample} + ${actionButtonTemplate("selectedC", "toggleC")} + + + + + + ${iconCardTemplate} + ${contentExample} + ${actionButtonTemplate("selectedD", "toggleD")} + + +
+
+ `, + props: { + selectedA: false, + selectedB: false, + selectedC: true, + selectedD: true, + + toggleA(selected: boolean) { + this["selectedA"] = selected; + }, + toggleB(selected: boolean) { + this["selectedB"] = selected; + }, + toggleC(selected: boolean) { + this["selectedC"] = selected; + }, + toggleD(selected: boolean) { + this["selectedD"] = selected; + }, + }, + }), +}; diff --git a/tedi/components/cards/accordion/accordion/accordion.component.spec.ts b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts new file mode 100644 index 000000000..803cc1958 --- /dev/null +++ b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts @@ -0,0 +1,99 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Component } from "@angular/core"; + +import { AccordionComponent } from "./accordion.component"; +import { AccordionItemComponent } from "../accordion-item/accordion-item.component"; +import { By } from "@angular/platform-browser"; +import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../../tokens/translation.token"; + +@Component({ + standalone: true, + imports: [AccordionComponent, AccordionItemComponent], + template: ` + + + + + + `, +}) +class TestHostComponent { + multiple = false; +} + +describe("AccordionComponent", () => { + let fixture: ComponentFixture; + let host: TestHostComponent; + let accordion: AccordionComponent; + let items: AccordionItemComponent[]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + host = fixture.componentInstance; + fixture.detectChanges(); + + accordion = fixture.debugElement.query( + By.directive(AccordionComponent), + ).componentInstance; + + items = fixture.debugElement + .queryAll(By.directive(AccordionItemComponent)) + .map((de) => de.componentInstance); + }); + + it("should create accordion component", () => { + expect(accordion).toBeTruthy(); + }); + + it("should register all accordion items via ContentChildren", () => { + expect(accordion.items.length).toBe(3); + }); + + it("should expand clicked item", () => { + items[0].toggle(); + fixture.detectChanges(); + + expect(items[0].expanded()).toBe(true); + }); + + it("should collapse an expanded item when toggled again", () => { + items[0].toggle(); + fixture.detectChanges(); + expect(items[0].expanded()).toBe(true); + + items[0].toggle(); + fixture.detectChanges(); + expect(items[0].expanded()).toBe(false); + }); + + it("should collapse other items when multiple=false", () => { + items[0].toggle(); + fixture.detectChanges(); + + items[1].toggle(); + fixture.detectChanges(); + + expect(items[0].expanded()).toBe(false); + expect(items[1].expanded()).toBe(true); + }); + + it("should allow multiple items expanded when multiple=true", () => { + host.multiple = true; + fixture.detectChanges(); + + items[0].toggle(); + fixture.detectChanges(); + + items[1].toggle(); + fixture.detectChanges(); + + expect(items[0].expanded()).toBe(true); + expect(items[1].expanded()).toBe(true); + expect(items[2].expanded()).toBe(false); + }); +}); diff --git a/tedi/components/cards/accordion/accordion/accordion.component.ts b/tedi/components/cards/accordion/accordion/accordion.component.ts new file mode 100644 index 000000000..80253ee29 --- /dev/null +++ b/tedi/components/cards/accordion/accordion/accordion.component.ts @@ -0,0 +1,48 @@ +import { + ChangeDetectionStrategy, + Component, + ContentChildren, + QueryList, + ViewEncapsulation, + AfterContentInit, + input, +} from "@angular/core"; +import { AccordionItemComponent } from "../accordion-item/accordion-item.component"; + +@Component({ + selector: "tedi-accordion", + standalone: true, + template: "", + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AccordionComponent implements AfterContentInit { + /** + * Whether the accordion allows multiple items to be expanded at the same time. + * If false, opening one item will collapse the others automatically. + */ + multiple = input(false); + + @ContentChildren(AccordionItemComponent) + items!: QueryList; + + ngAfterContentInit() { + this.items.forEach((item) => { + item.toggled.subscribe(() => { + this.onItemToggled(item); + }); + }); + } + + private onItemToggled(activeItem: AccordionItemComponent) { + const shouldExpand = !activeItem.expanded(); + + this.items.forEach((item) => { + if (item === activeItem) { + item.setExpanded(shouldExpand); + } else if (!this.multiple()) { + item.setExpanded(false); + } + }); + } +} diff --git a/tedi/components/cards/accordion/index.ts b/tedi/components/cards/accordion/index.ts new file mode 100644 index 000000000..13b1d8cd2 --- /dev/null +++ b/tedi/components/cards/accordion/index.ts @@ -0,0 +1,2 @@ +export * from "./accordion-item/accordion-item.component"; +export * from "./accordion/accordion.component"; From 02e451276c5cde5f361cb8bb968832240de55069 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Fri, 13 Feb 2026 17:30:42 +0200 Subject: [PATCH 02/10] feat(accordion): update accordion tedi-ready component #262 --- package-lock.json | 2 +- package.json | 2 +- public/accordion_example.png | Bin 0 -> 10686 bytes .../accordion-item.component.html | 147 ++++++----- .../accordion-item.component.scss | 65 +++-- .../accordion-item.component.spec.ts | 43 +-- .../accordion-item.component.ts | 88 +++---- .../cards/accordion/accordion.stories.ts | 244 ++++++++++++------ .../accordion/accordion.component.spec.ts | 2 +- .../accordion/accordion.component.ts | 35 +-- tedi/components/cards/accordion/index.ts | 2 - 11 files changed, 349 insertions(+), 281 deletions(-) create mode 100644 public/accordion_example.png delete mode 100644 tedi/components/cards/accordion/index.ts diff --git a/package-lock.json b/package-lock.json index fcb4226a2..64d04e785 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "@tedi-design-system/angular", "version": "0.0.0-semantic-version", "dependencies": { - "@tedi-design-system/core": "^3.0.1" + "@tedi-design-system/core": "^3.1.0" }, "devDependencies": { "@angular-devkit/core": "19.2.15", diff --git a/package.json b/package.json index bdd0e1607..ec0182607 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "ngx-float-ui": "^19.0.1 || ^20.0.0" }, "dependencies": { - "@tedi-design-system/core": "^3.0.1" + "@tedi-design-system/core": "^3.1.0" }, "devDependencies": { "@angular-devkit/core": "19.2.15", diff --git a/public/accordion_example.png b/public/accordion_example.png new file mode 100644 index 0000000000000000000000000000000000000000..58ae745edd8723beb4d6f3a3fa32743c6e2c143c GIT binary patch literal 10686 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%0DdEqQEGD(6CKp##B1XNlxQwrjPrgwb$ka#Mn~A{ zxUs%Ci5>gz$9C#yp#c)~nQ?^j25fH3;;Cm}r?h3q!u&igPA+1O6X|!{c7U{rdf0cq zd*`v&j;}+!DBekGP-3WW?`y^%-EARNcN*`@edBjT1R8s2r-A`Y{2OqkN zgJVDb{rf*99cn>>z4I+Mj^Zm{ILb)%lWd!yWec@qP3#5#>LWW4VC!z9JS^Ugdh+Q- z?A{f`u3(B#NASXH^LY3DL-O6wNHmLGJA#ti*q9>n>O^$kHYMvbllvG&mH@FSllkEzbV<7 z)Oe37{&t9pgez#GoLnZJ<r_fK6Aes^i^uX9+SP^_I}?{J zh7bJ3hb5mo|I!4yGdd36dkDilP4Fg{G5PX1(u5^PM+@1w6^ZfjdF{TV{Ys@;xktR! z5{(uxO>($zpBFWzrl1qgJjaHXJzcoPiPzrVkdx}Al{~sTJeZhUWCR8`_s;F|FH3v@1-<=LAI@`DFd4z+*V7nk58?g~9>v(} z*Kq2>+eofQ@XhBhQe=pr$)aPfN>U!H{gZa``07cA$Lu}0-*+IsSwnvhE77S%oS#gf zFXYFL9cFa41u?&nVvAvBwJ~F6It>zDdf1BRUzwFJw`q)^D80NMV_7FiMxsTuGF_@{ zvu?k&3%~p2HByosF-!yTnvtQx(_Fx#FPuf4DZ8t;pA3B!OKWwUn|=)!$L8_-Up|8! z!(D8I1K4@!HcYN<;$8RLg+Gm*;)Ke8n@4x!{N*Xm$?EcDJMmX4T~vMVKG?}B8D*rU zU=@FI!{;Z`jJ_^ETc~&Ax4->8t9=C?jtOrcxd(^X{@i%)J|D!HA5 z>0C#M#B+7AAVmx+`z(xG!|y_m>!R^3ozi0JHkj6jV$9war}kn@f{Wvzh#+h zocnz8*Z9(>KZ+iY1J6^Kyklewyg`-t8pg@v%W!xsY|*N?I=zf{-QS9@{L598Nh^X> zNvt&L%+xiuatkj5zkG58>UBIz#KaBnx--}`MLHyk({!C0T kIbSU&AN=@t#)Yl^KWEr!4_fjKj{pDw07*qoM6N<$f?y4OhyVZp literal 0 HcmV?d00001 diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html index b7ab27aef..6d980a958 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -8,16 +8,7 @@ }
- @if (withAction()) { -
- -
- } @else { + @if (headerClickable()) { + } @else { +
+ +
}
@@ -47,87 +47,92 @@ -
+
+ + - @if (showStartExpandIcon()) { - - } + + + @if (showStartExpandAction()) { + + } - @if (withAction()) { - - } @else { - - {{ title() | tediTranslate }} + @if (showDefaultTitle()) { + + {{ title() | tediTranslate }} + + } - } - + @if (descriptionPosition() !== "end") { + - @if (descriptionPosition() !== "end") { - - - } + @if (description(); as desc) { + + {{ desc | tediTranslate }} + + } + } + + +
-
- @if (descriptionPosition() !== "start") { - - - } + @if (descriptionPosition() !== "start") { + - @if (withAction()) { - + @if (description(); as desc) { + + {{ desc | tediTranslate }} + } + } - @if (showEndExpandIcon()) { - @if (showExpandLabel()) { - - } @else { - - } - } -
+ @if (showEndExpandAction()) { + + } + + - + @if (headerClickable()) { +
+ + {{ (showExpandLabel() ? expandLabel() : "") | tediTranslate }} + + +
+ } @else { + + }
- - - @if (description()) { - - {{ description() ?? "" | tediTranslate }} - - } - diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss index c75df4423..71643d494 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss @@ -48,10 +48,6 @@ border: 1px solid var(--card-border-primary); border-radius: var(--card-radius-rounded); - &:has([tedi-accordion-action]) { - padding: var(--card-padding-sm) var(--card-padding-md-default); - } - &--expanded { border-radius: var(--card-radius-rounded) var(--card-radius-rounded) 0 0; } @@ -106,27 +102,12 @@ transform: rotate(180deg); } -.tedi-accordion__start, -.tedi-accordion__end { - display: flex; - align-items: center; -} - .tedi-accordion__start { - flex: 1 0 0; + display: flex; + flex: 1; gap: var(--layout-grid-gutters-08); + align-items: center; min-width: 0; - - &--with-description, - &:has([tedi-accordion-description-start]) { - flex-direction: column; - gap: 0; - align-items: flex-start; - } -} - -.tedi-accordion__end { - gap: var(--layout-grid-gutters-16); } [tedi-accordion-icon-card] { @@ -145,14 +126,50 @@ var(--card-radius-sharp) var(--card-radius-rounded); } +.tedi-accordion__title { + display: flex; + gap: var(--layout-grid-gutters-08); + + &--main { + display: flex; + gap: inherit; + } + + &--with-description, + &:has([tedi-accordion-start-description]) { + flex-direction: column; + gap: 0; + align-items: flex-start; + } + + &--grow { + flex: 1; + justify-content: space-between; + } +} + .tedi-accordion__toggle-button { display: flex; padding: 0; font-size: var(--body-regular-size); background: transparent; border: transparent; +} - &-disabled { - pointer-events: none; +.tedi-accordion__expand-indicator { + display: flex; + align-items: center; + color: var(--accordion-action-color); + + tedi-icon { + color: inherit; + } + + &--with-label { + tedi-icon { + margin-left: var(--button-sm-inner-spacing); + font-size: var(--tedi-size-02); + line-height: inherit; + } } } diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts index 04039bf49..4347adaf9 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts @@ -39,25 +39,23 @@ describe("AccordionItemComponent", () => { expect(body).not.toBeNull(); }); - it("should emit toggled when header button is clicked", () => { + it("should expand when header button is clicked", () => { fixture.detectChanges(); - const spy = jest.fn(); - component.toggled.subscribe(spy); - const button = fixture.debugElement.query( By.css("button.tedi-accordion__header"), ); + expect(component.expanded()).toBe(false); + button.triggerEventHandler("click"); fixture.detectChanges(); - expect(spy).toHaveBeenCalled(); - expect(component.expanded()).toBe(false); + expect(component.expanded()).toBe(true); }); - it("should not toggle expanded when withAction is true and header is clicked", () => { - fixture.componentRef.setInput("withAction", true); + it("should not toggle expanded when headerClickable is false and header is clicked", () => { + fixture.componentRef.setInput("headerClickable", false); fixture.detectChanges(); const header = fixture.debugElement.query( @@ -93,15 +91,15 @@ describe("AccordionItemComponent", () => { expect(button.getAttribute("aria-expanded")).toBe("true"); }); - it("should emit toggled selected state when action button is clicked", () => { - const spy = jest.fn(); - component.selectToggle.subscribe(spy); - - fixture.componentRef.setInput("selected", false); + it("should apply selected class when selected=true", () => { + fixture.componentRef.setInput("selected", true); + fixture.detectChanges(); - component.onSelectClick(new MouseEvent("click")); + const item = fixture.debugElement.query(By.css(".tedi-accordion__item")); - expect(spy).toHaveBeenCalledWith(true); + expect(item.nativeElement.classList).toContain( + "tedi-accordion__item--selected", + ); }); it("should show open label when collapsed and close label when expanded", () => { @@ -115,19 +113,4 @@ describe("AccordionItemComponent", () => { expect(component.expandLabel()).toBe("Close"); }); - it("should show start expand icon only when position=start and no action", () => { - fixture.componentRef.setInput("expandIconPosition", "start"); - fixture.componentRef.setInput("withAction", false); - - expect(component.showStartExpandIcon()).toBe(true); - expect(component.showEndExpandIcon()).toBe(false); - }); - - it("should not show expand icons when withAction is true", () => { - fixture.componentRef.setInput("expandIconPosition", "start"); - fixture.componentRef.setInput("withAction", true); - - expect(component.showStartExpandIcon()).toBe(false); - expect(component.showEndExpandIcon()).toBe(false); - }); }); diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index 50a3fc9bb..fde51df42 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -4,10 +4,10 @@ import { Component, ViewEncapsulation, input, - output, - signal, OnInit, computed, + model, + inject, } from "@angular/core"; import { IconComponent, @@ -16,6 +16,7 @@ import { generateUUID, TediTranslationPipe, } from "@tedi-design-system/angular/tedi"; +import { AccordionComponent } from "../accordion/accordion.component"; @Component({ selector: "tedi-accordion-item", @@ -33,94 +34,85 @@ import { ], }) export class AccordionItemComponent implements OnInit { - title = input(""); - - /** Optional description text shown in the header */ - description = input(undefined); - /** - * Whether the accordion item is expanded initially. - * Does not control the expanded state after initialization. + * If false, disables header toggling and enables using interactive elements in the accordion header. */ - defaultExpanded = input(false); - + headerClickable = input(true); + /** The title of the accordion item. */ + title = input(""); /** - * Marks the accordion item as selected. - * Used together with `withAction` to render selection UI. + * Sets how the accordion title stretches horizontally. + * `hug` - container sizes to its content. + * `fill` - container expands to available space, moving any trailing elements to the end. */ - selected = input(false); - + titleLayout = input<"hug" | "fill">("hug"); + /** Whether the default title text is shown in the header. */ + showDefaultTitle = input(true); + /** Label shown when accordion is collapsed */ + openLabel = input("open"); + /** Label shown when accordion is expanded */ + closeLabel = input("close"); /** * Controls whether the expand/collapse label is shown. */ showExpandLabel = input(true); - /** - * Uses the inverted color variant for the expand label. + * Controls whether the default expand/collapse icon is shown. */ - expandLabelInverted = input(false); - - /** Label shown when accordion is collapsed */ - openLabel = input("open"); - - /** Label shown when accordion is expanded */ - closeLabel = input("close"); - + showExpandIcon = input(true); /** - * Position of the expand icon relative to the header content. - * Has no effect when `withAction` is true. + * Position of the expand action relative to the header content. */ - expandIconPosition = input<"start" | "end">("end"); - + expandActionPosition = input<"start" | "end">("end"); + /** + * Whether the accordion item is expanded initially. + * Does not control the expanded state after initialization. + */ + defaultExpanded = input(false); + /** Optional description text shown in the header */ + description = input(undefined); /** * Position of the description relative to the title. */ descriptionPosition = input<"start" | "end" | "both">("start"); - /** * Enables the icon-card layout variant. */ showIconCard = input(false); - /** - * Disables header toggling and enables action slot usage. + * Marks the accordion item as selected. */ - withAction = input(false); - - expanded = signal(false); + selected = input(false); - toggled = output(); - selectToggle = output(); + expanded = model(false); readonly bodyId = `tedi-accordion-body-${generateUUID()}`; readonly headerId = `tedi-accordion-header-${generateUUID()}`; + private readonly accordion = inject(AccordionComponent, { optional: true }); + ngOnInit() { - this.expanded.set(this.defaultExpanded()); + this.setExpanded(this.defaultExpanded()); } toggle() { - this.toggled.emit(); + this.setExpanded(!this.expanded()); + this.accordion?.onItemToggled(this); } setExpanded(value: boolean) { this.expanded.set(value); } - onSelectClick(event: MouseEvent) { - event.stopPropagation(); - this.selectToggle.emit(!this.selected()); - } - expandLabel = computed(() => this.expanded() ? this.closeLabel() : this.openLabel(), ); - showStartExpandIcon = computed( - () => !this.withAction() && this.expandIconPosition() === "start", + showStartExpandAction = computed( + () => this.showExpandIcon() && this.expandActionPosition() === "start", ); - showEndExpandIcon = computed( - () => !this.withAction() && this.expandIconPosition() === "end", + showEndExpandAction = computed( + () => this.showExpandIcon() && this.expandActionPosition() === "end", ); } diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts index 4f5dedb15..9b7cee62d 100644 --- a/tedi/components/cards/accordion/accordion.stories.ts +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -19,27 +19,87 @@ export default { ], }), ], + parameters: { + docs: { + description: { + component: ` +Figma ↗
+Zeroheight ↗

+ +### Slots + +| Selector | Description | +|----------|------------| +| \`[tedi-accordion-start-action]\` | Custom actions at the start of the header. | +| \`[tedi-accordion-start-before-title]\` | Custom elements before the title. | +| \`[tedi-accordion-start-after-title]\` | Custom elements after the title. | +| \`[tedi-accordion-end-action]\` | Custom actions at the end of the header. | +| \`[tedi-accordion-start-description]\` | Custom description content rendered below the title. | +| \`[tedi-accordion-end-description]\` | Custom description content rendered at the end of the header. | +| \`[tedi-accordion-icon-card]\` | Template for rendering the icon card layout. | + `, + }, + }, + }, argTypes: { multiple: { control: "boolean", description: "Whether multiple accordion items can be opened at once.", table: { + category: "Accordion", type: { summary: "boolean" }, defaultValue: { summary: "false" }, }, }, + headerClickable: { + control: "boolean", + description: + "Defines whether the entire header acts as the toggle trigger.\n\n" + + "`true` (default): clicking anywhere on the header toggles the item.\n\n" + + "`false`: the header does not toggle automatically. You must provide a custom toggle control inside the header (e.g. button or link).", + table: { + category: "Accordion Item", + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, title: { control: "text", description: "The title of the accordion item.", table: { + category: "Accordion Item", type: { summary: "string" }, defaultValue: { summary: "Title" }, }, }, + titleLayout: { + control: "radio", + options: ["hug", "fill"], + description: + "Controls how the title stretches.\n\n" + + "`hug`: wraps tightly around content.\n\n" + + "`fill`: expands to available space and pushes trailing elements to the end.", + table: { + category: "Accordion Item", + type: { summary: "'hug' | 'fill'" }, + defaultValue: { summary: "hug" }, + }, + }, + showDefaultTitle: { + control: "boolean", + description: + "Controls whether the default title text is rendered inside the header.", + table: { + category: "Accordion Item", + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, openLabel: { control: "text", description: "Label for the open action.", table: { + category: "Accordion Item", type: { summary: "string" }, defaultValue: { summary: "open" }, }, @@ -48,6 +108,7 @@ export default { control: "text", description: "Label for the close action.", table: { + category: "Accordion Item", type: { summary: "string" }, defaultValue: { summary: "close" }, }, @@ -56,23 +117,27 @@ export default { control: "boolean", description: "Whether to show the expand/collapse labels.", table: { + category: "Accordion Item", type: { summary: "boolean" }, defaultValue: { summary: "true" }, }, }, - expandLabelInverted: { + showExpandIcon: { control: "boolean", - description: "Whether the expand label should be inverted.", + description: + "Whether to show the default expand/collapse icon. If false, you can add your own expand icon with slots.", table: { + category: "Accordion Item", type: { summary: "boolean" }, - defaultValue: { summary: "false" }, + defaultValue: { summary: "true" }, }, }, - expandIconPosition: { + expandActionPosition: { control: "radio", options: ["start", "end"], - description: "Position of the expand/collapse icon.", + description: "Position of the expand/collapse action.", table: { + category: "Accordion Item", type: { summary: "'start' | 'end'" }, defaultValue: { summary: "end" }, }, @@ -82,14 +147,17 @@ export default { description: "Whether the accordion item is initially expanded or collapsed.", table: { + category: "Accordion Item", type: { summary: "boolean" }, defaultValue: { summary: "false" }, }, }, description: { control: "text", - description: "The description text of the accordion item.", + description: + "The description text of the accordion item. If you need to have different descriptions, use slots.", table: { + category: "Accordion Item", type: { summary: "string" }, defaultValue: { summary: "" }, }, @@ -99,31 +167,26 @@ export default { options: ["start", "end", "both"], description: "Position of the description text.", table: { + category: "Accordion Item", type: { summary: "'start' | 'end' | 'both'" }, defaultValue: { summary: "start" }, }, }, showIconCard: { control: "boolean", - description: "Whether to show the icon card in the accordion item.", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - }, - }, - withAction: { - control: "boolean", - description: - "Whether the accordion header contains an additional action element for managing selection state.", + description: "Whether to show the icon card.", table: { + category: "Accordion Item", type: { summary: "boolean" }, defaultValue: { summary: "false" }, }, }, selected: { control: "boolean", - description: "Whether the accordion item is selected.", + description: + "Whether the accordion item is selected. Applies a visual 'selected' state to the accordion item.", table: { + category: "Accordion Item", type: { summary: "boolean" }, defaultValue: { summary: "false" }, }, @@ -147,7 +210,7 @@ const iconCardTemplate = ` const actionButtonTemplate = (selectedState: string, toggleFn: string) => ` + ${contentExample} + +
`, props: { @@ -347,12 +423,12 @@ export const HeaderWithBody: StoryObj = {
- + ${contentExample} - + ${contentExample} @@ -360,12 +436,12 @@ export const HeaderWithBody: StoryObj = {
- + ${contentExample} - + ${contentExample} @@ -374,10 +450,11 @@ export const HeaderWithBody: StoryObj = {
${contentExample} ${actionButtonTemplate("selectedA", "toggleA")} @@ -386,11 +463,12 @@ export const HeaderWithBody: StoryObj = { ${contentExample} ${actionButtonTemplate("selectedB", "toggleB")} @@ -401,10 +479,11 @@ export const HeaderWithBody: StoryObj = {
${contentExample} ${actionButtonTemplate("selectedC", "toggleC")} @@ -413,11 +492,12 @@ export const HeaderWithBody: StoryObj = { ${contentExample} ${actionButtonTemplate("selectedD", "toggleD")} @@ -454,13 +534,13 @@ export const AccordionWithIconCard: StoryObj = {
- + ${iconCardTemplate} ${contentExample} - + ${iconCardTemplate} ${contentExample} @@ -469,13 +549,13 @@ export const AccordionWithIconCard: StoryObj = {
- + ${iconCardTemplate} ${contentExample} - + ${iconCardTemplate} ${contentExample} @@ -485,11 +565,12 @@ export const AccordionWithIconCard: StoryObj = {
${iconCardTemplate} ${contentExample} @@ -499,12 +580,13 @@ export const AccordionWithIconCard: StoryObj = { ${iconCardTemplate} ${contentExample} @@ -516,11 +598,12 @@ export const AccordionWithIconCard: StoryObj = {
${iconCardTemplate} ${contentExample} @@ -530,12 +613,13 @@ export const AccordionWithIconCard: StoryObj = { ${iconCardTemplate} ${contentExample} diff --git a/tedi/components/cards/accordion/accordion/accordion.component.spec.ts b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts index 803cc1958..1797de0ff 100644 --- a/tedi/components/cards/accordion/accordion/accordion.component.spec.ts +++ b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts @@ -51,7 +51,7 @@ describe("AccordionComponent", () => { }); it("should register all accordion items via ContentChildren", () => { - expect(accordion.items.length).toBe(3); + expect(accordion.items().length).toBe(3); }); it("should expand clicked item", () => { diff --git a/tedi/components/cards/accordion/accordion/accordion.component.ts b/tedi/components/cards/accordion/accordion/accordion.component.ts index 80253ee29..f131dcf92 100644 --- a/tedi/components/cards/accordion/accordion/accordion.component.ts +++ b/tedi/components/cards/accordion/accordion/accordion.component.ts @@ -1,11 +1,9 @@ import { ChangeDetectionStrategy, Component, - ContentChildren, - QueryList, ViewEncapsulation, - AfterContentInit, input, + contentChildren, } from "@angular/core"; import { AccordionItemComponent } from "../accordion-item/accordion-item.component"; @@ -16,33 +14,24 @@ import { AccordionItemComponent } from "../accordion-item/accordion-item.compone encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AccordionComponent implements AfterContentInit { +export class AccordionComponent { /** * Whether the accordion allows multiple items to be expanded at the same time. * If false, opening one item will collapse the others automatically. */ multiple = input(false); - @ContentChildren(AccordionItemComponent) - items!: QueryList; + items = contentChildren(AccordionItemComponent); - ngAfterContentInit() { - this.items.forEach((item) => { - item.toggled.subscribe(() => { - this.onItemToggled(item); - }); - }); - } + onItemToggled(activeItem: AccordionItemComponent) { + if (this.multiple()) return; - private onItemToggled(activeItem: AccordionItemComponent) { - const shouldExpand = !activeItem.expanded(); - - this.items.forEach((item) => { - if (item === activeItem) { - item.setExpanded(shouldExpand); - } else if (!this.multiple()) { - item.setExpanded(false); - } - }); + if (activeItem.expanded()) { + this.items().forEach((item) => { + if (item !== activeItem) { + item.setExpanded(false); + } + }); + } } } diff --git a/tedi/components/cards/accordion/index.ts b/tedi/components/cards/accordion/index.ts deleted file mode 100644 index 13b1d8cd2..000000000 --- a/tedi/components/cards/accordion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./accordion-item/accordion-item.component"; -export * from "./accordion/accordion.component"; From be65177aa9aa5fd0a6da7b2d9e13006b40eb06b1 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Mon, 16 Feb 2026 10:02:57 +0200 Subject: [PATCH 03/10] feat(accordion): update package-lock.json #262 --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 64d04e785..bd9917fc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9631,9 +9631,9 @@ } }, "node_modules/@tedi-design-system/core": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.0.1.tgz", - "integrity": "sha512-ioet8RlFmWjg8fic4WUuYeavLiqUsKx3vFGZzzXkL91xNNjHexNVKhhtMLLkpCywzOc2tKXMx3AYdDhu2dsbwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.1.0.tgz", + "integrity": "sha512-hI59htF7iEZpba21p/cnPx9kt9Uud3WQ2aUw0+b9+/bvHk5OwcoLPwn9UkyZgeQfGpz8uHMJec3ugEVdrQFZ2A==", "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" From 18aef90277c07887fc0efb10fea12ffb5b7ac4a4 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Fri, 27 Feb 2026 13:30:28 +0200 Subject: [PATCH 04/10] feat(accordion): add design review fixes #262 --- .storybook/preview.tsx | 13 +-- .../accordion-item.component.html | 14 +-- .../accordion-item.component.spec.ts | 21 +++- .../accordion-item.component.ts | 41 ++++++- .../cards/accordion/accordion.stories.ts | 102 ++++++++++++------ .../accordion/accordion.component.scss | 5 + .../accordion/accordion.component.spec.ts | 12 +-- .../accordion/accordion.component.ts | 5 +- 8 files changed, 152 insertions(+), 61 deletions(-) create mode 100644 tedi/components/cards/accordion/accordion/accordion.component.scss diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 95a595e1d..242ee3c0b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -78,6 +78,7 @@ const preview: Preview = { + @@ -106,16 +107,16 @@ const preview: Preview = { description: "TEDI-ready", }, partiallyTediReady: { - background: '#9bbb5f', - color: '#fff', + background: "#9bbb5f", + color: "#fff", description: - 'This component lacks some TEDI-Ready functionality, e.g it may rely on another component that has not yet been developed', + "This component lacks some TEDI-Ready functionality, e.g it may rely on another component that has not yet been developed", }, mobileViewDifference: { - background: '#99BDDA', - color: '#000', + background: "#99BDDA", + color: "#000", description: - 'This component has a different layout on mobile. Use the mobile breakpoint or resize the browser window to review the mobile design.', + "This component has a different layout on mobile. Use the mobile breakpoint or resize the browser window to review the mobile design.", }, }, }, diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html index 6d980a958..0b10b42bd 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -12,9 +12,7 @@ } @else { -
+
} @@ -35,11 +28,10 @@ @if (expanded()) {
diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts index 4347adaf9..82b5db48d 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.spec.ts @@ -10,7 +10,7 @@ describe("AccordionItemComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AccordionItemComponent], - providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "en" }], }).compileComponents(); fixture = TestBed.createComponent(AccordionItemComponent); @@ -113,4 +113,23 @@ describe("AccordionItemComponent", () => { expect(component.expandLabel()).toBe("Close"); }); + it("should include custom header and body classes when set", () => { + fixture.componentRef.setInput("headerClass", "custom-header"); + fixture.componentRef.setInput("bodyClass", "custom-body"); + fixture.detectChanges(); + + expect(component.headerClasses()).toEqual({ + "custom-header": true, + "tedi-accordion__header": true, + "tedi-accordion__header--hoverable": true, + "tedi-accordion__header--expanded": false, + "tedi-accordion__header--with-icon-card": false, + }); + + expect(component.bodyClasses()).toEqual({ + "custom-body": true, + "tedi-accordion__body": true, + "tedi-accordion__body--with-icon-card": false, + }); + }); }); diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index fde51df42..0b437bc0a 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -17,6 +17,7 @@ import { TediTranslationPipe, } from "@tedi-design-system/angular/tedi"; import { AccordionComponent } from "../accordion/accordion.component"; +import { NgClass } from "@angular/common"; @Component({ selector: "tedi-accordion-item", @@ -31,6 +32,7 @@ import { AccordionComponent } from "../accordion/accordion.component"; TextComponent, LinkComponent, TediTranslationPipe, + NgClass, ], }) export class AccordionItemComponent implements OnInit { @@ -57,9 +59,9 @@ export class AccordionItemComponent implements OnInit { */ showExpandLabel = input(true); /** - * Controls whether the default expand/collapse icon is shown. + * Controls whether the default expand/collapse action is shown. */ - showExpandIcon = input(true); + showDefaultExpandAction = input(true); /** * Position of the expand action relative to the header content. */ @@ -83,6 +85,14 @@ export class AccordionItemComponent implements OnInit { * Marks the accordion item as selected. */ selected = input(false); + /** + * Custom CSS classes for the accordion header. + */ + headerClass = input(null); + /** + * Custom CSS classes for the accordion body. + */ + bodyClass = input(null); expanded = model(false); @@ -109,10 +119,33 @@ export class AccordionItemComponent implements OnInit { ); showStartExpandAction = computed( - () => this.showExpandIcon() && this.expandActionPosition() === "start", + () => + this.showDefaultExpandAction() && this.expandActionPosition() === "start", ); showEndExpandAction = computed( - () => this.showExpandIcon() && this.expandActionPosition() === "end", + () => + this.showDefaultExpandAction() && this.expandActionPosition() === "end", ); + + readonly headerClasses = computed(() => { + const customClass = this.headerClass(); + + return { + ...(customClass ? { [customClass]: true } : {}), + "tedi-accordion__header": true, + "tedi-accordion__header--hoverable": this.headerClickable(), + "tedi-accordion__header--expanded": this.expanded(), + "tedi-accordion__header--with-icon-card": this.showIconCard(), + }; + }); + + readonly bodyClasses = computed(() => { + const customClass = this.bodyClass(); + return { + ...(customClass ? { [customClass]: true } : {}), + "tedi-accordion__body": true, + "tedi-accordion__body--with-icon-card": this.showIconCard(), + }; + }); } diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts index 9b7cee62d..c06d2d4eb 100644 --- a/tedi/components/cards/accordion/accordion.stories.ts +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -4,6 +4,14 @@ import { AccordionItemComponent } from "./accordion-item/accordion-item.componen import { IconComponent, TextComponent } from "tedi/components/base"; import { ButtonComponent } from "tedi/components/buttons"; import { StatusBadgeComponent } from "community/components/tags"; +import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token"; + +document.cookie = "tedi-lang=en; path=/;"; + +/** + * Figma ↗
+ * Zeroheight ↗

+ */ export default { title: "TEDI-Ready/Components/Cards/Accordion", @@ -17,32 +25,11 @@ export default { ButtonComponent, StatusBadgeComponent, ], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "en" }], }), ], - parameters: { - docs: { - description: { - component: ` -Figma ↗
-Zeroheight ↗

- -### Slots - -| Selector | Description | -|----------|------------| -| \`[tedi-accordion-start-action]\` | Custom actions at the start of the header. | -| \`[tedi-accordion-start-before-title]\` | Custom elements before the title. | -| \`[tedi-accordion-start-after-title]\` | Custom elements after the title. | -| \`[tedi-accordion-end-action]\` | Custom actions at the end of the header. | -| \`[tedi-accordion-start-description]\` | Custom description content rendered below the title. | -| \`[tedi-accordion-end-description]\` | Custom description content rendered at the end of the header. | -| \`[tedi-accordion-icon-card]\` | Template for rendering the icon card layout. | - `, - }, - }, - }, argTypes: { - multiple: { + allowMultiple: { control: "boolean", description: "Whether multiple accordion items can be opened at once.", table: { @@ -122,7 +109,7 @@ export default { defaultValue: { summary: "true" }, }, }, - showExpandIcon: { + showDefaultExpandAction: { control: "boolean", description: "Whether to show the default expand/collapse icon. If false, you can add your own expand icon with slots.", @@ -191,6 +178,24 @@ export default { defaultValue: { summary: "false" }, }, }, + headerClass: { + control: "text", + description: "Custom CSS classes for the accordion header.", + table: { + category: "Accordion Item", + type: { summary: "string" }, + defaultValue: { summary: "" }, + }, + }, + bodyClass: { + control: "text", + description: "Custom CSS classes for the accordion body.", + table: { + category: "Accordion Item", + type: { summary: "string" }, + defaultValue: { summary: "" }, + }, + }, }, } as Meta; @@ -222,8 +227,25 @@ const actionButtonTemplate = (selectedState: string, toggleFn: string) => ` `; export const Default: StoryObj = { + parameters: { + docs: { + description: { + story: ` +| Selector | Description | +|----------|------------| +| \`[tedi-accordion-start-action]\` | Custom actions at the start of the header. | +| \`[tedi-accordion-start-before-title]\` | Custom elements before the title. | +| \`[tedi-accordion-start-after-title]\` | Custom elements after the title. | +| \`[tedi-accordion-end-action]\` | Custom actions at the end of the header. | +| \`[tedi-accordion-start-description]\` | Custom description content rendered below the title. | +| \`[tedi-accordion-end-description]\` | Custom description content rendered at the end of the header. | +| \`[tedi-accordion-icon-card]\` | Template for rendering the icon card layout. | + `, + }, + }, + }, args: { - multiple: false, + allowMultiple: false, headerClickable: true, title: "Title", titleLayout: "hug", @@ -231,13 +253,15 @@ export const Default: StoryObj = { openLabel: "open", closeLabel: "close", showExpandLabel: true, - showExpandIcon: true, + showDefaultExpandAction: true, expandActionPosition: "end", defaultExpanded: false, description: "", descriptionPosition: "start", showIconCard: false, selected: false, + headerClass: "", + bodyClass: "", }, render: (args) => ({ props: { @@ -247,7 +271,7 @@ export const Default: StoryObj = { }, }, template: ` - + ${` @if (!headerClickable) { @@ -284,6 +310,13 @@ export const Default: StoryObj = { export const Header: StoryObj = { render: () => ({ template: ` +
@@ -377,11 +410,18 @@ export const Header: StoryObj = { - + Accordion example ${contentExample} diff --git a/tedi/components/cards/accordion/accordion/accordion.component.scss b/tedi/components/cards/accordion/accordion/accordion.component.scss new file mode 100644 index 000000000..c98fcb9b1 --- /dev/null +++ b/tedi/components/cards/accordion/accordion/accordion.component.scss @@ -0,0 +1,5 @@ +tedi-accordion { + display: flex; + flex-direction: column; + gap: var(--layout-grid-gutters-08); +} diff --git a/tedi/components/cards/accordion/accordion/accordion.component.spec.ts b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts index 1797de0ff..7311ea516 100644 --- a/tedi/components/cards/accordion/accordion/accordion.component.spec.ts +++ b/tedi/components/cards/accordion/accordion/accordion.component.spec.ts @@ -10,7 +10,7 @@ import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../../tokens/translation.t standalone: true, imports: [AccordionComponent, AccordionItemComponent], template: ` - + @@ -18,7 +18,7 @@ import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../../tokens/translation.t `, }) class TestHostComponent { - multiple = false; + allowMultiple = false; } describe("AccordionComponent", () => { @@ -30,7 +30,7 @@ describe("AccordionComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TestHostComponent], - providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" }], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "en" }], }).compileComponents(); fixture = TestBed.createComponent(TestHostComponent); @@ -71,7 +71,7 @@ describe("AccordionComponent", () => { expect(items[0].expanded()).toBe(false); }); - it("should collapse other items when multiple=false", () => { + it("should collapse other items when allowMultiple=false", () => { items[0].toggle(); fixture.detectChanges(); @@ -82,8 +82,8 @@ describe("AccordionComponent", () => { expect(items[1].expanded()).toBe(true); }); - it("should allow multiple items expanded when multiple=true", () => { - host.multiple = true; + it("should allow multiple items expanded when allowMultiple=true", () => { + host.allowMultiple = true; fixture.detectChanges(); items[0].toggle(); diff --git a/tedi/components/cards/accordion/accordion/accordion.component.ts b/tedi/components/cards/accordion/accordion/accordion.component.ts index f131dcf92..9d5f3c357 100644 --- a/tedi/components/cards/accordion/accordion/accordion.component.ts +++ b/tedi/components/cards/accordion/accordion/accordion.component.ts @@ -11,6 +11,7 @@ import { AccordionItemComponent } from "../accordion-item/accordion-item.compone selector: "tedi-accordion", standalone: true, template: "", + styleUrl: "./accordion.component.scss", encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -19,12 +20,12 @@ export class AccordionComponent { * Whether the accordion allows multiple items to be expanded at the same time. * If false, opening one item will collapse the others automatically. */ - multiple = input(false); + allowMultiple = input(false); items = contentChildren(AccordionItemComponent); onItemToggled(activeItem: AccordionItemComponent) { - if (this.multiple()) return; + if (this.allowMultiple()) return; if (activeItem.expanded()) { this.items().forEach((item) => { From 8ae862c12d9efc6e71ff353daa3083e6f0e7c72c Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Thu, 5 Mar 2026 17:23:57 +0200 Subject: [PATCH 05/10] feat(accordion): add design review fixes #262 --- .../cards/accordion/accordion.stories.ts | 5 + .../accordion/accordion.component.ts | 3 + public/custom_accordion_1.png | Bin 0 -> 10855 bytes ...ion_example.png => custom_accordion_2.png} | Bin .../accordion-item.component.html | 11 +- .../accordion-item.component.scss | 5 + .../accordion-item.component.ts | 11 +- .../cards/accordion/accordion.stories.ts | 299 ++++++++++++++---- 8 files changed, 257 insertions(+), 77 deletions(-) create mode 100644 public/custom_accordion_1.png rename public/{accordion_example.png => custom_accordion_2.png} (100%) diff --git a/community/components/cards/accordion/accordion.stories.ts b/community/components/cards/accordion/accordion.stories.ts index 9601a1e91..e86873dca 100644 --- a/community/components/cards/accordion/accordion.stories.ts +++ b/community/components/cards/accordion/accordion.stories.ts @@ -39,6 +39,11 @@ export default { ], }), ], + parameters: { + status: { + type: ["deprecated"], + }, + }, } as Meta; type AccordionStory = StoryObj; diff --git a/community/components/cards/accordion/accordion/accordion.component.ts b/community/components/cards/accordion/accordion/accordion.component.ts index 37370b6b0..4021f9f0f 100644 --- a/community/components/cards/accordion/accordion/accordion.component.ts +++ b/community/components/cards/accordion/accordion/accordion.component.ts @@ -8,6 +8,9 @@ import { } from "@angular/core"; import { AccordionItemComponent } from "../accordion-item/accordion-item.component"; +/** + * @deprecated Use Accordion from TEDI-ready instead. This component will be removed from future versions. + */ @Component({ selector: "tedi-accordion", standalone: true, diff --git a/public/custom_accordion_1.png b/public/custom_accordion_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2930d0168528336df9ee22daea9c9f7bac1af65c GIT binary patch literal 10855 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

FJcQTpmJ}F=dlWX5L-|(#XJpA1* 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 } - @if (showDefaultTitle()) { + @if (showSeparateTitle()) { {{ title() | tediTranslate }} @@ -100,9 +100,7 @@ class="tedi-accordion__expand-indicator" [class.tedi-accordion__expand-indicator--with-label]="showExpandLabel()" > - - {{ (showExpandLabel() ? expandLabel() : "") | tediTranslate }} - + {{ (showExpandLabel() ? expandLabel() : "") | tediTranslate }}

} @else { @@ -115,7 +113,9 @@ [attr.aria-expanded]="expanded()" [attr.aria-controls]="bodyId" > - {{ title() | tediTranslate }} + @if (showExpandLabel()) { + {{ (showSeparateTitle() ? expandLabel() : title()) | tediTranslate }} + } } @@ -126,5 +126,6 @@ aria-hidden="true" name="expand_more" [class.tedi-accordion__icon--expanded]="expanded()" + [class.tedi-accordion__icon--no-label]="!showExpandLabel()" /> diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss index 71643d494..ded51f27f 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss @@ -133,6 +133,7 @@ &--main { display: flex; gap: inherit; + align-items: center; } &--with-description, @@ -173,3 +174,7 @@ } } } + +.tedi-link tedi-icon.tedi-accordion__icon--no-label { + font-size: var(--icon-05); +} diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index 0b437bc0a..7aa1041d4 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -48,8 +48,11 @@ export class AccordionItemComponent implements OnInit { * `fill` - container expands to available space, moving any trailing elements to the end. */ titleLayout = input<"hug" | "fill">("hug"); - /** Whether the default title text is shown in the header. */ - showDefaultTitle = input(true); + /** + * Whether the title is rendered as separate text in the accordion header. + * If false and `showExpandLabel` is true, the title is used as the expand button label. + */ + showSeparateTitle = input(true); /** Label shown when accordion is collapsed */ openLabel = input("open"); /** Label shown when accordion is expanded */ @@ -132,8 +135,8 @@ export class AccordionItemComponent implements OnInit { const customClass = this.headerClass(); return { - ...(customClass ? { [customClass]: true } : {}), "tedi-accordion__header": true, + ...(customClass ? { [customClass]: true } : {}), "tedi-accordion__header--hoverable": this.headerClickable(), "tedi-accordion__header--expanded": this.expanded(), "tedi-accordion__header--with-icon-card": this.showIconCard(), @@ -143,8 +146,8 @@ export class AccordionItemComponent implements OnInit { readonly bodyClasses = computed(() => { const customClass = this.bodyClass(); return { - ...(customClass ? { [customClass]: true } : {}), "tedi-accordion__body": true, + ...(customClass ? { [customClass]: true } : {}), "tedi-accordion__body--with-icon-card": this.showIconCard(), }; }); diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts index c06d2d4eb..f4a886a84 100644 --- a/tedi/components/cards/accordion/accordion.stories.ts +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -5,6 +5,7 @@ import { IconComponent, TextComponent } from "tedi/components/base"; import { ButtonComponent } from "tedi/components/buttons"; import { StatusBadgeComponent } from "community/components/tags"; import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token"; +import { CheckboxComponent } from "tedi/components/form"; document.cookie = "tedi-lang=en; path=/;"; @@ -24,6 +25,7 @@ export default { TextComponent, ButtonComponent, StatusBadgeComponent, + CheckboxComponent, ], providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "en" }], }), @@ -56,7 +58,6 @@ export default { table: { category: "Accordion Item", type: { summary: "string" }, - defaultValue: { summary: "Title" }, }, }, titleLayout: { @@ -72,10 +73,11 @@ export default { defaultValue: { summary: "hug" }, }, }, - showDefaultTitle: { + showSeparateTitle: { control: "boolean", description: - "Controls whether the default title text is rendered inside the header.", + "Controls whether the title is rendered as a separate text in the accordion header.\n" + + "If false and `showExpandLabel` is true, the title is used as the expand button label.", table: { category: "Accordion Item", type: { summary: "boolean" }, @@ -146,7 +148,6 @@ export default { table: { category: "Accordion Item", type: { summary: "string" }, - defaultValue: { summary: "" }, }, }, descriptionPosition: { @@ -184,7 +185,6 @@ export default { table: { category: "Accordion Item", type: { summary: "string" }, - defaultValue: { summary: "" }, }, }, bodyClass: { @@ -193,7 +193,6 @@ export default { table: { category: "Accordion Item", type: { summary: "string" }, - defaultValue: { summary: "" }, }, }, }, @@ -249,19 +248,16 @@ export const Default: StoryObj = { headerClickable: true, title: "Title", titleLayout: "hug", - showDefaultTitle: true, + showSeparateTitle: true, openLabel: "open", closeLabel: "close", showExpandLabel: true, showDefaultExpandAction: true, expandActionPosition: "end", defaultExpanded: false, - description: "", descriptionPosition: "start", showIconCard: false, selected: false, - headerClass: "", - bodyClass: "", }, render: (args) => ({ props: { @@ -276,7 +272,7 @@ export const Default: StoryObj = { [headerClickable]="headerClickable" [title]="title" [titleLayout]="titleLayout" - [showDefaultTitle]="showDefaultTitle" + [showSeparateTitle]="showSeparateTitle" [openLabel]="openLabel" [closeLabel]="closeLabel" [showExpandLabel]="showExpandLabel" @@ -307,17 +303,10 @@ export const Default: StoryObj = { }), }; -export const Header: StoryObj = { +export const Variants: StoryObj = { render: () => ({ template: ` - -
+
${contentExample} @@ -370,16 +359,20 @@ export const Header: StoryObj = { - + ${contentExample} + + + + Description - Another description + ${contentExample} @@ -387,7 +380,7 @@ export const Header: StoryObj = { @@ -400,7 +393,7 @@ export const Header: StoryObj = { @@ -408,24 +401,6 @@ export const Header: StoryObj = { ${contentExample} - - - - Accordion example - - ${contentExample} - -
`, props: { @@ -441,13 +416,13 @@ export const Header: StoryObj = { }), }; -export const HeaderWithBody: StoryObj = { +export const ActionTypes: StoryObj = { render: () => ({ template: ` -
+
@@ -476,10 +451,23 @@ export const HeaderWithBody: StoryObj = {
- + ${contentExample} + + + ${contentExample} + + +
+ +
+ + + ${contentExample} + + ${contentExample} @@ -487,17 +475,30 @@ export const HeaderWithBody: StoryObj = {
+
+ + + ${contentExample} + + + + + ${contentExample} + + +
+
- ${contentExample} ${actionButtonTemplate("selectedA", "toggleA")} + ${contentExample} @@ -505,13 +506,13 @@ export const HeaderWithBody: StoryObj = { - ${contentExample} ${actionButtonTemplate("selectedB", "toggleB")} + ${contentExample}
@@ -521,12 +522,12 @@ export const HeaderWithBody: StoryObj = { - ${contentExample} ${actionButtonTemplate("selectedC", "toggleC")} + ${contentExample}
@@ -534,13 +535,13 @@ export const HeaderWithBody: StoryObj = { - ${contentExample} ${actionButtonTemplate("selectedD", "toggleD")} + ${contentExample}
@@ -568,11 +569,11 @@ export const HeaderWithBody: StoryObj = { }), }; -export const AccordionWithIconCard: StoryObj = { +export const WithIconCard: StoryObj = { render: () => ({ template: ` -
-
+
+
${iconCardTemplate} @@ -587,7 +588,7 @@ export const AccordionWithIconCard: StoryObj = {
-
+
${iconCardTemplate} @@ -602,19 +603,19 @@ export const AccordionWithIconCard: StoryObj = {
-
+
${iconCardTemplate} - ${contentExample} ${actionButtonTemplate("selectedA", "toggleA")} + ${contentExample} @@ -623,31 +624,31 @@ export const AccordionWithIconCard: StoryObj = { [headerClickable]="false" [showIconCard]="true" [title]="'Title'" - [showDefaultTitle]="false" + [showSeparateTitle]="false" expandActionPosition="start" [defaultExpanded]="true" [selected]="selectedB" > ${iconCardTemplate} - ${contentExample} ${actionButtonTemplate("selectedB", "toggleB")} + ${contentExample}
-
+
${iconCardTemplate} - ${contentExample} ${actionButtonTemplate("selectedC", "toggleC")} + ${contentExample} @@ -656,14 +657,14 @@ export const AccordionWithIconCard: StoryObj = { [headerClickable]="false" [showIconCard]="true" [title]="'Title'" - [showDefaultTitle]="false" + [showSeparateTitle]="false" expandActionPosition="start" [defaultExpanded]="true" [selected]="selectedD" > ${iconCardTemplate} - ${contentExample} ${actionButtonTemplate("selectedD", "toggleD")} + ${contentExample}
@@ -690,3 +691,165 @@ export const AccordionWithIconCard: StoryObj = { }, }), }; + +export const Customized: StoryObj = { + render: () => ({ + props: { + selectedState: false, + toggleSelect(event: Event) { + const checkbox = event.target as HTMLInputElement; + this["selectedState"] = checkbox.checked; + }, + }, + template: ` + +
+ + + Public + ${contentExample} + + + + + + + New + ${contentExample} + + + + + + + + ${contentExample} + + + + + + Approved + ${contentExample} + + + + + + Accordion example + + mari.maasikas@gmail.com + + + Verified + + ${contentExample} + + + + + + Accordion example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + ${contentExample} + + + + + + Accordion example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + ${contentExample} + + +
+ `, + }), +}; + +export const AccordionBehavior: StoryObj = { + render: () => ({ + template: ` +
+

Single-expand accordion

+ + + ${contentExample} + + + ${contentExample} + + + +

Multi-expand accordion

+ + + ${contentExample} + + + ${contentExample} + + +
+ `, + }), +}; From f4a3c4c485c7c13e2d7f184184989f5987133421 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Fri, 6 Mar 2026 10:29:42 +0200 Subject: [PATCH 06/10] feat(accordion): fix WCAG issues #262 --- .../accordion-item/accordion-item.component.html | 3 +++ .../accordion/accordion-item/accordion-item.component.ts | 8 ++++---- tedi/components/cards/accordion/accordion.stories.ts | 7 +++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html index f5c6c69f7..f0768456f 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -112,6 +112,9 @@ (click)="toggle()" [attr.aria-expanded]="expanded()" [attr.aria-controls]="bodyId" + [attr.aria-label]=" + !showExpandLabel() ? (expandLabel() | tediTranslate) : null + " > @if (showExpandLabel()) { {{ (showSeparateTitle() ? expandLabel() : title()) | tediTranslate }} diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index 7aa1041d4..fd76e2413 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -13,11 +13,11 @@ import { IconComponent, TextComponent, LinkComponent, - generateUUID, TediTranslationPipe, } from "@tedi-design-system/angular/tedi"; import { AccordionComponent } from "../accordion/accordion.component"; import { NgClass } from "@angular/common"; +import { _IdGenerator } from "@angular/cdk/a11y"; @Component({ selector: "tedi-accordion-item", @@ -36,6 +36,9 @@ import { NgClass } from "@angular/common"; ], }) export class AccordionItemComponent implements OnInit { + readonly idGenerator = inject(_IdGenerator); + readonly bodyId = this.idGenerator.getId("tedi-accordion-body"); + readonly headerId = this.idGenerator.getId("tedi-accordion-header"); /** * If false, disables header toggling and enables using interactive elements in the accordion header. */ @@ -99,9 +102,6 @@ export class AccordionItemComponent implements OnInit { expanded = model(false); - readonly bodyId = `tedi-accordion-body-${generateUUID()}`; - readonly headerId = `tedi-accordion-header-${generateUUID()}`; - private readonly accordion = inject(AccordionComponent, { optional: true }); ngOnInit() { diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts index f4a886a84..3e276cfb7 100644 --- a/tedi/components/cards/accordion/accordion.stories.ts +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -567,6 +567,13 @@ export const ActionTypes: StoryObj = { }, }, }), + parameters: { + a11y: { + config: { + rules: [{ id: "landmark-unique", enabled: false }], + }, + }, + }, }; export const WithIconCard: StoryObj = { From a40708e21ad0785efd2a0ca80d7caa3533db9621 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Tue, 10 Mar 2026 14:07:02 +0200 Subject: [PATCH 07/10] feat(accordion): add transitions #262 --- .../cards/accordion/accordion.stories.ts | 2 +- .../accordion-item.component.html | 17 ++++---- .../accordion-item.component.scss | 40 +++++++++++++------ .../accordion-item.component.spec.ts | 2 +- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/community/components/cards/accordion/accordion.stories.ts b/community/components/cards/accordion/accordion.stories.ts index e86873dca..768b68889 100644 --- a/community/components/cards/accordion/accordion.stories.ts +++ b/community/components/cards/accordion/accordion.stories.ts @@ -41,7 +41,7 @@ export default { ], parameters: { status: { - type: ["deprecated"], + type: ["deprecated", "existsInTediReady"], }, }, } as Meta; diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html index f0768456f..03c309e9e 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -26,16 +26,16 @@ }
- @if (expanded()) { -
+
+
- } +
@@ -128,6 +128,7 @@ diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss index 2cb78af2b..42c24e4e5 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss @@ -109,11 +109,8 @@ } .tedi-accordion__icon { + transform: rotate(var(--accordion-icon-rotation, 0deg)); transition: transform 0.3s; - - &--expanded { - transform: rotateX(180deg); - } } .tedi-accordion__start { diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index fd76e2413..90df66484 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -9,12 +9,10 @@ import { model, inject, } from "@angular/core"; -import { - IconComponent, - TextComponent, - LinkComponent, - TediTranslationPipe, -} from "@tedi-design-system/angular/tedi"; +import { IconComponent } from "../../../base/icon/icon.component"; +import { TextComponent } from "../../../base/text/text.component"; +import { LinkComponent } from "../../../navigation/link/link.component"; +import { TediTranslationPipe } from "../../../../services/translation/translation.pipe"; import { AccordionComponent } from "../accordion/accordion.component"; import { NgClass } from "@angular/common"; import { _IdGenerator } from "@angular/cdk/a11y"; @@ -108,9 +106,12 @@ export class AccordionItemComponent implements OnInit { this.setExpanded(this.defaultExpanded()); } + iconRotation = 0; + toggle() { this.setExpanded(!this.expanded()); this.accordion?.onItemToggled(this); + this.iconRotation += 180; } setExpanded(value: boolean) { diff --git a/tedi/components/cards/accordion/accordion.stories.ts b/tedi/components/cards/accordion/accordion.stories.ts index 8a37178a9..27c1cd8c1 100644 --- a/tedi/components/cards/accordion/accordion.stories.ts +++ b/tedi/components/cards/accordion/accordion.stories.ts @@ -1,11 +1,14 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { AccordionComponent } from "./accordion/accordion.component"; -import { AccordionItemComponent } from "./accordion-item/accordion-item.component"; -import { IconComponent, TextComponent } from "tedi/components/base"; -import { ButtonComponent } from "tedi/components/buttons"; -import { StatusBadgeComponent } from "tedi/components/tags/status-badge/status-badge.component"; +import { + AccordionComponent, + AccordionItemComponent, + IconComponent, + TextComponent, + ButtonComponent, + StatusBadgeComponent, + CheckboxComponent, +} from "@tedi-design-system/angular/tedi"; import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token"; -import { CheckboxComponent } from "tedi/components/form"; document.cookie = "tedi-lang=en; path=/;"; @@ -727,6 +730,12 @@ export const Customized: StoryObj = { } } + ::ng-deep .tedi-accordion__header.custom-icon-rotation { + .tedi-accordion__icon--expanded { + transform: rotateX(180deg); + } + } + .custom-description { overflow: hidden; text-overflow: ellipsis; @@ -752,6 +761,7 @@ export const Customized: StoryObj = { diff --git a/tedi/components/cards/accordion/index.ts b/tedi/components/cards/accordion/index.ts new file mode 100644 index 000000000..d4c8cd77d --- /dev/null +++ b/tedi/components/cards/accordion/index.ts @@ -0,0 +1,2 @@ +export * from "./accordion/accordion.component"; +export * from "./accordion-item/accordion-item.component"; diff --git a/tedi/components/cards/index.ts b/tedi/components/cards/index.ts new file mode 100644 index 000000000..d236f3b99 --- /dev/null +++ b/tedi/components/cards/index.ts @@ -0,0 +1 @@ +export * from "./accordion"; diff --git a/tedi/components/index.ts b/tedi/components/index.ts index b92d21eb8..023e4e11a 100644 --- a/tedi/components/index.ts +++ b/tedi/components/index.ts @@ -9,3 +9,4 @@ export * from "./navigation"; export * from "./overlay"; export * from "./notifications"; export * from "./tags"; +export * from "./cards"; diff --git a/tedi/components/tags/index.ts b/tedi/components/tags/index.ts index 2a0e388a9..84d7f3348 100644 --- a/tedi/components/tags/index.ts +++ b/tedi/components/tags/index.ts @@ -1 +1,2 @@ export * from "./tag/tag.component"; +export * from "./status-badge/status-badge.component"; diff --git a/tedi/components/tags/status-badge/status-badge.component.ts b/tedi/components/tags/status-badge/status-badge.component.ts index 09d318ae7..21d66a4dc 100644 --- a/tedi/components/tags/status-badge/status-badge.component.ts +++ b/tedi/components/tags/status-badge/status-badge.component.ts @@ -7,7 +7,7 @@ import { computed, inject, } from "@angular/core"; -import { IconComponent } from "@tedi-design-system/angular/tedi"; +import { IconComponent } from "../../base/icon/icon.component"; import { _IdGenerator } from "@angular/cdk/a11y"; export type StatusBadgeColor = diff --git a/tedi/components/tags/status-badge/status-badge.stories.ts b/tedi/components/tags/status-badge/status-badge.stories.ts index 9db59e673..127f47460 100644 --- a/tedi/components/tags/status-badge/status-badge.stories.ts +++ b/tedi/components/tags/status-badge/status-badge.stories.ts @@ -4,21 +4,21 @@ import { moduleMetadata, argsToTemplate, } from "@storybook/angular"; -import { IconComponent, TextComponent } from "tedi/components/base"; -import { ButtonComponent } from "tedi/components/buttons"; -import { ColComponent, RowComponent } from "tedi/components/helpers"; import { - StatusBadgeColor, + IconComponent, + TextComponent, + ButtonComponent, StatusBadgeComponent, - StatusBadgeSize, - StatusBadgeStatus, - StatusBadgeVariant, -} from "./status-badge.component"; -import { + ColComponent, + RowComponent, TooltipComponent, TooltipContentComponent, TooltipTriggerComponent, -} from "tedi/components/overlay/tooltip"; + StatusBadgeColor, + StatusBadgeSize, + StatusBadgeStatus, + StatusBadgeVariant, +} from "@tedi-design-system/angular/tedi"; const colors: StatusBadgeColor[] = [ "neutral", From 7c8212130d7903476808d71a8e068d943eea8a9d Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Fri, 13 Mar 2026 09:01:06 +0200 Subject: [PATCH 10/10] feat(accordion): change icon transition #262 --- .../accordion/accordion-item/accordion-item.component.html | 1 - .../accordion/accordion-item/accordion-item.component.scss | 7 +++++-- .../accordion/accordion-item/accordion-item.component.ts | 3 --- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html index e7e695c2d..03c309e9e 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.html +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.html @@ -131,6 +131,5 @@ class="tedi-accordion__icon" [class.tedi-accordion__icon--expanded]="expanded()" [class.tedi-accordion__icon--no-label]="!showExpandLabel()" - [style.--accordion-icon-rotation]="iconRotation + 'deg'" /> diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss index 42c24e4e5..eaa603256 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.scss @@ -109,8 +109,11 @@ } .tedi-accordion__icon { - transform: rotate(var(--accordion-icon-rotation, 0deg)); - transition: transform 0.3s; + transition: transform 0.2s ease-in-out; + + &--expanded { + transform: rotate(-180deg); + } } .tedi-accordion__start { diff --git a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts index 90df66484..9460d9f12 100644 --- a/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts +++ b/tedi/components/cards/accordion/accordion-item/accordion-item.component.ts @@ -106,12 +106,9 @@ export class AccordionItemComponent implements OnInit { this.setExpanded(this.defaultExpanded()); } - iconRotation = 0; - toggle() { this.setExpanded(!this.expanded()); this.accordion?.onItemToggled(this); - this.iconRotation += 180; } setExpanded(value: boolean) {