mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-30 02:08:12 +08:00
fix(components): [tooltip] SSR hydration error caused by random ID (#10541)
This commit is contained in:
parent
d8a116c37f
commit
b456125431
@ -2,7 +2,7 @@ import { nextTick, reactive } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { beforeEach, describe, expect, it, test, vi } from 'vitest'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import { ElFormItem as FormItem } from '@element-plus/components/form'
|
||||
import Autocomplete from '../src/autocomplete.vue'
|
||||
|
||||
@ -321,9 +321,10 @@ describe('Autocomplete.vue', () => {
|
||||
_mount()
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)?.innerHTML).not.toBe(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -333,9 +334,8 @@ describe('Autocomplete.vue', () => {
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)?.innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, test, vi } from 'vitest'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
import triggerEvent from '@element-plus/test-utils/trigger-event'
|
||||
import { ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import { hasClass } from '@element-plus/utils'
|
||||
import ElForm, { ElFormItem } from '@element-plus/components/form'
|
||||
import Cascader from '../src/index.vue'
|
||||
@ -376,9 +376,10 @@ describe('Cascader.vue', () => {
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)!.innerHTML).not.toBe(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -394,9 +395,8 @@ describe('Cascader.vue', () => {
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)!.innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { rAF } from '@element-plus/test-utils/tick'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
import { ElTooltip } from '@element-plus/components/tooltip'
|
||||
import Button from '@element-plus/components/button'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import Dropdown from '../src/dropdown.vue'
|
||||
import DropdownItem from '../src/dropdown-item.vue'
|
||||
import DropdownMenu from '../src/dropdown-menu.vue'
|
||||
@ -785,9 +785,8 @@ describe('Dropdown', () => {
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value).innerHTML).not.toBe('')
|
||||
})
|
||||
|
||||
test('should not mount on the popper container', async () => {
|
||||
@ -812,9 +811,8 @@ describe('Dropdown', () => {
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value).innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,7 @@ import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { rAF } from '@element-plus/test-utils/tick'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import Popconfirm from '../src/popconfirm.vue'
|
||||
|
||||
const AXIOM = 'rem is the best girl'
|
||||
@ -55,9 +55,10 @@ describe('Popconfirm.vue', () => {
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)!.innerHTML).not.toBe(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -75,9 +76,8 @@ describe('Popconfirm.vue', () => {
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)!.innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { POPPER_CONTAINER_SELECTOR, useZIndex } from '@element-plus/hooks'
|
||||
import { usePopperContainerId, useZIndex } from '@element-plus/hooks'
|
||||
import { rAF } from '@element-plus/test-utils/tick'
|
||||
import { ElPopperTrigger } from '@element-plus/components/popper'
|
||||
import Popover from '../src/popover.vue'
|
||||
@ -199,9 +199,10 @@ describe('Popover.vue', () => {
|
||||
_mount()
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)?.innerHTML).not.toBe(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -209,9 +210,8 @@ describe('Popover.vue', () => {
|
||||
_mount({ teleported: false })
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)?.innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -7,7 +7,7 @@ import { EVENT_CODE } from '@element-plus/constants'
|
||||
import { makeMountFunc } from '@element-plus/test-utils/make-mount'
|
||||
import { rAF } from '@element-plus/test-utils/tick'
|
||||
import { CircleClose } from '@element-plus/icons-vue'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import Select from '../src/select.vue'
|
||||
|
||||
vi.mock('lodash-unified', async () => {
|
||||
@ -1502,9 +1502,10 @@ describe('Select', () => {
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)!.innerHTML).not.toBe(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -1542,9 +1543,8 @@ describe('Select', () => {
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value).innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, test, vi } from 'vitest'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
import { ArrowDown, CaretTop, CircleClose } from '@element-plus/icons-vue'
|
||||
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import { hasClass } from '@element-plus/utils'
|
||||
import { ElFormItem } from '@element-plus/components/form'
|
||||
import Select from '../src/select.vue'
|
||||
@ -1910,9 +1910,8 @@ describe('Select', () => {
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
|
||||
).not.toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value).innerHTML).not.toBe('')
|
||||
})
|
||||
|
||||
it('should not mount on the popper container', async () => {
|
||||
@ -1940,9 +1939,8 @@ describe('Select', () => {
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
|
||||
).toBe('')
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value).innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
import { popperContentProps } from '@element-plus/components/popper'
|
||||
import {
|
||||
POPPER_CONTAINER_SELECTOR,
|
||||
useDelayedToggleProps,
|
||||
useNamespace,
|
||||
} from '@element-plus/hooks'
|
||||
import { useDelayedToggleProps, useNamespace } from '@element-plus/hooks'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
const ns = useNamespace('tooltip')
|
||||
@ -14,7 +10,6 @@ export const useTooltipContentProps = buildProps({
|
||||
...popperContentProps,
|
||||
appendTo: {
|
||||
type: definePropType<string | HTMLElement>([String, Object]),
|
||||
default: POPPER_CONTAINER_SELECTOR,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
|
@ -36,7 +36,6 @@
|
||||
@blur="onBlur"
|
||||
@close="onClose"
|
||||
>
|
||||
<!-- Workaround bug #6378 -->
|
||||
<template v-if="!destroyed">
|
||||
<slot />
|
||||
</template>
|
||||
@ -48,6 +47,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onBeforeUnmount, ref, unref, watch } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { usePopperContainerId } from '@element-plus/hooks'
|
||||
import { composeEventHandlers } from '@element-plus/utils'
|
||||
import { ElPopperContent } from '@element-plus/components/popper'
|
||||
import { TOOLTIP_INJECTION_KEY } from '@element-plus/tokens'
|
||||
@ -59,6 +59,8 @@ defineOptions({
|
||||
})
|
||||
|
||||
const props = defineProps(useTooltipContentProps)
|
||||
|
||||
const { selector } = usePopperContainerId()
|
||||
// TODO any is temporary, replace with `InstanceType<typeof ElPopperContent> | null` later
|
||||
const contentRef = ref<any>(null)
|
||||
const destroyed = ref(false)
|
||||
@ -95,6 +97,10 @@ const shouldShow = computed(() => {
|
||||
return props.disabled ? false : unref(open)
|
||||
})
|
||||
|
||||
const appendTo = computed(() => {
|
||||
return props.appendTo || selector.value
|
||||
})
|
||||
|
||||
const contentStyle = computed(() => (props.style ?? {}) as any)
|
||||
|
||||
const ariaHidden = computed(() => !unref(open))
|
||||
|
71
packages/hooks/__tests__/use-id.test.tsx
Normal file
71
packages/hooks/__tests__/use-id.test.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { ID_INJECTION_KEY, useId, useIdInjection } from '../use-id'
|
||||
|
||||
describe('no injection value', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('useIdInjection', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const idInjection = useIdInjection()
|
||||
return idInjection
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.prefix).toMatch(/^\d{0,4}$/)
|
||||
expect(wrapper.vm.current).toBe(0)
|
||||
})
|
||||
|
||||
it('useId', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const id = useId()
|
||||
return { id }
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.id).toMatch(/^el-id-\d{0,4}-\d+$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with injection value', () => {
|
||||
beforeEach(() => {
|
||||
config.global.provide = {
|
||||
[ID_INJECTION_KEY as symbol]: {
|
||||
prefix: 1024,
|
||||
current: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
config.global.provide = {}
|
||||
})
|
||||
|
||||
it('useIdInjection', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const idInjection = useIdInjection()
|
||||
return idInjection
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.prefix).toBe(1024)
|
||||
expect(wrapper.vm.current).toBe(0)
|
||||
})
|
||||
|
||||
it('useId', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const id = useId()
|
||||
return { id }
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.id).toBe('el-id-1024-0')
|
||||
})
|
||||
})
|
@ -1,11 +1,12 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { config, mount, shallowMount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vueuse from '@vueuse/core'
|
||||
import {
|
||||
POPPER_CONTAINER_SELECTOR,
|
||||
usePopperContainer,
|
||||
usePopperContainerId,
|
||||
} from '../use-popper-container'
|
||||
import { ID_INJECTION_KEY } from '../use-id'
|
||||
|
||||
const AXIOM = 'rem is the best girl'
|
||||
|
||||
@ -31,14 +32,62 @@ describe('usePopperContainer', () => {
|
||||
it('should append container to the DOM root', async () => {
|
||||
mountComponent()
|
||||
await nextTick()
|
||||
|
||||
expect(document.body.querySelector(POPPER_CONTAINER_SELECTOR)).toBeDefined()
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)).toBeDefined()
|
||||
})
|
||||
|
||||
it('should not append container to the DOM root', async () => {
|
||||
;(vueuse as any).isClient = false
|
||||
mountComponent()
|
||||
await nextTick()
|
||||
expect(document.body.querySelector(POPPER_CONTAINER_SELECTOR)).toBeNull()
|
||||
const { selector } = usePopperContainerId()
|
||||
expect(document.body.querySelector(selector.value)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('no injection value', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('usePopperContainerId', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const data = usePopperContainerId()
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.id).toMatch(/^el-popper-container-\d{0,4}$/)
|
||||
expect(wrapper.vm.selector).toMatch(/^#el-popper-container-\d{0,4}$/)
|
||||
expect(wrapper.vm.selector).toBe(`#${wrapper.vm.id}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with injection value', () => {
|
||||
beforeEach(() => {
|
||||
config.global.provide = {
|
||||
[ID_INJECTION_KEY as symbol]: {
|
||||
prefix: 1024,
|
||||
current: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
config.global.provide = {}
|
||||
})
|
||||
|
||||
it('usePopperContainerId', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const data = usePopperContainerId()
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.id).toBe('el-popper-container-1024')
|
||||
expect(wrapper.vm.selector).toBe('#el-popper-container-1024')
|
||||
})
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { computed, inject, unref } from 'vue'
|
||||
import { computed, getCurrentInstance, inject, unref } from 'vue'
|
||||
import { isClient } from '@vueuse/core'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { useGlobalConfig } from '../use-global-config'
|
||||
@ -20,9 +20,14 @@ const defaultIdInjection = {
|
||||
export const ID_INJECTION_KEY: InjectionKey<ElIdInjectionContext> =
|
||||
Symbol('elIdInjection')
|
||||
|
||||
export const useId = (deterministicId?: MaybeRef<string>): Ref<string> => {
|
||||
const idInjection = inject(ID_INJECTION_KEY, defaultIdInjection)
|
||||
export const useIdInjection = (): ElIdInjectionContext => {
|
||||
return getCurrentInstance()
|
||||
? inject(ID_INJECTION_KEY, defaultIdInjection)
|
||||
: defaultIdInjection
|
||||
}
|
||||
|
||||
export const useId = (deterministicId?: MaybeRef<string>): Ref<string> => {
|
||||
const idInjection = useIdInjection()
|
||||
if (!isClient && idInjection === defaultIdInjection) {
|
||||
debugWarn(
|
||||
'IdInjection',
|
||||
|
@ -1,22 +1,29 @@
|
||||
import { onBeforeMount } from 'vue'
|
||||
import { computed, onBeforeMount } from 'vue'
|
||||
import { isClient } from '@vueuse/core'
|
||||
import { generateId } from '@element-plus/utils'
|
||||
import { useGlobalConfig } from '../use-global-config'
|
||||
import { defaultNamespace } from '../use-namespace'
|
||||
import { useIdInjection } from '../use-id'
|
||||
|
||||
let cachedContainer: HTMLElement
|
||||
|
||||
const namespace = useGlobalConfig('namespace', defaultNamespace)
|
||||
export const usePopperContainerId = () => {
|
||||
const namespace = useGlobalConfig('namespace', defaultNamespace)
|
||||
const idInjection = useIdInjection()
|
||||
|
||||
export const POPPER_CONTAINER_ID = `${
|
||||
namespace.value
|
||||
}-popper-container-${generateId()}`
|
||||
const id = computed(() => {
|
||||
return `${namespace.value}-popper-container-${idInjection.prefix}`
|
||||
})
|
||||
const selector = computed(() => `#${id.value}`)
|
||||
|
||||
export const POPPER_CONTAINER_SELECTOR = `#${POPPER_CONTAINER_ID}`
|
||||
return {
|
||||
id,
|
||||
selector,
|
||||
}
|
||||
}
|
||||
|
||||
const createContainer = () => {
|
||||
const createContainer = (id: string) => {
|
||||
const container = document.createElement('div')
|
||||
container.id = POPPER_CONTAINER_ID
|
||||
container.id = id
|
||||
document.body.appendChild(container)
|
||||
return container
|
||||
}
|
||||
@ -25,15 +32,15 @@ export const usePopperContainer = () => {
|
||||
onBeforeMount(() => {
|
||||
if (!isClient) return
|
||||
|
||||
const { id, selector } = usePopperContainerId()
|
||||
// This is for bypassing the error that when under testing env, we often encounter
|
||||
// document.body.innerHTML = '' situation
|
||||
// for this we need to disable the caching since it's not really needed
|
||||
if (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
!cachedContainer ||
|
||||
!document.body.querySelector(POPPER_CONTAINER_SELECTOR)
|
||||
(!cachedContainer && !document.body.querySelector(selector.value))
|
||||
) {
|
||||
cachedContainer = createContainer()
|
||||
cachedContainer = createContainer(id.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user