@vaadin-component-factory/vcf-popup
Advanced tools
Comparing version 24.0.1 to 24.0.2
{ | ||
"name": "@vaadin-component-factory/vcf-popup", | ||
"version": "24.0.1", | ||
"version": "24.0.2", | ||
"description": "Vaadin Component Factory Popup for Polymer 3", | ||
@@ -58,2 +58,7 @@ "main": "theme/lumo/vcf-popup.js", | ||
"@vaadin/button": "^24.0.0", | ||
"@vaadin/radio-group": "^24.0.0", | ||
"@vaadin/checkbox-group": "^24.0.0", | ||
"@vaadin/text-area": "^24.0.0", | ||
"@vaadin/text-field": "^24.0.0", | ||
"@vaadin/time-picker": "^24.0.0", | ||
"@polymer/iron-demo-helpers": "^3.0.0-pre.19", | ||
@@ -60,0 +65,0 @@ "@polymer/test-fixture": "^4.0.2", |
@@ -119,2 +119,6 @@ /* | ||
const pointerArrow = document.createElement('div'); | ||
pointerArrow.setAttribute('part', 'pointer-arrow'); | ||
overlayPart.appendChild(pointerArrow); | ||
const headerContainer = document.createElement('header'); | ||
@@ -163,3 +167,28 @@ headerContainer.setAttribute('part', 'header'); | ||
footerRenderer: Function | ||
footerRenderer: Function, | ||
/** | ||
* Position of the popup overlay with respect to its target. | ||
* Supported values: | ||
* `bottom` - under the target element | ||
* `end` - in LTR environment to the right of the target element, in RTL environment to the left | ||
*/ | ||
preferredPosition: { | ||
type: String, | ||
value: 'bottom', | ||
reflectToAttribute: true, | ||
observer: '__preferredPositionChanged' | ||
}, | ||
/** | ||
* Alignment of the popup with respect to its target. | ||
* Supported values: | ||
* `center` - the popup will be aligned to the center of the target element | ||
* By default alignment is not set | ||
*/ | ||
preferredAlignment: { | ||
type: String, | ||
reflectToAttribute: true, | ||
observer: '__preferredAlignmentChanged' | ||
} | ||
}; | ||
@@ -209,4 +238,20 @@ } | ||
}); | ||
this._pointerArrow = this.shadowRoot.querySelector('[part="pointer-arrow"]'); | ||
} | ||
__preferredPositionChanged(position) { | ||
if (position === 'bottom') { | ||
this.noHorizontalOverlap = false; | ||
this.noVerticalOverlap = true; | ||
} else if (position === 'end') { | ||
this.noHorizontalOverlap = true; | ||
this.noVerticalOverlap = false; | ||
} | ||
} | ||
__preferredAlignmentChanged() { | ||
this._updatePosition(); | ||
} | ||
/** @private */ | ||
@@ -395,4 +440,195 @@ __createContainer(slot) { | ||
} | ||
/** | ||
* @protected | ||
* @override | ||
*/ | ||
_updatePosition() { | ||
super._updatePosition(); | ||
if (!this.positionTarget) { | ||
return; | ||
} | ||
if (this.preferredPosition === 'end' && this.preferredAlignment === 'center') { | ||
this._centerVertically(); | ||
} | ||
if (this.hasAttribute('highlight-target')) { | ||
this.highlightTargetInBackdrop(); | ||
} else { | ||
this.$.backdrop.style.clipPath = null; | ||
} | ||
if (this._theme && this._theme.includes('pointer-arrow')) { | ||
this._updatePointerArrowPosition(); | ||
} | ||
} | ||
highlightTargetInBackdrop() { | ||
const ENLARGE_TARGET_HOLE_BY_PIXELS = 5; | ||
let targetRect = this.positionTarget.getBoundingClientRect(); | ||
targetRect = this._addPixelsAroundRect(targetRect, ENLARGE_TARGET_HOLE_BY_PIXELS); | ||
this._makeHoleInBackdrop(targetRect); | ||
this._repositionPopupToPointToBackropHole(ENLARGE_TARGET_HOLE_BY_PIXELS); | ||
} | ||
_repositionPopupToPointToBackropHole(pixelsToMove) { | ||
let pxShift = pixelsToMove; | ||
if (this.preferredPosition === 'end') { | ||
pxShift = -pxShift; | ||
} | ||
if (this.style.bottom) { | ||
this.style.bottom = parseFloat(this.style.bottom) + pxShift + 'px'; | ||
} | ||
if (this.style.left) { | ||
this.style.left = parseFloat(this.style.left) - pxShift + 'px'; | ||
} | ||
if (this.style.top) { | ||
this.style.top = parseFloat(this.style.top) + pxShift + 'px'; | ||
} | ||
if (this.style.right) { | ||
this.style.right = parseFloat(this.style.right) - pxShift + 'px'; | ||
} | ||
} | ||
_makeHoleInBackdrop(targetRect) { | ||
const topLeft = targetRect.x + 'px ' + targetRect.y + 'px'; | ||
const topRight = targetRect.right + 'px ' + targetRect.y + 'px'; | ||
const bottomRight = targetRect.right + 'px ' + targetRect.bottom + 'px'; | ||
const bottomLeft = targetRect.x + 'px ' + targetRect.bottom + 'px'; | ||
this.$.backdrop.style.clipPath = `polygon(0px 0px,0 100%,100% 100%,100% 0px,0px 0px, ${topLeft}, ${topRight},${bottomRight},${bottomLeft},${topLeft})`; | ||
} | ||
_centerVertically() { | ||
const targetRect = this.positionTarget.getBoundingClientRect(); | ||
const offset = targetRect.height / 2 - this._getNormalizedOverlayHeight() / 2; | ||
if (this.style.top) { | ||
const currentValue = parseFloat(this.style.top); | ||
this.style.top = Math.max(currentValue + offset, 15) + 'px'; | ||
} | ||
if (this.style.bottom) { | ||
const currentValue = parseFloat(this.style.bottom); | ||
this.style.bottom = Math.max(currentValue + offset, 15) + 'px'; | ||
} | ||
} | ||
_updatePointerArrowPosition() { | ||
new PopupPointerArrowPositionUpdater(this).updatePosition(); | ||
} | ||
_getNormalizedOverlayHeight() { | ||
// Using previous size to fix a case where window resize may cause the overlay to be squeezed | ||
// smaller than its current space before the fit-calculations. Taken from PositionMixin#__shouldAlignStartHorizontally(). | ||
return this.requiredVerticalSpace || Math.max(this.__oldContentHeight || 0, this.$.overlay.offsetHeight); | ||
} | ||
_getNormalizedOverlayWidth() { | ||
// Using previous size to fix a case where window resize may cause the overlay to be squeezed | ||
// smaller than its current space before the fit-calculations. Taken from PositionMixin#__shouldAlignStartVertically(). | ||
return Math.max(this.__oldContentWidth || 0, this.$.overlay.offsetWidth); | ||
} | ||
_addPixelsAroundRect(targetRect, pixels) { | ||
targetRect.x = targetRect.x - pixels; | ||
targetRect.y = targetRect.y - pixels; | ||
targetRect.width = targetRect.width + 2 * pixels; | ||
targetRect.height = targetRect.height + 2 * pixels; | ||
return targetRect; | ||
} | ||
} | ||
class PopupPointerArrowPositionUpdater { | ||
constructor(popupOverlay) { | ||
this._pointerArrow = popupOverlay._pointerArrow; | ||
this._preferredPosition = popupOverlay.preferredPosition; | ||
this._isPopupStartAligned = popupOverlay.hasAttribute('start-aligned'); | ||
this._isPopupTopAligned = popupOverlay.hasAttribute('top-aligned'); | ||
this._targetRect = popupOverlay.positionTarget.getBoundingClientRect(); | ||
this._pointerArrowRect = this._pointerArrow.getBoundingClientRect(); | ||
this._overlayRect = popupOverlay.$.overlay.getBoundingClientRect(); | ||
this._overlayWidth = popupOverlay._getNormalizedOverlayWidth(); | ||
this._overlayHeight = popupOverlay._getNormalizedOverlayHeight(); | ||
} | ||
updatePosition() { | ||
this._clearPositionProperties(); | ||
if (this._preferredPosition === 'bottom') { | ||
this._positionArrowHorizontally(); | ||
} | ||
if (this._preferredPosition === 'end') { | ||
this._positionArrowVertically(); | ||
} | ||
} | ||
_clearPositionProperties() { | ||
this._pointerArrow.style.top = null; | ||
this._pointerArrow.style.bottom = null; | ||
this._pointerArrow.style.left = null; | ||
this._pointerArrow.style.right = null; | ||
} | ||
_positionArrowHorizontally() { | ||
if (this._isTargetWiderThanOverlay()) { | ||
this._alignToHorizontalCenterOfOverlay(); | ||
} else { | ||
this._alignToHorizontalCenterOfTarget(); | ||
} | ||
} | ||
_positionArrowVertically() { | ||
if (this._isTargetTallerThanOverlay()) { | ||
this._alignToVerticalCenterOfOverlay(); | ||
} else { | ||
this._alignToVerticalCenterOfTarget(); | ||
} | ||
} | ||
_isTargetWiderThanOverlay() { | ||
return this._targetRect.width > this._overlayWidth; | ||
} | ||
_isTargetTallerThanOverlay() { | ||
return this._targetRect.height > this._overlayHeight; | ||
} | ||
_alignToHorizontalCenterOfTarget() { | ||
const offset = this._targetRect.width / 2 - this._pointerArrowRect.width / 2; | ||
if (this._isPopupStartAligned) { | ||
this._pointerArrow.style.left = offset + 'px'; | ||
} else { | ||
this._pointerArrow.style.right = offset + 'px'; | ||
} | ||
} | ||
_alignToHorizontalCenterOfOverlay() { | ||
const offset = this._overlayWidth / 2 - this._pointerArrowRect.width / 2; | ||
this._pointerArrow.style.left = offset + 'px'; | ||
} | ||
_alignToVerticalCenterOfOverlay() { | ||
const offset = this._overlayHeight / 2 - this._pointerArrowRect.height / 2; | ||
this._pointerArrow.style.top = offset + 'px'; | ||
} | ||
_alignToVerticalCenterOfTarget() { | ||
let offset = this._targetRect.height / 2 - this._pointerArrowRect.height / 2; | ||
if (this._isPopupTopAligned) { | ||
offset = offset + (this._targetRect.y - this._overlayRect.y); | ||
offset = Math.max(offset, 3); // do not display pointer arrow at the very corner of the popup, but slightly below it | ||
this._pointerArrow.style.top = offset + 'px'; | ||
} else { | ||
offset = offset + (this._overlayRect.bottom - this._targetRect.bottom); | ||
offset = Math.max(offset, 3); // do not display pointer arrow at the very corner of the popup, but slightly above it | ||
this._pointerArrow.style.bottom = offset + 'px'; | ||
} | ||
} | ||
} | ||
customElements.define(PopupOverlayElement.is, PopupOverlayElement); |
@@ -38,9 +38,9 @@ /* | ||
theme$="[[theme]]" | ||
with-backdrop="[[_phone]]" | ||
with-backdrop="[[_withBackdrop]]" | ||
phone$="[[_phone]]" | ||
position-target="[[target]]" | ||
no-vertical-overlap | ||
position-target="[[_positionTarget]]" | ||
close-on-scroll="[[closeOnScroll]]" | ||
modeless="[[modeless]]" | ||
focus-trap | ||
focus-trap="[[focusTrap]]" | ||
highlight-target$="[[highlightTarget]]" | ||
restore-focus-on-close | ||
@@ -59,3 +59,3 @@ > | ||
static get version() { | ||
return '24.0.1'; | ||
return '24.0.2'; | ||
} | ||
@@ -95,2 +95,14 @@ | ||
/** | ||
* When set to false (default), the Popup will be shown when the target element (set either by 'for' or 'target' property) | ||
* is clicked. When set to true, you have to open the Popup manually by calling the 'show()' method on the Popup element. | ||
* | ||
* By default, it's set to 'false' for backwards compatibility. | ||
*/ | ||
ignoreTargetClick: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
closeOnClick: { | ||
@@ -161,2 +173,44 @@ type: Boolean, | ||
/** | ||
* When true the overlay will receive focus when opened and | ||
* the Tab and Shift+Tab keys will cycle through the Popup's | ||
* tabbable elements but will not leave the Popup. | ||
*/ | ||
focusTrap: { | ||
type: Boolean, | ||
value: false | ||
}, | ||
/** | ||
* Position of the popup with respect to its target. | ||
* Supported values: | ||
* `bottom` - under the target element | ||
* `end` - in LTR environment to the right of the target element, in RTL environment to the left | ||
*/ | ||
position: { | ||
type: String, | ||
value: 'bottom', | ||
observer: '__positionChanged' | ||
}, | ||
/** | ||
* When true, the popup target will be highlighted, to make it absolutely clear what is the target of the popup. | ||
*/ | ||
highlightTarget: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Alignment of the popup with respect to its target. | ||
* Supported values: | ||
* `center` - the popup will be aligned to the center of the target element | ||
* By default alignment is not set | ||
*/ | ||
alignment: { | ||
type: String, | ||
observer: '__alignmentChanged' | ||
}, | ||
_phone: Boolean, | ||
@@ -166,3 +220,7 @@ | ||
value: '(max-width: 420px), (max-height: 420px)' | ||
} | ||
}, | ||
_positionTarget: Object, | ||
_withBackdrop: Boolean | ||
}; | ||
@@ -172,3 +230,3 @@ } | ||
static get observers() { | ||
return ['_rendererChanged(headerRenderer, footerRenderer)']; | ||
return ['_rendererChanged(headerRenderer, footerRenderer)', '_backdropDisplayChanged(_phone, highlightTarget)']; | ||
} | ||
@@ -181,2 +239,3 @@ | ||
this.hide = this.hide.bind(this); | ||
this.__targetClicked = this.__targetClicked.bind(this); | ||
this._handleOverlayClick = this._handleOverlayClick.bind(this); | ||
@@ -279,2 +338,3 @@ | ||
} | ||
__forChanged(forId) { | ||
@@ -292,2 +352,14 @@ if (forId) { | ||
__positionChanged(position) { | ||
this.$.popupOverlay.preferredPosition = position; | ||
} | ||
__alignmentChanged(alignment) { | ||
this.$.popupOverlay.preferredAlignment = alignment; | ||
} | ||
_backdropDisplayChanged(phone, highlightTarget) { | ||
this._withBackdrop = phone || highlightTarget; | ||
} | ||
__targetChanged(target, oldTarget) { | ||
@@ -303,2 +375,8 @@ if (oldTarget) { | ||
__targetClicked() { | ||
if (!this.ignoreTargetClick) { | ||
this.show(); | ||
} | ||
} | ||
show() { | ||
@@ -313,3 +391,3 @@ this.opened = true; | ||
_handleOverlayClick(event) { | ||
if (!this.closeOnClick && !this._phone) { | ||
if (!this.closeOnClick && !this._withBackdrop) { | ||
event.stopPropagation(); | ||
@@ -321,4 +399,5 @@ } | ||
if (target) { | ||
target.addEventListener('click', this.show); | ||
target.addEventListener('click', this.__targetClicked); | ||
target.setAttribute('has-popup', ''); | ||
this.__setPositionTarget(target); | ||
@@ -332,5 +411,16 @@ // Wait before observing to avoid Chrome issue. | ||
__setPositionTarget(target) { | ||
// Make sure that target element is rendered including shadowRoot | ||
setTimeout(() => { | ||
// position the popup relative to the internal input field for Vaadin components | ||
// which have input fields rather than overall copmonents (including label, | ||
// helper texts etc.) | ||
const inputField = target.shadowRoot && target.shadowRoot.querySelector('[part="input-field"]'); | ||
this._positionTarget = inputField ? inputField : target; | ||
}); | ||
} | ||
_detachFromTarget(target) { | ||
if (target) { | ||
target.removeEventListener('click', this.show); | ||
target.removeEventListener('click', this.__targetClicked); | ||
target.removeAttribute('has-popup'); | ||
@@ -337,0 +427,0 @@ this.__targetVisibilityObserver.unobserve(target); |
@@ -82,2 +82,6 @@ import '@vaadin/vaadin-lumo-styles/spacing.js'; | ||
:host([highlight-target]) [part='backdrop'] { | ||
background-color: var(--lumo-shade-50pct); | ||
} | ||
/* Animations */ | ||
@@ -97,4 +101,85 @@ | ||
} | ||
/* Pointer arrow theme */ | ||
:host([theme~='pointer-arrow'][top-aligned][preferred-position='bottom']:not([phone])) { | ||
padding-top: 0.5rem; | ||
} | ||
:host([theme~='pointer-arrow'][top-aligned][preferred-position='bottom']:not([phone])) [part='pointer-arrow'] { | ||
position: absolute; | ||
border-left: 0.5rem solid transparent; | ||
border-right: 0.5rem solid transparent; | ||
border-bottom: 0.5rem solid var(--lumo-base-color); | ||
top: 0; | ||
height: 0; | ||
width: 0; | ||
filter: drop-shadow(0px -2px 1px var(--lumo-shade-10pct)); | ||
} | ||
:host([theme~='pointer-arrow'][bottom-aligned][preferred-position='bottom']:not([phone])) { | ||
padding-bottom: 0.5rem; | ||
} | ||
/* the following will color the pointer arrow to the same color as the popup footer */ | ||
:host([theme~='pointer-arrow'][has-footer][bottom-aligned][preferred-position='bottom']:not([phone])) | ||
[part='pointer-arrow']:after { | ||
content: ''; | ||
display: block; | ||
position: absolute; | ||
border-left: 0.5rem solid transparent; | ||
border-right: 0.5rem solid transparent; | ||
border-top: 0.5rem solid var(--lumo-contrast-5pct); | ||
bottom: 0; | ||
left: -0.5rem; | ||
height: 0; | ||
width: 0; | ||
} | ||
:host([theme~='pointer-arrow'][bottom-aligned][preferred-position='bottom']:not([phone])) [part='pointer-arrow'] { | ||
position: absolute; | ||
border-left: 0.5rem solid transparent; | ||
border-right: 0.5rem solid transparent; | ||
border-top: 0.5rem solid var(--lumo-base-color); | ||
bottom: 0; | ||
height: 0; | ||
width: 0; | ||
filter: drop-shadow(0px 2px 1px var(--lumo-shade-10pct)); | ||
} | ||
:host([theme~='pointer-arrow'][start-aligned][preferred-position='end']:not([phone])) { | ||
padding-inline-start: 0.5rem; | ||
} | ||
:host([theme~='pointer-arrow'][start-aligned][preferred-position='end']:not([phone])) [part='pointer-arrow'] { | ||
position: absolute; | ||
border-top: 0.5rem solid transparent; | ||
border-bottom: 0.5rem solid transparent; | ||
border-right: 0.5rem solid var(--lumo-base-color); | ||
left: 0; | ||
height: 0; | ||
width: 0; | ||
filter: drop-shadow(-2px 0 1px var(--lumo-shade-10pct)); | ||
} | ||
:host([theme~='pointer-arrow'][end-aligned][preferred-position='end']:not([phone])) { | ||
padding-inline-end: 0.5rem; | ||
} | ||
:host([theme~='pointer-arrow'][end-aligned][preferred-position='end']:not([phone])) [part='pointer-arrow'] { | ||
position: absolute; | ||
border-top: 0.5rem solid transparent; | ||
border-bottom: 0.5rem solid transparent; | ||
border-left: 0.5rem solid var(--lumo-base-color); | ||
right: 0; | ||
height: 0; | ||
width: 0; | ||
filter: drop-shadow(2px 0 1px var(--lumo-shade-10pct)); | ||
} | ||
`; | ||
registerStyles('vcf-popup-overlay', [overlay, popupOverlay], { moduleId: 'lumo-vcf-popup-overlay' }); |
54334
1056
55