[IMP] web: keyboard navigation in form view

1. Added the ability to navigate within the List view using UP and
        DOWN keys.
    2. By defaults, the focus should be set on the search input field.
    3. From the search input field, using the DOWN key, the focus goes
        to the list
    4. From the search input field, using TAB key, the focus goes to the
        create button
    5. From the create/import buttons, using TAB or DOWN key goes to the
        first row of the list, then UP and DOWN keys moves the focus from
        item to item in the list.
    6. When on a row, hitting ENTER opens the view of that row.

Also in this commit
    1. When on the first row, hitting the UP key does nothing
    2. When on the last row, hitting the DOWN key does nothing

Not included in this commit
    1. Memorize the position on the list when opening an row, than
        restoring when going back to that list
    2. Navigation on grouped list using the keyboard
This commit is contained in:
Vincent Schippefilt
2018-04-10 09:39:08 +02:00
parent a4d00a56b6
commit e0924fd01d
9 changed files with 140 additions and 2 deletions
@@ -25,6 +25,7 @@ ActionManager.include({
update_filters: '_onUpdateFilters',
search: '_onSearch',
switch_view: '_onSwitchView',
navigation_move: '_onNavigationMove',
}),
//--------------------------------------------------------------------------
@@ -820,6 +821,21 @@ ActionManager.include({
var addedFilters = action.searchView.updateFilters(data.newFilters, data.filtersToRemove);
data.callback(addedFilters);
},
/**
* Called mainly from the control panel when the focus should be given to a controller
*
* @param {OdooEvent} event
* @private
*/
_onNavigationMove : function(event) {
switch(event.data.direction) {
case 'down' :
var currentController = this.getCurrentController().widget;
currentController.giveFocus();
event.stopPropagation();
break;
}
},
/**
* Called when there is a change in the search view, so the current action's
* environment needs to be updated with the new domain, context and groupby.
@@ -248,6 +248,12 @@ var SearchView = Widget.extend({
}
e.preventDefault();
break;
case $.ui.keyCode.DOWN:
if (!this.autocomplete.is_expanded()) {
e.preventDefault();
this.trigger_up('navigation_move', {direction: 'down'});
break;
}
}
},
},
@@ -336,6 +342,9 @@ var SearchView = Widget.extend({
}
return $.when.apply($, menu_defs).then(this.set_default_filters.bind(this));
},
on_attach_callback: function () {
this._focusInput();
},
get_title: function () {
return this.title;
},
@@ -417,6 +426,12 @@ var SearchView = Widget.extend({
if (this.$buttons) {
this.$buttons.toggle(!this.headless && is_visible && this.visible_filters);
}
this._focusInput();
},
/**
* puts the focus on the search input
*/
_focusInput: function () {
if (!config.device.touch && config.device.size_class >= config.device.SIZES.SM) {
this.$('input').focus();
}
@@ -115,6 +115,12 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this.renderer.on_detach_callback();
},
/**
* Gives the focus to the renderer
*/
giveFocus:function(){
this.renderer.giveFocus();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -120,6 +120,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
getSelectedIds: function () {
return [];
},
/**
* Gives the focus to the renderer
*/
giveFocus:function() {
this.renderer.giveFocus();
},
/**
* Returns true iff the given recordID (or the main recordID) is dirty.
*
@@ -136,6 +136,12 @@ var BasicRenderer = AbstractRenderer.extend({
}
},
/**
* Order to focus to be given to the content of the current view
*/
giveFocus:function() {
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -116,6 +116,15 @@ var ListController = BasicController.extend({
if (!this.noLeaf && this.hasButtons) {
this.$buttons = $(qweb.render('ListView.buttons', {widget: this}));
this.$buttons.on('click', '.o_list_button_add', this._onCreateRecord.bind(this));
this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add'));
this.$buttons.find('.o_list_button_add').tooltip({
delay: {show: 200, hide:0},
title: function(){
return qweb.render('CreateButton.tooltip');
},
trigger: 'manual',
});
this.$buttons.on('click', '.o_list_button_discard', this._onDiscard.bind(this));
this.$buttons.appendTo($node);
}
@@ -221,6 +230,33 @@ var ListController = BasicController.extend({
.toggleActive(ids, !archive, this.handle)
.then(this.update.bind(this, {}, {reload: false}));
},
/**
* Assign on the buttons create additionnal behavior to facilitate the work of the users doing input only using the keyboard
*
* @param {jQueryElement} $createButton The create button itself
*/
_assignCreateKeyboardBehavior: function($createButton) {
var self = this;
$createButton.on('keydown', function(e) {
$createButton.tooltip('hide');
switch(e.which) {
case $.ui.keyCode.ENTER:
e.preventDefault();
self._onCreateRecord.apply(self);
break;
case $.ui.keyCode.DOWN:
e.preventDefault();
self.renderer.giveFocus();
break;
case $.ui.keyCode.TAB:
if (!e.shiftKey && (e.target.classList.contains("btn-primary") || e.target.classList.contains("oe_highlight"))) {
e.preventDefault();
$createButton.tooltip('show');
}
break;
}
});
},
/**
* This function is the hook called by the field manager mixin to confirm
* that a record has been saved.
@@ -353,7 +389,9 @@ var ListController = BasicController.extend({
// we prevent the event propagation because we don't want this event to
// trigger a click on the main bus, which would be then caught by the
// list editable renderer and would unselect the newly created row
event.stopPropagation();
if (event) {
event.stopPropagation();
}
var state = this.model.get(this.handle, {raw: true});
if (this.editable && !state.groupedBy.length) {
this._addRecord();
@@ -37,6 +37,9 @@ var ListRenderer = BasicRenderer.extend({
'click thead th.o_column_sortable': '_onSortColumn',
'click .o_group_header': '_onToggleGroup',
'click thead .o_list_record_selector input': '_onToggleSelection',
'keypress thead tr td' : '_onKeyPress',
'keydown tr' : '_onKeyDown',
'keydown thead tr' : '_onKeyDown',
},
/**
* @constructor
@@ -65,7 +68,14 @@ var ListRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Order to focus to be given to the content of the current view
* @override
* @public
*/
giveFocus:function() {
this.$('tbody .o_list_record_selector input:first()').focus();
},
/**
* @override
*/
@@ -716,6 +726,32 @@ var ListRenderer = BasicRenderer.extend({
// Handlers
//--------------------------------------------------------------------------
/**
* Manages the keyboard events on the list. If the list is not editable, when the user navigates to
* a cell using the keyboard, if he presses enter, enter the model represented by the line
*
* @private
* @param {KeyboardEvent} e
*/
_onKeyDown : function(e) {
if (!this.editable) {
switch(e.which) {
case $.ui.keyCode.DOWN:
$(e.currentTarget).next().find('input').focus();
break;
case $.ui.keyCode.UP:
$(e.currentTarget).prev().find('input').focus();
break;
case $.ui.keyCode.ENTER:
e.preventDefault();
var id = $(e.currentTarget).data('id');
if (id) {
this.trigger_up('open_record', {id:id, target: e.target});
}
break;
}
}
},
/**
* @private
* @param {MouseEvent} event
@@ -216,5 +216,8 @@ return Widget.extend({
is_expandable: function () {
return !!this.$('.o-selection-focus .o-expand').length;
},
is_expanded: function() {
return this.$el[0].style.display === "block";
}
});
});
+12
View File
@@ -117,6 +117,18 @@
border-top: none; // Override bootstrap for grouped list views
}
tbody {
tr:focus-within {
background-color: @odoo-form-lightsecondary;
}
tr{
.focussed {
background-color: @odoo-form-lightsecondary;
}
}
}
// Values styling
.o_list_number {
text-align: right;