diff --git a/docs/components/layout-navigation/list-details.md b/docs/components/layout-navigation/list-details.md index bc74d61d69..d2b301c115 100644 --- a/docs/components/layout-navigation/list-details.md +++ b/docs/components/layout-navigation/list-details.md @@ -156,6 +156,16 @@ though it can also be moved to suite the design needs. +### Custom details header + +You can also use a custom header instead of ``. When using a custom header: + +- **Opening the pane:** The custom header must provide a responsive Back button and set focus to it immediately when opened. + +- **Closing the pane:** The si-list-details component automatically restores focus to the selected list item. + + + diff --git a/playwright/e2e/element-examples/si-list-details.spec.ts b/playwright/e2e/element-examples/si-list-details.spec.ts index eaf4e996a1..655a0dc149 100644 --- a/playwright/e2e/element-examples/si-list-details.spec.ts +++ b/playwright/e2e/element-examples/si-list-details.spec.ts @@ -112,4 +112,28 @@ test.describe('si-list-details', () => { await page.goBack(); await expect(page.getByRole('cell', { name: 'Max Meier 2', exact: true })).toBeInViewport(); }); + + test('with custom details header', async ({ page, si }, testInfo) => { + const customHeaderExample = 'si-list-details/si-list-details-custom-header'; + + await si.visitExample(customHeaderExample); + await si.runVisualAndA11yTests(); + + await page.getByRole('tab', { name: 'Address' }).click(); + await expect(page.getByRole('tabpanel')).toContainText('Industriestrasse 10'); + + await page.setViewportSize({ width: 600, height: 800 }); + await si.visitExample(customHeaderExample); + + const firstUser = page.getByRole('button', { name: 'Select Jane Smith' }); + const backButton = page.getByRole('button', { name: 'Back to users' }); + await expect(firstUser).toBeInViewport(); + await firstUser.click(); + await expect(backButton).toBeFocused(); + await expect(page.getByRole('heading', { name: 'Jane Smith', level: 2 })).toBeInViewport(); + await si.runVisualAndA11yTests('mobile-details'); + + await backButton.click(); + await expect(firstUser).toBeFocused(); + }); }); diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-dark-linux.png b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-dark-linux.png new file mode 100644 index 0000000000..b179bf52f3 --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-dark-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a66fe8957fd46aacd7e2323234f0264bfc68265fcd53a2544c5daad9a90fde +size 21459 diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-light-linux.png b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-light-linux.png new file mode 100644 index 0000000000..62339fab09 --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details-element-examples-chromium-light-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90f25cf1d13502e37486cd8acd8ccc5dbe1a9e701d3eff38589d147e29a679cb +size 20765 diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details.yaml b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details.yaml new file mode 100644 index 0000000000..ac00f1247f --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header--mobile-details.yaml @@ -0,0 +1,22 @@ +- text: Users +- button "Select Jane Smith": JS Jane Smith Automation Engineer +- button "Select John Doe": JD John Doe Product Designer +- button "Select Alex Johnson": AJ Alex Johnson Platform Architect +- banner: + - button "Back to users" + - text: JS + - heading "Jane Smith" [level=2] + - paragraph: Automation engineer focused on industrial connectivity. + - menubar "User actions": + - menuitem "Edit" + - menuitem "Delete" + - tablist: + - tab "General" [selected] + - tab "Address" +- tabpanel "General": + - term: Role + - definition: Automation Engineer + - term: Email + - definition: jane.smith@example.org + - term: Phone + - definition: /\+\d+ \d+ \d+ \d+ \d+/ \ No newline at end of file diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-dark-linux.png b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-dark-linux.png new file mode 100644 index 0000000000..b0e56c212b --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-dark-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e538bfd564b84babaea3c0f48a68491246c553b1d5e7cad7cffb6ff7f797f26 +size 32679 diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-light-linux.png b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-light-linux.png new file mode 100644 index 0000000000..4dfd645efc --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header-element-examples-chromium-light-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7722c309483d9fcad7fe7bab6633c38bad2f94d083f603cbf16331e3f42601df +size 32010 diff --git a/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header.yaml b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header.yaml new file mode 100644 index 0000000000..30baffc9af --- /dev/null +++ b/playwright/snapshots/si-list-details.spec.ts-snapshots/si-list-details--si-list-details-custom-header.yaml @@ -0,0 +1,21 @@ +- text: Users +- button "Select Jane Smith": JS Jane Smith Automation Engineer +- button "Select John Doe": JD John Doe Product Designer +- button "Select Alex Johnson": AJ Alex Johnson Platform Architect +- banner: + - text: JS + - heading "Jane Smith" [level=2] + - paragraph: Automation engineer focused on industrial connectivity. + - menubar "User actions": + - menuitem "Edit" + - menuitem "Delete" + - tablist: + - tab "General" [selected] + - tab "Address" +- tabpanel "General": + - term: Role + - definition: Automation Engineer + - term: Email + - definition: jane.smith@example.org + - term: Phone + - definition: /\+\d+ \d+ \d+ \d+ \d+/ \ No newline at end of file diff --git a/src/app/examples/si-list-details/si-list-details-custom-header.html b/src/app/examples/si-list-details/si-list-details-custom-header.html new file mode 100644 index 0000000000..a19bb86fe6 --- /dev/null +++ b/src/app/examples/si-list-details/si-list-details-custom-header.html @@ -0,0 +1,108 @@ + + + + Users + + +
+
+ @for (user of users; track user.id) { + + } +
+
+
+
+ + +
+
+ @if (showBackButton()) { + + } + +
+

{{ selectedUser().name }}

+

+ {{ selectedUser().description }} +

+
+ + Edit + Delete + +
+ + +
+ + + + +
+
diff --git a/src/app/examples/si-list-details/si-list-details-custom-header.ts b/src/app/examples/si-list-details/si-list-details-custom-header.ts new file mode 100644 index 0000000000..4e0d5915c8 --- /dev/null +++ b/src/app/examples/si-list-details/si-list-details-custom-header.ts @@ -0,0 +1,142 @@ +/** + * Copyright (c) Siemens 2016 - 2026 + * SPDX-License-Identifier: MIT + */ +import { + afterNextRender, + Component, + computed, + ElementRef, + inject, + Injector, + signal, + viewChild +} from '@angular/core'; +import { elementBack, elementDelete, elementEdit } from '@siemens/element-icons'; +import { SiAvatarComponent } from '@siemens/element-ng/avatar'; +import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; +import { + SiDetailsPaneBodyComponent, + SiDetailsPaneComponent, + SiListDetailsComponent, + SiListPaneBodyComponent, + SiListPaneComponent, + SiListPaneHeaderComponent +} from '@siemens/element-ng/list-details'; +import { SiMenuBarDirective, SiMenuItemComponent } from '@siemens/element-ng/menu'; +import { BOOTSTRAP_BREAKPOINTS } from '@siemens/element-ng/resize-observer'; +import { SiTabComponent, SiTabPortalComponent, SiTabsetComponent } from '@siemens/element-ng/tabs'; +import { LOG_EVENT } from '@siemens/live-preview'; + +type User = { + id: number; + name: string; + initials: string; + description: string; + role: string; + email: string; + phone: string; + street: string; + city: string; + country: string; +}; + +@Component({ + selector: 'app-sample', + imports: [ + SiListDetailsComponent, + SiListPaneComponent, + SiListPaneHeaderComponent, + SiListPaneBodyComponent, + SiDetailsPaneComponent, + SiDetailsPaneBodyComponent, + SiAvatarComponent, + SiIconComponent, + SiMenuBarDirective, + SiMenuItemComponent, + SiTabsetComponent, + SiTabComponent, + SiTabPortalComponent + ], + templateUrl: './si-list-details-custom-header.html', + host: { + class: 'si-layout-fixed-height' + } +}) +export class SampleComponent { + readonly logEvent = inject(LOG_EVENT); + readonly icons = addIcons({ elementBack, elementDelete, elementEdit }); + readonly expandBreakpoint = BOOTSTRAP_BREAKPOINTS.mdMinimum; + readonly detailsActive = signal(false); + readonly users: User[] = [ + { + id: 1, + name: 'Jane Smith', + initials: 'JS', + description: 'Automation engineer focused on industrial connectivity.', + role: 'Automation Engineer', + email: 'jane.smith@example.org', + phone: '+41 44 123 45 67', + street: 'Industriestrasse 10', + city: 'Zug 6300', + country: 'Switzerland' + }, + { + id: 2, + name: 'John Doe', + initials: 'JD', + description: 'Product designer creating clear and accessible experiences.', + role: 'Product Designer', + email: 'john.doe@example.org', + phone: '+49 89 123 45 67', + street: 'Example Street 24', + city: 'Munich 80331', + country: 'Germany' + }, + { + id: 3, + name: 'Alex Johnson', + initials: 'AJ', + description: 'Platform architect helping teams build reliable software.', + role: 'Platform Architect', + email: 'alex.johnson@example.org', + phone: '+44 20 1234 5678', + street: 'Innovation Road 8', + city: 'London SW1A 1AA', + country: 'United Kingdom' + } + ]; + readonly selectedUser = signal(this.users[0]); + + private readonly injector = inject(Injector); + private readonly listDetails = viewChild(SiListDetailsComponent); + private readonly backButton = viewChild>('backButton'); + + readonly showBackButton = computed(() => this.listDetails()?.hasLargeSize() === false); + + selectUser(user: User): void { + this.selectedUser.set(user); + this.detailsActive.set(true); + + if (this.showBackButton()) { + afterNextRender( + () => { + this.backButton()?.nativeElement.focus(); + }, + { injector: this.injector } + ); + } + } + + backToList(): void { + this.detailsActive.set(false); + } + + editUser(): void { + this.logEvent(`Edit ${this.selectedUser().name}`); + } + + deleteUser(): void { + this.logEvent(`Delete ${this.selectedUser().name}`); + } +}