diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4fade97f6..503da81bc 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -12,6 +12,8 @@
- `n-input` add `input-props` prop.
- `n-message` optimize the error message of `useMessage` when there is no `n-message-provider`, add the related document link.
- Add `web-types.json` for webstorm, however I recommend using VSCode and Volar. `web-types.json` only provides limited information for coding.
+- `n-tree-select` add `leaf-only` prop.
+- `n-tree` add `leaf-only` prop.
### Fixes
diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md
index 44a99950f..295eb392a 100644
--- a/CHANGELOG.zh-CN.md
+++ b/CHANGELOG.zh-CN.md
@@ -12,6 +12,8 @@
- `n-input` 新增 `input-props` 属性
- `n-message` 优化 `useMessage` 当没有 `n-message-provider` 时的报错信息,增加关联的文档链接
- 为 webstorm 添加 `web-types.json`,但是我还是推荐使用 VSCode 和 Volar,`web-types.json` 只能为编码提供很有限的信息
+- `n-tree-select` 新增 `leaf-only` 属性
+- `n-tree` 新增 `leaf-only` 属性
### Fixes
diff --git a/package.json b/package.json
index 0dec9c32b..ff25d5360 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
"@vue/eslint-config-standard": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/server-renderer": "^3.0.11",
- "@vue/test-utils": "^2.0.0-rc.4",
+ "@vue/test-utils": "^2.0.0-rc.9",
"autoprefixer": "^10.2.6",
"babel-eslint": "^10.1.0",
"babel-jest": "^27.0.2",
diff --git a/src/collapse/tests/Collapse.spec.tsx b/src/collapse/tests/Collapse.spec.tsx
index c700da615..127c158c9 100644
--- a/src/collapse/tests/Collapse.spec.tsx
+++ b/src/collapse/tests/Collapse.spec.tsx
@@ -6,6 +6,7 @@ describe('n-collapse', () => {
it('should work with import on demand', () => {
mount(NCollapse)
})
+
it('can customize icon', () => {
const wrapper = mount(() => {
return (
@@ -19,4 +20,74 @@ describe('n-collapse', () => {
})
expect(wrapper.find('.my-icon').exists()).toEqual(true)
})
+
+ it('should work with `arrow-placement` prop', async () => {
+ const wrapper = mount(NCollapse, {
+ slots: {
+ default: () =>
+ }
+ })
+ expect(wrapper.find('.n-collapse-item').classes()).toContain(
+ 'n-collapse-item--left-arrow-placement'
+ )
+
+ await wrapper.setProps({ arrowPlacement: 'right' })
+ expect(wrapper.find('.n-collapse-item').classes()).toContain(
+ 'n-collapse-item--right-arrow-placement'
+ )
+ })
+
+ it('should work with nested structure', async () => {
+ mount(NCollapse, {
+ slots: {
+ default: () =>
+ h(
+ NCollapseItem,
+ { name: '1', title: 'test1' },
+ {
+ default: () =>
+ h(NCollapse, null, {
+ default: () => h(NCollapseItem, { name: '2', title: 'test2' })
+ })
+ }
+ )
+ }
+ })
+
+ // todo: test display-directive
+ // I wanted to test this function, but I was bothered by the
+ })
+
+ it('should work with `display-directive` prop', async () => {
+ mount(NCollapse, {
+ props: {
+ displayDirective: 'show'
+ },
+ slots: {
+ default: () =>
+ h(
+ NCollapseItem,
+ { name: '1', title: 'test' },
+ { default: () => h('div', null, { default: () => 'test' }) }
+ )
+ }
+ // todo: test display-directive
+ // I wanted to test this function, but I was bothered by the
+ })
+ })
+
+ it('should work with `on-item-header-click` prop', async () => {
+ const onClick = jest.fn()
+ const wrapper = mount(NCollapse, {
+ props: {
+ onItemHeaderClick: onClick
+ },
+ slots: {
+ default: () =>
+ }
+ })
+ const triggerNodeWrapper = wrapper.find('.n-collapse-item__header')
+ await triggerNodeWrapper.trigger('click')
+ expect(onClick).toHaveBeenCalled()
+ })
})
diff --git a/src/input/demos/zhCN/index.demo-entry.md b/src/input/demos/zhCN/index.demo-entry.md
index 91fd5876f..1a26716e4 100644
--- a/src/input/demos/zhCN/index.demo-entry.md
+++ b/src/input/demos/zhCN/index.demo-entry.md
@@ -25,41 +25,42 @@ count
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
-| autofocus | `boolean` | `false` | |
-| autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | |
-| clearable | `boolean` | `false` | |
-| default-value | `string \| [string, string] \| null` | `null` | |
-| disabled | `boolean` | `false` | |
+| autofocus | `boolean` | `false` | 自动获取焦点 |
+| autosize | `boolean \| { minRows?: number, maxRows?: number }` | `false` | 自适应内容高度,只对 type="textarea" 有效,可传入对象,如,{ minRows: 1, maxRows: 3 } |
+| clearable | `boolean` | `false` | 是否可清空 |
+| default-value | `string \| [string, string] \| null` | `null` | 输入框默认值 |
+| disabled | `boolean` | `false` | 是否禁用 |
| input-props | `object` | `undefined` | 组件中 input 元素的属性,对 `pair` 类型不生效 |
| show-password-toggle | `boolean` | `false` | 控制密码的显示隐藏 |
-| maxlength | `number` | `undefined` | |
-| minlength | `number` | `undefined` | |
+| maxlength | `number` | `undefined` | 最大输入长度 |
+| minlength | `number` | `undefined` | 最小输入长度 |
| pair | `boolean` | `false` | 是否输入成对的值 |
-| passively-activated | `boolean` | `false` | |
+| passively-activated | `boolean` | `false` | 是否被动激活输入框 |
| placeholder | `string \| [string, string]` | `undefined` | 文本输入的占位符。如果是 `pair` 是 `true`,`placeholder`是一个数组 |
-| readonly | `boolean` | `false` | |
-| round | `boolean` | `false` | |
-| rows | `number` | `3` | |
-| separator | `string` | `undefined` | 成对的值中间的分隔符 |
+| readonly | `boolean` | `false` | 是否只读 |
+| round | `boolean` | `false` | 输入框是否圆角 |
+| rows | `number` | `3` | 输入框行数,对 type="textarea" 有效 |
+| separator | `string` | `undefined` | 成对输入框中间的分隔符 |
| show-count | `boolean` | `false` | 是否显示字数统计 |
-| size | `'small' \| 'medium' \| 'large'` | `'medium'` | |
-| type | `'text' \| 'password' \| 'textarea'` | `'text'` | |
+| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 输入框尺寸 |
+| type | `'text' \| 'password' \| 'textarea'` | `'text'` | 输入框类型 |
| value | `string \| [string, string] \| null` | `undefined` | 文本输入的值。如果是 `pair` 是 `true`,`value` 是一个数组 |
-| on-blur | `() => void` | `undefined` | |
-| on-change | `(value: string \| [string, string]) => void` | `undefined` | |
-| on-clear | `() => void` | `undefined` | |
-| on-focus | `() => void` | `undefined` | |
-| on-update:value | `(value: string \| [string, string]) => void` | `undefined` | |
+| on-blur | `() => void` | `undefined` | 输入框失去焦点时触发 |
+| on-change | `(value: string \| [string, string]) => void` | `undefined` | 输入框失去焦点且值 change 时触发 |
+| on-clear | `() => void` | `undefined` | 输入框点击清空按钮时触发 |
+| on-focus | `() => void` | `undefined` | 输入框获得焦点时触发 |
+| on-input | `() => void` | `undefined` | 输入框值 change 时触发 |
+| on-update:value | `(value: string \| [string, string]) => void` | `undefined` | 输入框值 change 时触发 |
## Slots
### Input Slots
-| 属性 | 类型 | 说明 |
-| --------- | ---- | ---- |
-| prefix | `()` | |
-| suffix | `()` | |
-| separator | `()` | |
+| 属性 | 类型 | 说明 |
+| --- | --- | --- |
+| prefix | `()` | 输入框头部内容 |
+| suffix | `()` | 输入框尾部内容 |
+| separator | `()` | 成对输入框之间分隔符,仅 `pair` = true 生效且优先级高于 separator 属性 |
### Input Group Slots
diff --git a/src/tree-select/demos/zhCN/index.demo-entry.md b/src/tree-select/demos/zhCN/index.demo-entry.md
index 224499c77..aaea712e7 100644
--- a/src/tree-select/demos/zhCN/index.demo-entry.md
+++ b/src/tree-select/demos/zhCN/index.demo-entry.md
@@ -29,6 +29,7 @@ debug
| expanded-keys | `Array` | `undefined` | 展开节点的 key |
| filterable | `boolean` | `false` | 是否可过滤 |
| filter | `(pattern: string, option: TreeSelectOption) => boolean` | - | 过滤器函数 |
+| leaf-only | `boolean` | `false` | 是否开启仅末层树节点可选 |
| max-tag-count | `number \| 'responsive'` | `undefined` | 多选时最多直接显示多少选项,设为 `'responsive'` 会保证最多一行 |
| multiple | `boolean` | `false` | 是否支持多选 |
| options | `TreeSelectOption[]` | `[]` | 选项 |
diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx
index 31386c8fb..0c6fb92a2 100644
--- a/src/tree-select/src/TreeSelect.tsx
+++ b/src/tree-select/src/TreeSelect.tsx
@@ -73,6 +73,7 @@ const props = {
},
disabled: Boolean,
filterable: Boolean,
+ leafOnly: Boolean,
maxTagCount: [String, Number] as PropType,
multiple: Boolean,
options: {
@@ -661,6 +662,7 @@ export default defineComponent({
selectedKeys={this.treeSelectedKeys}
checkable={checkable}
cascade={this.mergedCascade}
+ leafOnly={this.leafOnly}
multiple={this.multiple}
virtualScroll={
this.consistentMenuWidth &&
diff --git a/src/tree/demos/zhCN/index.demo-entry.md b/src/tree/demos/zhCN/index.demo-entry.md
index 2fdc63d38..ef69c9908 100644
--- a/src/tree/demos/zhCN/index.demo-entry.md
+++ b/src/tree/demos/zhCN/index.demo-entry.md
@@ -39,6 +39,7 @@ disabled
| expand-on-dragenter | `boolean` | `true` | 是否在拖入后展开节点 |
| expanded-keys | `Array` | `undefined` | 如果设定则展开受控 |
| filter | `(node: TreeOption) => boolean` | 一个简单的字符串过滤算法 | |
+| leaf-only | `boolean` | `false` | 是否开启仅末层树节点可选 |
| multiple | `boolean` | `false` | |
| on-load | `(node: TreeOption) => Promise` | `undefined` | |
| pattern | `string` | `''` | |
diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx
index e2ac6a106..71bc00191 100644
--- a/src/tree/src/Tree.tsx
+++ b/src/tree/src/Tree.tsx
@@ -107,6 +107,7 @@ const treeProps = {
default: () => []
},
remote: Boolean,
+ leafOnly: Boolean,
multiple: Boolean,
pattern: {
type: String,
@@ -494,7 +495,7 @@ export default defineComponent({
nodeKeyToBeExpanded = null
}
function handleCheck (node: TmNode, checked: boolean): void {
- if (props.disabled || node.disabled) return
+ if (props.disabled || node.disabled || (props.leafOnly && !node.isLeaf)) { return }
const { checkedKeys } = dataTreeMateRef.value![
checked ? 'check' : 'uncheck'
](node.key, displayedCheckedKeysRef.value, {
@@ -521,7 +522,12 @@ export default defineComponent({
toggleExpand(node.key)
}
function handleSelect (node: TmNode): void {
- if (props.disabled || node.disabled || !props.selectable) return
+ if (
+ props.disabled ||
+ node.disabled ||
+ !props.selectable ||
+ (props.leafOnly && !node.isLeaf)
+ ) { return }
pendingNodeKeyRef.value = node.key
if (props.internalCheckOnSelect) {
const {
@@ -941,10 +947,12 @@ export default defineComponent({
mergedExpandedKeysRef,
mergedThemeRef: themeRef,
disabledRef: toRef(props, 'disabled'),
+ checkableRef: toRef(props, 'checkable'),
+ leafOnlyRef: toRef(props, 'leafOnly'),
+ selectableRef: toRef(props, 'selectable'),
remoteRef: toRef(props, 'remote'),
onLoadRef: toRef(props, 'onLoad'),
draggableRef: toRef(props, 'draggable'),
- checkableRef: toRef(props, 'checkable'),
blockLineRef: toRef(props, 'blockLine'),
indentRef: toRef(props, 'indent'),
droppingMouseNodeRef,
@@ -970,6 +978,7 @@ export default defineComponent({
handleKeydown,
handleKeyup
}
+
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: themeRef,
@@ -1024,7 +1033,6 @@ export default defineComponent({
blockNode,
blockLine,
draggable,
- selectable,
disabled,
internalFocusable,
handleKeyup,
@@ -1036,11 +1044,10 @@ export default defineComponent({
const treeClass = [
`${mergedClsPrefix}-tree`,
(blockLine || blockNode) && `${mergedClsPrefix}-tree--block-node`,
- blockLine && `${mergedClsPrefix}-tree--block-line`,
- selectable && `${mergedClsPrefix}-tree--selectable`
+ blockLine && `${mergedClsPrefix}-tree--block-line`
]
- const createNode = (tmNode: TmNode | MotionData): VNode =>
- '__motion' in tmNode ? (
+ const createNode = (tmNode: TmNode | MotionData): VNode => {
+ return '__motion' in tmNode ? (
)
+ }
+
if (this.virtualScroll) {
const { mergedTheme, internalScrollablePadding } = this
const padding = getPadding(internalScrollablePadding || '0')
diff --git a/src/tree/src/TreeNode.tsx b/src/tree/src/TreeNode.tsx
index e02151d55..df89f4da4 100644
--- a/src/tree/src/TreeNode.tsx
+++ b/src/tree/src/TreeNode.tsx
@@ -46,6 +46,7 @@ const TreeNode = defineComponent({
const contentInstRef = ref(null)
// must be non-reactive
const contentElRef: { value: HTMLElement | null } = { value: null }
+
onMounted(() => {
contentElRef.value = contentInstRef.value!.$el as HTMLElement
})
@@ -177,9 +178,18 @@ const TreeNode = defineComponent({
disabled: computed(
() => NTree.disabledRef.value || props.tmNode.disabled
),
+ checkable: computed(
+ () =>
+ NTree.checkableRef.value &&
+ (NTree.leafOnlyRef.value ? props.tmNode.isLeaf : true)
+ ),
checkboxDisabled: computed(() => !!props.tmNode.rawNode.checkboxDisabled),
+ selectable: computed(
+ () =>
+ NTree.selectableRef.value &&
+ (NTree.leafOnlyRef.value ? !!props.tmNode.isLeaf : true)
+ ),
internalScrollable: NTree.internalScrollableRef,
- checkable: NTree.checkableRef,
draggable: NTree.draggableRef,
blockLine: NTree.blockLineRef,
checkboxFocusable: NTree.internalCheckboxFocusableRef,
@@ -204,6 +214,7 @@ const TreeNode = defineComponent({
tmNode,
clsPrefix,
checkable,
+ selectable,
selected,
highlight,
draggable,
@@ -239,7 +250,8 @@ const TreeNode = defineComponent({
[`${clsPrefix}-tree-node--checkable`]: checkable,
[`${clsPrefix}-tree-node--highlight`]: highlight,
[`${clsPrefix}-tree-node--pending`]: pending,
- [`${clsPrefix}-tree-node--disabled`]: disabled
+ [`${clsPrefix}-tree-node--disabled`]: disabled,
+ [`${clsPrefix}-tree-node--selectable`]: selectable
}
]}
data-key={dataKey}
diff --git a/src/tree/src/interface.ts b/src/tree/src/interface.ts
index 3a8e89d23..7b29c240b 100644
--- a/src/tree/src/interface.ts
+++ b/src/tree/src/interface.ts
@@ -60,7 +60,6 @@ export interface TreeInjection {
fNodesRef: Ref>>
remoteRef: Ref
draggableRef: Ref
- checkableRef: Ref
mergedThemeRef: Ref>
onLoadRef: Ref<((node: TreeOption) => Promise) | undefined>
blockLineRef: Ref
@@ -71,6 +70,9 @@ export interface TreeInjection {
droppingPositionRef: Ref
droppingOffsetLevelRef: Ref
disabledRef: Ref
+ checkableRef: Ref
+ leafOnlyRef: Ref
+ selectableRef: Ref
pendingNodeKeyRef: Ref
internalScrollableRef: Ref
internalCheckboxFocusableRef: Ref
diff --git a/src/tree/src/styles/index.cssr.ts b/src/tree/src/styles/index.cssr.ts
index f650f9919..e46f6e1d6 100644
--- a/src/tree/src/styles/index.cssr.ts
+++ b/src/tree/src/styles/index.cssr.ts
@@ -74,18 +74,9 @@ export default cB('tree', `
color: var(--node-text-color-disabled);
cursor: not-allowed;
`)
- ])
- ]),
- cM('selectable', [
- cM('block-line', [
- cB('tree-node', [
- cNotM('disabled', `
- cursor: pointer;
- `)
- ])
]),
- cB('tree-node', [
- cNotM('disabled', [
+ cNotM('disabled', [
+ cM('selectable', [
cB('tree-node-content', `
cursor: pointer;
`)
@@ -188,6 +179,7 @@ export default cB('tree', `
align-items: center;
vertical-align: bottom;
padding: 0 6px;
+ cursor: default;
border-radius: var(--node-border-radius);
text-decoration-color: #0000;
text-decoration-line: underline;