Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Menuable): correctly calculate position in embedded v-app #13670

Merged
merged 13 commits into from
Jun 1, 2021
Merged
2 changes: 0 additions & 2 deletions packages/vuetify/src/components/VMenu/VMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { VThemeProvider } from '../VThemeProvider'
import Activatable from '../../mixins/activatable'
import Delayable from '../../mixins/delayable'
import Dependent from '../../mixins/dependent'
import Detachable from '../../mixins/detachable'
import Menuable from '../../mixins/menuable'
import Returnable from '../../mixins/returnable'
import Roundable from '../../mixins/roundable'
Expand All @@ -33,7 +32,6 @@ import { VNode, VNodeDirective, VNodeData } from 'vue'
const baseMixins = mixins(
Dependent,
Delayable,
Detachable,
Menuable,
Returnable,
Roundable,
Expand Down
3 changes: 1 addition & 2 deletions packages/vuetify/src/components/VTooltip/VTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import Activatable from '../../mixins/activatable'
import Colorable from '../../mixins/colorable'
import Delayable from '../../mixins/delayable'
import Dependent from '../../mixins/dependent'
import Detachable from '../../mixins/detachable'
import Menuable from '../../mixins/menuable'
import Toggleable from '../../mixins/toggleable'

Expand All @@ -18,7 +17,7 @@ import { VNode } from 'vue'
import mixins from '../../util/mixins'

/* @vue/component */
export default mixins(Colorable, Delayable, Dependent, Detachable, Menuable, Toggleable).extend({
export default mixins(Colorable, Delayable, Dependent, Menuable, Toggleable).extend({
name: 'v-tooltip',

props: {
Expand Down
29 changes: 29 additions & 0 deletions packages/vuetify/src/mixins/menuable/__tests__/menuable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MountOptions,
Wrapper,
} from '@vue/test-utils'
import VApp from '../../../components/VApp'

describe('menuable.ts', () => {
const Mock = Menuable.extend({
Expand Down Expand Up @@ -67,4 +68,32 @@ describe('menuable.ts', () => {

expect(wrapper.vm.computedLeft).toBe(-200)
})

it('should have the correct position non attached', async () => {
const wrapper = mount({
render (h) {
return h(VApp, [h(Mock)])
},
}, {
mocks: {
sync: false,
$vuetify: {
theme: {},
rtl: false,
},
},
})

await wrapper.vm.$nextTick()

const { vm } = wrapper.find(Mock) as Wrapper<Instance>

Object.assign(vm.dimensions.activator, { top: 100, left: 80 })
Object.assign(vm.dimensions.content, { width: 300, height: 50 })

await wrapper.vm.$nextTick()

expect(vm.computedTop).toBe(100)
expect(vm.computedLeft).toBe(80)
})
})
53 changes: 48 additions & 5 deletions packages/vuetify/src/mixins/menuable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,42 @@
import Positionable from '../positionable'
import Stackable from '../stackable'
import Activatable from '../activatable'
import Detachable from '../detachable'

// Utilities
import mixins, { ExtractVue } from '../../util/mixins'
import { convertToUnit } from '../../util/helpers'

// Types
import { VNode } from 'vue'

const baseMixins = mixins(
Stackable,
Positionable,
Activatable
Activatable,
Detachable,
)

interface dimensions {
top: number
left: number
bottom: number
right: number
width: number
height: number
offsetTop: number
scrollHeight: number
offsetLeft: number
}

interface options extends ExtractVue<typeof baseMixins> {
attach: boolean | string | Element
offsetY: boolean
offsetX: boolean
dimensions: {
activator: dimensions
content: dimensions
}
$refs: {
content: HTMLElement
activator: HTMLElement
Expand Down Expand Up @@ -74,6 +94,7 @@ export default baseMixins.extend<options>().extend({
},

data: () => ({
activatorNode: [] as VNode[],
absoluteX: 0,
absoluteY: 0,
activatedBy: null as EventTarget | null,
Expand Down Expand Up @@ -101,6 +122,7 @@ export default baseMixins.extend<options>().extend({
scrollHeight: 0,
},
},
relativeYOffset: 0,
hasJustFocused: false,
hasWindow: false,
inputActivator: false,
Expand Down Expand Up @@ -148,6 +170,9 @@ export default baseMixins.extend<options>().extend({
hasActivator (): boolean {
return !!this.$slots.activator || !!this.$scopedSlots.activator || !!this.activator || !!this.inputActivator
},
absoluteYOffset (): number {
return this.pageYOffset - this.relativeYOffset
},
},

watch: {
Expand All @@ -165,6 +190,16 @@ export default baseMixins.extend<options>().extend({

beforeMount () {
this.hasWindow = typeof window !== 'undefined'

if (this.hasWindow) {
window.addEventListener('resize', this.updateDimensions, false)
}
},

beforeDestroy () {
if (this.hasWindow) {
window.removeEventListener('resize', this.updateDimensions, false)
}
},

methods: {
Expand Down Expand Up @@ -205,7 +240,7 @@ export default baseMixins.extend<options>().extend({
},
calcYOverflow (top: number) {
const documentHeight = this.getInnerHeight()
const toTop = this.pageYOffset + documentHeight
const toTop = this.absoluteYOffset + documentHeight
const activator = this.dimensions.activator
const contentHeight = this.dimensions.content.height
const totalHeight = top + contentHeight
Expand All @@ -224,8 +259,8 @@ export default baseMixins.extend<options>().extend({
} else if (isOverflowing && !this.allowOverflow) {
top = toTop - contentHeight - 12
// If overflowing top
} else if (top < this.pageYOffset && !this.allowOverflow) {
top = this.pageYOffset + 12
} else if (top < this.absoluteYOffset && !this.allowOverflow) {
top = this.absoluteYOffset + 12
}

return top < 12 ? 12 : top
Expand Down Expand Up @@ -369,7 +404,15 @@ export default baseMixins.extend<options>().extend({

// Display and hide to get dimensions
this.sneakPeek(() => {
this.$refs.content && (dimensions.content = this.measure(this.$refs.content))
if (this.$refs.content && this.$refs.content.offsetParent) {
const offsetRect = this.getRoundedBoundedClientRect(this.$refs.content.offsetParent)

this.relativeYOffset = window.pageYOffset + offsetRect.top
dimensions.activator.top -= this.relativeYOffset
dimensions.activator.left -= window.pageXOffset + offsetRect.left

dimensions.content = this.measure(this.$refs.content)
}

this.dimensions = dimensions
})
Expand Down