diff --git a/src/vaadin-context-menu-overlay.html b/src/vaadin-context-menu-overlay.html index c958e79f..c9a51fcb 100644 --- a/src/vaadin-context-menu-overlay.html +++ b/src/vaadin-context-menu-overlay.html @@ -92,7 +92,8 @@ xMax: overlayRect.right - contentRect.width, yMax, left: overlayRect.left, - top: overlayRect.top + top: overlayRect.top, + width: contentRect.width }; } } diff --git a/src/vaadin-context-menu.html b/src/vaadin-context-menu.html index ed08da7a..c38fc1cb 100644 --- a/src/vaadin-context-menu.html +++ b/src/vaadin-context-menu.html @@ -556,28 +556,51 @@ ['right-aligned', 'bottom-aligned'].forEach(attr => overlay.removeAttribute(attr)); // Maximum x and y values are imposed by content size and overlay limits. - const {xMax, yMax, left, top} = overlay.getBoundaries(); + const {xMax, yMax, left, top, width} = overlay.getBoundaries(); // Reuse saved x and y event values, in order to this method be used async // in the `vaadin-overlay-change` which guarantees that overlay is ready - const x = this.__x || left; + let x = this.__x || left; const y = this.__y || top; // Select one overlay corner and move to the event x/y position. - // Then set styling attrs for flex-aligning the content appropriatelly. + // Then set styling attrs for flex-aligning the content appropriately. const wdthVport = document.documentElement.clientWidth; const hghtVport = document.documentElement.clientHeight; + // Align to the parent menu overlay, if any. const parent = overlay.parentOverlay; - if (parent && parent.hasAttribute('right-aligned')) { - const parentStyle = getComputedStyle(parent); + let alignedToParent = false; + if (parent) { const parentContentRect = parent.$.overlay.getBoundingClientRect(); - overlay.setAttribute('right-aligned', ''); - style.right = parseFloat(parentStyle.right) + parentContentRect.width + 'px'; - } else if (x < wdthVport / 2 || x < xMax) { - style.left = x + 'px'; - } else { - style.right = Math.max(0, (wdthVport - x)) + 'px'; - overlay.setAttribute('right-aligned', ''); + if (parent.hasAttribute('right-aligned')) { + const parentStyle = getComputedStyle(parent); + + const getPadding = (el, direction) => { + return parseFloat(getComputedStyle(el.$.content)['padding' + direction]); + }; + const right = parseFloat(parentStyle.right) + parentContentRect.width; + const padding = getPadding(parent, 'Left') + getPadding(overlay, 'Right'); + + // Preserve right-aligned, if possible. + if ((wdthVport - (right - padding)) > width) { + overlay.setAttribute('right-aligned', ''); + style.right = right - padding + 'px'; + alignedToParent = true; + } else if (!(x < wdthVport / 2 || x < xMax)) { + // We use parent menu item to predict "x", which isn't accurate. + // Adjust it now, based on the rendered sub-menu content width. + x = x - (width - parentContentRect.width); + } + } + } + + if (!alignedToParent) { + if (x < wdthVport / 2 || x < xMax) { + style.left = x + 'px'; + } else { + style.right = Math.max(0, (wdthVport - x)) + 'px'; + overlay.setAttribute('right-aligned', ''); + } } if (y < hghtVport / 2 || y < yMax) { style.top = y + 'px'; diff --git a/test/items.html b/test/items.html index 174b8539..4271982a 100644 --- a/test/items.html +++ b/test/items.html @@ -56,7 +56,10 @@ {text: 'foo-0', children: [ {text: 'foo-0-0', checked: true}, - {text: 'foo-0-1', disabled: true} + {text: 'foo-0-1', disabled: true}, + {text: 'foo-0-2', children: [ + {text: 'foo-0-2-0'} + ]}, ] }, {text: 'foo-1'} @@ -130,19 +133,69 @@ }); it('should open the subMenu on the left if root menu is right-aligned', done => { + let padding; subMenu.close(); - const rootItemRect = menuComponents()[0].getBoundingClientRect(); - rootMenu.$.overlay.style.removeProperty('left'); - rootMenu.$.overlay.style.right = rootItemRect.width + 'px'; - rootMenu.$.overlay.setAttribute('right-aligned', ''); - open(menuComponents()[0]); + const rootItem = menuComponents()[0]; + const rootItemRect = rootItem.getBoundingClientRect(); + const rootOverlay = rootMenu.$.overlay; + rootOverlay.style.removeProperty('left'); + rootOverlay.style.right = rootItemRect.width + 'px'; + rootOverlay.setAttribute('right-aligned', ''); + padding = parseFloat(getComputedStyle(rootOverlay.$.content).paddingLeft) * 2; + open(rootItem); Polymer.RenderStatus.afterNextRender(subMenu, () => { expect(subMenu.$.overlay.hasAttribute('right-aligned')).to.be.true; - const rootMenuRect = rootMenu.$.overlay.$.content.getBoundingClientRect(); + const rootMenuRect = rootOverlay.$.content.getBoundingClientRect(); const subMenuRect = subMenu.$.overlay.$.content.getBoundingClientRect(); - expect(subMenuRect.right).to.be.closeTo(rootMenuRect.left, 1); - done(); + expect(subMenuRect.right).to.be.closeTo(rootMenuRect.left + padding, 1); + + const nestedItem = menuComponents(subMenu)[2]; + padding = parseFloat(getComputedStyle(subMenu.$.overlay.$.content).paddingLeft) * 2; + open(nestedItem); + + Polymer.RenderStatus.afterNextRender(subMenu, () => { + const subMenu2 = getSubMenu(subMenu); + expect(subMenu2.$.overlay.hasAttribute('right-aligned')).to.be.true; + const subMenu2Rect = subMenu2.$.overlay.$.content.getBoundingClientRect(); + expect(subMenu2Rect.right).to.be.closeTo(subMenuRect.left + padding, 1); + done(); + }); + }); + }); + + it('should open the second subMenu on the right again if not enough space', done => { + let padding; + subMenu.close(); + rootMenu.items[0].children[2].text = 'foo-0-2-longer'; + + const rootItem = menuComponents()[0]; + const rootItemRect = rootItem.getBoundingClientRect(); + const rootOverlay = rootMenu.$.overlay; + rootOverlay.style.removeProperty('left'); + rootOverlay.style.right = rootItemRect.width + 'px'; + rootOverlay.setAttribute('right-aligned', ''); + padding = parseFloat(getComputedStyle(rootOverlay.$.content).paddingLeft) * 2; + open(rootItem); + + Polymer.RenderStatus.afterNextRender(subMenu, () => { + expect(subMenu.$.overlay.hasAttribute('right-aligned')).to.be.true; + const rootMenuRect = rootOverlay.$.content.getBoundingClientRect(); + const subMenuRect = subMenu.$.overlay.$.content.getBoundingClientRect(); + expect(subMenuRect.right).to.be.closeTo(rootMenuRect.left + padding, 1); + + const nestedItem = menuComponents(subMenu)[2]; + const nestedItemRect = nestedItem.getBoundingClientRect(); + padding = parseFloat(getComputedStyle(subMenu.$.overlay.$.content).paddingLeft) * 2; + open(nestedItem); + + Polymer.RenderStatus.afterNextRender(subMenu, () => { + const subMenu2 = getSubMenu(subMenu); + expect(subMenu2.$.overlay.hasAttribute('right-aligned')).to.be.false; + const subMenu2Rect = subMenu2.$.overlay.$.content.getBoundingClientRect(); + expect(subMenu2Rect.left).to.be.closeTo(nestedItemRect.right, 1); + done(); + }); }); }); }