[FIX] web, website, web_editor: fix popup scrollbar

Steps to reproduce the bug:

- Drag and drop several snippets onto a web page in edit mode to make
the vertical scrollbar appear.
- Drag and drop a popup onto the page.
- Drag and drop snippets into the popup so that the height of the popup
exceeds the height of the viewport.
- Try to scroll the popup downwards by clicking and dragging on the
scrollbar (not using the mouse wheel).
- Bug: the page is scrolled instead of the popup.

Note: This bug occurs in Chrome (not in Firefox).

This issue occurs because two scrollbars are present at the same
location (one for the page and one for the popup) and they overlap each
other. Normally, Bootstrap removes the scrollbar from the body when a
modal is opened, and this behavior was adapted for the #wrapwrap element
with this commit [1]. However, when transitioning to Bootstrap 5, the
code that overrides Bootstrap was removed instead of being adapted (this
was done in this commit [2]).

In later commits [3] and [4], we continued to remove code that updated
the scrollbar based on the opening of a modal because this code was
causing errors due to the fact that it was incomplete without the part
removed by commit [2].

This commit restores the original behavior (before the deletions made by
the aforementioned commits) by restoring the missing code and properly
adapting it to Bootstrap 5.

[1]: https://github.com/odoo/odoo/commit/9cf8b97fe40444b44ebb6e6fb992bc658a087d32
[2]: https://github.com/odoo/odoo/commit/0b94da214b7017e8580e671cbaa68ade6de2fbc7
[3]: https://github.com/odoo/odoo/commit/cb7cf77ed080e86f800af61c9a3dc4ad36ad6cfb
[4]: https://github.com/odoo/odoo/commit/51939d09f84579f61f1ff77aacd754beba036dc2

task-3102275

closes odoo/odoo#126709

X-original-commit: 0e75f1c01ca4fcc3eb483a7256bcf72b2bca39c3
Signed-off-by: Benoit Socias (bso) <bso@odoo.com>
This commit is contained in:
Benjamin Vray
2023-06-30 00:20:47 +02:00
parent 5921343c2a
commit 7ee6d9ff2f
7 changed files with 178 additions and 11 deletions
@@ -152,6 +152,13 @@ var dom = {
closestScrollable(el) {
return $(el).closestScrollable()[0] || null;
},
/**
* @param {HTMLElement} el
* @see $.compensateScrollbar
*/
compensateScrollbar(el, ...rest) {
$(el).compensateScrollbar(...rest);
},
/**
* jQuery find function behavior is::
*
+24
View File
@@ -158,3 +158,27 @@ ScrollSpy.prototype._activate = function (target) {
Dropdown.prototype._detectNavbar = function () {
return false;
};
/* Bootstrap modal scrollbar compensation on non-body */
const bsAdjustDialogFunction = Modal.prototype._adjustDialog;
Modal.prototype._adjustDialog = function () {
const document = this._element.ownerDocument;
document.body.classList.remove('modal-open');
const $scrollable = $().getScrollingElement(document);
if (document.body.contains($scrollable[0])) {
$scrollable.compensateScrollbar(true);
}
document.body.classList.add('modal-open');
return bsAdjustDialogFunction.apply(this, arguments);
};
const bsResetAdjustmentsFunction = Modal.prototype._resetAdjustments;
Modal.prototype._resetAdjustments = function () {
const document = this._element.ownerDocument;
document.body.classList.remove('modal-open');
const $scrollable = $().getScrollingElement(document);
if (document.body.contains($scrollable[0])) {
$scrollable.compensateScrollbar(false);
}
return bsResetAdjustmentsFunction.apply(this, arguments);
};
+31
View File
@@ -146,6 +146,37 @@ $.fn.extend({
}
return $el;
},
/**
* Adapt the given css property by adding the size of a scrollbar if any.
* Limitation: only works if the given css property is not already used as
* inline style for another reason.
*
* @param {boolean} [add=true]
* @param {boolean} [isScrollElement=true]
* @param {string} [cssProperty='padding-right']
*/
compensateScrollbar(add = true, isScrollElement = true, cssProperty = 'padding-right') {
for (const el of this) {
// Compensate scrollbar
const scrollableEl = isScrollElement ? el : $(el).parent().closestScrollable()[0];
const isRTL = scrollableEl.matches(".o_rtl");
if (isRTL) {
cssProperty = cssProperty.replace("right", "left");
}
el.style.removeProperty(cssProperty);
if (!add) {
return;
}
const style = window.getComputedStyle(el);
// Round up to the nearest integer to be as close as possible to
// the correct value in case of browser zoom.
const borderLeftWidth = Math.ceil(parseFloat(style.borderLeftWidth.replace('px', '')));
const borderRightWidth = Math.ceil(parseFloat(style.borderRightWidth.replace('px', '')));
const bordersWidth = borderLeftWidth + borderRightWidth;
const newValue = parseInt(style[cssProperty]) + scrollableEl.offsetWidth - scrollableEl.clientWidth - bordersWidth;
el.style.setProperty(cssProperty, `${newValue}px`, 'important');
}
},
/**
* @returns {jQuery}
*/
@@ -24,3 +24,6 @@ html, body, #wrapwrap {
flex: 1 0 auto;
}
}
.modal-open #wrapwrap {
overflow: hidden;
}
@@ -409,6 +409,12 @@ var SnippetEditor = Widget.extend({
const editableOffsetTop = this.$editable.offset().top - manipulatorOffset.top;
this.$el.toggleClass('o_top_cover', offset.top - editableOffsetTop < 25);
// If the element covered by the overlay has a scrollbar, we remove its
// right border as it interferes with proper scrolling. (e.g. modal)
const handleEReadonlyEl = this.$el[0].querySelector('.o_handle.e.readonly');
if (handleEReadonlyEl) {
handleEReadonlyEl.style.width = dom.hasScrollableContent(targetEl) ? 0 : '';
}
},
/**
* DOMElements have a default name which appears in the overlay when they
+2 -11
View File
@@ -4,6 +4,7 @@ import config from "web.config";
import publicWidget from "web.public.widget";
import animations from "website.content.snippets.animation";
const extraMenuUpdateCallbacks = [];
import dom from "web.dom";
const BaseAnimatedHeader = animations.Animation.extend({
disabledInEditableMode: false,
@@ -78,17 +79,7 @@ const BaseAnimatedHeader = animations.Animation.extend({
* @private
*/
_adaptFixedHeaderPosition() {
// Compensate scrollbar
this.el.style.removeProperty('right');
if (this.fixedHeader) {
const scrollableEl = $(this.el).parent().closestScrollable()[0];
const style = window.getComputedStyle(this.el);
const borderLeftWidth = parseInt(style.borderLeftWidth.replace('px', ''));
const borderRightWidth = parseInt(style.borderRightWidth.replace('px', ''));
const bordersWidth = borderLeftWidth + borderRightWidth;
const newValue = parseInt(style['right']) + scrollableEl.offsetWidth - scrollableEl.clientWidth - bordersWidth;
this.el.style.setProperty('right', `${newValue}px`, 'important');
}
dom.compensateScrollbar(this.el, this.fixedHeader, false, 'right');
},
/**
* @private
@@ -3,6 +3,8 @@
import config from "web.config";
import publicWidget from "web.public.widget";
import {getCookie, setCookie} from "web.utils.cookies";
import dom from "web.dom";
import throttleForAnimation from "@web/core/utils/timing";
// TODO In master, export this class too or merge it with PopupWidget
const SharedPopupWidget = publicWidget.Widget.extend({
@@ -178,6 +180,109 @@ const PopupWidget = publicWidget.Widget.extend({
publicWidget.registry.popup = PopupWidget;
const noBackdropPopupWidget = publicWidget.Widget.extend({
selector: '.s_popup_no_backdrop',
disabledInEditableMode: false,
events: {
'shown.bs.modal': '_onModalNoBackdropShown',
'hide.bs.modal': '_onModalNoBackdropHide',
},
/**
* @override
*/
start() {
this.throttledUpdateScrollbar = throttleForAnimation(() => this._updateScrollbar());
if (this.editableMode && this.el.classList.contains('show')) {
// Use case: When the "Backdrop" option is disabled in edit mode.
// The page scrollbar must be adjusted and events must be added.
this._updateScrollbar();
this._addModalNoBackdropEvents();
}
return this._super(...arguments);
},
/**
* @override
*/
destroy() {
this._super(...arguments);
this._removeModalNoBackdropEvents();
// After destroying the widget, we need to trigger a resize event so that
// the scrollbar can adjust to its default behavior.
window.dispatchEvent(new Event('resize'));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_updateScrollbar() {
// When there is no backdrop the element with the scrollbar is
// '.modal-content' (see comments in CSS).
const modalContent = this.el.querySelector('.modal-content');
const isOverflowing = dom.hasScrollableContent(modalContent);
const modalInstance = window.Modal.getInstance(this.el);
if (isOverflowing) {
// If the "no-backdrop" modal has a scrollbar, the page's scrollbar
// must be hidden. This is because if the two scrollbars overlap, it
// is no longer possible to scroll using the modal's scrollbar.
modalInstance._adjustDialog();
} else {
// If the "no-backdrop" modal does not have a scrollbar, the page
// scrollbar must be displayed because we must be able to scroll the
// page (e.g. a "cookies bar" popup at the bottom of the page must
// not prevent scrolling the page).
modalInstance._resetAdjustments();
}
},
/**
* @private
*/
_addModalNoBackdropEvents() {
window.addEventListener('resize', this.throttledUpdateScrollbar);
this.resizeObserver = new window.ResizeObserver(() => {
// When the size of the modal changes, the scrollbar needs to be
// adjusted.
this._updateScrollbar();
});
this.resizeObserver.observe(this.el.querySelector('.modal-content'));
},
/**
* @private
*/
_removeModalNoBackdropEvents() {
this.throttledUpdateScrollbar.cancel();
window.removeEventListener('resize', this.throttledUpdateScrollbar);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
delete this.resizeObserver;
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onModalNoBackdropShown() {
this._updateScrollbar();
this._addModalNoBackdropEvents();
},
/**
* @private
*/
_onModalNoBackdropHide() {
this._removeModalNoBackdropEvents();
},
});
publicWidget.registry.noBackdropPopup = noBackdropPopupWidget;
// Extending the popup widget with cookiebar functionality.
// This allows for refusing optional cookies for now and can be
// extended to picking which cookies categories are accepted.