summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js1
-rw-r--r--addons/web/static/src/js/core/dialog.js102
-rw-r--r--addons/web/static/src/js/core/mixins.js1
-rw-r--r--addons/web/static/src/js/core/widget.js10
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js28
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js12
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js38
-rw-r--r--addons/web/static/src/js/services/crash_manager.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js9
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js75
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js67
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js47
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js16
-rw-r--r--addons/web/static/src/xml/base.xml18
-rw-r--r--addons/web/static/src/xml/dialog.xml2
15 files changed, 393 insertions, 35 deletions
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 154df08051a..d312746578e 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -417,6 +417,7 @@ var ActionManager = Widget.extend({
callbacks: [{widget: dialog}, {widget: controller.widget}],
});
widget.renderButtons(dialog.$footer);
+ dialog.rebindButtonBehavior();
return action;
});
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index d5be248b16e..5ba24f57cad 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -18,7 +18,12 @@ var _t = core._t;
**/
var Dialog = Widget.extend({
xmlDependencies: ['/web/static/src/xml/dialog.xml'],
-
+ custom_events: _.extend({}, Widget.prototype.custom_events, {
+ focus_control_button: '_onFocusControlButton',
+ }),
+ events: _.extend({} , Widget.prototype.events, {
+ 'keydown .modal-footer button':'_onFooterButtonKeyDown',
+ }),
/**
* @param {Widget} parent
* @param {Object} [options]
@@ -102,6 +107,9 @@ var Dialog = Widget.extend({
}
this.$el.addClass('modal-body ' + this.dialogClass);
},
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
/**
* @param {Object[]} buttons - @see init
*/
@@ -148,7 +156,14 @@ var Dialog = Widget.extend({
return (handler)? this._opened.then(handler) : this._opened;
},
- open: function () {
+ /**
+ * Show a dialog
+ *
+ * @param {Object} options
+ * @param {boolean} options.shouldFocusButtons if true, put the focus on
+ * the first button primary when the dialog opens
+ */
+ open: function (options) {
$('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened
var self = this;
@@ -157,6 +172,9 @@ var Dialog = Widget.extend({
self.$modal.modal('show');
self._opened.resolve();
});
+ if (options && options.shouldFocusButtons) {
+ self._onFocusControlButton();
+ }
return self;
},
@@ -176,6 +194,8 @@ var Dialog = Widget.extend({
if (this.isDestroyed()) {
return;
}
+ var isFocusSet = this._focusOnClose();
+
this._super();
$('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
@@ -184,11 +204,75 @@ var Dialog = Widget.extend({
this.$modal.remove();
}
- var modals = $('body > .modal').filter(':visible');
- if (modals.length) {
- modals.last().focus();
- // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal
- $('body').addClass('modal-open');
+ if (!isFocusSet) {
+ var modals = $('body > .modal').filter(':visible');
+ if (modals.length) {
+ modals.last().focus();
+ // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal
+ $('body').addClass('modal-open');
+ }
+ }
+ },
+ /**
+ * adds the keydown behavior to the dialogs after external files modifies
+ * its DOM.
+ */
+ rebindButtonBehavior: function () {
+ this.$footer.on('keydown', this._onFooterButtonKeyDown);
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Manages the focus when the dialog closes. The default behavior is to set the focus on the top-most opened popup.
+ * The goal of this function is to be overridden by all children of the dialog class.
+ *
+ * @returns: boolean should return true if the focus has already been set else false.
+ */
+ _focusOnClose: function() {
+ return false;
+ },
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * Moves the focus to the first button primary in the footer of the dialog
+ *
+ * @private
+ * @param {odooEvent} e
+ */
+ _onFocusControlButton: function (e) {
+ if (this.$footer) {
+ if (e) {
+ e.stopPropagation();
+ }
+ this.$footer.find('.btn-primary:visible:first()').focus();
+ }
+ },
+ /**
+ * Manages the TAB key on the buttons. If you the focus is on a primary
+ * button and the users tries to tab to go to the next button, display
+ * a tooltip
+ *
+ * @param {jQueryEvent} e
+ * @private
+ */
+ _onFooterButtonKeyDown: function (e) {
+ switch(e.which) {
+ case $.ui.keyCode.TAB:
+ if (!e.shiftKey && (e.target.classList.contains("btn-primary") || e.target.classList.contains("oe_highlight"))) {
+ e.preventDefault();
+ var $primaryButton = $(e.target);
+ $primaryButton.tooltip({
+ delay: {show: 200, hide:0},
+ title: function(){
+ return QWeb.render('FormButton.tooltip',{title:$primaryButton.text().toUpperCase()});
+ },
+ trigger: 'manual',
+ });
+ $primaryButton.tooltip('show');
+ }
+ break;
}
}
});
@@ -207,7 +291,7 @@ Dialog.alert = function (owner, message, options) {
text: message,
}),
title: _t("Alert"),
- }, options)).open();
+ }, options)).open({shouldFocusButtons:true});
};
// static method to open simple confirm dialog
@@ -232,7 +316,7 @@ Dialog.confirm = function (owner, message, options) {
text: message,
}),
title: _t("Confirmation"),
- }, options)).open();
+ }, options)).open({shouldFocusButtons:true});
};
/**
diff --git a/addons/web/static/src/js/core/mixins.js b/addons/web/static/src/js/core/mixins.js
index e4738fc2f43..fd8e8f93dbe 100644
--- a/addons/web/static/src/js/core/mixins.js
+++ b/addons/web/static/src/js/core/mixins.js
@@ -331,6 +331,7 @@ var EventDispatcherMixin = _.extend({}, ParentedMixin, {
},
trigger_up: function (name, info) {
var event = new OdooEvent(this, name, info);
+ //console.info('event: ', name, info);
this._trigger_up(event);
},
_trigger_up: function (event) {
diff --git a/addons/web/static/src/js/core/widget.js b/addons/web/static/src/js/core/widget.js
index 5e23c3f2c47..bd868596546 100644
--- a/addons/web/static/src/js/core/widget.js
+++ b/addons/web/static/src/js/core/widget.js
@@ -284,6 +284,16 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
return this;
},
+
+ /**
+ * Update the modifiers with the newest value.
+ * Now this.attrs.modifiersValue can be used consistantly even with
+ * conditional modifiers
+ *
+ * @param {Object} modifiers the updated modifiers
+ */
+ updateModifiersValue: function(modifiers) {
+ },
//--------------------------------------------------------------------------
// Private
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 5cc3f1d17d2..36a9f0cb258 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -299,6 +299,34 @@ var AbstractField = Widget.extend({
return this._render() || $.when();
},
+ /**
+ * Remove the invalid class on a field
+ */
+ removeInvalidClass: function () {
+ this.$el.removeClass('o_field_invalid');
+ },
+
+ /**
+ * add the invalid class on a field
+ */
+ setInvalidClass: function () {
+ this.$el.addClass('o_field_invalid');
+ },
+
+ /**
+ * Update the modifiers with the newest value.
+ * Now this.attrs.modifiersValue can be used consistantly even with
+ * conditional modifiers inside field widgets, and without needing new
+ * events or synchronization between the widgets, renderer and controller
+ *
+ * @param {Object || null} modifiers the updated modifiers
+ * @override
+ */
+ updateModifiersValue: function(modifiers) {
+ this.attrs.modifiersValue = modifiers || {};
+ },
+
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index ee9958406c5..ae0da5427ad 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -312,13 +312,21 @@ var InputField = DebouncedField.extend({
this._super.apply(this, arguments);
// the following code only makes sense in edit mode, with an input
- if (this.mode === 'edit') {
+ if (this.mode === 'edit' && ev.data.direction !== 'cancel') {
var input = this.$input[0];
var selecting = (input.selectionEnd !== input.selectionStart);
if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
- || (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
+ || (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
ev.stopPropagation();
}
+ if (ev.data.direction ==='next' && this.attrs.modifiersValue.required) {
+ if (!this.$input.val()){
+ this.setInvalidClass();
+ ev.stopPropagation();
+ } else {
+ this.removeInvalidClass();
+ }
+ }
}
},
});
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index fedaf5c07f6..93c924fad4c 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -682,6 +682,8 @@ var FieldX2Many = AbstractField.extend({
resequence: '_onResequence',
save_line: '_onSaveLine',
toggle_column_order: '_onToggleColumnOrder',
+ activate_next_widget: '_onActiveNextWidget',
+ navigation_move: '_onNavigationMove',
}),
// We need to trigger the reset on every changes to be aware of the parent changes
@@ -797,6 +799,30 @@ var FieldX2Many = AbstractField.extend({
return this._super.apply(this, arguments);
},
+ /**
+ * @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ return (this.mode === 'edit' && this.$input) || this.$el;
+ },
+
+ /**
+ * @override
+ * @param {Object} [options]
+ */
+ activate: function (options) {
+ if (!this.activeActions.create || this.isReadonly || !this.$el.is(":visible")) {
+ return false;
+ }
+ if (this.view.type === 'kanban') {
+ this.$buttons.find(".o-kanban-button-new").focus();
+ }
+ if (this.view.arch.tag === 'tree') {
+ this.renderer.$('.o_field_x2many_list_row_add a').focus();
+ }
+ return true;
+ },
//--------------------------------------------------------------------------
// Private
@@ -1159,6 +1185,16 @@ var FieldX2Many = AbstractField.extend({
_onToggleColumnOrder: function (ev) {
ev.data.field = this.name;
},
+ /*
+ * Move to next widget.
+ *
+ * @private
+ */
+ _onActiveNextWidget: function (e) {
+ e.stopPropagation();
+ this.renderer.unselectRow();
+ this.trigger_up('navigation_move',{direction:'next'});
+ },
});
var FieldOne2Many = FieldX2Many.extend({
@@ -1257,7 +1293,6 @@ var FieldOne2Many = FieldX2Many.extend({
var self = this;
// we don't want interference with the components upstream.
ev.stopPropagation();
-
if (this.editable) {
if (!this.activeActions.create) {
if (ev.data.onFail) {
@@ -1329,7 +1364,6 @@ var FieldMany2Many = FieldX2Many.extend({
_onAddRecord: function (ev) {
var self = this;
ev.stopPropagation();
-
var domain = this.record.getDomain({fieldName: this.name});
new dialogs.SelectCreateDialog(this, {
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index ccc7e089e65..8d15efda686 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -108,7 +108,7 @@ var CrashManager = core.Class.extend({
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
subtitle: error.data.title,
$content: $(QWeb.render('CrashManager.warning', {error: error}))
- }).open();
+ }).open({shouldFocusButtons:true});
},
show_error: function(error) {
if (!this.active) {
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 e8367e79c26..4fb7983d00f 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -172,7 +172,7 @@ var BasicRenderer = AbstractRenderer.extend({
* @param {Object} [options]
* @param {integer} [options.inc=1] - the increment to use when searching for the
* "next" possible one
- * @param {boolean} [options.wrap=true] if true, when we arrive at the end of the
+ * @param {boolean} [options.wrap=false] if true, when we arrive at the end of the
* list of widget, we wrap around and try to activate widgets starting at
* the beginning. Otherwise, we just stop trying and return -1
* @returns {integer} the index of the widget that was activated or -1 if
@@ -180,7 +180,8 @@ var BasicRenderer = AbstractRenderer.extend({
*/
_activateFieldWidget: function (record, currentIndex, options) {
options = options || {};
- _.defaults(options, {inc: 1, wrap: true});
+ _.defaults(options, {inc: 1, wrap: false});
+ currentIndex = Math.max(0,currentIndex); // do not allow negative currentIndex
var recordWidgets = this.allFieldWidgets[record.id] || [];
for (var i = 0 ; i < recordWidgets.length ; i++) {
@@ -271,6 +272,10 @@ var BasicRenderer = AbstractRenderer.extend({
element.$el.toggleClass("o_readonly_modifier", !!modifiers.readonly);
element.$el.toggleClass("o_required_modifier", !!modifiers.required);
+ if (element.widget) {
+ element.widget.updateModifiersValue(modifiers);
+ }
+
// Call associated callback
if (element.callback) {
element.callback(element, modifiers, record);
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 00a4631d3f7..084c9ae4579 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -19,6 +19,8 @@ var FormController = BasicController.extend({
open_one2many_record: '_onOpenOne2ManyRecord',
open_record: '_onOpenRecord',
toggle_column_order: '_onToggleColumnOrder',
+ focus_control_button: '_onFocusControlButton',
+ form_dialog_discarded: '_onFormDialogDiscarded',
}),
/**
* @override
@@ -41,12 +43,19 @@ var FormController = BasicController.extend({
// Public
//--------------------------------------------------------------------------
+
/**
* Calls autofocus on the renderer
*/
autofocus: function () {
if (!this.disableAutofocus) {
- this.renderer.autofocus();
+ var isControlActivted = this.renderer.autofocus();
+ if (!isControlActivted) {
+ // this can happen in read mode if there are no button with class oe_highlight
+ if (this.$buttons && this.mode === 'readonly') {
+ return this.$buttons.find('.o_form_button_edit').focus();
+ }
+ }
}
},
/**
@@ -119,13 +128,21 @@ var FormController = BasicController.extend({
this.$buttons = $('<div/>');
if (mustRenderFooterButtons) {
this.$buttons.append($footer);
+
} else {
this.$buttons.append(qweb.render("FormView.buttons", {widget: this}));
this.$buttons.on('click', '.o_form_button_edit', this._onEdit.bind(this));
this.$buttons.on('click', '.o_form_button_create', this._onCreate.bind(this));
this.$buttons.on('click', '.o_form_button_save', this._onSave.bind(this));
this.$buttons.on('click', '.o_form_button_cancel', this._onDiscard.bind(this));
-
+ this._assignSaveCancelKeyboardBehavior(this.$buttons.find('.o_form_buttons_edit'));
+ this.$buttons.find('.o_form_buttons_edit').tooltip({
+ delay: {show: 200, hide:0},
+ title: function(){
+ return qweb.render('SaveCancelButton.tooltip');
+ },
+ trigger: 'manual',
+ });
this._updateButtons();
}
this.$buttons.appendTo($node);
@@ -218,7 +235,37 @@ var FormController = BasicController.extend({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
-
+ /**
+ * Assign on the buttons save and discard additionnal behavior to facilitate
+ * the work of the users doing input only using the keyboard
+ *
+ * @param {jQueryElement} $saveCancelButtonContainer The div containing the
+ * save and cancel buttons
+ * @private
+ */
+ _assignSaveCancelKeyboardBehavior: function ($saveCancelButtonContainer) {
+ var self = this;
+ $saveCancelButtonContainer.children().on('keydown', function(e) {
+ switch(e.which) {
+ case $.ui.keyCode.ENTER:
+ e.preventDefault();
+ self.saveRecord.apply(self);
+ break;
+ case $.ui.keyCode.ESCAPE:
+ e.preventDefault();
+ self._discardChanges.apply(self);
+ break;
+ case $.ui.keyCode.TAB:
+ if (!e.shiftKey &&
+ (e.target.classList.contains("btn-primary") ||
+ e.target.classList.contains("oe_highlight"))) {
+ $saveCancelButtonContainer.tooltip('show');
+ e.preventDefault();
+ }
+ break;
+ }
+ });
+ },
/**
* When a save operation has been confirmed from the model, this method is
* called.
@@ -489,6 +536,28 @@ var FormController = BasicController.extend({
this.model.freezeOrder(event.data.id);
},
/**
+ * Set the focus on the first primary button of the controller (likely Edit)
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onFocusControlButton:function(e) {
+ if (this.$buttons) {
+ e.stopPropagation();
+ this.$buttons.find('.btn-primary:visible:first()').focus();
+ }
+ },
+ /**
+ * Reset the focus on the control that openned a Dialog after it was closed
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onFormDialogDiscarded: function(e) {
+ e.stopPropagation();
+ this.renderer.focusLastActivatedWidget();
+ },
+ /**
* Opens a one2many record (potentially new) in a dialog. This handler is
* o2m specific as in this case, the changes done on the related record
* shouldn't be saved in DB when the user clicks on 'Save' in the dialog,
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index d221f93a4e8..94e870b78d6 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -15,6 +15,10 @@ var FormRenderer = BasicRenderer.extend({
'click .o_notification_box .oe_field_translate': '_onTranslate',
'click .oe_title, .o_inner_group': '_onClick',
}),
+ custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
+ 'navigation_move':'_onNavigationMove',
+ 'activate_next_widget' : '_onActivateNextWidget',
+ }),
// default col attributes for the rendering of groups
INNER_GROUP_COL: 2,
OUTER_GROUP_COL: 2,
@@ -25,6 +29,7 @@ var FormRenderer = BasicRenderer.extend({
init: function () {
this._super.apply(this, arguments);
this.idsForLabels = {};
+ this.lastActivatedFieldIndex = -1;
},
/**
* @override
@@ -43,10 +48,19 @@ var FormRenderer = BasicRenderer.extend({
/**
* Focuses the field having attribute 'default_focus' set, if any, or the
* first focusable field otherwise.
+ * In read mode, delegate which button to give the focus to, to the form_renderer
+ *
+ * @returns {int || undefined} the index of the widget activated else
+ * undefined
*/
autofocus: function () {
if (this.mode === 'readonly') {
- return;
+ var firstPrimaryFormButton = this.$el.find('button.oe_highlight:enabled:visible:first()');
+ if (firstPrimaryFormButton.length > 0) {
+ return firstPrimaryFormButton.focus();
+ } else {
+ return;
+ }
}
var focusWidget = this.defaultFocusField;
if (!focusWidget || !focusWidget.isFocusable()) {
@@ -60,7 +74,7 @@ var FormRenderer = BasicRenderer.extend({
}
}
if (focusWidget) {
- focusWidget.activate({noselect: true});
+ return focusWidget.activate({noselect: true});
}
},
/**
@@ -145,6 +159,15 @@ var FormRenderer = BasicRenderer.extend({
.removeAttr('disabled');
},
/**
+ * Put the focus on the last activated widget.
+ * This function is used when closing a dialog to give the focus back to the
+ * form that has opened it and ensures that the focus is in the correct
+ * field.
+ */
+ focusLastActivatedWidget: function () {
+ this._activateNextFieldWidget(this.state, this.lastActivatedFieldIndex - 1);
+ },
+ /**
* returns the active tab pages for each notebook
*
* @todo currently, this method is unused...
@@ -211,7 +234,27 @@ var FormRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
-
+ /**
+ * @override
+ */
+ _activateNextFieldWidget: function (record, currentIndex) {
+ //if we are the last widget, we should give the focus to the first Primary Button in the form
+ //else do the default behavior
+ if ( (currentIndex + 1) >= (this.allFieldWidgets[record.id] || []).length) {
+ this.trigger_up('focus_control_button');
+ this.lastActivatedFieldIndex = -1;
+ } else {
+ var activatedIndex = this._super.apply(this, arguments);
+ if (activatedIndex === -1 ) { // no widget have been activated, we should go to the edit/save buttons
+ this.trigger_up('focus_control_button');
+ this.lastActivatedFieldIndex = -1;
+ }
+ else {
+ this.lastActivatedFieldIndex = activatedIndex;
+ }
+ }
+ return this.lastActivatedFieldIndex;
+ },
/**
* Add a tooltip on a button
*
@@ -819,11 +862,15 @@ var FormRenderer = BasicRenderer.extend({
this.defs = defs;
var $form = this._renderNode(this.arch).addClass(this.className);
delete this.defs;
-
+
return $.when.apply($, defs).then(function () {
self._updateView($form.contents());
}, function () {
$form.remove();
+ }).then(function(){
+ if (self.lastActivatedFieldIndex >= 0) {
+ self._activateNextFieldWidget(self.state, self.lastActivatedFieldIndex);
+ }
});
},
/**
@@ -882,7 +929,11 @@ var FormRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
-
+ _onActivateNextWidget: function (e) {
+ e.stopPropagation();
+ var index = this.allFieldWidgets[this.state.id].indexOf(e.data.target);
+ this._activateNextFieldWidget(this.state, index);
+ },
/**
* Makes the Edit button bounce in readonly
*
@@ -899,11 +950,13 @@ var FormRenderer = BasicRenderer.extend({
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
- ev.stopPropagation();
+ if (ev.data.direction !== "cancel") {
+ ev.stopPropagation();
+ }
var index;
if (ev.data.direction === "next") {
- index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
+ index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target);
this._activateNextFieldWidget(this.state, index);
} else if (ev.data.direction === "previous") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 11735fa94f8..f3dc79cd4bf 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -24,6 +24,7 @@ ListRenderer.include({
}),
events: _.extend({}, ListRenderer.prototype.events, {
'click .o_field_x2many_list_row_add a': '_onAddRecord',
+ 'keydown .o_field_x2many_list_row_add a': '_onKeyDownAddRecord',
'click tbody td.o_data_cell': '_onCellClick',
'click tbody tr:not(.o_data_row)': '_onEmptyRowClick',
'click tfoot': '_onFooterClick',
@@ -738,6 +739,34 @@ ListRenderer.include({
_onFooterClick: function () {
this.unselectRow();
},
+ _onKeyDownAddRecord: function(e) {
+ switch(e.keyCode) {
+ case $.ui.keyCode.ENTER:
+ e.stopPropagation();
+ e.preventDefault();
+ this._onAddRecord(e);
+ break;
+ }
+ },
+ /**
+ * It will returns the first visible widget that is editable
+ *
+ * @private
+ * @returns {Class} Widget returns first widget
+ */
+ _getFirstWidget: function () {
+ var record = this.state.data[this.currentRow];
+ var recordWidgets = this.allFieldWidgets[record.id];
+ var firstWidget = _.find(recordWidgets, function (widget) {
+ var isFirst = widget.$el.is(':visible') &&
+ (widget.$el.has('input').length > 0 ||
+ widget.tagName== 'input') &&
+ !widget.$el.hasClass('o_readonly_modifier');
+ return isFirst;
+ });
+ return firstWidget;
+ },
+
/**
* Handles the keyboard navigation according to events triggered by field
* widgets.
@@ -791,12 +820,20 @@ ListRenderer.include({
}
break;
case 'next':
- if (this.currentFieldIndex + 1 < this.columns.length) {
- this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
- .fail(this._moveToNextLine.bind(this));
+ // When navigating with the keyboard, we want to get out of the list editable if the
+ // first field is left empty.
+ var column = this.columns[this.currentFieldIndex];
+ var firstWidget = this._getFirstWidget();
+ if (column.attrs.name === firstWidget.name && !firstWidget.$input.val()) {
+ this.trigger_up('activate_next_widget');
} else {
- this._moveToNextLine();
- }
+ if (this.currentFieldIndex + 1 < this.columns.length) {
+ this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
+ .fail(this._moveToNextLine.bind(this));
+ } else {
+ this._moveToNextLine();
+ }
+ }
break;
case 'next_line':
this._moveToNextLine();
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index d4ae709008c..55f9a2e85d7 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -209,7 +209,13 @@ var FormViewDialog = ViewDialog.extend({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
-
+ /**
+ * @override
+ */
+ _focusOnClose: function() {
+ this.trigger_up('form_dialog_discarded');
+ return true;
+ },
_save: function () {
var self = this;
return this.form_view.saveRecord(this.form_view.handle, {
@@ -418,7 +424,13 @@ var SelectCreateDialog = ViewDialog.extend({
dialog.on('closed', this, this.close);
return dialog;
},
-
+ /**
+ * @override
+ */
+ _focusOnClose: function() {
+ this.trigger_up('form_dialog_discarded');
+ return true;
+ },
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index be992c8f38a..94536d30dd2 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -209,7 +209,7 @@
<t t-name="WebClient.DebugManager">
<li class="o_debug_manager">
- <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
+ <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1">
<span class="fa fa-bug"/>
</a>
<ul class="dropdown-menu o_debug_dropdown" role="menu"/>
@@ -411,6 +411,22 @@
</button>
</div>
</t>
+<t t-name="SaveCancelButton.tooltip">
+ <div class="oe_tooltip_string">
+ <div class="tooltip-inner">
+ Hit ENTER to SAVE<br/>
+ Hit ESCAPE to DISCARD
+ </div>
+ </div>
+</t>
+<t t-name="CreateButton.tooltip">
+ <div class="oe_tooltip_string">
+ <div class="tooltip-inner">
+ Hit ENTER to CREATE<br/>
+ Hit DOWN to navigate to the list bellow
+ </div>
+ </div>
+</t>
<form t-name="FormView.set_default">
<t t-set="args" t-value="widget.args"/>
<table style="width: 100%">
diff --git a/addons/web/static/src/xml/dialog.xml b/addons/web/static/src/xml/dialog.xml
index 8f5779e0eb8..68c3ab3f2e3 100644
--- a/addons/web/static/src/xml/dialog.xml
+++ b/addons/web/static/src/xml/dialog.xml
@@ -7,7 +7,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true" tabindex="-1">×</button>
<h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small"><t t-esc="subtitle"/></span></h4>
</div>
<div class="modal-body"/>