Skip to content

Commit 62dc5e3

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[css value tracing] Add keyboard navigation
Bug: 403246243 Change-Id: I01f255ede3a74c082d31c4cbaa97b2909e46ab8a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6352460 Commit-Queue: Philip Pfaffe <[email protected]> Reviewed-by: Eric Leese <[email protected]>
1 parent 1f65f38 commit 62dc5e3

File tree

6 files changed

+116
-29
lines changed

6 files changed

+116
-29
lines changed

front_end/panels/elements/CSSValueTraceView.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from './PropertyRenderer.js';
1717
import stylePropertiesTreeOutlineStyles from './stylePropertiesTreeOutline.css.js';
1818

19-
const {html, render} = Lit;
19+
const {html, render, Directives: {ref, ifDefined}} = Lit;
2020

2121
export interface ViewInput {
2222
substitutions: Node[][];
@@ -25,18 +25,32 @@ export interface ViewInput {
2525
onToggle: () => void;
2626
}
2727

28+
export interface ViewOutput {
29+
defaultFocusedElement?: Element;
30+
}
31+
2832
export type View = (
2933
input: ViewInput,
30-
output: object,
34+
output: ViewOutput,
3135
target: HTMLElement,
3236
) => void;
3337

34-
function defaultView(input: ViewInput, _: unknown, target: HTMLElement): void {
38+
function defaultView(input: ViewInput, output: ViewOutput, target: HTMLElement): void {
3539
const [firstEvaluation, ...intermediateEvaluations] = input.evaluations;
40+
41+
const hiddenSummary = !firstEvaluation || intermediateEvaluations.length === 0;
42+
const summaryTabIndex = hiddenSummary ? undefined : 0;
3643
render(
3744
// clang-format off
3845
html`
39-
<div class="css-value-trace monospace">
46+
<div
47+
role=dialog
48+
${ref(e => {
49+
output.defaultFocusedElement = (e as HTMLDivElement)?.querySelector('[tabindex]') ?? undefined;
50+
})}
51+
class="css-value-trace monospace"
52+
@keydown=${onKeyDown}
53+
>
4054
${input.substitutions.map(
4155
line =>
4256
html`<span class="trace-line-icon" aria-label="is equal to"></span
@@ -47,10 +61,9 @@ function defaultView(input: ViewInput, _: unknown, target: HTMLElement): void {
4761
><span class="trace-line">${firstEvaluation}</span>`
4862
: html`<details
4963
@toggle=${input.onToggle}
50-
?hidden=${!firstEvaluation ||
51-
intermediateEvaluations.length === 0}
64+
?hidden=${hiddenSummary}
5265
>
53-
<summary>
66+
<summary tabindex=${ifDefined(summaryTabIndex)}>
5467
<span class="trace-line-icon" aria-label="is equal to"></span
5568
><devtools-icon class="marker"></devtools-icon
5669
><span class="trace-line">${firstEvaluation}</span>
@@ -73,6 +86,34 @@ function defaultView(input: ViewInput, _: unknown, target: HTMLElement): void {
7386
// clang-format on
7487
target,
7588
);
89+
90+
function onKeyDown(this: HTMLDivElement, e: KeyboardEvent): void {
91+
// prevent styles-tab keyboard navigation
92+
if (!e.altKey) {
93+
if (e.key.startsWith('Arrow') || e.key === ' ' || e.key === 'Enter') {
94+
e.consume();
95+
}
96+
}
97+
98+
// Capture tab focus within
99+
if (e.key === 'Tab') {
100+
const tabstops = this.querySelectorAll('[tabindex]') ?? [];
101+
const firstTabStop = tabstops[0];
102+
const lastTabStop = tabstops[tabstops.length - 1];
103+
if (e.target === lastTabStop && !e.shiftKey) {
104+
e.consume(true);
105+
if (firstTabStop instanceof HTMLElement) {
106+
firstTabStop.focus();
107+
}
108+
}
109+
if (e.target === firstTabStop && e.shiftKey) {
110+
e.consume(true);
111+
if (lastTabStop instanceof HTMLElement) {
112+
lastTabStop.focus();
113+
}
114+
}
115+
}
116+
}
76117
}
77118

78119
export class CSSValueTraceView extends UI.Widget.VBox {
@@ -168,7 +209,8 @@ export class CSSValueTraceView extends UI.Widget.VBox {
168209
finalResult: this.#finalResult,
169210
onToggle: () => this.onResize(),
170211
};
171-
const viewOutput = {};
212+
const viewOutput: ViewOutput = {};
172213
this.#view(viewInput, viewOutput, this.contentElement);
214+
this.setDefaultFocusedElement(viewOutput.defaultFocusedElement ?? null);
173215
}
174216
}

front_end/panels/elements/PropertyRenderer.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export class Highlighting {
6868
readonly #nodesForMatches = new Map<SDK.CSSPropertyParser.Match, Node[][]>();
6969
readonly #matchesForNodes = new Map<Node, SDK.CSSPropertyParser.Match[]>();
7070
readonly #registry: Highlight;
71-
readonly #boundOnEnter: (ev: MouseEvent) => void;
72-
readonly #boundOnExit: (ev: MouseEvent) => void;
71+
readonly #boundOnEnter: (e: Event) => void;
72+
readonly #boundOnExit: (e: Event) => void;
7373

7474
constructor() {
7575
const registry = CSS.highlights.get(Highlighting.REGISTRY_NAME);
@@ -100,11 +100,14 @@ export class Highlighting {
100100
if (node instanceof HTMLElement) {
101101
node.onmouseenter = this.#boundOnEnter;
102102
node.onmouseleave = this.#boundOnExit;
103+
node.onfocus = this.#boundOnEnter;
104+
node.onblur = this.#boundOnExit;
105+
node.tabIndex = 0;
103106
}
104107
}
105108
}
106109

107-
* #nodeRangesHitByMouseEvent(e: MouseEvent): Generator<Node[]> {
110+
* #nodeRangesHitByMouseEvent(e: Event): Generator<Node[]> {
108111
for (const node of e.composedPath()) {
109112
const matches = this.#matchesForNodes.get(node as Node);
110113
if (matches) {
@@ -116,15 +119,19 @@ export class Highlighting {
116119
}
117120
}
118121

119-
#onEnter(e: MouseEvent): void {
122+
#onEnter(e: Event): void {
120123
this.#registry.clear();
121124
this.#activeHighlights.push([]);
122125
for (const nodeRange of this.#nodeRangesHitByMouseEvent(e)) {
123126
const range = new Range();
124-
range.setStartBefore(nodeRange[0]);
125-
range.setEndAfter(nodeRange[nodeRange.length - 1]);
126-
this.#activeHighlights[this.#activeHighlights.length - 1].push(range);
127-
this.#registry.add(range);
127+
const begin = nodeRange[0];
128+
const end = nodeRange[nodeRange.length - 1];
129+
if (begin.parentNode && end.parentNode) {
130+
range.setStartBefore(begin);
131+
range.setEndAfter(end);
132+
this.#activeHighlights[this.#activeHighlights.length - 1].push(range);
133+
this.#registry.add(range);
134+
}
128135
}
129136
}
130137

@@ -413,10 +420,19 @@ export class Renderer extends SDK.CSSPropertyParser.TreeWalker {
413420
})}`);
414421
UI.ARIAUtils.setLabel(valueElement, i18nString(UIStrings.cssPropertyValue, {PH1: property.value}));
415422
valueElement.className = 'value';
423+
const {nodes, cssControls} = this.renderValueNodes(property, matchedResult, renderers, tracing);
424+
nodes.forEach(node => valueElement.appendChild(node));
425+
valueElement.normalize();
426+
return {valueElement, cssControls};
427+
}
416428

429+
static renderValueNodes(
430+
property: SDK.CSSProperty.CSSProperty|{name: string, value: string},
431+
matchedResult: SDK.CSSPropertyParser.BottomUpTreeMatching|null,
432+
renderers: Array<MatchRenderer<SDK.CSSPropertyParser.Match>>,
433+
tracing?: TracingContext): {nodes: Node[], cssControls: SDK.CSSPropertyParser.CSSControlMap} {
417434
if (!matchedResult) {
418-
valueElement.appendChild(document.createTextNode(property.value));
419-
return {valueElement, cssControls: new Map()};
435+
return {nodes: [document.createTextNode(property.value)], cssControls: new Map()};
420436
}
421437
const rendererMap = new Map<
422438
Platform.Constructor.Constructor<SDK.CSSPropertyParser.Match>, MatchRenderer<SDK.CSSPropertyParser.Match>>();
@@ -427,10 +443,7 @@ export class Renderer extends SDK.CSSPropertyParser.TreeWalker {
427443
const context = new RenderingContext(
428444
matchedResult.ast, property instanceof SDK.CSSProperty.CSSProperty ? property : null, rendererMap,
429445
matchedResult, undefined, {}, tracing);
430-
const {nodes, cssControls} = Renderer.render([matchedResult.ast.tree, ...matchedResult.ast.trailingNodes], context);
431-
nodes.forEach(node => valueElement.appendChild(node));
432-
valueElement.normalize();
433-
return {valueElement, cssControls};
446+
return Renderer.render([matchedResult.ast.tree, ...matchedResult.ast.trailingNodes], context);
434447
}
435448
}
436449

front_end/panels/elements/StylePropertyTreeElement.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ function getTracingTooltip(
247247
// clang-format off
248248
return html`<span tabIndex=-1 aria-details=${tooltipId}>${functionName}</span><devtools-tooltip
249249
id=${tooltipId}
250-
heavy-focus
250+
use-hotkey
251251
variant=rich
252252
jslogContext=elements.css-value-trace
253253
@beforetoggle=${function(this: Tooltips.Tooltip.Tooltip, e: ToggleEvent) {
@@ -261,7 +261,25 @@ function getTracingTooltip(
261261
computedStyles));
262262
}
263263
}}
264+
@toggle=${function(this: Tooltips.Tooltip.Tooltip,e: ToggleEvent) {
265+
if (e.newState === 'open') {
266+
(this.querySelector('devtools-widget') as UI.Widget.WidgetElement<CSSValueTraceView>| null)
267+
?.getWidget()
268+
?.focus();
269+
}
270+
}}
264271
><devtools-widget
272+
@keydown=${(e: KeyboardEvent) => {
273+
const maybeTooltip = (e.target as Element).parentElement ;
274+
if (!(maybeTooltip instanceof Tooltips.Tooltip.Tooltip)) {
275+
return;
276+
}
277+
if (e.key === 'Escape' || (e.altKey && e.key === 'ArrowDown')){
278+
maybeTooltip.hideTooltip();
279+
maybeTooltip.anchor?.focus();
280+
e.consume();
281+
}
282+
}}
265283
.widgetConfig=${UI.Widget.widgetConfig(CSSValueTraceView, {})}
266284
></devtools-widget></devtools-tooltip>`;
267285
// clang-format on
@@ -294,14 +312,14 @@ export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
294312
const substitution = context.tracing?.substitution();
295313
if (substitution) {
296314
if (declaration?.declaration instanceof SDK.CSSProperty.CSSProperty) {
297-
const {valueElement, cssControls} = Renderer.renderValueElement(
315+
const {nodes, cssControls} = Renderer.renderValueNodes(
298316
declaration.declaration,
299317
substitution.cachedParsedValue(declaration.declaration, this.#matchedStyles, this.#computedStyles),
300318
getPropertyRenderers(
301319
declaration.declaration.ownerStyle, this.#stylesPane, this.#matchedStyles, null, this.#computedStyles),
302320
substitution);
303321
cssControls.forEach((value, key) => value.forEach(control => context.addControl(key, control)));
304-
return [valueElement];
322+
return nodes;
305323
}
306324
if (!declaration && match.fallback.length > 0) {
307325
return Renderer.render(match.fallback, substitution.renderingContext(context)).nodes;

front_end/panels/elements/cssValueTraceView.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* Use of this source code is governed by a BSD-style license that can be
44
* found in the LICENSE file.
55
*/
6+
:host(:focus-within) {
7+
/* stylelint-disable-next-line declaration-no-important */
8+
outline: none !important;
9+
}
610

711
.css-value-trace {
812
--cell-width: 1.5em;
@@ -20,6 +24,11 @@
2024
padding-top: var(--sys-size-4);
2125
}
2226

27+
:focus {
28+
border-radius: var(--sys-size-2);
29+
outline: var(--sys-size-2) solid var(--sys-color-state-focus-ring);
30+
}
31+
2332
details {
2433
height: min-content;
2534
grid-column: 1 / 4;

front_end/panels/elements/stylePropertiesTreeOutline.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
color: inherit;
99
}
1010

11-
.inactive-value:not(:hover) {
11+
.inactive-value:not(:hover,:focus,:focus-within) {
1212
text-decoration: line-through;
1313
}
1414

@@ -298,5 +298,6 @@ devtools-icon.open-in-animations-panel {
298298
}
299299

300300
.value :focus {
301-
outline: 1px solid var(--sys-color-outline);
301+
border-radius: var(--sys-size-2);
302+
outline: var(--sys-size-2) solid var(--sys-color-state-focus-ring);
302303
}

front_end/ui/components/tooltips/Tooltip.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export class Tooltip extends HTMLElement {
8989
this.#updateJslog();
9090
}
9191

92+
get anchor(): HTMLElement|null {
93+
return this.#anchor;
94+
}
95+
9296
constructor(properties?: TooltipProperties) {
9397
super();
9498
if (properties) {
@@ -152,12 +156,12 @@ export class Tooltip extends HTMLElement {
152156
}, this.hoverDelay);
153157
};
154158

155-
hideTooltip = (event: MouseEvent|FocusEvent): void => {
159+
hideTooltip = (event?: MouseEvent|FocusEvent): void => {
156160
if (this.#timeout) {
157161
window.clearTimeout(this.#timeout);
158162
}
159163
// Don't hide a rich tooltip when hovering over the tooltip itself.
160-
if (this.variant === 'rich' &&
164+
if (event && this.variant === 'rich' &&
161165
(event.relatedTarget === this || (event.relatedTarget as Element)?.parentElement === this)) {
162166
return;
163167
}

0 commit comments

Comments
 (0)