diff --git a/goldens/aria/accordion/index.api.md b/goldens/aria/accordion/index.api.md index df89ab8ce58d..3378a901b755 100644 --- a/goldens/aria/accordion/index.api.md +++ b/goldens/aria/accordion/index.api.md @@ -25,11 +25,13 @@ export class AccordionGroup { expandAll(): void; readonly multiExpandable: _angular_core.InputSignalWithTransform; readonly _pattern: AccordionGroupPattern; + _registerTrigger(trigger: AccordionTrigger): void; readonly softDisabled: _angular_core.InputSignalWithTransform; readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>; + _unregisterTrigger(trigger: AccordionTrigger): void; readonly wrap: _angular_core.InputSignalWithTransform; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } @@ -38,6 +40,7 @@ export class AccordionGroup { export class AccordionPanel { constructor(); collapse(): void; + readonly element: HTMLElement; expand(): void; readonly id: _angular_core.InputSignal; _pattern?: AccordionTriggerPattern; @@ -50,7 +53,7 @@ export class AccordionPanel { } // @public -export class AccordionTrigger implements OnInit { +export class AccordionTrigger implements OnInit, OnDestroy { readonly active: _angular_core.Signal; collapse(): void; readonly disabled: _angular_core.InputSignalWithTransform; @@ -58,6 +61,9 @@ export class AccordionTrigger implements OnInit { expand(): void; readonly expanded: _angular_core.ModelSignal; readonly id: _angular_core.InputSignal; + readonly index: _angular_core.InputSignal; + // (undocumented) + ngOnDestroy(): void; // (undocumented) ngOnInit(): void; readonly panel: _angular_core.InputSignal; @@ -65,7 +71,7 @@ export class AccordionTrigger implements OnInit { _pattern: AccordionTriggerPattern; toggle(): void; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index f484be549c6c..9a367cb10791 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -437,6 +437,12 @@ export class GridRowPattern { rowIndex: SignalLike; } +// @public (undocumented) +export interface HasElement { + // (undocumented) + element: HTMLElement; +} + // @public (undocumented) export function linkedSignal(sourceFn: () => T): WritableSignalLike; @@ -669,6 +675,9 @@ export function signal(initialValue: T): WritableSignalLike; // @public (undocumented) export type SignalLike = () => T; +// @public +export function sortDirectives(a: HasElement, b: HasElement): 1 | -1; + // @public export interface TabInputs extends Omit, Omit { tablist: SignalLike; diff --git a/src/aria/accordion/accordion-group.ts b/src/aria/accordion/accordion-group.ts index 929ae7aea49a..f6d1a013cb65 100644 --- a/src/aria/accordion/accordion-group.ts +++ b/src/aria/accordion/accordion-group.ts @@ -11,13 +11,12 @@ import { ElementRef, booleanAttribute, computed, - contentChildren, inject, input, signal, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; -import {AccordionGroupPattern} from '../private'; +import {AccordionGroupPattern, sortDirectives} from '../private'; import {AccordionTrigger} from './accordion-trigger'; import {ACCORDION_GROUP} from './accordion-tokens'; @@ -75,10 +74,23 @@ export class AccordionGroup { readonly element = this._elementRef.nativeElement as HTMLElement; /** The AccordionTriggers nested inside this group. */ - private readonly _triggers = contentChildren(AccordionTrigger, {descendants: true}); + private readonly _triggers = signal(new Set()); + + /** The AccordionTriggers nested inside this group. */ + private readonly _sortedTriggers = computed(() => { + const triggers = [...this._triggers()] as AccordionTrigger[]; + const sortFn = + triggers[0]?.index() === undefined + ? sortDirectives + : (a: AccordionTrigger, b: AccordionTrigger) => a.index()! - b.index()!; + + return triggers.sort(sortFn); + }); /** The corresponding patterns for the accordion triggers. */ - private readonly _triggerPatterns = computed(() => this._triggers().map(t => t._pattern)); + private readonly _triggerPatterns = computed(() => { + return this._sortedTriggers().map(t => t._pattern); + }); /** The text direction (ltr or rtl). */ readonly textDirection = inject(Directionality).valueSignal; @@ -117,4 +129,16 @@ export class AccordionGroup { collapseAll() { this._pattern.collapseAll(); } + + /** Internal method to register each trigger as we can not use contentChildren. */ + _registerTrigger(trigger: AccordionTrigger) { + this._triggers().add(trigger); + this._triggers.set(new Set(this._triggers())); + } + + /** Internal method to unregister each trigger as we can not use contentChildren. */ + _unregisterTrigger(trigger: AccordionTrigger) { + this._triggers().delete(trigger); + this._triggers.set(new Set(this._triggers())); + } } diff --git a/src/aria/accordion/accordion-panel.ts b/src/aria/accordion/accordion-panel.ts index ad4a0762c90e..56d52426f2a7 100644 --- a/src/aria/accordion/accordion-panel.ts +++ b/src/aria/accordion/accordion-panel.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Directive, afterRenderEffect, computed, inject, input} from '@angular/core'; +import {Directive, ElementRef, afterRenderEffect, computed, inject, input} from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {DeferredContentAware, AccordionTriggerPattern} from '../private'; @@ -48,6 +48,12 @@ import {DeferredContentAware, AccordionTriggerPattern} from '../private'; }, }) export class AccordionPanel { + /** A reference to the trigger element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the trigger element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware); diff --git a/src/aria/accordion/accordion-trigger.ts b/src/aria/accordion/accordion-trigger.ts index 238f3d8171ec..0d982bd4f1b7 100644 --- a/src/aria/accordion/accordion-trigger.ts +++ b/src/aria/accordion/accordion-trigger.ts @@ -9,6 +9,7 @@ import { Directive, ElementRef, + OnDestroy, OnInit, booleanAttribute, computed, @@ -53,7 +54,7 @@ import {AccordionPanel} from './accordion-panel'; '[attr.tabindex]': '_pattern.tabIndex()', }, }) -export class AccordionTrigger implements OnInit { +export class AccordionTrigger implements OnInit, OnDestroy { /** A reference to the trigger element. */ private readonly _elementRef = inject(ElementRef); @@ -69,12 +70,15 @@ export class AccordionTrigger implements OnInit { /** The unique identifier for the trigger. */ readonly id = input(inject(_IdGenerator).getId('ng-accordion-trigger-', true)); - /** The unique identifier for the correspondingtrigger panel. */ + /** The unique identifier for the corresponding trigger panel. */ readonly panelId = computed(() => this.panel().id()); /** Whether the trigger is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); + /** The index of the trigger within the accordion group. */ + readonly index = input(); + /** Whether the corresponding panel is expanded. */ readonly expanded = model(false); @@ -93,6 +97,14 @@ export class AccordionTrigger implements OnInit { }); this.panel()._pattern = this._pattern; + + this._accordionGroup._registerTrigger(this); + } + + ngOnDestroy() { + this.panel()._pattern = undefined; + + this._accordionGroup._unregisterTrigger(this); } /** Expands this item. */ diff --git a/src/aria/accordion/accordion.spec.ts b/src/aria/accordion/accordion.spec.ts index 0cbfb1861965..7dfce63a9af4 100644 --- a/src/aria/accordion/accordion.spec.ts +++ b/src/aria/accordion/accordion.spec.ts @@ -1,4 +1,4 @@ -import {Component, DebugElement, signal, ChangeDetectionStrategy} from '@angular/core'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private'; @@ -9,367 +9,490 @@ import {AccordionContent} from './accordion-content'; import {AccordionGroup} from './accordion-group'; describe('AccordionGroup', () => { - let fixture: ComponentFixture; - let triggerDebugElements: DebugElement[]; - let panelDebugElements: DebugElement[]; + let fixture: ComponentFixture; + let testComponent: AccordionGroupWithLoop; + let groupElement: HTMLElement; + let triggerElements: HTMLElement[]; let panelElements: HTMLElement[]; - const keydown = (target: HTMLElement, key: string) => { - target.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key})); + const click = (target: HTMLElement) => { + target.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true})); fixture.detectChanges(); }; - const click = (target: HTMLElement) => { - target.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true})); + const keydown = (key: string, target = groupElement) => { + target.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key})); fixture.detectChanges(); }; - const spaceKey = (target: HTMLElement) => keydown(target, ' '); - const enterKey = (target: HTMLElement) => keydown(target, 'Enter'); - const downArrowKey = (target: HTMLElement) => keydown(target, 'ArrowDown'); - const upArrowKey = (target: HTMLElement) => keydown(target, 'ArrowUp'); - const homeKey = (target: HTMLElement) => keydown(target, 'Home'); - const endKey = (target: HTMLElement) => keydown(target, 'End'); - - interface SetupOptions { - multiExpandable?: boolean; - disabledGroup?: boolean; - expandedItemValues?: string[]; - disabledItemValues?: string[]; - softDisabled?: boolean; - wrap?: boolean; - } + const spaceKey = (target: HTMLElement) => keydown(' ', target); + const enterKey = (target: HTMLElement) => keydown('Enter', target); + const downArrowKey = () => keydown('ArrowDown'); + const upArrowKey = () => keydown('ArrowUp'); + const homeKey = () => keydown('Home'); + const endKey = () => keydown('End'); - function configureAccordionComponent(opts: SetupOptions = {}) { - const testComponent = fixture.componentInstance as AccordionGroupExample; - - if (opts.multiExpandable !== undefined) { - testComponent.multiExpandable.set(opts.multiExpandable); - } - if (opts.disabledGroup !== undefined) { - testComponent.disabledGroup.set(opts.disabledGroup); - } - if (opts.softDisabled !== undefined) { - testComponent.softDisabled.set(opts.softDisabled); - } - if (opts.wrap !== undefined) { - testComponent.wrap.set(opts.wrap); - } - if (opts.disabledItemValues !== undefined) { - opts.disabledItemValues.forEach(value => testComponent.disableItem(value, true)); - } - if (opts.expandedItemValues !== undefined) { - opts.expandedItemValues.forEach(value => testComponent.expandItem(value, true)); - } + function setupAccordionGroup() { + testComponent = fixture.componentInstance as AccordionGroupWithLoop; + groupElement = fixture.nativeElement.querySelector('[ngAccordionGroup]') as HTMLElement; - fixture.detectChanges(); - defineTestVariables(fixture); + setupTriggerAndPanels(); } - function defineTestVariables(currentFixture: ComponentFixture) { - triggerDebugElements = currentFixture.debugElement.queryAll(By.directive(AccordionTrigger)); - panelDebugElements = currentFixture.debugElement.queryAll(By.directive(AccordionPanel)); + function setupTriggerAndPanels() { + fixture.detectChanges(); + + const triggerDebugElements = fixture.debugElement.queryAll(By.directive(AccordionTrigger)); + const panelDebugElements = fixture.debugElement.queryAll(By.directive(AccordionPanel)); triggerElements = triggerDebugElements.map(el => el.nativeElement); panelElements = panelDebugElements.map(el => el.nativeElement); } - function isTriggerActive(target: HTMLElement): boolean { - return target.getAttribute('data-active') === 'true'; - } + const isTriggerActive = (index: number) => + triggerElements[index].getAttribute('data-active') === 'true'; + const isTriggerExpanded = (index: number) => + triggerElements[index].getAttribute('aria-expanded') === 'true'; - function isTriggerExpanded(target: HTMLElement): boolean { - return target.getAttribute('aria-expanded') === 'true'; - } + const getTriggerAttribute = (index: number, attribute: string) => + triggerElements[index].getAttribute(attribute); + const getTriggerText = (index: number) => triggerElements[index].textContent?.trim(); - afterEach(async () => { - await runAccessibilityChecks(fixture.nativeElement); - }); + const getPanelAttribute = (index: number, attribute: string) => + panelElements[index].getAttribute(attribute); beforeEach(() => { TestBed.configureTestingModule({ providers: [provideFakeDirectionality('ltr'), _IdGenerator], }); + }); - fixture = TestBed.createComponent(AccordionGroupExample); + afterEach(async () => { + await runAccessibilityChecks(fixture.nativeElement); }); - describe('ARIA attributes and roles', () => { - describe('AccordionTrigger', () => { - beforeEach(() => { - configureAccordionComponent(); - }); + describe('using a loop', () => { + beforeEach(() => { + fixture = TestBed.createComponent(AccordionGroupWithLoop); - it('should have role="button"', () => { - expect(triggerElements[0].getAttribute('role')).toBe('button'); - expect(triggerElements[1].getAttribute('role')).toBe('button'); - expect(triggerElements[2].getAttribute('role')).toBe('button'); - }); + setupAccordionGroup(); + }); - it('should have aria-expanded="false" when collapsed', () => { - configureAccordionComponent(); - expect(triggerElements[0].getAttribute('aria-expanded')).toBe('false'); - expect(triggerElements[1].getAttribute('aria-expanded')).toBe('false'); - expect(triggerElements[2].getAttribute('aria-expanded')).toBe('false'); - }); + describe('ARIA attributes and roles', () => { + describe('AccordionTrigger', () => { + it('should have role="button"', () => { + expect(getTriggerAttribute(0, 'role')).toBe('button'); + expect(getTriggerAttribute(1, 'role')).toBe('button'); + expect(getTriggerAttribute(2, 'role')).toBe('button'); + }); - it('should have aria-controls pointing to the panel id', () => { - expect(triggerElements[0].getAttribute('aria-controls')).toBe(panelElements[0].id); - expect(triggerElements[1].getAttribute('aria-controls')).toBe(panelElements[1].id); - expect(triggerElements[2].getAttribute('aria-controls')).toBe(panelElements[2].id); - }); + it('should have aria-expanded="false" when collapsed', () => { + expect(getTriggerAttribute(0, 'aria-expanded')).toBe('false'); + expect(getTriggerAttribute(1, 'aria-expanded')).toBe('false'); + expect(getTriggerAttribute(2, 'aria-expanded')).toBe('false'); + }); + + it('should have aria-controls pointing to the panel id', () => { + expect(getTriggerAttribute(0, 'aria-controls')).toBe(panelElements[0].id); + expect(getTriggerAttribute(1, 'aria-controls')).toBe(panelElements[1].id); + expect(getTriggerAttribute(2, 'aria-controls')).toBe(panelElements[2].id); + }); + + it('should have aria-disabled="false" when not disabled', () => { + expect(getTriggerAttribute(0, 'aria-disabled')).toBe('false'); + expect(getTriggerAttribute(1, 'aria-disabled')).toBe('false'); + expect(getTriggerAttribute(2, 'aria-disabled')).toBe('false'); + }); - it('should have aria-disabled="false" when not disabled', () => { - configureAccordionComponent({disabledItemValues: []}); - expect(triggerElements[0].getAttribute('aria-disabled')).toBe('false'); - expect(triggerElements[1].getAttribute('aria-disabled')).toBe('false'); - expect(triggerElements[2].getAttribute('aria-disabled')).toBe('false'); + it('should set aria-disabled="true" if trigger is disabled', () => { + testComponent.disableItem('item-1', true); + fixture.detectChanges(); + + expect(getTriggerAttribute(0, 'aria-disabled')).toBe('true'); + expect(getTriggerAttribute(1, 'aria-disabled')).toBe('false'); + expect(getTriggerAttribute(2, 'aria-disabled')).toBe('false'); + }); }); - it('should set aria-disabled="true" if trigger is disabled', () => { - configureAccordionComponent({disabledItemValues: ['item-1']}); - expect(triggerElements[0].getAttribute('aria-disabled')).toBe('true'); - expect(triggerElements[1].getAttribute('aria-disabled')).toBe('false'); - expect(triggerElements[2].getAttribute('aria-disabled')).toBe('false'); + describe('AccordionPanel', () => { + it('should have role="region"', () => { + expect(getPanelAttribute(0, 'role')).toBe('region'); + expect(getPanelAttribute(1, 'role')).toBe('region'); + expect(getPanelAttribute(2, 'role')).toBe('region'); + }); + + it('should have aria-labelledby pointing to the trigger id', () => { + expect(getPanelAttribute(0, 'aria-labelledby')).toBe(getTriggerAttribute(0, 'id')); + expect(getPanelAttribute(1, 'aria-labelledby')).toBe(getTriggerAttribute(1, 'id')); + expect(getPanelAttribute(2, 'aria-labelledby')).toBe(getTriggerAttribute(2, 'id')); + }); + + it('should have "inert" attribute when collapsed', () => { + expect(getPanelAttribute(0, 'inert')).toBe('true'); + expect(getPanelAttribute(1, 'inert')).toBe('true'); + expect(getPanelAttribute(2, 'inert')).toBe('true'); + }); }); }); - describe('AccordionPanel', () => { - beforeEach(() => { - configureAccordionComponent(); - }); + describe('Expansion behavior', () => { + describe('single expansion mode (multiExpandable=false)', () => { + it('should expand panel on trigger click and update expanded panels', () => { + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeTrue(); + expect(panelElements[0].getAttribute('inert')).toBe(null); + }); + + it('should collapes panel on trigger click and update expanded panels', () => { + click(triggerElements[0]); + click(triggerElements[0]); // Collapse + expect(isTriggerExpanded(0)).toBeFalse(); + expect(panelElements[0].getAttribute('inert')).toBe('true'); + }); + + it('should expand one and collapse others', () => { + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeTrue(); - it('should have role="region"', () => { - expect(panelElements[0].getAttribute('role')).toBe('region'); - expect(panelElements[1].getAttribute('role')).toBe('region'); - expect(panelElements[2].getAttribute('role')).toBe('region'); + click(triggerElements[1]); + expect(isTriggerExpanded(0)).toBeFalse(); + expect(panelElements[0].getAttribute('inert')).toBe('true'); + expect(isTriggerExpanded(1)).toBeTrue(); + expect(panelElements[1].getAttribute('inert')).toBe(null); + }); + + it('should allow setting initial value', () => { + testComponent.expandItem('item-2', true); + fixture.detectChanges(); + + expect(isTriggerExpanded(0)).toBeFalse(); + expect(isTriggerExpanded(1)).toBeTrue(); + expect(isTriggerExpanded(2)).toBeFalse(); + }); }); - it('should have aria-labelledby pointing to the trigger id', () => { - expect(panelElements[0].getAttribute('aria-labelledby')).toBe(triggerElements[0].id); - expect(panelElements[1].getAttribute('aria-labelledby')).toBe(triggerElements[1].id); - expect(panelElements[2].getAttribute('aria-labelledby')).toBe(triggerElements[2].id); + describe('multiple expansion mode (multiExpandable=true)', () => { + beforeEach(() => { + testComponent.multiExpandable.set(true); + fixture.detectChanges(); + }); + + it('should expand multiple panels', () => { + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeTrue(); + + click(triggerElements[1]); + expect(isTriggerExpanded(0)).toBeTrue(); + expect(isTriggerExpanded(1)).toBeTrue(); + }); + + it('should collapse an item without affecting others', () => { + click(triggerElements[0]); + click(triggerElements[1]); + expect(isTriggerExpanded(0)).toBeTrue(); + expect(isTriggerExpanded(1)).toBeTrue(); + + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); + expect(isTriggerExpanded(1)).toBeTrue(); + }); + + it('should allow setting initial multiple values', () => { + testComponent.expandItem('item-1', true); + testComponent.expandItem('item-3', true); + fixture.detectChanges(); + + expect(isTriggerExpanded(0)).toBeTrue(); + expect(isTriggerExpanded(1)).toBeFalse(); + expect(isTriggerExpanded(2)).toBeTrue(); + }); }); - it('should have "inert" attribute when collapsed', () => { - configureAccordionComponent(); - expect(panelElements[0].hasAttribute('inert')).toBeTrue(); - expect(panelElements[1].hasAttribute('inert')).toBeTrue(); - expect(panelElements[2].hasAttribute('inert')).toBeTrue(); + describe('disabled items and group', () => { + it('should not expand a disabled trigger', () => { + testComponent.disableItem('item-1', true); + fixture.detectChanges(); + + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); + expect(triggerElements[0].getAttribute('aria-disabled')).toBe('true'); + }); + + it('should not expand any trigger if group is disabled', () => { + testComponent.disabledGroup.set(true); + fixture.detectChanges(); + + click(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); + + click(triggerElements[1]); + expect(isTriggerExpanded(1)).toBeFalse(); + }); }); }); - }); - describe('Expansion behavior', () => { - describe('single expansion mode (multiExpandable=false)', () => { + describe('Keyboard navigation and interaction', () => { beforeEach(() => { - configureAccordionComponent({multiExpandable: false}); + // Focus on the first trigger as initial state. + triggerElements[0].focus(); + fixture.detectChanges(); + expect(isTriggerActive(0)).toBeTrue(); }); - it('should expand panel on trigger click and update expanded panels', () => { - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - expect(panelElements[0].hasAttribute('inert')).toBeFalse(); - }); + it('should focus next trigger with ArrowDown', () => { + downArrowKey(); - it('should collapes panel on trigger click and update expanded panels', () => { - click(triggerElements[0]); - click(triggerElements[0]); // Collapse - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - expect(panelElements[0].hasAttribute('inert')).toBeTrue(); + expect(isTriggerActive(0)).toBeFalse(); + expect(isTriggerActive(1)).toBeTrue(); }); - it('should expand one and collapse others', () => { - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); + it('should focus previous trigger with ArrowUp', () => { + downArrowKey(); + expect(isTriggerActive(1)).toBeTrue(); - click(triggerElements[1]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - expect(panelElements[0].hasAttribute('inert')).toBeTrue(); - expect(isTriggerExpanded(triggerElements[1])).toBeTrue(); - expect(panelElements[1].hasAttribute('inert')).toBeFalse(); + upArrowKey(); + expect(isTriggerActive(1)).toBeFalse(); + expect(isTriggerActive(0)).toBeTrue(); }); - it('should allow setting initial value', () => { - configureAccordionComponent({expandedItemValues: ['item-2'], multiExpandable: false}); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - expect(isTriggerExpanded(triggerElements[1])).toBeTrue(); - expect(isTriggerExpanded(triggerElements[2])).toBeFalse(); + it('should focus first trigger with Home when another item is focused', () => { + downArrowKey(); + downArrowKey(); + + expect(isTriggerActive(2)).toBeTrue(); + + homeKey(); + expect(isTriggerActive(0)).toBeTrue(); }); - }); - describe('multiple expansion mode (multiExpandable=true)', () => { - beforeEach(() => { - configureAccordionComponent({multiExpandable: true}); + it('should focus last trigger with End', () => { + endKey(); + + expect(isTriggerActive(2)).toBeTrue(); }); - it('should expand multiple panels', () => { - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); + it('should toggle expansion of focused trigger with Enter', () => { + expect(isTriggerExpanded(0)).toBeFalse(); - click(triggerElements[1]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - expect(isTriggerExpanded(triggerElements[1])).toBeTrue(); + enterKey(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeTrue(); + + enterKey(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); }); - it('should collapse an item without affecting others', () => { - click(triggerElements[0]); - click(triggerElements[1]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - expect(isTriggerExpanded(triggerElements[1])).toBeTrue(); + it('should toggle expansion of focused trigger with Space', () => { + expect(isTriggerExpanded(0)).toBeFalse(); + + spaceKey(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeTrue(); - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - expect(isTriggerExpanded(triggerElements[1])).toBeTrue(); + spaceKey(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); }); - it('should allow setting initial multiple values', () => { - configureAccordionComponent({ - expandedItemValues: ['item-1', 'item-3'], - multiExpandable: true, + describe('with shuffled items', () => { + it('should focus on new last trigger with End', () => { + const items = testComponent.items().reverse(); + testComponent.items.set([...items]); + fixture.detectChanges(); + + // Now reversed, End should move to the former first trigger. + endKey(); + expect(isTriggerActive(0)).toBeTrue(); }); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - expect(isTriggerExpanded(triggerElements[1])).toBeFalse(); - expect(isTriggerExpanded(triggerElements[2])).toBeTrue(); - }); - }); - describe('disabled items and group', () => { - it('should not expand a disabled trigger', () => { - configureAccordionComponent({disabledItemValues: ['item-1']}); - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - expect(triggerElements[0].getAttribute('aria-disabled')).toBe('true'); - }); + it('should focus on newly prepended trigger with Begin', () => { + const items = testComponent.items(); + items.unshift({ + panelId: 'item-0', + header: 'Item 0 Header', + content: 'Item 0 Content', + disabled: signal(false), + expanded: signal(false), + }); + testComponent.items.set([...items]); + setupTriggerAndPanels(); + + homeKey(); + expect(isTriggerActive(0)).toBeTrue(); + expect(getTriggerText(0)).toBe('Item 0 Header'); + }); + + it('should focus on newly appended trigger with End', () => { + const items = testComponent.items(); + items.push({ + panelId: 'item-4', + header: 'Item 4 Header', + content: 'Item 4 Content', + disabled: signal(false), + expanded: signal(false), + }); + testComponent.items.set([...items]); + setupTriggerAndPanels(); + + endKey(); + expect(isTriggerActive(3)).toBeTrue(); + expect(getTriggerText(3)).toBe('Item 4 Header'); + }); - it('should not expand any trigger if group is disabled', () => { - configureAccordionComponent({disabledGroup: true}); - click(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - click(triggerElements[1]); - expect(isTriggerExpanded(triggerElements[1])).toBeFalse(); + it('should focus on inserted trigger with navigation', () => { + const items = testComponent.items(); + items.splice(2, 0, { + panelId: 'item-2a', + header: 'Item 2a Header', + content: 'Item 2a Content', + disabled: signal(false), + expanded: signal(false), + }); + testComponent.items.set([...items]); + setupTriggerAndPanels(); + + downArrowKey(); + downArrowKey(); + expect(isTriggerActive(2)).toBeTrue(); + expect(triggerElements[2].textContent?.trim()).toBe('Item 2a Header'); + }); }); - }); - }); - describe('Keyboard navigation and interaction', () => { - beforeEach(() => { - configureAccordionComponent({}); + describe('wrap behavior', () => { + it('should wrap to first on ArrowDown from last if wrap=true', () => { + testComponent.wrap.set(true); + fixture.detectChanges(); - // Focus on the first trigger as initial state. - triggerElements[0].focus(); - fixture.detectChanges(); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - }); + endKey(); + expect(isTriggerActive(2)).toBeTrue(); - it('should focus next trigger with ArrowDown', () => { - downArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[0])).toBeFalse(); - expect(isTriggerActive(triggerElements[1])).toBeTrue(); - }); + downArrowKey(); + expect(isTriggerActive(0)).toBeTrue(); + }); - it('should focus previous trigger with ArrowUp', () => { - downArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[1])).toBeTrue(); - upArrowKey(triggerElements[1]); - expect(isTriggerActive(triggerElements[1])).toBeFalse(); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - }); + it('should not wrap on ArrowDown from last if wrap=false', () => { + testComponent.wrap.set(false); + fixture.detectChanges(); - it('should focus first trigger with Home when another item is focused', () => { - downArrowKey(triggerElements[0]); - downArrowKey(triggerElements[1]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); - homeKey(triggerElements[2]); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - }); + endKey(); + expect(isTriggerActive(2)).toBeTrue(); - it('should focus last trigger with End', () => { - endKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); - }); + downArrowKey(); + expect(isTriggerActive(2)).toBeTrue(); + }); - it('should toggle expansion of focused trigger with Enter', () => { - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - enterKey(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - enterKey(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - }); + it('should wrap to last on ArrowUp from first if wrap=true', () => { + testComponent.wrap.set(true); + fixture.detectChanges(); - it('should toggle expansion of focused trigger with Space', () => { - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - spaceKey(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeTrue(); - spaceKey(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); - }); + expect(isTriggerActive(0)).toBeTrue(); - describe('wrap behavior', () => { - it('should wrap to first on ArrowDown from last if wrap=true', () => { - configureAccordionComponent({wrap: true}); - endKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); - downArrowKey(triggerElements[2]); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); + upArrowKey(); + expect(isTriggerActive(2)).toBeTrue(); + }); + + it('should not wrap on ArrowUp from first if wrap=false', () => { + testComponent.wrap.set(false); + fixture.detectChanges(); + + expect(isTriggerActive(0)).toBeTrue(); + + upArrowKey(); + expect(isTriggerActive(0)).toBeTrue(); + }); }); - it('should not wrap on ArrowDown from last if wrap=false', () => { - configureAccordionComponent({wrap: false}); - endKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); - downArrowKey(triggerElements[2]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); + describe('softDisabled behavior', () => { + it('should skip disabled items if softDisabled=false', () => { + testComponent.softDisabled.set(false); + testComponent.disableItem('item-2'); + fixture.detectChanges(); + + expect(isTriggerActive(0)).toBeTrue(); + + downArrowKey(); + expect(isTriggerActive(2)).toBeTrue(); + }); + + it('should focus disabled items if softDisabled=true', () => { + testComponent.softDisabled.set(true); + testComponent.disableItem('item-2'); + + expect(isTriggerActive(0)).toBeTrue(); + + downArrowKey(); + expect(isTriggerActive(1)).toBeTrue(); + + enterKey(triggerElements[1]); + expect(isTriggerExpanded(1)).toBeFalse(); + }); }); - it('should wrap to last on ArrowUp from first if wrap=true', () => { - configureAccordionComponent({wrap: true}); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - upArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); + it('should not allow keyboard navigation if group is disabled', () => { + testComponent.disabledGroup.set(true); + fixture.detectChanges(); + + downArrowKey(); + expect(isTriggerActive(1)).toBeFalse(); }); - it('should not wrap on ArrowUp from first if wrap=false', () => { - configureAccordionComponent({wrap: false}); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - upArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); + it('should not allow expansion if group is disabled', () => { + testComponent.disabledGroup.set(true); + fixture.detectChanges(); + + enterKey(triggerElements[0]); + expect(isTriggerExpanded(0)).toBeFalse(); }); }); + }); - describe('softDisabled behavior', () => { - it('should skip disabled items if softDisabled=false', () => { - configureAccordionComponent({softDisabled: false, disabledItemValues: ['item-2']}); + describe('using an if', () => { + let testComponent: AccordionGroupWithIfs; - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - downArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[2])).toBeTrue(); + beforeEach(() => { + fixture = TestBed.createComponent(AccordionGroupWithIfs); + testComponent = fixture.componentInstance as AccordionGroupWithIfs; + groupElement = fixture.nativeElement.querySelector('[ngAccordionGroup]') as HTMLElement; + + setupTriggerAndPanels(); + }); + + describe('Keyboard navigation and interaction', () => { + beforeEach(() => { + // Focus on the first trigger as initial state. + triggerElements[0].focus(); + fixture.detectChanges(); + expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus disabled items if softDisabled=true', () => { - configureAccordionComponent({softDisabled: true, disabledItemValues: ['item-2']}); + it('should focus between first and last triggers when second removed ', () => { + testComponent.includeSecond.set(false); + fixture.detectChanges(); - expect(isTriggerActive(triggerElements[0])).toBeTrue(); - downArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[1])).toBeTrue(); - enterKey(triggerElements[1]); - expect(isTriggerExpanded(triggerElements[1])).toBeFalse(); + downArrowKey(); + expect(isTriggerActive(0)).toBeFalse(); + expect(isTriggerActive(2)).toBeTrue(); + + upArrowKey(); + expect(isTriggerActive(2)).toBeFalse(); + expect(isTriggerActive(0)).toBeTrue(); }); - }); - it('should not allow keyboard navigation if group is disabled', () => { - configureAccordionComponent({disabledGroup: true}); + it('should focus second trigger with Home when first is removed', () => { + triggerElements[2].focus(); + testComponent.includeFirst.set(false); + fixture.detectChanges(); - downArrowKey(triggerElements[0]); - expect(isTriggerActive(triggerElements[1])).toBeFalse(); - }); + homeKey(); + expect(isTriggerActive(0)).toBeTrue(); + }); - it('should not allow expansion if group is disabled', () => { - configureAccordionComponent({disabledGroup: true}); + it('should focus second trigger with End when last is removed', () => { + triggerElements[0].focus(); + testComponent.includeThird.set(false); + fixture.detectChanges(); - enterKey(triggerElements[0]); - expect(isTriggerExpanded(triggerElements[0])).toBeFalse(); + endKey(); + expect(isTriggerActive(1)).toBeTrue(); + }); }); }); }); @@ -384,11 +507,12 @@ describe('AccordionGroup', () => { [wrap]="wrap()" > @for (item of items(); track item.panelId) { -
+
{ imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent], changeDetection: ChangeDetectionStrategy.Eager, }) -class AccordionGroupExample { +class AccordionGroupWithLoop { items = signal([ { panelId: 'item-1', header: 'Item 1 Header', content: 'Item 1 Content', - disabled: false, - expanded: false, + disabled: signal(false), + expanded: signal(false), }, { panelId: 'item-2', header: 'Item 2 Header', content: 'Item 2 Content', - disabled: false, - expanded: false, + disabled: signal(false), + expanded: signal(false), }, { panelId: 'item-3', header: 'Item 3 Header', content: 'Item 3 Content', - disabled: false, - expanded: false, + disabled: signal(false), + expanded: signal(false), }, ]); @@ -436,15 +560,65 @@ class AccordionGroupExample { softDisabled = signal(true); wrap = signal(false); - disableItem(itemValue: string, disabled: boolean) { - this.items.update(items => - items.map(item => (item.panelId === itemValue ? {...item, disabled} : item)), - ); + disableItem(itemValue: string, disabled = true) { + this.items() + .find(item => item.panelId === itemValue) + ?.disabled.set(disabled); } - expandItem(itemValue: string, expanded: boolean) { - this.items.update(items => - items.map(item => (item.panelId === itemValue ? {...item, expanded} : item)), - ); + expandItem(itemValue: string, expanded = true) { + this.items() + .find(item => item.panelId === itemValue) + ?.expanded.set(expanded); } } + +@Component({ + template: ` +
+ @if (includeFirst()) { +
+ +
+ + Item 1 Content + +
+
+ } + @if (includeSecond()) { +
+ +
+ + Item 2 Content + +
+
+ } + @if (includeThird()) { +
+ +
+ + Item 3 Content + +
+
+ } +
+ `, + imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent], + changeDetection: ChangeDetectionStrategy.Eager, +}) +class AccordionGroupWithIfs extends AccordionGroupWithLoop { + includeFirst = signal(true); + includeSecond = signal(true); + includeThird = signal(true); +} diff --git a/src/aria/private/BUILD.bazel b/src/aria/private/BUILD.bazel index f688ab1b20e1..22b2130483dc 100644 --- a/src/aria/private/BUILD.bazel +++ b/src/aria/private/BUILD.bazel @@ -20,5 +20,6 @@ ts_project( "//src/aria/private/tabs", "//src/aria/private/toolbar", "//src/aria/private/tree", + "//src/aria/private/utils", ], ) diff --git a/src/aria/private/public-api.ts b/src/aria/private/public-api.ts index ed8716c7b67b..0b402dd342a6 100644 --- a/src/aria/private/public-api.ts +++ b/src/aria/private/public-api.ts @@ -25,3 +25,4 @@ export * from './grid/row'; export * from './grid/cell'; export * from './grid/widget'; export * from './deferred-content'; +export * from './utils/element'; diff --git a/src/aria/private/utils/BUILD.bazel b/src/aria/private/utils/BUILD.bazel new file mode 100644 index 000000000000..765abd4edc7c --- /dev/null +++ b/src/aria/private/utils/BUILD.bazel @@ -0,0 +1,8 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "utils", + srcs = ["element.ts"], +) diff --git a/src/aria/tree/utils.ts b/src/aria/private/utils/element.ts similarity index 100% rename from src/aria/tree/utils.ts rename to src/aria/private/utils/element.ts diff --git a/src/aria/tabs/tab-list.ts b/src/aria/tabs/tab-list.ts index 9932a6207aec..d9fda7adb7ce 100644 --- a/src/aria/tabs/tab-list.ts +++ b/src/aria/tabs/tab-list.ts @@ -20,8 +20,8 @@ import { OnInit, OnDestroy, } from '@angular/core'; -import {TabListPattern, TabPattern} from '../private'; -import {sortDirectives, TABS} from './utils'; +import {TabListPattern, TabPattern, sortDirectives} from '../private'; +import {TABS} from './tab-tokens'; import type {Tab} from './tab'; /** diff --git a/src/aria/tabs/tab-panel.ts b/src/aria/tabs/tab-panel.ts index 31f594e7443c..13b5513c662f 100644 --- a/src/aria/tabs/tab-panel.ts +++ b/src/aria/tabs/tab-panel.ts @@ -18,7 +18,7 @@ import { OnDestroy, } from '@angular/core'; import {TabPanelPattern, DeferredContentAware} from '../private'; -import {TABS} from './utils'; +import {TABS} from './tab-tokens'; /** * A TabPanel container for the resources of layered content associated with a tab. diff --git a/src/aria/tabs/utils.ts b/src/aria/tabs/tab-tokens.ts similarity index 58% rename from src/aria/tabs/utils.ts rename to src/aria/tabs/tab-tokens.ts index 8a00da60c027..83b3ea089a6e 100644 --- a/src/aria/tabs/utils.ts +++ b/src/aria/tabs/tab-tokens.ts @@ -11,16 +11,3 @@ import type {Tabs} from './tabs'; /** Token used to expose the `Tabs` directive to child directives. */ export const TABS = new InjectionToken('TABS'); - -export interface HasElement { - element: HTMLElement; -} - -/** - * Sort directives by their document order. - */ -export function sortDirectives(a: HasElement, b: HasElement) { - return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 - ? 1 - : -1; -} diff --git a/src/aria/tabs/tab.ts b/src/aria/tabs/tab.ts index fcc6c07a5763..c446cec5a17a 100644 --- a/src/aria/tabs/tab.ts +++ b/src/aria/tabs/tab.ts @@ -18,9 +18,9 @@ import { OnInit, OnDestroy, } from '@angular/core'; -import {TabPattern} from '../private'; +import {TabPattern, HasElement} from '../private'; import {TabList} from './tab-list'; -import {HasElement, TABS} from './utils'; +import {TABS} from './tab-tokens'; /** * A selectable tab in a TabList. diff --git a/src/aria/tabs/tabs.ts b/src/aria/tabs/tabs.ts index 0c8d616f411e..33600a1c38b0 100644 --- a/src/aria/tabs/tabs.ts +++ b/src/aria/tabs/tabs.ts @@ -9,7 +9,7 @@ import {computed, Directive, ElementRef, inject, signal} from '@angular/core'; import {TabList} from './tab-list'; import {TabPanel} from './tab-panel'; -import {TABS} from './utils'; +import {TABS} from './tab-tokens'; import {TabPanelPattern, TabPattern} from '../private'; /** diff --git a/src/aria/toolbar/utils.ts b/src/aria/toolbar/toolbar-tokens.ts similarity index 68% rename from src/aria/toolbar/utils.ts rename to src/aria/toolbar/toolbar-tokens.ts index 558f8508fde7..3e3c083a8cb0 100644 --- a/src/aria/toolbar/utils.ts +++ b/src/aria/toolbar/toolbar-tokens.ts @@ -13,13 +13,3 @@ import type {ToolbarWidgetGroup} from './toolbar-widget-group'; export const TOOLBAR_WIDGET_GROUP = new InjectionToken>( 'TOOLBAR_WIDGET_GROUP', ); - -interface HasElement { - element: HTMLElement; -} - -export function sortDirectives(a: HasElement, b: HasElement) { - return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 - ? 1 - : -1; -} diff --git a/src/aria/toolbar/toolbar-widget-group.ts b/src/aria/toolbar/toolbar-widget-group.ts index 01a76ec848cd..5c73b44f38a6 100644 --- a/src/aria/toolbar/toolbar-widget-group.ts +++ b/src/aria/toolbar/toolbar-widget-group.ts @@ -18,7 +18,7 @@ import { import {ToolbarWidgetPattern, ToolbarWidgetGroupPattern} from '../private'; import {Toolbar} from './toolbar'; import {ToolbarWidget} from './toolbar-widget'; -import {TOOLBAR_WIDGET_GROUP} from './utils'; +import {TOOLBAR_WIDGET_GROUP} from './toolbar-tokens'; /** * A directive that groups toolbar widgets, used for more complex widgets like radio groups diff --git a/src/aria/toolbar/toolbar-widget.ts b/src/aria/toolbar/toolbar-widget.ts index 76d632952607..9a255da51890 100644 --- a/src/aria/toolbar/toolbar-widget.ts +++ b/src/aria/toolbar/toolbar-widget.ts @@ -24,7 +24,7 @@ import { } from '../private'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Toolbar} from './toolbar'; -import {TOOLBAR_WIDGET_GROUP} from './utils'; +import {TOOLBAR_WIDGET_GROUP} from './toolbar-tokens'; import type {ToolbarWidgetGroup} from './toolbar-widget-group'; /** diff --git a/src/aria/toolbar/toolbar.ts b/src/aria/toolbar/toolbar.ts index bbb877b58129..8e2964884397 100644 --- a/src/aria/toolbar/toolbar.ts +++ b/src/aria/toolbar/toolbar.ts @@ -17,10 +17,9 @@ import { signal, model, } from '@angular/core'; -import {ToolbarPattern, ToolbarWidgetPattern} from '../private'; +import {ToolbarPattern, ToolbarWidgetPattern, sortDirectives} from '../private'; import {Directionality} from '@angular/cdk/bidi'; import type {ToolbarWidget} from './toolbar-widget'; -import {sortDirectives} from './utils'; /** * A toolbar widget container for a group of interactive widgets, such as diff --git a/src/aria/tree/tree-item-group.ts b/src/aria/tree/tree-item-group.ts index 01be388d4de1..d20cb200972f 100644 --- a/src/aria/tree/tree-item-group.ts +++ b/src/aria/tree/tree-item-group.ts @@ -16,9 +16,8 @@ import { OnInit, OnDestroy, } from '@angular/core'; -import {TreeItemPattern, DeferredContent} from '../private'; +import {TreeItemPattern, DeferredContent, sortDirectives} from '../private'; import type {TreeItem} from './tree-item'; -import {sortDirectives} from './utils'; /** * Group that contains children tree items. diff --git a/src/aria/tree/tree-item.ts b/src/aria/tree/tree-item.ts index 728ec42701d5..78aadaf1bb1b 100644 --- a/src/aria/tree/tree-item.ts +++ b/src/aria/tree/tree-item.ts @@ -22,10 +22,9 @@ import { afterNextRender, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; -import {ComboboxTreePattern, TreeItemPattern, DeferredContentAware} from '../private'; +import {ComboboxTreePattern, TreeItemPattern, DeferredContentAware, HasElement} from '../private'; import {Tree} from './tree'; import {TreeItemGroup} from './tree-item-group'; -import {HasElement} from './utils'; /** * A selectable and expandable item in an `ngTree`. diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index c50465478b8d..31de2e750c88 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -20,10 +20,9 @@ import { } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; -import {ComboboxTreePattern, TreeItemPattern, TreePattern} from '../private'; +import {ComboboxTreePattern, TreeItemPattern, TreePattern, sortDirectives} from '../private'; import {ComboboxPopup} from '../combobox'; import type {TreeItem} from './tree-item'; -import {sortDirectives} from './utils'; /** * A container that transforms nested lists into an accessible, ARIA-compliant tree structure. diff --git a/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.html b/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.html index 19c3e49bc15f..41b9fcc56bf7 100644 --- a/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.html +++ b/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.html @@ -13,75 +13,26 @@ [softDisabled]="softDisabled.value" [wrap]="wrap.value" > -
-

- -

-
- -

This is the content for Item 1.

- -
-
-
- -
-

- -

-
- -

This is the content for Item 2.

- -
-
-
- -
-

- -

-
- -

This is the content for Item 3.

-
-
-
- -
-

- -

-
- -

This is the content for Item 4

-
-
-
- -
-

- -

-
- -

This is the content for Item 5

-
-
-
+ @for (item of items(); track item.panelId) { +
+

+ +

+
+ +

{{item.content}}

+ +
+
+
+ }
diff --git a/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.ts b/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.ts index ce9e2f7a28f7..d0c2370bd394 100644 --- a/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.ts +++ b/src/components-examples/aria/accordion/accordion-configurable/accordion-configurable-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, signal} from '@angular/core'; import {FormControl, ReactiveFormsModule} from '@angular/forms'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -37,7 +37,41 @@ export class AccordionConfigurableExample { disabled = new FormControl(false, {nonNullable: true}); softDisabled = new FormControl(true, {nonNullable: true}); - expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; - } + items = signal([ + { + panelId: 'item-1', + header: 'Item 1 Trigger', + content: 'This is the content for Item 1.', + disabled: signal(false), + expanded: signal(false), + }, + { + panelId: 'item-2', + header: 'Item 2 Trigger (disabled)', + content: 'This is the content for Item 2.', + disabled: signal(true), + expanded: signal(false), + }, + { + panelId: 'item-3', + header: 'Item 3 Trigger', + content: 'This is the content for Item 3.', + disabled: signal(false), + expanded: signal(false), + }, + { + panelId: 'item-4', + header: 'Item 4 Trigger', + content: 'This is the content for Item 4.', + disabled: signal(false), + expanded: signal(false), + }, + { + panelId: 'item-5', + header: 'Item 5 Trigger', + content: 'This is the content for Item 5.', + disabled: signal(false), + expanded: signal(false), + }, + ]); } diff --git a/src/components-examples/aria/accordion/accordion-disabled-focusable/accordion-disabled-focusable-example.ts b/src/components-examples/aria/accordion/accordion-disabled-focusable/accordion-disabled-focusable-example.ts index cc98f4b90df4..17cdb83c7b0f 100644 --- a/src/components-examples/aria/accordion/accordion-disabled-focusable/accordion-disabled-focusable-example.ts +++ b/src/components-examples/aria/accordion/accordion-disabled-focusable/accordion-disabled-focusable-example.ts @@ -16,6 +16,6 @@ import { }) export class AccordionDisabledFocusableExample { expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; + return panel.visible() ? 'expand_less' : 'expand_more'; } } diff --git a/src/components-examples/aria/accordion/accordion-disabled-skipped/accordion-disabled-skipped-example.ts b/src/components-examples/aria/accordion/accordion-disabled-skipped/accordion-disabled-skipped-example.ts index 0dbce2e86b28..976d32e46b4a 100644 --- a/src/components-examples/aria/accordion/accordion-disabled-skipped/accordion-disabled-skipped-example.ts +++ b/src/components-examples/aria/accordion/accordion-disabled-skipped/accordion-disabled-skipped-example.ts @@ -16,6 +16,6 @@ import { }) export class AccordionDisabledSkippedExample { expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; + return panel.visible() ? 'expand_less' : 'expand_more'; } } diff --git a/src/components-examples/aria/accordion/accordion-disabled/accordion-disabled-example.ts b/src/components-examples/aria/accordion/accordion-disabled/accordion-disabled-example.ts index 5d5993367fd7..3862a1fe110f 100644 --- a/src/components-examples/aria/accordion/accordion-disabled/accordion-disabled-example.ts +++ b/src/components-examples/aria/accordion/accordion-disabled/accordion-disabled-example.ts @@ -16,6 +16,6 @@ import { }) export class AccordionDisabledExample { expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; + return panel.visible() ? 'expand_less' : 'expand_more'; } } diff --git a/src/components-examples/aria/accordion/accordion-multi-expansion/accordion-multi-expansion-example.ts b/src/components-examples/aria/accordion/accordion-multi-expansion/accordion-multi-expansion-example.ts index 2eeb5166e014..24e49f64d5de 100644 --- a/src/components-examples/aria/accordion/accordion-multi-expansion/accordion-multi-expansion-example.ts +++ b/src/components-examples/aria/accordion/accordion-multi-expansion/accordion-multi-expansion-example.ts @@ -16,6 +16,6 @@ import { }) export class AccordionMultiExpansionExample { expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; + return panel.visible() ? 'expand_less' : 'expand_more'; } } diff --git a/src/components-examples/aria/accordion/accordion-single-expansion/accordion-single-expansion-example.ts b/src/components-examples/aria/accordion/accordion-single-expansion/accordion-single-expansion-example.ts index 865ed34b61f1..17cb542ec90d 100644 --- a/src/components-examples/aria/accordion/accordion-single-expansion/accordion-single-expansion-example.ts +++ b/src/components-examples/aria/accordion/accordion-single-expansion/accordion-single-expansion-example.ts @@ -16,6 +16,6 @@ import { }) export class AccordionSingleExpansionExample { expansionIcon(panel: AccordionPanel): string { - return panel ? 'expand_less' : 'expand_more'; + return panel.visible() ? 'expand_less' : 'expand_more'; } }