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
1 change: 1 addition & 0 deletions api-goldens/element-ng/navbar-vertical-next/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class SiNavbarVerticalNextItemComponent implements OnInit {
readonly group: SiNavbarVerticalNextGroupTriggerDirective | null;
readonly hideBadgeWhenCollapsed: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly icon: _angular_core.InputSignal<string | undefined>;
readonly pinned: _angular_core.InputSignalWithTransform<boolean, unknown>;
}

// @public
Expand Down

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@panch1739 for the pinned items, should they support text + icon or only icon?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one thing to consider if we go with icon only, what should be the scenario in case of text only navbar

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mistrykaran91 after discussing with @spike-rabbit, we thought we could start only with icons

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mistrykaran91 For the text-only version, let's skip the key item feature...at least for now.

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.
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
@@ -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';

Expand Down Expand Up @@ -138,10 +138,31 @@
}

// 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: also flush the right edge (the active chip follows).
// They are auto-sized so each only consumes as much space as its label.
:host-context(.chip).is-pinned-chip {
flex: none; // override the flex: 1 from .is-chip
}

:host-context(.chip).is-chip:not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}

@include variables.media-breakpoint-down(sm) {
:host-context(.chip).is-pinned-chip {
display: none;
}
}
Comment thread
mistrykaran91 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,75 @@ describe('SiNavbarVerticalNextItemComponent', () => {
expect(linkElement).not.toHaveClass('hide-badge-collapsed');
});
});

describe('pinned input', () => {
@Component({
imports: [SiNavbarVerticalNextItemComponent],
template: `<a
si-navbar-vertical-next-item
[pinned]="pinned()"
[activeOverride]="activeOverride()"
>
Pinned Test Item
</a>`
})
class TestHostWithPinnedComponent {
readonly pinned = signal(false);
readonly activeOverride = signal<boolean | undefined>(undefined);
}

let pinnedFixture: ComponentFixture<TestHostWithPinnedComponent>;
let pinnedComponent: TestHostWithPinnedComponent;

beforeEach(async () => {
TestBed.resetTestingModule();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this really needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so

mockNavbar.chipMode.set(false);
mockNavbar.chipPortalAttached.set(false);
await TestBed.configureTestingModule({
providers: [{ provide: SI_NAVBAR_VERTICAL_NEXT, useValue: mockNavbar }]
}).compileComponents();

pinnedFixture = TestBed.createComponent(TestHostWithPinnedComponent);
pinnedComponent = pinnedFixture.componentInstance;
pinnedFixture.detectChanges();
Comment thread
mistrykaran91 marked this conversation as resolved.
});

it('should not add is-pinned-chip class when chipMode is false', async () => {
pinnedComponent.pinned.set(true);
await pinnedFixture.whenStable();

const link = pinnedFixture.nativeElement.querySelector('a');
expect(link).not.toHaveClass('is-pinned-chip');
expect(link).not.toHaveClass('is-chip');
});

it('should add is-pinned-chip class when chipMode is true and item is not active', async () => {
mockNavbar.chipMode.set(true);
pinnedComponent.pinned.set(true);
await pinnedFixture.whenStable();

const link = pinnedFixture.nativeElement.querySelector('a');
expect(link).toHaveClass('is-pinned-chip');
expect(link).toHaveClass('is-chip');
});

it('should not add is-pinned-chip class when chipMode is true but item is active', async () => {
mockNavbar.chipMode.set(true);
pinnedComponent.pinned.set(true);
pinnedComponent.activeOverride.set(true);
await pinnedFixture.whenStable();

const link = pinnedFixture.nativeElement.querySelector('a');
expect(link).not.toHaveClass('is-pinned-chip');
});

it('should not add is-pinned-chip class when item is not pinned', async () => {
mockNavbar.chipMode.set(true);
pinnedComponent.pinned.set(false);
await pinnedFixture.whenStable();

const link = pinnedFixture.nativeElement.querySelector('a');
expect(link).not.toHaveClass('is-pinned-chip');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { SI_NAVBAR_VERTICAL_NEXT } from './si-navbar-vertical-next.provider';
'[class.navbar-vertical-item]': '!isChip()',
'[class.active]': 'showActive()',
'[class.is-chip]': 'isChip()',
'[class.is-pinned-chip]': 'isPinnedChip()',
'[class.btn]': 'isChip()',
'[class.btn-primary-ghost]': 'isChip()',
'[class.hide-badge-collapsed]': 'hideBadgeWhenCollapsed()',
Expand Down Expand Up @@ -63,6 +64,16 @@ export class SiNavbarVerticalNextItemComponent implements OnInit {
/** Override the active state. Useful for action items. */
readonly activeOverride = input<boolean>();

/**
* When `true`, the item is always shown in the inline-collapse chip slot
* alongside the active item, regardless of which route is active.
*
* At screen widths less than or equal to `sm` only the active item is shown.
*
* @defaultValue false
*/
readonly pinned = input(false, { transform: booleanAttribute });
Comment thread
mistrykaran91 marked this conversation as resolved.

protected readonly navbar = inject(SI_NAVBAR_VERTICAL_NEXT);
protected readonly parent = inject(SiNavbarVerticalNextItemComponent, {
skipSelf: true,
Expand Down Expand Up @@ -165,10 +176,24 @@ export class SiNavbarVerticalNextItemComponent implements OnInit {
*/
readonly isActiveRootItem = computed(() => !this.parent && this.isOnActiveRoute());

/** `true` when this is a root-level item (no parent item).
* @internal
*/
readonly isRootItem = computed(() => !this.parent);

/** `true` when this item is pinned and currently in the chip slot.
* @internal
*/
readonly isPinnedChip = computed(
() => this.navbar.chipMode() && this.pinned() && this.isRootItem() && !this.isActiveRootItem()
);

/** `true` when this item is currently rendered as a chip in the inline-collapse bar.
* @internal
*/
readonly isChip = computed(() => this.navbar.chipPortalAttached() && this.isActiveRootItem());
readonly isChip = computed(
() => (this.navbar.chipPortalAttached() && this.isActiveRootItem()) || this.isPinnedChip()
);

ngOnInit(): void {
if (this.group && this.active()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@
</button>
</div>
</div>
@if (inlineCollapse() && activeItem(); as item) {
@let activeItem = this.activeItem();
@if (inlineCollapse() && (pinnedItems().length > 0 || activeItem)) {
<div
class="chip"
[class.chip-visible]="collapsed()"
[attr.inert]="collapsed() ? null : ''"
[attr.aria-hidden]="collapsed() ? null : 'true'"
>
@if (chipPortalAttached()) {
<ng-container [cdkPortalOutlet]="item.hostPortal" />
@if (chipMode()) {
@for (pinnedItem of pinnedItems(); track pinnedItem) {
<ng-container [cdkPortalOutlet]="pinnedItem.hostPortal" />
}
}
@if (chipPortalAttached() && activeItem && !activeItem.pinned()) {
<ng-container [cdkPortalOutlet]="activeItem.hostPortal" />
}
</div>
<ng-container #flyoutAnchorHost />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -239,6 +242,7 @@ nav {
#{$toggle-parked-inline-start} + #{$toggle-size} + #{$segmented-gap}
);
opacity: 1;
gap: $segmented-gap;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ 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`.
* @internal
* */
readonly pinnedItems = computed(() =>
this.items().filter(item => item.pinned() && item.isRootItem())
);

/** `true` when the active item's portal should occupy the chip slot.
* @internal
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ class EmptyComponent {}
</button>
</si-navbar-vertical-next-items>
}
@if (showPinnedItems()) {
<si-navbar-vertical-next-items>
<a si-navbar-vertical-next-item routerLink="pinned-item" routerLinkActive [pinned]="true">
pinned-item
</a>
</si-navbar-vertical-next-items>
}
</si-navbar-vertical-next>

<ng-template #flyoutGroup>
Expand Down Expand Up @@ -166,6 +173,7 @@ class TestHostComponent {
readonly showDeclarativeFlyoutGroup = signal(false);
readonly showDeclarativeNavigationGroup = signal(false);
readonly showDeclarativeStateGroups = signal(false);
readonly showPinnedItems = signal(false);

searchEvent(event: string): void {}
}
Expand All @@ -191,7 +199,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 }
]
Expand Down Expand Up @@ -502,5 +511,68 @@ 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);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ <h1 class="application-name">Navbar Vertical Next Example</h1>
Home
</a>
<si-navbar-vertical-next-header>Modules</si-navbar-vertical-next-header>
<a si-navbar-vertical-next-item routerLink="energy" routerLinkActive icon="element-trend">
<a
pinned
si-navbar-vertical-next-item
routerLink="energy"
routerLinkActive
icon="element-trend"
>
Energy &amp; sustainability
</a>
<button
pinned
type="button"
si-navbar-vertical-next-item
icon="element-user-group"
Expand Down
Loading