pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/angular/components/commit/33593e781b364a20efafa8436501ddd5b47dd5df

rossorigen="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-9c8f61f9f58ad7b2.css" /> test(multiple): check for incorrect usage of Angular Aria directives … · angular/components@33593e7 · GitHub
Skip to content

Commit 33593e7

Browse files
authored
test(multiple): check for incorrect usage of Angular Aria directives and log violations (#33195)
* test(aria/accordion): check for incorrect usage of Accordion directives and log violations * test(aria/grid): check for incorrect usage of Grid directives and log violations * test(aria/listbox): check for incorrect usage of Listbox directives and log violations * test(aria/menu): check for incorrect usage of Menu directives and log violations * test(aria/tabs): check for incorrect usage of Tabs directives and log violations * test(aria/toolbar): check for incorrect usage of Toolbar directives and log violations * test(aria/tree): check for incorrect usage of Tree directives and log violations * test(multiple): Add reportViolations method for aria directives to log element and violations
1 parent 930fe9b commit 33593e7

33 files changed

Lines changed: 916 additions & 37 deletions

goldens/aria/accordion/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,14 @@ export class AccordionPanel {
5050
toggle(): void;
5151
readonly visible: _angular_core.Signal<boolean>;
5252
// (undocumented)
53-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionPanel, "[ngAccordionPanel]", ["ngAccordionPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
53+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionPanel, "[ngAccordionPanel]", ["ngAccordionPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, ["_accordionContent"], never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
5454
// (undocumented)
5555
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionPanel, never>;
5656
}
5757

5858
// @public
5959
export class AccordionTrigger implements OnInit, OnDestroy {
60+
constructor();
6061
readonly active: _angular_core.Signal<boolean>;
6162
collapse(): void;
6263
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;

goldens/aria/private/index.api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class AccordionGroupPattern {
3232
onKeydown(event: KeyboardEvent): void;
3333
readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
3434
toggle(): void;
35+
validate(): string[];
3536
}
3637

3738
// @public
@@ -271,6 +272,7 @@ export class GridPattern {
271272
restoreFocusEffect(): void;
272273
setDefaultStateEffect(): void;
273274
readonly tabIndex: SignalLike<0 | -1>;
275+
validate(): string[];
274276
}
275277

276278
// @public
@@ -461,6 +463,7 @@ export class MenuPattern<V> {
461463
readonly tabIndex: () => 0 | -1;
462464
trigger(): void;
463465
readonly typeaheadRegexp: RegExp;
466+
validate(): string[];
464467
readonly visible: SignalLike<boolean>;
465468
}
466469

@@ -520,6 +523,9 @@ export class OptionPattern<V> {
520523
readonly value: SignalLike<V>;
521524
}
522525

526+
// @public
527+
export function reportViolations(violations: string[], element: Element): void;
528+
523529
// @public
524530
export function resolveElement<T = HTMLElement>(resolver: ElementResolver<T>, context: HTMLElement): T | undefined;
525531

@@ -652,6 +658,7 @@ export class ToolbarPattern<V> {
652658
setDefaultStateEffect(): void;
653659
readonly softDisabled: SignalLike<boolean>;
654660
readonly tabIndex: SignalLike<0 | -1>;
661+
validate(): string[];
655662
}
656663

657664
// @public

goldens/aria/tabs/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { WritableSignal } from '@angular/core';
1313

1414
// @public
1515
export class Tab implements HasElement, OnInit, OnDestroy {
16+
constructor();
1617
readonly active: _angular_core.Signal<boolean>;
1718
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1819
readonly element: HTMLElement;
@@ -81,7 +82,7 @@ export class TabPanel implements OnInit, OnDestroy {
8182
readonly value: _angular_core.InputSignal<string>;
8283
readonly visible: _angular_core.Signal<boolean>;
8384
// (undocumented)
84-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TabPanel, "[ngTabPanel]", ["ngTabPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
85+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TabPanel, "[ngTabPanel]", ["ngTabPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; }, {}, ["_tabContent"], never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
8586
// (undocumented)
8687
static ɵfac: _angular_core.ɵɵFactoryDeclaration<TabPanel, never>;
8788
}

goldens/aria/toolbar/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class ToolbarWidget<V> implements OnInit, OnDestroy {
5555

5656
// @public
5757
export class ToolbarWidgetGroup<V> {
58+
constructor();
5859
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
5960
readonly element: HTMLElement;
6061
readonly multi: _angular_core.InputSignalWithTransform<boolean, unknown>;

src/aria/accordion/accordion-group.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import {
1515
input,
1616
signal,
1717
afterNextRender,
18+
afterRenderEffect,
1819
OnDestroy,
1920
} from '@angular/core';
2021
import {Directionality} from '@angular/cdk/bidi';
21-
import {AccordionGroupPattern, SortedCollection} from '../private';
22+
import {AccordionGroupPattern, SortedCollection, reportViolations} from '../private';
2223
import {ACCORDION_GROUP} from './accordion-tokens';
2324
import {AccordionTrigger} from './accordion-trigger';
2425

@@ -113,6 +114,15 @@ export class AccordionGroup implements OnDestroy {
113114
afterNextRender(() => {
114115
this._collection.startObserving(this.element);
115116
});
117+
118+
// Check for any violations after the DOM has been updated.
119+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
120+
afterRenderEffect({
121+
read: () => {
122+
reportViolations(this._pattern.validate(), this.element);
123+
},
124+
});
125+
}
116126
}
117127

118128
ngOnDestroy() {

src/aria/accordion/accordion-panel.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Directive, ElementRef, afterRenderEffect, computed, inject, input} from '@angular/core';
9+
import {
10+
Directive,
11+
ElementRef,
12+
afterRenderEffect,
13+
computed,
14+
contentChild,
15+
inject,
16+
input,
17+
} from '@angular/core';
1018
import {_IdGenerator} from '@angular/cdk/a11y';
11-
import {DeferredContentAware, AccordionTriggerPattern} from '../private';
19+
import {DeferredContentAware, AccordionTriggerPattern, reportViolations} from '../private';
20+
import {AccordionContent} from './accordion-content';
1221

1322
/**
1423
* The content panel of an accordion item that is conditionally visible.
@@ -56,6 +65,8 @@ export class AccordionPanel {
5665
/** The DeferredContentAware host directive. */
5766
private readonly _deferredContentAware = inject(DeferredContentAware);
5867

68+
private readonly _accordionContent = contentChild(AccordionContent);
69+
5970
/** A global unique identifier for the panel. */
6071
readonly id = input(inject(_IdGenerator).getId('ng-accordion-panel-', true));
6172

@@ -76,6 +87,24 @@ export class AccordionPanel {
7687
this._deferredContentAware.contentVisible.set(this.visible());
7788
},
7889
});
90+
91+
// Check for any violations after the DOM has been updated.
92+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
93+
afterRenderEffect({
94+
read: () => {
95+
const violations: string[] = [];
96+
97+
if (!this._accordionContent()) {
98+
violations.push('ngAccordionPanel must have an ngAccordionContent to render.');
99+
}
100+
if (!this._pattern) {
101+
violations.push('ngAccordionPanel must have an ngAccordionTrigger to control it.');
102+
}
103+
104+
reportViolations(violations, this.element);
105+
},
106+
});
107+
}
79108
}
80109

81110
/** Expands this item. */

src/aria/accordion/accordion-trigger.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {
1616
inject,
1717
input,
1818
model,
19+
afterRenderEffect,
1920
} from '@angular/core';
2021
import {_IdGenerator} from '@angular/cdk/a11y';
21-
import {AccordionTriggerPattern} from '../private';
22+
import {AccordionTriggerPattern, reportViolations} from '../private';
2223
import {ACCORDION_GROUP} from './accordion-tokens';
2324
import {AccordionPanel} from './accordion-panel';
2425

@@ -84,6 +85,30 @@ export class AccordionTrigger implements OnInit, OnDestroy {
8485
/** The UI pattern instance for this trigger. */
8586
_pattern!: AccordionTriggerPattern;
8687

88+
constructor() {
89+
// Check for any violations after the DOM has been updated.
90+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
91+
afterRenderEffect({
92+
read: () => {
93+
const violations: string[] = [];
94+
95+
if (this.panel() && this.panel().element.contains(this.element)) {
96+
violations.push(
97+
'ngAccordionTrigger must not be nested inside its controlled ngAccordionPanel, otherwise it will become unreachable when collapsed.',
98+
);
99+
}
100+
if (this.panel() && (this.panel() as any)._pattern !== this._pattern) {
101+
violations.push(
102+
'ngAccordionPanel is already controlled by another ngAccordionTrigger.',
103+
);
104+
}
105+
106+
reportViolations(violations, this.element);
107+
},
108+
});
109+
}
110+
}
111+
87112
ngOnInit() {
88113
this._pattern = new AccordionTriggerPattern({
89114
...this,

src/aria/accordion/accordion.spec.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,89 @@ describe('AccordionGroup', () => {
480480
});
481481
});
482482
});
483+
484+
describe('structural validations', () => {
485+
let consoleSpy: jasmine.Spy;
486+
487+
beforeEach(() => {
488+
consoleSpy = spyOn(console, 'warn');
489+
});
490+
491+
afterEach(() => {
492+
TestBed.resetTestingModule();
493+
TestBed.configureTestingModule({
494+
imports: [AccordionGroupWithLoop],
495+
providers: [provideFakeDirectionality('ltr'), _IdGenerator],
496+
});
497+
fixture = TestBed.createComponent(AccordionGroupWithLoop);
498+
setupAccordionGroup();
499+
});
500+
501+
it('should warn when multiple triggers control the same panel', () => {
502+
TestBed.resetTestingModule();
503+
TestBed.configureTestingModule({
504+
imports: [AccordionWithDuplicateTriggers],
505+
});
506+
const duplicateFixture = TestBed.createComponent(AccordionWithDuplicateTriggers);
507+
duplicateFixture.detectChanges();
508+
509+
expect(consoleSpy).toHaveBeenCalledWith(
510+
'ngAccordionPanel is already controlled by another ngAccordionTrigger.',
511+
);
512+
});
513+
514+
it('should warn when trigger is nested inside its controlled panel', () => {
515+
TestBed.resetTestingModule();
516+
TestBed.configureTestingModule({
517+
imports: [AccordionWithNestedTrigger],
518+
});
519+
const nestedFixture = TestBed.createComponent(AccordionWithNestedTrigger);
520+
nestedFixture.detectChanges();
521+
522+
expect(consoleSpy).toHaveBeenCalledWith(
523+
'ngAccordionTrigger must not be nested inside its controlled ngAccordionPanel, otherwise it will become unreachable when collapsed.',
524+
);
525+
});
526+
527+
it('should warn when ngAccordionPanel is missing ngAccordionContent', () => {
528+
TestBed.resetTestingModule();
529+
TestBed.configureTestingModule({
530+
imports: [AccordionPanelWithoutContent],
531+
});
532+
const noContentFixture = TestBed.createComponent(AccordionPanelWithoutContent);
533+
noContentFixture.detectChanges();
534+
535+
expect(consoleSpy).toHaveBeenCalledWith(
536+
'ngAccordionPanel must have an ngAccordionContent to render.',
537+
);
538+
});
539+
540+
it('should warn when ngAccordionPanel is missing controlling trigger', () => {
541+
TestBed.resetTestingModule();
542+
TestBed.configureTestingModule({
543+
imports: [AccordionPanelWithoutTrigger],
544+
});
545+
const noTriggerFixture = TestBed.createComponent(AccordionPanelWithoutTrigger);
546+
noTriggerFixture.detectChanges();
547+
548+
expect(consoleSpy).toHaveBeenCalledWith(
549+
'ngAccordionPanel must have an ngAccordionTrigger to control it.',
550+
);
551+
});
552+
553+
it('should warn when multiple items are expanded in single-expand mode', () => {
554+
TestBed.resetTestingModule();
555+
TestBed.configureTestingModule({
556+
imports: [AccordionWithMultipleExpandedItems],
557+
});
558+
const multipleExpandedFixture = TestBed.createComponent(AccordionWithMultipleExpandedItems);
559+
multipleExpandedFixture.detectChanges();
560+
561+
expect(consoleSpy).toHaveBeenCalledWith(
562+
'ngAccordionGroup has multiExpandable set to false, but multiple ngAccordionTrigger panels are initially expanded.',
563+
);
564+
});
565+
});
483566
});
484567

485568
@Component({
@@ -606,3 +689,81 @@ class AccordionGroupWithIfs extends AccordionGroupWithLoop {
606689
includeSecond = signal(true);
607690
includeThird = signal(true);
608691
}
692+
693+
@Component({
694+
template: `
695+
<div ngAccordionGroup>
696+
<button ngAccordionTrigger [panel]="panel1">Trigger 1</button>
697+
<button ngAccordionTrigger [panel]="panel1">Trigger 2</button>
698+
<div ngAccordionPanel #panel1="ngAccordionPanel">
699+
<ng-template ngAccordionContent>Content</ng-template>
700+
</div>
701+
</div>
702+
`,
703+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
704+
changeDetection: ChangeDetectionStrategy.Eager,
705+
})
706+
class AccordionWithDuplicateTriggers {}
707+
708+
@Component({
709+
template: `
710+
<div ngAccordionGroup>
711+
<div ngAccordionPanel #panel1="ngAccordionPanel">
712+
<button ngAccordionTrigger [panel]="panel1">Nested Trigger</button>
713+
<ng-template ngAccordionContent>Content</ng-template>
714+
</div>
715+
</div>
716+
`,
717+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
718+
changeDetection: ChangeDetectionStrategy.Eager,
719+
})
720+
class AccordionWithNestedTrigger {}
721+
722+
@Component({
723+
template: `
724+
<div ngAccordionGroup>
725+
<button ngAccordionTrigger [panel]="panel1">Trigger</button>
726+
<div ngAccordionPanel #panel1="ngAccordionPanel">
727+
Content
728+
</div>
729+
</div>
730+
`,
731+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel],
732+
changeDetection: ChangeDetectionStrategy.Eager,
733+
})
734+
class AccordionPanelWithoutContent {}
735+
736+
@Component({
737+
template: `
738+
<div ngAccordionGroup>
739+
<div ngAccordionPanel>
740+
<ng-template ngAccordionContent>Content</ng-template>
741+
</div>
742+
</div>
743+
`,
744+
imports: [AccordionGroup, AccordionPanel, AccordionContent],
745+
changeDetection: ChangeDetectionStrategy.Eager,
746+
})
747+
class AccordionPanelWithoutTrigger {}
748+
749+
@Component({
750+
template: `
751+
<div ngAccordionGroup [multiExpandable]="false">
752+
<div>
753+
<button ngAccordionTrigger [panel]="panel1" [expanded]="true">Trigger 1</button>
754+
<div ngAccordionPanel #panel1="ngAccordionPanel">
755+
<ng-template ngAccordionContent>Content 1</ng-template>
756+
</div>
757+
</div>
758+
<div>
759+
<button ngAccordionTrigger [panel]="panel2" [expanded]="true">Trigger 2</button>
760+
<div ngAccordionPanel #panel2="ngAccordionPanel">
761+
<ng-template ngAccordionContent>Content 2</ng-template>
762+
</div>
763+
</div>
764+
</div>
765+
`,
766+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
767+
changeDetection: ChangeDetectionStrategy.Eager,
768+
})
769+
class AccordionWithMultipleExpandedItems {}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy