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;