fix(components): [global-config] (#11847)

* fix(components): [loading]

* Remove inappropriate way of using injection in directives.

* chore: rewrite implementation

* fix(components): [global-config]

* Fix global config injection in global components.

* chore: fix format

* chore: remove .only modifier

* chore: fix failing tests
This commit is contained in:
Jeremy 2023-03-06 23:20:21 +08:00 committed by GitHub
parent df600f2bed
commit c2710d97d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 281 additions and 133 deletions

View File

@ -1,4 +1,4 @@
import { nextTick, reactive } from 'vue'
import { defineComponent, nextTick, reactive } from 'vue'
import { mount } from '@vue/test-utils'
import { NOOP } from '@vue/shared'
import { beforeEach, describe, expect, it, test, vi } from 'vitest'
@ -14,61 +14,73 @@ const _mount = (
payload = {},
type: 'fn-cb' | 'fn-promise' | 'fn-arr' | 'fn-async' | 'arr' = 'fn-cb'
) =>
mount({
setup() {
const state = reactive({
value: '',
list: [
{ value: 'Java', tag: 'java' },
{ value: 'Go', tag: 'go' },
{ value: 'JavaScript', tag: 'javascript' },
{ value: 'Python', tag: 'python' },
],
payload,
})
mount(
defineComponent({
setup(_, { expose }) {
const state = reactive({
value: '',
list: [
{ value: 'Java', tag: 'java' },
{ value: 'Go', tag: 'go' },
{ value: 'JavaScript', tag: 'javascript' },
{ value: 'Python', tag: 'python' },
],
payload,
})
function filterList(queryString: string) {
return queryString
? state.list.filter(
(i) => i.value.indexOf(queryString.toLowerCase()) === 0
)
: state.list
}
const querySearch = (() => {
switch (type) {
case 'fn-cb':
return (
queryString: string,
cb: (arg: typeof state.list) => void
) => {
cb(filterList(queryString))
}
case 'fn-promise':
return (queryString: string) =>
Promise.resolve(filterList(queryString))
case 'fn-async':
return async (queryString: string) => {
await Promise.resolve()
return filterList(queryString)
}
case 'fn-arr':
return (queryString: string) => filterList(queryString)
case 'arr':
return state.list
function filterList(queryString: string) {
return queryString
? state.list.filter(
(i) => i.value.indexOf(queryString.toLowerCase()) === 0
)
: state.list
}
})()
return () => (
<Autocomplete
ref="autocomplete"
v-model={state.value}
fetch-suggestions={querySearch}
{...state.payload}
/>
)
},
})
const querySearch = (() => {
switch (type) {
case 'fn-cb':
return (
queryString: string,
cb: (arg: typeof state.list) => void
) => {
cb(filterList(queryString))
}
case 'fn-promise':
return (queryString: string) =>
Promise.resolve(filterList(queryString))
case 'fn-async':
return async (queryString: string) => {
await Promise.resolve()
return filterList(queryString)
}
case 'fn-arr':
return (queryString: string) => filterList(queryString)
case 'arr':
return state.list
}
})()
const containerExposes = usePopperContainerId()
expose(containerExposes)
return () => (
<Autocomplete
ref="autocomplete"
v-model={state.value}
fetch-suggestions={querySearch}
{...state.payload}
/>
)
},
}),
{
global: {
provide: {
namespace: 'el',
},
},
}
)
describe('Autocomplete.vue', () => {
beforeEach(() => {
@ -318,24 +330,22 @@ describe('Autocomplete.vue', () => {
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount()
const { vm } = _mount()
await nextTick()
const { selector } = usePopperContainerId()
expect(document.body.querySelector(selector.value)?.innerHTML).not.toBe(
''
)
const { selector } = vm
expect(document.body.querySelector(selector)?.innerHTML).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount({
const { vm } = _mount({
teleported: false,
})
await nextTick()
const { selector } = usePopperContainerId()
expect(document.body.querySelector(selector.value)?.innerHTML).toBe('')
const { selector } = vm
expect(document.body.querySelector(selector)?.innerHTML).toBe('')
})
})

View File

@ -6,7 +6,10 @@ import Chinese from '@element-plus/locale/lang/zh-cn'
import English from '@element-plus/locale/lang/en'
import { ElButton, ElMessage } from '@element-plus/components'
import { rAF } from '@element-plus/test-utils/tick'
import { useGlobalConfig } from '../src/hooks/use-global-config'
import {
useGlobalComponentSettings,
useGlobalConfig,
} from '../src/hooks/use-global-config'
import ConfigProvider from '../src/config-provider'
import type { PropType } from 'vue'
@ -239,4 +242,30 @@ describe('config-provider', () => {
}
)
})
describe('global component configs', () => {
it('should use global configured settings', () => {
const namespace = 'test'
const locale = Chinese
const zIndex = 1000
const block = 'button'
const receiverRef = ref()
const ReceiverComponent = defineComponent({
setup() {
receiverRef.value = useGlobalComponentSettings(block)
},
template: '<div></div>',
})
mount(() => (
<ConfigProvider zIndex={zIndex} locale={locale} namespace={namespace}>
<ReceiverComponent />
</ConfigProvider>
))
const vm = receiverRef.value
expect(vm.ns.namespace).toBe(namespace)
expect(vm.locale.locale).toBe(locale)
expect(vm.zIndex.currentZIndex).toBeGreaterThanOrEqual(zIndex)
})
})
})

View File

@ -2,8 +2,13 @@ import { computed, getCurrentInstance, inject, provide, ref, unref } from 'vue'
import { debugWarn, keysOf } from '@element-plus/utils'
import {
SIZE_INJECTION_KEY,
defaultInitialZIndex,
defaultNamespace,
localeContextKey,
namespaceContextKey,
useLocale,
useNamespace,
useZIndex,
zIndexContextKey,
} from '@element-plus/hooks'
import { configProviderContextKey } from '../constants'
@ -39,6 +44,27 @@ export function useGlobalConfig(
}
}
// for components like `ElMessage` `ElNotification` `ElMessageBox`.
export function useGlobalComponentSettings(block: string) {
const config = useGlobalConfig()
const ns = useNamespace(
block,
computed(() => config.value?.namespace || defaultNamespace)
)
const locale = useLocale(computed(() => config.value?.locale))
const zIndex = useZIndex(
computed(() => config.value?.zIndex || defaultInitialZIndex)
)
return {
ns,
locale,
zIndex,
}
}
export const provideGlobalConfig = (
config: MaybeRef<ConfigProviderContext>,
app?: App,

View File

@ -2,6 +2,7 @@ import {
Transition,
createApp,
createVNode,
defineComponent,
h,
reactive,
ref,
@ -10,15 +11,16 @@ import {
withCtx,
withDirectives,
} from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { useNamespace, useZIndex } from '@element-plus/hooks'
import { removeClass } from '@element-plus/utils'
import type { UseNamespaceReturn } from '@element-plus/hooks'
import type { LoadingOptionsResolved } from './types'
export function createLoadingComponent(options: LoadingOptionsResolved) {
let afterLeaveTimer: number
const ns = useNamespace('loading')
// IMPORTANT NOTE: this is only a hacking way to expose the injections on an
// instance, DO NOT FOLLOW this pattern in your own code.
const afterLeaveFlag = ref(false)
const data = reactive({
...options,
@ -33,6 +35,7 @@ export function createLoadingComponent(options: LoadingOptionsResolved) {
function destroySelf() {
const target = data.parent
const ns = (vm as any).ns as UseNamespaceReturn
if (!target.vLoadingAddClassList) {
let loadingNumber: number | string | null =
target.getAttribute('loading-number')
@ -71,9 +74,17 @@ export function createLoadingComponent(options: LoadingOptionsResolved) {
destroySelf()
}
const elLoadingComponent = {
const elLoadingComponent = defineComponent({
name: 'ElLoading',
setup() {
setup(_, { expose }) {
const ns = useNamespace('loading')
const zIndex = useZIndex()
expose({
ns,
zIndex,
})
return () => {
const svg = data.spinner || data.svg
const spinner = h(
@ -136,7 +147,7 @@ export function createLoadingComponent(options: LoadingOptionsResolved) {
)
}
},
}
})
const loadingInstance = createApp(elLoadingComponent)
const vm = loadingInstance.mount(document.createElement('div'))

View File

@ -3,8 +3,9 @@ import { nextTick } from 'vue'
import { isString } from '@vue/shared'
import { isClient } from '@vueuse/core'
import { addClass, getStyle, removeClass } from '@element-plus/utils'
import { useNamespace, useZIndex } from '@element-plus/hooks'
import { createLoadingComponent } from './loading'
import type { UseNamespaceReturn, UseZIndexReturn } from '@element-plus/hooks'
import type { LoadingInstance } from './loading'
import type { LoadingOptionsResolved } from '..'
import type { LoadingOptions } from './types'
@ -93,7 +94,7 @@ const addStyle = async (
parent: HTMLElement,
instance: LoadingInstance
) => {
const { nextZIndex } = useZIndex()
const { nextZIndex } = (instance.vm as any).zIndex as UseZIndexReturn
const maskStyle: CSSProperties = {}
if (options.fullscreen) {
@ -135,7 +136,7 @@ const addClassList = (
parent: HTMLElement,
instance: LoadingInstance
) => {
const ns = useNamespace('loading')
const ns = (instance.vm as any).ns as UseNamespaceReturn
if (
!['absolute', 'fixed', 'sticky'].includes(instance.originalPosition.value)

View File

@ -164,12 +164,9 @@ import { TrapFocus } from '@element-plus/directives'
import {
useDraggable,
useId,
useLocale,
useLockscreen,
useNamespace,
useRestoreActive,
useSameTarget,
useZIndex,
} from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import { useFormSize } from '@element-plus/components/form'
@ -181,8 +178,9 @@ import {
} from '@element-plus/utils'
import { ElIcon } from '@element-plus/components/icon'
import ElFocusTrap from '@element-plus/components/focus-trap'
import { useGlobalComponentSettings } from '@element-plus/components/config-provider'
import type { ComponentPublicInstance, PropType } from 'vue'
import type { ComponentPublicInstance, DefineComponent, PropType } from 'vue'
import type { ComponentSize } from '@element-plus/constants'
import type {
Action,
@ -251,10 +249,12 @@ export default defineComponent({
emits: ['vanish', 'action'],
setup(props, { emit }) {
// const popup = usePopup(props, doClose)
const { t } = useLocale()
const ns = useNamespace('message-box')
const { locale, zIndex, ns } = useGlobalComponentSettings('message-box')
const { t } = locale
const { nextZIndex } = zIndex
const visible = ref(false)
const { nextZIndex } = useZIndex()
// s represents state
const state = reactive<MessageBoxState>({
// autofocus element when open message-box
@ -501,5 +501,5 @@ export default defineComponent({
t,
}
},
})
}) as DefineComponent
</script>

View File

@ -49,8 +49,8 @@ import { useEventListener, useResizeObserver, useTimeoutFn } from '@vueuse/core'
import { TypeComponents, TypeComponentsMap } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import ElBadge from '@element-plus/components/badge'
import { useGlobalComponentSettings } from '@element-plus/components/config-provider'
import { ElIcon } from '@element-plus/components/icon'
import { useNamespace, useZIndex } from '@element-plus/hooks'
import { messageEmits, messageProps } from './message'
import { getLastOffset, getOffsetOrSpace } from './instance'
import type { BadgeProps } from '@element-plus/components/badge'
@ -65,8 +65,8 @@ defineOptions({
const props = defineProps(messageProps)
defineEmits(messageEmits)
const ns = useNamespace('message')
const { currentZIndex, nextZIndex } = useZIndex()
const { ns, zIndex } = useGlobalComponentSettings('message')
const { currentZIndex, nextZIndex } = zIndex
const messageRef = ref<HTMLDivElement>()
const visible = ref(false)

View File

@ -3,7 +3,6 @@ import { mount } from '@vue/test-utils'
import { describe, expect, test, vi } from 'vitest'
import { TypeComponentsMap } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { useZIndex } from '@element-plus/hooks'
import { notificationTypes } from '../src/notification'
import Notification from '../src/notification.vue'
@ -40,10 +39,11 @@ describe('Notification.vue', () => {
expect(wrapper.vm.visible).toBe(true)
expect(wrapper.vm.iconComponent).toBeUndefined()
expect(wrapper.vm.horizontalClass).toBe('right')
expect(wrapper.vm.positionStyle).toEqual({
top: '0px',
zIndex: 0,
})
expect(wrapper.vm.positionStyle).toEqual(
expect.objectContaining({
top: '0px',
})
)
})
test('should be able to render VNode', () => {
@ -80,19 +80,15 @@ describe('Notification.vue', () => {
expect(HTMLWrapper.find(`.${tagClass}`).exists()).toBe(false)
})
test('should be able to render z-index style with zIndex flag', () => {
const { nextZIndex } = useZIndex()
const zIndex = nextZIndex()
const wrapper = _mount({
props: {
zIndex,
},
})
test('should be able to render z-index style with zIndex flag', async () => {
const wrapper = _mount({})
await nextTick()
expect(wrapper.vm.positionStyle).toEqual({
top: '0px',
zIndex,
})
expect(wrapper.vm.positionStyle).toEqual(
expect.objectContaining({
top: '0px',
})
)
})
})

View File

@ -43,7 +43,7 @@ import { useEventListener, useTimeoutFn } from '@vueuse/core'
import { CloseComponents, TypeComponentsMap } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { ElIcon } from '@element-plus/components/icon'
import { useNamespace } from '@element-plus/hooks'
import { useGlobalComponentSettings } from '@element-plus/components/config-provider'
import { notificationEmits, notificationProps } from './notification'
import type { CSSProperties } from 'vue'
@ -55,7 +55,9 @@ defineOptions({
const props = defineProps(notificationProps)
defineEmits(notificationEmits)
const ns = useNamespace('notification')
const { ns, zIndex } = useGlobalComponentSettings('notification')
const { nextZIndex, currentZIndex } = zIndex
const { Close } = CloseComponents
const visible = ref(false)
@ -82,7 +84,7 @@ const verticalProperty = computed(() =>
const positionStyle = computed<CSSProperties>(() => {
return {
[verticalProperty.value]: `${props.offset}px`,
zIndex: props.zIndex,
zIndex: currentZIndex.value,
}
})
@ -118,6 +120,7 @@ function onKeydown({ code }: KeyboardEvent) {
// lifecycle
onMounted(() => {
startTimer()
nextZIndex()
visible.value = true
})

View File

@ -1,6 +1,5 @@
import { createVNode, render } from 'vue'
import { isClient } from '@vueuse/core'
import { useZIndex } from '@element-plus/hooks'
import { debugWarn, isElement, isString, isVNode } from '@element-plus/utils'
import NotificationConstructor from './notification.vue'
import { notificationTypes } from './notification'
@ -45,12 +44,9 @@ const notify: NotifyFn & Partial<Notify> & { _context: AppContext | null } =
})
verticalOffset += GAP_SIZE
const { nextZIndex } = useZIndex()
const id = `notification_${seed++}`
const userOnClose = options.onClose
const props: Partial<NotificationProps> = {
zIndex: nextZIndex(),
...options,
offset: verticalOffset,
id,

View File

@ -50,10 +50,18 @@ const _mount = (template: string, data: any = () => ({}), otherObj?) =>
},
template,
data,
setup() {
return usePopperContainerId()
},
...otherObj,
},
{
attachTo: 'body',
global: {
provide: {
namespace: 'el',
},
},
}
)
@ -1896,8 +1904,8 @@ describe('Select', () => {
)
await nextTick()
const { selector } = usePopperContainerId()
expect(document.body.querySelector(selector.value).innerHTML).not.toBe('')
const { selector } = wrapper.vm
expect(document.body.querySelector(selector).innerHTML).not.toBe('')
})
it('should not mount on the popper container', async () => {
@ -1925,8 +1933,8 @@ describe('Select', () => {
)
await nextTick()
const { selector } = usePopperContainerId()
expect(document.body.querySelector(selector.value).innerHTML).toBe('')
const { selector } = wrapper.vm
expect(document.body.querySelector(selector).innerHTML).toBe('')
})
})

View File

@ -13,6 +13,7 @@ describe('no injection value', () => {
const idInjection = useIdInjection()
return idInjection
},
template: '<div></div>',
})
expect(wrapper.vm.prefix).toMatch(/^\d{0,4}$/)

View File

@ -67,4 +67,31 @@ describe('use-locale', () => {
const t = buildTranslator(English)
expect(t('el.popconfirm.someThing')).toBe('el.popconfirm.someThing')
})
describe('overrides', () => {
it('should be override correctly', () => {
const override = computed(() => English)
const wrapper = mount(
defineComponent({
setup(_, { expose }) {
const { locale } = useLocale(override)
expose({
locale,
})
},
template: '<div></div>',
}),
{
global: {
provide: {
locale: Chinese,
},
},
}
)
expect(wrapper.vm.locale).toBe(override.value)
})
})
})

View File

@ -1,4 +1,4 @@
import { defineComponent, nextTick } from 'vue'
import { computed, defineComponent, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { provideGlobalConfig } from '@element-plus/components/config-provider'
@ -81,4 +81,31 @@ describe('use-locale', () => {
expect(style).toMatch('--ep-table-text-color: #409eff;')
expect(style).not.toMatch('--ep-table-active-color:')
})
it('overrides namespace', () => {
const overrides = 'override'
const { vm } = mount(
defineComponent({
setup(_, { expose }) {
const { namespace } = useNamespace(
'ns',
computed(() => overrides)
)
expose({
namespace,
})
},
template: '<div></div>',
}),
{
global: {
provide: {
namespace: 'el',
},
},
}
)
expect(vm.namespace).toBe(overrides)
})
})

View File

@ -1,4 +1,4 @@
import { nextTick } from 'vue'
import { defineComponent, nextTick } from 'vue'
import { config, mount, shallowMount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import * as vueuse from '@vueuse/core'
@ -17,12 +17,15 @@ vi.mock('@vueuse/core', () => {
})
const mountComponent = () =>
shallowMount({
setup() {
usePopperContainer()
return () => <div>{AXIOM}</div>
},
})
shallowMount(
defineComponent({
setup(_, { expose }) {
const exposes = usePopperContainer()
expose(exposes)
return () => <div>{AXIOM}</div>
},
})
)
describe('usePopperContainer', () => {
afterEach(() => {
@ -30,17 +33,17 @@ describe('usePopperContainer', () => {
})
it('should append container to the DOM root', async () => {
mountComponent()
const { vm } = mountComponent()
await nextTick()
const { selector } = usePopperContainerId()
const { selector } = vm
expect(document.body.querySelector(selector.value)).toBeDefined()
})
it('should not append container to the DOM root', async () => {
;(vueuse as any).isClient = false
mountComponent()
const { vm } = mountComponent()
await nextTick()
const { selector } = usePopperContainerId()
const { selector } = vm
expect(document.body.querySelector(selector.value)).toBeNull()
})
})

View File

@ -44,7 +44,7 @@ export const buildLocaleContext = (
export const localeContextKey: InjectionKey<Ref<Language | undefined>> =
Symbol('localeContextKey')
export const useLocale = () => {
const locale = inject(localeContextKey, ref())!
export const useLocale = (localeOverrides?: Ref<Language | undefined>) => {
const locale = localeOverrides || inject(localeContextKey, ref())!
return buildLocaleContext(computed(() => locale.value || English))
}

View File

@ -28,17 +28,20 @@ const _bem = (
export const namespaceContextKey: InjectionKey<Ref<string | undefined>> =
Symbol('localeContextKey')
export const useGetDerivedNamespace = () => {
const derivedNamespace = inject(namespaceContextKey, ref(defaultNamespace))
export const useGetDerivedNamespace = (namespaceOverrides?: Ref<string>) => {
const derivedNamespace =
namespaceOverrides || inject(namespaceContextKey, ref(defaultNamespace))
const namespace = computed(() => {
return unref(derivedNamespace) || defaultNamespace
})
return namespace
}
export const useNamespace = (block: string) => {
const namespace = useGetDerivedNamespace()
export const useNamespace = (
block: string,
namespaceOverrides?: Ref<string>
) => {
const namespace = useGetDerivedNamespace(namespaceOverrides)
const b = (blockSuffix = '') =>
_bem(namespace.value, block, blockSuffix, '', '')
const e = (element?: string) =>

View File

@ -28,10 +28,10 @@ const createContainer = (id: string) => {
}
export const usePopperContainer = () => {
const { id, selector } = usePopperContainerId()
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
@ -42,4 +42,9 @@ export const usePopperContainer = () => {
cachedContainer = createContainer(id.value)
}
})
return {
id,
selector,
}
}

View File

@ -4,13 +4,13 @@ import { isNumber } from '@element-plus/utils'
import type { InjectionKey, Ref } from 'vue'
const zIndex = ref(0)
const defaultInitialZIndex = 2000
export const defaultInitialZIndex = 2000
export const zIndexContextKey: InjectionKey<Ref<number | undefined>> =
Symbol('zIndexContextKey')
export const useZIndex = () => {
const zIndexInjection = inject(zIndexContextKey, undefined)
export const useZIndex = (zIndexOverrides?: Ref<number>) => {
const zIndexInjection = zIndexOverrides || inject(zIndexContextKey, undefined)
const initialZIndex = computed(() => {
const zIndexFromInjection = unref(zIndexInjection)
return isNumber(zIndexFromInjection)
@ -30,3 +30,5 @@ export const useZIndex = () => {
nextZIndex,
}
}
export type UseZIndexReturn = ReturnType<typeof useZIndex>