Skip to content

Commit 248f318

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[devtools-tooltip] Add a heavy-focus mode
This allows for tooltips to not appear automatically on keyboard focus but to require a hotkey in order to open. This behavior and the hotkey choice is motivated by interesttarget. Hovering with a mouse still works regularly. Bug: 396311936 Change-Id: I15291a7376c0a061e34d8f958914bfe4dd2fc150 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6336175 Reviewed-by: Kim-Anh Tran <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]>
1 parent d87b624 commit 248f318

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

front_end/ui/components/tooltips/Tooltip.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ interface RenderProps {
1414
variant?: Tooltips.Tooltip.TooltipVariant;
1515
attribute?: 'aria-describedby'|'aria-details';
1616
useClick?: boolean;
17+
useHotkey?: boolean;
1718
jslogContext?: string;
1819
}
1920

2021
function renderTooltip({
2122
variant = 'simple',
2223
attribute = 'aria-describedby',
2324
useClick = false,
25+
useHotkey = false,
2426
jslogContext = undefined,
2527
}: RenderProps = {}) {
2628
const container = document.createElement('div');
@@ -30,7 +32,13 @@ function renderTooltip({
3032
html`<button aria-details="tooltip-id">Button</button>` :
3133
html`<button aria-describedby="tooltip-id">Button</button>`
3234
}
33-
<devtools-tooltip id="tooltip-id" variant=${variant} ?use-click=${useClick} jslogContext=${jslogContext??nothing}>
35+
<devtools-tooltip
36+
id="tooltip-id"
37+
variant=${variant}
38+
?use-click=${useClick}
39+
?use-hotkey=${useHotkey}
40+
jslogContext=${jslogContext??nothing}
41+
>
3442
${variant === 'rich' ? html`<p>Rich content</p>` : 'Simple content'}
3543
</devtools-tooltip>
3644
`, container);
@@ -125,6 +133,25 @@ describe('Tooltip', () => {
125133
assert.isTrue(container.querySelector('devtools-tooltip')?.open);
126134
});
127135

136+
it('should open with hotkey if use-hotkey is set', () => {
137+
const container = renderTooltip({useHotkey: true});
138+
139+
const button = container.querySelector('button');
140+
button?.dispatchEvent(new KeyboardEvent('keydown', {altKey: true, key: 'ArrowDown'}));
141+
142+
assert.isTrue(container.querySelector('devtools-tooltip')?.open);
143+
});
144+
145+
it('should not open on focus if use-hotkey is set', async () => {
146+
const container = renderTooltip({useHotkey: true});
147+
148+
const button = container.querySelector('button');
149+
button?.dispatchEvent(new FocusEvent('focus'));
150+
151+
await checkForPendingActivity();
152+
assert.isFalse(container.querySelector('devtools-tooltip')?.open);
153+
});
154+
128155
const eventsNotToPropagate = ['click', 'mouseup'];
129156

130157
eventsNotToPropagate.forEach(eventName => {

front_end/ui/components/tooltips/Tooltip.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ export interface TooltipProperties {
2121
/**
2222
* @attr id - Id of the tooltip. Used for searching an anchor element with aria-describedby.
2323
* @attr hover-delay - Hover length in ms before the tooltip is shown and hidden.
24-
* @attr variant - Variant of the tooltip, `"simple"` for strings only, inverted background,
25-
* `"rich"` for interactive content, background according to theme's surface.
24+
* @attr variant - Variant of the tooltip, `"simple"` for strings only, inverted background, `"rich"` for interactive
25+
* content, background according to theme's surface.
2626
* @attr use-click - If present, the tooltip will be shown on click instead of on hover.
27+
* @attr use-hotkey - If present, the tooltip will be shown on hover but not when receiving focus. Requires a hotkey to
28+
* open when fosed (Alt-down). When `"use-click"` is present as well, use-click takes precedence.
2729
* @prop {String} id - reflects the `"id"` attribute.
2830
* @prop {Number} hoverDelay - reflects the `"hover-delay"` attribute.
2931
* @prop {String} variant - reflects the `"variant"` attribute.
3032
* @prop {Boolean} useClick - reflects the `"click"` attribute.
33+
* @prop {Boolean} useHotkey - reflects the `"use-hotkey"` attribute.
3134
*/
3235
export class Tooltip extends HTMLElement {
3336
static readonly observedAttributes = ['id', 'variant', 'jslogcontext'];
@@ -42,6 +45,17 @@ export class Tooltip extends HTMLElement {
4245
return this.matches(':popover-open');
4346
}
4447

48+
get useHotkey(): boolean {
49+
return this.hasAttribute('use-hotkey') ?? false;
50+
}
51+
set useHotkey(useHotkey: boolean) {
52+
if (useHotkey) {
53+
this.setAttribute('use-hotkey', '');
54+
} else {
55+
this.removeAttribute('use-hotkey');
56+
}
57+
}
58+
4559
get useClick(): boolean {
4660
return this.hasAttribute('use-click') ?? false;
4761
}
@@ -143,7 +157,8 @@ export class Tooltip extends HTMLElement {
143157
window.clearTimeout(this.#timeout);
144158
}
145159
// Don't hide a rich tooltip when hovering over the tooltip itself.
146-
if (this.variant === 'rich' && event.relatedTarget === this) {
160+
if (this.variant === 'rich' &&
161+
(event.relatedTarget === this || (event.relatedTarget as Element)?.parentElement === this)) {
147162
return;
148163
}
149164
this.#timeout = window.setTimeout(() => {
@@ -192,13 +207,24 @@ export class Tooltip extends HTMLElement {
192207
}
193208
};
194209

210+
#keyDown = (event: KeyboardEvent): void => {
211+
if ((event.altKey && event.key === 'ArrowDown') || (event.key === 'Escape' && this.open)) {
212+
this.toggle();
213+
event.stopPropagation();
214+
}
215+
};
216+
195217
#registerEventListeners(): void {
196218
if (this.#anchor) {
197219
if (this.useClick) {
198220
this.#anchor.addEventListener('click', this.toggle);
199221
} else {
200222
this.#anchor.addEventListener('mouseenter', this.showTooltip);
201-
this.#anchor.addEventListener('focus', this.showTooltip);
223+
if (this.useHotkey) {
224+
this.#anchor.addEventListener('keydown', this.#keyDown);
225+
} else {
226+
this.#anchor.addEventListener('focus', this.showTooltip);
227+
}
202228
this.#anchor.addEventListener('blur', this.hideTooltip);
203229
this.#anchor.addEventListener('mouseleave', this.hideTooltip);
204230
this.addEventListener('mouseleave', this.hideTooltip);
@@ -220,6 +246,7 @@ export class Tooltip extends HTMLElement {
220246
this.#anchor.removeEventListener('mouseenter', this.showTooltip);
221247
this.#anchor.removeEventListener('focus', this.showTooltip);
222248
this.#anchor.removeEventListener('blur', this.hideTooltip);
249+
this.#anchor.removeEventListener('keydown', this.#keyDown);
223250
this.#anchor.removeEventListener('mouseleave', this.hideTooltip);
224251
}
225252
this.removeEventListener('mouseleave', this.hideTooltip);

0 commit comments

Comments
 (0)