[IMP] web: keyboard navigation between searchbar and renderer

This change allows direct keyboard navigation between the current action
searchbar and renderer.

Focus can now be toggled using DOWN arrow when focusing the searchbar
or the UP arrow when focusing the top cell/record of a kanban or a list.
This commit is contained in:
Julien Mougenot
2019-08-16 09:03:49 +00:00
parent 0aea29da8d
commit bec84c10a1
7 changed files with 44 additions and 10 deletions
@@ -559,15 +559,19 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
ev.data.callback(state || {});
},
/**
* Called mainly from the control panel when the focus should be given to
* the controller
* Called either from the control panel to focus the controller
* or from the view to focus the search bar
*
* @private
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
switch (ev.data.direction) {
case 'down' :
case 'up':
ev.stopPropagation();
this._controlPanel.focusSearchBar();
break;
case 'down':
ev.stopPropagation();
this.giveFocus();
break;
@@ -629,7 +633,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
_onSwitchView: function (ev) {
ev.data.controllerID = this.controllerID;
},
});
return AbstractController;
@@ -54,6 +54,14 @@ var ControlPanelController = mvc.Controller.extend({
exportState: function () {
return this.model.exportState();
},
/**
* Called by the abstract controller to give focus to the searchbar
*/
focusSearchBar: function () {
if (this.renderer.searchBar) {
this.renderer.searchBar.focus();
}
},
/**
* Compute the search related values that will be used to fetch data.
*
@@ -221,6 +221,14 @@ var SearchBar = Widget.extend({
this._focusFollowing();
e.preventDefault();
break;
case $.ui.keyCode.DOWN:
// if the searchbar dropdown is closed, try to focus the renderer
const $dropdown = this.$('.o_searchview_autocomplete:visible');
if (!$dropdown.length) {
this.trigger_up('navigation_move', { direction: 'down' });
e.preventDefault();
}
break;
case $.ui.keyCode.BACKSPACE:
if (this.$input.val() === '') {
this.trigger_up('facet_removed');
@@ -262,13 +262,17 @@ var KanbanRenderer = BasicRenderer.extend({
}
},
/**
* Tries to give focus to the previous card, and returns true if successful
*
* @private
* @param {DOMElement} currentColumn
* @returns {boolean}
*/
_focusOnPreviousCard: function (currentCardElement) {
var previousCard = currentCardElement.previousElementSibling;
if (previousCard && previousCard.classList.contains("o_kanban_record")) { //previous element might be column title
previousCard.focus();
return true;
}
},
/**
@@ -611,7 +615,10 @@ var KanbanRenderer = BasicRenderer.extend({
e.preventDefault();
break;
case $.ui.keyCode.UP:
this._focusOnPreviousCard(e.currentTarget);
const previousFocused = this._focusOnPreviousCard(e.currentTarget);
if (!previousFocused) {
this.trigger_up('navigation_move', { direction: 'up' });
}
e.stopPropagation();
e.preventDefault();
break;
@@ -1155,6 +1155,10 @@ ListRenderer.include({
*/
_onNavigationMove: function (ev) {
var self = this;
// Don't stop the propagation when navigating up while not editing any row
if (this.currentRow === null && ev.data.direction === 'up') {
return;
}
ev.stopPropagation(); // stop the event, the action is done by this renderer
switch (ev.data.direction) {
case 'previous':
@@ -1108,6 +1108,9 @@ var ListRenderer = BasicRenderer.extend({
$cell.closest('tbody').addClass('o_keyboard_navigation');
colIndex = this.currentColIndex || $cell.index();
$futureCell = this._findConnectedCell($cell, 'prev', colIndex);
if (!$futureCell) {
this.trigger_up('navigation_move', { direction: 'up' });
}
break;
case $.ui.keyCode.DOWN:
ev.preventDefault();
+6 -5
View File
@@ -5883,14 +5883,15 @@ QUnit.module('Views', {
},
});
assert.ok(document.activeElement.classList.contains('o_searchview_input'), 'default focus should be in search view');
// switch focus to the create button in tests while it works on live
$('.o_list_button_add').focus();
assert.ok(document.activeElement.classList.contains('o_list_button_add'),
'focus should now be in create button');
assert.ok(document.activeElement.classList.contains('o_searchview_input'),
'default focus should be in search view');
await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
assert.strictEqual(document.activeElement.tagName, 'INPUT',
'focus should now be on the record selector');
await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
assert.ok(document.activeElement.classList.contains('o_searchview_input'),
'focus should have come back to the search view');
await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
assert.strictEqual(document.activeElement.tagName, 'INPUT',
'focus should now be in first row input');