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({ ) : (
- {resolveSlot($slots.empty, () => [ - - ])} + {resolveSlot($slots.empty, () => { + return [ + this.mergedRenderEmpty?.('Select') || ( + + ) + ] + })}
)} {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({ ) : (
- {resolveSlot(this.$slots.empty, () => [ - - ])} + {resolveSlot(this.$slots.empty, () => { + return [ + this.mergedRenderEmpty?.('Cascader') || ( + + ) + ] + })}
)} {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({
- {resolveSlot($slots.empty, () => [ - - ])} + {resolveSlot($slots.empty, () => { + return [ + this.mergedRenderEmpty?.( + 'TreeSelect' + ) || ( + + ) + ] + })}
)} 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)} )