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
10 changes: 10 additions & 0 deletions docs/components/layout-navigation/list-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ though it can also be moved to suite the design needs.

<si-docs-component example="si-list-details/si-list-details" height="500"></si-docs-component>

### Custom details header

You can also use a custom header instead of `<si-details-pane-header>`. 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.

<si-docs-component example="si-list-details/si-list-details-custom-header" height="500"></si-docs-component>

<si-docs-api component="SiListDetailsComponent"></si-docs-api>

<si-docs-api component="SiListPaneComponent"></si-docs-api>
Expand Down
24 changes: 24 additions & 0 deletions playwright/e2e/element-examples/si-list-details.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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+/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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+/
108 changes: 108 additions & 0 deletions src/app/examples/si-list-details/si-list-details-custom-header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<si-list-details
class="p-6 px-md-9"
[expandBreakpoint]="expandBreakpoint"
[listWidth]="35"
[minListSize]="220"
[minDetailsSize]="320"
[(detailsActive)]="detailsActive"
>
<si-list-pane class="card">
<si-list-pane-header>
<span class="si-h5">Users</span>
</si-list-pane-header>
<si-list-pane-body>
<div class="si-layout-fixed-height overflow-auto">
<div class="list-group list-group-flush">
@for (user of users; track user.id) {
<button
type="button"
class="list-group-item list-group-item-action d-flex align-items-center gap-4"
[class.active]="selectedUser().id === user.id"
[attr.aria-current]="selectedUser().id === user.id ? 'true' : null"
[attr.aria-label]="'Select ' + user.name"
(click)="selectUser(user)"
>
<si-avatar
size="small"
autoColor="true"
[initials]="user.initials"
[altText]="user.name"
/>
<span class="d-flex flex-column text-start overflow-hidden">
<span class="si-body-sbold text-truncate">{{ user.name }}</span>
<span class="si-body-sm text-secondary text-truncate">{{ user.role }}</span>
</span>
</button>
}
</div>
</div>
</si-list-pane-body>
</si-list-pane>

<si-details-pane class="card">
<header class="flex-shrink-0">
<div class="d-flex align-items-center gap-4 p-6">
@if (showBackButton()) {
<button
#backButton
type="button"
class="btn btn-icon btn-tertiary flex-shrink-0"
aria-label="Back to users"
(click)="backToList()"
>
<si-icon class="icon" [icon]="icons.elementBack" />
</button>
}
<si-avatar
size="regular"
autoColor="true"
class="flex-shrink-0"
[initials]="selectedUser().initials"
[altText]="selectedUser().name"
/>
<div class="flex-grow-1 overflow-hidden">
<h2 class="si-h4 text-truncate mb-1">{{ selectedUser().name }}</h2>
<p class="si-body-paragraph text-secondary text-wrap mb-0">
{{ selectedUser().description }}
</p>
</div>
<si-menu-bar class="flex-shrink-0" aria-label="User actions">
<si-menu-item [icon]="icons.elementEdit" (triggered)="editUser()">Edit</si-menu-item>
<si-menu-item [icon]="icons.elementDelete" (triggered)="deleteUser()"
>Delete</si-menu-item
>
</si-menu-bar>
</div>

<div class="nav-tabs d-flex align-items-center px-6">
<si-tabset #tabset class="flex-fill overflow-hidden">
<si-tab heading="General" [active]="true">
<div class="p-6">
<dl class="row mb-0">
<dt class="col-sm-4">Role</dt>
<dd class="col-sm-8">{{ selectedUser().role }}</dd>
<dt class="col-sm-4">Email</dt>
<dd class="col-sm-8">{{ selectedUser().email }}</dd>
<dt class="col-sm-4">Phone</dt>
<dd class="col-sm-8">{{ selectedUser().phone }}</dd>
</dl>
</div>
</si-tab>
<si-tab heading="Address">
<div class="p-6">
<address class="mb-0">
{{ selectedUser().street }}<br />
{{ selectedUser().city }}<br />
{{ selectedUser().country }}
</address>
</div>
</si-tab>
</si-tabset>
</div>
</header>

<si-details-pane-body>
<si-tab-portal class="si-layout-fixed-height overflow-auto" [tabset]="tabset" />
</si-details-pane-body>
</si-details-pane>
</si-list-details>
142 changes: 142 additions & 0 deletions src/app/examples/si-list-details/si-list-details-custom-header.ts
Original file line number Diff line number Diff line change
@@ -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<User>(this.users[0]);

private readonly injector = inject(Injector);
private readonly listDetails = viewChild(SiListDetailsComponent);
private readonly backButton = viewChild<ElementRef<HTMLButtonElement>>('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}`);
}
}
Loading