Merge pull request #524 from TuSimple/main

sync main
This commit is contained in:
07akioni 2021-07-16 01:58:13 +08:00 committed by GitHub
commit 229a4e37e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1262 additions and 134 deletions

18
.github/workflows/issue-add-bug.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Add Bug Labels
on:
issues:
types: [opened]
jobs:
add-labels:
runs-on: ubuntu-latest
if: contains(github.event.issue.body, '__BUG__') == true
steps:
- name: Add labels
uses: actions-cool/issues-helper@v2.2.1
with:
actions: 'add-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'untriaged'

View File

@ -1,5 +1,33 @@
# CHANGELOG
## 2.15.5 (2021-07-16)
### Feats
- `n-tree` add `render-label`, `render-prefix` and `render-suffix` props.
- `n-rate` add `allow-half` prop.
- `n-carousel` add `show-arrow` prop.
- `n-slider` add `format-tooltip` prop.
- `n-upload` add `event` in `on-finish` callback params.
- `n-rate` add `readonly` prop.
- `n-time-picker` add `seconds`, `minutes`, `hours` props.
- `n-notification` export `NotificationApi`, `NotificationOptions` and `NotificationReactive` type.
- `n-avatar` add `on-error` prop, closes [#394](https://github.com/TuSimple/naive-ui/issues/394).
- `n-image` add `on-error` prop, closes [#394](https://github.com/TuSimple/naive-ui/issues/394).
- `n-image` add `object-fit` prop, closes [#394](https://github.com/TuSimple/naive-ui/issues/394).
- `n-avatar` add `object-fit` prop, closes [#394](https://github.com/TuSimple/naive-ui/issues/394).
- `n-menu` expands all the ascendant of selected item by default, closes [#481](https://github.com/TuSimple/naive-ui/issues/481).
### Fixes
- Fix `n-calendar`'s `default-value` prop cannot be used.
- Fix `n-pagination` page count is not correct when `item-count` is 0.
- Fix `n-scrollbar` `content-style` can not override the default width of style.
- Fix `n-select` placeholder transition.
- Fix `n-loading-bar` `useLoadingBar`'s return type can be undefined.
- Fix `n-tag`'s `type` prop add `primary` type.
- Fix `n-dynamic-tag`'s `type` prop add `primary` type.
## 2.15.4 (2021-07-09)
### Feats
@ -31,7 +59,7 @@
### Feats
- `n-loading-bar` export `LoadingBarApi` type.
- `n-image` add `imgProps` prop.
- `n-image` add `img-props` prop.
- Add native `title` attributes to some components to enhance the experience.
- `n-tree` add `prefix` and `suffix` in TreeOption.
- `n-carousel` add `dot-placement` prop.

View File

@ -1,5 +1,34 @@
# CHANGELOG
## 2.15.5 (2021-07-16)
### Feats
- `n-tree` 新增 `render-label`、`render-prefix` 和 `render-suffix` 属性
- `n-rate` 新增 `allow-half` 属性
- `n-carousel` 新增 `show-arrow` 属性
- `n-slider` 新增 `format-tooltip` 属性
- `n-upload``on-finish` 回调参数中新增 `event`
- `n-slider` 新增 `format-tooltip` 属性
- `n-rate` 新增 `readonly` 属性
- `n-time-picker` 新增 `seconds`、`minutes`、`hours`属性
- `n-notification` 导出 `NotificationApi`, `NotificationOptions` and `NotificationReactive` 类型
- `n-avatar` 新增 `on-error` 属性,关闭[#394](https://github.com/TuSimple/naive-ui/issues/394)
- `n-image` 新增 `on-error` 属性,关闭[#394](https://github.com/TuSimple/naive-ui/issues/394)
- `n-image` 新增 `object-fit` 属性,关闭[#394](https://github.com/TuSimple/naive-ui/issues/394)
- `n-avatar` 新增 `object-fit` 属性,关闭[#394](https://github.com/TuSimple/naive-ui/issues/394)
- `n-menu` 默认展开选中项的全部父级,关闭[#481](https://github.com/TuSimple/naive-ui/issues/481)
### Fixes
- 修复 `n-calendar``default-value` 属性无法使用
- 修复 `n-pagination` `item-count` 为 0 时页数不对
- 修复 `n-scrollbar` `content-style` 无法覆盖默认样式的宽度
- 修复 `n-select` placeholder transition
- 修复 `n-loading-bar` `useLoadingBar` 返回类型可能为 undefined
- 修复 `n-tag``type` 增加 `primary` 类型
- 修复 `n-dynamic-tag``type` 增加 `primary` 类型
## 2.15.4 (2021-07-09)
### Feats
@ -31,7 +60,7 @@
### Feats
- `n-loading-bar` 导出 `LoadingBarApi` 类型
- `n-image` 增加 `imgProps` 属性
- `n-image` 增加 `img-props` 属性
- 在部分组件上添加原生 `title` 属性,以提高用户体验
- `n-tree` 在 TreeOption 中增加 `prefix``suffix` 属性
- `n-carousel` 增加 `dot-placement` 属性

View File

@ -1,6 +1,6 @@
{
"name": "naive-ui",
"version": "2.15.4",
"version": "2.15.5",
"description": "A Vue 3 Component Library. Fairly Complete, Customizable Themes, Uses TypeScript, Not Too Slow",
"main": "lib/index.js",
"module": "es/index.js",
@ -127,7 +127,7 @@
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.1",
"treemate": "^0.2.11",
"treemate": "^0.2.12",
"vdirs": "^0.1.4",
"vfonts": "^0.1.0",
"vooks": "^0.2.6",

View File

@ -746,6 +746,7 @@ export default defineComponent({
this.active ? null : (
<div
class={`${clsPrefix}-base-selection-label__render-label ${clsPrefix}-base-render-dom`}
key="input"
>
{renderLabel
? renderLabel(this.selectedOption as SelectBaseOption, true)
@ -755,6 +756,7 @@ export default defineComponent({
{showPlaceholder ? (
<div
class={`${clsPrefix}-base-selection-placeholder ${clsPrefix}-base-render-dom`}
key="placeholder"
>
{this.filterablePlaceholder}
</div>

View File

@ -26,11 +26,18 @@ const avatarProps = {
type: Boolean,
default: false
},
color: String,
objectFit: {
type: String as PropType<
'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
>,
default: 'fill'
},
round: {
type: Boolean,
default: false
},
color: String
onError: Function as PropType<(e: Event) => void>
} as const
export type AvatarProps = ExtractPublicPropTypes<typeof avatarProps>
@ -112,7 +119,11 @@ export default defineComponent({
style={this.cssVars as any}
>
{!$slots.default && src ? (
<img src={src} />
<img
src={src}
onError={this.onError}
style={{ objectFit: this.objectFit }}
/>
) : (
<span
ref="textRef"

View File

@ -118,4 +118,28 @@ describe('n-avatar', () => {
)
expect(wrapper.html()).toMatchSnapshot()
})
it('image avatar error handle when load failed', async () => {
const onError = jest.fn()
const wrapper = mount(NAvatar, {
props: {
src: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
onError
}
})
await wrapper.find('img').trigger('error')
expect(onError).toHaveBeenCalled()
})
it('should work with `objectFit` prop', () => {
const wrapper = mount(NAvatar, {
props: {
src: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
objectFit: 'contain'
}
})
expect(wrapper.find('img').attributes('style')).toContain(
'object-fit: contain;'
)
})
})

View File

@ -6,7 +6,7 @@ exports[`n-avatar custom style 1`] = `"<span class=\\"n-avatar\\" style=\\"--fon
exports[`n-avatar icon avatar 1`] = `"<span class=\\"n-avatar\\" style=\\"--font-size: 14px; --border-radius: 3px; --color: rgba(204, 204, 204, 1); --bezier: cubic-bezier(.4, 0, .2, 1); --size: 34px;\\"><span class=\\"n-avatar__text\\" style=\\"transform: translateX(-50%) translateY(-50%) scale(1);\\"><i role=\\"img\\" class=\\"n-icon\\" style=\\"--bezier: cubic-bezier(.4, 0, .2, 1);\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" viewBox=\\"0 0 512 512\\"><rect x=\\"32\\" y=\\"80\\" width=\\"448\\" height=\\"256\\" rx=\\"16\\" ry=\\"16\\" transform=\\"rotate(180 256 208)\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></rect><path fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\" d=\\"M64 384h384\\"></path><path fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\" d=\\"M96 432h320\\"></path><circle cx=\\"256\\" cy=\\"208\\" r=\\"80\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></circle><path d=\\"M480 160a80 80 0 0 1-80-80\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></path><path d=\\"M32 160a80 80 0 0 0 80-80\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></path><path d=\\"M480 256a80 80 0 0 0-80 80\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></path><path d=\\"M32 256a80 80 0 0 1 80 80\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" stroke-width=\\"32\\"></path></svg></i></span></span>"`;
exports[`n-avatar image avatar 1`] = `"<span class=\\"n-avatar\\" style=\\"--font-size: 14px; --border-radius: 3px; --color: rgba(204, 204, 204, 1); --bezier: cubic-bezier(.4, 0, .2, 1); --size: 34px;\\"><img src=\\"https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg\\"></span>"`;
exports[`n-avatar image avatar 1`] = `"<span class=\\"n-avatar\\" style=\\"--font-size: 14px; --border-radius: 3px; --color: rgba(204, 204, 204, 1); --bezier: cubic-bezier(.4, 0, .2, 1); --size: 34px;\\"><img src=\\"https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg\\" style=\\"object-fit: fill;\\"></span>"`;
exports[`n-avatar round avatar 1`] = `"<span class=\\"n-avatar\\" style=\\"--font-size: 14px; --border-radius: 50%; --color: rgba(204, 204, 204, 1); --bezier: cubic-bezier(.4, 0, .2, 1); --size: 34px;\\"><span class=\\"n-avatar__text\\" style=\\"transform: translateX(-50%) translateY(-50%) scale(1);\\"></span></span>"`;

View File

@ -3,21 +3,34 @@
A basic calender.
```html
<n-calendar @update:value="handleUpdateValue" #="{ year, month, date }">
<n-calendar
@update:value="handleUpdateValue"
#="{ year, month, date }"
v-model:value="value"
:is-date-disabled="isDateDisabled"
>
{{ year }}-{{ month }}-{{ date }}
</n-calendar>
```
```js
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import { useMessage } from 'naive-ui'
import { isYesterday, addDays } from 'date-fns'
export default defineComponent({
setup () {
const message = useMessage()
return {
value: ref(addDays(Date.now(), 1)),
handleUpdateValue (_, { year, month, date }) {
message.success(`${year}-${month}-${date}`)
},
isDateDisabled (timestamp) {
if (isYesterday(timestamp)) {
return true
}
return false
}
}
}

View File

@ -3,21 +3,34 @@
一个普通的日历。
```html
<n-calendar @update:value="handleUpdateValue" #="{ year, month, date }">
<n-calendar
@update:value="handleUpdateValue"
#="{ year, month, date }"
v-model:value="value"
:is-date-disabled="isDateDisabled"
>
{{ year }}-{{ month }}-{{ date }}
</n-calendar>
```
```js
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import { useMessage } from 'naive-ui'
import { isYesterday, addDays } from 'date-fns'
export default defineComponent({
setup () {
const message = useMessage()
return {
value: ref(addDays(Date.now(), 1)),
handleUpdateValue (_, { year, month, date }) {
message.success(`${year}-${month}-${date}`)
},
isDateDisabled (timestamp) {
if (isYesterday(timestamp)) {
return true
}
return false
}
}
}

View File

@ -55,7 +55,7 @@ export default defineComponent({
const now = Date.now()
// ts => timestamp
const monthTsRef = ref(startOfMonth(now).valueOf())
const uncontrolledValueRef = ref<number | null>(null)
const uncontrolledValueRef = ref<number | null>(props.defaultValue || null)
const mergedValueRef = useMergedState(
toRef(props, 'value'),
uncontrolledValueRef

View File

@ -1,8 +1,11 @@
import { h } from 'vue'
import { mount } from '@vue/test-utils'
import { NCalendar } from '../index'
import { isYesterday } from 'date-fns'
describe('n-calendar', () => {
const now = Date.now()
describe('n-button', () => {
it('should work with import on demand', () => {
mount(NCalendar)
})
@ -18,4 +21,35 @@ describe('n-button', () => {
) => {}}
/>
})
it('should work with `default-value` prop', async () => {
const wrapper = mount(NCalendar, { props: { defaultValue: now } })
expect(wrapper.find('.n-calendar-cell--selected').exists()).toBe(true)
})
it('should work with `value` prop', async () => {
const wrapper = mount(NCalendar, { props: { value: now } })
expect(wrapper.find('.n-calendar-cell--selected').exists()).toBe(true)
})
it('should work with `is-date-disabled` prop', async () => {
function disableFunction (timestamp: number): boolean {
if (isYesterday(timestamp)) {
return true
}
return false
}
const wrapper = mount(NCalendar, {
props: { 'is-date-disabled': disableFunction }
})
expect(wrapper.find('.n-calendar-cell--disabled').exists()).toBe(true)
})
it('should work with `on-update:value` prop', async () => {
const onUpdate = jest.fn()
const wrapper = mount(NCalendar, { props: { 'on-update:value': onUpdate } })
await wrapper.findAll('.n-calendar-date')[1].trigger('click')
expect(onUpdate).toHaveBeenCalled()
})
})

View File

@ -9,6 +9,7 @@ basic
autoplay
hover
dot-placement
show-arrow
```
## API
@ -20,6 +21,7 @@ dot-placement
| autoplay | `boolean` | `false` | Whether to scroll automatically. |
| interval | `number` | `5000` | Auto play interval. |
| dot-placement | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Dot placement in the panel. |
| show-arrow | `boolean` | `false` | Whether to show arrow button. |
| trigger | `'click' \| 'hover'` | `'click'` | The way to trigger the switch. |
### Carousel Slots

View File

@ -0,0 +1,30 @@
# Show Arrow Button
```html
<n-carousel show-arrow>
<img
class="carousel-img"
src="https://s.anw.red/fav/1623979004.jpg!/fw/600/quality/77/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623372884.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623177220.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623152423.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
</n-carousel>
```
```css
.carousel-img {
width: 100%;
height: 240px;
object-fit: cover;
}
```

View File

@ -9,6 +9,7 @@ basic
autoplay
hover
dot-placement
show-arrow
```
## API
@ -20,6 +21,7 @@ dot-placement
| autoplay | `boolean` | `false` | 是否自动播放 |
| dot-placement | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | 轮播指示点位置 |
| interval | `number` | `5000` | 自动播放的间隔 |
| show-arrow | `boolean` | `false` | 是否显示箭头按钮 |
| trigger | `'click' \| 'hover'` | `'click'` | 触发切换的方式 |
### Carousel Slots

View File

@ -0,0 +1,30 @@
# 显示箭头按钮
```html
<n-carousel show-arrow>
<img
class="carousel-img"
src="https://s.anw.red/fav/1623979004.jpg!/fw/600/quality/77/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623372884.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623177220.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
<img
class="carousel-img"
src="https://s.anw.red/news/1623152423.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true"
/>
</n-carousel>
```
```css
.carousel-img {
width: 100%;
height: 240px;
object-fit: cover;
}
```

View File

@ -14,6 +14,7 @@ import {
} from 'vue'
import { indexMap } from 'seemly'
import { on, off } from 'evtd'
import { BackwardIcon, ForwardIcon } from '../../_internal/icons'
import { useConfig, useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { flatten } from '../../_utils'
@ -24,6 +25,7 @@ import style from './styles/index.cssr'
const carouselProps = {
...(useTheme.props as ThemeProps<CarouselTheme>),
showArrow: Boolean,
autoplay: Boolean,
dotPlacement: {
type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
@ -258,19 +260,21 @@ export default defineComponent({
cssVars: computed(() => {
const {
common: { cubicBezierEaseInOut },
self: { dotColor, dotColorActive, dotSize }
self: { dotColor, dotColorActive, dotSize, arrowColor }
} = themeRef.value
return {
'--bezier': cubicBezierEaseInOut,
'--dot-color': dotColor,
'--dot-color-active': dotColorActive,
'--dot-size': dotSize
'--dot-size': dotSize,
'--arrow-color': arrowColor
}
})
}
},
render () {
const {
showArrow,
dotPlacement,
mergedClsPrefix,
current,
@ -350,6 +354,34 @@ export default defineComponent({
)
})}
</div>
{showArrow && [
<div
class={[
`${mergedClsPrefix}-carousel__arrow`,
`${mergedClsPrefix}-carousel__arrow--${
vertical ? 'bottom' : 'right'
}`
]}
role="button"
onClick={() => {
this.next()
}}
>
<ForwardIcon />
</div>,
<div
class={[
`${mergedClsPrefix}-carousel__arrow`,
`${mergedClsPrefix}-carousel__arrow--${vertical ? 'top' : 'left'}`
]}
role="button"
onClick={() => {
this.prev()
}}
>
<BackwardIcon />
</div>
]}
</div>
)
}

View File

@ -5,6 +5,7 @@ import { c, cB, cE, cM } from '../../../_utils/cssr'
// --dot-color
// --dot-color-active
// --dot-size
// --arrow-color
export default cB('carousel', `
overflow: hidden;
position: relative;
@ -47,6 +48,58 @@ export default cB('carousel', `
margin-right: 0;
`)
]),
cE('arrow', `
position: absolute;
transition: transform .3s var(--bezier);
transform: scale(1);
cursor: pointer;
height: 48px;
width: 48px;
display: flex;
align-items: center;
justify-content: center;
color: var(--arrow-color);
`, [
cM('right', `
transform: translateY(-50%);
top: 50%;
right: 0;
`, [
c('&:hover', {
transform: 'translateY(-50%) scale(1.1)'
}),
c('&:active', {
transform: 'translateY(-50%) scale(1)'
})
]),
cM('left', `
transform: translateY(-50%);
top: 50%;
left: 0;
`, [
c('&:hover', {
transform: 'translateY(-50%) scale(1.1)'
})
]),
cM('top', `
transform: translateX(-50%) rotate(90deg);
top: 0;
left: 50%;
`, [
c('&:hover', {
transform: 'translateX(-50%) scale(1.1) rotate(90deg)'
})
]),
cM('bottom', `
transform: translateX(-50%) rotate(90deg);
bottom: 0;
left: 50%;
`, [
c('&:hover', {
transform: 'translateX(-50%) scale(1.1) rotate(90deg)'
})
])
]),
cM('left', [
cE('slides', `
flex-direction: column;

View File

@ -6,7 +6,8 @@ export const self = (vars: ThemeCommonVars) => {
return {
dotSize: '8px',
dotColor: 'rgba(255, 255, 255, .3)',
dotColorActive: 'rgba(255, 255, 255, 1)'
dotColorActive: 'rgba(255, 255, 255, 1)',
arrowColor: 'rgba(255, 255, 255, .6)'
}
}

View File

@ -1,5 +1,7 @@
import { h, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { NCarousel } from '../index'
import { sleep } from 'seemly'
describe('n-carousel', () => {
it('should work with import on demand', () => {
@ -15,4 +17,75 @@ describe('n-carousel', () => {
)
}
})
it('should work with `showArrow` prop', async () => {
const wrapper = mount(NCarousel)
const dotToArrow = [
{
dot: ['top', 'bottom'],
arrow: ['left', 'right']
},
{
dot: ['left', 'right'],
arrow: ['top', 'bottom']
}
]
for (const item of dotToArrow) {
for (const dotItem of item.dot) {
await wrapper.setProps({ showArrow: true, dotPlacement: dotItem })
expect(
wrapper.find(`.n-carousel__arrow--${item.arrow[0]}`).exists()
).toBe(true)
expect(
wrapper.find(`.n-carousel__arrow--${item.arrow[1]}`).exists()
).toBe(true)
}
}
})
it('arrow button should work', async () => {
const wrapper = mount(NCarousel, {
slots: {
default: () => {
return [
h('img', {
style: 'width: 100%; height: 240px; object-fit: cover;',
src: 'https://s.anw.red/news/1623152423.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true'
}),
h('img', {
style: 'width: 100%; height: 240px; object-fit: cover;',
src: 'https://s.anw.red/news/1623152423.jpg!/both/800x450/quality/78/progressive/true/ignore-error/true'
})
]
}
}
})
await wrapper.setProps({
showArrow: true
})
const slidesDOMArray = wrapper.find('.n-carousel__slides').findAll('div')
expect(slidesDOMArray[1].attributes('aria-hidden')).toBe('false')
wrapper
.find('.n-carousel__arrow--right')
.trigger('click')
.then(async () => {
expect(slidesDOMArray[2].attributes('aria-hidden')).toBe('false')
await sleep(1000)
nextTick(() => {
wrapper
.find('.n-carousel__arrow--left')
.trigger('click')
.then(() => {
expect(slidesDOMArray[1].attributes('aria-hidden')).toBe('false')
})
})
})
})
})

View File

@ -1,6 +1,6 @@
import { nextTick } from 'vue'
import { h, nextTick } from 'vue'
import { mount, VueWrapper } from '@vue/test-utils'
import { NCheckbox } from '../index'
import { NCheckbox, NCheckboxGroup } from '../index'
function expectChecked (wrapper: VueWrapper<any>, value: boolean): void {
expect(wrapper.classes().some((c) => c.includes('checked'))).toEqual(value)
@ -10,6 +10,7 @@ describe('n-checkbox', () => {
it('should work with import on demand', () => {
mount(NCheckbox)
})
describe('uncontrolled mode', () => {
it('works', async () => {
const wrapper = mount(NCheckbox)
@ -30,4 +31,125 @@ describe('n-checkbox', () => {
expectChecked(wrapper, true)
})
})
it('should work with `indeterminate` prop', () => {
const wrapper = mount(NCheckbox, {
props: {
indeterminate: true
},
slots: {
default: () => 'test'
}
})
expect(wrapper.find('.n-checkbox').classes()).toContain(
'n-checkbox--indeterminate'
)
})
it('should work with `disabled` prop', () => {
const wrapper = mount(NCheckbox, {
props: {
disabled: true
},
slots: {
default: () => 'test'
}
})
expect(wrapper.find('.n-checkbox').classes()).toContain(
'n-checkbox--disabled'
)
})
it('should work with `focusable` prop', async () => {
const wrapper = mount(NCheckbox, {
props: {
focusable: false
},
slots: {
default: () => 'test'
}
})
expect(wrapper.find('[tabindex]').exists()).not.toBe(true)
await wrapper.setProps({ focusable: true })
expect(wrapper.find('[tabindex]').exists()).toBe(true)
expect(wrapper.find('.n-checkbox').attributes('tabindex')).toContain('0')
})
it('should work with `label` prop', async () => {
const wrapper = mount(NCheckbox, {
props: {
label: 'test'
}
})
expect(wrapper.find('.n-checkbox__label').text()).toContain('test')
})
it('should work with `on-update:checked` prop', async () => {
const onClick = jest.fn()
const wrapper = mount(NCheckbox, {
props: {
'onUpdate:checked': onClick
},
slots: {
default: () => 'test'
}
})
wrapper.trigger('click')
expect(onClick).toBeCalled()
})
it('should work with default slots', async () => {
const wrapper = mount(NCheckbox, {
slots: {
default: () => 'test'
}
})
expect(wrapper.find('.n-checkbox__label').text()).toContain('test')
})
})
describe('n-checkbox-group', () => {
it('should work with import on demand', () => {
mount(NCheckboxGroup)
})
it('should work with `disabled` prop', () => {
const wrapper = mount(NCheckboxGroup, {
props: {
disabled: true
},
slots: {
default: () => h(NCheckbox, null, { default: () => 'test' })
}
})
expect(wrapper.find('.n-checkbox--disabled').exists()).toBe(true)
})
it('should work with `on-update:value` prop', async () => {
const onClick = jest.fn()
const wrapper = mount(NCheckboxGroup, {
props: {
'on-update:value': onClick
},
slots: {
default: () => h(NCheckbox, { value: 'test' })
}
})
await wrapper.findComponent(NCheckbox).trigger('click')
expect(onClick).toBeCalled()
})
it('should work with default slots', async () => {
const wrapper = mount(NCheckboxGroup, {
props: {
disabled: true
},
slots: {
default: () => h(NCheckbox, null, { default: () => 'test' })
}
})
expect(wrapper.find('.n-checkbox__label').text()).toContain('test')
})
})

View File

@ -6,6 +6,6 @@ export type {
DialogProviderInst,
DialogOptions,
DialogReactive,
DialogApiInjection
DialogApiInjection as DialogApi
} from './src/DialogProvider'
export { useDialog } from './src/use-dialog'

View File

@ -44,11 +44,11 @@ export default defineComponent({
}
},
render () {
const { $slots } = this
const { tag, mergedClsPrefix, cssVars } = this
const { tag, mergedClsPrefix, cssVars, $slots } = this
return h(
tag,
{
role: 'none',
class: `${mergedClsPrefix}-element`,
style: cssVars
},

View File

@ -29,9 +29,16 @@ const imageProps = {
alt: String,
height: [String, Number] as PropType<string | number>,
imgProps: Object as PropType<imgProps>,
objectFit: {
type: String as PropType<
'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
>,
default: 'fill'
},
width: [String, Number] as PropType<string | number>,
src: String,
showToolbar: { type: Boolean, default: true }
showToolbar: { type: Boolean, default: true },
onError: Function as PropType<(e: Event) => void>
}
export type ImageProps = ExtractPublicPropTypes<typeof imageProps>
@ -86,6 +93,8 @@ export default defineComponent({
alt={this.alt ? this.alt : imgProps.alt}
aria-label={this.alt ? this.alt : imgProps.alt}
onClick={this.handleClick}
onError={this.onError}
style={{ objectFit: this.objectFit }}
/>
)

View File

@ -73,6 +73,8 @@ export default c([
display: inline-flex;
cursor: pointer;
`, [
c('img', 'border-radius: inherit;')
c('img', `
border-radius: inherit;
`)
])
])

View File

@ -91,4 +91,28 @@ describe('n-image', () => {
})
expect(wrapper.find('[data-cool]').exists()).toEqual(true)
})
it('should work with `onError` prop', async () => {
const onError = jest.fn()
const wrapper = mount(NImage, {
props: {
src: 'https:// 07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
onError
}
})
await wrapper.find('img').trigger('error')
expect(onError).toHaveBeenCalled()
})
it('should work with `objectFit` prop', () => {
const wrapper = mount(NImage, {
props: {
src: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
objectFit: 'contain'
}
})
expect(wrapper.find('img').attributes('style')).toContain(
'object-fit: contain;'
)
})
})

View File

@ -1,7 +1,12 @@
import { inject } from 'vue'
import { loadingBarApiInjectionKey } from './LoadingBarProvider'
import type { LoadingBarApiInjection } from './LoadingBarProvider'
import { throwError } from '../../_utils'
export function useLoadingBar (): LoadingBarApiInjection | undefined {
return inject(loadingBarApiInjectionKey)
export function useLoadingBar (): LoadingBarApiInjection {
const loadingBar = inject(loadingBarApiInjectionKey, null)
if (loadingBar === null) {
throwError('use-loading-bar', 'No outer <n-loading-bar-provider /> founded.')
}
return loadingBar
}

View File

@ -2,6 +2,8 @@
You can set `default-expanded-keys` to make menu work in an uncontrolled manner or use `expanded-keys` and `@update:expanded-keys` to make it work in a controlled manner.
If you don't set `default-expanded-keys`, menu will expand all the ascendant of selected option by default.
```html
<n-menu
:options="menuOptions"

View File

@ -10,7 +10,7 @@ No Food.
horizontal
select
render-label
default-expanded-names
default-expanded-keys
indent
collapse
inverted

View File

@ -2,6 +2,8 @@
你可以设定 `default-expanded-keys` 让菜单工作在非受控状态下或者使用 `expanded-keys``@update:expanded-keys` 以受控的方式控制菜单。
如果你不设定 `default-expanded-keys`,菜单会默认展开选中项的全部父级。
```html
<n-menu
:options="menuOptions"

View File

@ -71,10 +71,7 @@ const menuProps = {
default: 32
},
defaultExpandAll: Boolean,
defaultExpandedKeys: {
type: Array as PropType<Key[]>,
default: () => []
},
defaultExpandedKeys: Array as PropType<Key[]>,
expandedKeys: {
type: Array as PropType<Key[]>,
default: undefined
@ -205,10 +202,22 @@ export default defineComponent({
}
)
)
const uncontrolledValueRef = ref(props.defaultValue)
const controlledValueRef = toRef(props, 'value')
const mergedValueRef = useMergedState(
controlledValueRef,
uncontrolledValueRef
)
const uncontrolledExpandedKeysRef = ref(
props.defaultExpandAll
? treeMateRef.value.getNonLeafKeys()
: props.defaultExpandedNames || props.defaultExpandedKeys
: props.defaultExpandedNames ||
props.defaultExpandedKeys ||
treeMateRef.value.getPath(mergedValueRef.value, {
includeSelf: false
}).keyPath
)
const controlledExpandedKeysRef = useCompitable(props, [
'expandedNames',
@ -218,12 +227,6 @@ export default defineComponent({
controlledExpandedKeysRef,
uncontrolledExpandedKeysRef
)
const uncontrolledValueRef = ref(props.defaultValue)
const controlledValueRef = toRef(props, 'value')
const mergedValueRef = useMergedState(
controlledValueRef,
uncontrolledValueRef
)
const tmNodesRef = computed(() => treeMateRef.value.treeNodes)
const activePathRef = computed(() => {
return treeMateRef.value.getPath(mergedValueRef.value).keyPath

View File

@ -1,6 +1,9 @@
export { default as NNotificationProvider } from './src/NotificationProvider'
export type {
NotificationProviderProps,
NotificationProviderInst
NotificationProviderInst,
NotificationApiInjection as NotificationApi,
NotificationOptions,
NotificationReactive
} from './src/NotificationProvider'
export { useNotification } from './src/use-notification'

View File

@ -24,7 +24,7 @@ import NotificationEnvironment, {
} from './NotificationEnvironment'
import style from './styles/index.cssr'
type NotificationOptions = Partial<
export type NotificationOptions = Partial<
ExtractPropTypes<typeof notificationEnvOptions>
>
@ -58,7 +58,7 @@ export const notificationApiInjectionKey: InjectionKey<NotificationApiInjection>
'notificationApi'
)
type NotificationReactive = {
export type NotificationReactive = {
readonly key: string
readonly destroy: () => void
/** @deprecated */

View File

@ -125,7 +125,7 @@ export default defineComponent({
// item count has high priority, for it can affect prefix slot rendering
const { itemCount } = props
if (itemCount !== undefined) {
return Math.ceil(itemCount / mergedPageSizeRef.value)
return Math.max(1, Math.ceil(itemCount / mergedPageSizeRef.value))
}
const { pageCount } = props
if (pageCount !== undefined) return pageCount

View File

@ -5,4 +5,17 @@ describe('n-pagination', () => {
it('should work with import on demand', () => {
mount(NPagination)
})
it('props.itemCount', async () => {
const wrapper = mount(NPagination, {
props: {
itemCount: 1,
pageSize: 10
}
})
expect(wrapper.findAll('.n-pagination-item').length).toEqual(3)
await wrapper.setProps({
itemCount: 11
})
expect(wrapper.findAll('.n-pagination-item').length).toEqual(4)
})
})

View File

@ -24,7 +24,7 @@ radio-focus-debug
| default-checked | `boolean` | `false` | |
| disabled | `boolean` | `false` | |
| name | `string` | `undefined` | 单选按钮 radio 元素的 name 属性。如果没有设定会使用 `n-radio-group``name` |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 只用于 `n-radio` |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | |
| value | `string` | `undefined` | |
| on-update:checked-value | `(checked: boolean) => void` | `undefined` | |

View File

@ -5,4 +5,31 @@ describe('n-radio', () => {
it('should work with import on demand', () => {
mount(NRadio)
})
it('should work with `checked` prop', async () => {
const wrapper = mount(NRadio, { props: { checked: false } })
expect(wrapper.find('.n-radio').classes()).not.toContain('n-radio--checked')
await wrapper.setProps({ checked: true })
expect(wrapper.find('.n-radio').classes()).toContain('n-radio--checked')
})
it('should work with `disabled` prop', async () => {
const wrapper = mount(NRadio, { props: { disabled: false } })
expect(wrapper.find('.n-radio').classes()).not.toContain(
'n-radio--disabled'
)
await wrapper.setProps({ disabled: true })
expect(wrapper.find('.n-radio').classes()).toContain('n-radio--disabled')
})
it('should work with `size` prop', async () => {
const wrapper = mount(NRadio, { props: { size: 'small' } })
expect(wrapper.find('.n-radio').attributes('style')).toMatchSnapshot()
await wrapper.setProps({ size: 'medium' })
expect(wrapper.find('.n-radio').attributes('style')).toMatchSnapshot()
await wrapper.setProps({ size: 'large' })
expect(wrapper.find('.n-radio').attributes('style')).toMatchSnapshot()
})
})

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`n-radio should work with \`size\` prop 1`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --box-shadow: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-active: inset 0 0 0 1px #18a058; --box-shadow-disabled: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-focus: inset 0 0 0 1px #18a058, 0 0 0 2px rgba(24, 160, 88, 0.2); --box-shadow-hover: inset 0 0 0 1px #18a058; --color: #FFF; --color-disabled: rgb(250, 250, 252); --dot-color-active: #18a058; --dot-color-disabled: rgb(224, 224, 230); --font-size: 14px; --radio-size: 14px; --text-color: rgb(51, 54, 57); --text-color-disabled: rgba(194, 194, 194, 1); --label-padding: 0 8px;"`;
exports[`n-radio should work with \`size\` prop 2`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --box-shadow: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-active: inset 0 0 0 1px #18a058; --box-shadow-disabled: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-focus: inset 0 0 0 1px #18a058, 0 0 0 2px rgba(24, 160, 88, 0.2); --box-shadow-hover: inset 0 0 0 1px #18a058; --color: #FFF; --color-disabled: rgb(250, 250, 252); --dot-color-active: #18a058; --dot-color-disabled: rgb(224, 224, 230); --font-size: 14px; --radio-size: 16px; --text-color: rgb(51, 54, 57); --text-color-disabled: rgba(194, 194, 194, 1); --label-padding: 0 8px;"`;
exports[`n-radio should work with \`size\` prop 3`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --box-shadow: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-active: inset 0 0 0 1px #18a058; --box-shadow-disabled: inset 0 0 0 1px rgb(224, 224, 230); --box-shadow-focus: inset 0 0 0 1px #18a058, 0 0 0 2px rgba(24, 160, 88, 0.2); --box-shadow-hover: inset 0 0 0 1px #18a058; --color: #FFF; --color-disabled: rgb(250, 250, 252); --dot-color-active: #18a058; --dot-color-disabled: rgb(224, 224, 230); --font-size: 15px; --radio-size: 18px; --text-color: rgb(51, 54, 57); --text-color-disabled: rgba(194, 194, 194, 1); --label-padding: 0 8px;"`;

View File

@ -0,0 +1,5 @@
# Allow Selecting Half Star
```html
<n-rate allow-half />
```

View File

@ -9,15 +9,19 @@ basic
size
color
icon
allow-half
readonly
```
## Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| allow-half | `boolean` | `false` | Allow activated half of the icon. |
| color | `string` | `undefined` | Icon color activated(support `#FFF`, `#FFFFFF`, `yellow`,`rgb(0, 0, 0)` formatted colors). |
| count | `number` | `5` | Icon count. |
| default-value | `number` | `0` | Value of activated icons by default. |
| readonly | `boolean` | `false` | Read only. |
| size | `'small' \| 'medium' \| 'large' \| number` | `'medium'` | Icon size. |
| value | `number` | `undefined` | Value of activated icons. |
| on-update:value | `(value: number) => void` | `undefined` | Callback when update value. |

View File

@ -0,0 +1,5 @@
# Read only
```html
<n-rate readonly :default-value="3" />
```

View File

@ -0,0 +1,5 @@
# 允许半颗
```html
<n-rate allow-half />
```

View File

@ -9,15 +9,19 @@ basic
size
color
icon
allow-half
readonly
```
## Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| allow-half | `boolean` | `false` | 允许只激活一半图标 |
| color | `string` | `undefined` | 已激活图标颜色(支持形如 `#FFF` `#FFFFFF` `yellow``rgb(0, 0, 0)` 的颜色) |
| count | `number` | `5` | 图标个数 |
| default-value | `number` | `0` | 默认已激活图标个数 |
| readonly | `boolean` | `false` | 只读,交互失效 |
| size | `'small' \| 'medium' \| 'large' \| number` | `'medium'` | 图标尺寸 |
| value | `number` | `undefined` | 绑定已激活图标个数 |
| on-update:value | `(value: number) => void` | `undefined` | 激活图标个数改变时触发 |

View File

@ -0,0 +1,5 @@
# 只读
```html
<n-rate readonly :default-value="3" />
```

View File

@ -21,6 +21,7 @@ import StarIcon from './StarIcon'
const rateProps = {
...(useTheme.props as ThemeProps<RateTheme>),
allowHalf: Boolean,
count: {
type: Number,
default: 5
@ -30,6 +31,7 @@ const rateProps = {
type: Number,
default: 0
},
readonly: Boolean,
size: {
type: [String, Number] as PropType<number | 'small' | 'medium' | 'large'>,
default: 'medium'
@ -76,20 +78,34 @@ export default defineComponent({
nTriggerFormChange()
nTriggerFormInput()
}
function handleMouseEnter (index: number): void {
hoverIndexRef.value = index
function getDerivedValue (index: number, e: MouseEvent): number {
if (props.allowHalf) {
if (
e.offsetX >=
Math.floor((e.currentTarget as HTMLDivElement).offsetWidth / 2)
) {
return index + 1
} else {
return index + 0.5
}
} else {
return index + 1
}
}
function handleMouseMove (index: number, e: MouseEvent): void {
hoverIndexRef.value = getDerivedValue(index, e)
}
function handleMouseLeave (): void {
hoverIndexRef.value = null
}
function handleClick (index: number): void {
doUpdateValue(index + 1)
function handleClick (index: number, e: MouseEvent): void {
doUpdateValue(getDerivedValue(index, e))
}
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedValue: useMergedState(controlledValueRef, uncontrolledValueRef),
hoverIndex: hoverIndexRef,
handleMouseEnter,
handleMouseMove,
handleClick,
handleMouseLeave,
cssVars: computed(() => {
@ -116,6 +132,7 @@ export default defineComponent({
},
render () {
const {
readonly,
hoverIndex,
mergedValue,
mergedClsPrefix,
@ -123,34 +140,69 @@ export default defineComponent({
} = this
return (
<div
class={`${mergedClsPrefix}-rate`}
class={[
`${mergedClsPrefix}-rate`,
{
[`${mergedClsPrefix}-rate--readonly`]: readonly
}
]}
style={this.cssVars as CSSProperties}
onMouseleave={this.handleMouseLeave}
>
{renderList(this.count, (_, index) => (
<div
key={index}
class={[
`${mergedClsPrefix}-rate__item`,
{
[`${mergedClsPrefix}-rate__item--active`]:
hoverIndex !== null
? index <= hoverIndex
: index < mergedValue
{renderList(this.count, (_, index) => {
const icon = defaultSlot ? (
defaultSlot()
) : (
<NBaseIcon clsPrefix={mergedClsPrefix}>
{{ default: () => StarIcon }}
</NBaseIcon>
)
return (
<div
key={index}
class={[
`${mergedClsPrefix}-rate__item`,
{
[`${mergedClsPrefix}-rate__item--active`]:
hoverIndex !== null
? index + 1 <= hoverIndex
: index + 1 <= mergedValue
}
]}
onClick={
readonly
? undefined
: (e) => {
this.handleClick(index, e)
}
}
]}
onClick={() => this.handleClick(index)}
onMouseenter={() => this.handleMouseEnter(index)}
>
{defaultSlot ? (
defaultSlot()
) : (
<NBaseIcon clsPrefix={mergedClsPrefix}>
{{ default: () => StarIcon }}
</NBaseIcon>
)}
</div>
))}
onMousemove={
readonly
? undefined
: (e) => {
this.handleMouseMove(index, e)
}
}
>
{icon}
{this.allowHalf ? (
<div
class={[
`${mergedClsPrefix}-rate__half`,
{
[`${mergedClsPrefix}-rate__half--active`]:
hoverIndex !== null
? index + 0.5 <= hoverIndex
: index + 0.5 <= mergedValue
}
]}
>
{icon}
</div>
) : null}
</div>
)
})}
</div>
)
}

View File

@ -1,4 +1,4 @@
import { c, cB, cE, cM } from '../../../_utils/cssr'
import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr'
// vars:
// --bezier
@ -12,30 +12,51 @@ export default cB('rate', {
cE('item', `
transition:
transform .1s var(--bezier),
color .1s var(--bezier);
color .3s var(--bezier);
`)
]),
cE('item', `
position: relative;
display: flex;
transition:
transform .1s var(--bezier),
color .3s var(--bezier);
transform: scale(1);
font-size: var(--item-size);
cursor: pointer;
color: var(--item-color);
`, [
c('&:hover', {
transform: 'scale(1.05)'
}),
c('&:active', {
transform: 'scale(0.96)'
}),
c('&:not(:first-child)', {
marginLeft: '6px'
}),
cM('active', {
color: 'var(--item-color-active)'
})
]),
cNotM('readonly', `
cursor: pointer;
`, [
cE('item', [
c('&:hover', {
transform: 'scale(1.05)'
}),
c('&:active', {
transform: 'scale(0.96)'
})
])
]),
cE('half', `
display: flex;
transition: inherit;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 50%;
overflow: hidden;
color: var(--item-color);
`, [
cM('active', {
color: 'var(--item-color-active)'
})
])
])

View File

@ -79,4 +79,23 @@ describe('n-rate', () => {
wrapper.unmount()
})
it('should work with `readonly` prop', async () => {
const wrapper = mount(NRate)
await wrapper.setProps({ readonly: true })
expect(wrapper.find('.n-rate').classes()).toContain('n-rate--readonly')
await wrapper.setProps({ readonly: true, value: 3 })
expect(wrapper.findAll('.n-rate__item--active').length).toBe(3)
await wrapper.findAll('.n-rate__item')[3].trigger('click')
expect(wrapper.findAll('.n-rate__item--active').length).toBe(3)
await wrapper.findAll('.n-rate__item')[3].trigger('mousemove')
expect(wrapper.findAll('.n-rate__item--active').length).toBe(3)
wrapper.unmount()
})
})

View File

@ -619,10 +619,10 @@ const Scrollbar = defineComponent({
ref="contentRef"
style={
[
this.contentStyle,
{
width: this.xScrollable ? 'fit-content' : null
}
},
this.contentStyle
] as any
}
class={[

View File

@ -19,6 +19,7 @@ disable-tooltip
| --- | --- | --- | --- |
| default-value | `number \| [number, number] \| null` | `null` | Default value. |
| disabled | `boolean` | `false` | Whether the slider is disabled. |
| format-tooltip | `(value: number) => string \| number` | `undefined` | Format tooltip. |
| marks | `{ [markValue: number]: string }` | `undefined` | Marks of the slider. |
| max | `number` | `100` | Max value of the slider. |
| min | `number` | `0` | Min value of the slider. |

View File

@ -19,6 +19,7 @@ disable-tooltip
| --- | --- | --- | --- |
| default-value | `number \| [number, number] \| null` | `null` | 默认值 |
| disabled | `boolean` | `false` | 是否禁用 |
| format-tooltip | `(value: number) => string \| number` | `undefined` | 格式化 tooltip |
| marks | `{ [markValue: number]: string }` | `undefined` | Slider 上的标记 |
| max | `number` | `100` | 最大值 |
| min | `number` | `0` | 最小值 |

View File

@ -38,6 +38,7 @@ const sliderProps = {
},
marks: Object as PropType<Record<string, string>>,
disabled: Boolean,
formatTooltip: Function as PropType<(value: number) => string | number>,
min: {
type: Number,
default: 0
@ -786,7 +787,7 @@ export default defineComponent({
}
},
render () {
const { mergedClsPrefix } = this
const { mergedClsPrefix, formatTooltip } = this
return (
<div
class={[
@ -875,7 +876,9 @@ export default defineComponent({
class={`${mergedClsPrefix}-slider-handle-indicator`}
style={this.indicatorCssVars as CSSProperties}
>
{this.handleValue1}
{typeof formatTooltip === 'function'
? formatTooltip(this.handleValue1)
: this.handleValue1}
</div>
) : null
}}
@ -931,7 +934,9 @@ export default defineComponent({
class={`${mergedClsPrefix}-slider-handle-indicator`}
style={this.indicatorCssVars as CSSProperties}
>
{this.handleValue2}
{typeof formatTooltip === 'function'
? formatTooltip(this.handleValue2)
: this.handleValue2}
</div>
) : null
}}

View File

@ -5,4 +5,41 @@ describe('n-switch', () => {
it('should work with import on demand', () => {
mount(NSwitch)
})
it('should work with `disabled` prop', async () => {
const wrapper = mount(NSwitch)
expect(wrapper.find('.n-switch--disabled').exists()).not.toBe(true)
await wrapper.setProps({ disabled: true })
expect(wrapper.find('.n-switch').classes()).toContain('n-switch--disabled')
})
it('should work with `size` prop', async () => {
const wrapper = mount(NSwitch)
await wrapper.setProps({ size: 'small' })
expect(wrapper.find('.n-switch').attributes('style')).toMatchSnapshot()
await wrapper.setProps({ size: 'medium' })
expect(wrapper.find('.n-switch').attributes('style')).toMatchSnapshot()
await wrapper.setProps({ size: 'large' })
expect(wrapper.find('.n-switch').attributes('style')).toMatchSnapshot()
})
it('should work with `value` prop', async () => {
const wrapper = mount(NSwitch, { props: { value: true } })
expect(wrapper.find('.n-switch--active').exists()).toBe(true)
await wrapper.setProps({ value: false })
expect(wrapper.find('.n-switch--active').exists()).not.toBe(true)
})
it('should work with `on-update:value` prop', async () => {
const onUpdate = jest.fn()
const wrapper = mount(NSwitch, { props: { 'onUpdate:value': onUpdate } })
await wrapper.trigger('click')
expect(onUpdate).toHaveBeenCalled()
})
})

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`n-switch should work with \`size\` prop 1`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --button-border-radius: 3px; --button-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.3), inset 0 0 1px 0 rgba(0, 0, 0, 0.05); --button-color: #FFF; --button-width: 14px; --button-width-pressed: 20px; --button-height: 14px; --height: 18px; --offset: 2px; --opacity-disabled: 0.5; --rail-border-radius: 3px; --rail-color: rgba(0, 0, 0, .14); --rail-color-active: #18a058; --rail-height: 18px; --rail-width: 32px; --width: 32px; --box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2);"`;
exports[`n-switch should work with \`size\` prop 2`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --button-border-radius: 3px; --button-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.3), inset 0 0 1px 0 rgba(0, 0, 0, 0.05); --button-color: #FFF; --button-width: 18px; --button-width-pressed: 24px; --button-height: 18px; --height: 22px; --offset: 2px; --opacity-disabled: 0.5; --rail-border-radius: 3px; --rail-color: rgba(0, 0, 0, .14); --rail-color-active: #18a058; --rail-height: 22px; --rail-width: 40px; --width: 40px; --box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2);"`;
exports[`n-switch should work with \`size\` prop 3`] = `"--bezier: cubic-bezier(.4, 0, .2, 1); --button-border-radius: 3px; --button-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.3), inset 0 0 1px 0 rgba(0, 0, 0, 0.05); --button-color: #FFF; --button-width: 22px; --button-width-pressed: 28px; --button-height: 22px; --height: 26px; --offset: 2px; --opacity-disabled: 0.5; --rail-border-radius: 3px; --rail-color: rgba(0, 0, 0, .14); --rail-color-active: #18a058; --rail-height: 26px; --rail-width: 48px; --width: 48px; --box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2);"`;

View File

@ -38,7 +38,7 @@ export default c([
--merged-border-color: var(--border-color);
`, [
c('th', `
white-spac: nowrap;
white-space: nowrap;
transition:
background-color .3s var(--bezier),
border-color .3s var(--bezier),

View File

@ -3,7 +3,7 @@ import { PropType } from 'vue'
export default {
type: {
type: String as PropType<
'default' | 'success' | 'info' | 'warning' | 'error'
'default' | 'primary' | 'success' | 'info' | 'warning' | 'error'
>,
default: 'default'
},

View File

@ -8,6 +8,7 @@ Like a digital clock.
basic
size
disabled-time
step-time
format
```
@ -19,6 +20,9 @@ format
| default-value | `number \| null` | `null` | |
| disabled | `boolean` | `false` | |
| format | `string` | `'HH:mm:ss'` | |
| hours | `number \| number[]` | `undefined` | The hours to be displayed. If it's a number, it'll be viewed as step. |
| minutes | `number \| number[]` | `undefined` | The minutes to be displayed. If it's a number, it'll be viewed as step. |
| seconds | `number \| number[]` | `undefined` | The seconds to be displayed. If it's a number, it'll be viewed as step. |
| is-hour-disabled | `(hour: number) => boolean` | `() => false` | |
| is-minute-disabled | `(minute: number, hour: number) => boolean` | `() => false` | |
| is-second-disabled | `(second: number, minute: number, hour: number) => boolean` | `() => false` | |

View File

@ -0,0 +1,7 @@
# Step Time
Pass a number as step or use an array to specify the items you want to show.
```html
<n-time-picker :seconds="[0]" :hours="[8,18]" :minutes="30" />
```

View File

@ -8,6 +8,7 @@
basic
size
disabled-time
step-time
format
```
@ -19,6 +20,9 @@ format
| default-value | `number \| null` | `null` | |
| disabled | `boolean` | `false` | |
| format | `string` | `'HH:mm:ss'` | |
| hours | `number \| number[]` | `undefined` | 通过数组指定显示的小时。当值为 number 时,将被当做时间步进处理 |
| minutes | `number \| number[]` | `undefined` | 通过数组指定显示的分钟。当值为 number 时,将被当做时间步进处理 |
| seconds | `number \| number[]` | `undefined` | 通过数组指定显示的秒。当值为 number 时,将被当做时间步进处理 |
| is-hour-disabled | `(hour: number) => boolean` | `() => false` | |
| is-minute-disabled | `(minute: number, hour: number) => boolean` | `() => false` | |
| is-second-disabled | `(second: number, minute: number, hour: number) => boolean` | `() => false` | |

View File

@ -0,0 +1,7 @@
# 展示某些时间
传递单独的数字来定义时间步进或用数组指定你需要显示的内容。
```html
<n-time-picker :seconds="[0]" :hours="[8,18]" :minutes="30" />
```

View File

@ -2,7 +2,7 @@ import { h, ref, defineComponent, inject, PropType, computed } from 'vue'
import { NScrollbar } from '../../scrollbar'
import { NButton } from '../../button'
import { NBaseFocusDetector } from '../../_internal'
import { time } from './utils'
import { time, getFixValue } from './utils'
import {
IsHourDisabled,
IsMinuteDisabled,
@ -10,6 +10,7 @@ import {
timePickerInjectionKey
} from './interface'
import PanelCol, { Item } from './PanelCol'
import { MaybeArray } from '../../_utils'
export default defineComponent({
name: 'TimePickerPanel',
@ -84,7 +85,10 @@ export default defineComponent({
onFocusin: Function as PropType<(e: FocusEvent) => void>,
onFocusout: Function as PropType<(e: FocusEvent) => void>,
onFocusDetectorFocus: Function as PropType<() => void>,
onKeydown: Function as PropType<(e: KeyboardEvent) => void>
onKeydown: Function as PropType<(e: KeyboardEvent) => void>,
hours: [Number, Array] as PropType<MaybeArray<number>>,
minutes: [Number, Array] as PropType<MaybeArray<number>>,
seconds: [Number, Array] as PropType<MaybeArray<number>>
},
setup (props) {
const {
@ -92,18 +96,34 @@ export default defineComponent({
mergedClsPrefixRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(timePickerInjectionKey)!
const hoursRef = computed<Item[]>(() =>
time.hours.map((hour) => {
const { isHourDisabled } = props
function getTimeUnits (
defaultValue: string[],
stepOrList: MaybeArray<number> | undefined
): string[] {
if (Array.isArray(stepOrList)) {
return stepOrList.map((v) => Math.floor(v)).map((v) => getFixValue(v))
} else if (typeof stepOrList === 'number') {
return defaultValue.filter((hour) => Number(hour) % stepOrList === 0)
} else {
return defaultValue
}
}
const hoursRef = computed<Item[]>(() => {
const { isHourDisabled, hours } = props
return getTimeUnits(time.hours, hours).map((hour) => {
return {
value: hour,
disabled: isHourDisabled ? isHourDisabled(Number(hour)) : false
}
})
)
const minutesRef = computed<Item[]>(() =>
time.minutes.map((minute) => {
const { isMinuteDisabled } = props
})
const minutesRef = computed<Item[]>(() => {
const { isMinuteDisabled, minutes } = props
return getTimeUnits(time.minutes, minutes).map((minute) => {
return {
value: minute,
disabled: isMinuteDisabled
@ -111,10 +131,11 @@ export default defineComponent({
: false
}
})
)
const secondsRef = computed<Item[]>(() =>
time.seconds.map((second) => {
const { isSecondDisabled } = props
})
const secondsRef = computed<Item[]>(() => {
const { isSecondDisabled, seconds } = props
return getTimeUnits(time.seconds, seconds).map((second) => {
return {
value: second,
disabled: isSecondDisabled
@ -126,7 +147,7 @@ export default defineComponent({
: false
}
})
)
})
return {
mergedTheme: mergedThemeRef,
mergedClsPrefix: mergedClsPrefixRef,
@ -156,10 +177,10 @@ export default defineComponent({
class={[
`${mergedClsPrefix}-time-picker-col`,
{
[`${mergedClsPrefix}-time-picker-col--invalid`]: this
.isHourInvalid,
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]: this
.transitionDisabled
[`${mergedClsPrefix}-time-picker-col--invalid`]:
this.isHourInvalid,
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]:
this.transitionDisabled
}
]}
>
@ -189,10 +210,10 @@ export default defineComponent({
class={[
`${mergedClsPrefix}-time-picker-col`,
{
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]: this
.transitionDisabled,
[`${mergedClsPrefix}-time-picker-col--invalid`]: this
.isMinuteInvalid
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]:
this.transitionDisabled,
[`${mergedClsPrefix}-time-picker-col--invalid`]:
this.isMinuteInvalid
}
]}
>
@ -222,10 +243,10 @@ export default defineComponent({
class={[
`${mergedClsPrefix}-time-picker-col`,
{
[`${mergedClsPrefix}-time-picker-col--invalid`]: this
.isSecondInvalid,
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]: this
.transitionDisabled
[`${mergedClsPrefix}-time-picker-col--invalid`]:
this.isSecondInvalid,
[`${mergedClsPrefix}-time-picker-col--transition-disabled`]:
this.transitionDisabled
}
]}
>

View File

@ -59,6 +59,18 @@ import {
} from './interface'
import { happensIn } from 'seemly'
// validate hours,minutes,seconds prop
function validateUnits (value: MaybeArray<number>, max: number): boolean {
if (value === undefined) {
return true
}
if (Array.isArray(value)) {
return value.every((v) => v >= 0 && v <= max)
} else {
return value >= 0 && value <= max
}
}
const timePickerProps = {
...(useTheme.props as ThemeProps<TimePickerTheme>),
to: useAdjustedTo.propTo,
@ -119,6 +131,18 @@ const timePickerProps = {
return true
},
default: undefined
},
hours: {
type: [Number, Array] as PropType<MaybeArray<number>>,
validator: (value: MaybeArray<number>) => validateUnits(value, 23)
},
minutes: {
type: [Number, Array] as PropType<MaybeArray<number>>,
validator: (value: MaybeArray<number>) => validateUnits(value, 59)
},
seconds: {
type: [Number, Array] as PropType<MaybeArray<number>>,
validator: (value: MaybeArray<number>) => validateUnits(value, 59)
}
}
@ -666,6 +690,9 @@ export default defineComponent({
<Panel
ref="panelInstRef"
style={this.cssVars as CSSProperties}
seconds={this.seconds}
minutes={this.minutes}
hours={this.hours}
transitionDisabled={this.transitionDisabled}
hourValue={this.hourValue}
showHour={this.hourInFormat}

View File

@ -151,3 +151,7 @@ export const time = {
],
period: ['AM', 'PM']
}
export function getFixValue (value: number): string {
return `00${value}`.slice(-2)
}

View File

@ -0,0 +1,72 @@
# Batch Rendering
As you can see, prefix, label, and suffix all have render functions.
```html
<n-tree
block-line
:data="data"
:default-expanded-keys="defaultExpandedKeys"
:render-prefix="renderPrefix"
:render-label="renderLabel"
:render-suffix="renderSuffix"
:selectable="false"
/>
```
```js
import { h, defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
function createData (level = 4, baseKey = '') {
if (!level) return undefined
return Array.apply(null, { length: 6 - level }).map((_, index) => {
const key = '' + baseKey + level + index
return {
label: createLabel(level),
key,
children: createData(level - 1, key),
level
}
})
}
function createLabel (level) {
if (level === 4) return 'Out of Tao, One is born'
if (level === 3) return 'Out of One, Two'
if (level === 2) return 'Out of Two, Three'
if (level === 1) return 'Out of Three, the created universe'
}
function renderPrefix ({ option }) {
return h(
NButton,
{ text: true, type: 'primary' },
{ default: () => `Prefix-${option.level}` }
)
}
function renderLabel ({ option }) {
return `${option.label} :)`
}
function renderSuffix ({ option }) {
return h(
NButton,
{ text: true, type: 'primary' },
{ default: () => `Suffix-${option.level}` }
)
}
export default defineComponent({
setup () {
return {
data: createData(),
defaultExpandedKeys: ref(['40', '41']),
renderPrefix,
renderLabel,
renderSuffix
}
}
})
```

View File

@ -16,6 +16,7 @@ virtual
async
disabled
prefix-and-suffix
batch-render
```
## API
@ -44,6 +45,9 @@ prefix-and-suffix
| on-load | `(node: TreeOption) => Promise<void>` | `undefined` | |
| pattern | `string` | `''` | |
| remote | `boolean` | `false` | Whether to load nodes async. It should work with `on-load` |
| render-label | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | Render function of all the options' label. |
| render-prefix | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | Render function of all the options' prefix. |
| render-suffix | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | Render function of all the options' suffix. |
| selectable | `boolean` | `true` | |
| selected-keys | `Array<string \| number>` | `undefined` | If set, selected status will work in controlled manner. |
| virtual-scroll | `boolean` | `false` | Whether to enable virtual scroll. You need to set proper style height of the tree in advance. |

View File

@ -0,0 +1,72 @@
# 批量渲染
如你所想,前缀、标签、后缀都可以批量渲染
```html
<n-tree
block-line
:data="data"
:default-expanded-keys="defaultExpandedKeys"
:render-prefix="renderPrefix"
:render-label="renderLabel"
:render-suffix="renderSuffix"
:selectable="false"
/>
```
```js
import { h, defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
function createData (level = 4, baseKey = '') {
if (!level) return undefined
return Array.apply(null, { length: 6 - level }).map((_, index) => {
const key = '' + baseKey + level + index
return {
label: createLabel(level),
key,
children: createData(level - 1, key),
level
}
})
}
function createLabel (level) {
if (level === 4) return '道生一'
if (level === 3) return '一生二'
if (level === 2) return '二生三'
if (level === 1) return '三生万物'
}
function renderPrefix ({ option }) {
return h(
NButton,
{ text: true, type: 'primary' },
{ default: () => `Prefix-${option.level}` }
)
}
function renderLabel ({ option }) {
return `${option.label} ^_^`
}
function renderSuffix ({ option }) {
return h(
NButton,
{ text: true, type: 'primary' },
{ default: () => `Suffix-${option.level}` }
)
}
export default defineComponent({
setup () {
return {
data: createData(),
defaultExpandedKeys: ref(['40', '41']),
renderPrefix,
renderLabel,
renderSuffix
}
}
})
```

View File

@ -16,6 +16,7 @@ virtual
async
disabled
prefix-and-suffix
batch-render
```
## API
@ -45,6 +46,9 @@ prefix-and-suffix
| on-load | `(node: TreeOption) => Promise<void>` | `undefined` | |
| pattern | `string` | `''` | |
| remote | `boolean` | `false` | 是否异步获取选项,和 onLoad 配合 |
| render-label | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | 节点内容的渲染函数 |
| render-prefix | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | 节点前缀的渲染函数 |
| render-suffix | `(info: {option: TreeOption, checked: boolean, selected: boolean}) => VNodeChild` | `undefined` | 节点后缀的渲染函数 |
| selectable | `boolean` | `true` | |
| selected-keys | `Array<string \| number>` | `undefined` | 如果设定则 selected 状态受控 |
| virtual-scroll | `boolean` | `false` | 是否启用虚拟滚动,启用前你需要设定好树的高度样式 |

View File

@ -40,7 +40,10 @@ import {
AllowDrop,
MotionData,
treeInjectionKey,
InternalTreeInst
InternalTreeInst,
RenderLabel,
RenderPrefix,
RenderSuffix
} from './interface'
import MotionWrapper from './MotionWrapper'
import { defaultAllowDrop } from './dnd'
@ -132,6 +135,9 @@ const treeProps = {
default: true
},
virtualScroll: Boolean,
renderLabel: Function as PropType<RenderLabel>,
renderPrefix: Function as PropType<RenderPrefix>,
renderSuffix: Function as PropType<RenderSuffix>,
onDragenter: [Function, Array] as PropType<
MaybeArray<(e: TreeDragInfo) => void>
>,
@ -981,6 +987,9 @@ export default defineComponent({
pendingNodeKeyRef,
internalScrollableRef: toRef(props, 'internalScrollable'),
internalCheckboxFocusableRef: toRef(props, 'internalCheckboxFocusable'),
renderLabelRef: toRef(props, 'renderLabel'),
renderPrefixRef: toRef(props, 'renderPrefix'),
renderSuffixRef: toRef(props, 'renderSuffix'),
handleSwitcherClick,
handleDragEnd,
handleDragEnter,

View File

@ -216,6 +216,7 @@ const TreeNode = defineComponent({
checkable,
selectable,
selected,
checked,
highlight,
draggable,
blockLine,
@ -289,6 +290,8 @@ const TreeNode = defineComponent({
<NTreeNodeContent
ref="contentInstRef"
clsPrefix={clsPrefix}
checked={checked}
selected={selected}
onClick={
blockLine || disabled ? undefined : this.handleContentClick
}

View File

@ -1,6 +1,6 @@
import { h, defineComponent, ref, PropType } from 'vue'
import { h, defineComponent, ref, PropType, inject } from 'vue'
import { render } from '../../_utils'
import { TmNode } from './interface'
import { TmNode, treeInjectionKey } from './interface'
export default defineComponent({
name: 'TreeNodeContent',
@ -13,6 +13,8 @@ export default defineComponent({
type: Boolean,
default: false
},
checked: Boolean,
selected: Boolean,
onClick: Function as PropType<(e: MouseEvent) => void>,
onDragstart: Function as PropType<(e: DragEvent) => void>,
tmNode: {
@ -21,6 +23,9 @@ export default defineComponent({
}
},
setup (props) {
const { renderLabelRef, renderPrefixRef, renderSuffixRef } =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
inject(treeInjectionKey)!
const selfRef = ref<HTMLElement | null>(null)
function doClick (e: MouseEvent): void {
const { onClick } = props
@ -31,15 +36,24 @@ export default defineComponent({
}
return {
selfRef,
renderLabel: renderLabelRef,
renderPrefix: renderPrefixRef,
renderSuffix: renderSuffixRef,
handleClick
}
},
render () {
const {
clsPrefix,
checked = false,
selected = false,
renderLabel,
renderPrefix,
renderSuffix,
handleClick,
onDragstart,
tmNode: {
rawNode,
rawNode: { prefix, label, suffix }
}
} = this
@ -51,17 +65,35 @@ export default defineComponent({
draggable={onDragstart === undefined ? undefined : true}
onDragstart={onDragstart}
>
{prefix ? (
{renderPrefix || prefix ? (
<div class={`${clsPrefix}-tree-node-content__prefix`}>
{render(prefix)}
{renderPrefix
? renderPrefix({
option: rawNode,
selected,
checked
})
: render(prefix)}
</div>
) : null}
<div class={`${clsPrefix}-tree-node-content__text`}>
{render(label)}
{renderLabel
? renderLabel({
option: rawNode,
selected,
checked
})
: render(label)}
</div>
{suffix ? (
{renderSuffix || suffix ? (
<div class={`${clsPrefix}-tree-node-content__suffix`}>
{render(suffix)}
{renderSuffix
? renderSuffix({
option: rawNode,
selected,
checked
})
: render(suffix)}
</div>
) : null}
</span>

View File

@ -20,6 +20,24 @@ export type TreeOption = TreeOptionBase & { [k: string]: unknown }
export type TreeOptions = TreeOption[]
export interface TreeRenderProps {
option: TreeOption
checked: boolean
selected: boolean
}
type RenderTreePart = ({
option,
checked,
selected
}: TreeRenderProps) => VNodeChild
export type RenderLabel = RenderTreePart
export type RenderPrefix = RenderTreePart
export type RenderSuffix = RenderTreePart
export interface TreeDragInfo {
event: DragEvent
node: TreeOption
@ -78,6 +96,9 @@ export interface TreeInjection {
pendingNodeKeyRef: Ref<null | Key>
internalScrollableRef: Ref<boolean>
internalCheckboxFocusableRef: Ref<boolean>
renderLabelRef: Ref<RenderLabel | undefined>
renderPrefixRef: Ref<RenderPrefix | undefined>
renderSuffixRef: Ref<RenderSuffix | undefined>
handleSwitcherClick: (node: TreeNode<TreeOption>) => void
handleSelect: (node: TreeNode<TreeOption>) => void
handleCheck: (node: TreeNode<TreeOption>, checked: boolean) => void

View File

@ -63,4 +63,33 @@ describe('n-tree', () => {
expect(wrapper.find('.n-tree-node-content__suffix').exists()).toBe(true)
expect(wrapper.find('.n-tree-node-content__suffix').text()).toBe('suffix')
})
it('should work with `render-label`, `render-prefix` and `render-suffix`', async () => {
const wrapper = mount(NTree, {
props: {
data: [
{
label: 'test',
key: '123',
children: [
{
label: '123',
key: '123'
}
]
}
],
renderPrefix: () => 'prefix',
renderLabel: () => 'label',
renderSuffix: () => 'suffix'
}
})
expect(wrapper.find('.n-tree-node-content__prefix').exists()).toBe(true)
expect(wrapper.find('.n-tree-node-content__prefix').text()).toBe('prefix')
expect(wrapper.find('.n-tree-node-content__text').exists()).toBe(true)
expect(wrapper.find('.n-tree-node-content__text').text()).toBe('label')
expect(wrapper.find('.n-tree-node-content__suffix').exists()).toBe(true)
expect(wrapper.find('.n-tree-node-content__suffix').text()).toBe('suffix')
})
})

View File

@ -38,7 +38,7 @@ before-upload
| with-credentials | `boolean` | `false` | If cookie attached. |
| on-change | `(options: { file: UploadFile, fileList: Array<UploadFile>, event?: Event }) => void` | `() => {}` | The callback of status change of the component. Any file status change would fire the callback. |
| on-update:file-list | `(fileList: UploadFile[]) => void` | `undefined` | Callback function triggered on fileList changes. |
| on-finish | `(options: { file: UploadFile }) => UploadFile \| void` | `({ file }) => file` | The callback of file upload finish. You can modify the UploadFile or retun a new UploadFile. |
| on-finish | `(options: { file: UploadFile, event: Event }) => UploadFile \| void` | `({ file }) => file` | The callback of file upload finish. You can modify the UploadFile or retun a new UploadFile. |
| on-remove | `(options: { file: UploadFile, fileList: Array<UploadFile> }) => boolean \| Promise<boolean> \| any` | `() => true` | The callback of file removal. Return false, promise resolve false or promise reject will cancel this removal. |
| on-before-upload | `(options: { file: UploadFile, fileList: Array<UploadFile> }) => (Promise<boolean \| void> \| boolean \| void)` | `true` | Callback before file is uploaded, return false or a Promise that resolve false or reject will cancel this upload. |

View File

@ -12,11 +12,19 @@ You can change file's property when upload finishes.
```
```js
import { useMessage } from 'naive-ui'
export default {
methods: {
handleFinish ({ file }) {
setup() {
const message = useMessage()
const handleFinish = ({ file, event }) => {
message.success(event.target.response)
file.url = 'http://www.mocky.io/v2/5e4bafc63100007100d8b70f'
}
return {
message,
handleFinish
}
}
}
```

View File

@ -37,7 +37,7 @@ before-upload
| show-retry-button | `boolean` | `true` | 是否显示重新上传按钮(在 error 时展示) |
| with-credentials | `boolean` | `false` | 是否携带 Cookie |
| on-change | `(options: { file: UploadFile, fileList: Array<UploadFile>, event?: Event }) => void` | `() => {}` | 组件状态变化的回调,组件的任何文件状态变化都会触发回调 |
| on-finish | `(options: { file: UploadFile }) => UploadFile \| void` | `({ file }) => file` | 文件上传结束的回调,可以修改传入的 UploadFile 或者返回一个新的 UploadFile |
| on-finish | `(options: { file: UploadFile, event: Event }) => UploadFile \| void` | `({ file }) => file` | 文件上传结束的回调,可以修改传入的 UploadFile 或者返回一个新的 UploadFile |
| on-update:file-list | `(fileList: UploadFile[]) => void` | `undefined` | 当 file-list 改变时触发的回调函数 |
| on-before-upload | `(options: { file: UploadFile, fileList: UploadFile[] }) => (Promise<boolean \| void> \| boolean \| void)` | `undefined` | 文件上传之前的回调,返回 `false`、`Promise resolve false`、`Promise rejected` 时会取消本次上传 |

View File

@ -12,11 +12,19 @@
```
```js
import { useMessage } from 'naive-ui'
export default {
methods: {
handleFinish ({ file }) {
setup() {
const message = useMessage()
const handleFinish = ({ file, event }) => {
message.success(event.target.response)
file.url = 'http://www.mocky.io/v2/5e4bafc63100007100d8b70f'
}
return {
message,
handleFinish
}
}
}
```

View File

@ -58,7 +58,7 @@ function createXhrHandlers (
})
XhrMap.delete(file.id)
fileAfterChange =
inst.onFinish?.({ file: fileAfterChange }) || fileAfterChange
inst.onFinish?.({ file: fileAfterChange, event: e }) || fileAfterChange
doChange(fileAfterChange, e)
},
handleXHRAbort (e) {

View File

@ -21,7 +21,13 @@ export type OnChange = (data: {
fileList: FileInfo[]
event: ProgressEvent | Event | undefined
}) => void
export type OnFinish = ({ file }: { file: FileInfo }) => FileInfo | undefined
export type OnFinish = ({
file,
event
}: {
file: FileInfo
event: Event
}) => FileInfo | undefined
export type OnRemove = (data: {
file: FileInfo
fileList: FileInfo[]

View File

@ -1 +1 @@
export default '2.15.4'
export default '2.15.5'

View File

@ -7,3 +7,4 @@
| 0.1.8 | 2.7.3 |
| 0.1.9 | 2.10.0 |
| 0.1.10 | 2.15.2 |
| 0.1.11 | 2.15.4 |

View File

@ -416,7 +416,7 @@ export const themeOverridesDark: GlobalThemeOverrides = {
inputWidth: '80px',
selectWidth: '100px',
inputMargin: '0 20px',
itemMargin: '0 20px 0 0',
itemMargin: '0 0 0 20px',
itemBorder: '0 solid #0000',
itemBorderActive: '0 solid #0000',
itemBorderDisabled: '0 solid #0000',

View File

@ -316,7 +316,7 @@ export const themeOverridesLight: GlobalThemeOverrides = {
inputWidth: '80px',
selectWidth: '100px',
inputMargin: '0 20px',
itemMargin: '0 20px 0 0',
itemMargin: '0 0 0 20px',
itemBorder: '0 solid #0000',
itemBorderHover: '0 solid #0000',
itemBorderActive: '0 solid #0000',

View File

@ -2,7 +2,7 @@ import {
useDialog as _useDialog,
DialogOptions,
DialogReactive,
DialogApiInjection
DialogApi
} from 'naive-ui'
import { icons } from './icons'
@ -10,7 +10,7 @@ export interface ExtendedApi {
danger: (options: DialogOptions) => DialogReactive
}
export type TsDialogApi = DialogApiInjection & ExtendedApi
export type TsDialogApi = DialogApi & ExtendedApi
function useDialog (): TsDialogApi {
const dialog = _useDialog()