Vuetify: [Feature Request] Make dropdown position calculation compatible for embedded Vuetify apps

Created on 30 Jun 2019  路  8Comments  路  Source: vuetifyjs/vuetify

Problem to solve

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.

Proposed solution

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: relatives 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.

VSelect bug

Most helpful comment

Is there a workaround meanwhile ? I am also embedding a Vuetify application, and I am confronted with the same problem.

All 8 comments

Can you provide a reproduction?

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:

`
v-model="selectedOpen"
: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>

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antway picture Antway  路  3Comments

jofftiquez picture jofftiquez  路  3Comments

smousa picture smousa  路  3Comments

paladin2005 picture paladin2005  路  3Comments

radicaled picture radicaled  路  3Comments