diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 8ac11f60537..9dc0ff4c1c8 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -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. diff --git a/addons/web/static/src/js/chrome/search_view.js b/addons/web/static/src/js/chrome/search_view.js index 8ec1eb9ec71..94087c999b6 100644 --- a/addons/web/static/src/js/chrome/search_view.js +++ b/addons/web/static/src/js/chrome/search_view.js @@ -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(); } diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 4fff15d5663..deb7c37bb59 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -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 //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 1a87c3433f6..8a9b3713f15 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -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. * diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 4fb7983d00f..55a6c7a6c94 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -136,6 +136,12 @@ var BasicRenderer = AbstractRenderer.extend({ } }, + /** + * Order to focus to be given to the content of the current view + */ + giveFocus:function() { + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 551fa8102be..3262196fa51 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -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(); diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index e9c9a4d1f76..354e7c8a90b 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -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 diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js index 883206ae4c2..a25dc0150f5 100644 --- a/addons/web/static/src/js/widgets/auto_complete.js +++ b/addons/web/static/src/js/widgets/auto_complete.js @@ -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"; + } }); }); diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index c6d81293fbf..ddc530e540a 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -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;