diff --git a/api-goldens/element-ng/navbar-vertical-next/index.api.md b/api-goldens/element-ng/navbar-vertical-next/index.api.md index 2f640df2e8..11cf18c2c0 100644 --- a/api-goldens/element-ng/navbar-vertical-next/index.api.md +++ b/api-goldens/element-ng/navbar-vertical-next/index.api.md @@ -73,6 +73,7 @@ export class SiNavbarVerticalNextItemComponent implements OnInit { readonly group: SiNavbarVerticalNextGroupTriggerDirective | null; readonly hideBadgeWhenCollapsed: _angular_core.InputSignalWithTransform; readonly icon: _angular_core.InputSignal; + readonly pinned: _angular_core.InputSignalWithTransform; } // @public diff --git a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-dark-linux.png b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-dark-linux.png index c66258ab06..fc2417918c 100644 --- a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3f51ec80829e160b0cc63b9d9850d6c21d7eb8c3bd15661e6bec209895ffb3 -size 16520 +oid sha256:268719f1b684cc6e92064e479989c3eb0f26925211938f4f0149a60aa59d7c5e +size 17780 diff --git a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-light-linux.png b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-light-linux.png index 8d4b5a7e9b..fa6316da16 100644 --- a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-chip-submenu-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98cb81ae571038770347dc3620a8e60a2b03f30c31f577c2b1e7edc4ab51c7cb -size 16467 +oid sha256:88af6065da291067fd3db4688bdc12973d4ab1838eef0aa4bf9613476410aae4 +size 17620 diff --git a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-dark-linux.png b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-dark-linux.png index a1d23197bb..cf44d06594 100644 --- a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b767338a53481bf44a433b3d0e8ceb1dd1921a5883bfbbcad74f589ce023af3c -size 13052 +oid sha256:c1b9159b6930c74120ace756b9b862366f7f2690297c06b3e0ec05d1901e78dc +size 14016 diff --git a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-light-linux.png b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-light-linux.png index d3ab751ae4..346b692318 100644 --- a/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/navbar-vertical-next.spec.ts-snapshots/si-navbar-vertical-next--si-navbar-vertical-next--inline-collapse-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdae2284c7983a482cef9f1b0295ad33158f32f9bff29c1f126b8b5f0f95f53a -size 12712 +oid sha256:ba78e61ea03561c7ec31c5f01b49b7fb1bae40e3f770f1624c1baa15859239aa +size 13593 diff --git a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next-item.component.scss b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next-item.component.scss index ea163c5f96..004dd0a79a 100644 --- a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next-item.component.scss +++ b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next-item.component.scss @@ -1,5 +1,5 @@ @use 'sass:map'; -@use '@siemens/element-theme/src/styles/variables'; +@use '@siemens/element-theme/src/styles/all-variables' as variables; @use '@siemens/element-theme/src/styles/bootstrap/mixins/badge-text'; @use '@siemens/element-theme/src/styles/bootstrap/mixins/visually-hidden'; @@ -138,10 +138,39 @@ } // Within the chip slot: fuse the left edge with the toggle button -// (segmented control) and fill the wrapper so the label can truncate. +// (segmented control). The active chip grows to fill remaining space. :host-context(.chip).is-chip { border-start-start-radius: 0; border-end-start-radius: 0; - inline-size: 100%; + flex: 1; min-inline-size: 0; + + &:focus-visible { + z-index: variables.$zindex-vertical-nav + 1; + } +} + +// Pinned chips are icon-only: hide the label when an icon is present. +// Using visually-hidden (not display:none) preserves the text as the +// accessible name of the host / - @if (inlineCollapse() && activeItem(); as item) { + @let activeItem = this.activeItem(); + @if (inlineCollapse() && (pinnedItems().length > 0 || activeItem)) {
- @if (chipPortalAttached()) { - + @if (chipMode()) { + @for (pinnedItem of pinnedItems(); track pinnedItem) { + + } + } + @if (chipPortalAttached() && activeItem && !activeItem.pinned()) { + }
diff --git a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.scss b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.scss index 9125eed610..db5536d7b4 100644 --- a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.scss +++ b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.scss @@ -213,6 +213,9 @@ nav { position: fixed; z-index: 0; inset-block-start: $toggle-parked-block-start; + // Flex row so multiple items (pinned + active) sit side-by-side. + display: flex; + align-items: stretch; // Cap the chip width so long labels truncate via `.item-title.text-truncate` // instead of overflowing the viewport. Reserves space for the toggle slot // on the start and an equal end-gutter so the chip never butts the edge. @@ -239,6 +242,7 @@ nav { #{$toggle-parked-inline-start} + #{$toggle-size} + #{$segmented-gap} ); opacity: 1; + gap: $segmented-gap; } } diff --git a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.ts b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.ts index 963622332c..c4e3c2218d 100644 --- a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.ts +++ b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.component.ts @@ -176,6 +176,15 @@ export class SiNavbarVerticalNextComponent implements OnChanges, OnInit { */ readonly activeItem = computed(() => this.items().find(item => item.isActiveRootItem())); + /** All root items with `pinned: true`. Each gets a stable portal outlet in the chip slot; + * the active pinned item stays in this list and is styled as the active chip via `isActiveRootItem`. + * Excluded in `textOnly` mode as there are no icons to show icon-only chips. + * @internal + * */ + readonly pinnedItems = computed(() => + this.items().filter(item => item.pinned() && item.isRootItem() && !this.textOnly()) + ); + /** `true` when the active item's portal should occupy the chip slot. * @internal */ diff --git a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.spec.ts b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.spec.ts index 401e6f38c4..bb7fb60b4c 100644 --- a/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.spec.ts +++ b/projects/element-ng/navbar-vertical-next/si-navbar-vertical-next.spec.ts @@ -115,6 +115,13 @@ class EmptyComponent {} } + @if (showPinnedItems()) { + + + pinned-item + + + } @@ -167,6 +174,7 @@ class TestHostComponent { readonly showDeclarativeFlyoutGroup = signal(false); readonly showDeclarativeNavigationGroup = signal(false); readonly showDeclarativeStateGroups = signal(false); + readonly showPinnedItems = signal(false); searchEvent(event: string): void {} } @@ -192,7 +200,8 @@ describe('SiNavbarVerticalNext', () => { { path: 'sub-item-2/sub-path', component: EmptyComponent } ] }, - { path: 'somewhere-else', component: EmptyComponent } + { path: 'somewhere-else', component: EmptyComponent }, + { path: 'pinned-item', component: EmptyComponent } ]), { provide: BreakpointObserver, useExisting: BreakpointObserverMock } ] @@ -503,5 +512,78 @@ describe('SiNavbarVerticalNext', () => { expect(flyoutMenu!.textContent).toContain('sub-item1'); }); }); + + describe('pinned items', () => { + beforeEach(() => { + component.collapsed.set(true); + component.showPinnedItems.set(true); + }); + + it('should render pinned item in chip slot when collapsed', async () => { + await fixture.whenStable(); + + const host = fixture.nativeElement.querySelector('si-navbar-vertical-next') as HTMLElement; + const chipWrapper = host.querySelector('.chip') as HTMLElement; + expect(chipWrapper).not.toHaveAttribute('inert'); + const pinnedLink = page + .elementLocator(chipWrapper) + .getByRole('link', { name: 'pinned-item' }); + await expect.element(pinnedLink).toBeVisible(); + expect(pinnedLink.element()).toHaveClass('is-pinned-chip'); + }); + + it('should not render pinned items in chip slot when expanded', async () => { + component.collapsed.set(false); + await fixture.whenStable(); + + const host = fixture.nativeElement.querySelector('si-navbar-vertical-next') as HTMLElement; + const chipWrapper = host.querySelector('.chip') as HTMLElement; + // Chip container still renders because pinnedItems.length > 0, but is inert + expect(chipWrapper).not.toBeNull(); + expect(chipWrapper).toHaveAttribute('inert', ''); + // No items teleported into chip slot when chipMode is inactive + expect(chipWrapper.querySelectorAll('a, button')).toHaveLength(0); + }); + + it('should render both pinned and active items in chip slot', async () => { + await TestBed.inject(Router).navigate(['/item-1/sub-item-1']); + await fixture.whenStable(); + + const host = fixture.nativeElement.querySelector('si-navbar-vertical-next') as HTMLElement; + const chipWrapper = host.querySelector('.chip') as HTMLElement; + const pinnedLink = page + .elementLocator(chipWrapper) + .getByRole('link', { name: 'pinned-item' }); + const activeButton = page + .elementLocator(chipWrapper) + .getByRole('button', { name: 'item1' }); + await expect.element(pinnedLink).toBeVisible(); + await expect.element(activeButton).toBeVisible(); + // pinned-item is not active here, so it keeps is-pinned-chip + expect(pinnedLink.element()).toHaveClass('is-pinned-chip'); + // item1 is the active chip + expect(activeButton.element()).toHaveClass('is-chip'); + expect(activeButton.element()).not.toHaveClass('is-pinned-chip'); + }); + + it('should render pinned active item only once in chip slot', async () => { + await TestBed.inject(Router).navigate(['/pinned-item']); + await fixture.whenStable(); + + const host = fixture.nativeElement.querySelector('si-navbar-vertical-next') as HTMLElement; + const chipWrapper = host.querySelector('.chip') as HTMLElement; + expect(chipWrapper.querySelectorAll('.is-chip')).toHaveLength(1); + }); + + it('should not render pinned items in chip slot when textOnly is true', async () => { + component.textOnly.set(true); + await fixture.whenStable(); + + const host = fixture.nativeElement.querySelector('si-navbar-vertical-next') as HTMLElement; + // pinnedItems is empty in textOnly mode; chip container only renders when there + // are pinned or active items, so it must not be present here. + expect(host.querySelector('.chip')).toBeNull(); + }); + }); }); }); diff --git a/src/app/examples/si-navbar-vertical-next/si-navbar-vertical-next.html b/src/app/examples/si-navbar-vertical-next/si-navbar-vertical-next.html index 1a1ec31487..79d93d4a4d 100644 --- a/src/app/examples/si-navbar-vertical-next/si-navbar-vertical-next.html +++ b/src/app/examples/si-navbar-vertical-next/si-navbar-vertical-next.html @@ -18,10 +18,17 @@

Navbar Vertical Next Example

Home Modules - + Energy & sustainability