Skip to content

Commit 5f2face

Browse files
authoredMay 5, 2023
feat: Add gutter controls to keyboard accessibility mode (#5146)
Adds operating the fold controls and annotations in the gutter to the keyboard accessibility mode. When tabbing through the page, you can focus on the gutter div. When pressing enter, you enter the gutter and can use the arrow keys to navigate through the controls in the gutter. Press enter to interact with the controls. Press escape to set focus back to the gutter div and be able to TAB through the page again.
1 parent b6799c1 commit 5f2face

12 files changed

+914
-119
lines changed
 

‎.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
prompt: true,
3030
XMLHttpRequest: true,
3131
localStorage: true,
32+
KeyboardEvent: true,
3233
},
3334
"rules": {
3435
curly: 0,

‎src/css/editor.css.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,13 @@ module.exports = `
120120
background-repeat: no-repeat;
121121
}
122122
123-
.ace_gutter-cell_svg-icons .ace_icon_svg {
123+
.ace_gutter-cell_svg-icons .ace_gutter_annotation {
124124
margin-left: -14px;
125125
float: left;
126126
}
127127
128-
.ace_gutter-cell .ace_icon {
129-
margin-left: -18px;
128+
.ace_gutter-cell .ace_gutter_annotation {
129+
margin-left: -19px;
130130
float: left;
131131
}
132132

‎src/edit_session/folding.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var Range = require("../range").Range;
44
var FoldLine = require("./fold_line").FoldLine;
55
var Fold = require("./fold").Fold;
66
var TokenIterator = require("../token_iterator").TokenIterator;
7+
var MouseEvent = require("../mouse/mouse_event").MouseEvent;
78

89
function Folding() {
910
/*
@@ -755,7 +756,9 @@ function Folding() {
755756
};
756757

757758
this.onFoldWidgetClick = function(row, e) {
758-
e = e.domEvent;
759+
if (e instanceof MouseEvent)
760+
e = e.domEvent;
761+
759762
var options = {
760763
children: e.shiftKey,
761764
all: e.ctrlKey || e.metaKey,

‎src/editor.js

+41-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var defaultCommands = require("./commands/default_commands").commands;
1717
var config = require("./config");
1818
var TokenIterator = require("./token_iterator").TokenIterator;
1919
var LineWidgets = require("./line_widgets").LineWidgets;
20+
var GutterKeyboardHandler = require("./keyboard/gutter_handler").GutterKeyboardHandler;
2021

2122
var clipboard = require("./clipboard");
2223
var keys = require('./lib/keys');
@@ -2874,34 +2875,68 @@ config.defineOptions(Editor.prototype, "editor", {
28742875

28752876
var focusOnEnterKeyup = function (e) {
28762877
if (e.target == this.renderer.content && e.keyCode === keys['enter']){
2877-
e.stopPropagation();
28782878
e.preventDefault();
2879+
var row = this.getCursorPosition().row;
2880+
2881+
if (!this.isRowVisible(row))
2882+
this.scrollToLine(row, true, true);
2883+
28792884
this.focus();
28802885
}
28812886
};
28822887

2883-
var keyboardFocusClassName = "ace_keyboard-focus";
2888+
var gutterKeyboardHandler;
28842889

28852890
// Prevent focus to be captured when tabbing through the page. When focus is set to the content div,
28862891
// press Enter key to give focus to Ace and press Esc to again allow to tab through the page.
28872892
if (value){
2893+
this.keyboardFocusClassName = "ace_keyboard-focus";
2894+
28882895
this.textInput.getElement().setAttribute("tabindex", -1);
28892896
this.renderer.content.setAttribute("tabindex", 0);
2890-
this.renderer.content.classList.add(keyboardFocusClassName);
2897+
this.renderer.content.setAttribute("role", "group");
2898+
this.renderer.content.setAttribute("aria-roledescription", "editor");
2899+
this.renderer.content.classList.add(this.keyboardFocusClassName);
28912900
this.renderer.content.setAttribute("aria-label",
2892-
"Editor, press Enter key to start editing, press Escape key to exit"
2901+
"Editor content, press Enter to start editing, press Escape to exit"
28932902
);
28942903

28952904
this.renderer.content.addEventListener("keyup", focusOnEnterKeyup.bind(this));
28962905
this.commands.addCommand(blurCommand);
2906+
2907+
this.renderer.$gutter.setAttribute("tabindex", 0);
2908+
this.renderer.$gutter.setAttribute("aria-hidden", false);
2909+
this.renderer.$gutter.setAttribute("role", "group");
2910+
this.renderer.$gutter.setAttribute("aria-roledescription", "editor");
2911+
this.renderer.$gutter.setAttribute("aria-label",
2912+
"Editor gutter, press Enter to interact with controls using arrow keys, press Escape to exit"
2913+
);
2914+
this.renderer.$gutter.classList.add(this.keyboardFocusClassName);
2915+
2916+
if (!gutterKeyboardHandler)
2917+
gutterKeyboardHandler = new GutterKeyboardHandler(this);
2918+
2919+
gutterKeyboardHandler.addListener();
28972920
} else {
28982921
this.textInput.getElement().setAttribute("tabindex", 0);
28992922
this.renderer.content.setAttribute("tabindex", -1);
2900-
this.renderer.content.classList.remove(keyboardFocusClassName);
2901-
this.renderer.content.setAttribute("aria-label", "");
2923+
this.renderer.content.removeAttribute("role");
2924+
this.renderer.content.removeAttribute("aria-roledescription");
2925+
this.renderer.content.classList.remove(this.keyboardFocusClassName);
2926+
this.renderer.content.removeAttribute("aria-label");
29022927

29032928
this.renderer.content.removeEventListener("keyup", focusOnEnterKeyup.bind(this));
29042929
this.commands.removeCommand(blurCommand);
2930+
2931+
this.renderer.$gutter.setAttribute("tabindex", -1);
2932+
this.renderer.$gutter.setAttribute("aria-hidden", true);
2933+
this.renderer.$gutter.removeAttribute("role");
2934+
this.renderer.$gutter.removeAttribute("aria-roledescription");
2935+
this.renderer.$gutter.removeAttribute("aria-label");
2936+
this.renderer.$gutter.classList.remove(this.keyboardFocusClassName);
2937+
2938+
if (gutterKeyboardHandler)
2939+
gutterKeyboardHandler.removeListener();
29052940
}
29062941
},
29072942
initialValue: false

‎src/keyboard/gutter_handler.js

+426
Large diffs are not rendered by default.

‎src/keyboard/gutter_handler_test.js

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
if (typeof process !== "undefined") {
2+
require("amd-loader");
3+
require("../test/mockdom");
4+
}
5+
6+
var keys = require('../lib/keys');
7+
8+
"use strict";
9+
10+
require("../multi_select");
11+
require("../theme/textmate");
12+
var Editor = require("../editor").Editor;
13+
var Mode = require("../mode/java").Mode;
14+
var VirtualRenderer = require("../virtual_renderer").VirtualRenderer;
15+
var assert = require("../test/assertions");
16+
17+
function emit(keyCode) {
18+
var data = {bubbles: true, keyCode};
19+
var event = new KeyboardEvent("keydown", data);
20+
21+
var el = document.activeElement;
22+
el.dispatchEvent(event);
23+
}
24+
25+
module.exports = {
26+
setUp : function(done) {
27+
this.editor = new Editor(new VirtualRenderer());
28+
this.editor.container.style.position = "absolute";
29+
this.editor.container.style.height = "500px";
30+
this.editor.container.style.width = "500px";
31+
this.editor.container.style.left = "50px";
32+
this.editor.container.style.top = "10px";
33+
document.body.appendChild(this.editor.container);
34+
done();
35+
},
36+
"test: keyboard code folding: basic functionality" : function(done) {
37+
var editor = this.editor;
38+
var value = "x {" + "\n".repeat(50) + "}\n";
39+
value = value.repeat(50);
40+
editor.session.setMode(new Mode());
41+
editor.setValue(value, -1);
42+
editor.setOption("enableKeyboardAccessibility", true);
43+
editor.renderer.$loop._flush();
44+
45+
var lines = editor.renderer.$gutterLayer.$lines;
46+
var toggler = lines.cells[0].element.children[1];
47+
48+
// Set focus to the gutter div.
49+
editor.renderer.$gutter.focus();
50+
assert.equal(document.activeElement, editor.renderer.$gutter);
51+
52+
// Focus on the fold widget.
53+
emit(keys["enter"]);
54+
55+
setTimeout(function() {
56+
assert.equal(document.activeElement, lines.cells[0].element.childNodes[1]);
57+
58+
// Click the fold widget.
59+
emit(keys["enter"]);
60+
61+
setTimeout(function() {
62+
// Check that code is folded.
63+
editor.renderer.$loop._flush();
64+
assert.ok(/ace_closed/.test(toggler.className));
65+
assert.equal(lines.cells[1].element.textContent, "52");
66+
67+
// After escape focus should be back to the gutter.
68+
emit(keys["escape"]);
69+
assert.equal(document.activeElement, editor.renderer.$gutter);
70+
71+
done();
72+
}, 20);
73+
}, 20);
74+
},
75+
"test: keyboard code folding: multiple folds" : function(done) {
76+
var editor = this.editor;
77+
var value = "\n x {" + "\n".repeat(5) + "}\n";
78+
value = value.repeat(50);
79+
editor.session.setMode(new Mode());
80+
editor.setValue(value, -1);
81+
editor.setOption("enableKeyboardAccessibility", true);
82+
editor.renderer.$loop._flush();
83+
84+
var lines = editor.renderer.$gutterLayer.$lines;
85+
86+
// Set focus to the gutter div.
87+
editor.renderer.$gutter.focus();
88+
assert.equal(document.activeElement, editor.renderer.$gutter);
89+
90+
assert.equal(lines.cells[2].element.textContent, "3");
91+
92+
// Focus on the fold widgets.
93+
emit(keys["enter"]);
94+
95+
setTimeout(function() {
96+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]);
97+
98+
// Click the first fold widget.
99+
emit(keys["enter"]);
100+
101+
setTimeout(function() {
102+
// Check that code is folded.
103+
editor.renderer.$loop._flush();
104+
assert.equal(lines.cells[2].element.textContent, "8");
105+
106+
// Move to the next fold widget.
107+
emit(keys["down"]);
108+
assert.equal(document.activeElement, lines.cells[3].element.childNodes[1]);
109+
assert.equal(lines.cells[4].element.textContent, "10");
110+
111+
// Click the fold widget.
112+
emit(keys["enter"]);
113+
114+
setTimeout(function() {
115+
// Check that code is folded.
116+
assert.equal(lines.cells[4].element.textContent, "15");
117+
118+
// Move back up one fold widget.
119+
emit(keys["up"]);
120+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]);
121+
122+
done();
123+
}, 20);
124+
}, 20);
125+
}, 20);
126+
},
127+
"test: keyboard annotation: basic functionality" : function(done) {
128+
var editor = this.editor;
129+
var value = "x {" + "\n".repeat(50) + "}\n";
130+
value = value.repeat(50);
131+
editor.session.setMode(new Mode());
132+
editor.setOption("enableKeyboardAccessibility", true);
133+
editor.setValue(value, -1);
134+
editor.session.setAnnotations([{row: 0, column: 0, text: "error test", type: "error"}]);
135+
editor.renderer.$loop._flush();
136+
137+
var lines = editor.renderer.$gutterLayer.$lines;
138+
139+
// Set focus to the gutter div.
140+
editor.renderer.$gutter.focus();
141+
assert.equal(document.activeElement, editor.renderer.$gutter);
142+
143+
// Focus on the annotation.
144+
emit(keys["enter"]);
145+
146+
setTimeout(function() {
147+
emit(keys["left"]);
148+
assert.equal(document.activeElement, lines.cells[0].element.childNodes[2]);
149+
150+
// Click annotation.
151+
emit(keys["enter"]);
152+
153+
setTimeout(function() {
154+
// Check annotation is rendered.
155+
editor.renderer.$loop._flush();
156+
var tooltip = editor.container.querySelector(".ace_tooltip");
157+
assert.ok(/error test/.test(tooltip.textContent));
158+
159+
// Press escape to dismiss the tooltip.
160+
emit(keys["escape"]);
161+
162+
// After escape again focus should be back to the gutter.
163+
emit(keys["escape"]);
164+
assert.equal(document.activeElement, editor.renderer.$gutter);
165+
166+
done();
167+
}, 20);
168+
}, 20);
169+
},"test: keyboard annotation: multiple annotations" : function(done) {
170+
var editor = this.editor;
171+
var value = "x {" + "\n".repeat(50) + "}\n";
172+
value = value.repeat(50);
173+
editor.session.setMode(new Mode());
174+
editor.setOption("enableKeyboardAccessibility", true);
175+
editor.setValue(value, -1);
176+
editor.session.setAnnotations([
177+
{row: 1, column: 0, text: "error test", type: "error"},
178+
{row: 2, column: 0, text: "warning test", type: "warning"}
179+
]);
180+
editor.renderer.$loop._flush();
181+
182+
var lines = editor.renderer.$gutterLayer.$lines;
183+
184+
// Set focus to the gutter div.
185+
editor.renderer.$gutter.focus();
186+
assert.equal(document.activeElement, editor.renderer.$gutter);
187+
188+
// Focus on the annotation.
189+
emit(keys["enter"]);
190+
191+
setTimeout(function() {
192+
emit(keys["left"]);
193+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]);
194+
195+
// Click annotation.
196+
emit(keys["enter"]);
197+
198+
setTimeout(function() {
199+
// Check annotation is rendered.
200+
editor.renderer.$loop._flush();
201+
var tooltip = editor.container.querySelector(".ace_tooltip");
202+
assert.ok(/error test/.test(tooltip.textContent));
203+
204+
// Press escape to dismiss the tooltip.
205+
emit(keys["escape"]);
206+
207+
// Press down to move to next annotation.
208+
emit(keys["down"]);
209+
assert.equal(document.activeElement, lines.cells[2].element.childNodes[2]);
210+
211+
// Click annotation.
212+
emit(keys["enter"]);
213+
214+
setTimeout(function() {
215+
// Check annotation is rendered.
216+
editor.renderer.$loop._flush();
217+
var tooltip = editor.container.querySelector(".ace_tooltip");
218+
assert.ok(/warning test/.test(tooltip.textContent));
219+
220+
// Press escape to dismiss the tooltip.
221+
emit(keys["escape"]);
222+
223+
// Move back up one annotation.
224+
emit(keys["up"]);
225+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]);
226+
227+
// Move back to the folds, focus should be on the fold on line 1.
228+
emit(keys["right"]);
229+
assert.equal(document.activeElement, lines.cells[0].element.childNodes[1]);
230+
231+
done();
232+
}, 20);
233+
}, 20);
234+
}, 20);
235+
},"test: keyboard annotation: no folds" : function(done) {
236+
var editor = this.editor;
237+
var value = "x\n";
238+
value = value.repeat(50);
239+
editor.session.setMode(new Mode());
240+
editor.setOption("enableKeyboardAccessibility", true);
241+
editor.setValue(value, -1);
242+
editor.session.setAnnotations([{row: 1, column: 0, text: "error test", type: "error"}]);
243+
editor.renderer.$loop._flush();
244+
245+
var lines = editor.renderer.$gutterLayer.$lines;
246+
247+
// Set focus to the gutter div.
248+
editor.renderer.$gutter.focus();
249+
assert.equal(document.activeElement, editor.renderer.$gutter);
250+
251+
// Focus on gutter interaction.
252+
emit(keys["enter"]);
253+
254+
setTimeout(function() {
255+
// Focus should be on the annotation directly.
256+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]);
257+
done();
258+
}, 20);
259+
},
260+
261+
tearDown : function() {
262+
this.editor.destroy();
263+
document.body.removeChild(this.editor.container);
264+
}
265+
266+
};
267+
268+
if (typeof module !== "undefined" && module === require.main) {
269+
require("asyncjs").test.testcase(module.exports).exec();
270+
}

‎src/keyboard/textinput.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,21 @@ var TextInput = function(parentNode, host) {
6161
}
6262
if (options.role) {
6363
text.setAttribute("role", options.role);
64-
}
64+
}
65+
};
66+
this.setAriaLabel = function() {
67+
var row;
68+
if (!host.session)
69+
row = 0;
70+
else
71+
row = host.session.selection.cursor.row;
72+
73+
text.setAttribute("aria-roledescription", "editor");
74+
text.setAttribute("aria-label", `Cursor at row ${row + 1}`);
6575
};
76+
6677
this.setAriaOptions({role: "textbox"});
78+
this.setAriaLabel();
6779

6880
event.addListener(text, "blur", function(e) {
6981
if (ignoreFocusEvents) return;
@@ -92,6 +104,9 @@ var TextInput = function(parentNode, host) {
92104
}, host);
93105
this.$focusScroll = false;
94106
this.focus = function() {
107+
// On focusing on the textarea, read active row to assistive tech.
108+
this.setAriaLabel();
109+
95110
if (tempStyle || HAS_FOCUS_ARGS || this.$focusScroll == "browser")
96111
return text.focus({ preventScroll: true });
97112

‎src/layer/gutter.js

+45-16
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ class Gutter{
279279
var textNode = element.childNodes[0];
280280
var foldWidget = element.childNodes[1];
281281
var annotationNode = element.childNodes[2];
282+
var annotationIconNode = annotationNode.firstChild;
282283

283284
var firstLineNumber = session.$firstLineNumber;
284285

@@ -293,6 +294,10 @@ class Gutter{
293294
var className = this.$useSvgGutterIcons ? "ace_gutter-cell_svg-icons " : "ace_gutter-cell ";
294295
var iconClassName = this.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon";
295296

297+
var rowText = (gutterRenderer
298+
? gutterRenderer.getText(session, row)
299+
: row + firstLineNumber).toString();
300+
296301
if (this.$highlightGutterLine) {
297302
if (row == this.$cursorRow || (fold && row < this.$cursorRow && row >= foldStart && this.$cursorRow <= fold.end.row)) {
298303
className += "ace_gutter-active-line ";
@@ -352,46 +357,67 @@ class Gutter{
352357

353358
dom.setStyle(foldWidget.style, "height", lineHeight);
354359
dom.setStyle(foldWidget.style, "display", "inline-block");
360+
361+
// Set a11y properties.
362+
foldWidget.setAttribute("role", "button");
363+
foldWidget.setAttribute("tabindex", "-1");
364+
var fold = session.getFoldLine(rowText - 1);
365+
if (fold) {
366+
foldWidget.setAttribute("aria-label", `Unfold rows ${rowText} to ${fold.end.row + 1}`);
367+
foldWidget.setAttribute("title", "Unfold code");
368+
}
369+
else {
370+
foldWidget.setAttribute("aria-label", `Fold at row ${rowText}`);
371+
foldWidget.setAttribute("title", "Fold code");
372+
}
355373
} else {
356374
if (foldWidget) {
357375
dom.setStyle(foldWidget.style, "display", "none");
376+
foldWidget.setAttribute("tabindex", "0");
377+
foldWidget.removeAttribute("role");
378+
foldWidget.removeAttribute("aria-label");
358379
}
359380
}
360381

361382
if (annotationInFold && this.$showFoldedAnnotations){
362-
annotationNode.className = iconClassName;
363-
annotationNode.className += foldAnnotationClass;
383+
annotationNode.className = "ace_gutter_annotation";
384+
annotationIconNode.className = iconClassName;
385+
annotationIconNode.className += foldAnnotationClass;
364386

365-
dom.setStyle(annotationNode.style, "height", lineHeight);
387+
dom.setStyle(annotationIconNode.style, "height", lineHeight);
366388
dom.setStyle(annotationNode.style, "display", "block");
389+
dom.setStyle(annotationNode.style, "height", lineHeight);
390+
annotationNode.setAttribute("aria-label", `Read annotations row ${rowText}`);
391+
annotationNode.setAttribute("tabindex", "-1");
367392
}
368393
else if (this.$annotations[row]){
369-
annotationNode.className = iconClassName;
394+
annotationNode.className = "ace_gutter_annotation";
395+
annotationIconNode.className = iconClassName;
370396

371397
if (this.$useSvgGutterIcons)
372-
annotationNode.className += this.$annotations[row].className;
398+
annotationIconNode.className += this.$annotations[row].className;
373399
else
374400
element.classList.add(this.$annotations[row].className.replace(" ", ""));
375401

376-
dom.setStyle(annotationNode.style, "height", lineHeight);
402+
dom.setStyle(annotationIconNode.style, "height", lineHeight);
377403
dom.setStyle(annotationNode.style, "display", "block");
404+
dom.setStyle(annotationNode.style, "height", lineHeight);
405+
annotationNode.setAttribute("aria-label", `Read annotations row ${rowText}`);
406+
annotationNode.setAttribute("tabindex", "-1");
378407
}
379408
else {
380409
dom.setStyle(annotationNode.style, "display", "none");
410+
annotationNode.removeAttribute("aria-label");
411+
annotationNode.setAttribute("tabindex", "0");
381412
}
382-
383-
var text = (gutterRenderer
384-
? gutterRenderer.getText(session, row)
385-
: row + firstLineNumber).toString();
386-
387-
if (text !== textNode.data) {
388-
textNode.data = text;
389-
}
390-
413+
if (rowText !== textNode.data) {
414+
textNode.data = rowText;
415+
}
416+
391417
dom.setStyle(cell.element.style, "height", this.$lines.computeLineHeight(row, config, session) + "px");
392418
dom.setStyle(cell.element.style, "top", this.$lines.computeLineTop(row, config, session) + "px");
393419

394-
cell.text = text;
420+
cell.text = rowText;
395421
return cell;
396422
}
397423

@@ -464,6 +490,9 @@ function onCreateCell(element) {
464490

465491
var annotationNode = dom.createElement("span");
466492
element.appendChild(annotationNode);
493+
494+
var annotationIconNode = dom.createElement("span");
495+
annotationNode.appendChild(annotationIconNode);
467496

468497
return element;
469498
}

‎src/layer/text.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,10 @@ class Text {
363363
if (!this.$textToken[token.type]) {
364364
var classes = "ace_" + token.type.replace(/\./g, " ace_");
365365
var span = this.dom.createElement("span");
366-
if (token.type == "fold")
366+
if (token.type == "fold"){
367367
span.style.width = (token.value.length * this.config.characterWidth) + "px";
368+
span.setAttribute("title", "Unfold code");
369+
}
368370

369371
span.className = classes;
370372
span.appendChild(valueFragment);

‎src/mouse/default_gutter_handler.js

+101-88
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var Tooltip = require("../tooltip").Tooltip;
66
function GutterHandler(mouseHandler) {
77
var editor = mouseHandler.editor;
88
var gutter = editor.renderer.$gutterLayer;
9-
var tooltip = new GutterTooltip(editor.container);
9+
var tooltip = new GutterTooltip(editor);
1010

1111
mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) {
1212
if (!editor.isFocused() || e.getButton() != 0)
@@ -33,62 +33,10 @@ function GutterHandler(mouseHandler) {
3333
return e.preventDefault();
3434
});
3535

36-
37-
var tooltipTimeout, mouseEvent, tooltipContent;
38-
39-
var annotationLabels = {
40-
error: {singular: "error", plural: "errors"},
41-
warning: {singular: "warning", plural: "warnings"},
42-
info: {singular: "information message", plural: "information messages"}
43-
};
36+
var tooltipTimeout, mouseEvent;
4437

4538
function showTooltip() {
4639
var row = mouseEvent.getDocumentPosition().row;
47-
var annotationsInRow = gutter.$annotations[row];
48-
var annotation;
49-
50-
if (annotationsInRow)
51-
annotation = {text: Array.from(annotationsInRow.text), type: Array.from(annotationsInRow.type)};
52-
else
53-
annotation = {text: [], type: []};
54-
55-
// If the tooltip is for a row which has a closed fold, check whether there are
56-
// annotations in the folded lines. If so, add a summary to the list of annotations.
57-
var fold = gutter.session.getFoldLine(row);
58-
if (fold && gutter.$showFoldedAnnotations){
59-
var annotationsInFold = {error: [], warning: [], info: []};
60-
var mostSevereAnnotationInFoldType;
61-
62-
for (var i = row + 1; i <= fold.end.row; i++){
63-
if (!gutter.$annotations[i])
64-
continue;
65-
66-
for (var j = 0; j < gutter.$annotations[i].text.length; j++) {
67-
var annotationType = gutter.$annotations[i].type[j];
68-
annotationsInFold[annotationType].push(gutter.$annotations[i].text[j]);
69-
70-
if (annotationType === "error"){
71-
mostSevereAnnotationInFoldType = "error_fold";
72-
continue;
73-
}
74-
75-
if (annotationType === "warning"){
76-
mostSevereAnnotationInFoldType = "warning_fold";
77-
continue;
78-
}
79-
}
80-
}
81-
82-
if (mostSevereAnnotationInFoldType === "error_fold" || mostSevereAnnotationInFoldType === "warning_fold"){
83-
var summaryFoldedAnnotations = `${annotationsToSummaryString(annotationsInFold)} in folded code.`;
84-
85-
annotation.text.push(summaryFoldedAnnotations);
86-
annotation.type.push(mostSevereAnnotationInFoldType);
87-
}
88-
}
89-
90-
if (annotation.text.length === 0)
91-
return hideTooltip();
9240

9341
var maxRow = editor.session.getLength();
9442
if (row == maxRow) {
@@ -98,25 +46,8 @@ function GutterHandler(mouseHandler) {
9846
return hideTooltip();
9947
}
10048

101-
var annotationMessages = {error: [], warning: [], info: []};
102-
var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon";
49+
tooltip.showTooltip(row);
10350

104-
// Construct the contents of the tooltip.
105-
for (var i = 0; i < annotation.text.length; i++) {
106-
var line = `<span class='ace_${annotation.type[i]} ${iconClassName}' aria-label='${annotationLabels[annotation.type[i].replace("_fold","")].singular}' role=img> </span> ${annotation.text[i]}`;
107-
annotationMessages[annotation.type[i].replace("_fold","")].push(line);
108-
}
109-
tooltipContent = [].concat(annotationMessages.error, annotationMessages.warning, annotationMessages.info).join("<br>");
110-
111-
tooltip.setHtml(tooltipContent);
112-
tooltip.setClassName("ace_gutter-tooltip");
113-
tooltip.$element.setAttribute("aria-live", "polite");
114-
115-
if (!tooltip.isOpen) {
116-
tooltip.setTheme(editor.renderer.theme);
117-
}
118-
tooltip.show();
119-
editor._signal("showGutterTooltip", tooltip);
12051
editor.on("mousewheel", hideTooltip);
12152

12253
if (mouseHandler.$tooltipFollowsMouse) {
@@ -133,25 +64,13 @@ function GutterHandler(mouseHandler) {
13364
function hideTooltip() {
13465
if (tooltipTimeout)
13566
tooltipTimeout = clearTimeout(tooltipTimeout);
136-
if (tooltipContent) {
67+
if (tooltip.isOpen) {
13768
tooltip.hide();
138-
tooltipContent = null;
13969
editor._signal("hideGutterTooltip", tooltip);
14070
editor.off("mousewheel", hideTooltip);
14171
}
14272
}
14373

144-
function annotationsToSummaryString(annotations) {
145-
const summary = [];
146-
const annotationTypes = ['error', 'warning', 'info'];
147-
for (const annotationType of annotationTypes) {
148-
if (!annotations[annotationType].length) continue;
149-
const label = annotations[annotationType].length === 1 ? annotationLabels[annotationType].singular : annotationLabels[annotationType].plural;
150-
summary.push(`${annotations[annotationType].length} ${label}`);
151-
}
152-
return summary.join(", ");
153-
}
154-
15574
function moveTooltip(e) {
15675
tooltip.setPosition(e.x, e.y);
15776
}
@@ -161,7 +80,7 @@ function GutterHandler(mouseHandler) {
16180
if (dom.hasCssClass(target, "ace_fold-widget"))
16281
return hideTooltip();
16382

164-
if (tooltipContent && mouseHandler.$tooltipFollowsMouse)
83+
if (tooltip.isOpen && mouseHandler.$tooltipFollowsMouse)
16584
moveTooltip(e);
16685

16786
mouseEvent = e;
@@ -178,7 +97,7 @@ function GutterHandler(mouseHandler) {
17897

17998
event.addListener(editor.renderer.$gutter, "mouseout", function(e) {
18099
mouseEvent = null;
181-
if (!tooltipContent || tooltipTimeout)
100+
if (!tooltip.isOpen || tooltipTimeout)
182101
return;
183102

184103
tooltipTimeout = setTimeout(function() {
@@ -190,7 +109,14 @@ function GutterHandler(mouseHandler) {
190109
editor.on("changeSession", hideTooltip);
191110
}
192111

112+
exports.GutterHandler = GutterHandler;
113+
193114
class GutterTooltip extends Tooltip {
115+
constructor(editor) {
116+
super(editor.container);
117+
this.editor = editor;
118+
}
119+
194120
setPosition(x, y) {
195121
var windowWidth = window.innerWidth || document.documentElement.clientWidth;
196122
var windowHeight = window.innerHeight || document.documentElement.clientHeight;
@@ -206,7 +132,94 @@ class GutterTooltip extends Tooltip {
206132
}
207133
Tooltip.prototype.setPosition.call(this, x, y);
208134
}
135+
136+
static get annotationLabels() { return {
137+
error: {singular: "error", plural: "errors"},
138+
warning: {singular: "warning", plural: "warnings"},
139+
info: {singular: "information message", plural: "information messages"}
140+
};
141+
}
142+
143+
showTooltip(row) {
144+
var gutter = this.editor.renderer.$gutterLayer;
145+
var annotationsInRow = gutter.$annotations[row];
146+
var annotation;
147+
148+
if (annotationsInRow)
149+
annotation = {text: Array.from(annotationsInRow.text), type: Array.from(annotationsInRow.type)};
150+
else
151+
annotation = {text: [], type: []};
152+
153+
// If the tooltip is for a row which has a closed fold, check whether there are
154+
// annotations in the folded lines. If so, add a summary to the list of annotations.
155+
var fold = gutter.session.getFoldLine(row);
156+
if (fold && gutter.$showFoldedAnnotations){
157+
var annotationsInFold = {error: [], warning: [], info: []};
158+
var mostSevereAnnotationInFoldType;
159+
160+
for (var i = row + 1; i <= fold.end.row; i++){
161+
if (!gutter.$annotations[i])
162+
continue;
163+
164+
for (var j = 0; j < gutter.$annotations[i].text.length; j++) {
165+
var annotationType = gutter.$annotations[i].type[j];
166+
annotationsInFold[annotationType].push(gutter.$annotations[i].text[j]);
167+
168+
if (annotationType === "error"){
169+
mostSevereAnnotationInFoldType = "error_fold";
170+
continue;
171+
}
172+
173+
if (annotationType === "warning"){
174+
mostSevereAnnotationInFoldType = "warning_fold";
175+
continue;
176+
}
177+
}
178+
}
179+
180+
if (mostSevereAnnotationInFoldType === "error_fold" || mostSevereAnnotationInFoldType === "warning_fold"){
181+
var summaryFoldedAnnotations = `${GutterTooltip.annotationsToSummaryString(annotationsInFold)} in folded code.`;
182+
183+
annotation.text.push(summaryFoldedAnnotations);
184+
annotation.type.push(mostSevereAnnotationInFoldType);
185+
}
186+
}
187+
188+
if (annotation.text.length === 0)
189+
return this.hide();
190+
191+
var annotationMessages = {error: [], warning: [], info: []};
192+
var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon";
193+
194+
// Construct the contents of the tooltip.
195+
for (var i = 0; i < annotation.text.length; i++) {
196+
var line = `<span class='ace_${annotation.type[i]} ${iconClassName}' aria-label='${GutterTooltip.annotationLabels[annotation.type[i].replace("_fold","")].singular}' role=img> </span> ${annotation.text[i]}`;
197+
annotationMessages[annotation.type[i].replace("_fold","")].push(line);
198+
}
199+
var tooltipContent = [].concat(annotationMessages.error, annotationMessages.warning, annotationMessages.info).join("<br>");
200+
201+
this.setHtml(tooltipContent);
202+
this.setClassName("ace_gutter-tooltip");
203+
this.$element.setAttribute("aria-live", "polite");
204+
205+
if (!this.isOpen) {
206+
this.setTheme(this.editor.renderer.theme);
207+
}
208+
209+
this.editor._signal("showGutterTooltip", this);
210+
this.show();
211+
}
209212

213+
static annotationsToSummaryString(annotations) {
214+
const summary = [];
215+
const annotationTypes = ['error', 'warning', 'info'];
216+
for (const annotationType of annotationTypes) {
217+
if (!annotations[annotationType].length) continue;
218+
const label = annotations[annotationType].length === 1 ? GutterTooltip.annotationLabels[annotationType].singular : GutterTooltip.annotationLabels[annotationType].plural;
219+
summary.push(`${annotations[annotationType].length} ${label}`);
220+
}
221+
return summary.join(", ");
222+
}
210223
}
211224

212-
exports.GutterHandler = GutterHandler;
225+
exports.GutterTooltip = GutterTooltip;

‎src/mouse/default_gutter_handler_test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ module.exports = {
120120
var line = lines.cells[0].element;
121121
assert.ok(/ace_gutter-cell_svg-icons/.test(line.className));
122122

123-
var annotation = line.children[2];
123+
var annotation = line.children[2].firstChild;
124124
assert.ok(/ace_icon_svg/.test(annotation.className));
125125
},
126126
"test: error show up in fold" : function() {
@@ -145,7 +145,7 @@ module.exports = {
145145
assert.equal(lines.cells[1].element.textContent, "51");
146146

147147
// Annotation node should have fold class.
148-
var annotation = lines.cells[0].element.children[2];
148+
var annotation = lines.cells[0].element.children[2].firstChild;
149149
assert.ok(/ace_error_fold/.test(annotation.className));
150150

151151
var rect = annotation.getBoundingClientRect();
@@ -180,7 +180,7 @@ module.exports = {
180180
assert.equal(lines.cells[1].element.textContent, "51");
181181

182182
// Annotation node should have fold class.
183-
var annotation = lines.cells[0].element.children[2];
183+
var annotation = lines.cells[0].element.children[2].firstChild;
184184
assert.ok(/ace_warning_fold/.test(annotation.className));
185185

186186
var rect = annotation.getBoundingClientRect();

‎src/test/all_browser.js

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var testNames = [
4040
"ace/keyboard/vim_test",
4141
"ace/keyboard/vim_ace_test",
4242
"ace/keyboard/sublime_test",
43+
"ace/keyboard/gutter_handler_test",
4344
"ace/layer/text_test",
4445
"ace/lib/event_emitter_test",
4546
"ace/mode/coffee/parser_test",

0 commit comments

Comments
 (0)
Please sign in to comment.