diff --git a/api-goldens/element-ng/accordion/index.api.md b/api-goldens/element-ng/accordion/index.api.md index 62ed1ce08c..b1c7f1e4cc 100644 --- a/api-goldens/element-ng/accordion/index.api.md +++ b/api-goldens/element-ng/accordion/index.api.md @@ -6,6 +6,7 @@ import { AfterContentInit } from '@angular/core'; import * as _angular_core from '@angular/core'; +import * as i1 from '@angular/aria/accordion'; import { OnChanges } from '@angular/core'; import { Subject } from 'rxjs'; import { TranslatableString } from '@siemens/element-translate-ng/translate'; diff --git a/projects/element-ng/accordion/si-accordion.component.ts b/projects/element-ng/accordion/si-accordion.component.ts index a7c062aed5..95918b9001 100644 --- a/projects/element-ng/accordion/si-accordion.component.ts +++ b/projects/element-ng/accordion/si-accordion.component.ts @@ -2,6 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ +import { AccordionGroup } from '@angular/aria/accordion'; import { AfterContentInit, booleanAttribute, @@ -31,7 +32,8 @@ const PANEL_MIN_HEIGHT = 100; host: { '[class.full-height]': 'fullHeight()', '[class.hcollapsed]': 'collapsed()' - } + }, + hostDirectives: [AccordionGroup] }) export class SiAccordionComponent implements AfterContentInit, OnChanges { /** diff --git a/projects/element-ng/accordion/si-accordion.service.ts b/projects/element-ng/accordion/si-accordion.service.ts index 707a7c2b9e..b8d05f79bf 100644 --- a/projects/element-ng/accordion/si-accordion.service.ts +++ b/projects/element-ng/accordion/si-accordion.service.ts @@ -5,15 +5,15 @@ import { Injectable, signal } from '@angular/core'; import { Subject } from 'rxjs'; +import type { SiCollapsiblePanelComponent } from './si-collapsible-panel.component'; + /** @internal */ @Injectable({ providedIn: null }) export class SiAccordionService { - /** - * Emit an item in the accordion which should be toggled. - */ - readonly toggle$ = new Subject(); + /** Emit the panel that was toggled so sibling panels can close. */ + readonly toggle$ = new Subject(); /** * Subject to emit when the items should be expanded to their full height or restored to normal height. * diff --git a/projects/element-ng/accordion/si-collapsible-panel.component.html b/projects/element-ng/accordion/si-collapsible-panel.component.html index 9c5a365e87..9b7e888534 100644 --- a/projects/element-ng/accordion/si-collapsible-panel.component.html +++ b/projects/element-ng/accordion/si-collapsible-panel.component.html @@ -1,65 +1,74 @@ -
- @let headerIcon = icon(); - @if (headerIcon) { - +} @else { +
+ - } - @if (icon() && badge() !== undefined && badge() !== '') { - - {{ badge() }} - - } -
- {{ heading() | translate }} -
+} + + +
+ @let headerIcon = icon(); + @if (headerIcon) { + + } + @if (icon() && badge() !== undefined && badge() !== '') { + + {{ badge() }} + + } +
+ {{ heading() | translate }} + +
- @if (badge() !== undefined && badge() !== '') { - - {{ badge() }} + @if (badge() !== undefined && badge() !== '') { + + {{ badge() }} + + } + + - } - - - -
-
- @if (opened()) { -
-
-
- +
+
+ +
+
+
+ +
-
- } -
+ +
+ diff --git a/projects/element-ng/accordion/si-collapsible-panel.component.ts b/projects/element-ng/accordion/si-collapsible-panel.component.ts index c057f31fc8..ede4a6f56e 100644 --- a/projects/element-ng/accordion/si-collapsible-panel.component.ts +++ b/projects/element-ng/accordion/si-collapsible-panel.component.ts @@ -2,6 +2,13 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ +import { + AccordionContent, + AccordionGroup, + AccordionPanel, + AccordionTrigger +} from '@angular/aria/accordion'; +import { NgTemplateOutlet } from '@angular/common'; import { booleanAttribute, Component, @@ -12,7 +19,8 @@ import { model, output, signal, - viewChild + viewChild, + ViewContainerRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { elementDown2 } from '@siemens/element-icons'; @@ -25,11 +33,18 @@ import { filter } from 'rxjs'; import { SiAccordionHCollapseService } from './si-accordion-hcollapse.service'; import { SiAccordionService } from './si-accordion.service'; -let controlIdCounter = 1; - @Component({ selector: 'si-collapsible-panel', - imports: [SiIconComponent, SiTranslatePipe, SiTooltipDirective], + imports: [ + AccordionContent, + AccordionGroup, + AccordionPanel, + AccordionTrigger, + NgTemplateOutlet, + SiIconComponent, + SiTranslatePipe, + SiTooltipDirective + ], templateUrl: './si-collapsible-panel.component.html', styleUrl: './si-collapsible-panel.component.scss', host: { @@ -106,19 +121,23 @@ export class SiCollapsiblePanelComponent { () => this.accordionHCollapseService?.hcollapsed() ?? false ); protected readonly fullHeight = computed(() => this.accordionService?.fullHeight() ?? false); - protected controlId = '__si-collapsible-' + controlIdCounter++; - protected headerId = this.controlId + '-header'; + private readonly standaloneAccordionGroupView = viewChild('standaloneAccordionGroup', { + read: ViewContainerRef + }); + protected readonly accordionGroupInjector = computed( + () => this.standaloneAccordionGroupView()?.injector + ); protected isHCollapsible = false; protected readonly icons = addIcons({ elementDown2 }); protected readonly disableAnimation = signal(false); - private readonly accordionService = inject(SiAccordionService, { optional: true }); + protected readonly accordionService = inject(SiAccordionService, { optional: true }); private readonly accordionHCollapseService = inject(SiAccordionHCollapseService, { optional: true }); /** Restore the content scroll position between open/close of the panel. */ private lastScrollPos = 0; - private readonly contentRef = viewChild.required>('content'); + private readonly contentRef = viewChild>('content'); constructor() { this.isHCollapsible = !!this.accordionHCollapseService; @@ -142,32 +161,23 @@ export class SiCollapsiblePanelComponent { if (open) { // Restore scroll position after opening setTimeout(() => { - this.contentRef().nativeElement.scrollTop = this.lastScrollPos; + const content = this.contentRef(); + if (content) { + content.nativeElement.scrollTop = this.lastScrollPos; + } }); } else { // Save scroll position before closing - this.lastScrollPos = this.contentRef().nativeElement.scrollTop; + this.lastScrollPos = this.contentRef()?.nativeElement.scrollTop ?? 0; } } - protected doToggle(event?: Event): void { - if (this.disabled()) { - return; - } - - event?.preventDefault(); - const opened = this.opened(); - this.panelToggle.emit(!opened); - this.openClose(this.hcollapsed() || !opened); + protected doToggle(open: boolean): void { + this.panelToggle.emit(open); + this.openClose(this.hcollapsed() || open); this.accordionService?.toggle$.next(this); if (this.hcollapsed()) { this.accordionHCollapseService?.open$.next(this); } } - - protected keydown(event: KeyboardEvent): void { - if (event.key === 'Enter' || event.key === 'Space' || event.key === ' ') { - this.doToggle(undefined); - } - } }