Skip to content

Commit 209498c

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[styles] make color-mix keyboard interactable
This change allows selecting color-mix swatches with the keyboard by using the left/right arrows when a style rules is selected. When a color-mix is focused, alt+down activates its popover. To get the focusing right (focus only the icon and not the entire function call), the color-mix text is moved out of the icon's DOM structure. Bug: 396311936 Change-Id: Ibd3b095961beaf97a6538579a605b2955ec076ce Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6286012 Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]>
1 parent c2a8663 commit 209498c

File tree

10 files changed

+77
-80
lines changed

10 files changed

+77
-80
lines changed

front_end/panels/elements/StylePropertyTreeElement.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
190190

191191
const colorMixSwatch = stylePropertyTreeElement.valueElement?.querySelector('devtools-color-mix-swatch');
192192
assert.exists(colorMixSwatch);
193-
assert.isTrue(colorMixSwatch.textContent?.includes('var(--space)'));
194193
});
195194

196195
it('should show color mix swatch when color-mix is used with an known variable in interpolation method even if it is not a valid method',
@@ -201,7 +200,6 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
201200

202201
const colorMixSwatch = stylePropertyTreeElement.valueElement?.querySelector('devtools-color-mix-swatch');
203202
assert.exists(colorMixSwatch);
204-
assert.isTrue(colorMixSwatch.textContent?.includes('var(--garbage-space)'));
205203
});
206204

207205
it('should not show color mix swatch when color-mix is used with an unknown variable in interpolation method',
@@ -225,7 +223,7 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
225223
renderElementIntoDOM(colorMixSwatch);
226224

227225
assert.isTrue(addPopoverSpy.calledOnce);
228-
assert.strictEqual(addPopoverSpy.args[0][0], colorMixSwatch.icon);
226+
assert.strictEqual(addPopoverSpy.args[0][0], colorMixSwatch);
229227
assert.strictEqual(addPopoverSpy.args[0][1].contents()?.textContent, '#ff8000');
230228
});
231229

@@ -239,7 +237,7 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
239237
renderElementIntoDOM(colorMixSwatch);
240238

241239
assert.isTrue(addPopoverSpy.calledOnce);
242-
assert.strictEqual(addPopoverSpy.args[0][0], colorMixSwatch.icon);
240+
assert.strictEqual(addPopoverSpy.args[0][0], colorMixSwatch);
243241
assert.strictEqual(addPopoverSpy.args[0][1].contents()?.textContent, 'color(srgb 1 0.24 0.17)');
244242
});
245243

@@ -248,12 +246,14 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
248246
getTreeElement('color', 'color-mix(in srgb, color-mix(in oklch, red, green), blue)');
249247
stylePropertyTreeElement.updateTitle();
250248

251-
const outerColorMix = stylePropertyTreeElement.valueElement?.querySelector('devtools-color-mix-swatch');
249+
assert.exists(stylePropertyTreeElement.valueElement);
250+
251+
const [outerColorMix, innerColorMix] =
252+
Array.from(stylePropertyTreeElement.valueElement.querySelectorAll('devtools-color-mix-swatch'));
252253
assert.exists(outerColorMix);
254+
assert.exists(innerColorMix);
253255
const handler = sinon.fake();
254256
outerColorMix.addEventListener(InlineEditor.ColorMixSwatch.Events.COLOR_CHANGED, handler);
255-
const innerColorMix = outerColorMix.querySelector('devtools-color-mix-swatch');
256-
assert.exists(innerColorMix);
257257
assert.strictEqual(outerColorMix.getText(), 'color-mix(in srgb, color-mix(in oklch, red, green), blue)');
258258
assert.strictEqual(innerColorMix.getText(), 'color-mix(in oklch, red, green)');
259259
innerColorMix.setFirstColor('blue');
@@ -262,7 +262,7 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
262262

263263
// setFirstColor does not actually update the rendered color swatches or the textContent, which is why the first
264264
// color is still red here.
265-
const colorSwatch = innerColorMix.querySelector('devtools-color-swatch');
265+
const colorSwatch = stylePropertyTreeElement.valueElement.querySelector('devtools-color-swatch');
266266
assert.isOk(colorSwatch);
267267
const newColor = colorSwatch.getColor()?.as(Common.Color.Format.HEX);
268268
assert.isOk(newColor);

front_end/panels/elements/StylePropertyTreeElement.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -588,33 +588,29 @@ export class ColorMixRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
588588
const space = match.space.map(space => context.matchedResult.getComputedText(space)).join(' ');
589589
const color1Text = match.color1.map(color => context.matchedResult.getComputedText(color)).join(' ');
590590
const color2Text = match.color2.map(color => context.matchedResult.getComputedText(color)).join(' ');
591-
swatch.appendChild(contentChild);
591+
swatch.tabIndex = -1;
592592
swatch.setColorMixText(`color-mix(${space}, ${color1Text}, ${color2Text})`);
593-
swatch.setRegisterPopoverCallback(swatch => {
594-
if (swatch.icon) {
595-
this.#pane.addPopover(swatch.icon, {
596-
contents: () => {
597-
const color = swatch.mixedColor();
598-
if (!color) {
599-
return undefined;
600-
}
601-
const span = document.createElement('span');
602-
span.style.padding = '11px 7px';
603-
const rgb = color.as(Common.Color.Format.HEX);
604-
const text = rgb.isGamutClipped() ? color.asString() : rgb.asString();
605-
if (!text) {
606-
return undefined;
607-
}
608-
span.appendChild(document.createTextNode(text));
609-
return span;
610-
},
611-
jslogContext: 'elements.css-color-mix',
612-
});
613-
}
593+
this.#pane.addPopover(swatch, {
594+
contents: () => {
595+
const color = swatch.mixedColor();
596+
if (!color) {
597+
return undefined;
598+
}
599+
const span = document.createElement('span');
600+
span.style.padding = '11px 7px';
601+
const rgb = color.as(Common.Color.Format.HEX);
602+
const text = rgb.isGamutClipped() ? color.asString() : rgb.asString();
603+
if (!text) {
604+
return undefined;
605+
}
606+
span.appendChild(document.createTextNode(text));
607+
return span;
608+
},
609+
jslogContext: 'elements.css-color-mix',
614610
});
615611

616612
context.addControl('color', swatch);
617-
return [swatch];
613+
return [swatch, contentChild];
618614
}
619615

620616
matcher(): SDK.CSSPropertyParserMatchers.ColorMixMatcher {

front_end/panels/elements/StylesSidebarPane.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
396396
this.#genericPopoverHelper.setTimeout(500, 200);
397397
}
398398

399-
addPopover(element: Node, popover: {contents: () => HTMLElement | undefined, jslogContext?: string}): void {
399+
addPopover(element: HTMLElement, popover: {contents: () => HTMLElement | undefined, jslogContext?: string}): void {
400400
this.#elementPopoverHooks.set(element, popover);
401401
}
402402

front_end/panels/network/NetworkWaterfallColumn.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,10 @@ export class NetworkWaterfallColumn extends UI.Widget.VBox {
227227
}
228228
}
229229

230-
private getPopoverRequest(event: MouseEvent): UI.PopoverHelper.PopoverRequest|null {
230+
private getPopoverRequest(event: MouseEvent|KeyboardEvent): UI.PopoverHelper.PopoverRequest|null {
231+
if (event instanceof KeyboardEvent) {
232+
return null;
233+
}
231234
if (!this.hoveredNode) {
232235
return null;
233236
}

front_end/panels/protocol_monitor/JSONEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ export class JSONEditor extends Common.ObjectWrapper.eventMixin<EventTypes, type
460460
};
461461
}
462462

463-
#handlePopoverDescriptions(event: MouseEvent):
463+
#handlePopoverDescriptions(event: MouseEvent|KeyboardEvent):
464464
{box: AnchorBox, show: (popover: UI.GlassPane.GlassPane) => Promise<boolean>}|null {
465465
const hintElement = event.composedPath()[0] as HTMLElement;
466466
const elementData = this.#getDescriptionAndTypeForElement(hintElement);

front_end/panels/sources/DebuggerPlugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,10 @@ export class DebuggerPlugin extends Plugin {
660660
tokenType === 'PropertyDefinition';
661661
}
662662

663-
private getPopoverRequest(event: MouseEvent): UI.PopoverHelper.PopoverRequest|null {
663+
private getPopoverRequest(event: MouseEvent|KeyboardEvent): UI.PopoverHelper.PopoverRequest|null {
664+
if (event instanceof KeyboardEvent) {
665+
return null;
666+
}
664667
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
665668
return null;
666669
}

front_end/ui/legacy/PopoverHelper.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class PopoverHelper {
4343
};
4444
private disableOnClick: boolean;
4545
private hasPadding: boolean;
46-
private getRequest: (arg0: MouseEvent) => PopoverRequest | null;
46+
private getRequest: (arg0: MouseEvent|KeyboardEvent) => PopoverRequest | null;
4747
private scheduledRequest: PopoverRequest|null;
4848
private hidePopoverCallback: (() => void)|null;
4949
readonly container: Element;
@@ -54,8 +54,11 @@ export class PopoverHelper {
5454
private readonly boundMouseDown: (event: Event) => void;
5555
private readonly boundMouseMove: (ev: Event) => void;
5656
private readonly boundMouseOut: (event: Event) => void;
57+
private readonly boundKeyUp: (ev: Event) => void;
5758
jslogContext?: string;
58-
constructor(container: Element, getRequest: (arg0: MouseEvent) => PopoverRequest | null, jslogContext?: string) {
59+
constructor(
60+
container: Element, getRequest: (arg0: MouseEvent|KeyboardEvent) => PopoverRequest | null,
61+
jslogContext?: string) {
5962
this.disableOnClick = false;
6063
this.hasPadding = false;
6164
this.getRequest = getRequest;
@@ -70,9 +73,11 @@ export class PopoverHelper {
7073
this.boundMouseDown = this.mouseDown.bind(this);
7174
this.boundMouseMove = this.mouseMove.bind(this);
7275
this.boundMouseOut = this.mouseOut.bind(this);
76+
this.boundKeyUp = this.keyUp.bind(this);
7377
this.container.addEventListener('mousedown', this.boundMouseDown, false);
7478
this.container.addEventListener('mousemove', this.boundMouseMove, false);
7579
this.container.addEventListener('mouseout', this.boundMouseOut, false);
80+
this.container.addEventListener('keyup', this.boundKeyUp, false);
7681
this.setTimeout(1000);
7782
}
7883

@@ -108,6 +113,23 @@ export class PopoverHelper {
108113
this.startShowPopoverTimer((event as MouseEvent), 0);
109114
}
110115

116+
private keyUp(ev: Event): void {
117+
const event = ev as KeyboardEvent;
118+
if (event.altKey && event.key === 'ArrowDown') {
119+
if (this.isPopoverVisible()) {
120+
this.hidePopover();
121+
} else {
122+
this.stopShowPopoverTimer();
123+
this.startHidePopoverTimer(0);
124+
this.startShowPopoverTimer(event, 0);
125+
}
126+
ev.stopPropagation();
127+
} else if (event.key === 'Escape' && this.isPopoverVisible()) {
128+
this.hidePopover();
129+
ev.stopPropagation();
130+
}
131+
}
132+
111133
private mouseMove(ev: Event): void {
112134
const event = (ev as MouseEvent);
113135
if (this.eventInScheduledContent(event)) {
@@ -163,7 +185,7 @@ export class PopoverHelper {
163185
}, timeout);
164186
}
165187

166-
private startShowPopoverTimer(event: MouseEvent, timeout: number): void {
188+
private startShowPopoverTimer(event: MouseEvent|KeyboardEvent, timeout: number): void {
167189
this.scheduledRequest = this.getRequest.call(null, event);
168190
if (!this.scheduledRequest) {
169191
return;

front_end/ui/legacy/components/inline_editor/ColorMixSwatch.test.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,22 @@ function createSwatch(text: string, firstColor: string, secondColor: string) {
1919
}
2020

2121
describeWithLocale('ColorMixSwatch', () => {
22-
it('should render color-mix swatch with icon and text when the syntax is correct', () => {
22+
it('should render color-mix swatch icon when the syntax is correct', () => {
2323
const swatch = createSwatch('color-mix(in srgb, red, blue)', 'red', 'blue');
2424

2525
const swatchIcon = swatch.shadowRoot?.querySelector('.swatch-icon');
2626

27-
assert.strictEqual(swatch.shadowRoot?.textContent?.trim(), 'color-mix(in srgb, red, blue)');
27+
assert.strictEqual(swatch?.getText(), 'color-mix(in srgb, red, blue)');
2828
assert.isNotNull(swatchIcon);
2929
});
3030

3131
it('should changing the second color work correctly when the colors are the same', () => {
3232
const swatch = createSwatch('color-mix(in srgb, red, red)', 'red', 'red');
3333
swatch.setSecondColor('blue');
3434

35-
const swatchIcon = swatch.shadowRoot?.querySelector('.swatch-icon');
35+
assert.strictEqual(swatch?.getText(), 'color-mix(in srgb, red, blue)');
3636

37-
assert.strictEqual(swatch.shadowRoot?.textContent?.trim(), 'color-mix(in srgb, red, blue)');
37+
const swatchIcon = swatch.shadowRoot?.querySelector('.swatch-icon');
3838
assert.isNotNull(swatchIcon);
3939
});
40-
41-
it('calls the popover registration callback upon rendering', () => {
42-
const swatch = createSwatch('color-mix(in srgb, red, red)', 'red', 'red');
43-
44-
const cb = sinon.stub<[InlineEditor.ColorMixSwatch.ColorMixSwatch], void>();
45-
const values: string[] = [];
46-
cb.callsFake(swatch => values.push(swatch.shadowRoot?.textContent?.trim() ?? ''));
47-
swatch.setRegisterPopoverCallback(cb);
48-
swatch.setFirstColor('blue');
49-
swatch.setSecondColor('purple');
50-
swatch.setColorMixText('color-mix(in hsl, yellow, yellow)');
51-
assert.lengthOf(cb.getCalls(), 4);
52-
53-
assert.deepEqual(values, [
54-
'color-mix(in srgb, red, red)',
55-
'color-mix(in srgb, blue, red)',
56-
'color-mix(in srgb, blue, purple)',
57-
'color-mix(in hsl, yellow, yellow)',
58-
]);
59-
});
6040
});

front_end/ui/legacy/components/inline_editor/ColorMixSwatch.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as Common from '../../../../core/common/common.js';
66
import * as Platform from '../../../../core/platform/platform.js';
7-
import {html, render} from '../../../lit/lit.js';
7+
import * as Lit from '../../../lit/lit.js';
88
import * as VisualLogging from '../../../visual_logging/visual_logging.js';
99

1010
import colorMixSwatchStylesRaw from './colorMixSwatch.css.js';
@@ -13,6 +13,8 @@ import colorMixSwatchStylesRaw from './colorMixSwatch.css.js';
1313
const colorMixSwatchStyles = new CSSStyleSheet();
1414
colorMixSwatchStyles.replaceSync(colorMixSwatchStylesRaw.cssContent);
1515

16+
const {html, render, Directives: {ref}} = Lit;
17+
1618
export const enum Events {
1719
COLOR_CHANGED = 'colorChanged',
1820
}
@@ -26,7 +28,7 @@ export class ColorMixSwatch extends Common.ObjectWrapper.eventMixin<EventTypes,
2628
private colorMixText = ''; // color-mix(in srgb, hotpink, white)
2729
private firstColorText = ''; // hotpink
2830
private secondColorText = ''; // white
29-
#registerPopoverCallback: undefined|((swatch: ColorMixSwatch) => void);
31+
#icon: HTMLElement|null = null;
3032

3133
constructor() {
3234
super();
@@ -35,12 +37,8 @@ export class ColorMixSwatch extends Common.ObjectWrapper.eventMixin<EventTypes,
3537
];
3638
}
3739

38-
get icon(): Element|null {
39-
return this.shadow.firstElementChild;
40-
}
41-
4240
mixedColor(): Common.Color.Color|null {
43-
const colorText = this.icon?.computedStyleMap().get('color')?.toString() ?? null;
41+
const colorText = this.#icon?.computedStyleMap().get('color')?.toString() ?? null;
4442
return colorText ? Common.Color.parse(colorText) : null;
4543
}
4644

@@ -75,11 +73,6 @@ export class ColorMixSwatch extends Common.ObjectWrapper.eventMixin<EventTypes,
7573
this.#render();
7674
}
7775

78-
setRegisterPopoverCallback(callback: (swatch: ColorMixSwatch) => void): void {
79-
this.#registerPopoverCallback = callback;
80-
callback(this);
81-
}
82-
8376
getText(): string {
8477
return this.colorMixText;
8578
}
@@ -98,15 +91,15 @@ export class ColorMixSwatch extends Common.ObjectWrapper.eventMixin<EventTypes,
9891
// Note also that whitespace between nodes is removed on purpose to avoid pushing these elements apart. Do not
9992
// re-format the HTML code.
10093
render(
101-
html`<div class="swatch-icon" jslog=${VisualLogging.cssColorMix()} style="--color: ${this.colorMixText}">
94+
html`<div class="swatch-icon"
95+
${ref(e => {this.#icon = e as HTMLElement; })}
96+
jslog=${VisualLogging.cssColorMix()}
97+
style="--color: ${this.colorMixText}">
10298
<span class="swatch swatch-left" id="swatch-1" style="--color: ${this.firstColorText}"></span>
10399
<span class="swatch swatch-right" id="swatch-2" style="--color: ${this.secondColorText}"></span>
104-
<span class="swatch swatch-mix" id="mix-result" style="--color: ${this.colorMixText}"></span>
105-
</div><slot>${this.colorMixText}</slot>`,
100+
<span class="swatch swatch-mix" id="mix-result" style="--color: ${this.colorMixText}"></span></div>`,
106101
this.shadow, {host: this});
107102
// clang-format on
108-
109-
this.#registerPopoverCallback && this.#registerPopoverCallback(this);
110103
}
111104
}
112105

front_end/ui/legacy/components/inline_editor/colorMixSwatch.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
inline-size: 15px;
1111
grid: [stack] 1fr / [stack] 1fr;
1212
margin-left: 1px;
13-
margin-right: 2px;
13+
margin-right: 1px;
1414
color: var(--color);
1515
}
1616

0 commit comments

Comments
 (0)