Skip to content

Commit abbd8d4

Browse files
committed
refactor(cdk-experimental/accordion): communicates trigger state more accurately
Updates previous changes to implement disabled and expanded states of the accordion trigger button to the visually hidden label to ensure the exact state of the button is communicated to the screen reader. Also updates previous change to pass the visuallyHiddenId to the AccordionTriggerPattern as recommended by Cheng. These updates should improve id consistency as well as an improved label which fully describes the accordion trigger state to screen readers.
1 parent fa572e5 commit abbd8d4

File tree

2 files changed

+50
-26
lines changed

2 files changed

+50
-26
lines changed

src/cdk-experimental/accordion/accordion.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ export class CdkAccordionTrigger {
106106
/** A global unique identifier for the trigger. */
107107
private readonly _id = inject(_IdGenerator).getId('cdk-accordion-trigger-');
108108

109+
/** A computed signal to generate a consistent ID for the visually hidden label. */
110+
private readonly _visuallyHiddenId = inject(_IdGenerator).getId('cdk-accordion-label-');
111+
109112
/** A reference to the trigger element. */
110113
private readonly _elementRef = inject(ElementRef);
111114

112115
private readonly _renderer = inject(Renderer2);
113116

114-
/** A computed signal to generate a consistent ID for the visually hidden label. */
115-
readonly visuallyHiddenId = computed(() => this.pattern.id() + '-label');
116-
117117
/** The parent CdkAccordionGroup. */
118118
private readonly _accordionGroup = inject(CdkAccordionGroup);
119119

@@ -136,43 +136,65 @@ export class CdkAccordionTrigger {
136136
/** The UI pattern instance for this trigger. */
137137
readonly pattern: AccordionTriggerPattern = new AccordionTriggerPattern({
138138
id: () => this._id,
139+
visuallyHiddenId: () => this._visuallyHiddenId,
139140
value: this.value,
140141
disabled: this.disabled,
141142
element: () => this._elementRef.nativeElement,
142143
accordionGroup: computed(() => this._accordionGroup.pattern),
143144
accordionPanel: this.accordionPanel,
144145
});
145146

146-
// Creating the visuallyHiddenSpan as an accessible reference for the accordion content
147+
/** The computed label value of this Accordion Trigger to be passed to a visually hidden
148+
* span that is accessible to screen readers whether the button is disabled or not.
149+
*/
150+
readonly visuallyHiddenLabel = computed(() => {
151+
let buttonText = '';
152+
const buttonElement = this._elementRef.nativeElement;
153+
for (const node of Array.from(buttonElement.childNodes)) {
154+
if (node instanceof Node && node.nodeType === Node.TEXT_NODE) {
155+
buttonText += (node as Text).textContent?.trim() + ' ';
156+
}
157+
}
158+
159+
// Determine the state labels of the Accordion Trigger to pass to the label
160+
const expansionLabel = this.pattern.expanded() ? '(Expanded)' : '(Collapsed)';
161+
const disabledLabel = this.pattern.disabled() ? '(Disabled)' : '';
162+
163+
// Combine all parts into the final label
164+
return `${buttonText.trim()} ${expansionLabel} ${disabledLabel}`.trim();
165+
});
166+
147167
constructor() {
148-
// We'll use afterRenderEffect to ensure the element is created after the host element.
149168
afterRenderEffect(() => {
150-
// Find the button element and its parent
151169
const buttonElement = this._elementRef.nativeElement;
152170
const parentElement = this._renderer.parentNode(buttonElement);
153171

154172
if (parentElement) {
155-
// Create a new visually hidden span element to be referenced by accordionPanel
156-
const visuallyHiddenSpan = this._renderer.createElement('span');
157-
this._renderer.addClass(visuallyHiddenSpan, 'cdk-visually-hidden');
158-
this._renderer.setAttribute(visuallyHiddenSpan, 'id', this.pattern.visuallyHiddenId());
159-
this._renderer.setAttribute(visuallyHiddenSpan, 'tabindex', '-1');
160-
161-
// Get the button's text content and set it on the span
162-
let buttonText = '';
163-
for (const node of Array.from(buttonElement.childNodes)) {
164-
if (node instanceof Node && node.nodeType === Node.TEXT_NODE) {
165-
buttonText += node.textContent?.trim() + ' ';
166-
}
173+
// Create the span and attach it to the DOM only once.
174+
if (!this._visuallyHiddenSpan) {
175+
this._visuallyHiddenSpan = this._renderer.createElement('span');
176+
this._renderer.addClass(this._visuallyHiddenSpan, 'cdk-visually-hidden');
177+
this._renderer.setAttribute(
178+
this._visuallyHiddenSpan,
179+
'id',
180+
this.pattern.visuallyHiddenId(),
181+
);
182+
this._renderer.setAttribute(this._visuallyHiddenSpan, 'tabindex', '-1');
183+
this._renderer.insertBefore(parentElement, this._visuallyHiddenSpan, buttonElement);
167184
}
168-
const textNode = this._renderer.createText(buttonText);
169-
this._renderer.appendChild(visuallyHiddenSpan, textNode);
170185

171-
// Insert the visually hidden span before the button, as its sibling
172-
this._renderer.insertBefore(parentElement, visuallyHiddenSpan, buttonElement);
186+
// Update its text content whenever the signal changes.
187+
this._renderer.setProperty(
188+
this._visuallyHiddenSpan,
189+
'textContent',
190+
this.visuallyHiddenLabel(),
191+
);
173192
}
174193
});
175194
}
195+
196+
// Add a private property to store a reference to the span
197+
private _visuallyHiddenSpan!: HTMLElement;
176198
}
177199

178200
/**

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,16 @@ export type AccordionTriggerInputs = Omit<ListNavigationItem & ListFocusItem, 'i
8080

8181
/** The accordion panel controlled by this trigger. */
8282
accordionPanel: SignalLike<AccordionPanelPattern | undefined>;
83+
84+
/** The id of the visually hidden span associated with the Accordion Trigger to be referenced by
85+
* screen readers at all times for consistent accessibility.
86+
*/
87+
visuallyHiddenId: SignalLike<string>;
8388
};
8489

8590
export interface AccordionTriggerPattern extends AccordionTriggerInputs {}
8691
/** A pattern controls the expansion state of an accordion. */
8792
export class AccordionTriggerPattern {
88-
/** A unique ID for the visually hidden label. */
89-
readonly visuallyHiddenId: SignalLike<string>;
90-
9193
/** Whether this tab has expandable content. */
9294
expandable: SignalLike<boolean>;
9395

@@ -121,7 +123,7 @@ export class AccordionTriggerPattern {
121123
this.value = inputs.value;
122124
this.accordionGroup = inputs.accordionGroup;
123125
this.accordionPanel = inputs.accordionPanel;
124-
this.visuallyHiddenId = computed(() => this.id() + '-label');
126+
this.visuallyHiddenId = inputs.visuallyHiddenId;
125127
this.expansionControl = new ExpansionControl({
126128
...inputs,
127129
expansionId: inputs.value,

0 commit comments

Comments
 (0)