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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { ButtonComponent } from "@tedi-design-system/angular/tedi";
styleUrl: "./table-of-contents-item.component.scss",
imports: [ButtonComponent],
})
/**
* @deprecated Use the TEDI-Ready `tedi-table-of-contents-item` from `@tedi-design-system/angular` instead.
*/
export class TableOfContentsItemComponent {
idTo = input.required<string>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ import { Component } from "@angular/core";
selector: "tedi-table-of-contents-nested-wrapper",
template: `<ng-content></ng-content>`,
})
/**
* @deprecated Use the TEDI-Ready TableOfContents from `@tedi-design-system/angular` instead:
* nest `tedi-table-of-contents-item` elements directly, no wrapper needed.
*/
export class TableOfContentsNestedWrapperComponent {}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export type TableOfContentsBreakpoint =
NgTemplateOutlet,
],
})
/**
* @deprecated Use the TEDI-Ready TableOfContents from `@tedi-design-system/angular` instead:
* `tedi-table-of-contents` with `tedi-table-of-contents-item`.
*/
export class TableOfContentsComponent implements OnDestroy, AfterContentInit {
/**
* Heading of the table of contents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type StoryTableOfContentsComponent = TableOfContentsComponent & {
**/
const meta: Meta<StoryTableOfContentsComponent> = {
title: "Community/Navigation/Table of Contents",
parameters: {
status: {
type: ["deprecated", "existsInTediReady"],
},
},
argTypes: {
heading: {
control: "text",
Expand Down
51 changes: 51 additions & 0 deletions skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,57 @@ Render the prev/next arrows as labelled primary buttons with custom icons:
</tedi-horizontal-stepper>
```

### TableOfContents
**Selector:** `tedi-table-of-contents`
**Composition:** compose from `tedi-table-of-contents-item` elements. An item's non-item content (a `tedi-link`, anchor or button) is its label; nested `tedi-table-of-contents-item` children become its sub-items (two levels, distinguished by indent). Renders a `nav` landmark wrapping a `role="list"` list; the active item carries `aria-current="true"`.
**Inputs:**
- `heading: string | null` — heading above the list; defaults to the `table-of-contents.title` translation (`Sisukord` in et). Pass `null` (or empty) to render headless — the `nav` keeps an accessible name via `aria-label`
- `headingLevel: "h1"–"h6" = "h3"` — semantic level of the heading element; the visual style stays H4. Set it to match the surrounding page's heading outline (avoids skipped levels)
- `variant: TableOfContentsVariant = "default"` — `default` renders inside a bordered card; `transparent` drops the card chrome and shows a continuous grey left rail
- `activeId: string` — id of the active item; it gets the accent bar + active colour, and the branch leading to it auto-expands
- `padding: number` — inner padding in rem; defaults to the card medium padding token
- `showIcons: boolean = false` — show a validation glyph before each item (check / warning / empty circle) with a localised text alternative; for multistep forms
- `numbered: boolean = false` — render an auto-generated hierarchical number before each item (`1.`, `2.`, `2.1`)
- `sticky: boolean = true` — stick the container to the viewport while scrolling
- `ariaLabel: string` — accessible name for the `nav` landmark; overrides the default (the heading via `aria-labelledby`, or the localised title when headless)

**Sub-component:** `tedi-table-of-contents-item`
- `itemId: string` — required to mark the item active (via the parent's `activeId`) and to parent nested items (named `itemId`, not `id`, to avoid shadowing the native attribute)
- `isValid: boolean | undefined` — validation state rendered when `showIcons` is on (`true` valid, `false` invalid, `undefined` not yet validated)
- `separator: boolean = false` — render a separator below the item
- `hideIcon: boolean = false` — hide the validation glyph for this item even when `showIcons` is on

```html
<tedi-table-of-contents heading="Sisukord" activeId="methods">
<tedi-table-of-contents-item itemId="intro">
<a tedi-link href="#intro" [underline]="false">Sissejuhatus</a>
</tedi-table-of-contents-item>
<tedi-table-of-contents-item itemId="methods">
<a tedi-link href="#methods" [underline]="false">Meetodid</a>
<tedi-table-of-contents-item itemId="methods-1">
<a tedi-link href="#methods-1" [underline]="false">Andmete kogumine</a>
</tedi-table-of-contents-item>
</tedi-table-of-contents-item>
</tedi-table-of-contents>
```

**Mobile variant:** `tedi-table-of-contents-collapsible` — a bottom bar that opens the list in a bottom-sheet overlay; takes the same `tedi-table-of-contents-item` children. Render it on small viewports (e.g. behind a `tedi-show-at` / `tedi-hide-at`).
- `heading: string | null` — bar/sheet title; defaults to the `table-of-contents.title` translation
- `activeId: string`, `showIcons: boolean = false`, `numbered: boolean = false` — same behavior as the desktop component
- `sticky: boolean = true` — pin the bar to the bottom of the viewport; set `false` to render it inline
- `ariaLabel: string` — accessible name for the sheet's navigation landmark and dialog; defaults to the visible title

```html
<tedi-table-of-contents-collapsible heading="Sisukord" activeId="methods">
<tedi-table-of-contents-item itemId="intro">
<a tedi-link href="#intro" [underline]="false">Sissejuhatus</a>
</tedi-table-of-contents-item>
<tedi-table-of-contents-item itemId="methods">
<a tedi-link href="#methods" [underline]="false">Meetodid</a>
</tedi-table-of-contents-item>
</tedi-table-of-contents-collapsible>
```

### Tabs
**Selector:** `tedi-tabs`
**Inputs:**
Expand Down
1 change: 1 addition & 0 deletions tedi/components/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./breadcrumbs";
export * from "./horizontal-stepper";
export * from "./link/link.component";
export * from "./pagination";
export * from "./table-of-contents";
export * from "./tabs";
3 changes: 3 additions & 0 deletions tedi/components/navigation/table-of-contents/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./table-of-contents.component";
export * from "./table-of-contents-item/table-of-contents-item.component";
export * from "./table-of-contents-collapsible/table-of-contents-collapsible.component";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<div
class="tedi-table-of-contents__bar"
[class.tedi-table-of-contents__bar--static]="!sticky()"
[class.tedi-table-of-contents__bar--hidden]="open()"
>
<span tedi-text modifiers="bold" class="tedi-table-of-contents__bar-title">
{{ title() }}
</span>
<button
tedi-collapse-button
aria-haspopup="dialog"
[open]="open()"
[openText]="openLabel()"
[closeText]="closeLabel()"
[underline]="false"
(openChange)="toggle($event)"
></button>
</div>

<ng-template #sheet>
<div class="tedi-table-of-contents tedi-table-of-contents__sheet">
<div class="tedi-table-of-contents__sheet-header">
<span tedi-text modifiers="bold" class="tedi-table-of-contents__bar-title">
{{ title() }}
</span>
<button
tedi-collapse-button
[open]="true"
[openText]="openLabel()"
[closeText]="closeLabel()"
[underline]="false"
(openChange)="close()"
></button>
</div>
<nav [attr.aria-label]="navLabel()" (click)="onListClick($event)">
<ul
class="tedi-table-of-contents__list tedi-table-of-contents__list--headless"
role="list"
>
<ng-content select="tedi-table-of-contents-item" />
</ul>
</nav>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Component, input } from "@angular/core";
import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";

import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../../tokens/translation.token";
import { TableOfContentsCollapsibleComponent } from "./table-of-contents-collapsible.component";
import { TableOfContentsItemComponent } from "../table-of-contents-item/table-of-contents-item.component";

@Component({
standalone: true,
imports: [TableOfContentsCollapsibleComponent, TableOfContentsItemComponent],
template: `
<tedi-table-of-contents-collapsible [heading]="heading()" activeId="methods">
<tedi-table-of-contents-item itemId="intro">
<a href="#intro">Sissejuhatus</a>
</tedi-table-of-contents-item>
<tedi-table-of-contents-item itemId="methods">
<a href="#methods">Meetodid</a>
<tedi-table-of-contents-item itemId="methods-1">
<a href="#methods-1">Andmete kogumine</a>
</tedi-table-of-contents-item>
</tedi-table-of-contents-item>
<tedi-table-of-contents-item itemId="results">
<a href="#results">Tulemused</a>
</tedi-table-of-contents-item>
</tedi-table-of-contents-collapsible>
`,
})
class HostComponent {
readonly heading = input<string | null | undefined>("Sisukord");
}

describe("TableOfContentsCollapsibleComponent", () => {
let fixture: ComponentFixture<HostComponent>;

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

fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
});

const barButton = (): HTMLButtonElement =>
fixture.debugElement.query(
By.css(".tedi-table-of-contents__bar button[tedi-collapse-button]"),
).nativeElement;

const sheet = (): HTMLElement | null =>
document.querySelector(".tedi-table-of-contents__sheet");

const collapsible = (): TableOfContentsCollapsibleComponent =>
fixture.debugElement.query(By.directive(TableOfContentsCollapsibleComponent))
.componentInstance;

const openSheet = () => {
barButton().click();
fixture.detectChanges();
flush();
fixture.detectChanges();
};

afterEach(fakeAsync(() => {
const closeButton = document.querySelector<HTMLButtonElement>(
".tedi-table-of-contents__sheet-header button[tedi-collapse-button]",
);
closeButton?.click();
fixture.detectChanges();
flush();
}));

it("renders the bottom bar with the heading and a closed trigger", () => {
expect(fixture.debugElement.query(By.css(".tedi-table-of-contents__bar"))).toBeTruthy();
expect(fixture.nativeElement.textContent).toContain("Sisukord");
expect(barButton().getAttribute("aria-haspopup")).toBe("dialog");
expect(sheet()).toBeNull();
});

it("opens the sheet, listing the items and auto-expanding the active branch", fakeAsync(() => {
openSheet();

const panel = sheet();
expect(panel).toBeTruthy();
const links = Array.from(panel!.querySelectorAll("a")).map((a) => a.textContent?.trim());
expect(links).toContain("Sissejuhatus");
expect(links).toContain("Andmete kogumine");
}));

it("closes the sheet from the sheet header", fakeAsync(() => {
openSheet();
expect(sheet()).toBeTruthy();

const closeButton = document.querySelector<HTMLButtonElement>(
".tedi-table-of-contents__sheet-header button[tedi-collapse-button]",
)!;
closeButton.click();
fixture.detectChanges();
flush();

expect(sheet()).toBeNull();
}));

it("closes the sheet when a list link is activated", fakeAsync(() => {
openSheet();
const link = sheet()!.querySelector<HTMLAnchorElement>("a")!;
link.click();
fixture.detectChanges();
flush();

expect(sheet()).toBeNull();
}));

it("closes the sheet when the trigger is toggled off", fakeAsync(() => {
openSheet();
expect(sheet()).toBeTruthy();

collapsible().toggle(false);
fixture.detectChanges();
flush();

expect(sheet()).toBeNull();
}));

it("closes the sheet when Escape is pressed", fakeAsync(() => {
openSheet();
expect(sheet()).toBeTruthy();

document.body.dispatchEvent(
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
);
fixture.detectChanges();
flush();

expect(sheet()).toBeNull();
}));

it("falls back to the localized title when no heading is provided", () => {
fixture.componentRef.setInput("heading", null);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain("Table of contents");
});

it("uses the localized title when heading is left undefined", () => {
fixture.componentRef.setInput("heading", undefined);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain("Table of contents");
});

it("ignores a second open request while the sheet is already open", fakeAsync(() => {
openSheet();
expect(document.querySelectorAll(".tedi-table-of-contents__sheet")).toHaveLength(1);

collapsible().toggle(true);
fixture.detectChanges();
flush();

expect(document.querySelectorAll(".tedi-table-of-contents__sheet")).toHaveLength(1);
expect(sheet()).toBeTruthy();
}));
});
Loading
Loading