Skip to content

Commit 3c318c6

Browse files
bmeurerDevtools-frontend LUCI CQ
authored andcommitted
[button] Ensure that Enter/Space triggers click events.
Previously we have applied specific duck tape in a bunch of places to prevent "keydown" event handlers on parent elements from eating up Enter/Space events and thus not firing click events on buttons as expected. With this CL we put the logic solely into the `<devtools-button>` component (together with a test) to ensure not every use site has to deal with this, and it's less easy to miss. Fixed: 373168872, 372411090 Change-Id: I00c183547a7f7f2e20a95467687885db463280ca Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5982917 Auto-Submit: Benedikt Meurer <[email protected]> Commit-Queue: Kim-Anh Tran <[email protected]> Reviewed-by: Kim-Anh Tran <[email protected]>
1 parent 0cd9217 commit 3c318c6

File tree

5 files changed

+43
-36
lines changed

5 files changed

+43
-36
lines changed

front_end/panels/issues/components/HideIssuesMenu.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class HideIssuesMenu extends HTMLElement {
4343
this.#shadow.adoptedStyleSheets = [hideIssuesMenuStyles];
4444
}
4545

46-
onMenuOpen(event: MouseEvent): void {
46+
onMenuOpen(event: Event): void {
4747
event.stopPropagation();
4848
const buttonElement = this.#shadow.querySelector('devtools-button');
4949
const contextMenu = new UI.ContextMenu.ContextMenu(event, {
@@ -55,14 +55,6 @@ export class HideIssuesMenu extends HTMLElement {
5555
void contextMenu.show();
5656
}
5757

58-
onKeydown(event: KeyboardEvent): void {
59-
if (event.key === 'Enter' || event.key === 'Space') {
60-
// Make sure we don't propagate 'Enter' or 'Space' key events to parents,
61-
// so that these get turned into 'click' events properly.
62-
event.stopImmediatePropagation();
63-
}
64-
}
65-
6658
#render(): void {
6759
// Disabled until https://crbug.com/1079231 is fixed.
6860
// clang-format off
@@ -71,8 +63,7 @@ export class HideIssuesMenu extends HTMLElement {
7163
.data=${{variant: Buttons.Button.Variant.ICON,iconName: 'dots-vertical', title: i18nString(UIStrings.tooltipTitle)} as Buttons.Button.ButtonData}
7264
.jslogContext=${'hide-issues'}
7365
class="hide-issues-menu-btn"
74-
@click=${this.onMenuOpen}
75-
@keydown=${this.onKeydown}></devtools-button>
66+
@click=${this.onMenuOpen}></devtools-button>
7667
`, this.#shadow, {host: this});
7768
}
7869
}

front_end/ui/components/buttons/Button.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ describe('Button', () => {
166166
assert.isFalse(innerButton.classList.contains('small'));
167167
});
168168

169+
it('prevents only "keydown" events for Enter and Space to bubble up', () => {
170+
const button = renderButton({variant: Buttons.Button.Variant.PRIMARY});
171+
const onKeydown = sinon.spy();
172+
button.addEventListener('keydown', onKeydown);
173+
174+
const innerButton = button.shadowRoot!.querySelector('button') as HTMLButtonElement;
175+
dispatchKeyDownEvent(innerButton, {bubbles: true, composed: true, key: 'Enter'});
176+
dispatchKeyDownEvent(innerButton, {bubbles: true, composed: true, key: ' '});
177+
dispatchKeyDownEvent(innerButton, {bubbles: true, composed: true, key: 'x'});
178+
179+
assert.isTrue(onKeydown.calledOnce);
180+
assert.strictEqual(onKeydown.getCall(0).args[0].key, 'x');
181+
});
182+
169183
describe('in forms', () => {
170184
function renderForm(data: Buttons.Button.ButtonData = {
171185
variant: Buttons.Button.Variant.PRIMARY,

front_end/ui/components/buttons/Button.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,28 @@ export class Button extends HTMLElement {
300300
}
301301
}
302302

303+
/**
304+
* Handles "keydown" events on the internal `<button>` element.
305+
*
306+
* This callback stops propagation of "keydown" events for Enter and Space
307+
* originating from the `<button>` element, to ensure that this custom element
308+
* can safely be used within parent elements (such as the `TreeOutline`) that
309+
* do have "keydown" handlers as well.
310+
*
311+
* Without this special logic, the Enter and Space events would be
312+
* consumed by parent elements, and no "click" event would be generated from
313+
* this button.
314+
*
315+
* @param event the "keydown" event.
316+
* @see https://crbug.com/373168872
317+
*/
318+
#onKeydown(event: KeyboardEvent): void {
319+
if (event.key !== 'Enter' && event.key !== ' ') {
320+
return;
321+
}
322+
event.stopPropagation();
323+
}
324+
303325
#isToolbarVariant(): boolean {
304326
return this.#props.variant === Variant.TOOLBAR || this.#props.variant === Variant.PRIMARY_TOOLBAR;
305327
}
@@ -362,10 +384,11 @@ export class Button extends HTMLElement {
362384
LitHtml.render(
363385
html`
364386
<button title=${ifDefined(this.#props.title)}
365-
.disabled=${this.#props.disabled}
366-
class=${classMap(classes)}
367-
aria-pressed=${ifDefined(this.#props.toggled)}
368-
jslog=${ifDefined(jslog)}
387+
.disabled=${this.#props.disabled}
388+
class=${classMap(classes)}
389+
aria-pressed=${ifDefined(this.#props.toggled)}
390+
jslog=${ifDefined(jslog)}
391+
@keydown=${this.#onKeydown}
369392
>${hasIcon
370393
? html`
371394
<devtools-icon name=${ifDefined(this.#props.toggled ? this.#props.toggledIconName : this.#props.iconName || this.#props.iconUrl)}>

front_end/ui/legacy/TabbedPane.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,6 @@ export class TabbedPaneTab {
12141214
tabElement.classList.add('measuring');
12151215
} else {
12161216
tabElement.addEventListener('click', this.tabClicked.bind(this), false);
1217-
tabElement.addEventListener('keydown', this.tabKeyDown.bind(this), false);
12181217
tabElement.addEventListener('auxclick', this.tabClicked.bind(this), false);
12191218
tabElement.addEventListener('mousedown', this.tabMouseDown.bind(this), false);
12201219
tabElement.addEventListener('mouseup', this.tabMouseUp.bind(this), false);
@@ -1265,19 +1264,6 @@ export class TabbedPaneTab {
12651264
element?.parentElement?.classList.contains('tabbed-pane-close-button') || false;
12661265
}
12671266

1268-
private tabKeyDown(ev: Event): void {
1269-
const event = ev as KeyboardEvent;
1270-
switch (event.key) {
1271-
case 'Enter':
1272-
case ' ':
1273-
if (this.isCloseIconClicked(event.target as HTMLElement)) {
1274-
this.closeTabs([this.id]);
1275-
ev.consume(true);
1276-
return;
1277-
}
1278-
}
1279-
}
1280-
12811267
private tabClicked(ev: Event): void {
12821268
const event = (ev as MouseEvent);
12831269
const middleButton = event.button === 1;

front_end/ui/legacy/UIUtils.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,13 +1100,6 @@ export function createTextButton(text: string, clickHandler?: ((arg0: Event) =>
11001100
button.variant = opts?.variant ? opts.variant : Buttons.Button.Variant.OUTLINED;
11011101
if (clickHandler) {
11021102
button.addEventListener('click', clickHandler);
1103-
button.addEventListener('keydown', (event: KeyboardEvent): void => {
1104-
if (event.key === 'Enter' || event.key === 'Space') {
1105-
// Make sure we don't propagate 'Enter' or 'Space' key events to parents,
1106-
// so that these get turned into 'click' events properly.
1107-
event.stopImmediatePropagation();
1108-
}
1109-
});
11101103
}
11111104
if (opts?.jslogContext) {
11121105
button.setAttribute('jslog', `${VisualLogging.action().track({click: true}).context(opts.jslogContext)}`);

0 commit comments

Comments
 (0)