Skip to content

Commit ed39bb0

Browse files
committed
fix(cdk-experimental/accordion): creates visually hidden span to improve screen reader accessibility
Adds a visually hidden span as a child of the accordion header/button which has a generated id for the associated accordion content panel to reference if/when the accordion header/button is disabled. This should allow screen readers to have an active reference for aria-labelledby to reference. Fixes b/438312273
1 parent ca1cd86 commit ed39bb0

File tree

2 files changed

+37
-1
lines changed

2 files changed

+37
-1
lines changed

src/cdk-experimental/accordion/accordion.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
booleanAttribute,
1919
computed,
2020
WritableSignal,
21+
Renderer2,
2122
} from '@angular/core';
2223
import {_IdGenerator} from '@angular/cdk/a11y';
2324
import {Directionality} from '@angular/cdk/bidi';
@@ -45,7 +46,7 @@ import {
4546
'class': 'cdk-accordion-panel',
4647
'role': 'region',
4748
'[attr.id]': 'pattern.id()',
48-
'[attr.aria-labelledby]': 'pattern.accordionTrigger()?.id()',
49+
'[attr.aria-labelledby]': 'pattern.accordionTrigger()?.visuallyHiddenId()',
4950
'[attr.inert]': 'pattern.hidden() ? true : null',
5051
},
5152
})
@@ -108,6 +109,11 @@ export class CdkAccordionTrigger {
108109
/** A reference to the trigger element. */
109110
private readonly _elementRef = inject(ElementRef);
110111

112+
private readonly _renderer = inject(Renderer2);
113+
114+
/** A computed signal to generate a consistent ID for the visually hidden label. */
115+
readonly visuallyHiddenId = computed(() => this.pattern.id() + '-label');
116+
111117
/** The parent CdkAccordionGroup. */
112118
private readonly _accordionGroup = inject(CdkAccordionGroup);
113119

@@ -136,6 +142,32 @@ export class CdkAccordionTrigger {
136142
accordionGroup: computed(() => this._accordionGroup.pattern),
137143
accordionPanel: this.accordionPanel,
138144
});
145+
146+
// Creating the visuallyHiddenSpan as an accessible reference for the accordion content
147+
constructor() {
148+
// We'll use afterRenderEffect to ensure the element is created after the host element.
149+
afterRenderEffect(() => {
150+
// Find the element that holds the label text, or the button itself.
151+
const buttonElement = this._elementRef.nativeElement;
152+
153+
// Create a new span element
154+
const visuallyHiddenSpan = this._renderer.createElement('span');
155+
156+
// Add the cdk-visually-hidden class
157+
this._renderer.addClass(visuallyHiddenSpan, 'cdk-visually-hidden');
158+
159+
// Set the ID for aria-labelledby
160+
this._renderer.setAttribute(visuallyHiddenSpan, 'id', this.visuallyHiddenId());
161+
162+
// Get the button's text content and set it on the span
163+
const buttonText = buttonElement.textContent?.trim() || '';
164+
const textNode = this._renderer.createText(buttonText);
165+
this._renderer.appendChild(visuallyHiddenSpan, textNode);
166+
167+
// Prepend the visually hidden span to the button element
168+
this._renderer.insertBefore(buttonElement, visuallyHiddenSpan, buttonElement.firstChild);
169+
});
170+
}
139171
}
140172

141173
/**

src/cdk-experimental/ui-patterns/accordion/accordion.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export type AccordionTriggerInputs = Omit<ListNavigationItem & ListFocusItem, 'i
8585
export interface AccordionTriggerPattern extends AccordionTriggerInputs {}
8686
/** A pattern controls the expansion state of an accordion. */
8787
export class AccordionTriggerPattern {
88+
/** A unique ID for the visually hidden label. */
89+
readonly visuallyHiddenId: SignalLike<string>;
90+
8891
/** Whether this tab has expandable content. */
8992
expandable: SignalLike<boolean>;
9093

@@ -118,6 +121,7 @@ export class AccordionTriggerPattern {
118121
this.value = inputs.value;
119122
this.accordionGroup = inputs.accordionGroup;
120123
this.accordionPanel = inputs.accordionPanel;
124+
this.visuallyHiddenId = computed(() => this.id() + '-label');
121125
this.expansionControl = new ExpansionControl({
122126
...inputs,
123127
expansionId: inputs.value,

0 commit comments

Comments
 (0)