If you have a Vuetify app that is embedded into a page where it is not taking up the full browser window, an app on a WordPress page for example, and you have a relative positioned element as its parent, dropdowns like VSelect will be offset.
I tried digging into the source code, but could not really track down the logic, but the only way for me to get Vuetify's dropdowns to work in an embedded app situation, I had to remove all position: relative
s from parent nodes. Add position: relative
to v-app didn't work, so I'm guessing the calculation is based on the window, but somehow relative position parents alter the calculation.
I propose rewriting how the positioning calculation is performed and making sure v-app is the reference used for positioning.
Can you provide a reproduction?
@jacekkarczmarczyk
Here is a reproduction:
https://codesandbox.io/s/breaking-vuetify-dropdown-calculation-b6fw4
Same bug here, dropdown opens 20kilometers away from where it's supposed to in embedded app
https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/mixins/menuable/index.ts
dimensions: {
activator: {
top: 0,
left: 0,
bottom: 0,
right: 0,
width: 0,
height: 0,
offsetTop: 0,
scrollHeight: 0,
offsetLeft: 0,
},
content: {
top: 0,
left: 0,
bottom: 0,
right: 0,
width: 0,
height: 0,
offsetTop: 0,
scrollHeight: 0,
},
},
//...
computedLeft () {
const a = this.dimensions.activator
const c = this.dimensions.content
const activatorLeft = (this.attach !== false ? a.offsetLeft : a.left) || 0
const minWidth = Math.max(a.width, c.width)
let left = 0
left += this.left ? activatorLeft - (minWidth - a.width) : activatorLeft
if (this.offsetX) {
const maxWidth = isNaN(Number(this.maxWidth))
? a.width
: Math.min(a.width, Number(this.maxWidth))
left += this.left ? -maxWidth : a.width
}
if (this.nudgeLeft) left -= parseInt(this.nudgeLeft)
if (this.nudgeRight) left += parseInt(this.nudgeRight)
return left
},
computedTop () {
const a = this.dimensions.activator
const c = this.dimensions.content
let top = 0
if (this.top) top += a.height - c.height
if (this.attach !== false) top += a.offsetTop
else top += a.top + this.pageYOffset
if (this.offsetY) top += this.top ? -a.height : a.height
if (this.nudgeTop) top -= parseInt(this.nudgeTop)
if (this.nudgeBottom) top += parseInt(this.nudgeBottom)
return top
},
I think the bug is that the the closest relative element is missing in the calculation, so the position is calculated according to an assumed 0,0 instead of the app or whatever closest relative element which has a different offset
I'd say you'd need to add a .getBoundingClientRect()
on the closest relative and subtract the offset from the calculation
if (!this.attach) {
let p = this.getActivator();
//Find the closest positioned element
while (p && p.css('position') !== 'relative' && p.css('position') !== 'absolute') {
p = p.parent();
}
if (p) {
const rect = p.getBoundingClientRect();
left -= rect.offsetLeft;
top -= rect.offsetTop;
}
}
So it would give this
computedRelativeOffset () {
let p = this.getActivator();
//Find the closest positioned element
const isRelative = (el) => {
let pos = window.getComputedStyle(el, null).getPropertyValue("position");
return pos === 'relative' || pos === 'absolute'
}
while (p && !isRelative(p)) {
p = p.parent();
}
if (p) {
const rect = p.getBoundingClientRect()
return { left: -rect.offsetLeft, top: -rect.offsetTop }
}
return { left: 0, top: 0 }
},
computedLeft () {
const a = this.dimensions.activator
const c = this.dimensions.content
const activatorLeft = (this.attach !== false ? a.offsetLeft : a.left) || 0
const minWidth = Math.max(a.width, c.width)
let left = 0
left += this.left ? activatorLeft - (minWidth - a.width) : activatorLeft
if (this.offsetX) {
const maxWidth = isNaN(Number(this.maxWidth))
? a.width
: Math.min(a.width, Number(this.maxWidth))
left += this.left ? -maxWidth : a.width
}
if (this.nudgeLeft) left -= parseInt(this.nudgeLeft)
if (this.nudgeRight) left += parseInt(this.nudgeRight)
if (this.attach !== false) {
left += this.computedRelativeOffset().left
}
return left
},
computedTop () {
const a = this.dimensions.activator
const c = this.dimensions.content
let top = 0
if (this.top) top += a.height - c.height
if (this.attach !== false) top += a.offsetTop
else top += a.top + this.pageYOffset
if (this.offsetY) top += this.top ? -a.height : a.height
if (this.nudgeTop) top -= parseInt(this.nudgeTop)
if (this.nudgeBottom) top += parseInt(this.nudgeBottom)
if (this.attach !== false) {
top += this.computedRelativeOffset().top
}
return top
},
I need to test it and do a PR, but I'm having trouble getting a cloned vuetify install to build in dev..
Is there a workaround meanwhile ? I am also embedding a Vuetify application, and I am confronted with the same problem.
The Quick Run Down: I am building a calendar using vuetify to embed as a widget for wordpress sites and the popups for the scheduled events appear way below the widget due to OP.
My Current Solution: For now I have a band-aid solution for anyone else in my situation. Referring to the following link posted below, I used "attach" on the pop-up menu and set it to #app so that the pop-up menu will be attached to the div that the Vue app is generated in.
Ex:
`
:close-on-content-click="false"
:activator="selectedElement"
attach="#app"
offset-x
`
It's not a perfect solution but something for the mean time. Also if you want to you can set the CSS for the popup to be absolute then position the exact spot of the div with something like the following:
.centered {
position: absolute ;
top: 50%;
left: 50%;
}
Stack Overflow Reference:
(https://stackoverflow.com/questions/50500842/how-to-attach-a-vuetify-v-menu-to-its-parent-element-in-a-scrollable-list)
attach="#app" offset-x
@dstearle I'm experiencing a similar issue but all my components built on the v-menu component appear empty i.e. are empty dom elements. How did you setup your wordpress/vue/vuetify app?
I've created a wp plugin and enqueued the vue and vuetify files there which seems to work but then I get this strange behavious.
Is there a workaround meanwhile ? I am also embedding a Vuetify application, and I am confronted with the same problem.
I too face this issue, is there a verified workaround as the solution from @dstearle doesn't seem to work.
For me the solution I found that works is to use the attach prop without a value i.e.
<v-select :items="items" attach></v-select>
Most helpful comment
Is there a workaround meanwhile ? I am also embedding a Vuetify application, and I am confronted with the same problem.