From bc1bba446a57e6b91c76c90a9b2a1d33772480c1 Mon Sep 17 00:00:00 2001 From: Vincent Schippefilt Date: Tue, 20 Mar 2018 12:48:46 +0100 Subject: [PATCH] [IMP] web: keyboard navigation in kanban view 1. Added the ability to navigate within the kanban view using UP, DOWN, LEFT and RIGHT 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 first card of the kanban 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 card of the kanban, then UP and DOWN keys moves the focus from card to card in the same column or if it is not a grouped view from left to right. 6. In grouped view, LEFT and RIGHT keys moves the focus to the next column 7. When on a card, hitting ENTER opens the view of that card. Not included in this commit 1. Memorize the position on the kanban when opening an card, than restoring when going back to that kanban --- .../src/js/views/abstract_controller.js | 3 + .../src/js/views/kanban/kanban_controller.js | 13 ++++ .../src/js/views/kanban/kanban_record.js | 11 +++- .../src/js/views/kanban/kanban_renderer.js | 58 +++++++++++++++++ .../web/static/src/less/kanban_dashboard.less | 8 +++ addons/web/static/src/less/kanban_view.less | 7 ++ addons/web/static/src/xml/base.xml | 14 ++-- addons/web/static/tests/views/kanban_tests.js | 64 +++++++++++++++++++ 8 files changed, 170 insertions(+), 8 deletions(-) diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index deb7c37bb59..7f82d550425 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -107,6 +107,9 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { */ on_attach_callback: function () { this.renderer.on_attach_callback(); + if (this.searchView) { + this.searchView.on_attach_callback(); + } }, /** * Called each time the controller is detached from the DOM. diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 8a779becdea..5f758953658 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -58,6 +58,7 @@ var KanbanController = BasicController.extend({ if (this.hasButtons && this.is_action_enabled('create')) { this.$buttons = $(qweb.render('KanbanView.buttons', {widget: this})); this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this)); + this.$buttons.on('keydown',this._onButtonsKeyDown.bind(this)); this._updateButtons(); this.$buttons.appendTo($node); } @@ -220,6 +221,18 @@ var KanbanController = BasicController.extend({ }); } }, + /** + * Moves the focus from the controller buttons to the first kanban record + * + * @private + * @param {jQueryEvent} e + */ + _onButtonsKeyDown: function (e) { + switch(e.keyCode) { + case $.ui.keyCode.DOWN: + this.$('.o_kanban_record:first').focus(); + } + }, /** * @private * @param {OdooEvent} event diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js index dfe62e3746e..072f4d41f27 100644 --- a/addons/web/static/src/js/views/kanban/kanban_record.js +++ b/addons/web/static/src/js/views/kanban/kanban_record.js @@ -23,6 +23,7 @@ var KanbanRecord = Widget.extend({ events: { 'click .oe_kanban_action': '_onKanbanActionClicked', 'click .o_kanban_manage_toggle_button': '_onManageTogglerClicked', + 'keydown' : '_onKanbanKeyDown', }, /** * @override @@ -283,7 +284,7 @@ var KanbanRecord = Widget.extend({ */ _render: function () { this._replaceElement(this.qweb.render('kanban-box', this.qweb_context)); - this.$el.addClass('o_kanban_record'); + this.$el.addClass('o_kanban_record').attr("tabindex",0); this.$el.data('record', this); if (this.$el.hasClass('oe_kanban_global_click') || this.$el.hasClass('oe_kanban_global_click_edit')) { @@ -504,6 +505,14 @@ var KanbanRecord = Widget.extend({ this.do_warn("Kanban: no action for type : " + type); } }, + _onKanbanKeyDown: function (event) { + switch (event.keyCode) { + case $.ui.keyCode.ENTER: + event.preventDefault(); + this._openRecord(); + break; + } + }, /** * Toggles the configuration panel of the record * diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index 913a4257257..953ffcf7dd7 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -88,6 +88,61 @@ var KanbanRenderer = BasicRenderer.extend({ start_quick_create: '_onStartQuickCreate', quick_create_column_updated: '_onQuickCreateColumnUpdated', }), + events:_.extend({}, BasicRenderer.prototype.events || {}, { + 'keydown .o_kanban_record' : '_onRecordKeyDown' + }), + _focusOnNextCard: function(currentCardElement) { + var nextCard = currentCardElement.nextElementSibling; + if (nextCard) { + nextCard.focus(); + } + }, + _focusOnPrevousCard: function(currentCardElement) { + var previousCard = currentCardElement.previousElementSibling; + if (previousCard && previousCard.classList.contains("o_kanban_record")) { //previous element might be column title + previousCard.focus(); + } + }, + _onRecordKeyDown: function(e) { + switch(e.which) { + case $.ui.keyCode.DOWN: + this._focusOnNextCard(e.currentTarget); + e.stopPropagation(); + break; + case $.ui.keyCode.UP: + this._focusOnPrevousCard(e.currentTarget); + e.stopPropagation(); + break; + case $.ui.keyCode.RIGHT: + var currentColumn = e.currentTarget.parentElement; + var nextColumn = currentColumn.nextElementSibling; + if (nextColumn) { + var allCardsOfNextColumn = nextColumn.getElementsByClassName('o_kanban_record'); + if (allCardsOfNextColumn.length) { + allCardsOfNextColumn[0].focus(); + } + } + else { + this._focusOnNextCard(e.currentTarget); + } + e.stopPropagation(); + break; + case $.ui.keyCode.LEFT: + var currentColumn = e.currentTarget.parentElement; + var previousColumn = currentColumn.previousElementSibling; + if (previousColumn) { + var allCardsOfPreviousColumn = previousColumn.getElementsByClassName('o_kanban_record'); + if (allCardsOfPreviousColumn.length) { + allCardsOfPreviousColumn[0].focus(); + } + } + else { + this._focusOnPrevousCard(e.currentTarget); + } + e.stopPropagation(); + break; + } + }, /** * @override @@ -132,6 +187,9 @@ var KanbanRenderer = BasicRenderer.extend({ addQuickCreate: function () { return this.widgets[0].addQuickCreate(); }, + giveFocus:function() { + this.$('.o_kanban_record:first').focus(); + }, /** * Toggle fold/unfold the Column quick create widget */ diff --git a/addons/web/static/src/less/kanban_dashboard.less b/addons/web/static/src/less/kanban_dashboard.less index adf67f31bf0..da8f0adac72 100644 --- a/addons/web/static/src/less/kanban_dashboard.less +++ b/addons/web/static/src/less/kanban_dashboard.less @@ -43,6 +43,14 @@ margin-bottom: 10px; } + &:focus, &:focus-within { + -webkit-box-shadow: 1px 1px 3px 0px rgba(0,0,0,1); + -moz-box-shadow: 1px 1px 3px 0px rgba(0,0,0,1); + box-shadow: 1px 1px 3px 0px rgba(0,0,0,1); + z-index: 1; // show the shadow on top of the previous & next cards in grouped mode + outline: none; + } + // ------- Generic layout adaptations ------- .container { width: 100%; diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less index b9eec962eaf..f9130469665 100644 --- a/addons/web/static/src/less/kanban_view.less +++ b/addons/web/static/src/less/kanban_view.less @@ -154,6 +154,13 @@ @media (max-width: @screen-xs-max) { padding: @o-kanban-inside-vgutter @o-kanban-inside-hgutter-mobile; } + &:focus, &:focus-within { + -webkit-box-shadow: 3px 3px 10px 0px rgba(0,0,0,1); + -moz-box-shadow: 3px 3px 10px 0px rgba(0,0,0,1); + box-shadow: 3px 3px 10px 0px rgba(0,0,0,1); + z-index: 1; // show the shadow on top of the previous & next cards in grouped mode + outline: none; + } } .o_kanban_quick_create { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 4a1bccc2850..ccd98c10962 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -203,7 +203,7 @@