diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 5a32ea9d2f7..a8b73c93806 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -1,14 +1,24 @@
# CHANGELOG
+## NEXT_VERSION
+
+`NEXT_VERSION`
+
+### Features
+
+- `n-config-provider` adds `render-empty` prop to globally customize the rendering of empty state
+
## 2.43.2
+`2025-11-16`
+
### Fixes
- Fix seemly dependency version range allows incompatible versions.
- Fix `n-progress` style is incorrect after using the dashboard mode exceeding 100%, closes [#6627](https://github.com/tusen-ai/naive-ui/issues/6627)
- Fix `n-modal`'s outside content can't be interacted with `show-mask` is set to `false`.
-### Feats
+### Features
- `n-date-picker` prop `defaultTime` can also accept a function that will return a formatted string
- `n-steps` adds `content-placement` prop, closes [#7044](https://github.com/tusen-ai/naive-ui/issues/7044).
diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md
index 6f4702bb4d1..7a37c8122dd 100644
--- a/CHANGELOG.zh-CN.md
+++ b/CHANGELOG.zh-CN.md
@@ -1,14 +1,24 @@
# CHANGELOG
+## NEXT_VERSION
+
+`NEXT_VERSION`
+
+### Features
+
+- `n-config-provider` 新增 `render-empty` 属性,用于全局自定义空状态的渲染
+
## 2.43.2
+`2025-11-16`
+
### Fixes
- 修复 seemly 依赖的版本未更新到最新
- 修复 `n-progress` 使用仪表盘模式超过 100% 之后样式不正确,关闭 [#6627](https://github.com/tusen-ai/naive-ui/issues/6627)
- 修复 `n-modal` 在 `show-mask` 为 `false` 的情况下,外部内容不能被操作
-### Feats
+### Features
- `n-date-picker` 的 `defaultTime` 属性可以接受返回格式化字符串的函数
- `n-steps` 增加 `content-placement` 属性,关闭 [#7044](https://github.com/tusen-ai/naive-ui/issues/7044)
diff --git a/src/_internal/select-menu/src/SelectMenu.tsx b/src/_internal/select-menu/src/SelectMenu.tsx
index 76681a4d4a6..bb596bce6f7 100644
--- a/src/_internal/select-menu/src/SelectMenu.tsx
+++ b/src/_internal/select-menu/src/SelectMenu.tsx
@@ -24,6 +24,7 @@ import {
computed,
defineComponent,
h,
+ inject,
nextTick,
onBeforeUnmount,
onMounted,
@@ -36,6 +37,7 @@ import { VirtualList } from 'vueuc'
import { useConfig, useRtl, useTheme, useThemeClass } from '../../../_mixins'
import { resolveSlot, resolveWrappedSlot, useOnResize } from '../../../_utils'
import { createKey } from '../../../_utils/cssr'
+import { configProviderInjectionKey } from '../../../config-provider/src/context'
import { NEmpty } from '../../../empty'
import NFocusDetector from '../../focus-detector'
import NInternalLoading from '../../loading'
@@ -118,6 +120,7 @@ export default defineComponent({
},
setup(props) {
const { mergedClsPrefixRef, mergedRtlRef } = useConfig(props)
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const rtlEnabledRef = useRtl(
'InternalSelectMenu',
mergedRtlRef,
@@ -425,6 +428,7 @@ export default defineComponent({
return {
mergedTheme: themeRef,
mergedClsPrefix: mergedClsPrefixRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
rtlEnabled: rtlEnabledRef,
virtualListRef,
scrollbarRef,
@@ -583,13 +587,17 @@ export default defineComponent({
) : (
)}
{resolveWrappedSlot(
diff --git a/src/cascader/src/CascaderMenu.tsx b/src/cascader/src/CascaderMenu.tsx
index 1a1b8854eac..63d42cf47b5 100644
--- a/src/cascader/src/CascaderMenu.tsx
+++ b/src/cascader/src/CascaderMenu.tsx
@@ -19,6 +19,7 @@ import {
import { NBaseMenuMask } from '../../_internal'
import FocusDetector from '../../_internal/focus-detector'
import { resolveSlot, resolveWrappedSlot, useOnResize } from '../../_utils'
+import { configProviderInjectionKey } from '../../config-provider/src/context'
import { NEmpty } from '../../empty'
import NCascaderSubmenu from './CascaderSubmenu'
import { cascaderInjectionKey } from './interface'
@@ -68,6 +69,7 @@ export default defineComponent({
mergedThemeRef,
getColumnStyleRef
} = inject(cascaderInjectionKey)!
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const submenuInstRefs: CascaderSubmenuInstance[] = []
const maskInstRef = ref(null)
const selfElRef = ref(null)
@@ -112,6 +114,7 @@ export default defineComponent({
return {
isMounted: isMountedRef,
mergedClsPrefix: mergedClsPrefixRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
selfElRef,
submenuInstRefs,
maskInstRef,
@@ -165,12 +168,16 @@ export default defineComponent({
) : (
)}
{resolveWrappedSlot(
diff --git a/src/config-provider/demos/enUS/index.demo-entry.md b/src/config-provider/demos/enUS/index.demo-entry.md
index a18903645d0..6e2198e2dec 100644
--- a/src/config-provider/demos/enUS/index.demo-entry.md
+++ b/src/config-provider/demos/enUS/index.demo-entry.md
@@ -31,6 +31,7 @@ inline-theme-disabled.vue
| locale | `Locale \| null` | `undefined` | The locale object to be consumed by its child. If set to `null` it will use the default `enUS` locale. If set to `undefined` it will inherit its parent `n-config-provider`. | |
| namespace | `string` | `undefined` | Class name of detached parts of components inside `n-config-provider` | |
| preflight-style-disabled | `boolean` | `false` | Whether to disabled preflight style of naive-ui. If you disable it, you can take control of all global css. Also you can use `n-global-style` to apply global style (which is recommend since global style will be reactive). | 2.29.0 |
+| render-empty | `(componentName: 'Cascader' \| 'DataTable' \| 'Select' \| 'Transfer' \| 'Tree' \| 'TreeSelect') => VNodeChild` | `undefined` | The render function to be consumed by its child to render empty state. If set to `undefined` it will inherit its parent `n-config-provider`. | |
| style-mount-target | `ParentNode` | `undefined` | Mounting target of style elements of components. Note that this prop is not reactive. | 2.40.0 |
| tag | `string` | `'div'` | What tag `n-config-provider` will be rendered as | |
| theme | `Theme \| null` | `undefined` | The theme object to be consumed by its child. If set to `null` it will use the default light theme. If set to `undefined` it will inherit its parent `n-config-provider`. For more details please see [Customizing Theme](../docs/customize-theme). | |
diff --git a/src/config-provider/demos/zhCN/index.demo-entry.md b/src/config-provider/demos/zhCN/index.demo-entry.md
index b7d894dc12a..3839af10587 100644
--- a/src/config-provider/demos/zhCN/index.demo-entry.md
+++ b/src/config-provider/demos/zhCN/index.demo-entry.md
@@ -31,6 +31,7 @@ inline-theme-disabled.vue
| locale | `Locale \| null` | `undefined` | 对后代组件生效的语言对象,为 `null` 时会使用默认 `enUS`,为 `undefined` 时会继承上级 `n-config-provider` | |
| namespace | `string` | `undefined` | `n-config-provider` 内部组件被卸载于其他位置的 DOM 的类名 | |
| preflight-style-disabled | `boolean` | `false` | 是否禁用默认样式,如果你禁用了它,便可以完全控制全局样式。你也可以使用 `n-global-style` 去挂载全局样式(推荐,样式是响应式的) | 2.29.0 |
+| render-empty | `(componentName: 'Cascader' \| 'DataTable' \| 'Select' \| 'Transfer' \| 'Tree' \| 'TreeSelect') => VNodeChild` | `undefined` | 对后代组件生效的空状态渲染函数,用于自定义空状态的显示内容。为 `undefined` 时会继承上级 `n-config-provider` | |
| style-mount-target | `ParentNode` | `undefined` | 组件样式的挂载位置。注意,该属性不是响应式的。 | 2.40.0 |
| tag | `string` | `'div'` | `n-config-provider` 被渲染成的元素 | |
| theme | `Theme \| null` | `undefined` | 对后代组件生效的主题对象,为 `null` 时会使用默认亮色,为 `undefined` 时会继承上级 `n-config-provider`。更多信息参见[调整主题](../docs/customize-theme) | |
diff --git a/src/config-provider/src/ConfigProvider.ts b/src/config-provider/src/ConfigProvider.ts
index d1897b1a983..70d6c3d9c7e 100644
--- a/src/config-provider/src/ConfigProvider.ts
+++ b/src/config-provider/src/ConfigProvider.ts
@@ -1,4 +1,4 @@
-import type { ComputedRef, ExtractPropTypes, PropType } from 'vue'
+import type { ComputedRef, ExtractPropTypes, PropType, VNodeChild } from 'vue'
import type { Hljs } from '../../_mixins'
import type { NDateLocale, NLocale } from '../../locales'
import type {
@@ -9,6 +9,7 @@ import type {
} from './interface'
import type {
Breakpoints,
+ RenderEmptyComponentName,
RtlEnabledState,
RtlProp
} from './internal-interface'
@@ -49,6 +50,9 @@ export const configProviderProps = {
type: Boolean,
default: undefined
},
+ renderEmpty: Function as PropType<
+ (componentName: RenderEmptyComponentName) => VNodeChild
+ >,
// deprecated
as: {
type: String as PropType,
@@ -125,6 +129,12 @@ export default defineComponent({
return componentOptions
return NConfigProvider?.mergedComponentPropsRef.value
})
+ const mergedRenderEmptyRef = computed(() => {
+ const { renderEmpty } = props
+ return renderEmpty === undefined
+ ? NConfigProvider?.mergedRenderEmptyRef.value
+ : renderEmpty
+ })
const mergedClsPrefixRef = computed(() => {
const { clsPrefix } = props
if (clsPrefix !== undefined)
@@ -187,6 +197,7 @@ export default defineComponent({
mergedRtlRef,
mergedIconsRef,
mergedComponentPropsRef,
+ mergedRenderEmptyRef,
mergedBorderedRef,
mergedNamespaceRef,
mergedClsPrefixRef,
diff --git a/src/config-provider/src/internal-interface.ts b/src/config-provider/src/internal-interface.ts
index 9bb9816f41c..87b2597d681 100644
--- a/src/config-provider/src/internal-interface.ts
+++ b/src/config-provider/src/internal-interface.ts
@@ -201,6 +201,14 @@ export interface GlobalThemeWithoutCommon {
InputOtp?: InputOtpTheme
}
+export type RenderEmptyComponentName
+ = | 'Cascader'
+ | 'DataTable'
+ | 'Select'
+ | 'Transfer'
+ | 'Tree'
+ | 'TreeSelect'
+
export interface GlobalComponentConfig {
Pagination?: {
inputSize?: InputSize
@@ -220,6 +228,7 @@ export interface GlobalComponentConfig {
buttonSize?: ButtonSize
}
Empty?: Pick
+ renderEmpty?: (componentName: RenderEmptyComponentName) => VNodeChild
}
export interface GlobalIconConfig {
@@ -267,6 +276,9 @@ export interface ConfigProviderInjection {
mergedKatexRef: Ref
mergedComponentPropsRef: Ref
mergedIconsRef: Ref
+ mergedRenderEmptyRef: Ref<
+ ((componentName: RenderEmptyComponentName) => VNodeChild) | undefined
+ >
mergedThemeRef: Ref
mergedThemeOverridesRef: Ref
mergedRtlRef: Ref
diff --git a/src/data-table/src/TableParts/Body.tsx b/src/data-table/src/TableParts/Body.tsx
index f0e33046d43..e76b55c83e6 100644
--- a/src/data-table/src/TableParts/Body.tsx
+++ b/src/data-table/src/TableParts/Body.tsx
@@ -455,6 +455,7 @@ export default defineComponent({
summary: summaryRef,
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: mergedThemeRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
scrollX: scrollXRef,
cols: colsRef,
loading: loadingRef,
@@ -1156,12 +1157,16 @@ export default defineComponent({
style={this.bodyStyle}
ref="emptyElRef"
>
- {resolveSlot(this.dataTableSlots.empty, () => [
-
- ])}
+ {resolveSlot(this.dataTableSlots.empty, () => {
+ return [
+ this.mergedRenderEmpty?.('DataTable') || (
+
+ )
+ ]
+ })}
)
if (this.shouldDisplaySomeTablePart) {
diff --git a/src/legacy-transfer/src/TransferList.tsx b/src/legacy-transfer/src/TransferList.tsx
index 02a71f5b674..2ee08a1981c 100644
--- a/src/legacy-transfer/src/TransferList.tsx
+++ b/src/legacy-transfer/src/TransferList.tsx
@@ -13,6 +13,7 @@ import {
} from 'vue'
import { VirtualList } from 'vueuc'
import { NScrollbar } from '../../_internal'
+import { configProviderInjectionKey } from '../../config-provider/src/context'
import { NEmpty } from '../../empty'
import { transferInjectionKey } from './interface'
import NTransferListItem from './TransferListItem'
@@ -51,6 +52,7 @@ export default defineComponent({
},
setup() {
const { mergedThemeRef, mergedClsPrefixRef } = inject(transferInjectionKey)!
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const scrollerInstRef = ref(null)
const vlInstRef = ref(null)
function syncVLScroller(): void {
@@ -73,6 +75,7 @@ export default defineComponent({
return {
mergedTheme: mergedThemeRef,
mergedClsPrefix: mergedClsPrefixRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
scrollerInstRef,
vlInstRef,
syncVLScroller,
@@ -152,13 +155,18 @@ export default defineComponent({
css={!this.isInputing}
>
{{
- default: () =>
- this.options.length ? null : (
-
+ default: () => {
+ if (this.options.length)
+ return null
+ return (
+ this.mergedRenderEmpty?.('Transfer') || (
+
+ )
)
+ }
}}
>
diff --git a/src/transfer/src/TransferList.tsx b/src/transfer/src/TransferList.tsx
index 97bbf86dd2d..b5bd09ab2a0 100644
--- a/src/transfer/src/TransferList.tsx
+++ b/src/transfer/src/TransferList.tsx
@@ -5,6 +5,7 @@ import type { Option } from './interface'
import { defineComponent, h, inject, ref } from 'vue'
import { VirtualList } from 'vueuc'
import { NScrollbar } from '../../_internal'
+import { configProviderInjectionKey } from '../../config-provider/src/context'
import { NEmpty } from '../../empty'
import { transferInjectionKey } from './interface'
import NTransferListItem from './TransferListItem'
@@ -32,6 +33,7 @@ export default defineComponent({
},
setup() {
const { mergedThemeRef, mergedClsPrefixRef } = inject(transferInjectionKey)!
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const scrollerInstRef = ref(null)
const vlInstRef = ref(null)
function syncVLScroller(): void {
@@ -54,6 +56,7 @@ export default defineComponent({
return {
mergedTheme: mergedThemeRef,
mergedClsPrefix: mergedClsPrefixRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
scrollerInstRef,
vlInstRef,
syncVLScroller,
@@ -65,10 +68,12 @@ export default defineComponent({
const { mergedTheme, options } = this
if (options.length === 0) {
return (
-
+ this.mergedRenderEmpty?.('Transfer') || (
+
+ )
)
}
const { mergedClsPrefix, virtualScroll, source, disabled, syncVLScroller }
diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx
index b5edc532f6c..51ec204a96b 100644
--- a/src/tree-select/src/TreeSelect.tsx
+++ b/src/tree-select/src/TreeSelect.tsx
@@ -44,6 +44,7 @@ import {
computed,
defineComponent,
h,
+ inject,
provide,
ref,
toRef,
@@ -69,6 +70,7 @@ import {
useOnResize,
warnOnce
} from '../../_utils'
+import { configProviderInjectionKey } from '../../config-provider/src/context'
import { NEmpty } from '../../empty'
import { NTree } from '../../tree'
import { createTreeMateOptions, treeSharedProps } from '../../tree/src/Tree'
@@ -206,6 +208,7 @@ export default defineComponent({
const menuElRef = ref(null)
const { mergedClsPrefixRef, namespaceRef, inlineThemeDisabled }
= useConfig(props)
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const { localeRef } = useLocale('Select')
const {
mergedSizeRef,
@@ -840,6 +843,7 @@ export default defineComponent({
mergedClsPrefix: mergedClsPrefixRef,
mergedValue: mergedValueRef,
mergedShow: mergedShowRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
namespace: namespaceRef,
adjustedTo: useAdjustedTo(props),
isMounted: useIsMounted(),
@@ -1042,14 +1046,20 @@ export default defineComponent({
)}
onLoad={this.onLoad}
diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx
index b0aa5204173..84cb8e9c433 100644
--- a/src/tree/src/Tree.tsx
+++ b/src/tree/src/Tree.tsx
@@ -49,6 +49,7 @@ import { VVirtualList } from 'vueuc'
import { NxScrollbar } from '../../_internal'
import { useConfig, useRtl, useTheme, useThemeClass } from '../../_mixins'
import { call, createDataKey, resolveSlot, warn, warnOnce } from '../../_utils'
+import { configProviderInjectionKey } from '../../config-provider/src/context'
import { NEmpty } from '../../empty'
import { treeSelectInjectionKey } from '../../tree-select/src/interface'
import { treeLight } from '../styles'
@@ -360,6 +361,7 @@ export default defineComponent({
}
const { mergedClsPrefixRef, inlineThemeDisabled, mergedRtlRef }
= useConfig(props)
+ const NConfigProvider = inject(configProviderInjectionKey, null)
const rtlEnabledRef = useRtl('Tree', mergedRtlRef, mergedClsPrefixRef)
const themeRef = useTheme(
'Tree',
@@ -1708,6 +1710,7 @@ export default defineComponent({
...exposedMethods,
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: themeRef,
+ mergedRenderEmpty: NConfigProvider?.mergedRenderEmptyRef.value,
rtlEnabled: rtlEnabledRef,
fNodes: mergedFNodesRef,
aip: aipRef,
@@ -1793,13 +1796,17 @@ export default defineComponent({
default: () => {
this.onRender?.()
return !fNodes.length ? (
- resolveSlot(this.$slots.empty, () => [
-
- ])
+ resolveSlot(this.$slots.empty, () => {
+ return [
+ this.mergedRenderEmpty?.('Tree') || (
+
+ )
+ ]
+ })
) : (
{!fNodes.length
- ? resolveSlot(this.$slots.empty, () => [
-
- ])
+ ? resolveSlot(this.$slots.empty, () => {
+ return [
+ this.mergedRenderEmpty?.('Tree') || (
+
+ )
+ ]
+ })
: fNodes.map(createNode)}
)