Skip to content

Commit fbe3ddc

Browse files
authored
fix: gutter hover tooltip a11y improvements (#5747)
- made tooltip mouse hoverable - tooltip will still close on any keypress, unless the CTRL/Command are also being pressed at the same time
1 parent 9574e1e commit fbe3ddc

File tree

3 files changed

+116
-49
lines changed

3 files changed

+116
-49
lines changed

src/keyboard/gutter_handler_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,11 @@ module.exports = {
149149

150150
// Click annotation.
151151
emit(keys["enter"]);
152-
152+
153153
setTimeout(function() {
154154
// Check annotation is rendered.
155155
editor.renderer.$loop._flush();
156-
var tooltip = editor.container.querySelector(".ace_tooltip");
156+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
157157
assert.ok(/error test/.test(tooltip.textContent));
158158

159159
// Press escape to dismiss the tooltip.
@@ -198,7 +198,7 @@ module.exports = {
198198
setTimeout(function() {
199199
// Check annotation is rendered.
200200
editor.renderer.$loop._flush();
201-
var tooltip = editor.container.querySelector(".ace_tooltip");
201+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
202202
assert.ok(/error test/.test(tooltip.textContent));
203203

204204
// Press escape to dismiss the tooltip.
@@ -214,7 +214,7 @@ module.exports = {
214214
setTimeout(function() {
215215
// Check annotation is rendered.
216216
editor.renderer.$loop._flush();
217-
var tooltip = editor.container.querySelector(".ace_tooltip");
217+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
218218
assert.ok(/warning test/.test(tooltip.textContent));
219219

220220
// Press escape to dismiss the tooltip.

src/mouse/default_gutter_handler.js

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ var dom = require("../lib/dom");
66
var event = require("../lib/event");
77
var Tooltip = require("../tooltip").Tooltip;
88
var nls = require("../config").nls;
9-
var lang = require("../lib/lang");
9+
10+
const GUTTER_TOOLTIP_LEFT_OFFSET = 5;
11+
const GUTTER_TOOLTIP_TOP_OFFSET = 3;
12+
exports.GUTTER_TOOLTIP_LEFT_OFFSET = GUTTER_TOOLTIP_LEFT_OFFSET;
13+
exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET;
14+
1015

1116
/**
1217
* @param {MouseHandler} mouseHandler
@@ -15,7 +20,7 @@ var lang = require("../lib/lang");
1520
function GutterHandler(mouseHandler) {
1621
var editor = mouseHandler.editor;
1722
var gutter = editor.renderer.$gutterLayer;
18-
var tooltip = new GutterTooltip(editor);
23+
var tooltip = new GutterTooltip(editor, true);
1924

2025
mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) {
2126
if (!editor.isFocused() || e.getButton() != 0)
@@ -61,6 +66,8 @@ function GutterHandler(mouseHandler) {
6166
return;
6267

6368
editor.on("mousewheel", hideTooltip);
69+
editor.on("changeSession", hideTooltip);
70+
window.addEventListener("keydown", hideTooltip, true);
6471

6572
if (mouseHandler.$tooltipFollowsMouse) {
6673
moveTooltip(mouseEvent);
@@ -71,20 +78,28 @@ function GutterHandler(mouseHandler) {
7178
var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation");
7279
var rect = gutterElement.getBoundingClientRect();
7380
var style = tooltip.getElement().style;
74-
style.left = rect.right + "px";
75-
style.top = rect.bottom + "px";
81+
style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px";
82+
style.top = (rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET) + "px";
7683
} else {
7784
moveTooltip(mouseEvent);
7885
}
7986
}
8087
}
8188

82-
function hideTooltip() {
89+
function hideTooltip(e) {
90+
// dont close tooltip in case the user wants to copy text from it (Ctrl/Meta + C)
91+
if (e && e.type === "keydown" && (e.ctrlKey || e.metaKey))
92+
return;
93+
// in case mouse moved but is still on the tooltip, dont close it
94+
if (e && e.type === "mouseout" && (!e.relatedTarget || tooltip.getElement().contains(e.relatedTarget)))
95+
return;
8396
if (tooltipTimeout)
8497
tooltipTimeout = clearTimeout(tooltipTimeout);
8598
if (tooltip.isOpen) {
8699
tooltip.hideTooltip();
87100
editor.off("mousewheel", hideTooltip);
101+
editor.off("changeSession", hideTooltip);
102+
window.removeEventListener("keydown", hideTooltip, true);
88103
}
89104
}
90105

@@ -107,34 +122,46 @@ function GutterHandler(mouseHandler) {
107122
tooltipTimeout = null;
108123
if (mouseEvent && !mouseHandler.isMousePressed)
109124
showTooltip();
110-
else
111-
hideTooltip();
112125
}, 50);
113126
});
114127

115128
event.addListener(editor.renderer.$gutter, "mouseout", function(e) {
116129
mouseEvent = null;
117-
if (!tooltip.isOpen || tooltipTimeout)
130+
if (!tooltip.isOpen)
118131
return;
119132

120133
tooltipTimeout = setTimeout(function() {
121134
tooltipTimeout = null;
122-
hideTooltip();
135+
hideTooltip(e);
123136
}, 50);
124137
}, editor);
125-
126-
editor.on("changeSession", hideTooltip);
127-
editor.on("input", hideTooltip);
128138
}
129139

130140
exports.GutterHandler = GutterHandler;
131141

132142
class GutterTooltip extends Tooltip {
133-
constructor(editor) {
143+
constructor(editor, isHover = false) {
134144
super(editor.container);
135145
this.editor = editor;
136146
/**@type {Number | Undefined}*/
137147
this.visibleTooltipRow;
148+
var el = this.getElement();
149+
el.setAttribute("role", "tooltip");
150+
el.style.pointerEvents = "auto";
151+
if (isHover) {
152+
this.onMouseOut = this.onMouseOut.bind(this);
153+
el.addEventListener("mouseout", this.onMouseOut);
154+
}
155+
}
156+
157+
// handler needed to hide tooltip after mouse hovers from tooltip to editor
158+
onMouseOut(e) {
159+
if (!this.isOpen) return;
160+
161+
if (!e.relatedTarget || this.getElement().contains(e.relatedTarget)) return;
162+
163+
if (e && e.currentTarget.contains(e.relatedTarget)) return;
164+
this.hideTooltip();
138165
}
139166

140167
setPosition(x, y) {

src/mouse/default_gutter_handler_test.js

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ var Editor = require("../editor").Editor;
1111
var Mode = require("../mode/java").Mode;
1212
var VirtualRenderer = require("../virtual_renderer").VirtualRenderer;
1313
var assert = require("../test/assertions");
14+
var user = require("../test/user");
15+
const {GUTTER_TOOLTIP_LEFT_OFFSET, GUTTER_TOOLTIP_TOP_OFFSET} = require("./default_gutter_handler");
1416
var MouseEvent = function(type, opts){
1517
var e = document.createEvent("MouseEvents");
1618
e.initMouseEvent(/click|wheel/.test(type) ? type : "mouse" + type,
@@ -36,8 +38,7 @@ module.exports = {
3638
editor = this.editor;
3739
next();
3840
},
39-
40-
"test: gutter error tooltip" : function() {
41+
"test: gutter error tooltip" : function(done) {
4142
var editor = this.editor;
4243
var value = "";
4344

@@ -56,11 +57,12 @@ module.exports = {
5657
// Wait for the tooltip to appear after its timeout.
5758
setTimeout(function() {
5859
editor.renderer.$loop._flush();
59-
var tooltip = editor.container.querySelector(".ace_tooltip");
60+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
6061
assert.ok(/error test/.test(tooltip.textContent));
61-
}, 100);
62+
done();
63+
}, 100);
6264
},
63-
"test: gutter security tooltip" : function() {
65+
"test: gutter security tooltip" : function(done) {
6466
var editor = this.editor;
6567
var value = "";
6668

@@ -79,11 +81,12 @@ module.exports = {
7981
// Wait for the tooltip to appear after its timeout.
8082
setTimeout(function() {
8183
editor.renderer.$loop._flush();
82-
var tooltip = editor.container.querySelector(".ace_tooltip");
84+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
8385
assert.ok(/security finding test/.test(tooltip.textContent));
84-
}, 100);
86+
done();
87+
}, 100);
8588
},
86-
"test: gutter warning tooltip" : function() {
89+
"test: gutter warning tooltip" : function(done) {
8790
var editor = this.editor;
8891
var value = "";
8992

@@ -102,11 +105,12 @@ module.exports = {
102105
// Wait for the tooltip to appear after its timeout.
103106
setTimeout(function() {
104107
editor.renderer.$loop._flush();
105-
var tooltip = editor.container.querySelector(".ace_tooltip");
108+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
106109
assert.ok(/warning test/.test(tooltip.textContent));
107-
}, 100);
110+
done();
111+
}, 100);
108112
},
109-
"test: gutter info tooltip" : function() {
113+
"test: gutter info tooltip" : function(done) {
110114
var editor = this.editor;
111115
var value = "";
112116

@@ -125,11 +129,12 @@ module.exports = {
125129
// Wait for the tooltip to appear after its timeout.
126130
setTimeout(function() {
127131
editor.renderer.$loop._flush();
128-
var tooltip = editor.container.querySelector(".ace_tooltip");
132+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
129133
assert.ok(/info test/.test(tooltip.textContent));
130-
}, 100);
134+
done();
135+
}, 100);
131136
},
132-
"test: gutter hint tooltip" : function() {
137+
"test: gutter hint tooltip" : function(done) {
133138
var editor = this.editor;
134139
var value = "";
135140

@@ -148,9 +153,10 @@ module.exports = {
148153
// Wait for the tooltip to appear after its timeout.
149154
setTimeout(function() {
150155
editor.renderer.$loop._flush();
151-
var tooltip = editor.container.querySelector(".ace_tooltip");
156+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
152157
assert.ok(/suggestion test/.test(tooltip.textContent));
153-
}, 100);
158+
done();
159+
}, 100);
154160
},
155161
"test: gutter svg icons" : function() {
156162
var editor = this.editor;
@@ -169,7 +175,7 @@ module.exports = {
169175
var annotation = line.children[2].firstChild;
170176
assert.ok(/ace_icon_svg/.test(annotation.className));
171177
},
172-
"test: error show up in fold" : function() {
178+
"test: error show up in fold" : function(done) {
173179
var editor = this.editor;
174180
var value = "x {" + "\n".repeat(50) + "}";
175181
value = value.repeat(50);
@@ -200,11 +206,12 @@ module.exports = {
200206
// Wait for the tooltip to appear after its timeout.
201207
setTimeout(function() {
202208
editor.renderer.$loop._flush();
203-
var tooltip = editor.container.querySelector(".ace_tooltip");
209+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
204210
assert.ok(/error in folded/.test(tooltip.textContent));
205-
}, 100);
211+
done();
212+
}, 50);
206213
},
207-
"test: security show up in fold" : function() {
214+
"test: security show up in fold" : function(done) {
208215
var editor = this.editor;
209216
var value = "x {" + "\n".repeat(50) + "}";
210217
value = value.repeat(50);
@@ -235,11 +242,12 @@ module.exports = {
235242
// Wait for the tooltip to appear after its timeout.
236243
setTimeout(function() {
237244
editor.renderer.$loop._flush();
238-
var tooltip = editor.container.querySelector(".ace_tooltip");
245+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
239246
assert.ok(/security finding in folded/.test(tooltip.textContent));
240-
}, 100);
247+
done();
248+
}, 100);
241249
},
242-
"test: warning show up in fold" : function() {
250+
"test: warning show up in fold" : function(done) {
243251
var editor = this.editor;
244252
var value = "x {" + "\n".repeat(50) + "}";
245253
value = value.repeat(50);
@@ -270,9 +278,10 @@ module.exports = {
270278
// Wait for the tooltip to appear after its timeout.
271279
setTimeout(function() {
272280
editor.renderer.$loop._flush();
273-
var tooltip = editor.container.querySelector(".ace_tooltip");
281+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
274282
assert.ok(/warning in folded/.test(tooltip.textContent));
275-
}, 100);
283+
done();
284+
}, 100);
276285
},
277286
"test: info not show up in fold" : function() {
278287
var editor = this.editor;
@@ -396,12 +405,12 @@ module.exports = {
396405
// Wait for the tooltip to appear after its timeout.
397406
setTimeout(function() {
398407
editor.renderer.$loop._flush();
399-
var tooltip = editor.container.querySelector(".ace_tooltip");
408+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
400409
assert.ok(/error test/.test(tooltip.textContent));
401-
assert.equal(tooltip.style.left, `${rect.right}px`);
402-
assert.equal(tooltip.style.top, `${rect.bottom}px`);
410+
assert.equal(tooltip.style.left, `${rect.right - GUTTER_TOOLTIP_LEFT_OFFSET}px`);
411+
assert.equal(tooltip.style.top, `${rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET}px`);
403412
done();
404-
}, 100);
413+
}, 100);
405414
},
406415
"test: gutter tooltip should properly display special characters (\" ' & <)" : function(done) {
407416
var editor = this.editor;
@@ -422,12 +431,43 @@ module.exports = {
422431
// Wait for the tooltip to appear after its timeout.
423432
setTimeout(function() {
424433
editor.renderer.$loop._flush();
425-
var tooltip = editor.container.querySelector(".ace_tooltip");
434+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
426435
assert.ok(/special characters " ' & </.test(tooltip.textContent));
427436
done();
428-
}, 100);
437+
}, 100);
438+
},
439+
"test: gutter hover tooltip should remain open when pressing ctrl key combination" : function(done) {
440+
var editor = this.editor;
441+
var value = "";
442+
443+
editor.session.setMode(new Mode());
444+
editor.setValue(value, -1);
445+
editor.session.setAnnotations([{row: 0, column: 0, text: "error test", type: "error"}]);
446+
editor.renderer.$loop._flush();
447+
448+
var lines = editor.renderer.$gutterLayer.$lines;
449+
var annotation = lines.cells[0].element;
450+
assert.ok(/ace_error/.test(annotation.className));
451+
452+
var rect = annotation.getBoundingClientRect();
453+
annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top}));
454+
455+
// Wait for the tooltip to appear after its timeout.
456+
setTimeout(function () {
457+
editor.renderer.$loop._flush();
458+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
459+
assert.ok(/error test/.test(tooltip.textContent));
460+
user.type("Ctrl-C");
461+
tooltip = editor.container.querySelector(".ace_gutter-tooltip");
462+
assert.ok(/error test/.test(tooltip.textContent));
463+
// also verify if it closes when presses another key
464+
user.type("Escape");
465+
tooltip = editor.container.querySelector(".ace_gutter-tooltip");
466+
assert.strictEqual(tooltip, undefined);
467+
done();
468+
}, 100);
429469
},
430-
470+
431471
tearDown : function() {
432472
this.editor.destroy();
433473
document.body.removeChild(this.editor.container);

0 commit comments

Comments
 (0)