[ADD] web: Owl Dialog
New Owl component: Dialog. It does not rely on Bootstrap but behaves the same and does not conflict with legacy dialogs. To call this component, you need to define it in the XML parent template instead of calling it in the JS file, and put a flag in the parent state to toggle the dialog. Old dialog file has also been slightly updated to ensure compatibility between both versions. This commit also updates a list view test which did not wait for click events to properly trigger and would cause the new dialog system to crash.
This commit is contained in:
@@ -4,6 +4,7 @@ odoo.define('web.Dialog', function (require) {
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var Widget = require('web.Widget');
|
||||
const OwlDialog = require('web.OwlDialog');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -205,6 +206,9 @@ var Dialog = Widget.extend({
|
||||
if (options && options.shouldFocusButtons) {
|
||||
self._onFocusControlButton();
|
||||
}
|
||||
|
||||
// Notifies OwlDialog to adjust focus/active properties on owl dialogs
|
||||
OwlDialog.display(self);
|
||||
});
|
||||
|
||||
return self;
|
||||
@@ -236,6 +240,9 @@ var Dialog = Widget.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
// Notifies OwlDialog to adjust focus/active properties on owl dialogs
|
||||
OwlDialog.hide(this);
|
||||
|
||||
// Triggers the onForceClose event if the callback is defined
|
||||
if (this.onForceClose) {
|
||||
this.onForceClose();
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
odoo.define('web.OwlDialog', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { useExternalListener } = require('web.custom_hooks');
|
||||
|
||||
const { Component, hooks, misc } = owl;
|
||||
const { Portal } = misc;
|
||||
const { useRef } = hooks;
|
||||
const SIZE_CLASSES = {
|
||||
'extra-large': 'modal-xl',
|
||||
'large': 'modal-lg',
|
||||
'small': 'modal-sm',
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog (owl version)
|
||||
*
|
||||
* Represents a bootstrap-styled dialog handled with pure JS. Its implementation
|
||||
* is roughly the same as the legacy dialog, the only exception being the buttons.
|
||||
* @extends Component
|
||||
**/
|
||||
class Dialog extends Component {
|
||||
/**
|
||||
* @param {Object} [props]
|
||||
* @param {(boolean|string)} [props.backdrop='static'] The kind of modal backdrop
|
||||
* to use (see Bootstrap documentation).
|
||||
* @param {string} [props.contentClass] Class to add to the dialog
|
||||
* @param {boolean} [props.fullscreen=false] Whether the dialog should be
|
||||
* open in fullscreen mode (the main usecase is mobile).
|
||||
* @param {boolean} [props.renderFooter=true] Whether the dialog footer
|
||||
* should be rendered.
|
||||
* @param {boolean} [props.renderHeader=true] Whether the dialog header
|
||||
* should be rendered.
|
||||
* @param {string} [props.size='large'] 'extra-large', 'large', 'medium'
|
||||
* or 'small'.
|
||||
* @param {string} [props.subtitle='']
|
||||
* @param {string} [props.title='Odoo']
|
||||
* @param {boolean} [props.technical=true] If set to false, the modal will have
|
||||
* the standard frontend style (use this for non-editor frontend features).
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.modalRef = useRef('modal');
|
||||
this.footerRef = useRef('modal-footer');
|
||||
|
||||
useExternalListener(window, 'keydown', this._onKeydown);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.constructor.display(this);
|
||||
|
||||
this.env.bus.on('close_dialogs', this, this._close);
|
||||
|
||||
if (this.props.renderFooter) {
|
||||
// Set up main button : will first look for an element with the
|
||||
// 'btn-primary' class, then a 'btn' class, then the first button
|
||||
// element.
|
||||
let mainButton = this.footerRef.el.querySelector('.btn.btn-primary');
|
||||
if (!mainButton) {
|
||||
mainButton = this.footerRef.el.querySelector('.btn');
|
||||
}
|
||||
if (!mainButton) {
|
||||
mainButton = this.footerRef.el.querySelector('button');
|
||||
}
|
||||
if (mainButton) {
|
||||
this.mainButton = mainButton;
|
||||
this.mainButton.addEventListener('keydown', this._onMainButtonKeydown.bind(this));
|
||||
this.mainButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
this._removeTooltips();
|
||||
}
|
||||
|
||||
async willUnmount() {
|
||||
this.env.bus.off('close_dialogs', this, this._close);
|
||||
|
||||
this._removeTooltips();
|
||||
|
||||
this.constructor.hide(this);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get size() {
|
||||
return SIZE_CLASSES[this.props.size];
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Send an event signaling that the dialog must be closed.
|
||||
* @private
|
||||
*/
|
||||
_close() {
|
||||
this.trigger('dialog_closed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any existing tooltip present in the DOM.
|
||||
* @private
|
||||
*/
|
||||
_removeTooltips() {
|
||||
for (const tooltip of document.querySelectorAll('.tooltip')) {
|
||||
tooltip.remove(); // remove open tooltip if any to prevent them staying when modal is opened
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onBackdropClick() {
|
||||
if (this.props.backdrop === 'static') {
|
||||
if (this.mainButton) {
|
||||
this.mainButton.focus();
|
||||
}
|
||||
} else {
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onFocus() {
|
||||
if (this.mainButton) {
|
||||
this.mainButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the TAB key on the main button. If the focus is on a primary
|
||||
* button and the user tries to tab to go to the next button : a tooltip
|
||||
* will be displayed.
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onMainButtonKeydown(ev) {
|
||||
if (ev.key === 'Tab' && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
$(this.mainButton)
|
||||
.tooltip({
|
||||
delay: { show: 200, hide: 0 },
|
||||
title: () => this.env.qweb.render('DialogButton.tooltip', {
|
||||
title: this.mainButton.innerText.toUpperCase(),
|
||||
}),
|
||||
trigger: 'manual',
|
||||
})
|
||||
.tooltip('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydown(ev) {
|
||||
if (
|
||||
ev.key === 'Escape' &&
|
||||
!['INPUT', 'TEXTAREA'].includes(ev.target.tagName) &&
|
||||
this.constructor.displayed[this.constructor.displayed.length - 1] === this
|
||||
) {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.stopPropagation();
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Static
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Push the given dialog at the end of the displayed list then set it as
|
||||
* active and all the others as passive.
|
||||
* @param {(LegacyDialog|OwlDialog)} dialog
|
||||
*/
|
||||
static display(dialog) {
|
||||
// Deactivate previous dialog
|
||||
const activeDialogEl = document.querySelector('.modal.o_active_modal');
|
||||
if (activeDialogEl) {
|
||||
activeDialogEl.classList.remove('o_active_modal');
|
||||
}
|
||||
// Push dialog
|
||||
this.displayed.push(dialog);
|
||||
// Add active class
|
||||
const modalEl = dialog instanceof this ?
|
||||
// Owl dialog
|
||||
dialog.modalRef.el :
|
||||
// Legacy dialog
|
||||
dialog.$modal[0];
|
||||
modalEl.classList.add('o_active_modal');
|
||||
// Update body class
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given displayed dialog as passive and the last added displayed dialog
|
||||
* as active, then remove it from the displayed list.
|
||||
* @param {(LegacyDialog|OwlDialog)} dialog
|
||||
*/
|
||||
static hide(dialog) {
|
||||
// Remove given dialog from the list
|
||||
this.displayed.splice(this.displayed.indexOf(dialog), 1);
|
||||
// Activate last dialog and update body class
|
||||
const lastDialog = this.displayed[this.displayed.length - 1];
|
||||
if (lastDialog) {
|
||||
lastDialog.el.focus();
|
||||
const modalEl = lastDialog instanceof this ?
|
||||
// Owl dialog
|
||||
lastDialog.modalRef.el :
|
||||
// Legacy dialog
|
||||
lastDialog.$modal[0];
|
||||
modalEl.classList.add('o_active_modal');
|
||||
} else {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog.displayed = [];
|
||||
|
||||
Dialog.components = { Portal };
|
||||
Dialog.defaultProps = {
|
||||
backdrop: 'static',
|
||||
renderFooter: true,
|
||||
renderHeader: true,
|
||||
size: 'large',
|
||||
technical: true,
|
||||
title: "Odoo",
|
||||
};
|
||||
Dialog.props = {
|
||||
backdrop: { validate: b => ['static', true, false].includes(b), optional: 1 },
|
||||
contentClass: { type: String, optional: 1 },
|
||||
fullscreen: { type: Boolean, optional: 1 },
|
||||
renderFooter: { type: Boolean, optional: 1 },
|
||||
renderHeader: { type: Boolean, optional: 1 },
|
||||
size: { validate: s => ['extra-large', 'large', 'medium', 'small'].includes(s), optional: 1 },
|
||||
subtitle: { type: String, optional: 1 },
|
||||
technical: { type: Boolean, optional: 1 },
|
||||
title: { type: String, optional: 1 },
|
||||
};
|
||||
Dialog.template = 'OwlDialog';
|
||||
|
||||
return Dialog;
|
||||
});
|
||||
@@ -54,10 +54,9 @@
|
||||
footer {
|
||||
> :not(:first-child) { margin-left: .25rem; }
|
||||
> :not(:last-child) { margin-right: .25rem; }
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: .5rem;
|
||||
button {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,9 +106,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal:not(.o_active_modal) {
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.o_dialog {
|
||||
|
||||
> .modal {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
// Allow vertical scrolling in modals on iOS
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.modal {
|
||||
@include o-scrollbar-overlay;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary fix for modals which are not instantiated thanks to the Dialog
|
||||
|
||||
@@ -385,6 +385,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollbar doesn't overlap with content
|
||||
@mixin o-scrollbar-overlay {
|
||||
overflow-y: auto;
|
||||
overflow-y: overlay;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
%o-nocontent-init-image {
|
||||
content: "";
|
||||
display: block;
|
||||
|
||||
@@ -1,6 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<!-- Owl Templates -->
|
||||
|
||||
<t t-name="DialogButton.tooltip" owl="1">
|
||||
<div class="oe_tooltip_string" role="tooltip">
|
||||
<div class="tooltip-inner">
|
||||
Hit ENTER to <t t-esc="title"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="OwlDialog" owl="1">
|
||||
<Portal target="'body'">
|
||||
<div class="o_dialog" t-on-focus="_onFocus">
|
||||
<div t-if="props.backdrop" class="modal-backdrop show" t-on-click="_onBackdropClick"/>
|
||||
<div role="dialog" class="modal"
|
||||
tabindex="-1"
|
||||
t-att-class="{ o_technical_modal: props.technical, o_modal_full: props.fullscreen }"
|
||||
t-ref="modal"
|
||||
t-on-close_dialog.stop="_close"
|
||||
>
|
||||
<div class="modal-dialog" t-att-class="size">
|
||||
<div class="modal-content" t-att-class="props.contentClass">
|
||||
<header t-if="props.renderHeader" class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<t t-esc="props.title"/>
|
||||
<span t-if="props.subtitle" class="o_subtitle text-muted small" t-esc="props.subtitle"/>
|
||||
</h4>
|
||||
<button type="button" class="close" aria-label="Close" tabindex="-1" t-on-click="_close">×</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<t t-slot="default"/>
|
||||
</main>
|
||||
<footer t-if="props.renderFooter" class="modal-footer" t-ref="modal-footer">
|
||||
<t t-slot="buttons">
|
||||
<button class="btn btn-primary" t-on-click.prevent="_close">
|
||||
Ok
|
||||
</button>
|
||||
</t>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
</t>
|
||||
|
||||
<!-- Legacy Templates -->
|
||||
|
||||
<div t-name="EmptyComponent"/>
|
||||
|
||||
<div t-name="Loading" class="o_loading"/>
|
||||
|
||||
@@ -3,17 +3,25 @@
|
||||
|
||||
<!-- These templates are accessible in backend and frontend -->
|
||||
|
||||
<div role="dialog" t-name="Dialog" t-attf-class="modal#{technical ? ' o_technical_modal' : ''}#{fullscreen ? ' o_modal_full': ''}" tabindex="-1" data-backdrop="static" t-att-id="_.uniqueId('modal_')" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header t-if="renderHeader" class="modal-header">
|
||||
<h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small"><t t-esc="subtitle"/></span></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">×</button>
|
||||
</header>
|
||||
<main class="modal-body"/>
|
||||
<footer t-if="renderFooter" class="modal-footer"/>
|
||||
<t t-name="Dialog">
|
||||
<div role="dialog"
|
||||
t-attf-class="modal o_legacy_dialog #{ technical ? ' o_technical_modal' : '' } #{ fullscreen ? ' o_modal_full': '' }"
|
||||
tabindex="-1"
|
||||
data-backdrop="static"
|
||||
t-att-id="_.uniqueId('modal_')"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header t-if="renderHeader" class="modal-header">
|
||||
<h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small" t-esc="subtitle"/></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">×</button>
|
||||
</header>
|
||||
<main class="modal-body"/>
|
||||
<footer t-if="renderFooter" class="modal-footer"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
odoo.define('web.owl_dialog_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const LegacyDialog = require('web.Dialog');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const Dialog = require('web.OwlDialog');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { Component, tags, useState } = owl;
|
||||
const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 };
|
||||
const { xml } = tags;
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
QUnit.module('OwlDialog');
|
||||
|
||||
QUnit.test("Rendering of all props", async function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
class SubComponent extends Component {
|
||||
// Handlers
|
||||
_onClick() {
|
||||
assert.step('subcomponent_clicked');
|
||||
}
|
||||
}
|
||||
SubComponent.template = xml`<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>`;
|
||||
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = useState({ textContent: "sup" });
|
||||
}
|
||||
// Handlers
|
||||
_onButtonClicked(ev) {
|
||||
assert.step('button_clicked');
|
||||
}
|
||||
_onDialogClosed() {
|
||||
assert.step('dialog_closed');
|
||||
}
|
||||
}
|
||||
Parent.components = { Dialog, SubComponent };
|
||||
Parent.env = makeTestEnvironment();
|
||||
Parent.template = xml`
|
||||
<Dialog
|
||||
backdrop="state.backdrop"
|
||||
contentClass="state.contentClass"
|
||||
fullscreen="state.fullscreen"
|
||||
renderFooter="state.renderFooter"
|
||||
renderHeader="state.renderHeader"
|
||||
size="state.size"
|
||||
subtitle="state.subtitle"
|
||||
technical="state.technical"
|
||||
title="state.title"
|
||||
t-on-dialog_closed="_onDialogClosed"
|
||||
>
|
||||
<SubComponent text="state.textContent"/>
|
||||
<t t-set="buttons">
|
||||
<button class="btn btn-primary" t-on-click="_onButtonClicked">The Button</button>
|
||||
</t>
|
||||
</Dialog>`;
|
||||
|
||||
const parent = new Parent();
|
||||
await parent.mount(testUtils.prepareTarget());
|
||||
const dialog = document.querySelector('.o_dialog');
|
||||
|
||||
// Helper function
|
||||
async function changeProps(key, value) {
|
||||
parent.state[key] = value;
|
||||
await testUtils.nextTick();
|
||||
}
|
||||
|
||||
// Basic layout with default properties
|
||||
assert.containsOnce(dialog, '.modal-backdrop.show');
|
||||
assert.containsOnce(dialog, '.modal.o_technical_modal');
|
||||
assert.hasClass(dialog.querySelector('.modal .modal-dialog'), 'modal-lg');
|
||||
assert.containsOnce(dialog, '.modal-header > button.close');
|
||||
assert.containsOnce(dialog, '.modal-footer > button.btn.btn-primary');
|
||||
assert.strictEqual(dialog.querySelector('.modal-body').innerText.trim(), "sup",
|
||||
"Subcomponent should match with its given text");
|
||||
|
||||
// Backdrop (default: 'static')
|
||||
// Static backdrop click should focus first button
|
||||
// => we need to reset that property
|
||||
await testUtils.dom.click(dialog.querySelector('.modal-backdrop'));
|
||||
assert.strictEqual(document.activeElement, dialog.querySelector('.btn-primary'),
|
||||
"Button should be focused when clicking on backdrop");
|
||||
|
||||
await changeProps('backdrop', false);
|
||||
assert.containsNone(document.body, '.modal-backdrop');
|
||||
|
||||
await changeProps('backdrop', true);
|
||||
await testUtils.dom.click(dialog.querySelector('.modal-backdrop'));
|
||||
|
||||
// Dialog class (default: '')
|
||||
await changeProps('contentClass', 'my_dialog_class');
|
||||
assert.hasClass(dialog.querySelector('.modal-content'), 'my_dialog_class');
|
||||
|
||||
// Full screen (default: false)
|
||||
assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_modal_full');
|
||||
await changeProps('fullscreen', true);
|
||||
assert.hasClass(dialog.querySelector('.modal'), 'o_modal_full');
|
||||
|
||||
// Size class (default: 'large')
|
||||
await changeProps('size', 'extra-large');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-xl',
|
||||
"Modal should have taken the class modal-xl");
|
||||
await changeProps('size', 'medium');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog',
|
||||
"Modal should not have any additionnal class with 'medium'");
|
||||
await changeProps('size', 'small');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-sm',
|
||||
"Modal should have taken the class modal-sm");
|
||||
|
||||
// Subtitle (default: '')
|
||||
await changeProps('subtitle', "The Subtitle");
|
||||
assert.strictEqual(dialog.querySelector('span.o_subtitle').innerText.trim(), "The Subtitle",
|
||||
"Subtitle should match with its given text");
|
||||
|
||||
// Technical (default: true)
|
||||
assert.hasClass(dialog.querySelector('.modal'), 'o_technical_modal');
|
||||
await changeProps('technical', false);
|
||||
assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_technical_modal');
|
||||
|
||||
// Title (default: 'Odoo')
|
||||
assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "Odoo" + "The Subtitle",
|
||||
"Title should match with its default text");
|
||||
await changeProps('title', "The Title");
|
||||
assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "The Title" + "The Subtitle",
|
||||
"Title should match with its given text");
|
||||
|
||||
// Reactivity of buttons
|
||||
await testUtils.dom.click(dialog.querySelector('.modal-footer .btn-primary'));
|
||||
|
||||
// Render footer (default: true)
|
||||
await changeProps('renderFooter', false);
|
||||
assert.containsNone(dialog, '.modal-footer');
|
||||
|
||||
// Render header (default: true)
|
||||
await changeProps('renderHeader', false);
|
||||
assert.containsNone(dialog, '.header');
|
||||
|
||||
// Reactivity of subcomponents
|
||||
await changeProps('textContent', "wassup");
|
||||
assert.strictEqual(dialog.querySelector('.o_subcomponent').innerText.trim(), "wassup",
|
||||
"Subcomponent should match with its given text");
|
||||
await testUtils.dom.click(dialog.querySelector('.o_subcomponent'));
|
||||
|
||||
assert.verifySteps(['dialog_closed', 'button_clicked', 'subcomponent_clicked']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Interactions between multiple dialogs", async function (assert) {
|
||||
assert.expect(22);
|
||||
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.dialogIds = useState([]);
|
||||
}
|
||||
// Handlers
|
||||
_onDialogClosed(id) {
|
||||
assert.step(`dialog_${id}_closed`);
|
||||
this.dialogIds.splice(this.dialogIds.findIndex(d => d === id), 1);
|
||||
}
|
||||
}
|
||||
Parent.components = { Dialog };
|
||||
Parent.env = makeTestEnvironment();
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Dialog t-foreach="dialogIds" t-as="dialogId" t-key="dialogId"
|
||||
contentClass="'dialog_' + dialogId"
|
||||
t-on-dialog_closed="_onDialogClosed(dialogId)"
|
||||
/>
|
||||
</div>`;
|
||||
|
||||
const parent = new Parent();
|
||||
await parent.mount(testUtils.prepareTarget());
|
||||
|
||||
// Dialog 1 : Owl
|
||||
parent.dialogIds.push(1);
|
||||
await testUtils.nextTick();
|
||||
// Dialog 2 : Legacy
|
||||
new LegacyDialog(null, {}).open();
|
||||
await testUtils.nextTick();
|
||||
// Dialog 3 : Legacy
|
||||
new LegacyDialog(null, {}).open();
|
||||
await testUtils.nextTick();
|
||||
// Dialog 4 : Owl
|
||||
parent.dialogIds.push(4);
|
||||
await testUtils.nextTick();
|
||||
// Dialog 5 : Owl
|
||||
parent.dialogIds.push(5);
|
||||
await testUtils.nextTick();
|
||||
|
||||
let modals = document.querySelectorAll('.modal');
|
||||
assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsN(document.body, '.o_dialog', 3);
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
// Reactivity with owl dialogs
|
||||
await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); // Press Escape
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsN(document.body, '.o_dialog', 2);
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn.btn-primary')); // Click on 'Ok' button
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.containsOnce(document.body, '.modal.o_legacy_dialog.o_active_modal',
|
||||
"active dialog should have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
// Reactivity with legacy dialogs
|
||||
await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey);
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.containsOnce(document.body, '.modal.o_legacy_dialog.o_active_modal',
|
||||
"active dialog should have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsOnce(document.body, '.o_legacy_dialog');
|
||||
|
||||
await testUtils.dom.click(modals[modals.length - 1].querySelector('.close'));
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsNone(document.body, '.o_legacy_dialog');
|
||||
|
||||
parent.unmount();
|
||||
|
||||
assert.containsNone(document.body, '.modal');
|
||||
// dialog 1 is closed through the removal of its parent => no callback
|
||||
assert.verifySteps(['dialog_5_closed', 'dialog_4_closed']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2462,18 +2462,18 @@ QUnit.module('Views', {
|
||||
assert.isNotVisible(list.sidebar.$el, 'sidebar should be invisible');
|
||||
assert.containsN(list, 'tbody td.o_list_record_selector', 4, "should have 4 records");
|
||||
|
||||
testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input'));
|
||||
await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input'));
|
||||
|
||||
assert.isVisible(list.sidebar.$el, 'sidebar should be visible');
|
||||
|
||||
assert.verifySteps(['/web/dataset/search_read']);
|
||||
testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
await testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
await testUtils.dom.click(list.sidebar.$('a:contains(Archive)'));
|
||||
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
|
||||
testUtils.dom.click($('.modal-footer .btn-secondary'));
|
||||
await testUtils.dom.click($('.modal-footer .btn-secondary'));
|
||||
assert.containsN(list, 'tbody td.o_list_record_selector', 4, "still should have 4 records");
|
||||
|
||||
testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
await testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
await testUtils.dom.click(list.sidebar.$('a:contains(Archive)'));
|
||||
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
|
||||
await testUtils.dom.click($('.modal-footer .btn-primary'));
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/core/collections.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/concurrency.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/dialog.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/owl_dialog.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/dom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/local_storage.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/mixins.js"></script>
|
||||
@@ -705,6 +706,7 @@
|
||||
<script type="text/javascript" src="/web/static/tests/core/util_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/widget_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/dialog_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/owl_dialog_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/dom_tests.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_tests.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user