Skip to content

Commit 8037400

Browse files
authored
feat(gutter): added keyboard handling for experimental feature custom widgets (#5796)
* feat(gutter-icon): added keyboard handling for experimental feature custom widgets * merged nearest fold and custom widget methods to fold lane widget mehtod
1 parent 6fde745 commit 8037400

File tree

4 files changed

+213
-43
lines changed

4 files changed

+213
-43
lines changed

src/keyboard/gutter_handler.js

+106-41
Original file line numberDiff line numberDiff line change
@@ -54,37 +54,31 @@ class GutterKeyboardHandler {
5454
/** @this {GutterKeyboardHandler} */
5555
function () {
5656
var index = this.$rowToRowIndex(this.gutterLayer.$cursorCell.row);
57-
var nearestFoldIndex = this.$findNearestFoldWidget(index);
57+
var nearestFoldLaneWidgetIndex = this.$findNearestFoldLaneWidget(index);
5858
var nearestAnnotationIndex = this.$findNearestAnnotation(index);
5959

60-
if (nearestFoldIndex === null && nearestAnnotationIndex === null) return;
60+
if (nearestFoldLaneWidgetIndex === null && nearestAnnotationIndex === null) return;
6161

62-
if (nearestFoldIndex === null && nearestAnnotationIndex !== null) {
63-
this.activeRowIndex = nearestAnnotationIndex;
64-
this.activeLane = "annotation";
65-
this.$focusAnnotation(this.activeRowIndex);
66-
return;
67-
}
62+
var futureActiveRowIndex = this.$findClosestNumber(nearestFoldLaneWidgetIndex, nearestAnnotationIndex, index);
6863

69-
if (nearestFoldIndex !== null && nearestAnnotationIndex === null) {
70-
this.activeRowIndex = nearestFoldIndex;
64+
if (futureActiveRowIndex === nearestFoldLaneWidgetIndex) {
7165
this.activeLane = "fold";
72-
this.$focusFoldWidget(this.activeRowIndex);
73-
return;
66+
this.activeRowIndex = nearestFoldLaneWidgetIndex;
67+
if(this.$isCustomWidgetVisible(nearestFoldLaneWidgetIndex)){
68+
this.$focusCustomWidget(this.activeRowIndex);
69+
return;
70+
}
71+
else {
72+
this.$focusFoldWidget(this.activeRowIndex);
73+
return;
74+
}
7475
}
75-
76-
if (Math.abs(nearestAnnotationIndex - index) < Math.abs(nearestFoldIndex - index)) {
76+
else {
7777
this.activeRowIndex = nearestAnnotationIndex;
7878
this.activeLane = "annotation";
7979
this.$focusAnnotation(this.activeRowIndex);
8080
return;
8181
}
82-
else {
83-
this.activeRowIndex = nearestFoldIndex;
84-
this.activeLane = "fold";
85-
this.$focusFoldWidget(this.activeRowIndex);
86-
return;
87-
}
8882
}.bind(this), 10);
8983
return;
9084
}
@@ -164,22 +158,26 @@ class GutterKeyboardHandler {
164158

165159
switch (this.activeLane) {
166160
case "fold":
167-
if (this.gutterLayer.session.foldWidgets[this.$rowIndexToRow(this.activeRowIndex)] === 'start') {
168-
var rowFoldingWidget = this.$rowIndexToRow(this.activeRowIndex);
161+
var row = this.$rowIndexToRow(this.activeRowIndex);
162+
var customWidget = this.editor.session.$gutterCustomWidgets[row];
163+
if (customWidget) {
164+
if (customWidget.callbacks && customWidget.callbacks.onClick) {
165+
customWidget.callbacks.onClick(e, row);
166+
}
167+
}
168+
else if (this.gutterLayer.session.foldWidgets[row] === 'start') {
169169
this.editor.session.onFoldWidgetClick(this.$rowIndexToRow(this.activeRowIndex), e);
170-
171170
// After folding, check that the right fold widget is still in focus.
172171
// If not (e.g. folding close to bottom of doc), put right widget in focus.
173172
setTimeout(
174173
/** @this {GutterKeyboardHandler} */
175174
function () {
176-
if (this.$rowIndexToRow(this.activeRowIndex) !== rowFoldingWidget) {
175+
if (this.$rowIndexToRow(this.activeRowIndex) !== row) {
177176
this.$blurFoldWidget(this.activeRowIndex);
178-
this.activeRowIndex = this.$rowToRowIndex(rowFoldingWidget);
177+
this.activeRowIndex = this.$rowToRowIndex(row);
179178
this.$focusFoldWidget(this.activeRowIndex);
180179
}
181180
}.bind(this), 10);
182-
183181
break;
184182
} else if (this.gutterLayer.session.foldWidgets[this.$rowIndexToRow(this.activeRowIndex)] === 'end') {
185183
/* TO DO: deal with 'end' fold widgets */
@@ -205,6 +203,7 @@ class GutterKeyboardHandler {
205203
switch (this.activeLane){
206204
case "fold":
207205
this.$blurFoldWidget(this.activeRowIndex);
206+
this.$blurCustomWidget(this.activeRowIndex);
208207
break;
209208

210209
case "annotation":
@@ -225,6 +224,12 @@ class GutterKeyboardHandler {
225224
return isRowFullyVisible && isIconVisible;
226225
}
227226

227+
$isCustomWidgetVisible(index) {
228+
var isRowFullyVisible = this.editor.isRowFullyVisible(this.$rowIndexToRow(index));
229+
var isIconVisible = !!this.$getCustomWidget(index);
230+
return isRowFullyVisible && isIconVisible;
231+
}
232+
228233
$isAnnotationVisible(index) {
229234
var isRowFullyVisible = this.editor.isRowFullyVisible(this.$rowIndexToRow(index));
230235
var isIconVisible = this.$getAnnotation(index).style.display !== "none";
@@ -237,22 +242,37 @@ class GutterKeyboardHandler {
237242
return element.childNodes[1];
238243
}
239244

245+
$getCustomWidget(index) {
246+
var cell = this.lines.get(index);
247+
var element = cell.element;
248+
return element.childNodes[3];
249+
}
250+
240251
$getAnnotation(index) {
241252
var cell = this.lines.get(index);
242253
var element = cell.element;
243254
return element.childNodes[2];
244255
}
245256

246-
// Given an index, find the nearest index with a foldwidget
247-
$findNearestFoldWidget(index) {
257+
// Given an index, find the nearest index with a widget in fold lane
258+
$findNearestFoldLaneWidget(index) {
259+
// If custom widget exists at index, return index
260+
if (this.$isCustomWidgetVisible(index))
261+
return index;
262+
248263
// If fold widget exists at index, return index.
249264
if (this.$isFoldWidgetVisible(index))
250265
return index;
251266

252-
// else, find the nearest index with fold widget within viewport.
267+
// else, find the nearest index with widget within viewport.
253268
var i = 0;
254269
while (index - i > 0 || index + i < this.lines.getLength() - 1){
255270
i++;
271+
if (index - i >= 0 && this.$isCustomWidgetVisible(index - i))
272+
return index - i;
273+
274+
if (index + i <= this.lines.getLength() - 1 && this.$isCustomWidgetVisible(index + i))
275+
return index + i;
256276

257277
if (index - i >= 0 && this.$isFoldWidgetVisible(index - i))
258278
return index - i;
@@ -261,7 +281,7 @@ class GutterKeyboardHandler {
261281
return index + i;
262282
}
263283

264-
// If there are no fold widgets within the viewport, return null.
284+
// If there are no widgets within the viewport, return null.
265285
return null;
266286
}
267287

@@ -297,6 +317,17 @@ class GutterKeyboardHandler {
297317
foldWidget.focus();
298318
}
299319

320+
$focusCustomWidget(index) {
321+
if (index == null)
322+
return;
323+
324+
var customWidget = this.$getCustomWidget(index);
325+
if (customWidget) {
326+
customWidget.classList.add(this.editor.renderer.keyboardFocusClassName);
327+
customWidget.focus();
328+
}
329+
}
330+
300331
$focusAnnotation(index) {
301332
if (index == null)
302333
return;
@@ -314,6 +345,14 @@ class GutterKeyboardHandler {
314345
foldWidget.blur();
315346
}
316347

348+
$blurCustomWidget(index) {
349+
var customWidget = this.$getCustomWidget(index);
350+
if (customWidget) {
351+
customWidget.classList.remove(this.editor.renderer.keyboardFocusClassName);
352+
customWidget.blur();
353+
}
354+
}
355+
317356
$blurAnnotation(index) {
318357
var annotation = this.$getAnnotation(index);
319358

@@ -327,10 +366,16 @@ class GutterKeyboardHandler {
327366
while (index > 0){
328367
index--;
329368

330-
if (this.$isFoldWidgetVisible(index)){
369+
if (this.$isFoldWidgetVisible(index) || this.$isCustomWidgetVisible(index)){
331370
this.$blurFoldWidget(this.activeRowIndex);
371+
this.$blurCustomWidget(this.activeRowIndex);
332372
this.activeRowIndex = index;
333-
this.$focusFoldWidget(this.activeRowIndex);
373+
if (this.$isFoldWidgetVisible(index)) {
374+
this.$focusFoldWidget(this.activeRowIndex);
375+
}
376+
else {
377+
this.$focusCustomWidget(this.activeRowIndex);
378+
}
334379
return;
335380
}
336381
}
@@ -343,10 +388,16 @@ class GutterKeyboardHandler {
343388
while (index < this.lines.getLength() - 1){
344389
index++;
345390

346-
if (this.$isFoldWidgetVisible(index)){
391+
if (this.$isFoldWidgetVisible(index) || this.$isCustomWidgetVisible(index)){
347392
this.$blurFoldWidget(this.activeRowIndex);
393+
this.$blurCustomWidget(this.activeRowIndex);
348394
this.activeRowIndex = index;
349-
this.$focusFoldWidget(this.activeRowIndex);
395+
if (this.$isFoldWidgetVisible(index)) {
396+
this.$focusFoldWidget(this.activeRowIndex);
397+
}
398+
else {
399+
this.$focusCustomWidget(this.activeRowIndex);
400+
}
350401
return;
351402
}
352403
}
@@ -385,6 +436,13 @@ class GutterKeyboardHandler {
385436
return;
386437
}
387438

439+
$findClosestNumber(num1, num2, target) {
440+
if (num1 === null) return num2;
441+
if (num2 === null) return num1;
442+
443+
return (Math.abs(target - num1) <= Math.abs(target - num2)) ? num1 : num2;
444+
}
445+
388446
$switchLane(desinationLane){
389447
switch (desinationLane) {
390448
case "annotation":
@@ -395,22 +453,29 @@ class GutterKeyboardHandler {
395453
this.activeLane = "annotation";
396454

397455
this.$blurFoldWidget(this.activeRowIndex);
456+
this.$blurCustomWidget(this.activeRowIndex);
398457
this.activeRowIndex = annotationIndex;
399458
this.$focusAnnotation(this.activeRowIndex);
400459

401460
break;
402461

403462
case "fold":
404-
if (this.activeLane === "fold") {break;}
405-
var foldWidgetIndex = this.$findNearestFoldWidget(this.activeRowIndex);
406-
if (foldWidgetIndex == null) {break;}
463+
if (this.activeLane === "fold") {break;}
464+
var foldLaneWidgetIndex = this.$findNearestFoldLaneWidget(this.activeRowIndex);
465+
if (foldLaneWidgetIndex === null) {break;}
407466

408-
this.activeLane = "fold";
467+
this.activeLane = "fold";
409468

410-
this.$blurAnnotation(this.activeRowIndex);
411-
this.activeRowIndex = foldWidgetIndex;
469+
this.$blurAnnotation(this.activeRowIndex);
470+
471+
this.activeRowIndex = foldLaneWidgetIndex;
472+
473+
if (this.$isCustomWidgetVisible(foldLaneWidgetIndex)) {
474+
this.$focusCustomWidget(this.activeRowIndex);
475+
}
476+
else {
412477
this.$focusFoldWidget(this.activeRowIndex);
413-
478+
}
414479
break;
415480
}
416481
return;

src/keyboard/gutter_handler_test.js

+105
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,111 @@ module.exports = {
337337
}, 20);
338338
}, 20);
339339
},
340+
"test: switching lanes with the custom widget should work" : function(done) {
341+
var editor = this.editor;
342+
var value = "x {" + "\n".repeat(50) + "}\n";
343+
value = value.repeat(50);
344+
editor.session.setMode(new Mode());
345+
editor.setOption("enableKeyboardAccessibility", true);
346+
editor.setValue(value, -1);
347+
editor.session.setAnnotations([
348+
{row: 1, column: 0, text: "error test", type: "error"},
349+
{row: 2, column: 0, text: "warning test", type: "warning"}
350+
]);
351+
editor.renderer.$loop._flush();
352+
353+
var lines = editor.renderer.$gutterLayer.$lines;
354+
355+
// Set focus to the gutter div.
356+
editor.renderer.$gutter.focus();
357+
assert.equal(document.activeElement, editor.renderer.$gutter);
358+
359+
editor.renderer.$gutterLayer.$addCustomWidget(1, {
360+
className: "ace_users_css",
361+
label: "Open_label",
362+
title: "Open_title",
363+
});
364+
365+
// Focus on the annotation.
366+
emit(keys["enter"]);
367+
368+
setTimeout(function() {
369+
emit(keys["left"]);
370+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]);
371+
372+
// Click annotation.
373+
emit(keys["enter"]);
374+
375+
setTimeout(function() {
376+
// Check annotation is rendered.
377+
editor.renderer.$loop._flush();
378+
var tooltip = editor.container.querySelector(".ace_gutter-tooltip");
379+
assert.ok(/error test/.test(tooltip.textContent));
380+
381+
// Press escape to dismiss the tooltip.
382+
emit(keys["escape"]);
383+
384+
// Switch lane move to custom widget
385+
emit(keys["right"]);
386+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[3]);
387+
388+
// Move back to the annotations, focus should be on the annotation on line 1.
389+
emit(keys["left"]);
390+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[2]);
391+
done();
392+
}, 20);
393+
}, 20);
394+
}, "test: moving up and down to custom widget and checking onclick callback as well" : function(done) {
395+
var editor = this.editor;
396+
var value = "\n x {" + "\n".repeat(5) + "}\n";
397+
value = value.repeat(50);
398+
editor.session.setMode(new Mode());
399+
editor.setValue(value, -1);
400+
editor.setOption("enableKeyboardAccessibility", true);
401+
editor.renderer.$loop._flush();
402+
403+
var lines = editor.renderer.$gutterLayer.$lines;
404+
405+
// Set focus to the gutter div.
406+
editor.renderer.$gutter.focus();
407+
assert.equal(document.activeElement, editor.renderer.$gutter);
408+
409+
assert.equal(lines.cells[2].element.textContent, "3");
410+
411+
let firstCallbackCalledCount=0;
412+
const firstCallback = (e) =>{
413+
firstCallbackCalledCount++;
414+
e.stopPropagation();
415+
};
416+
417+
editor.renderer.$gutterLayer.$addCustomWidget(2, {
418+
className: "ace_users_css",
419+
label: "Open_label",
420+
title: "Open_title",
421+
callbacks: {
422+
onClick: firstCallback
423+
}
424+
});
425+
426+
// Focus on the fold widgets.
427+
emit(keys["enter"]);
428+
429+
setTimeout(function() {
430+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]);
431+
432+
// Move down to the custom widget.
433+
emit(keys["down"]);
434+
assert.equal(document.activeElement, lines.cells[2].element.childNodes[3]);
435+
436+
emit(keys["enter"]);
437+
assert.equal(firstCallbackCalledCount,1);
438+
439+
// Move up to the previous fold widget.
440+
emit(keys["up"]);
441+
assert.equal(document.activeElement, lines.cells[1].element.childNodes[1]);
442+
done();
443+
}, 20);
444+
},
340445

341446
tearDown : function() {
342447
this.editor.destroy();

src/layer/gutter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ class Gutter{
553553
cell.text = rowText;
554554

555555
// If there are no annotations or fold widgets in the gutter cell, hide it from assistive tech.
556-
if (annotationNode.style.display === "none" && foldWidget.style.display === "none")
556+
if (annotationNode.style.display === "none" && foldWidget.style.display === "none" && !customWidgetAttributes)
557557
cell.element.setAttribute("aria-hidden", true);
558558
else
559559
cell.element.setAttribute("aria-hidden", false);

0 commit comments

Comments
 (0)