diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 7c2fb163d35..db17ae8452e 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -1,5 +1,13 @@ # CHANGELOG +### Fixes + +### Feats + +- `n-switch` add `activeColor` & `inactiveColor` & `activeButtonColor` & `inactiveButtonColor` prop, closes [#1718](https://github.com/TuSimple/naive-ui/issues/1718) +- `n-image` add `can-preview` & `on-load` prop, closes [#1647](https://github.com/TuSimple/naive-ui/issues/1647). +- `n-image` add `loading` & `errorbox` slot. + ## 2.21.2 (2021-11-29) ### Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 908a4f3fdf3..dc1698d9b37 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -1,5 +1,13 @@ # CHANGELOG +### Fixes + +### Feats + +- `n-switch` 新增 `activeColor` & `inactiveColor` & `activeButtonColor` & `inactiveButtonColor` 属性,关闭 [#1718](https://github.com/TuSimple/naive-ui/issues/1718) +- `n-image` 新增 `can-preview` & `on-load` 属性,关闭 [#1647](https://github.com/TuSimple/naive-ui/issues/1647) +- `n-image` 新增 `loading` & `errorbox` slot + ## 2.21.2 (2021-11-29) ### Fixes diff --git a/demo/pages/docs/customize-theme/zhCN/index.md b/demo/pages/docs/customize-theme/zhCN/index.md index aff4898a11d..3f6eeb0f066 100644 --- a/demo/pages/docs/customize-theme/zhCN/index.md +++ b/demo/pages/docs/customize-theme/zhCN/index.md @@ -89,9 +89,8 @@ Naive UI 通过使用 `n-config-provider` 调整主题。 如果你正在使用 ts 写代码,这块比较适合你。 ```html - ``` @@ -204,9 +208,9 @@ peers 相关的主题变量还没有暴露,使用 `GlobalThemeOverrides` 可 textColor: '#FF0000' }, InternalSelectMenu: { - borderRadius: '6px', + borderRadius: '6px' } - }, + } }, DataTable: { paginationMargin: '40px 0 0 0', @@ -218,16 +222,14 @@ peers 相关的主题变量还没有暴露,使用 `GlobalThemeOverrides` 可 itemTextColor: '#ccc' } } - }, + } // ... } // ... diff --git a/src/_utils/naive/extract-public-props.ts b/src/_utils/naive/extract-public-props.ts index f3574e7e0b0..b47292bada0 100644 --- a/src/_utils/naive/extract-public-props.ts +++ b/src/_utils/naive/extract-public-props.ts @@ -5,7 +5,8 @@ type themePropKeys = keyof typeof useTheme.props export type ExtractPublicPropTypes = Omit< Partial>, -Exclude | Extract +| Exclude +| Extract > export type ExtractInternalPropTypes = Partial> diff --git a/src/image/demos/enUS/error.demo.md b/src/image/demos/enUS/error.demo.md new file mode 100644 index 00000000000..1c859602405 --- /dev/null +++ b/src/image/demos/enUS/error.demo.md @@ -0,0 +1,57 @@ +# Load failed + +{{showError ? "Don't worry, the darkness will pass." : "In life, failure is inevitable."}} + +```html +{{showError ? "To repair the image" : "Destroy the image"}} + +
+ Default style +
+ +
+
+
+ Error slot style +
+ + + +
+
+
+``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + const showError = ref(false) + return { + showError + } + } +}) +``` diff --git a/src/image/demos/enUS/index.demo-entry.md b/src/image/demos/enUS/index.demo-entry.md index 7304d7e3487..26a16f227cc 100644 --- a/src/image/demos/enUS/index.demo-entry.md +++ b/src/image/demos/enUS/index.demo-entry.md @@ -7,6 +7,8 @@ Preview it. ```demo basic group +loading +error ``` ## API @@ -20,10 +22,19 @@ group | img-props | `object` | `undefined` | The props of the img element inside the component. | | object-fit | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down'` | `fill` | Object-fit type of the image in the container. | | preview-src | `string` | `undefined` | Source of preview image. | +| can-preview | `boolean` | `true` | Whether clicking image preview is allowed. | | show-toolbar | `boolean` | `true` | Whether to show the bottom toolbar when the image enlarge. | | src | `string` | `undefined` | Image source. | | width | `string \| number` | `undefined` | Image width. | | on-error | `(e: Event) => void` | `undefined` | Callback executed when the image fails to load. | +| on-load | `(e: Event) => void` | `undefined` | Callback executed after the image is loaded. | + +### Image Slots + +| Name | Type | Description | +| -------- | ---- | ---------------------------------------------------- | +| loading | `()` | Excessive animation during image loading. | +| errorbox | `()` | A placeholder in the event of an image load failure. | ### ImageGroup Props diff --git a/src/image/demos/enUS/loading.demo.md b/src/image/demos/enUS/loading.demo.md new file mode 100644 index 00000000000..e6c76d717ba --- /dev/null +++ b/src/image/demos/enUS/loading.demo.md @@ -0,0 +1,71 @@ +# Loading Image + +This is just a demo of the loading effect. The component will automatically display the loading effect when the image is loaded. It is `not recommended` to change the showLoadIng property unless you know exactly what you are doing! + +```html +{{showLoadIng ? "Finished loading" : "Show loading"}} + +
+ Default style +
+ +
+
+
+ Loading slot style +
+ + + +
+
+
+``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + const nImageRef1 = ref(null) + const nImageRef2 = ref(null) + const percentage = ref(0) + const TimeFun = ref(null) + const showLoadIng = ref(false) + return { + loadImg () { + nImageRef1.value.showLoadIng = !showLoadIng.value + nImageRef2.value.showLoadIng = !showLoadIng.value + showLoadIng.value = !showLoadIng.value + if (showLoadIng.value) { + TimeFun.value = setInterval(() => { + percentage.value = + percentage.value < 100 ? percentage.value + 1 : 100 + }, 1000) + } else { + clearInterval(TimeFun.value) + percentage.value = 0 + } + }, + nImageRef1, + nImageRef2, + percentage, + showLoadIng + } + } +}) +``` diff --git a/src/image/demos/zhCN/error.demo.md b/src/image/demos/zhCN/error.demo.md new file mode 100644 index 00000000000..4d0a1532ac3 --- /dev/null +++ b/src/image/demos/zhCN/error.demo.md @@ -0,0 +1,53 @@ +# 加载失败 + +{{showError ? "别担心,柳暗花明又一村。" : "人生嘛,失败总是难免的。"}} + +```html +{{showError ? "修复一下" : "干掉图片"}} + +
+ 默认样式 +
+ +
+
+
+ error插槽样式 +
+ + + +
+
+
+``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + const showError = ref(false) + return { + showError + } + } +}) +``` diff --git a/src/image/demos/zhCN/index.demo-entry.md b/src/image/demos/zhCN/index.demo-entry.md index ac3b74dd293..b7242f6f433 100644 --- a/src/image/demos/zhCN/index.demo-entry.md +++ b/src/image/demos/zhCN/index.demo-entry.md @@ -7,6 +7,8 @@ ```demo basic group +loading +error ``` ## API @@ -20,10 +22,19 @@ group | img-props | `object` | `undefined` | 组件中 img 元素的属性 | | object-fit | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down'` | `fill` | 图片在容器内的的适应类型 | | preview-src | `string` | `undefined` | 预览图片的图片地址 | +| can-preview | `boolean` | `true` | 是否可以点击图片进行预览 | | show-toolbar | `boolean` | `true` | 图片放大后是否展示底部工具栏 | | src | `string` | `undefined` | 图片来源 | | width | `string \| number` | `undefined` | 图片宽度 | | on-error | `(e: Event) => void` | `undefined` | 图片加载失败执行的回调 | +| on-load | `(e: Event) => void` | `undefined` | 图片加载完成执行的回调 | + +### Image Slots + +| 名称 | 参数 | 说明 | +| -------- | ---- | -------------------- | +| loading | `()` | 图像加载中过度动画 | +| errorbox | `()` | 图像加载失败的占位图 | ### ImageGroup Props diff --git a/src/image/demos/zhCN/loading.demo.md b/src/image/demos/zhCN/loading.demo.md new file mode 100644 index 00000000000..2f0280f2f57 --- /dev/null +++ b/src/image/demos/zhCN/loading.demo.md @@ -0,0 +1,71 @@ +# 加载图片 + +仅仅只是演示加载效果,在图片加载的时候组件会自动显示加载的效果,`不推荐`直接修改 showLoadIng 属性,除非你很明确知道你在做什么! + +```html +{{showLoadIng ? "完成加载" : "加载图片"}} + +
+ 默认样式 +
+ +
+
+
+ loading插槽样式 +
+ + + +
+
+
+``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + const nImageRef1 = ref(null) + const nImageRef2 = ref(null) + const percentage = ref(0) + const TimeFun = ref(null) + const showLoadIng = ref(false) + return { + loadImg () { + nImageRef1.value.showLoadIng = !showLoadIng.value + nImageRef2.value.showLoadIng = !showLoadIng.value + showLoadIng.value = !showLoadIng.value + if (showLoadIng.value) { + TimeFun.value = setInterval(() => { + percentage.value = + percentage.value < 100 ? percentage.value + 1 : 100 + }, 1000) + } else { + clearInterval(TimeFun.value) + percentage.value = 0 + } + }, + nImageRef1, + nImageRef2, + percentage, + showLoadIng + } + } +}) +``` diff --git a/src/image/src/Image.tsx b/src/image/src/Image.tsx index d955f943e54..b8890a640bb 100644 --- a/src/image/src/Image.tsx +++ b/src/image/src/Image.tsx @@ -5,13 +5,21 @@ import { ref, PropType, toRef, - mergeProps + renderSlot, + mergeProps, + computed, + CSSProperties } from 'vue' import NImagePreview from './ImagePreview' import type { ImagePreviewInst } from './ImagePreview' import { imageGroupInjectionKey } from './ImageGroup' -import { ExtractPublicPropTypes } from '../../_utils' -import { useConfig } from '../../_mixins' +import { call, ExtractPublicPropTypes } from '../../_utils' +import { useConfig, useTheme } from '../../_mixins' +import { NSpin } from '../../spin' +import { errorIcon } from './icons' +import { NIcon } from '../../icon' +import { imageLight } from '../styles' +import style from './styles/index.cssr' interface imgProps { alt?: string @@ -42,7 +50,10 @@ const imageProps = { width: [String, Number] as PropType, src: String, showToolbar: { type: Boolean, default: true }, - onError: Function as PropType<(e: Event) => void> + canPreview: { type: Boolean, default: true }, + loadDescription: String, + onError: Function as PropType<(e: Event) => void>, + onLoad: Function as PropType<(e: Event) => void> } export type ImageProps = ExtractPublicPropTypes @@ -53,12 +64,16 @@ export default defineComponent({ inheritAttrs: false, setup (props) { const imageRef = ref(null) + const showLoadIng = ref(true) + const showErrorBox = ref(false) + const themeRef = useTheme('Image', 'Image', style, imageLight, {}) const imgPropsRef = toRef(props, 'imgProps') const previewInstRef = ref(null) const imageGroupHandle = inject(imageGroupInjectionKey, null) const { mergedClsPrefixRef } = imageGroupHandle || useConfig(props) const exposedMethods = { click: () => { + if (!props.canPreview) return const mergedPreviewSrc = props.previewSrc || props.src if (imageGroupHandle) { imageGroupHandle.setPreviewSrc(mergedPreviewSrc) @@ -71,6 +86,22 @@ export default defineComponent({ previewInst.setPreviewSrc(mergedPreviewSrc) previewInst.setThumbnailEl(imageRef.value) previewInst.toggleShow() + }, + error: (e: Event) => { + showLoadIng.value = false + showErrorBox.value = true + const { onError } = props + if (onError) { + call(onError, e) + } + }, + load: (e: Event) => { + showLoadIng.value = false + showErrorBox.value = false + const { onLoad } = props + if (onLoad) { + call(onLoad, e) + } } } return { @@ -79,32 +110,75 @@ export default defineComponent({ previewInstRef, imageRef, imgProps: imgPropsRef, - ...exposedMethods + showLoadIng, + showErrorBox, + ...exposedMethods, + cssVars: computed(() => { + const { + self: { + errorBackgroundColor, + loadBackgroundColor, + errorDefaultFilter + } + } = themeRef.value + return { + '--error-background-color': errorBackgroundColor, + '--load-background-color': loadBackgroundColor, + '--error-default-filter': errorDefaultFilter + } + }) } }, render () { - const { mergedClsPrefix, imgProps = {} } = this + const { $slots, mergedClsPrefix, imgProps = {} } = this const imgWrapperNode = h( 'div', mergeProps(this.$attrs, { role: 'none', - class: `${mergedClsPrefix}-image` + class: `${mergedClsPrefix}-image`, + style: this.cssVars as CSSProperties }), - {this.alt + [ + {this.alt, + this.showLoadIng && (this.src || imgProps.src) && ( +
+ {$slots.loading ? ( + renderSlot($slots, 'loading') + ) : ( + + )} +
+ ), + this.showErrorBox && ( +
+ {$slots.errorbox ? ( + renderSlot($slots, 'errorbox') + ) : ( + + {{ default: () => errorIcon }} + + )} +
+ ) + ] ) return this.groupId ? ( diff --git a/src/image/src/icons.tsx b/src/image/src/icons.tsx index cb697ca9814..59690cc995a 100644 --- a/src/image/src/icons.tsx +++ b/src/image/src/icons.tsx @@ -26,3 +26,59 @@ export const closeIcon = ( /> ) + +export const errorIcon = ( + + + + + + + + + + + + + +) diff --git a/src/image/src/styles/index.cssr.ts b/src/image/src/styles/index.cssr.ts index e00b215f892..edf6b1b1cda 100644 --- a/src/image/src/styles/index.cssr.ts +++ b/src/image/src/styles/index.cssr.ts @@ -5,6 +5,7 @@ import fadeInzoomInTransiton from '../../../_styles/transitions/fade-in-scale-up // vars: // --icon-color // --bezier +// --error-background-color export default c([ c('body >', [ cB('image-container', 'position: fixed;') @@ -74,9 +75,53 @@ export default c([ cursor: pointer; max-height: 100%; max-width: 100%; + min-width: 100px; + min-height: 100px; + position: relative; `, [ c('img', ` border-radius: inherit; + `), + c('image-error-box', ` + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + color: var(--icon-color); + background-color: var(--error-background-color); + z-index: 2; + `) + ]), + cB('image-load-box', ` + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--load-background-color); + z-index: 1; + `), + cB('image-error-box', ` + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--error-background-color); + z-index: 2; + `, [ + cB('image-error-default-icon', ` + filter: var(--error-default-filter) `) ]) ]) diff --git a/src/image/styles/dark.ts b/src/image/styles/dark.ts index 2df8b68687a..f41f946de95 100644 --- a/src/image/styles/dark.ts +++ b/src/image/styles/dark.ts @@ -7,7 +7,10 @@ export const imageDark: ImageTheme = { self: (vars) => { const { textColor2 } = vars return { - iconColor: textColor2 + iconColor: textColor2, + errorBackgroundColor: 'rgba(24, 24, 28, 1)', + loadBackgroundColor: 'rgba(24, 24, 28, .8)', + errorDefaultFilter: 'brightness(0.8)' } } } diff --git a/src/image/styles/light.ts b/src/image/styles/light.ts index da07edc003e..7c3530e72d1 100644 --- a/src/image/styles/light.ts +++ b/src/image/styles/light.ts @@ -3,7 +3,10 @@ import { commonLight } from '../../_styles/common' function self () { return { - iconColor: 'rgba(255, 255, 255, .9)' + iconColor: 'rgba(255, 255, 255, .9)', + errorBackgroundColor: 'rgba(255, 255, 255, 1)', + loadBackgroundColor: 'rgba(255, 255, 255, .8)', + errorDefaultFilter: 'brightness(1)' } } export const imageLight = createTheme({ diff --git a/src/image/tests/Image.spec.tsx b/src/image/tests/Image.spec.tsx index 01d7a4f39c3..090ef1983f1 100644 --- a/src/image/tests/Image.spec.tsx +++ b/src/image/tests/Image.spec.tsx @@ -144,8 +144,35 @@ describe('n-image', () => { expect(document.querySelector('.n-image-preview-toolbar')).not.toEqual(null) expect(wrapper.findComponent(NImagePreview).exists()).toBe(true) const toolbars = document.querySelector('.n-image-preview-toolbar') - toolbars?.children[toolbars?.children.length - 1].dispatchEvent(new MouseEvent('click')) + toolbars?.children[toolbars?.children.length - 1].dispatchEvent( + new MouseEvent('click') + ) await nextTick() expect(document.querySelector('.n-image-preview-toolbar')).toEqual(null) }) + + it('should work with `onLoad` prop', async () => { + const onLoad = jest.fn() + const wrapper = mount(NImage, { + props: { + src: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', + onLoad + } + }) + await wrapper.find('img').trigger('load') + expect(onLoad).toHaveBeenCalled() + wrapper.unmount() + }) + + it('should work with `canPreview` prop', async () => { + const wrapper = mount(NImage) + + await wrapper.setProps({ + canPreview: false + }) + + await wrapper.find('img').trigger('click') + expect(document.querySelector('.n-image-preview-overlay')).toEqual(null) + wrapper.unmount() + }) }) diff --git a/src/slider/demos/enUS/restrict-selectable-values.demo.md b/src/slider/demos/enUS/restrict-selectable-values.demo.md index e340a4f66e9..8546abc8466 100644 --- a/src/slider/demos/enUS/restrict-selectable-values.demo.md +++ b/src/slider/demos/enUS/restrict-selectable-values.demo.md @@ -24,4 +24,4 @@ export default defineComponent({ } } }) -``` \ No newline at end of file +``` diff --git a/src/slider/demos/zhCN/restrict-selectable-values.demo.md b/src/slider/demos/zhCN/restrict-selectable-values.demo.md index fb6fabffdaf..3b2da6ba0f4 100644 --- a/src/slider/demos/zhCN/restrict-selectable-values.demo.md +++ b/src/slider/demos/zhCN/restrict-selectable-values.demo.md @@ -24,4 +24,4 @@ export default defineComponent({ } } }) -``` \ No newline at end of file +``` diff --git a/src/switch/demos/enUS/color.demo.md b/src/switch/demos/enUS/color.demo.md new file mode 100644 index 00000000000..a42953bc0fb --- /dev/null +++ b/src/switch/demos/enUS/color.demo.md @@ -0,0 +1,26 @@ +# Custom colors + +The colours of the rainbow. + +```html + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + active: ref(true) + } + } +}) +``` diff --git a/src/switch/demos/enUS/index.demo-entry.md b/src/switch/demos/enUS/index.demo-entry.md index 46ede2d5de9..1c94b77128d 100644 --- a/src/switch/demos/enUS/index.demo-entry.md +++ b/src/switch/demos/enUS/index.demo-entry.md @@ -12,6 +12,7 @@ loading event customize-value shape +color ``` ## API @@ -29,6 +30,10 @@ shape | value | `boolean` | `undefined` | Value when being set manually. | | unchecked-value | `string \| boolean \| number` | `false` | Value of unchecked state. | | on-update:value | `(value: boolean) => void` | `undefined` | Callback when the component's value changes. | +| activeColor | `string` | `undefined` | Background of checked state. | +| inactiveColor | `string` | `undefined` | Background of unchecked state. | +| activeButtonColor | `string` | `undefined` | Button background of checked state. | +| inactiveButtonColor | `string` | `undefined` | Button background of unchecked state. | ### Switch Slots diff --git a/src/switch/demos/zhCN/color.demo.md b/src/switch/demos/zhCN/color.demo.md new file mode 100644 index 00000000000..e0ec8b2f891 --- /dev/null +++ b/src/switch/demos/zhCN/color.demo.md @@ -0,0 +1,26 @@ +# 自定义颜色 + +生活,就应该五彩斑斓 + +```html + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + active: ref(true) + } + } +}) +``` diff --git a/src/switch/demos/zhCN/index.demo-entry.md b/src/switch/demos/zhCN/index.demo-entry.md index c8dd0b18699..cc4e252ee58 100644 --- a/src/switch/demos/zhCN/index.demo-entry.md +++ b/src/switch/demos/zhCN/index.demo-entry.md @@ -12,6 +12,7 @@ loading event customize-value shape +color ``` ## API @@ -29,6 +30,10 @@ shape | unchecked-value | `string \| boolean \| number` | `false` | 未选中时对应的值 | | value | `boolean` | `undefined` | 受控模式下的值 | | on-update:value | `(value: boolean) => void` | `undefined` | 组件值发生变化的回调 | +| activeColor | `string` | `undefined` | 选中时对应的背景色 | +| inactiveColor | `string` | `undefined` | 未选中时对应的背景色 | +| activeButtonColor | `string` | `undefined` | 选中时对应的按钮颜色 | +| inactiveButtonColor | `string` | `undefined` | 未选中时对应的按钮颜色 | ### Switch Slots diff --git a/src/switch/src/Switch.tsx b/src/switch/src/Switch.tsx index 5d132d47ee8..2071a1fab12 100644 --- a/src/switch/src/Switch.tsx +++ b/src/switch/src/Switch.tsx @@ -57,7 +57,13 @@ const switchProps = { default: false }, /** @deprecated */ - onChange: [Function, Array] as PropType | undefined> + onChange: [Function, Array] as PropType< + MaybeArray | undefined + >, + activeColor: String, + inactiveColor: String, + activeButtonColor: String, + inactiveButtonColor: String } as const export type SwitchProps = ExtractPublicPropTypes @@ -235,7 +241,15 @@ export default defineComponent({ onKeyup={this.handleKeyup} onKeydown={this.handleKeydown} > -