Skip to content

Commit 18c644a

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: add aria-labels for estimated savings
This CL adds information about estimated savings to the aria-label for the insight for screen readers to hear when they tab onto the insight. I also took the chance to update the "Ask AI" label as "Ask AI" to a screen reader is unintuitive given you might not have remembered which insight you have expanded. Fixed: 408299691 Change-Id: I362eebd4b12c8accbdaf2b7d940f2fc57779106f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6433947 Reviewed-by: Alina Varkki <alinavarkki@chromium.org> Commit-Queue: Alina Varkki <alinavarkki@chromium.org> Auto-Submit: Jack Franklin <jacktfranklin@chromium.org>
1 parent 74ec898 commit 18c644a

File tree

2 files changed

+174
-3
lines changed

2 files changed

+174
-3
lines changed

front_end/panels/timeline/components/insights/BaseInsightComponent.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,110 @@ describeWithEnvironment('BaseInsightComponent', () => {
107107
});
108108
});
109109

110+
describe('estimated savings output', () => {
111+
let testComponentIndex = 0; // used for defining the custom element and making it unique
112+
function makeTestComponent(opts: {byteSavings?: number, timeSavings?: number}) {
113+
class TestInsight extends BaseInsightComponent<Trace.Insights.Types.InsightModel> {
114+
override internalName = 'test-insight';
115+
override createOverlays(): TimelineOverlay[] {
116+
return [];
117+
}
118+
119+
override getEstimatedSavingsTime(): Trace.Types.Timing.Milli|null {
120+
return opts.timeSavings ? Trace.Types.Timing.Milli(opts.timeSavings) : null;
121+
}
122+
123+
override getEstimatedSavingsBytes(): number|null {
124+
return opts.byteSavings ?? null;
125+
}
126+
127+
override renderContent(): Lit.LitTemplate {
128+
return html`<div>test content</div>`;
129+
}
130+
}
131+
customElements.define(`test-insight-est-savings-${testComponentIndex++}`, TestInsight);
132+
return new TestInsight();
133+
}
134+
135+
it('outputs the correct estimated savings for both bytes and time', async () => {
136+
const component = makeTestComponent({byteSavings: 5_000, timeSavings: 50});
137+
component.model = {
138+
insightKey: 'LCPPhases',
139+
strings: {},
140+
title: 'LCP by Phase' as Common.UIString.LocalizedString,
141+
description: 'some description' as Common.UIString.LocalizedString,
142+
category: Trace.Insights.Types.InsightCategory.ALL,
143+
state: 'fail',
144+
frameId: '123',
145+
};
146+
renderElementIntoDOM(component);
147+
148+
await RenderCoordinator.done();
149+
const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]');
150+
assert.isOk(estSavings);
151+
assert.strictEqual(estSavings.innerText, 'Est savings: 50 ms & 5.0 kB');
152+
});
153+
154+
it('outputs the correct estimated savings for bytes only', async () => {
155+
const component = makeTestComponent({byteSavings: 5_000});
156+
component.model = {
157+
insightKey: 'LCPPhases',
158+
strings: {},
159+
title: 'LCP by Phase' as Common.UIString.LocalizedString,
160+
description: 'some description' as Common.UIString.LocalizedString,
161+
category: Trace.Insights.Types.InsightCategory.ALL,
162+
state: 'fail',
163+
frameId: '123',
164+
};
165+
renderElementIntoDOM(component);
166+
167+
await RenderCoordinator.done();
168+
const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]');
169+
assert.isOk(estSavings);
170+
assert.strictEqual(estSavings.innerText, 'Est savings: 5.0 kB');
171+
});
172+
173+
it('outputs the correct estimated savings for time only', async () => {
174+
const component = makeTestComponent({timeSavings: 50});
175+
component.model = {
176+
insightKey: 'LCPPhases',
177+
strings: {},
178+
title: 'LCP by Phase' as Common.UIString.LocalizedString,
179+
description: 'some description' as Common.UIString.LocalizedString,
180+
category: Trace.Insights.Types.InsightCategory.ALL,
181+
state: 'fail',
182+
frameId: '123',
183+
};
184+
renderElementIntoDOM(component);
185+
186+
await RenderCoordinator.done();
187+
const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]');
188+
assert.isOk(estSavings);
189+
assert.strictEqual(estSavings.innerText, 'Est savings: 50 ms');
190+
});
191+
192+
it('includes the output in the insight aria label', async () => {
193+
const component = makeTestComponent({byteSavings: 5_000, timeSavings: 50});
194+
component.model = {
195+
insightKey: 'LCPPhases',
196+
strings: {},
197+
title: 'LCP by Phase' as Common.UIString.LocalizedString,
198+
description: 'some description' as Common.UIString.LocalizedString,
199+
category: Trace.Insights.Types.InsightCategory.ALL,
200+
state: 'fail',
201+
frameId: '123',
202+
};
203+
renderElementIntoDOM(component);
204+
205+
await RenderCoordinator.done();
206+
const label = component.shadowRoot?.querySelector('header')?.getAttribute('aria-label');
207+
assert.isOk(label);
208+
209+
assert.strictEqual(
210+
label, 'View details for LCP by Phase insight. Estimated savings for this insight: 50 ms and 5.0 kB');
211+
});
212+
});
213+
110214
describe('Ask AI Insights', () => {
111215
const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace;
112216
const FAKE_LCP_MODEL = {
@@ -145,6 +249,20 @@ describeWithEnvironment('BaseInsightComponent', () => {
145249
assert.isOk(button);
146250
});
147251

252+
it('adds a descriptive aria label to the button', async () => {
253+
updateHostConfig({
254+
devToolsAiAssistancePerformanceAgent: {
255+
enabled: true,
256+
insightsEnabled: true,
257+
}
258+
});
259+
const component = await renderComponent({insightHasAISupport: true});
260+
assert.isOk(component.shadowRoot);
261+
const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]');
262+
assert.isOk(button);
263+
assert.strictEqual(button.getAttribute('aria-label'), 'Ask AI about LCP by Phase insight');
264+
});
265+
148266
it('does not render the "Ask AI" button if disabled by enterprise policy', async () => {
149267
updateHostConfig({
150268
devToolsAiAssistancePerformanceAgent: {

front_end/panels/timeline/components/insights/BaseInsightComponent.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,23 @@ const UIStrings = {
4343
* @example {112 kB} PH2
4444
*/
4545
estimatedSavingsTimingAndBytes: 'Est savings: {PH1} & {PH2}',
46+
/**
47+
* @description Text to tell the user the estimated time or size savings for this insight that is used for screen readers.
48+
* @example {401 ms} PH1
49+
* @example {112 kB} PH1
50+
*/
51+
estimatedSavingsAria: 'Estimated savings for this insight: {PH1}',
52+
/**
53+
* @description Text to tell the user the estimated time and size savings for this insight that is used for screen readers.
54+
* @example {401 ms} PH1
55+
* @example {112 kB} PH2
56+
*/
57+
estimatedSavingsTimingAndBytesAria: 'Estimated savings for this insight: {PH1} and {PH2}',
4658
/**
4759
* @description Used for screen-readers as a label on the button to expand an insight to view details
4860
* @example {LCP by phase} PH1
4961
*/
50-
viewDetails: 'View details for {PH1}',
62+
viewDetails: 'View details for {PH1} insight.',
5163
} as const;
5264

5365
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/insights/BaseInsightComponent.ts', UIStrings);
@@ -256,7 +268,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
256268
return null;
257269
}
258270

259-
#getEstimatedSavingsString(): string|null {
271+
#getEstimatedSavingsTextParts(): {bytesString?: string, timeString?: string} {
260272
const savingsTime = this.getEstimatedSavingsTime();
261273
const savingsBytes = this.getEstimatedSavingsBytes();
262274

@@ -267,6 +279,37 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
267279
if (savingsBytes) {
268280
bytesString = i18n.ByteUtilities.bytesToString(savingsBytes);
269281
}
282+
return {
283+
timeString,
284+
bytesString,
285+
};
286+
}
287+
288+
#getEstimatedSavingsAriaLabel(): string|null {
289+
const {bytesString, timeString} = this.#getEstimatedSavingsTextParts();
290+
291+
if (timeString && bytesString) {
292+
return i18nString(UIStrings.estimatedSavingsTimingAndBytesAria, {
293+
PH1: timeString,
294+
PH2: bytesString,
295+
});
296+
}
297+
if (timeString) {
298+
return i18nString(UIStrings.estimatedSavingsAria, {
299+
PH1: timeString,
300+
});
301+
}
302+
if (bytesString) {
303+
return i18nString(UIStrings.estimatedSavingsAria, {
304+
PH1: bytesString,
305+
});
306+
}
307+
308+
return null;
309+
}
310+
311+
#getEstimatedSavingsString(): string|null {
312+
const {bytesString, timeString} = this.#getEstimatedSavingsTextParts();
270313

271314
if (timeString && bytesString) {
272315
return i18nString(UIStrings.estimatedSavingsTimingAndBytes, {
@@ -335,6 +378,8 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
335378
if (!this.#selected) {
336379
return Lit.nothing;
337380
}
381+
382+
const ariaLabel = `Ask AI about ${insightModel.title} insight`;
338383
// Only render the insight body content if it is selected.
339384
// To avoid re-rendering triggered from elsewhere.
340385
const content = this.renderContent();
@@ -351,6 +396,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
351396
data-insights-ask-ai
352397
jslog=${VisualLogging.action(`timeline.insight-ask-ai.${this.internalName}`).track({click: true})}
353398
@click=${this.#askAIButtonClick}
399+
aria-label=${ariaLabel}
354400
>Ask AI</devtools-button>
355401
</div>
356402
`: Lit.nothing}
@@ -369,6 +415,13 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
369415
closed: !this.#selected,
370416
});
371417
const estimatedSavingsString = this.#getEstimatedSavingsString();
418+
const estimatedSavingsAriaLabel = this.#getEstimatedSavingsAriaLabel();
419+
420+
let ariaLabel = `${i18nString(UIStrings.viewDetails, {PH1: this.#model.title})}`;
421+
if (estimatedSavingsAriaLabel) {
422+
// space prefix is deliberate to add a gap after the view details text
423+
ariaLabel += ` ${estimatedSavingsAriaLabel}`;
424+
}
372425

373426
// clang-format off
374427
const output = html`
@@ -379,7 +432,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
379432
tabIndex="0"
380433
role="button"
381434
aria-expanded=${this.#selected}
382-
aria-label=${i18nString(UIStrings.viewDetails, {PH1: this.#model.title})}
435+
aria-label=${ariaLabel}
383436
>
384437
${this.#renderHoverIcon(this.#selected)}
385438
<h3 class="insight-title">${this.#model?.title}</h3>

0 commit comments

Comments
 (0)