diff options
| author | Vincent Schippefilt <vsc@odoo.com> | 2018-03-20 12:48:46 +0100 |
|---|---|---|
| committer | Vincent Schippefilt <vsc@odoo.com> | 2018-04-10 09:39:08 +0200 |
| commit | bc1bba446a57e6b91c76c90a9b2a1d33772480c1 (patch) | |
| tree | 0cb534d5748e1a7fba04ea3efdc934badfd095e5 | |
| parent | e0924fd01d8ffaf95b677b10ea0a5627b484a7d6 (diff) | |
[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
| -rw-r--r-- | addons/web/static/src/js/views/abstract_controller.js | 3 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/kanban/kanban_controller.js | 13 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/kanban/kanban_record.js | 11 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/kanban/kanban_renderer.js | 58 | ||||
| -rw-r--r-- | addons/web/static/src/less/kanban_dashboard.less | 8 | ||||
| -rw-r--r-- | addons/web/static/src/less/kanban_view.less | 7 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 14 | ||||
| -rw-r--r-- | 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); } @@ -221,6 +222,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 @@ <button type="button" t-att-accesskey="view.accessKey" t-attf-class="btn btn-icon fa fa-lg #{view.icon} o_cp_switch_#{view.type}" t-att-aria-label="view.type" - t-att-data-view-type="view.type" t-att-title="view.label"/> + t-att-data-view-type="view.type" t-att-title="view.label" tabindex="-1"/> </t> </t> @@ -1033,7 +1033,7 @@ <ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul> </t> <div t-name="SearchView.FilterMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1"> <span class="fa fa-filter"/> Filters <span class="caret"/> </button> <ul class="dropdown-menu o_filters_menu" role="menu"> @@ -1145,7 +1145,7 @@ </select> </t> <div t-name="SearchView.GroupByMenu" class="btn-group hidden-xs o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1"> <span class="fa fa-bars"/> Group By <span class="caret"/> </button> <ul class="dropdown-menu o_group_by_menu" role="menu"> @@ -1162,11 +1162,11 @@ </t> </select></li> <li> - <button type="button" class="btn btn-primary btn-sm o_add_group o_select_group">Apply</button> + <button type="button" class="btn btn-primary btn-sm o_add_group o_select_group" tabindex="-1">Apply</button> </li> </t> <div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1"> <span class="fa fa-star"/> Favorites <span class="caret"/> </button> <ul class="dropdown-menu o_favorites_menu" role="menu"> @@ -1310,8 +1310,8 @@ <t t-set="att_prev" t-value="{'accesskey': 'p'}" /> <t t-set="att_next" t-value="{'accesskey': 'n'}" /> </t> - <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous"/> - <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next"/> + <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous" tabindex="-1"/> + <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next" tabindex="-1"/> </span> </div> diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 437d048dae7..e2fa4bb6bec 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -3339,6 +3339,70 @@ QUnit.module('Views', { testUtils.unpatch(mixins.ParentedMixin); }); + QUnit.test('keyboard navigation on kanban basic rendering', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' + + '<div>' + + '<t t-esc="record.foo.value"/>' + + '<field name="foo"/>' + + '</div>' + + '</t></templates></kanban>', + }); + + var $fisrtCard = kanban.$('.o_kanban_record:first'); + var $secondCard = kanban.$('.o_kanban_record:eq(1)'); + + $fisrtCard.focus(); + assert.strictEqual(document.activeElement, $fisrtCard[0], "the kanban cards are focussable"); + + $fisrtCard.trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); + assert.strictEqual(document.activeElement, $secondCard[0], "the second card should be focussed"); + + $secondCard.trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); + assert.strictEqual(document.activeElement, $fisrtCard[0], "the first card should be focussed"); + kanban.destroy(); + }); + + QUnit.test('keyboard navigation on kanban grouped rendering', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="bar"/>' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates></kanban>', + groupBy: ['bar'], + }); + + var $firstColumnFisrtCard = kanban.$('.o_kanban_record:first'); + var $secondColumnFirstCard = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:first'); + var $secondColumnSecondCard = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(1)'); + + $firstColumnFisrtCard.focus(); + + //RIGHT should select the next column + $firstColumnFisrtCard.trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); + assert.strictEqual(document.activeElement, $secondColumnFirstCard[0], "RIGHT should select the first card of the next column"); + + //DOWN should move up one card + $secondColumnFirstCard.trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN, })); + assert.strictEqual(document.activeElement, $secondColumnSecondCard[0], "DOWN should select the second card of the current column"); + + //LEFT should go back to the first column + $secondColumnSecondCard.trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); + assert.strictEqual(document.activeElement, $firstColumnFisrtCard[0], "LEFT should select the first card of the first column"); + + kanban.destroy(); + }); }); }); |
