From 960feafeb80f097174c9b934a682a729a4bc19a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Gonz=C3=A1lez?= Date: Mon, 13 Apr 2020 03:36:19 +0000 Subject: [FIX] web: Get correctly focusable elements on selection widgets When navigating fields using the keyboard, it's not possible to reach most of widgets for selection fields. That is because most of them consist of one main element containing several other ones, one per possible selectable value (e.g. buttons). When their focusable element is computed, the main element is taken, instead of its sub-elements, which causes the field to be skipped when navigating. This commit fixes the above, as well as some missing aria attributes and keyboard events, for the following widgets: - `priority` - `state_selection` - `statusbar` - `radio` opw-231112 closes #58302 closes odoo/odoo#58418 X-original-commit: 2471e7c176c5df0f310f2939f8a6cf6948c0ac0c Signed-off-by: Nicolas Lempereur (nle) --- addons/web/static/src/js/fields/basic_fields.js | 69 +++++++++++++++++++++- .../web/static/src/js/fields/relational_fields.js | 24 ++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src/js/fields') diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index cdb4c000e57..774fb6462fd 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2215,10 +2215,14 @@ var PriorityWidget = AbstractField.extend({ // the current implementation of this widget makes it // only usable for fields of type selection className: "o_priority", + attributes: { + 'role': 'radiogroup', + }, events: { 'mouseover > a': '_onMouseOver', 'mouseout > a': '_onMouseOut', 'click > a': '_onClick', + 'keydown > a': '_onKeydown', }, supportedFieldTypes: ['selection'], @@ -2236,6 +2240,17 @@ var PriorityWidget = AbstractField.extend({ return true; }, + /** + * Returns the currently-checked star, or the first one if no star is + * checked. + * + * @override + */ + getFocusableElement: function () { + var checked = this.$("[aria-checked='true']"); + return checked.length ? checked : this.$("[data-index='1']"); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -2253,8 +2268,9 @@ var PriorityWidget = AbstractField.extend({ }) : 0; this.$el.empty(); this.empty_value = this.field.selection[0][0]; + this.$el.attr('aria-label', this.string); _.each(this.field.selection.slice(1), function (choice, index) { - self.$el.append(self._renderStar('', index_value >= index+1, index+1, choice[1])); + self.$el.append(self._renderStar('', index_value >= index+1, index+1, choice[1], index_value)); }); }, @@ -2265,12 +2281,18 @@ var PriorityWidget = AbstractField.extend({ * @param {boolean} isFull whether the star is a full star or not * @param {integer} index the index of the star in the series * @param {string} tip tooltip for this star's meaning + * @param {integer} indexValue the index of the last full star or 0 * @private */ - _renderStar: function (tag, isFull, index, tip) { + _renderStar: function (tag, isFull, index, tip, indexValue) { + var isChecked = indexValue === index; + var defaultFocus = indexValue === 0 && index === 1; return $(tag) + .attr('role', 'radio') + .attr('aria-checked', isChecked) .attr('title', tip) .attr('aria-label', tip) + .attr('tabindex', isChecked || defaultFocus ? 0 : -1) .attr('data-index', index) .addClass('o_priority_star fa') .toggleClass('fa-star', isFull) @@ -2322,6 +2344,36 @@ var PriorityWidget = AbstractField.extend({ this.$('.o_priority_star').removeClass('fa-star-o').addClass('fa-star'); $(event.currentTarget).nextAll().removeClass('fa-star').addClass('fa-star-o'); }, + + /** + * Runs the default behavior when is pressed over a star + * (the same as if it was clicked); otherwise forwards event to the widget. + * + * @param {KeydownEvent} event + * @private + */ + _onKeydown: function (event) { + if (event.which === $.ui.keyCode.ENTER) { + return; + } + this._super.apply(this, arguments); + }, + + _onNavigationMove: function (ev) { + var $curControl = this.$('a:focus'); + var $nextControl; + if (ev.data.direction === 'right' || ev.data.direction === 'down') { + $nextControl = $curControl.next('a'); + } else if (ev.data.direction === 'left' || ev.data.direction === 'up') { + $nextControl = $curControl.prev('a'); + } + if ($nextControl && $nextControl.length) { + ev.stopPropagation(); + $nextControl.focus(); + return; + } + this._super.apply(this, arguments); + }, }); var AttachmentImage = AbstractField.extend({ @@ -2354,6 +2406,19 @@ var StateSelectionWidget = AbstractField.extend({ }, supportedFieldTypes: ['selection'], + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the drop down button. + * + * @override + */ + getFocusableElement: function () { + return this.$("a[data-toggle='dropdown']"); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 432961bc0bf..c6db91778ec 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -3068,6 +3068,27 @@ var FieldRadio = FieldSelection.extend({ return true; }, + /** + * Returns the currently-checked radio button, or the first one if no radio + * button is checked. + * + * @override + */ + getFocusableElement: function () { + var checked = this.$("[checked='true']"); + return checked.length ? checked : this.$("[data-index='0']"); + }, + + /** + * Associates the 'for' attribute to the radiogroup, instead of the selected + * radio button. + * + * @param {string} id + */ + setIDForLabel: function (id) { + this.$el.attr('id', id); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -3085,11 +3106,14 @@ var FieldRadio = FieldSelection.extend({ currentValue = this.value; } this.$el.empty(); + this.$el.attr('role', 'radiogroup') + .attr('aria-label', this.string); _.each(this.values, function (value, index) { self.$el.append(qweb.render('FieldRadio.button', { checked: value[0] === currentValue, id: self.unique_id + '_' + value[0], index: index, + name: self.unique_id, value: value, })); }); -- cgit v1.2.3