diff --git a/packages/base/src/UI5Element.js b/packages/base/src/UI5Element.js index d3e8918d7b99..51af56a5d73a 100644 --- a/packages/base/src/UI5Element.js +++ b/packages/base/src/UI5Element.js @@ -470,7 +470,7 @@ class UI5Element extends HTMLElement { const focusDomRef = this.getFocusDomRef(); - if (focusDomRef) { + if (focusDomRef && typeof focusDomRef.focus === "function") { focusDomRef.focus(); } } diff --git a/packages/main/package.json b/packages/main/package.json index 0208e201dfb2..fd97f926206e 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -109,7 +109,7 @@ "@wdio/sync": "^5.12.1", "@webcomponents/webcomponentsjs": "^2.2.7", "chai": "^4.2.0", - "chromedriver": "^76.0.0", + "chromedriver": "^77.0.0", "clean-css": "^4.2.1", "copy-and-watch": "^0.1.2", "cpx": "^1.5.0", @@ -153,6 +153,6 @@ "resolutions": { "abstract-syntax-tree": "1.0.3", "dir-glob": "2.0.0", - "wdio-chromedriver-service/chromedriver": "^76.0.0" + "wdio-chromedriver-service/chromedriver": "^77.0.0" } } \ No newline at end of file diff --git a/packages/main/src/Input.hbs b/packages/main/src/Input.hbs index 3294f4f6c316..6a4fe7c8d433 100644 --- a/packages/main/src/Input.hbs +++ b/packages/main/src/Input.hbs @@ -11,6 +11,7 @@ keyup should not be called yet */ - const skipFiring = (this.getInputDOMRef().value === this.value) && isIE() && !this._keyDown && this.placeholder; + const skipFiring = (this.getInputDOMRef().value === this.value) && isIE() && !this._keyDown && !!this.placeholder; !skipFiring && this.fireEventByAction(this.ACTION_USER_INPUT); diff --git a/packages/main/src/MultiComboBox.hbs b/packages/main/src/MultiComboBox.hbs index a71c486f8a5f..597accc3f930 100644 --- a/packages/main/src/MultiComboBox.hbs +++ b/packages/main/src/MultiComboBox.hbs @@ -20,13 +20,15 @@ diff --git a/packages/main/src/MultiComboBox.js b/packages/main/src/MultiComboBox.js index fc5af8d86795..879b4121bf41 100644 --- a/packages/main/src/MultiComboBox.js +++ b/packages/main/src/MultiComboBox.js @@ -4,6 +4,7 @@ import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import { isShow, isDown, isBackSpace } from "@ui5/webcomponents-base/dist/events/PseudoEvents.js"; import "./icons/slim-arrow-down.js"; import { getRTL } from "@ui5/webcomponents-base/dist/config/RTL.js"; +import { isIE } from "@ui5/webcomponents-core/dist/sap/ui/Device.js"; import MultiComboBoxTemplate from "./generated/templates/MultiComboBoxTemplate.lit.js"; import Input from "./Input.js"; import Tokenizer from "./Tokenizer.js"; @@ -278,7 +279,20 @@ class MultiComboBox extends UI5Element { const filteredItems = this._filterItems(value); const oldValueState = this.valueState; + /* skip calling change event when an input with a placeholder is focused on IE + - value of the host and the internal input should be differnt in case of actual input + - input is called when a key is pressed => keyup should not be called yet + */ + const skipFiring = (this._inputDom.value === this.value) && isIE && !this._keyDown && !!this.placeholder; + + if (skipFiring) { + event.preventDefault(); + + return; + } + if (this._validationTimeout) { + input.value = this._inputLastValue; return; } @@ -290,6 +304,7 @@ class MultiComboBox extends UI5Element { this.valueState = oldValueState; this._validationTimeout = null; }, 2000); + return; } @@ -331,7 +346,11 @@ class MultiComboBox extends UI5Element { } } - _keydown(event) { + _onkeyup() { + this._keyDown = false; + } + + _onkeydown(event) { if (isShow(event) && !this.readonly && !this.disabled) { event.preventDefault(); this._togglePopover(); @@ -356,6 +375,8 @@ class MultiComboBox extends UI5Element { this._tokenizer.tokens[lastTokenIndex].focus(); this._tokenizer._itemNav.currentIndex = lastTokenIndex; } + + this._keyDown = true; } _filterItems(value) { diff --git a/packages/main/src/Popover.hbs b/packages/main/src/Popover.hbs index 7790df379b70..fdb012218ea0 100644 --- a/packages/main/src/Popover.hbs +++ b/packages/main/src/Popover.hbs @@ -1,17 +1,27 @@ -{{>include "./Popup.hbs"}} +
header
slot is provided, the headerText
is ignored.
+ *
+ * @type {string}
+ * @defaultvalue: ""
+ * @public
+ */
+ headerText: {
+ type: String,
+ },
/**
* Determines on which side the ui5-popover
is placed at.
@@ -81,17 +101,6 @@ const metadata = {
type: Boolean,
},
- /**
- * Determines whether the ui5-popover
would close upon user scroll.
- *
- * @type {boolean}
- * @defaultvalue false
- * @public
- */
- stayOpenOnScroll: {
- type: Boolean,
- },
-
/**
* Determines if there is no enough space, the ui5-popover
can be placed
* over the target.
@@ -104,91 +113,117 @@ const metadata = {
type: Boolean,
},
- _left: {
+ /**
+ * Sets the X translation of the arrow
+ *
+ * @private
+ */
+ arrowTranslateX: {
type: Integer,
+ defaultValue: 0,
noAttribute: true,
},
- _top: {
+
+ /**
+ * Sets the Y translation of the arrow
+ *
+ * @private
+ */
+ arrowTranslateY: {
type: Integer,
+ defaultValue: 0,
noAttribute: true,
},
- _width: {
- type: String,
- noAttribute: true,
- },
- _height: {
- type: String,
- noAttribute: true,
+ /**
+ * Returns the calculated placement depending on the free space
+ *
+ * @private
+ */
+ actualPlacementType: {
+ type: PopoverPlacementType,
+ defaultValue: PopoverPlacementType.Right,
},
- _maxContentHeight: {
- type: Integer,
- noAttribute: true,
+ /**
+ * Defines whether the ui5-popover
is open
+ *
+ * @private
+ */
+ opened: { type: Boolean },
+ },
+ slots: {
+ /**
+ * Defines the content of the Web Component.
+ * @type {Node[]}
+ * @slot
+ * @public
+ */
+ "default": {
+ type: Node,
},
- _arrowTranslateX: {
- type: Integer,
- defaultValue: 0,
- noAttribute: true,
+ /**
+ * Defines the header HTML Element.
+ *
+ * @type {HTMLElement[]}
+ * @slot
+ * @public
+ */
+ header: {
+ type: HTMLElement,
},
- _arrowTranslateY: {
- type: Integer,
- defaultValue: 0,
- noAttribute: true,
- },
- _actualPlacementType: {
- type: PopoverPlacementType,
- defaultValue: PopoverPlacementType.Right,
- noAttribute: true,
+ /**
+ * Defines the footer HTML Element.
+ *
+ * @type {HTMLElement[]}
+ * @slot
+ * @public
+ */
+ footer: {
+ type: HTMLElement,
},
- _focusElementsHandlers: {
- type: Object,
+ },
+ events: {
+ /**
+ * Fired before the component is opened.
+ *
+ * @public
+ * @event
+ */
+ beforeOpen: {},
+
+ /**
+ * Fired after the component is opened.
+ *
+ * @public
+ * @event
+ */
+ afterOpen: {},
+
+ /**
+ * Fired before the component is closed.
+ *
+ * @public
+ * @event
+ * @param {Boolean} escPressed Indicates that ESC
key has triggered the event.
+ */
+ beforeClose: {
+ escPressed: { type: Boolean },
},
+
+ /**
+ * Fired after the component is closed.
+ *
+ * @public
+ * @event
+ */
+ afterClose: {},
},
};
-const diffTolerance = 32;
-const dockInterval = 200;
-const arrowSize = 8;
-
-/**
- * @class
- *
- * ui5-popover
component displays additional information for an object
- * in a compact way and without leaving the page.
- * The Popover can contain various UI elements, such as fields, tables, images, and charts.
- * It can also include actions in the footer.
- *
- * ui5-popover
is closed when the user clicks
- * or taps outside the popover
- * or selects an action within the popover. You can prevent this with the
- * modal
property.
- *
- * import "@ui5/webcomponents/dist/Popover";
- *
- * @constructor
- * @author SAP SE
- * @alias sap.ui.webcomponents.main.Popover
- * @extends Popup
- * @tagname ui5-popover
- * @public
- */
-class Popover extends Popup {
+class Popover extends UI5Element {
static get metadata() {
return metadata;
}
@@ -197,202 +232,200 @@ class Popover extends Popup {
return litRender;
}
+ static get styles() {
+ return PopoverCss;
+ }
+
static get template() {
return PopoverTemplate;
}
- static get styles() {
- return [Popup.styles, popoverCss];
+ forwardToFirst() {
+ const firstFocusable = FocusHelper.findFirstFocusableElement(this.contentDOM);
+
+ if (firstFocusable) {
+ firstFocusable.focus();
+ }
}
- constructor() {
- super();
+ forwardToLast() {
+ const lastFocusable = FocusHelper.findLastFocusableElement(this.contentDOM);
- this._documentMouseDownHandler = this.documentMouseDown.bind(this);
+ if (lastFocusable) {
+ lastFocusable.focus();
+ }
+ }
- const that = this;
+ isOpenerClicked(event) {
+ return event.target === this._opener;
+ }
- this._focusElementsHandlers = {
- forwardToFirst: event => {
- const firstFocusable = FocusHelper.findFirstFocusableElement(that);
+ openBy(opener) {
+ if (!opener || this.opened) {
+ return;
+ }
- if (firstFocusable) {
- firstFocusable.focus();
- }
- },
- forwardToLast: event => {
- const lastFocusable = FocusHelper.findLastFocusableElement(that);
+ this._opener = opener;
+ this._focusedElementBeforeOpen = getFocusedElement();
- if (lastFocusable) {
- lastFocusable.focus();
- }
- },
- };
- }
+ this.fireEvent("beforeOpen", {});
+ this.reposition();
+ this.applyInitialFocus();
- isModal() {
- return this.modal;
- }
+ addOpenedPopover(this);
- static isInRect(x, y, rect) {
- return x >= rect.left && x <= rect.right
- && y >= rect.top && y <= rect.bottom;
+ this.opened = true;
+ this.fireEvent("afterOpen", {});
}
- static getClientRect(domRef) {
- const rect = domRef.getBoundingClientRect();
- const computedStyle = window.getComputedStyle(domRef);
+ /**
+ *
+ * @param {*} escPressed
+ * @param {*} preventRegitryUpdate
+ * @public
+ */
+ close(escPressed = false, preventRegitryUpdate = false) {
+ if (!this.opened) {
+ return;
+ }
- const offsetLeft = parseFloat(computedStyle.paddingLeft);
- const offsetRight = parseFloat(computedStyle.paddingRight);
- const offsetTop = parseFloat(computedStyle.paddingTop);
- const offsetBottom = parseFloat(computedStyle.paddingBottom);
+ this.fireEvent("beforeClose", {
+ escPressed,
+ }, true);
- return {
- left: rect.left + offsetLeft,
- right: rect.right - offsetRight,
- top: rect.top + offsetTop,
- bottom: rect.bottom - offsetBottom,
- width: rect.width - offsetLeft - offsetRight,
- height: rect.height - offsetTop - offsetBottom,
- };
- }
- hitTest(event) {
- const domRef = this.getPopupDomRef();
- const rect = domRef.getBoundingClientRect();
- let x,
- y;
+ this.opened = false;
- if (event.touches) {
- const touch = event.touches[0];
- x = touch.clientX;
- y = touch.clientY;
- } else {
- x = event.clientX;
- y = event.clientY;
+ if (!preventRegitryUpdate) {
+ removeOpenedPopover(this);
}
- // don't close the popover if the "initial focus" is outside the popover
- // and the user click/touch on it
- if (this.initialFocus && this._initialFocusDomRef) {
- const initialFocusRect = this._initialFocusDomRef.getBoundingClientRect();
- if (Popover.isInRect(x, y, initialFocusRect)) {
- return true;
- }
- }
+ this.resetFocus();
- if (this._targetElement) {
- const targetElementRect = this._targetElement.getBoundingClientRect();
- if (Popover.isInRect(x, y, targetElementRect)) {
- return true;
- }
+ this.hide();
+ this.fireEvent("afterClose", {});
+ }
+
+ get focusedElement() {
+ let element = document.activeElement;
+
+ while (element.shadowRoot && element.shadowRoot.activeElement) {
+ element = element.shadowRoot.activeElement;
}
- return Popover.isInRect(x, y, rect);
+ return (element && typeof element.focus === "function") ? element : null;
}
- documentMouseDown(event) {
- if (!this.modal && !Popup.hitTest(this, event)) {
- this.close();
+ applyInitialFocus() {
+ const element = this.getRootNode().getElementById(this.initialFocus) || document.getElementById(this.initialFocus) || FocusHelper.findFirstFocusableElement(this.contentDOM);
+
+ if (element) {
+ element.focus();
}
}
- checkDocking() {
- if (!this.stayOpenOnScroll && this.hasTargetElementMoved()) {
- this.close();
+ resetFocus() {
+ if (!this._focusedElementBeforeOpen) {
+ return;
}
- const popoverDomRef = this.getPopupDomRef();
+ this._focusedElementBeforeOpen.focus();
+ this._focusedElementBeforeOpen = null;
+ }
+
+ shouldCloseDueOverflow(placement, openerRect) {
+ const threshold = 32;
- const popoverSize = {
- width: popoverDomRef.offsetWidth,
- height: popoverDomRef.offsetHeight,
+ const limits = {
+ "Right": openerRect.top,
+ "Left": openerRect.top,
+ "Top": openerRect.top,
+ "Bottom": openerRect.top,
};
- const targetRect = Popover.getClientRect(this._targetElement);
+ const closedPopupParent = getClosedPopupParent(this._opener);
+ let overflowsBottom = false;
+ let overflowsTop = false;
+
+ if (closedPopupParent.openBy) {
+ const contentRect = closedPopupParent.contentDOM.getBoundingClientRect();
+ overflowsBottom = openerRect.top > (contentRect.top + contentRect.height);
+ overflowsTop = (openerRect.top + openerRect.height) < contentRect.top;
+ }
- this.setLocation(targetRect, popoverSize);
+ return (limits[placement] < 0 || (limits[placement] + threshold > closedPopupParent.innerHeight)) || overflowsBottom || overflowsTop;
}
- getVerticalLeft(targetRect, popoverSize) {
- let left;
+ reposition() {
+ const popoverSize = this.popoverSize;
+ const openerRect = this._opener.getBoundingClientRect();
+ const placement = this.calcPlacement(openerRect, popoverSize);
+ const streching = this.horizontalAlign === PopoverHorizontalAlign.Stretch;
- switch (this.horizontalAlign) {
- case PopoverHorizontalAlign.Center:
- case PopoverHorizontalAlign.Stretch:
- left = targetRect.left - (popoverSize.width - targetRect.width) / 2;
- break;
- case PopoverHorizontalAlign.Left:
- left = targetRect.left;
- break;
- case PopoverHorizontalAlign.Right:
- left = targetRect.right - popoverSize.width;
- break;
+ if (this._preventRepositionAndClose) {
+ return this.close();
}
- return left;
- }
+ if (this._oldPlacement && (this._oldPlacement.left === placement.left) && (this._oldPlacement.top === placement.top) && streching) {
+ this.style.display = "inline-block";
+ this.style.width = this._width;
+ return;
+ }
- getHorizontalTop(targetRect, popoverSize) {
- let top;
+ this._oldPlacement = placement;
- switch (this.verticalAlign) {
- case PopoverVerticalAlign.Center:
- case PopoverVerticalAlign.Stretch:
- top = targetRect.top - (popoverSize.height - targetRect.height) / 2;
- break;
- case PopoverVerticalAlign.Top:
- top = targetRect.top;
- break;
- case PopoverVerticalAlign.Bottom:
- top = targetRect.bottom - popoverSize.height;
- break;
+ this.actualPlacementType = placement.placementType;
+ this.arrowTranslateX = placement.arrowX;
+ this.arrowTranslateY = placement.arrowY;
+
+ this.style.left = `${this._left}px`;
+ this.style.top = `${this._top}px`;
+ this.style.display = "inline-block";
+
+ if (streching && this._width) {
+ this.style.width = this._width;
}
+ }
- return top;
+ hide() {
+ this.style.display = "none";
}
- getActualPlacementType(targetRect, popoverSize) {
- const placementType = this.placementType;
- let actualPlacementType = placementType;
+ get popoverSize() {
+ let width,
+ height;
+ let rect = this.getBoundingClientRect();
- const clientWidth = document.documentElement.clientWidth;
- const clientHeight = document.documentElement.clientHeight;
+ if (this.opened) {
+ width = rect.width;
+ height = rect.height;
- switch (placementType) {
- case PopoverPlacementType.Top:
- if (targetRect.top < popoverSize.height
- && targetRect.top < clientHeight - targetRect.bottom) {
- actualPlacementType = PopoverPlacementType.Bottom;
- }
- break;
- case PopoverPlacementType.Bottom:
- if (clientHeight - targetRect.bottom < popoverSize.height
- && clientHeight - targetRect.bottom < targetRect.top) {
- actualPlacementType = PopoverPlacementType.Top;
- }
- break;
- case PopoverPlacementType.Left:
- if (targetRect.left < popoverSize.width
- && targetRect.left < clientWidth - targetRect.right) {
- actualPlacementType = PopoverPlacementType.Right;
- }
- break;
- case PopoverPlacementType.Right:
- if (clientWidth - targetRect.right < popoverSize.width
- && clientWidth - targetRect.right < targetRect.left) {
- actualPlacementType = PopoverPlacementType.Left;
- }
- break;
+ return { width, height };
}
- this._actualPlacementType = actualPlacementType;
+ this.style.visibility = "hidden";
+ this.style.display = "inline-block";
- return actualPlacementType;
+ rect = this.getBoundingClientRect();
+
+ width = rect.width;
+ height = rect.height;
+
+ this.style.display = "none";
+ this.style.visibility = "visible";
+
+ return { width, height };
+ }
+
+ get contentDOM() {
+ return this.shadowRoot.querySelector(".ui5-popover-content");
}
- setLocation(targetRect, popoverSize) {
+ get arrowDOM() {
+ return this.shadowRoot.querySelector(".ui5-popover-arr");
+ }
+
+ calcPlacement(targetRect, popoverSize) {
let left = 0;
let top = 0;
const allowTargetOverlap = this.allowTargetOverlap;
@@ -407,6 +440,8 @@ class Popover extends Popup {
const placementType = this.getActualPlacementType(targetRect, popoverSize);
+ this._preventRepositionAndClose = this.shouldCloseDueOverflow(placementType, targetRect);
+
const isVertical = placementType === PopoverPlacementType.Top
|| placementType === PopoverPlacementType.Bottom;
@@ -484,13 +519,8 @@ class Popover extends Popup {
this._maxContentHeight = maxContentHeight;
- const arrowTranslateX = isVertical
- ? targetRect.left + targetRect.width / 2 - left - popoverSize.width / 2 : 0;
- const arrowTranslateY = !isVertical
- ? targetRect.top + targetRect.height / 2 - top - popoverSize.height / 2 : 0;
-
- this._arrowTranslateX = Math.round(arrowTranslateX);
- this._arrowTranslateY = Math.round(arrowTranslateY);
+ const arrowTranslateX = isVertical ? targetRect.left + targetRect.width / 2 - left - popoverSize.width / 2 : 0;
+ const arrowTranslateY = !isVertical ? targetRect.top + targetRect.height / 2 - top - popoverSize.height / 2 : 0;
if (this._left === undefined || Math.abs(this._left - left) > 1.5) {
this._left = Math.round(left);
@@ -499,155 +529,98 @@ class Popover extends Popup {
if (this._top === undefined || Math.abs(this._top - top) > 1.5) {
this._top = Math.round(top);
}
- }
-
- /**
- * Opens the Popover
.
- * @param {object} control This is the component to which the
- * ui5-popover
will be placed.
- * The side of the placement depends on the placementType
property
- * set in the ui5-popover
.
- * @public
- */
- openBy(element) {
- if (this.opened) {
- return;
- }
-
- const cancelled = super.open();
- if (cancelled) {
- return true;
- }
-
- this.storeCurrentFocus();
- const targetDomRef = element;
-
- const popoverSize = this.getPopoverSize();
- const targetRect = Popover.getClientRect(targetDomRef);
-
- this._targetElement = targetDomRef;
- this._targetRect = targetRect;
+ return {
+ arrowX: Math.round(arrowTranslateX),
+ arrowY: Math.round(arrowTranslateY),
+ top: this._top,
+ left: this._left,
+ placementType,
+ };
+ }
- this.setLocation(targetRect, popoverSize);
+ getActualPlacementType(targetRect, popoverSize) {
+ const placementType = this.placementType;
+ let actualPlacementType = placementType;
- this.opened = true;
+ const clientWidth = document.documentElement.clientWidth;
+ const clientHeight = document.documentElement.clientHeight;
- setTimeout(_ => {
- if (this.opened) {
- this._dockInterval = setInterval(this.checkDocking.bind(this), dockInterval);
+ switch (placementType) {
+ case PopoverPlacementType.Top:
+ if (targetRect.top < popoverSize.height
+ && targetRect.top < clientHeight - targetRect.bottom) {
+ actualPlacementType = PopoverPlacementType.Bottom;
}
- }, 0);
-
- setTimeout(_ => {
- if (this.opened) {
- document.addEventListener("mousedown", this._documentMouseDownHandler, true);
- document.addEventListener("touchstart", this._documentMouseDownHandler, true);
+ break;
+ case PopoverPlacementType.Bottom:
+ if (clientHeight - targetRect.bottom < popoverSize.height
+ && clientHeight - targetRect.bottom < targetRect.top) {
+ actualPlacementType = PopoverPlacementType.Top;
}
- }, 0);
- }
-
- /**
- * Closes the ui5-popover
.
- * @public
- */
- close() {
- if (!this.opened) {
- return;
- }
-
- const cancelled = super.close();
- if (cancelled) {
- return;
+ break;
+ case PopoverPlacementType.Left:
+ if (targetRect.left < popoverSize.width
+ && targetRect.left < clientWidth - targetRect.right) {
+ actualPlacementType = PopoverPlacementType.Right;
+ }
+ break;
+ case PopoverPlacementType.Right:
+ if (clientWidth - targetRect.right < popoverSize.width
+ && clientWidth - targetRect.right < targetRect.left) {
+ actualPlacementType = PopoverPlacementType.Left;
+ }
+ break;
}
- this.opened = false;
-
- clearInterval(this._dockInterval);
-
- document.removeEventListener("mousedown", this._documentMouseDownHandler, true);
- document.removeEventListener("touchstart", this._documentMouseDownHandler, true);
-
- this.resetFocus();
-
- RenderScheduler.whenFinished()
- .then(_ => {
- this.fireEvent("afterClose", {});
- });
+ return actualPlacementType;
}
- getPopoverSize() {
- const popoverFrameDomRef = this.shadowRoot.querySelector(".ui5-popup-frame"); // this.getDomRef();
- const popoverDomRef = popoverFrameDomRef.querySelector(".ui5-popover-root");
-
- popoverFrameDomRef.style.visibility = "hidden";
- popoverFrameDomRef.style.display = "block";
-
- const width = popoverDomRef.offsetWidth;
- const height = popoverDomRef.offsetHeight;
+ getVerticalLeft(targetRect, popoverSize) {
+ let left;
- popoverFrameDomRef.style.display = "";
- popoverFrameDomRef.style.visibility = "visible";
+ switch (this.horizontalAlign) {
+ case PopoverHorizontalAlign.Center:
+ case PopoverHorizontalAlign.Stretch:
+ left = targetRect.left - (popoverSize.width - targetRect.width) / 2;
+ break;
+ case PopoverHorizontalAlign.Left:
+ left = targetRect.left;
+ break;
+ case PopoverHorizontalAlign.Right:
+ left = targetRect.right - popoverSize.width;
+ break;
+ }
- return {
- width,
- height,
- };
+ return left;
}
- hasTargetElementMoved() {
- const newRect = this._targetElement.getBoundingClientRect();
- const targetRect = this._targetRect;
+ getHorizontalTop(targetRect, popoverSize) {
+ let top;
- return Math.abs(newRect.left - targetRect.left) > diffTolerance
- || Math.abs(newRect.top - targetRect.top) > diffTolerance;
- }
+ switch (this.verticalAlign) {
+ case PopoverVerticalAlign.Center:
+ case PopoverVerticalAlign.Stretch:
+ top = targetRect.top - (popoverSize.height - targetRect.height) / 2;
+ break;
+ case PopoverVerticalAlign.Top:
+ top = targetRect.top;
+ break;
+ case PopoverVerticalAlign.Bottom:
+ top = targetRect.bottom - popoverSize.height;
+ break;
+ }
- get classes() {
- return {
- blockLayer: {
- "ui5-popup-BLy": true,
- "ui5-popup-blockLayer": true,
- "ui5-popup-blockLayer--hidden": !this.modal || this._hideBlockLayer,
- },
- };
+ return top;
}
get styles() {
return {
- main: {
- left: `${this._left}px`,
- top: `${this._top}px`,
- width: this._width,
- height: this._height,
- "z-index": this._zIndex + 1,
- },
- content: {
- "max-height": `${this._maxContentHeight}px`,
- },
arrow: {
- transform: `translate(${this._arrowTranslateX}px, ${this._arrowTranslateY}px)`,
- },
- blockLayer: {
- "z-index": this._zIndex,
+ transform: `translate(${this.arrowTranslateX}px, ${this.arrowTranslateY}px)`,
},
};
}
-
- get headerId() {
- return this.hasHeader ? `${this._id}-header` : undefined;
- }
-
- get focusHelper() {
- return {
- forwardToLast: this._focusElementsHandlers.forwardToLast,
- forwardToFirst: this._focusElementsHandlers.forwardToFirst,
- };
- }
-
- get role() {
- return "toolbar";
- }
}
Popover.define();
diff --git a/packages/main/src/Popup.js b/packages/main/src/Popup.js
index 9ca59741f987..ce1377a82037 100644
--- a/packages/main/src/Popup.js
+++ b/packages/main/src/Popup.js
@@ -5,6 +5,7 @@ import { isEscape } from "@ui5/webcomponents-base/dist/events/PseudoEvents.js";
// Styles
import styles from "./generated/themes/Popup.css.js";
+import { addOpenedPopup, removeOpenedPopup } from "./popup-utils/OpenedPopupsRegistry.js";
/**
* @public
@@ -290,10 +291,10 @@ class Popup extends UI5Element {
this._zIndex = Popup.getNextZIndex();
openedPopups.push(this);
+ addOpenedPopup(this);
- updateBlockLayers();
- document.addEventListener("keydown", this._documentKeyDownHandler, true);
+ updateBlockLayers();
}
close() {
@@ -303,11 +304,13 @@ class Popup extends UI5Element {
this.escPressed = false;
- document.removeEventListener("keydown", this._documentKeyDownHandler, true);
-
const index = openedPopups.indexOf(this);
openedPopups.splice(index, 1);
+ if (this.opened) {
+ removeOpenedPopup(this);
+ }
+
updateBlockLayers();
}
diff --git a/packages/main/src/Select.js b/packages/main/src/Select.js
index 0c29662d4bb1..14dd36323244 100644
--- a/packages/main/src/Select.js
+++ b/packages/main/src/Select.js
@@ -226,9 +226,7 @@ class Select extends UI5Element {
if (this._isPickerOpen) {
popover.close();
- this.opened = false;
} else {
- this.opened = true;
popover.openBy(this);
}
}
diff --git a/packages/main/src/features/InputSuggestions.js b/packages/main/src/features/InputSuggestions.js
index 184a6888b92f..72798a8426fb 100644
--- a/packages/main/src/features/InputSuggestions.js
+++ b/packages/main/src/features/InputSuggestions.js
@@ -233,7 +233,7 @@ class Suggestions {
_getScrollContainer() {
if (!this._scrollContainer) {
const popover = this._getPopover();
- this._scrollContainer = popover.getDomRef().querySelector(".ui5-popup-content");
+ this._scrollContainer = popover.getDomRef().querySelector(".ui5-popover-content");
}
return this._scrollContainer;
diff --git a/packages/main/src/popup-utils/OpenedPopupsRegistry.js b/packages/main/src/popup-utils/OpenedPopupsRegistry.js
new file mode 100644
index 000000000000..4d7dda05eb72
--- /dev/null
+++ b/packages/main/src/popup-utils/OpenedPopupsRegistry.js
@@ -0,0 +1,45 @@
+import { isEscape } from "@ui5/webcomponents-base/dist/events/PseudoEvents.js";
+
+let registry = [];
+
+const addOpenedPopup = instance => {
+ if (!registry.includes(instance)) {
+ registry.push(instance);
+ }
+
+ if (registry.length === 1) {
+ attachGlobalListener();
+ }
+};
+
+const removeOpenedPopup = instance => {
+ registry = registry.filter(el => {
+ return el !== instance;
+ });
+
+ if (!registry.length) {
+ detachGlobalListener();
+ }
+};
+
+const getOpenedPopups = () => {
+ return [...registry];
+};
+
+const _keydownListener = event => {
+ if (isEscape(event)) {
+ const topPopup = registry[registry.length - 1];
+
+ topPopup && topPopup.close();
+ }
+};
+
+const attachGlobalListener = () => {
+ document.addEventListener("keydown", _keydownListener);
+};
+
+const detachGlobalListener = () => {
+ document.removeEventListener("keydown", _keydownListener);
+};
+
+export { addOpenedPopup, removeOpenedPopup, getOpenedPopups };
diff --git a/packages/main/src/popup-utils/PopoverRegistry.js b/packages/main/src/popup-utils/PopoverRegistry.js
new file mode 100644
index 000000000000..be96ea8ef610
--- /dev/null
+++ b/packages/main/src/popup-utils/PopoverRegistry.js
@@ -0,0 +1,111 @@
+import { isClickInRect } from "./PopupUtils.js";
+import { getOpenedPopups, addOpenedPopup, removeOpenedPopup } from "./OpenedPopupsRegistry.js";
+
+let updateInterval = null;
+const intervalTimeout = 300;
+const openedRegistry = [];
+
+const repositionPopovers = event => {
+ openedRegistry.forEach(popover => {
+ popover.reposition();
+ });
+};
+
+const attachGlobalScrollHandler = () => {
+ document.body.addEventListener("scroll", repositionPopovers, true);
+};
+
+const detachGlobalScrollHandler = () => {
+ document.body.removeEventListener("scroll", repositionPopovers, true);
+};
+
+const runUpdateInterval = () => {
+ updateInterval = setInterval(() => {
+ repositionPopovers();
+ }, intervalTimeout);
+};
+
+const stopUpdateInterval = () => {
+ clearInterval(updateInterval);
+};
+
+const attachGlobalClickHandler = () => {
+ document.addEventListener("mousedown", clickHandler);
+};
+
+const detachGlobalClickHandler = () => {
+ document.removeEventListener("mousedown", clickHandler);
+};
+
+const clickHandler = event => {
+ const openedPopovers = openedRegistry;
+ const openedPopups = getOpenedPopups();
+
+ if (openedPopups.length === 0 || !(openedPopups[openedPopups.length - 1].openBy)) {
+ return;
+ }
+
+ // loop all open popovers
+ for (let i = (openedPopovers.length - 1); i !== -1; i--) {
+ const popover = openedPopovers[i];
+
+ // if popover is modal, opener is clicked or there is one more popover above, skip closing
+ if (popover.modal || popover.isOpenerClicked(event)) {
+ return;
+ }
+
+ if (isClickInRect(event, popover.getBoundingClientRect())) {
+ break;
+ }
+
+ popover.close();
+ }
+};
+
+const attachScrollHandler = popover => {
+ popover && popover.shadowRoot.addEventListener("scroll", repositionPopovers, true);
+};
+
+const detachScrollHandler = popover => {
+ popover && popover.shadowRoot.removeEventListener("scroll", repositionPopovers);
+};
+
+const addOpenedPopover = instance => {
+ addOpenedPopup(instance);
+ openedRegistry.push(instance);
+
+ attachScrollHandler(instance);
+
+ if (openedRegistry.length === 1) {
+ attachGlobalScrollHandler();
+ attachGlobalClickHandler();
+ runUpdateInterval();
+ }
+};
+
+const removeOpenedPopover = instance => {
+ let count = 0;
+
+
+ for (let i = openedRegistry.indexOf(instance); i < openedRegistry.length; i++) {
+ openedRegistry[i].close(false, true);
+ removeOpenedPopup(openedRegistry[i]);
+ detachScrollHandler(openedRegistry[i]);
+ count++;
+ }
+
+ // remove top popovers from registry
+ Array(count).fill().forEach(() => { openedRegistry.pop(); });
+
+ if (!openedRegistry.length) {
+ detachGlobalScrollHandler();
+ detachGlobalClickHandler();
+ stopUpdateInterval();
+ }
+};
+
+const getRegistry = () => {
+ return openedRegistry;
+};
+
+export { addOpenedPopover, removeOpenedPopover, getRegistry };
diff --git a/packages/main/src/popup-utils/PopupUtils.js b/packages/main/src/popup-utils/PopupUtils.js
new file mode 100644
index 000000000000..78a31402881b
--- /dev/null
+++ b/packages/main/src/popup-utils/PopupUtils.js
@@ -0,0 +1,42 @@
+const getFocusedElement = () => {
+ let element = document.activeElement;
+
+ while (element.shadowRoot && element.shadowRoot.activeElement) {
+ element = element.shadowRoot.activeElement;
+ }
+
+ return (element && typeof element.focus === "function") ? element : null;
+};
+
+const isPointInRect = (x, y, rect) => {
+ return x >= rect.left && x <= rect.right
+ && y >= rect.top && y <= rect.bottom;
+};
+
+const isClickInRect = (event, rect) => {
+ let x;
+ let y;
+
+ if (event.touches) {
+ const touch = event.touches[0];
+ x = touch.clientX;
+ y = touch.clientY;
+ } else {
+ x = event.clientX;
+ y = event.clientY;
+ }
+
+ return isPointInRect(x, y, rect);
+};
+
+const getClosedPopupParent = el => {
+ const parent = el.parentElement || (el.getRootNode && el.getRootNode().host);
+
+ if ((parent.openBy && parent.isUI5Element) || parent === document.documentElement) {
+ return parent;
+ }
+
+ return getClosedPopupParent(parent);
+};
+
+export { getFocusedElement, isClickInRect, getClosedPopupParent };
diff --git a/packages/main/src/themes/Input.css b/packages/main/src/themes/Input.css
index a93b29346e71..bde0f9d4e3dc 100644
--- a/packages/main/src/themes/Input.css
+++ b/packages/main/src/themes/Input.css
@@ -20,7 +20,7 @@
height: var(--_ui5_input_compact_height);
}
-:host([data-ui5-compact-size]) input {
+:host([data-ui5-compact-size]) [inner-input] {
padding: 0 0.5rem;
}
@@ -55,7 +55,7 @@
color: var(--sapUiContentDisabledTextColor);
}
-input {
+[inner-input] {
background: transparent;
color: inherit;
border: none;
@@ -73,22 +73,22 @@ input {
font-family: inherit;
}
-input::-webkit-input-placeholder {
+[inner-input]::-webkit-input-placeholder {
/* Chrome/Opera/Safari */
color: var(--sapUiFieldPlaceholderTextColor);
}
-input::-moz-placeholder {
+[inner-input]::-moz-placeholder {
/* Firefox 19+ */
color: var(--sapUiFieldPlaceholderTextColor);
}
-input:-ms-input-placeholder {
+[inner-input]:-ms-input-placeholder {
/* IE 10+ */
color: var(--sapUiFieldPlaceholderTextColor);
}
-input:-moz-placeholder {
+[inner-input]:-moz-placeholder {
/* Firefox 18- */
color: var(--sapUiFieldPlaceholderTextColor);
}
@@ -119,12 +119,12 @@ input:-moz-placeholder {
border-width: var(--_ui5_input_state_border_width);
}
-:host([value-state="Error"]) input,
-:host([value-state="Warning"]) input {
+:host([value-state="Error"]) [inner-input],
+:host([value-state="Warning"]) [inner-input] {
font-style: var(--_ui5_input_error_warning_font_style);
}
-:host([value-state="Error"]) input {
+:host([value-state="Error"]) [inner-input] {
font-weight: var(--_ui5_input_error_font_weight);
}
@@ -149,7 +149,7 @@ input:-moz-placeholder {
}
/* Remove IE's defaut clear button */
-input::-ms-clear {
+[inner-input]::-ms-clear {
height: 0;
width: 0;
}
@@ -170,10 +170,14 @@ input::-ms-clear {
width: var(--sap_wc_input_compact_min_width);
}
-::slotted(ui5-icon) {
+/* TODO: Remove this after parser is fixed
+ - this statement is transformed to "ui5-multi-combobox ui5-icon" which
+ affects all icons in the combobox incuding these in the list items
+*/
+::slotted(ui5-icon[slot="icon"]) {
padding: var(--_ui5_input_icon_padding);
}
-:host([data-ui5-compact-size]) ::slotted(ui5-icon) {
+:host([data-ui5-compact-size]) ::slotted(ui5-icon[slot="icon"]) {
padding: .25rem .5rem;
-}
+}
\ No newline at end of file
diff --git a/packages/main/src/themes/InputIcon.css b/packages/main/src/themes/InputIcon.css
index d6d231d42b88..99f28f12eae0 100644
--- a/packages/main/src/themes/InputIcon.css
+++ b/packages/main/src/themes/InputIcon.css
@@ -11,6 +11,8 @@
outline: none;
padding: var(--_ui5-input-icon-padding);
border-left: 1px solid transparent;
+ min-width: 1rem;
+ min-height: 1rem;
}
[input-icon][data-ui5-compact-size] {
diff --git a/packages/main/src/themes/MultiComboBox.css b/packages/main/src/themes/MultiComboBox.css
index 1f28a2ba930d..a4879170e1d2 100644
--- a/packages/main/src/themes/MultiComboBox.css
+++ b/packages/main/src/themes/MultiComboBox.css
@@ -22,3 +22,9 @@
min-width: 0px;
height: 100%;
}
+
+/* Workaround for IE */
+
+ui5-multi-combobox ui5-tokenizer {
+ flex: 3;
+}
\ No newline at end of file
diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css
index ba30e3d80e9d..21e18104d827 100644
--- a/packages/main/src/themes/Popover.css
+++ b/packages/main/src/themes/Popover.css
@@ -1,7 +1,38 @@
-.ui5-popover-root {
+:host {
+ display: none;
position: fixed;
z-index: 10;
- transform: translate3d(0,0,0); /* Fixes z-index issue on iOS within iframe without side effect in the rest of the browsers */
+ min-width: 6.25rem;
+ background: var(--sapUiGroupContentBackground);
+ box-shadow: var(--sapUiShadowLevel2);
+ border-radius: 0.25rem;
+ min-height: 2rem;
+ box-sizing: border-box;
+}
+
+.ui5-popover-root {
+ background: inherit;
+ border-radius: inherit;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+:host([modal])::before {
+ content: "";
+ position: fixed;
+ background-color: #000000;
+ opacity: 0.6;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ position: fixed;
+ outline: 0 none;
+ z-index: -1;
}
.ui5-popover-arr {
@@ -14,7 +45,7 @@
}
.ui5-popover-arr:after {
- content: " ";
+ content: "";
display: block;
width: 0.7rem;
height: 0.7rem;
@@ -23,51 +54,52 @@
}
/* pointing upward arrow */
-:host([placement-type="Bottom"]) .ui5-popover-arr {
+:host([actual-placement-type="Bottom"]) .ui5-popover-arr {
left: calc(50% - 0.5625rem);
top: -0.5rem;
height: 0.5625rem;
}
-:host([placement-type="Bottom"]) .ui5-popover-arr:after {
+:host([actual-placement-type="Bottom"]) .ui5-popover-arr:after {
margin: 0.1875rem 0 0 0.1875rem;
box-shadow: -0.375rem 0.375rem 0.75rem 0 var(--_ui5_popover_arrow_shadow_color), 0 0 0.125rem 0 var(--_ui5_popover_arrow_shadow_color);
}
/* pointing right arrow */
-:host([placement-type="Left"]) .ui5-popover-arr {
+:host([actual-placement-type="Left"]) .ui5-popover-arr {
top: calc(50% - 0.5625rem);
right: -0.5625rem;
width: 0.5625rem;
}
-:host([placement-type="Left"]) .ui5-popover-arr:after {
+:host([actual-placement-type="Left"]) .ui5-popover-arr:after {
margin: 0.1875rem 0 0 -0.375rem;
box-shadow: -0.375rem -0.375rem 0.75rem 0 var(--_ui5_popover_arrow_shadow_color), 0 0 0.125rem 0 var(--_ui5_popover_arrow_shadow_color);
}
/* pointing downward arrow */
-:host([placement-type="Top"]) .ui5-popover-arr {
+:host([actual-placement-type="Top"]) .ui5-popover-arr {
left: calc(50% - 0.5625rem);
height: 0.5625rem;
+ bottom: calc(-1 * (var(--_ui5_popover_content_padding) + 2px));
}
-:host([placement-type="Top"]) .ui5-popover-arr:after {
+:host([actual-placement-type="Top"]) .ui5-popover-arr:after {
margin: -0.375rem 0 0 0.125rem;
box-shadow: 0.375rem -0.375rem 0.75rem 0 var(--_ui5_popover_arrow_shadow_color), 0 0 0.125rem 0 var(--_ui5_popover_arrow_shadow_color);
}
/* pointing left arrow */
-:host(:not([placement-type])) .ui5-popover-arr,
-:host([placement-type="Right"]) .ui5-popover-arr {
+:host(:not([actual-placement-type])) .ui5-popover-arr,
+:host([actual-placement-type="Right"]) .ui5-popover-arr {
left: -0.5625rem;
top: calc(50% - 0.5625rem);
width: 0.5625rem;
height: 1rem;
}
-:host(:not([placement-type])) .ui5-popover-arr:after,
-:host([placement-type="Right"]) .ui5-popover-arr:after {
+:host(:not([actual-placement-type])) .ui5-popover-arr:after,
+:host([actual-placement-type="Right"]) .ui5-popover-arr:after {
margin: 0.125rem 0 0 0.25rem;
box-shadow: 0.375rem 0.375rem 0.75rem 0 var(--_ui5_popover_arrow_shadow_color), 0 0 0.125rem 0 var(--_ui5_popover_arrow_shadow_color);
}
@@ -75,3 +107,51 @@
:host([no-arrow]) .ui5-popover-arr {
display: none;
}
+
+.ui5-popover-header-root,
+.ui5-popup-header-text {
+ border-bottom: 1px solid var(--sapUiPageFooterBorderColor);
+}
+
+.ui5-popover-footer-root {
+ background: var(--sapUiPageFooterBackground);
+ border-top: 1px solid var(--sapUiPageFooterBorderColor);
+ color: var(--sapUiPageFooterTextColor);
+}
+
+.ui5-popover-header-root,
+.ui5-popover-footer-root,
+:host([header-text]) .ui5-popup-header-text {
+ margin: 0;
+ color: var(--sapUiPageHeaderTextColor);
+ font-size: 1rem;
+ font-weight: 400;
+ font-family: var(--sapUiFontFamily);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.ui5-popover-content {
+ overflow: auto;
+
+ /* Consider how to make this top level */
+ padding: var(--_ui5_popover_content_padding);
+}
+
+:host([header-text]) .ui5-popup-header-text {
+ padding: 0 0.25rem;
+ text-align: center;
+ min-height: 3rem;
+ max-height: 3rem;
+ line-height: 3rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 100%;
+ display: inline-block;
+}
+
+:host(:not([header-text])) .ui5-popup-header-text {
+ display: none;
+}
diff --git a/packages/main/src/themes/Tokenizer.css b/packages/main/src/themes/Tokenizer.css
index 7a811d6611cb..92122e473687 100644
--- a/packages/main/src/themes/Tokenizer.css
+++ b/packages/main/src/themes/Tokenizer.css
@@ -69,7 +69,7 @@
overflow: auto;
}
-:host ::slotted(ui5-token) {
+::slotted(ui5-token) {
margin-left: .25rem;
}
diff --git a/packages/main/test/pageobjects/DatePickerFGPage.js b/packages/main/test/pageobjects/DatePickerFGPage.js
index 1a488251f0d6..a924ba85f455 100644
--- a/packages/main/test/pageobjects/DatePickerFGPage.js
+++ b/packages/main/test/pageobjects/DatePickerFGPage.js
@@ -1,7 +1,7 @@
class DatePickerFGPage {
get dpStart() { return $('#ui5-datepicker--startDate'); }
- get startPopoverContent() { return browser.$("#ui5-datepicker--startDate").shadow$("ui5-popover").shadow$(".ui5-popup-root"); }
+ get startPopoverContent() { return browser.$("#ui5-datepicker--startDate").shadow$("ui5-popover") }
get startInnerInput() { return browser.$("#ui5-datepicker--startDate").shadow$("ui5-input").shadow$("input"); }
get dpEnd() { return $('#ui5-datepicker--endDate'); }
diff --git a/packages/main/test/pageobjects/DatePickerTestPage.js b/packages/main/test/pageobjects/DatePickerTestPage.js
index 041d5b54fd04..365f805a864c 100644
--- a/packages/main/test/pageobjects/DatePickerTestPage.js
+++ b/packages/main/test/pageobjects/DatePickerTestPage.js
@@ -20,7 +20,7 @@ class DatePickerTestPage {
}
get popoverContent() {
- return browser.$(this._sut).shadow$("ui5-popover").shadow$(".ui5-popup-root");
+ return browser.$(this._sut).shadow$("ui5-popover").shadow$(".ui5-popover-root");
}
get calendar() {
@@ -86,6 +86,7 @@ class DatePickerTestPage {
}
isPickerOpen() {
+
return browser.execute((id) => {
return document.querySelector(id).isOpen();
}, this._sut);
diff --git a/packages/main/test/sap/ui/webcomponents/main/pages/MultiComboBox.html b/packages/main/test/sap/ui/webcomponents/main/pages/MultiComboBox.html
index a80d6e8c994c..e2fdad19ee55 100644
--- a/packages/main/test/sap/ui/webcomponents/main/pages/MultiComboBox.html
+++ b/packages/main/test/sap/ui/webcomponents/main/pages/MultiComboBox.html
@@ -176,7 +176,7 @@