From 441fb5a16d59b692fee146997de7184f442634c3 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 11:27:54 +0800 Subject: [PATCH 01/29] site: tree-select route --- demo/routes/routes.js | 10 ++++++++++ demo/store/menu-options.js | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/demo/routes/routes.js b/demo/routes/routes.js index 8e5c015c822..3603f761340 100644 --- a/demo/routes/routes.js +++ b/demo/routes/routes.js @@ -445,6 +445,11 @@ export const enComponentRoutes = [ path: 'color-picker', component: () => import('../../src/color-picker/demos/enUS/index.demo-entry.md') + }, + { + path: 'tree-select', + component: () => + import('../../src/tree-select/demos/enUS/index.demo-entry.md') } ] @@ -775,6 +780,11 @@ export const zhComponentRoutes = [ path: 'color-picker', component: () => import('../../src/color-picker/demos/zhCN/index.demo-entry.md') + }, + { + path: 'tree-select', + component: () => + import('../../src/tree-select/demos/zhCN/index.demo-entry.md') } ] diff --git a/demo/store/menu-options.js b/demo/store/menu-options.js index 6c234ad686e..b10a8a1b12c 100644 --- a/demo/store/menu-options.js +++ b/demo/store/menu-options.js @@ -355,6 +355,12 @@ export function createComponentMenuOptions ({ lang, theme, mode }) { enSuffix: true, path: '/transfer' }, + { + en: 'Tree Select', + zh: '树型选择', + enSuffix: true, + path: '/tree-select' + }, { en: 'Upload', zh: '上传', From cfbbac798e5613cda1cab5a913a8d45c000ae22d Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 11:51:44 +0800 Subject: [PATCH 02/29] refactor(internal-selection): remove useless remote prop & on-delete-last-option --- src/_internal/selection/src/Selection.tsx | 11 ++++------- src/cascader/src/Cascader.tsx | 12 ------------ src/select/src/Select.tsx | 20 +------------------- 3 files changed, 5 insertions(+), 38 deletions(-) diff --git a/src/_internal/selection/src/Selection.tsx b/src/_internal/selection/src/Selection.tsx index 1d3f2044860..ab04d868b92 100644 --- a/src/_internal/selection/src/Selection.tsx +++ b/src/_internal/selection/src/Selection.tsx @@ -60,7 +60,6 @@ export default defineComponent({ }, multiple: Boolean, filterable: Boolean, - remote: Boolean, clearable: Boolean, disabled: Boolean, size: { @@ -80,7 +79,6 @@ export default defineComponent({ onBlur: Function as PropType<(e: FocusEvent) => void>, onFocus: Function as PropType<(e: FocusEvent) => void>, onDeleteOption: Function, - onDeleteLastOption: Function, maxTagCount: [String, Number] as PropType, onClear: Function as PropType<(e: MouseEvent) => void>, onPatternInput: Function as PropType<(e: InputEvent) => void> @@ -160,10 +158,6 @@ export default defineComponent({ const { onDeleteOption } = props if (onDeleteOption) onDeleteOption(value) } - function doDeleteLastOption (): void { - const { onDeleteLastOption } = props - if (onDeleteLastOption) onDeleteLastOption() - } function doClear (e: MouseEvent): void { const { onClear } = props if (onClear) onClear(e) @@ -204,7 +198,10 @@ export default defineComponent({ function handlePatternKeyDown (e: KeyboardEvent): void { if (e.code === 'Backspace') { if (!props.pattern.length) { - doDeleteLastOption() + const { selectedOptions } = props + if (selectedOptions?.length) { + handleDeleteOption(selectedOptions[selectedOptions.length - 1]) + } } } } diff --git a/src/cascader/src/Cascader.tsx b/src/cascader/src/Cascader.tsx index a1dc8d60d88..3bd84efe4f4 100644 --- a/src/cascader/src/Cascader.tsx +++ b/src/cascader/src/Cascader.tsx @@ -609,16 +609,6 @@ export default defineComponent({ } } } - function handleDeleteLastOption (): void { - if (props.multiple) { - const { value: mergedValue } = mergedValueRef - if (Array.isArray(mergedValue)) { - const newValue = Array.from(mergedValue) - newValue.pop() - doUpdateValue(newValue) - } - } - } function handlePatternInput (e: InputEvent): void { patternRef.value = (e.target as HTMLInputElement).value } @@ -716,7 +706,6 @@ export default defineComponent({ handleTriggerBlur, handleTriggerClick, handleClear, - handleDeleteLastOption, handleDeleteOption, handlePatternInput, handleKeyDown, @@ -799,7 +788,6 @@ export default defineComponent({ onBlur={this.handleTriggerBlur} onClick={this.handleTriggerClick} onClear={this.handleClear} - onDeleteLastOption={this.handleDeleteLastOption} onDeleteOption={this.handleDeleteOption} onPatternInput={this.handlePatternInput} onKeydown={this.handleKeyDown} diff --git a/src/select/src/Select.tsx b/src/select/src/Select.tsx index 136128826a9..e09b5d2f514 100644 --- a/src/select/src/Select.tsx +++ b/src/select/src/Select.tsx @@ -496,21 +496,6 @@ export default defineComponent({ doUpdateValue(option.value) } } - function handleDeleteLastOption (): void { - if (!patternRef.value.length) { - const changedValue = createClearedMultipleSelectValue( - mergedValueRef.value - ) - if (Array.isArray(changedValue)) { - const poppedValue = changedValue.pop() - if (poppedValue === undefined) return - const createdOptionIndex = getCreatedOptionIndex(poppedValue) - ~createdOptionIndex && - createdOptionsRef.value.splice(createdOptionIndex, 1) - doUpdateValue(changedValue) - } - } - } function getCreatedOptionIndex (optionValue: string | number): number { const createdOptions = createdOptionsRef.value return createdOptions.findIndex( @@ -660,7 +645,6 @@ export default defineComponent({ handleMenuBlur, handleMenuTabOut, handleTriggerClick, - handleDeleteLastOption, handleToggleOption, handlePatternInput, handleClear, @@ -709,7 +693,6 @@ export default defineComponent({ selectedOptions={this.selectedOptions} multiple={this.multiple} filterable={this.filterable} - remote={this.remote} clearable={this.clearable} disabled={this.disabled} size={this.mergedSize} @@ -720,7 +703,6 @@ export default defineComponent({ loading={this.loading} focused={this.focused} onClick={this.handleTriggerClick} - onDeleteLastOption={this.handleDeleteLastOption} onDeleteOption={this.handleToggleOption} onPatternInput={this.handlePatternInput} onClear={this.handleClear} @@ -740,7 +722,7 @@ export default defineComponent({ containerClass={this.namespace} width={this.consistentMenuWidth ? 'target' : undefined} minWidth="target" - placement="bottom-start" + placement={this.placement} > {{ default: () => ( From 074fda361a834c1ffdd9e54f814ba78e7c53101f Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 11:54:06 +0800 Subject: [PATCH 03/29] refactor(dropdown, popselect): set slots stable mark from true to 1 --- src/dropdown/src/Dropdown.tsx | 2 +- src/popselect/src/Popselect.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dropdown/src/Dropdown.tsx b/src/dropdown/src/Dropdown.tsx index 0480f6d7683..53d5fe3d48c 100644 --- a/src/dropdown/src/Dropdown.tsx +++ b/src/dropdown/src/Dropdown.tsx @@ -398,7 +398,7 @@ export default defineComponent({ {{ trigger: this.$slots.default, - _: true + _: 1 }} ) diff --git a/src/popselect/src/Popselect.tsx b/src/popselect/src/Popselect.tsx index 7b29f94fed4..aa6b10194ba 100644 --- a/src/popselect/src/Popselect.tsx +++ b/src/popselect/src/Popselect.tsx @@ -92,7 +92,7 @@ export default defineComponent({ {{ trigger: this.$slots.default, - _: true + _: 1 }} ) From 0818af470fbc84d30d104df89b090978af1404e0 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 11:55:34 +0800 Subject: [PATCH 04/29] fix(scrollbar): attributes applied multiple times --- src/scrollbar/src/ScrollBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scrollbar/src/ScrollBar.tsx b/src/scrollbar/src/ScrollBar.tsx index f49caf98ce1..3b96dafcef3 100644 --- a/src/scrollbar/src/ScrollBar.tsx +++ b/src/scrollbar/src/ScrollBar.tsx @@ -95,6 +95,7 @@ export type ScrollbarProps = ExtractPublicPropTypes export default defineComponent({ name: 'Scrollbar', props: scrollbarProps, + inheritAttrs: false, setup (props) { const { mergedClsPrefixRef } = useConfig(props) From 00bee747cf790de042feaca64feaa52e9a766ac2 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 11:58:14 +0800 Subject: [PATCH 05/29] wip(tree-select): add exports --- src/components.ts | 1 + src/config-provider/src/internal-interface.ts | 6 ++++-- src/styles.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components.ts b/src/components.ts index 4807189753a..13d9c86c86e 100644 --- a/src/components.ts +++ b/src/components.ts @@ -72,6 +72,7 @@ export * from './timeline' export * from './tooltip' export * from './transfer' export * from './tree' +export * from './tree-select' export * from './typography' export * from './upload' diff --git a/src/config-provider/src/internal-interface.ts b/src/config-provider/src/internal-interface.ts index b9a1eb8c642..441f8fcdc32 100644 --- a/src/config-provider/src/internal-interface.ts +++ b/src/config-provider/src/internal-interface.ts @@ -68,8 +68,9 @@ import type { TimePickerTheme } from '../../time-picker/styles' import type { TimelineTheme } from '../../timeline/styles' import type { TooltipTheme } from '../../tooltip/styles' import type { TransferTheme } from '../../transfer/styles' -import type { TypographyTheme } from '../../typography/styles' import type { TreeTheme } from '../../tree/styles' +import type { TreeSelectTheme } from '../../tree-select/styles' +import type { TypographyTheme } from '../../typography/styles' import type { UploadTheme } from '../../upload/styles' import type { InternalSelectMenuTheme } from '../../_internal/select-menu/styles' import type { InternalSelectionTheme } from '../../_internal/selection/styles' @@ -152,8 +153,9 @@ export interface GlobalThemeWithoutCommon { Timeline?: TimelineTheme Tooltip?: TooltipTheme Transfer?: TransferTheme - Typography?: TypographyTheme Tree?: TreeTheme + TreeSelect?: TreeSelectTheme + Typography?: TypographyTheme Upload?: UploadTheme // internal InternalSelectMenu?: InternalSelectMenuTheme diff --git a/src/styles.ts b/src/styles.ts index eaff3244ee2..d101c5f96b2 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -61,8 +61,9 @@ export { timePickerDark } from './time-picker/styles' export { timelineDark } from './timeline/styles' export { tooltipDark } from './tooltip/styles' export { transferDark } from './transfer/styles' -export { typographyDark } from './typography/styles' export { treeDark } from './tree/styles' +export { treeSelectDark } from './tree-select/styles' +export { typographyDark } from './typography/styles' export { uploadDark } from './upload/styles' // danger zone, internal styles From 0e7c21085ef2c0af2f345ce2d88a4e9060d69bb3 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 12:10:21 +0800 Subject: [PATCH 06/29] fix(tree): misses `on-update-expanded-keys`, `on-update-selected-keys`, `on-update-checked-keys` prop --- CHANGELOG.en-US.md | 4 +- CHANGELOG.zh-CN.md | 6 +- src/tree/src/Tree.tsx | 156 ++++++++++++++++++++++++++------------ src/tree/src/TreeNode.tsx | 2 +- src/tree/src/interface.ts | 2 +- src/tree/src/utils.ts | 14 +++- 6 files changed, 123 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index ee74595b6f7..9c994fa5e0e 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -3,14 +3,12 @@ ### Feats - `n-dropdown` add `on-clickoutside` prop, closes [#123](https://github.com/TuSimple/naive-ui/issues/123). - -## Pending - - `n-menu` add `renderLabel` prop, closes [#84](https://github.com/TuSimple/naive-ui/issues/84) ### Fixes - Fix `n-tree` drag over leaf node causes error, closes [#200](https://github.com/TuSimple/naive-ui/issues/200). +- Fix `n-tree` misses `on-update-expanded-keys`, `on-update-selected-keys`, `on-update-checked-keys` prop. ## 2.12.2 (2021-06-19) diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index ee997fc36be..73e8de02b2e 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -3,13 +3,11 @@ ### Feats - `n-dropdown` 新增 `on-clickoutside` 属性,关闭 [#123](https://github.com/TuSimple/naive-ui/issues/123) - -## Pending - -- `n-menu` 新增 `renderLabel` 属性,关闭 [#84](https://github.com/TuSimple/naive-ui/issues/84) +- `n-menu` 新增 `render-label` 属性,关闭 [#84](https://github.com/TuSimple/naive-ui/issues/84) ### Fixes +- 修复 `n-tree` 缺少 `on-update-expanded-keys`、`on-update-selected-keys`、`on-update-checked-keys` 属性 - 修复 `n-tree` 拖拽悬浮叶节点报错,关闭 [#200](https://github.com/TuSimple/naive-ui/issues/200) ## 2.12.2 (2021-06-19) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 9326822b6b3..c83767dece8 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -11,14 +11,15 @@ import { CSSProperties, VNode } from 'vue' -import { createTreeMate, flatten, createIndexGetter } from 'treemate' +import { createTreeMate, flatten, createIndexGetter, TreeMate } from 'treemate' import { useMergedState } from 'vooks' import { VirtualListInst, VVirtualList } from 'vueuc' import { useConfig, useTheme } from '../../_mixins' import type { ThemeProps } from '../../_mixins' import { call, warn } from '../../_utils' import type { ExtractPublicPropTypes, MaybeArray } from '../../_utils' -import { NScrollbar, ScrollbarInst } from '../../scrollbar' +import { NScrollbar } from '../../scrollbar' +import type { ScrollbarInst } from '../../scrollbar' import { treeLight } from '../styles' import type { TreeTheme } from '../styles' import NTreeNode from './TreeNode' @@ -40,6 +41,7 @@ import { } from './interface' import MotionWrapper from './MotionWrapper' import { defaultAllowDrop } from './dnd' +import { getPadding } from 'seemly' // TODO: // During expanding, some node are mis-applied with :active style @@ -47,13 +49,33 @@ import { defaultAllowDrop } from './dnd' const ITEM_SIZE = 30 +export const treeMateOptions = { + getDisabled (node: TreeOption) { + return !!(node.disabled || node.checkboxDisabled) + } +} + +export const treeSharedProps = { + defaultExpandAll: Boolean, + expandedKeys: Array as PropType, + defaultExpandedKeys: { + type: Array as PropType, + default: () => [] + }, + onUpdateExpandedKeys: [Function, Array] as PropType< + MaybeArray<(value: Key[]) => void> + >, + 'onUpdate:expandedKeys': [Function, Array] as PropType< + MaybeArray<(value: Key[]) => void> + > +} as const + const treeProps = { ...(useTheme.props as ThemeProps), data: { type: Array as PropType, default: () => [] }, - defaultExpandAll: Boolean, expandOnDragenter: { type: Boolean, default: true @@ -72,11 +94,6 @@ const treeProps = { type: Array as PropType, default: () => [] }, - expandedKeys: Array as PropType, - defaultExpandedKeys: { - type: Array as PropType, - default: () => [] - }, selectedKeys: Array as PropType, defaultSelectedKeys: { type: Array as PropType, @@ -116,18 +133,24 @@ const treeProps = { onDragstart: [Function, Array] as PropType void>>, onDragover: [Function, Array] as PropType void>>, onDrop: [Function, Array] as PropType void>>, - // eslint-disable-next-line vue/prop-name-casing - 'onUpdate:expandedKeys': [Function, Array] as PropType< + onUpdateCheckedKeys: [Function, Array] as PropType< MaybeArray<(value: Key[]) => void> >, - // eslint-disable-next-line vue/prop-name-casing 'onUpdate:checkedKeys': [Function, Array] as PropType< MaybeArray<(value: Key[]) => void> >, - // eslint-disable-next-line vue/prop-name-casing + onUpdateSelectedKeys: [Function, Array] as PropType< + MaybeArray<(value: Key[]) => void> + >, 'onUpdate:selectedKeys': [Function, Array] as PropType< MaybeArray<(value: Key[]) => void> >, + ...treeSharedProps, + // set for tree-select + internalScrollable: Boolean, + internalScrollablePadding: String, + internalTreeMate: Object as PropType>, + internalHighlightKeySet: Object as PropType>, // deprecated /** @deprecated */ onExpandedKeysChange: { @@ -197,13 +220,11 @@ export default defineComponent({ function getScrollContent (): HTMLElement | null | undefined { return virtualListInstRef.value?.itemsElRef } - const treeMateRef = computed(() => - createTreeMate(props.data, { - getDisabled (node) { - return !!(node.disabled || node.checkboxDisabled) - } - }) - ) + const treeMateRef = computed(() => { + return ( + props.internalTreeMate || createTreeMate(props.data, treeMateOptions) + ) + }) const uncontrolledCheckedKeysRef = ref( props.defaultCheckedKeys || props.checkedKeys ) @@ -248,7 +269,12 @@ export default defineComponent({ let expandTimerId: number | null = null let nodeKeyToBeExpanded: Key | null = null - const highlightKeysRef = ref([]) + const uncontrolledHighlightKeySetRef = ref>(new Set()) + const controlledHighlightKeySetRef = toRef(props, 'internalHighlightKeySet') + const mergedHighlightKeySetRef = useMergedState( + controlledHighlightKeySetRef, + uncontrolledHighlightKeySetRef + ) const loadingKeysRef = ref([]) let dragStartX: number = 0 @@ -280,15 +306,12 @@ export default defineComponent({ ) watch(toRef(props, 'pattern'), (value) => { if (value) { - const [expandedKeysAfterChange, highlightKeys] = keysWithFilter( - props.data, - props.pattern, - props.filter - ) - highlightKeysRef.value = highlightKeys - doExpandedKeysChange(expandedKeysAfterChange) + const { expandedKeys: expandedKeysAfterChange, highlightKeySet } = + keysWithFilter(props.data, props.pattern, props.filter) + uncontrolledHighlightKeySetRef.value = highlightKeySet + doUpdateExpandedKeys(expandedKeysAfterChange) } else { - highlightKeysRef.value = [] + uncontrolledHighlightKeySetRef.value = new Set() } }) @@ -389,31 +412,37 @@ export default defineComponent({ aipRef.value = false } - function doExpandedKeysChange (value: Key[]): void { + function doUpdateExpandedKeys (value: Key[]): void { const { - 'onUpdate:expandedKeys': onUpdateExpandedKeys, + 'onUpdate:expandedKeys': _onUpdateExpandedKeys, + onUpdateExpandedKeys, onExpandedKeysChange } = props uncontrolledExpandedKeysRef.value = value + if (_onUpdateExpandedKeys) call(_onUpdateExpandedKeys, value) if (onUpdateExpandedKeys) call(onUpdateExpandedKeys, value) if (onExpandedKeysChange) call(onExpandedKeysChange, value) } function doCheckedKeysChange (value: Key[]): void { const { - 'onUpdate:checkedKeys': onUpdateCheckedKeys, + 'onUpdate:checkedKeys': _onUpdateCheckedKeys, + onUpdateCheckedKeys, onCheckedKeysChange } = props uncontrolledCheckedKeysRef.value = value if (onUpdateCheckedKeys) call(onUpdateCheckedKeys, value) + if (_onUpdateCheckedKeys) call(_onUpdateCheckedKeys, value) if (onCheckedKeysChange) call(onCheckedKeysChange, value) } function doUpdateSelectedKeys (value: Key[]): void { const { - 'onUpdate:selectedKeys': onUpdateSelectedKeys, + 'onUpdate:selectedKeys': _onUpdateSelectedKeys, + onUpdateSelectedKeys, onSelectedKeysChange } = props uncontrolledSelectedKeysRef.value = value if (onUpdateSelectedKeys) call(onUpdateSelectedKeys, value) + if (_onUpdateSelectedKeys) call(_onUpdateSelectedKeys, value) if (onSelectedKeysChange) call(onSelectedKeysChange, value) } // Drag & Drop @@ -482,9 +511,9 @@ export default defineComponent({ if (~index) { const expandedKeysAfterChange = Array.from(mergedExpandedKeys) expandedKeysAfterChange.splice(index, 1) - doExpandedKeysChange(expandedKeysAfterChange) + doUpdateExpandedKeys(expandedKeysAfterChange) } else { - doExpandedKeysChange(mergedExpandedKeys.concat(node.key)) + doUpdateExpandedKeys(mergedExpandedKeys.concat(node.key)) } } function handleSwitcherClick (node: TmNode): void { @@ -532,7 +561,7 @@ export default defineComponent({ droppingMouseNode.key === node.key && !mergedExpandedKeysRef.value.includes(node.key) ) { - doExpandedKeysChange(mergedExpandedKeysRef.value.concat(node.key)) + doUpdateExpandedKeys(mergedExpandedKeysRef.value.concat(node.key)) } expandTimerId = null nodeKeyToBeExpanded = null @@ -863,7 +892,7 @@ export default defineComponent({ } provide(treeInjectionKey, { loadingKeysRef, - highlightKeysRef, + highlightKeySetRef: mergedHighlightKeySetRef, displayedCheckedKeysRef, displayedIndeterminateKeysRef, mergedSelectedKeysRef, @@ -964,7 +993,8 @@ export default defineComponent({ /> ) if (this.virtualScroll) { - const { mergedTheme } = this + const { mergedTheme, internalScrollablePadding } = this + const padding = getPadding(internalScrollablePadding || '0') return ( ) } - return ( -
- {this.fNodes.map(createNode)} -
- ) + const { internalScrollable } = this + if (internalScrollable) { + return ( + + {{ + default: () => ( +
+ {this.fNodes.map(createNode)} +
+ ) + }} +
+ ) + } else { + return ( +
+ {this.fNodes.map(createNode)} +
+ ) + } } }) diff --git a/src/tree/src/TreeNode.tsx b/src/tree/src/TreeNode.tsx index a05f5bfabcd..5cc6aef71c9 100644 --- a/src/tree/src/TreeNode.tsx +++ b/src/tree/src/TreeNode.tsx @@ -155,7 +155,7 @@ const TreeNode = defineComponent({ NTree.loadingKeysRef.value.includes(props.tmNode.key) ), highlight: useMemo(() => - NTree.highlightKeysRef.value.includes(props.tmNode.key) + NTree.highlightKeySetRef.value.has(props.tmNode.key) ), checked: useMemo(() => NTree.displayedCheckedKeysRef.value.includes(props.tmNode.key) diff --git a/src/tree/src/interface.ts b/src/tree/src/interface.ts index efe7e722387..76ec7649e46 100644 --- a/src/tree/src/interface.ts +++ b/src/tree/src/interface.ts @@ -50,7 +50,7 @@ export interface InternalDropInfo { export interface TreeInjection { loadingKeysRef: Ref - highlightKeysRef: Ref + highlightKeySetRef: Ref> displayedCheckedKeysRef: Ref displayedIndeterminateKeysRef: Ref mergedSelectedKeysRef: Ref diff --git a/src/tree/src/utils.ts b/src/tree/src/utils.ts index c768f6b0a2f..934d87237fb 100644 --- a/src/tree/src/utils.ts +++ b/src/tree/src/utils.ts @@ -16,16 +16,19 @@ export function keysWithFilter ( nodes: TreeOption[], pattern: string, filter: (pattern: string, node: TreeOption) => boolean -): [Key[], Key[]] { +): { + expandedKeys: Key[] + highlightKeySet: Set + } { const keys = new Set() - const highlightKeys = new Set() + const highlightKeySet = new Set() const path: TreeOption[] = [] traverse( nodes, (node) => { path.push(node) if (filter(pattern, node)) { - highlightKeys.add(node.key) + highlightKeySet.add(node.key) for (let i = path.length - 2; i >= 0; --i) { if (!keys.has(path[i].key)) { keys.add(path[i].key) @@ -39,7 +42,10 @@ export function keysWithFilter ( path.pop() } ) - return [Array.from(keys), Array.from(highlightKeys)] + return { + expandedKeys: Array.from(keys), + highlightKeySet + } } const emptyImage: HTMLImageElement | null = null From 82da33f77b4331a86792367fdef3fdbc002382d7 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 14:39:08 +0800 Subject: [PATCH 07/29] feat(tree): support keyboard operations --- CHANGELOG.en-US.md | 4 + CHANGELOG.zh-CN.md | 4 + src/data-table/src/TableParts/Body.tsx | 2 +- src/scrollbar/index.ts | 5 +- src/scrollbar/src/ScrollBar.tsx | 27 +++++-- src/tree/src/Tree.tsx | 74 ++++++++++++++--- src/tree/src/TreeNode.tsx | 5 ++ src/tree/src/interface.ts | 1 + src/tree/src/keyboard.tsx | 108 +++++++++++++++++++++++++ src/tree/src/styles/index.cssr.ts | 15 +++- 10 files changed, 219 insertions(+), 26 deletions(-) create mode 100644 src/tree/src/keyboard.tsx diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 9c994fa5e0e..95b0c215bbd 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -5,6 +5,10 @@ - `n-dropdown` add `on-clickoutside` prop, closes [#123](https://github.com/TuSimple/naive-ui/issues/123). - `n-menu` add `renderLabel` prop, closes [#84](https://github.com/TuSimple/naive-ui/issues/84) +### Feats + +- `n-tree` supports keyboard operations. + ### Fixes - Fix `n-tree` drag over leaf node causes error, closes [#200](https://github.com/TuSimple/naive-ui/issues/200). diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 73e8de02b2e..31197512cbe 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,10 @@ - `n-dropdown` 新增 `on-clickoutside` 属性,关闭 [#123](https://github.com/TuSimple/naive-ui/issues/123) - `n-menu` 新增 `render-label` 属性,关闭 [#84](https://github.com/TuSimple/naive-ui/issues/84) +### Feats + +- `n-tree` 支持键盘操作 + ### Fixes - 修复 `n-tree` 缺少 `on-update-expanded-keys`、`on-update-selected-keys`、`on-update-checked-keys` 属性 diff --git a/src/data-table/src/TableParts/Body.tsx b/src/data-table/src/TableParts/Body.tsx index c4b45fe71dc..4f71f60ff3c 100644 --- a/src/data-table/src/TableParts/Body.tsx +++ b/src/data-table/src/TableParts/Body.tsx @@ -294,7 +294,7 @@ export default defineComponent({ verticalRailStyle={{ zIndex: 3 }} xScrollable onScroll={virtualScroll ? undefined : this.handleTableBodyScroll} - privateOnSetSL={setHeaderScrollLeft} + internalOnUpdateScrollLeft={setHeaderScrollLeft} onResize={onResize} > {{ diff --git a/src/scrollbar/index.ts b/src/scrollbar/index.ts index 2c226d7be0e..84542116437 100644 --- a/src/scrollbar/index.ts +++ b/src/scrollbar/index.ts @@ -1,2 +1,5 @@ -export { default as NScrollbar } from './src/ScrollBar' +export { + default as NScrollbar, + XScrollbar as NxScrollbar +} from './src/ScrollBar' export type { ScrollbarInst, ScrollbarProps } from './src/ScrollBar' diff --git a/src/scrollbar/src/ScrollBar.tsx b/src/scrollbar/src/ScrollBar.tsx index 3b96dafcef3..ce231ac7f8e 100644 --- a/src/scrollbar/src/ScrollBar.tsx +++ b/src/scrollbar/src/ScrollBar.tsx @@ -11,14 +11,18 @@ import { Transition, CSSProperties, watchEffect, - VNode + VNode, + HTMLAttributes } from 'vue' import { on, off } from 'evtd' import { VResizeObserver } from 'vueuc' import { useIsIos } from 'vooks' import { useConfig, useTheme } from '../../_mixins' import type { ThemeProps } from '../../_mixins' -import type { ExtractPublicPropTypes } from '../../_utils' +import type { + ExtractInternalPropTypes, + ExtractPublicPropTypes +} from '../../_utils' import { scrollbarLight } from '../styles' import type { ScrollbarTheme } from '../styles' import style from './styles/index.cssr' @@ -86,13 +90,15 @@ const scrollbarProps = { onScroll: Function as PropType<(e: Event) => void>, onWheel: Function as PropType<(e: WheelEvent) => void>, onResize: Function as PropType<(e: ResizeObserverEntry) => void>, - onDragleave: Function as PropType<(e: DragEvent) => void>, - privateOnSetSL: Function as PropType<(scrollLeft: number) => void> + internalOnUpdateScrollLeft: Function as PropType<(scrollLeft: number) => void> } as const export type ScrollbarProps = ExtractPublicPropTypes +export type ScrollbarInternalProps = ExtractInternalPropTypes< + typeof scrollbarProps +> -export default defineComponent({ +const Scrollbar = defineComponent({ name: 'Scrollbar', props: scrollbarProps, inheritAttrs: false, @@ -428,8 +434,8 @@ export default defineComponent({ const { value: container } = mergedContainerRef if (container) { container.scrollLeft = toScrollLeft - const { privateOnSetSL } = props - if (privateOnSetSL) privateOnSetSL(toScrollLeft) + const { internalOnUpdateScrollLeft } = props + if (internalOnUpdateScrollLeft) internalOnUpdateScrollLeft(toScrollLeft) } } function handleXScrollMouseUp (e: MouseEvent): void { @@ -592,7 +598,6 @@ export default defineComponent({ mergeProps(this.$attrs, { class: `${mergedClsPrefix}-scrollbar`, style: this.cssVars, - onDragleave: this.onDragleave, onMouseenter: this.handleMouseEnterWrapper, onMouseleave: this.handleMouseLeaveWrapper }), @@ -691,3 +696,9 @@ export default defineComponent({ ) } }) + +type NativeScrollbarProps = Omit +type MergedProps = Partial + +export default Scrollbar +export const XScrollbar: new () => { $props: MergedProps } = Scrollbar as any diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index c83767dece8..b51607d30e9 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -18,12 +18,13 @@ import { useConfig, useTheme } from '../../_mixins' import type { ThemeProps } from '../../_mixins' import { call, warn } from '../../_utils' import type { ExtractPublicPropTypes, MaybeArray } from '../../_utils' -import { NScrollbar } from '../../scrollbar' +import { NxScrollbar } from '../../scrollbar' import type { ScrollbarInst } from '../../scrollbar' import { treeLight } from '../styles' import type { TreeTheme } from '../styles' import NTreeNode from './TreeNode' import { keysWithFilter, emptyImage } from './utils' +import { useKeyboard } from './keyboard' import style from './styles/index.cssr' import { DragInfo, @@ -255,7 +256,7 @@ export default defineComponent({ const uncontrolledExpandedKeysRef = ref( props.defaultExpandAll ? treeMateRef.value.getNonLeafKeys() - : props.defaultExpandedKeys || props.expandedKeys + : props.defaultExpandedKeys ) const controlledExpandedKeysRef = toRef(props, 'expandedKeys') const mergedExpandedKeysRef = useMergedState( @@ -267,6 +268,13 @@ export default defineComponent({ treeMateRef.value.getFlattenedNodes(mergedExpandedKeysRef.value) ) + const { pendingNodeKeyRef, handleKeyup, handleKeydown } = useKeyboard({ + fNodesRef, + mergedExpandedKeysRef, + handleSelect, + handleSwitcherClick + }) + let expandTimerId: number | null = null let nodeKeyToBeExpanded: Key | null = null const uncontrolledHighlightKeySetRef = ref>(new Set()) @@ -502,26 +510,27 @@ export default defineComponent({ ) doCheckedKeysChange(checkedKeys) } - function toggleExpand (node: TmNode): void { + function toggleExpand (key: Key): void { if (props.disabled) return const { value: mergedExpandedKeys } = mergedExpandedKeysRef const index = mergedExpandedKeys.findIndex( - (expandNodeId) => expandNodeId === node.key + (expandNodeId) => expandNodeId === key ) if (~index) { const expandedKeysAfterChange = Array.from(mergedExpandedKeys) expandedKeysAfterChange.splice(index, 1) doUpdateExpandedKeys(expandedKeysAfterChange) } else { - doUpdateExpandedKeys(mergedExpandedKeys.concat(node.key)) + doUpdateExpandedKeys(mergedExpandedKeys.concat(key)) } } function handleSwitcherClick (node: TmNode): void { if (props.disabled || aipRef.value) return - toggleExpand(node) + toggleExpand(node.key) } function handleSelect (node: TmNode): void { if (props.disabled || node.disabled || !props.selectable) return + pendingNodeKeyRef.value = node.key if (props.multiple) { const selectedKeys = mergedSelectedKeysRef.value const index = selectedKeys.findIndex((key) => key === node.key) @@ -890,6 +899,19 @@ export default defineComponent({ function handleResize (): void { scrollbarInstRef.value?.sync() } + function handleFocusout (e: FocusEvent): void { + if (props.virtualScroll || props.internalScrollable) { + const { value: scrollbarInst } = scrollbarInstRef + if (scrollbarInst?.containerRef?.contains(e.relatedTarget as Element)) { + return + } + pendingNodeKeyRef.value = null + } else { + const { value: selfEl } = selfElRef + if (selfEl?.contains(e.relatedTarget as Element)) return + pendingNodeKeyRef.value = null + } + } provide(treeInjectionKey, { loadingKeysRef, highlightKeySetRef: mergedHighlightKeySetRef, @@ -911,6 +933,7 @@ export default defineComponent({ droppingPositionRef, droppingOffsetLevelRef, fNodesRef, + pendingNodeKeyRef, handleSwitcherClick, handleDragEnd, handleDragEnter, @@ -929,6 +952,9 @@ export default defineComponent({ selfElRef, virtualListInstRef, scrollbarInstRef, + handleKeydown, + handleKeyup, + handleFocusout, handleDragLeaveTree, handleScroll, getScrollContainer, @@ -968,8 +994,18 @@ export default defineComponent({ } }, render () { - const { mergedClsPrefix, blockNode, blockLine, draggable, selectable } = - this + const { + mergedClsPrefix, + blockNode, + blockLine, + draggable, + selectable, + disabled, + handleKeyup, + handleKeydown, + handleFocusout + } = this + const tabindex = disabled ? undefined : '0' const treeClass = [ `${mergedClsPrefix}-tree`, (blockLine || blockNode) && `${mergedClsPrefix}-tree--block-node`, @@ -996,7 +1032,7 @@ export default defineComponent({ const { mergedTheme, internalScrollablePadding } = this const padding = getPadding(internalScrollablePadding || '0') return ( - {{ default: () => ( @@ -1033,14 +1073,18 @@ export default defineComponent({ ) }} - + ) } const { internalScrollable } = this if (internalScrollable) { return ( - @@ -1054,15 +1098,19 @@ export default defineComponent({ ) }} - + ) } else { return (
{this.fNodes.map(createNode)}
diff --git a/src/tree/src/TreeNode.tsx b/src/tree/src/TreeNode.tsx index 5cc6aef71c9..db5de48e04e 100644 --- a/src/tree/src/TreeNode.tsx +++ b/src/tree/src/TreeNode.tsx @@ -151,6 +151,9 @@ const TreeNode = defineComponent({ } return false }), + pending: useMemo( + () => NTree.pendingNodeKeyRef.value === props.tmNode.key + ), loading: useMemo(() => NTree.loadingKeysRef.value.includes(props.tmNode.key) ), @@ -204,6 +207,7 @@ const TreeNode = defineComponent({ blockLine, indent, disabled, + pending, suffix } = this // drag start not inside @@ -227,6 +231,7 @@ const TreeNode = defineComponent({ [`${clsPrefix}-tree-node--selected`]: selected, [`${clsPrefix}-tree-node--checkable`]: checkable, [`${clsPrefix}-tree-node--highlight`]: highlight, + [`${clsPrefix}-tree-node--pending`]: pending, [`${clsPrefix}-tree-node--disabled`]: disabled } ]} diff --git a/src/tree/src/interface.ts b/src/tree/src/interface.ts index 76ec7649e46..b66d9921420 100644 --- a/src/tree/src/interface.ts +++ b/src/tree/src/interface.ts @@ -69,6 +69,7 @@ export interface TreeInjection { droppingPositionRef: Ref droppingOffsetLevelRef: Ref disabledRef: Ref + pendingNodeKeyRef: Ref handleSwitcherClick: (node: TreeNode) => void handleSelect: (node: TreeNode) => void handleCheck: (node: TreeNode, checked: boolean) => void diff --git a/src/tree/src/keyboard.tsx b/src/tree/src/keyboard.tsx new file mode 100644 index 00000000000..48e1eed56f7 --- /dev/null +++ b/src/tree/src/keyboard.tsx @@ -0,0 +1,108 @@ +import { ref, Ref } from 'vue' +import { TreeNode } from 'treemate' +import { Key, TmNode } from '../../tree/src/interface' +import { TreeOption } from './interface' + +export function useKeyboard ({ + fNodesRef, + mergedExpandedKeysRef, + handleSelect, + handleSwitcherClick +}: { + fNodesRef: Ref>> + mergedExpandedKeysRef: Ref + handleSelect: (node: TmNode) => void + handleSwitcherClick: (node: TmNode) => void +}): { + pendingNodeKeyRef: Ref + handleKeyup: (e: KeyboardEvent) => void + handleKeydown: (e: KeyboardEvent) => void + } { + const pendingNodeKeyRef = ref(null) + function handleKeyup (e: KeyboardEvent): void { + const { value: pendingNodeKey } = pendingNodeKeyRef + if (pendingNodeKey === null) { + if ( + ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.code) + ) { + if (pendingNodeKey === null) { + const { value: fNodes } = fNodesRef + let fIndex = 0 + while (fIndex < fNodes.length) { + if (!fNodes[fIndex].disabled) { + pendingNodeKeyRef.value = fNodes[fIndex].key + break + } + fIndex += 1 + } + } + } + } else { + const { value: fNodes } = fNodesRef + let fIndex = fNodes.findIndex((tmNode) => tmNode.key === pendingNodeKey) + if (!~fIndex) return + if (e.code === 'Enter') { + handleSelect(fNodes[fIndex]) + } else if (e.code === 'ArrowDown') { + fIndex += 1 + while (fIndex < fNodes.length) { + if (!fNodes[fIndex].disabled) { + pendingNodeKeyRef.value = fNodes[fIndex].key + break + } + fIndex += 1 + } + } else if (e.code === 'ArrowUp') { + fIndex -= 1 + while (fIndex >= 0) { + if (!fNodes[fIndex].disabled) { + pendingNodeKeyRef.value = fNodes[fIndex].key + break + } + fIndex -= 1 + } + } else if (e.code === 'ArrowLeft') { + const pendingNode = fNodes[fIndex] + if ( + pendingNode.isLeaf || + !mergedExpandedKeysRef.value.includes(pendingNodeKey) + ) { + const parentTmNode = pendingNode.getParent() + if (parentTmNode) { + pendingNodeKeyRef.value = parentTmNode.key + } + } else { + handleSwitcherClick(pendingNode) + } + } else if (e.code === 'ArrowRight') { + const pendingNode = fNodes[fIndex] + if (pendingNode.isLeaf) return + if (!mergedExpandedKeysRef.value.includes(pendingNodeKey)) { + handleSwitcherClick(pendingNode) + } else { + // Tha same as ArrowDown + fIndex += 1 + while (fIndex < fNodes.length) { + if (!fNodes[fIndex].disabled) { + pendingNodeKeyRef.value = fNodes[fIndex].key + break + } + fIndex += 1 + } + } + } + } + } + function handleKeydown (e: KeyboardEvent): void { + switch (e.code) { + case 'ArrowUp': + case 'ArrowDown': + e.preventDefault() + } + } + return { + pendingNodeKeyRef, + handleKeyup, + handleKeydown + } +} diff --git a/src/tree/src/styles/index.cssr.ts b/src/tree/src/styles/index.cssr.ts index 9733077cc3a..fdfade7e964 100644 --- a/src/tree/src/styles/index.cssr.ts +++ b/src/tree/src/styles/index.cssr.ts @@ -21,9 +21,10 @@ const nodeStateStyle = [ // --node-color-pressed // --node-text-color // --node-text-color-disabled -export default cB('tree', { - fontSize: 'var(--font-size)' -}, [ +export default cB('tree', ` + font-size: var(--font-size); + outline: none; +`, [ c('ul, li', ` margin: 0; padding: 0; @@ -100,6 +101,11 @@ export default cB('tree', { cB('tree-node', [ cNotM('disabled', [ cB('tree-node-content', nodeStateStyle), + cM('pending', [ + cB('tree-node-content', ` + background-color: var(--node-color-hover); + `) + ]), cM('selected', [ cB('tree-node-content', { backgroundColor: 'var(--node-color-active)' @@ -112,6 +118,9 @@ export default cB('tree', { cB('tree-node', [ cNotM('disabled', [ nodeStateStyle, + cM('pending', ` + background-color: var(--node-color-hover); + `), cM('selected', { backgroundColor: 'var(--node-color-active)' }) From 99b8a321537ab6d3227cdbd73d5b467528e742df Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 15:25:00 +0800 Subject: [PATCH 08/29] feat(tree): scroll with keyboard target node --- src/_utils/index.ts | 3 +- src/_utils/vue/create-data-key.ts | 3 ++ src/_utils/vue/index.ts | 1 + src/tree/src/Tree.tsx | 62 +++++++++++++++++++++++-------- src/tree/src/TreeNode.tsx | 9 ++++- src/tree/src/interface.ts | 6 +++ 6 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 src/_utils/vue/create-data-key.ts diff --git a/src/_utils/index.ts b/src/_utils/index.ts index 9914a77e47e..f3bdffb8138 100644 --- a/src/_utils/index.ts +++ b/src/_utils/index.ts @@ -7,7 +7,8 @@ export { getVNodeChildren, keysOf, render, - getFirstSlotVNode + getFirstSlotVNode, + createDataKey } from './vue' export type { MaybeArray } from './vue' export { warn, warnOnce, throwError, smallerSize, largerSize } from './naive' diff --git a/src/_utils/vue/create-data-key.ts b/src/_utils/vue/create-data-key.ts new file mode 100644 index 00000000000..4cfaf2d1ded --- /dev/null +++ b/src/_utils/vue/create-data-key.ts @@ -0,0 +1,3 @@ +export function createDataKey (key: string | number): string { + return typeof key === 'string' ? `s-${key}` : `n-${key}` +} diff --git a/src/_utils/vue/index.ts b/src/_utils/vue/index.ts index d5679ba2f24..547d5452517 100644 --- a/src/_utils/vue/index.ts +++ b/src/_utils/vue/index.ts @@ -7,4 +7,5 @@ export { call } from './call' export { keysOf } from './keysOf' export { render } from './render' export { getFirstSlotVNode } from './get-first-slot-vnode' +export { createDataKey } from './create-data-key' export type { MaybeArray } from './call' diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index b51607d30e9..85c48f6459f 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -16,7 +16,7 @@ import { useMergedState } from 'vooks' import { VirtualListInst, VVirtualList } from 'vueuc' import { useConfig, useTheme } from '../../_mixins' import type { ThemeProps } from '../../_mixins' -import { call, warn } from '../../_utils' +import { call, createDataKey, warn } from '../../_utils' import type { ExtractPublicPropTypes, MaybeArray } from '../../_utils' import { NxScrollbar } from '../../scrollbar' import type { ScrollbarInst } from '../../scrollbar' @@ -38,7 +38,8 @@ import { DropPosition, AllowDrop, MotionData, - treeInjectionKey + treeInjectionKey, + InternalTreeInst } from './interface' import MotionWrapper from './MotionWrapper' import { defaultAllowDrop } from './dnd' @@ -152,6 +153,12 @@ const treeProps = { internalScrollablePadding: String, internalTreeMate: Object as PropType>, internalHighlightKeySet: Object as PropType>, + internalCheckOnSelect: Boolean, + internalFocusable: { + // Make tree-select take over keyboard operations + type: Boolean, + default: true + }, // deprecated /** @deprecated */ onExpandedKeysChange: { @@ -531,6 +538,9 @@ export default defineComponent({ function handleSelect (node: TmNode): void { if (props.disabled || node.disabled || !props.selectable) return pendingNodeKeyRef.value = node.key + if (props.internalCheckOnSelect) { + handleCheck(node, mergedCheckedKeysRef.value.includes(node.key)) + } if (props.multiple) { const selectedKeys = mergedSelectedKeysRef.value const index = selectedKeys.findIndex((key) => key === node.key) @@ -912,6 +922,22 @@ export default defineComponent({ pendingNodeKeyRef.value = null } } + watch(pendingNodeKeyRef, (value) => { + if (value === null) return + if (props.virtualScroll) { + virtualListInstRef.value?.scrollTo({ key: value }) + } else if (props.internalScrollable) { + const { value: scrollbarInst } = scrollbarInstRef + if (scrollbarInst === null) return + const targetEl = scrollbarInst.contentRef?.querySelector( + `[data-key="${createDataKey(value)}"]` + ) + if (!targetEl) return + scrollbarInst.scrollTo({ + el: targetEl as any + }) + } + }) provide(treeInjectionKey, { loadingKeysRef, highlightKeySetRef: mergedHighlightKeySetRef, @@ -927,6 +953,7 @@ export default defineComponent({ checkableRef: toRef(props, 'checkable'), blockLineRef: toRef(props, 'blockLine'), indentRef: toRef(props, 'indent'), + internalScrollableRef: toRef(props, 'internalScrollable'), droppingMouseNodeRef, droppingNodeParentRef, draggingNodeRef, @@ -944,6 +971,10 @@ export default defineComponent({ handleSelect, handleCheck }) + const exposedMethods: InternalTreeInst = { + handleKeydown, + handleKeyup + } return { mergedClsPrefix: mergedClsPrefixRef, mergedTheme: themeRef, @@ -952,8 +983,6 @@ export default defineComponent({ selfElRef, virtualListInstRef, scrollbarInstRef, - handleKeydown, - handleKeyup, handleFocusout, handleDragLeaveTree, handleScroll, @@ -990,7 +1019,8 @@ export default defineComponent({ '--node-text-color-disabled': nodeTextColorDisabled, '--drop-mark-color': dropMarkColor } - }) + }), + ...exposedMethods } }, render () { @@ -1001,11 +1031,13 @@ export default defineComponent({ draggable, selectable, disabled, + internalFocusable, handleKeyup, handleKeydown, handleFocusout } = this - const tabindex = disabled ? undefined : '0' + const mergedFocusable = internalFocusable && !disabled + const tabindex = mergedFocusable ? '0' : undefined const treeClass = [ `${mergedClsPrefix}-tree`, (blockLine || blockNode) && `${mergedClsPrefix}-tree--block-node`, @@ -1041,9 +1073,9 @@ export default defineComponent({ theme={mergedTheme.peers.Scrollbar} themeOverrides={mergedTheme.peerOverrides.Scrollbar} tabindex={tabindex} - onKeyup={handleKeyup} - onKeydown={handleKeydown} - onFocusout={handleFocusout} + onKeyup={mergedFocusable ? handleKeyup : undefined} + onKeydown={mergedFocusable ? handleKeydown : undefined} + onFocusout={mergedFocusable ? handleFocusout : undefined} > {{ default: () => ( @@ -1082,9 +1114,9 @@ export default defineComponent({ @@ -1107,9 +1139,9 @@ export default defineComponent({ tabindex={tabindex} ref="selfElRef" style={this.cssVars as CSSProperties} - onKeyup={handleKeyup} - onKeydown={handleKeydown} - onFocusout={handleFocusout} + onKeyup={mergedFocusable ? handleKeyup : undefined} + onKeydown={mergedFocusable ? handleKeydown : undefined} + onFocusout={mergedFocusable ? handleFocusout : undefined} onDragleave={draggable ? this.handleDragLeaveTree : undefined} > {this.fNodes.map(createNode)} diff --git a/src/tree/src/TreeNode.tsx b/src/tree/src/TreeNode.tsx index db5de48e04e..a875b4178bb 100644 --- a/src/tree/src/TreeNode.tsx +++ b/src/tree/src/TreeNode.tsx @@ -11,6 +11,7 @@ import { } from 'vue' import { useMemo } from 'vooks' import { happensIn, repeat } from 'seemly' +import { createDataKey } from '../../_utils' import NTreeNodeSwitcher from './TreeNodeSwitcher' import NTreeNodeCheckbox from './TreeNodeCheckbox' import NTreeNodeContent from './TreeNodeContent' @@ -177,6 +178,7 @@ const TreeNode = defineComponent({ () => NTree.disabledRef.value || props.tmNode.disabled ), checkboxDisabled: computed(() => !!props.tmNode.rawNode.checkboxDisabled), + internalScrollable: NTree.internalScrollableRef, checkable: NTree.checkableRef, draggable: NTree.draggableRef, blockLine: NTree.blockLineRef, @@ -208,7 +210,8 @@ const TreeNode = defineComponent({ indent, disabled, pending, - suffix + suffix, + internalScrollable } = this // drag start not inside // it need to be append to node itself, not wrapper @@ -222,6 +225,9 @@ const TreeNode = defineComponent({ onDragover: this.handleDragOver } : undefined + // In non virtual mode, there's no evidence that which element should be + // scrolled to, so we need data-key to query the target element. + const dataKey = internalScrollable ? createDataKey(tmNode.key) : undefined return (
disabledRef: Ref pendingNodeKeyRef: Ref + internalScrollableRef: Ref handleSwitcherClick: (node: TreeNode) => void handleSelect: (node: TreeNode) => void handleCheck: (node: TreeNode, checked: boolean) => void @@ -91,3 +92,8 @@ export interface MotionData { mode: 'expand' | 'collapse' nodes: TmNode[] } + +export interface InternalTreeInst { + handleKeyup: (e: KeyboardEvent) => void + handleKeydown: (e: KeyboardEvent) => void +} From 68f171d02f221ecd3075e32af0938dfb0ca165fd Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 15:38:41 +0800 Subject: [PATCH 09/29] fix(tree): `selected-keys` prop influences original array --- CHANGELOG.en-US.md | 1 + CHANGELOG.zh-CN.md | 1 + src/tree/src/Tree.tsx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 95b0c215bbd..910d2283049 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -13,6 +13,7 @@ - Fix `n-tree` drag over leaf node causes error, closes [#200](https://github.com/TuSimple/naive-ui/issues/200). - Fix `n-tree` misses `on-update-expanded-keys`, `on-update-selected-keys`, `on-update-checked-keys` prop. +- Fix `n-tree`'s `selected-keys` prop influences original array. ## 2.12.2 (2021-06-19) diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 31197512cbe..e6e0c7045ad 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -13,6 +13,7 @@ - 修复 `n-tree` 缺少 `on-update-expanded-keys`、`on-update-selected-keys`、`on-update-checked-keys` 属性 - 修复 `n-tree` 拖拽悬浮叶节点报错,关闭 [#200](https://github.com/TuSimple/naive-ui/issues/200) +- 修复 `n-tree` 对 `selected-keys` 属性影响原数组 ## 2.12.2 (2021-06-19) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 85c48f6459f..b29030e2b80 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -542,7 +542,7 @@ export default defineComponent({ handleCheck(node, mergedCheckedKeysRef.value.includes(node.key)) } if (props.multiple) { - const selectedKeys = mergedSelectedKeysRef.value + const selectedKeys = Array.from(mergedSelectedKeysRef.value) const index = selectedKeys.findIndex((key) => key === node.key) if (~index) { if (props.cancelable) { From 2f5cb3e5adce72ed275d6ad1c996f0d66ef478db Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 15:53:31 +0800 Subject: [PATCH 10/29] feat(tree): pending on selected node at first --- src/tree/src/Tree.tsx | 16 +++++++++++++--- src/tree/src/keyboard.tsx | 9 ++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index b29030e2b80..60c9d2a94d6 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -276,6 +276,7 @@ export default defineComponent({ ) const { pendingNodeKeyRef, handleKeyup, handleKeydown } = useKeyboard({ + mergedSelectedKeysRef, fNodesRef, mergedExpandedKeysRef, handleSelect, @@ -438,7 +439,7 @@ export default defineComponent({ if (onUpdateExpandedKeys) call(onUpdateExpandedKeys, value) if (onExpandedKeysChange) call(onExpandedKeysChange, value) } - function doCheckedKeysChange (value: Key[]): void { + function doUpdateCheckedKeys (value: Key[]): void { const { 'onUpdate:checkedKeys': _onUpdateCheckedKeys, onUpdateCheckedKeys, @@ -515,7 +516,7 @@ export default defineComponent({ cascade: props.cascade } ) - doCheckedKeysChange(checkedKeys) + doUpdateCheckedKeys(checkedKeys) } function toggleExpand (key: Key): void { if (props.disabled) return @@ -539,7 +540,16 @@ export default defineComponent({ if (props.disabled || node.disabled || !props.selectable) return pendingNodeKeyRef.value = node.key if (props.internalCheckOnSelect) { - handleCheck(node, mergedCheckedKeysRef.value.includes(node.key)) + const { + value: { checkedKeys, indeterminateKeys } + } = checkedStatusRef + handleCheck( + node, + !( + checkedKeys.includes(node.key) || + indeterminateKeys.includes(node.key) + ) + ) } if (props.multiple) { const selectedKeys = Array.from(mergedSelectedKeysRef.value) diff --git a/src/tree/src/keyboard.tsx b/src/tree/src/keyboard.tsx index 48e1eed56f7..bce887e4ef5 100644 --- a/src/tree/src/keyboard.tsx +++ b/src/tree/src/keyboard.tsx @@ -6,11 +6,13 @@ import { TreeOption } from './interface' export function useKeyboard ({ fNodesRef, mergedExpandedKeysRef, + mergedSelectedKeysRef, handleSelect, handleSwitcherClick }: { fNodesRef: Ref>> mergedExpandedKeysRef: Ref + mergedSelectedKeysRef: Ref handleSelect: (node: TmNode) => void handleSwitcherClick: (node: TmNode) => void }): { @@ -18,7 +20,12 @@ export function useKeyboard ({ handleKeyup: (e: KeyboardEvent) => void handleKeydown: (e: KeyboardEvent) => void } { - const pendingNodeKeyRef = ref(null) + const { value: mergedSelectedKeys } = mergedSelectedKeysRef + const pendingNodeKeyRef = ref( + mergedSelectedKeys.length + ? mergedSelectedKeys[mergedSelectedKeys.length - 1] + : null + ) function handleKeyup (e: KeyboardEvent): void { const { value: pendingNodeKey } = pendingNodeKeyRef if (pendingNodeKey === null) { From a38aa40b439472f0b0dc2462a2d278403f17d447 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 16:22:01 +0800 Subject: [PATCH 11/29] feat(tree): follow tree-select's pending status --- src/tree/src/Tree.tsx | 1 + src/tree/src/keyboard.tsx | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 60c9d2a94d6..504a72e784d 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -314,6 +314,7 @@ export default defineComponent({ toRef(props, 'data'), () => { loadingKeysRef.value = [] + pendingNodeKeyRef.value = null resetDndState() }, { diff --git a/src/tree/src/keyboard.tsx b/src/tree/src/keyboard.tsx index bce887e4ef5..75666c453a9 100644 --- a/src/tree/src/keyboard.tsx +++ b/src/tree/src/keyboard.tsx @@ -1,7 +1,7 @@ -import { ref, Ref } from 'vue' +import { inject, ref, Ref } from 'vue' import { TreeNode } from 'treemate' -import { Key, TmNode } from '../../tree/src/interface' -import { TreeOption } from './interface' +import { Key, TmNode, TreeOption } from './interface' +import { treeSelectInjectionKey } from '../../tree-select/src/interface' export function useKeyboard ({ fNodesRef, @@ -21,11 +21,16 @@ export function useKeyboard ({ handleKeydown: (e: KeyboardEvent) => void } { const { value: mergedSelectedKeys } = mergedSelectedKeysRef - const pendingNodeKeyRef = ref( - mergedSelectedKeys.length - ? mergedSelectedKeys[mergedSelectedKeys.length - 1] - : null - ) + + // If it's used in tree-select, make it take over pending state + const treeSelectInjection = inject(treeSelectInjectionKey, null) + const pendingNodeKeyRef = treeSelectInjection + ? treeSelectInjection.pendingNodeKeyRef + : ref( + mergedSelectedKeys.length + ? mergedSelectedKeys[mergedSelectedKeys.length - 1] + : null + ) function handleKeyup (e: KeyboardEvent): void { const { value: pendingNodeKey } = pendingNodeKeyRef if (pendingNodeKey === null) { From ab39a2e15a010721438f2096f6524521f0d74b05 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 20:21:51 +0800 Subject: [PATCH 12/29] feat(tree): `internalCheckboxFocusable` prop --- src/tree/src/Tree.tsx | 7 ++++++- src/tree/src/TreeNode.tsx | 2 ++ src/tree/src/TreeNodeCheckbox.tsx | 3 +++ src/tree/src/interface.ts | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 504a72e784d..0d505d3403a 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -154,6 +154,10 @@ const treeProps = { internalTreeMate: Object as PropType>, internalHighlightKeySet: Object as PropType>, internalCheckOnSelect: Boolean, + internalCheckboxFocusable: { + type: Boolean, + default: true + }, internalFocusable: { // Make tree-select take over keyboard operations type: Boolean, @@ -964,7 +968,6 @@ export default defineComponent({ checkableRef: toRef(props, 'checkable'), blockLineRef: toRef(props, 'blockLine'), indentRef: toRef(props, 'indent'), - internalScrollableRef: toRef(props, 'internalScrollable'), droppingMouseNodeRef, droppingNodeParentRef, draggingNodeRef, @@ -972,6 +975,8 @@ export default defineComponent({ droppingOffsetLevelRef, fNodesRef, pendingNodeKeyRef, + internalScrollableRef: toRef(props, 'internalScrollable'), + internalCheckboxFocusableRef: toRef(props, 'internalCheckboxFocusable'), handleSwitcherClick, handleDragEnd, handleDragEnter, diff --git a/src/tree/src/TreeNode.tsx b/src/tree/src/TreeNode.tsx index a875b4178bb..e02151d55b2 100644 --- a/src/tree/src/TreeNode.tsx +++ b/src/tree/src/TreeNode.tsx @@ -182,6 +182,7 @@ const TreeNode = defineComponent({ checkable: NTree.checkableRef, draggable: NTree.draggableRef, blockLine: NTree.blockLineRef, + checkboxFocusable: NTree.internalCheckboxFocusableRef, droppingPosition: droppingPositionRef, droppingOffsetLevel: droppingOffsetLevelRef, indent: indentRef, @@ -266,6 +267,7 @@ const TreeNode = defineComponent({ /> {checkable ? ( pendingNodeKeyRef: Ref internalScrollableRef: Ref + internalCheckboxFocusableRef: Ref handleSwitcherClick: (node: TreeNode) => void handleSelect: (node: TreeNode) => void handleCheck: (node: TreeNode, checked: boolean) => void From 773ffb4c8d72b8b4f28b0c230437beee2cc909b7 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 21:00:45 +0800 Subject: [PATCH 13/29] refactor(tree): split treeMate to displayTreeMate & dataTreeMate to work with tree-select --- src/tree/src/Tree.tsx | 114 ++++++++++++------------------------------ src/tree/src/utils.ts | 5 ++ 2 files changed, 38 insertions(+), 81 deletions(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 0d505d3403a..d271164800f 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -14,6 +14,7 @@ import { import { createTreeMate, flatten, createIndexGetter, TreeMate } from 'treemate' import { useMergedState } from 'vooks' import { VirtualListInst, VVirtualList } from 'vueuc' +import { getPadding } from 'seemly' import { useConfig, useTheme } from '../../_mixins' import type { ThemeProps } from '../../_mixins' import { call, createDataKey, warn } from '../../_utils' @@ -23,9 +24,8 @@ import type { ScrollbarInst } from '../../scrollbar' import { treeLight } from '../styles' import type { TreeTheme } from '../styles' import NTreeNode from './TreeNode' -import { keysWithFilter, emptyImage } from './utils' +import { keysWithFilter, emptyImage, defaultFilter } from './utils' import { useKeyboard } from './keyboard' -import style from './styles/index.cssr' import { DragInfo, DropInfo, @@ -43,7 +43,7 @@ import { } from './interface' import MotionWrapper from './MotionWrapper' import { defaultAllowDrop } from './dnd' -import { getPadding } from 'seemly' +import style from './styles/index.cssr' // TODO: // During expanding, some node are mis-applied with :active style @@ -58,6 +58,10 @@ export const treeMateOptions = { } export const treeSharedProps = { + filter: { + type: Function as PropType<(pattern: string, node: TreeOption) => boolean>, + default: defaultFilter + }, defaultExpandAll: Boolean, expandedKeys: Array as PropType, defaultExpandedKeys: { @@ -107,13 +111,6 @@ const treeProps = { type: String, default: '' }, - filter: { - type: Function as PropType<(pattern: string, node: TreeOption) => boolean>, - default: (pattern: string, node: TreeOption) => { - if (!pattern) return true - return ~node.label.toLowerCase().indexOf(pattern.toLowerCase()) - } - }, onLoad: Function as PropType<(node: TreeOption) => Promise>, cascade: Boolean, selectable: { @@ -148,12 +145,16 @@ const treeProps = { MaybeArray<(value: Key[]) => void> >, ...treeSharedProps, - // set for tree-select + // internal props for tree-select internalScrollable: Boolean, internalScrollablePadding: String, - internalTreeMate: Object as PropType>, + // use it to do check + internalDataTreeMate: Object as PropType>, + // use it to display + internalDisplayTreeMate: Object as PropType>, internalHighlightKeySet: Object as PropType>, internalCheckOnSelect: Boolean, + internalHideFilteredNode: Boolean, // I'm sure this won't work with draggable internalCheckboxFocusable: { type: Boolean, default: true @@ -162,49 +163,6 @@ const treeProps = { // Make tree-select take over keyboard operations type: Boolean, default: true - }, - // deprecated - /** @deprecated */ - onExpandedKeysChange: { - type: [Function, Array] as PropType< - MaybeArray<(value: Key[]) => void> | undefined - >, - validator: () => { - warn( - 'tree', - '`on-expanded-keys-change` is deprecated, please use `on-update:expanded-keys` instead.' - ) - return true - }, - default: undefined - }, - /** @deprecated */ - onCheckedKeysChange: { - type: [Function, Array] as PropType< - MaybeArray<(value: Key[]) => void> | undefined - >, - validator: () => { - warn( - 'tree', - '`on-checked-keys-change` is deprecated, please use `on-update:expanded-keys` instead.' - ) - return true - }, - default: undefined - }, - /** @deprecated */ - onSelectedKeysChange: { - type: [Function, Array] as PropType< - MaybeArray<(value: Key[]) => void> | undefined - >, - validator: () => { - warn( - 'tree', - '`on-selected-keys-change` is deprecated, please use `on-update:selected-keys` instead.' - ) - return true - }, - default: undefined } } as const @@ -232,11 +190,13 @@ export default defineComponent({ function getScrollContent (): HTMLElement | null | undefined { return virtualListInstRef.value?.itemsElRef } - const treeMateRef = computed(() => { - return ( - props.internalTreeMate || createTreeMate(props.data, treeMateOptions) - ) - }) + // We don't expect data source to change so we just determine it once + const displayTreeMateRef = props.internalDisplayTreeMate + ? toRef(props, 'internalDisplayTreeMate') + : computed(() => createTreeMate(props.data, treeMateOptions)) + const dataTreeMateRef = props.internalDataTreeMate + ? toRef(props, 'internalDataTreeMate') + : displayTreeMateRef const uncontrolledCheckedKeysRef = ref( props.defaultCheckedKeys || props.checkedKeys ) @@ -246,7 +206,7 @@ export default defineComponent({ uncontrolledCheckedKeysRef ) const checkedStatusRef = computed(() => { - return treeMateRef.value.getCheckedKeys(mergedCheckedKeysRef.value, { + return dataTreeMateRef.value!.getCheckedKeys(mergedCheckedKeysRef.value, { cascade: props.cascade }) }) @@ -266,7 +226,7 @@ export default defineComponent({ ) const uncontrolledExpandedKeysRef = ref( props.defaultExpandAll - ? treeMateRef.value.getNonLeafKeys() + ? dataTreeMateRef.value!.getNonLeafKeys() : props.defaultExpandedKeys ) const controlledExpandedKeysRef = toRef(props, 'expandedKeys') @@ -276,7 +236,7 @@ export default defineComponent({ ) const fNodesRef = computed(() => - treeMateRef.value.getFlattenedNodes(mergedExpandedKeysRef.value) + displayTreeMateRef.value!.getFlattenedNodes(mergedExpandedKeysRef.value) ) const { pendingNodeKeyRef, handleKeyup, handleKeydown } = useKeyboard({ @@ -371,7 +331,7 @@ export default defineComponent({ if (addedKey !== null) { // play add animation aipRef.value = true - afNodeRef.value = treeMateRef.value.getFlattenedNodes(prevValue) + afNodeRef.value = displayTreeMateRef.value!.getFlattenedNodes(prevValue) const expandedNodeIndex = afNodeRef.value.findIndex( (node) => (node as any).key === addedKey ) @@ -396,7 +356,7 @@ export default defineComponent({ if (removedKey !== null) { // play remove animation aipRef.value = true - afNodeRef.value = treeMateRef.value.getFlattenedNodes(value) + afNodeRef.value = displayTreeMateRef.value!.getFlattenedNodes(value) const collapsedNodeIndex = afNodeRef.value.findIndex( (node) => (node as any).key === removedKey ) @@ -436,35 +396,29 @@ export default defineComponent({ function doUpdateExpandedKeys (value: Key[]): void { const { 'onUpdate:expandedKeys': _onUpdateExpandedKeys, - onUpdateExpandedKeys, - onExpandedKeysChange + onUpdateExpandedKeys } = props uncontrolledExpandedKeysRef.value = value if (_onUpdateExpandedKeys) call(_onUpdateExpandedKeys, value) if (onUpdateExpandedKeys) call(onUpdateExpandedKeys, value) - if (onExpandedKeysChange) call(onExpandedKeysChange, value) } function doUpdateCheckedKeys (value: Key[]): void { const { 'onUpdate:checkedKeys': _onUpdateCheckedKeys, - onUpdateCheckedKeys, - onCheckedKeysChange + onUpdateCheckedKeys } = props uncontrolledCheckedKeysRef.value = value if (onUpdateCheckedKeys) call(onUpdateCheckedKeys, value) if (_onUpdateCheckedKeys) call(_onUpdateCheckedKeys, value) - if (onCheckedKeysChange) call(onCheckedKeysChange, value) } function doUpdateSelectedKeys (value: Key[]): void { const { 'onUpdate:selectedKeys': _onUpdateSelectedKeys, - onUpdateSelectedKeys, - onSelectedKeysChange + onUpdateSelectedKeys } = props uncontrolledSelectedKeysRef.value = value if (onUpdateSelectedKeys) call(onUpdateSelectedKeys, value) if (_onUpdateSelectedKeys) call(_onUpdateSelectedKeys, value) - if (onSelectedKeysChange) call(onSelectedKeysChange, value) } // Drag & Drop function doDragEnter (info: DragInfo): void { @@ -514,13 +468,11 @@ export default defineComponent({ } function handleCheck (node: TmNode, checked: boolean): void { if (props.disabled || node.disabled) return - const { checkedKeys } = treeMateRef.value[checked ? 'check' : 'uncheck']( - node.key, - displayedCheckedKeysRef.value, - { - cascade: props.cascade - } - ) + const { checkedKeys } = dataTreeMateRef.value![ + checked ? 'check' : 'uncheck' + ](node.key, displayedCheckedKeysRef.value, { + cascade: props.cascade + }) doUpdateCheckedKeys(checkedKeys) } function toggleExpand (key: Key): void { diff --git a/src/tree/src/utils.ts b/src/tree/src/utils.ts index 934d87237fb..90c78d36565 100644 --- a/src/tree/src/utils.ts +++ b/src/tree/src/utils.ts @@ -55,4 +55,9 @@ if (typeof window !== 'undefined') { 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' } +export const defaultFilter = (pattern: string, node: TreeOption): boolean => { + if (!pattern.length) return true + return node.label.toLowerCase().includes(pattern.toLowerCase()) +} + export { emptyImage } From b79d474ed5ac3f73e743c1fd3aa81dc26b78dab6 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 21:10:06 +0800 Subject: [PATCH 14/29] fix(tree): scrollbar won't sync in virtual scroll mode --- src/tree/src/Tree.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index d271164800f..f17411b815d 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -9,7 +9,8 @@ import { PropType, watch, CSSProperties, - VNode + VNode, + nextTick } from 'vue' import { createTreeMate, flatten, createIndexGetter, TreeMate } from 'treemate' import { useMergedState } from 'vooks' @@ -391,6 +392,15 @@ export default defineComponent({ function handleAfterEnter (): void { aipRef.value = false + if (props.virtualScroll) { + // If virtual scroll, we won't listen to resize during animation, so + // resize callback of virtual list won't be called and as a result + // scrollbar won't sync. We need to sync scrollbar manually. + void nextTick(() => { + const { value: scrollbarInst } = scrollbarInstRef + if (scrollbarInst) scrollbarInst.sync() + }) + } } function doUpdateExpandedKeys (value: Key[]): void { From c054d942f6b3a0d052637891b7afd568b99aeec1 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 22:30:56 +0800 Subject: [PATCH 15/29] feat(tree-select) --- package.json | 2 +- src/themes/dark.ts | 2 + src/themes/light.ts | 2 + src/tree-select/demos/enUS/basic.demo.md | 34 + .../demos/enUS/index.demo-entry.md | 9 + src/tree-select/demos/zhCN/basic.demo.md | 48 ++ .../demos/zhCN/index.demo-entry.md | 9 + src/tree-select/index.ts | 1 + src/tree-select/src/TreeSelect.tsx | 677 ++++++++++++++++++ src/tree-select/src/interface.ts | 31 + src/tree-select/src/styles/index.cssr.ts | 37 + src/tree-select/src/utils.ts | 76 ++ src/tree-select/styles/dark.ts | 17 + src/tree-select/styles/index.ts | 3 + src/tree-select/styles/light.ts | 33 + src/tree-select/tests/TreeSelect.spec.ts | 8 + 16 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 src/tree-select/demos/enUS/basic.demo.md create mode 100644 src/tree-select/demos/enUS/index.demo-entry.md create mode 100644 src/tree-select/demos/zhCN/basic.demo.md create mode 100644 src/tree-select/demos/zhCN/index.demo-entry.md create mode 100644 src/tree-select/index.ts create mode 100644 src/tree-select/src/TreeSelect.tsx create mode 100644 src/tree-select/src/interface.ts create mode 100644 src/tree-select/src/styles/index.cssr.ts create mode 100644 src/tree-select/src/utils.ts create mode 100644 src/tree-select/styles/dark.ts create mode 100644 src/tree-select/styles/index.ts create mode 100644 src/tree-select/styles/light.ts create mode 100644 src/tree-select/tests/TreeSelect.spec.ts diff --git a/package.json b/package.json index d0c28dae488..28a7da0e3e5 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "superagent": "^6.1.0", "typescript": "^4.3.2", "vite": "^2.1.3", - "vue": "^3.0.10", + "vue": "^3.1.1", "vue-router": "^4.0.5" }, "peerDependencies": { diff --git a/src/themes/dark.ts b/src/themes/dark.ts index fe94279a9c9..d71b488f42b 100644 --- a/src/themes/dark.ts +++ b/src/themes/dark.ts @@ -67,6 +67,7 @@ import { timePickerDark } from '../time-picker/styles' import { timelineDark } from '../timeline/styles' import { tooltipDark } from '../tooltip/styles' import { transferDark } from '../transfer/styles' +import { treeSelectDark } from '../tree-select/styles' import { typographyDark } from '../typography/styles' import { treeDark } from '../tree/styles' import { uploadDark } from '../upload/styles' @@ -143,6 +144,7 @@ export const darkTheme: BuiltInGlobalTheme = { Tooltip: tooltipDark, Transfer: transferDark, Tree: treeDark, + TreeSelect: treeSelectDark, Typography: typographyDark, Upload: uploadDark } diff --git a/src/themes/light.ts b/src/themes/light.ts index 5b15d9a7bd6..93e94956100 100644 --- a/src/themes/light.ts +++ b/src/themes/light.ts @@ -71,6 +71,7 @@ import { tooltipLight } from '../tooltip/styles' import { transferLight } from '../transfer/styles' import { typographyLight } from '../typography/styles' import { treeLight } from '../tree/styles' +import { treeSelectLight } from '../tree-select/styles' import { uploadLight } from '../upload/styles' import type { BuiltInGlobalTheme } from './interface' @@ -145,6 +146,7 @@ export const lightTheme: BuiltInGlobalTheme = { Tooltip: tooltipLight, Transfer: transferLight, Tree: treeLight, + TreeSelect: treeSelectLight, Typography: typographyLight, Upload: uploadLight } diff --git a/src/tree-select/demos/enUS/basic.demo.md b/src/tree-select/demos/enUS/basic.demo.md new file mode 100644 index 00000000000..a4ea74dbb91 --- /dev/null +++ b/src/tree-select/demos/enUS/basic.demo.md @@ -0,0 +1,34 @@ +# Basic + +```html + +``` + +```js +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) + } + }) +} + +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' +} + +export default { + options () { + return { + options: createData() + } + } +} +``` diff --git a/src/tree-select/demos/enUS/index.demo-entry.md b/src/tree-select/demos/enUS/index.demo-entry.md new file mode 100644 index 00000000000..0cb4ff8bb1d --- /dev/null +++ b/src/tree-select/demos/enUS/index.demo-entry.md @@ -0,0 +1,9 @@ +# Tree Select + +It's said that 99% of the people can't distinguish it from cascader. + +## Demos + +```demo +basic +``` diff --git a/src/tree-select/demos/zhCN/basic.demo.md b/src/tree-select/demos/zhCN/basic.demo.md new file mode 100644 index 00000000000..edf6de5e684 --- /dev/null +++ b/src/tree-select/demos/zhCN/basic.demo.md @@ -0,0 +1,48 @@ +# 基础用法 + +```html + + + Multiple + Checkable + Cascade + Filterable + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +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: key, + key, + children: createData(level - 1, key) + } + }) +} + +export default defineComponent({ + setup () { + return { + multiple: ref(false), + checkable: ref(false), + cascade: ref(false), + filterable: ref(false), + options: createData() + } + } +}) +``` diff --git a/src/tree-select/demos/zhCN/index.demo-entry.md b/src/tree-select/demos/zhCN/index.demo-entry.md new file mode 100644 index 00000000000..6c42ac5e48a --- /dev/null +++ b/src/tree-select/demos/zhCN/index.demo-entry.md @@ -0,0 +1,9 @@ +# 树型选择 Tree Select + +据说 99% 的人分不清它和 Cascader 的区别。 + +## 演示 + +```demo +basic +``` diff --git a/src/tree-select/index.ts b/src/tree-select/index.ts new file mode 100644 index 00000000000..c10f8df6af8 --- /dev/null +++ b/src/tree-select/index.ts @@ -0,0 +1 @@ +export { default as NTreeSelect } from './src/TreeSelect' diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx new file mode 100644 index 00000000000..0a159e59829 --- /dev/null +++ b/src/tree-select/src/TreeSelect.tsx @@ -0,0 +1,677 @@ +import { + h, + defineComponent, + PropType, + ref, + toRef, + Transition, + withDirectives, + computed, + CSSProperties, + provide, + watch, + nextTick +} from 'vue' +import { + FollowerPlacement, + VBinder, + VFollower, + VTarget, + FollowerInst +} from 'vueuc' +import { useIsMounted, useMergedState } from 'vooks' +import { clickoutside } from 'vdirs' +import { createTreeMate } from 'treemate' +import { TreeOptions, Key, InternalTreeInst } from '../../tree/src/interface' +import type { SelectBaseOption } from '../../select/src/interface' +import { treeMateOptions, treeSharedProps } from '../../tree/src/Tree' +import { + NInternalSelection, + InternalSelectionInst, + NBaseFocusDetector +} from '../../_internal' +import { NTree } from '../../tree' +import { NEmpty } from '../../empty' +import { useConfig, useFormItem, useLocale, useTheme } from '../../_mixins' +import type { ThemeProps } from '../../_mixins' +import { + call, + ExtractPublicPropTypes, + MaybeArray, + useAdjustedTo +} from '../../_utils' +import { treeSelectLight, TreeSelectTheme } from '../styles' +import { + OnUpdateValue, + OnUpdateValueImpl, + treeSelectInjectionKey, + Value +} from './interface' +import { treeOption2SelectOption, filterTree } from './utils' +import style from './styles/index.cssr' + +const props = { + ...(useTheme.props as ThemeProps), + bordered: { + type: Boolean, + default: true + }, + cascade: Boolean, + checkable: Boolean, + clearable: Boolean, + consistentMenuWidth: { + type: Boolean, + default: true + }, + defaultShow: Boolean, + defaultValue: { + type: [String, Number, Array] as PropType< + string | number | Array | null + >, + default: null + }, + disabled: Boolean, + filterable: Boolean, + maxTagCount: [String, Number] as PropType, + multiple: Boolean, + options: { + type: Array as PropType, + default: () => [] + }, + placeholder: String, + placement: { + type: String as PropType, + default: 'bottom-start' + }, + show: { + type: Boolean as PropType, + default: undefined + }, + size: String as PropType<'small' | 'medium' | 'large'>, + value: [String, Number, Array] as PropType< + string | number | Array | null + >, + to: useAdjustedTo.propTo, + virtualScroll: { + type: Boolean, + default: true + }, + ...treeSharedProps, + onBlur: Function as PropType<(e: FocusEvent) => void>, + onFocus: Function as PropType<(e: FocusEvent) => void>, + onUpdateShow: [Function, Array] as PropType< + MaybeArray<(show: boolean) => void> + >, + onUpdateValue: [Function, Array] as PropType>, + 'onUpdate:value': [Function, Array] as PropType>, + 'onUpdate:show': [Function, Array] as PropType< + MaybeArray<(show: boolean) => void> + > +} as const + +export type TreeSelectProps = ExtractPublicPropTypes + +export default defineComponent({ + name: 'TreeSelect', + props, + setup (props) { + const followerInstRef = ref(null) + const triggerInstRef = ref(null) + const treeInstRef = ref(null) + const menuElRef = ref(null) + const { mergedClsPrefixRef, namespaceRef } = useConfig(props) + const { localeRef } = useLocale('Select') + const { + mergedSizeRef, + nTriggerFormBlur, + nTriggerFormChange, + nTriggerFormFocus, + nTriggerFormInput + } = useFormItem(props) + const uncontrolledValueRef = ref(props.defaultValue) + const controlledValueRef = toRef(props, 'value') + const mergedValueRef = useMergedState( + controlledValueRef, + uncontrolledValueRef + ) + const uncontrolledShowRef = ref(props.defaultShow) + const controlledShowRef = toRef(props, 'show') + const mergedShowRef = useMergedState(controlledShowRef, uncontrolledShowRef) + const patternRef = ref('') + const filteredTreeInfoRef = computed<{ + filteredTree: TreeOptions + highlightKeySet: Set | undefined + }>(() => { + if (!props.filterable) { + return { + filteredTree: props.options, + highlightKeySet: undefined + } + } + const { value: pattern } = patternRef + if (!pattern.length || !props.filter) { + return { + filteredTree: props.options, + highlightKeySet: undefined + } + } + return filterTree(props.options, props.filter, pattern) + }) + // used to resolve selected options + const dataTreeMateRef = computed(() => + createTreeMate(props.options, treeMateOptions) + ) + const displayTreeMateRef = computed(() => + createTreeMate(filteredTreeInfoRef.value.filteredTree, treeMateOptions) + ) + const { value: initMergedValue } = mergedValueRef + const pendingNodeKeyRef = ref( + props.checkable + ? null + : Array.isArray(initMergedValue) && initMergedValue.length + ? initMergedValue[initMergedValue.length - 1] + : null + ) + const mergedCascadeRef = computed(() => { + return props.multiple && props.cascade + }) + // The same logic as tree, now it's not that complex so I don't extract a + // function to reuse it. + const uncontrolledExpandedKeysRef = ref( + props.defaultExpandAll + ? displayTreeMateRef.value.getNonLeafKeys() + : props.defaultExpandedKeys || props.expandedKeys + ) + const controlledExpandedKeysRef = toRef(props, 'expandedKeys') + const mergedExpandedKeysRef = useMergedState( + controlledExpandedKeysRef, + uncontrolledExpandedKeysRef + ) + const focusedRef = ref(false) + const mergedPlaceholderRef = computed(() => { + const { placeholder } = props + if (placeholder !== undefined) return placeholder + return localeRef.value.placeholder + }) + const treeSelectedKeysRef = computed(() => { + if (props.checkable) return [] + const { value: mergedValue } = mergedValueRef + const { multiple } = props + return Array.isArray(mergedValue) + ? multiple + ? mergedValue + : [] + : multiple + ? [] + : mergedValue === null + ? [] + : [mergedValue] + }) + const treeCheckedKeysRef = computed(() => { + if (!props.checkable) return [] + const { value: mergedValue } = mergedValueRef + if (props.multiple) { + if (Array.isArray(mergedValue)) return mergedValue + else return [] + } else { + if (mergedValue === null || Array.isArray(mergedValue)) return [] + else return [mergedValue] + } + }) + const selectedOptionRef = computed(() => { + if (props.multiple) return null + const { value: mergedValue } = mergedValueRef + if (!Array.isArray(mergedValue) && mergedValue !== null) { + const tmNode = dataTreeMateRef.value.getNode(mergedValue) + if (tmNode !== null) return treeOption2SelectOption(tmNode.rawNode) + } + return null + }) + const selectedOptionsRef = computed(() => { + if (!props.multiple) return null + const { value: mergedValue } = mergedValueRef + if (Array.isArray(mergedValue)) { + const res: SelectBaseOption[] = [] + const { value: treeMate } = dataTreeMateRef + mergedValue.forEach((value) => { + const tmNode = treeMate.getNode(value) + if (tmNode !== null) res.push(treeOption2SelectOption(tmNode.rawNode)) + }) + return res + } + return [] + }) + const menuPaddingRef = computed(() => { + const { + self: { menuPadding } + } = themeRef.value + return menuPadding + }) + function focusSelection (): void { + triggerInstRef.value?.focus() + } + function focusSelectionInput (): void { + triggerInstRef.value?.focusInput() + } + function doUpdateShow (value: boolean): void { + const { onUpdateShow, 'onUpdate:show': _onUpdateShow } = props + if (onUpdateShow) call(onUpdateShow, value) + if (_onUpdateShow) call(_onUpdateShow, value) + uncontrolledShowRef.value = value + } + function doUpdateValue ( + value: string | number | Array | null + ): void { + const { onUpdateValue, 'onUpdate:value': _onUpdateValue } = props + if (onUpdateValue) call(onUpdateValue as OnUpdateValueImpl, value) + if (_onUpdateValue) call(_onUpdateValue as OnUpdateValueImpl, value) + uncontrolledValueRef.value = value + nTriggerFormInput() + nTriggerFormChange() + } + function doUpdateExpandedKeys (keys: Key[]): void { + const { + onUpdateExpandedKeys, + 'onUpdate:expandedKeys': _onUpdateExpandedKeys + } = props + if (onUpdateExpandedKeys) call(onUpdateExpandedKeys, keys) + if (_onUpdateExpandedKeys) call(_onUpdateExpandedKeys, keys) + uncontrolledExpandedKeysRef.value = keys + } + function doFocus (e: FocusEvent): void { + const { onFocus } = props + if (onFocus) onFocus(e) + nTriggerFormFocus() + } + function doBlur (e: FocusEvent): void { + closeMenu() + const { onBlur } = props + if (onBlur) onBlur(e) + nTriggerFormBlur() + } + function closeMenu (): void { + doUpdateShow(false) + } + function openMenu (): void { + if (!props.disabled) { + patternRef.value = '' + doUpdateShow(true) + if (props.filterable) { + focusSelectionInput() + } + } + } + function handleMenuLeave (): void { + patternRef.value = '' + } + function handleMenuClickoutside (e: MouseEvent): void { + if (mergedShowRef.value) { + if (!triggerInstRef.value?.$el.contains(e.target as Node)) { + // outside select, don't need to return focus + closeMenu() + } + } + } + function handleTriggerClick (): void { + if (props.disabled) return + if (!mergedShowRef.value) { + openMenu() + } else { + if (!props.filterable) { + // already focused, don't need to return focus + closeMenu() + } + } + } + function handleUpdateSelectedKeys (keys: Key[]): void { + if (props.checkable && props.multiple) { + return + } + if (props.multiple) { + doUpdateValue(keys) + } else { + doUpdateValue(keys[0] ?? null) + } + if (props.filterable) { + focusSelectionInput() + patternRef.value = '' + } + } + function handleUpdateCheckedKeys (keys: Key[]): void { + // only in checkable & multiple mode, we use tree's check update + if (props.checkable && props.multiple) { + doUpdateValue(keys) + if (props.filterable) { + focusSelectionInput() + patternRef.value = '' + } + } + } + function handleTriggerFocus (e: FocusEvent): void { + if (menuElRef.value?.contains(e.relatedTarget as Element)) return + focusedRef.value = true + doFocus(e) + } + function handleTriggerBlur (e: FocusEvent): void { + if (menuElRef.value?.contains(e.relatedTarget as Element)) return + focusedRef.value = false + doBlur(e) + } + function handleMenuFocusin (e: FocusEvent): void { + if ( + menuElRef.value?.contains(e.relatedTarget as Element) || + triggerInstRef.value?.$el?.contains(e.relatedTarget as Element) + ) { + return + } + focusedRef.value = true + doFocus(e) + } + function handleMenuFocusout (e: FocusEvent): void { + if ( + menuElRef.value?.contains(e.relatedTarget as Element) || + triggerInstRef.value?.$el?.contains(e.relatedTarget as Element) + ) { + return + } + focusedRef.value = false + doBlur(e) + } + function handleClear (e: MouseEvent): void { + e.stopPropagation() + const { multiple } = props + if (!multiple && props.filterable) { + closeMenu() + } + if (multiple) { + doUpdateValue([]) + } else { + doUpdateValue(null) + } + } + function handleDeleteOption (option: SelectBaseOption): void { + // only work for multiple mode + const { value: mergedValue } = mergedValueRef + if (Array.isArray(mergedValue)) { + const index = mergedValue.findIndex((key) => key === option.value) + if (~index) { + if (props.checkable) { + const { checkedKeys } = dataTreeMateRef.value.uncheck( + option.value, + mergedValue, + { + cascade: mergedCascadeRef.value + } + ) + doUpdateValue(checkedKeys) + } else { + const nextValue = Array.from(mergedValue) + nextValue.splice(index, 1) + doUpdateValue(nextValue) + } + } + } + } + function handlePatternInput (e: InputEvent): void { + const { value } = e.target as unknown as HTMLInputElement + patternRef.value = value + } + function handleKeydown (e: KeyboardEvent): void { + const { value: treeInst } = treeInstRef + if (treeInst) { + treeInst.handleKeydown(e) + } + } + function handleKeyup (e: KeyboardEvent): void { + if (e.code === 'Enter') { + if (mergedShowRef.value) { + treeHandleKeyup(e) + if (!props.multiple) { + closeMenu() + focusSelection() + } + } else { + openMenu() + } + e.preventDefault() + } else if (e.code === 'Escape') { + closeMenu() + focusSelection() + } else { + treeHandleKeyup(e) + } + } + function treeHandleKeyup (e: KeyboardEvent): void { + const { value: treeInst } = treeInstRef + if (treeInst) { + treeInst.handleKeyup(e) + } + } + function handleTabOut (): void { + closeMenu() + focusSelection() + } + provide(treeSelectInjectionKey, { + pendingNodeKeyRef + }) + function syncPosition (): void { + followerInstRef.value?.syncPosition() + } + watch(mergedValueRef, () => { + if (!mergedShowRef.value) return + void nextTick(syncPosition) + }) + const themeRef = useTheme( + 'TreeSelect', + 'TreeSelect', + style, + treeSelectLight, + props, + mergedClsPrefixRef + ) + return { + menuElRef, + triggerInstRef, + followerInstRef, + treeInstRef, + mergedClsPrefix: mergedClsPrefixRef, + mergedValue: mergedValueRef, + mergedShow: mergedShowRef, + namespace: namespaceRef, + adjustedTo: useAdjustedTo(props), + isMounted: useIsMounted(), + focused: focusedRef, + filteredTreeInfo: filteredTreeInfoRef, + dataTreeMate: dataTreeMateRef, + displayTreeMate: displayTreeMateRef, + menuPadding: menuPaddingRef, + mergedPlaceholder: mergedPlaceholderRef, + mergedExpandedKeys: mergedExpandedKeysRef, + treeSelectedKeys: treeSelectedKeysRef, + treeCheckedKeys: treeCheckedKeysRef, + mergedSize: mergedSizeRef, + selectedOption: selectedOptionRef, + selectedOptions: selectedOptionsRef, + pattern: patternRef, + pendingNodeKey: pendingNodeKeyRef, + mergedCascade: mergedCascadeRef, + doUpdateExpandedKeys, + handleMenuLeave, + handleTriggerClick, + handleMenuClickoutside, + handleUpdateSelectedKeys, + handleUpdateCheckedKeys, + handleTriggerFocus, + handleTriggerBlur, + handleMenuFocusin, + handleMenuFocusout, + handleClear, + handleDeleteOption, + handlePatternInput, + handleKeydown, + handleKeyup, + handleTabOut, + cssVars: computed(() => { + const { + common: { cubicBezierEaseInOut }, + self: { menuBoxShadow, menuBorderRadius, menuColor, menuHeight } + } = themeRef.value + return { + '--menu-box-shadow': menuBoxShadow, + '--menu-border-radius': menuBorderRadius, + '--menu-color': menuColor, + '--menu-height': menuHeight, + '--bezier': cubicBezierEaseInOut + } + }), + mergedTheme: themeRef + } + }, + render () { + const { mergedTheme, mergedClsPrefix } = this + return ( +
+ + {{ + default: () => [ + + {{ + default: () => ( + + ) + }} + , + + {{ + default: () => ( + + {{ + default: () => { + if (!this.mergedShow) return null + const { + mergedClsPrefix, + filteredTreeInfo, + checkable, + multiple + } = this + return withDirectives( +
+ {filteredTreeInfo.filteredTree.length ? ( + + ) : ( +
+ +
+ )} + +
, + [[clickoutside, this.handleMenuClickoutside]] + ) + } + }} +
+ ) + }} +
+ ] + }} +
+
+ ) + } +}) diff --git a/src/tree-select/src/interface.ts b/src/tree-select/src/interface.ts new file mode 100644 index 00000000000..0558b093f93 --- /dev/null +++ b/src/tree-select/src/interface.ts @@ -0,0 +1,31 @@ +import { InjectionKey, Ref } from 'vue' + +export type OnUpdateValue = ( + value: string & + number & + (string | number) & + string[] & + number[] & + Array & + null +) => void + +export type OnUpdateValueImpl = ( + value: + | string + | number + | (string | number) + | string[] + | number[] + | Array + | null +) => void + +export type Value = string | number | Array | null + +export interface TreeSelectInjection { + pendingNodeKeyRef: Ref +} + +export const treeSelectInjectionKey: InjectionKey = + Symbol('tree-select') diff --git a/src/tree-select/src/styles/index.cssr.ts b/src/tree-select/src/styles/index.cssr.ts new file mode 100644 index 00000000000..b835eb3ed24 --- /dev/null +++ b/src/tree-select/src/styles/index.cssr.ts @@ -0,0 +1,37 @@ +import fadeInScaleUpTransition from '../../../_styles/transitions/fade-in-scale-up.cssr' +import { c, cB, cE } from '../../../_utils/cssr' + +// vars: +// --bezier +// --menu-height +// --menu-border-radius +// --menu-box-shadow +// --menu-color +export default c([ + cB('tree-select', ` + z-index: auto; + outline: none; + width: 100%; + position: relative; + `), + cB('tree-select-menu', ` + position: relative; + overflow: hidden; + margin: 4px 0; + max-height: var(--menu-height); + transition: box-shadow .3s var(--bezier), background-color .3s var(--bezier); + border-radius: var(--menu-border-radius); + box-shadow: var(--menu-box-shadow); + background-color: var(--menu-color); + outline: none; + `, [ + cB('tree', 'max-height: inherit;'), + cE('empty', ` + display: flex; + padding: 12px 32px; + flex: 1; + justify-content: center; + `), + fadeInScaleUpTransition() + ]) +]) diff --git a/src/tree-select/src/utils.ts b/src/tree-select/src/utils.ts new file mode 100644 index 00000000000..f1c66fe9953 --- /dev/null +++ b/src/tree-select/src/utils.ts @@ -0,0 +1,76 @@ +import { SelectBaseOption } from '../../select/src/interface' +import { TreeOption, TreeOptions, Key } from '../../tree/src/interface' + +export function treeOption2SelectOption (treeOpt: TreeOption): SelectBaseOption { + return { + ...treeOpt, + value: treeOpt.key + } +} + +export function filterTree ( + tree: TreeOptions, + filter: (pattern: string, v: TreeOption) => boolean, + pattern: string +): { + filteredTree: TreeOptions + highlightKeySet: Set + } { + const visitedTailKeys = new Set() + const visitedNonTailKeys = new Set() + const highlightKeySet = new Set() + const filteredTree: TreeOptions = [] + const path: TreeOptions = [] + function visit (t: TreeOptions): void { + t.forEach((n) => { + path.push(n) + if (filter(pattern, n)) { + visitedTailKeys.add(n.key) + highlightKeySet.add(n.key) + for (let i = path.length - 2; i >= 0; --i) { + const { key } = path[i] + if (!visitedNonTailKeys.has(key)) { + visitedNonTailKeys.add(key) + if (visitedTailKeys.has(key)) { + visitedTailKeys.delete(key) + } + } else { + break + } + } + } + if (n.children) { + visit(n.children) + } + path.pop() + }) + } + visit(tree) + function build (t: TreeOptions, sibs: TreeOptions): void { + t.forEach((n) => { + const { key } = n + const isVisitedTail = visitedTailKeys.has(key) + const isVisitedNonTail = visitedNonTailKeys.has(key) + if (!isVisitedTail && !isVisitedNonTail) return + const { children } = n + if (children) { + if (isVisitedTail) { + // If it is visited path tail, use origin node + sibs.push(n) + } else { + // It it is not visited path tail, use cloned node + const clonedNode = { ...n, children: [] } + sibs.push(clonedNode) + build(children, clonedNode.children) + } + } else { + sibs.push(n) + } + }) + } + build(tree, filteredTree) + return { + filteredTree, + highlightKeySet + } +} diff --git a/src/tree-select/styles/dark.ts b/src/tree-select/styles/dark.ts new file mode 100644 index 00000000000..586dc08bf5a --- /dev/null +++ b/src/tree-select/styles/dark.ts @@ -0,0 +1,17 @@ +import { emptyDark } from '../../empty/styles' +import { treeDark } from '../../tree/styles' +import { commonDark } from '../../_styles/common' +import { internalSelectionDark } from '../../_internal/selection/styles' +import type { TreeSelectTheme } from './light' + +const treeSelectDark: TreeSelectTheme = { + name: 'TreeSelect', + common: commonDark, + peers: { + Tree: treeDark, + Empty: emptyDark, + InternalSelection: internalSelectionDark + } +} + +export default treeSelectDark diff --git a/src/tree-select/styles/index.ts b/src/tree-select/styles/index.ts new file mode 100644 index 00000000000..a93ed5f6fb7 --- /dev/null +++ b/src/tree-select/styles/index.ts @@ -0,0 +1,3 @@ +export { default as treeSelectDark } from './dark' +export { default as treeSelectLight } from './light' +export type { TreeSelectThemeVars, TreeSelectTheme } from './light' diff --git a/src/tree-select/styles/light.ts b/src/tree-select/styles/light.ts new file mode 100644 index 00000000000..90c88e9b49d --- /dev/null +++ b/src/tree-select/styles/light.ts @@ -0,0 +1,33 @@ +import { commonLight } from '../../_styles/common' +import type { ThemeCommonVars } from '../../_styles/common' +import { createTheme } from '../../_mixins/use-theme' +import { treeLight } from '../../tree/styles' +import { emptyLight } from '../../empty/styles' +import { internalSelectionLight } from '../../_internal/selection/styles' + +export const self = (vars: ThemeCommonVars) => { + const { popoverColor, boxShadow2, borderRadius, heightMedium } = vars + return { + menuPadding: '4px', + menuColor: popoverColor, + menuBoxShadow: boxShadow2, + menuBorderRadius: borderRadius, + menuHeight: `calc(${heightMedium} * 7.6)` + } +} + +export type TreeSelectThemeVars = ReturnType + +const treeSelectLight = createTheme({ + name: 'TreeSelect', + common: commonLight, + peers: { + Tree: treeLight, + Empty: emptyLight, + InternalSelection: internalSelectionLight + }, + self +}) + +export default treeSelectLight +export type TreeSelectTheme = typeof treeSelectLight diff --git a/src/tree-select/tests/TreeSelect.spec.ts b/src/tree-select/tests/TreeSelect.spec.ts new file mode 100644 index 00000000000..126ab2f9279 --- /dev/null +++ b/src/tree-select/tests/TreeSelect.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils' +import { NTreeSelect } from '../index' + +describe('n-tree-select', () => { + it('should work with import on demand', () => { + mount(NTreeSelect) + }) +}) From 8dc154158ad872799befe3d5190c5daa49f2d632 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 22:36:02 +0800 Subject: [PATCH 16/29] docs: changelog --- CHANGELOG.en-US.md | 1 + CHANGELOG.zh-CN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 910d2283049..30a29dcd39b 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -8,6 +8,7 @@ ### Feats - `n-tree` supports keyboard operations. +- Add `n-tree-select` component. ### Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index e6e0c7045ad..ca3adbef103 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -8,6 +8,7 @@ ### Feats - `n-tree` 支持键盘操作 +- 新增 `n-tree-select` 组件 ### Fixes From 88f487494e6e2677658108bdd1c103c06f1466da Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 23:30:11 +0800 Subject: [PATCH 17/29] docs(tree-select) --- .../demos/enUS/index.demo-entry.md | 37 +++++++++++++++++++ .../demos/zhCN/index.demo-entry.md | 37 +++++++++++++++++++ src/tree-select/src/TreeSelect.tsx | 5 ++- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/tree-select/demos/enUS/index.demo-entry.md b/src/tree-select/demos/enUS/index.demo-entry.md index 0cb4ff8bb1d..1fc331eecfb 100644 --- a/src/tree-select/demos/enUS/index.demo-entry.md +++ b/src/tree-select/demos/enUS/index.demo-entry.md @@ -7,3 +7,40 @@ It's said that 99% of the people can't distinguish it from cascader. ```demo basic ``` + +## API + +### TreeSelect Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| cascade | `boolean` | `false` | Whether to do cascade check when use checkboxes. | +| checkable | `boolean` | `false` | Whether to use checkbox to select value. | +| clearable | `boolean` | `false` | Whether it's clearable. | +| consistent-menu-width | `boolean` | `true` | Whether to make menu's width consistent with input. Set to `true` will disable virtual scroll. | +| default-value | `string \| number \| Array \| null` | `null` | Selected key (or keys when multiple) by default. | +| default-expand-all | `boolean` | `false` | Expand all nodes by default. | +| default-expanded-keys | `Array` | `[]` | Expanded keys by default. | +| disabled | `boolean` | `false` | Whether to disable the tree select. | +| expanded-keys | `Array` | `undefined` | Expanded keys. | +| filterable | `boolean` | `false` | Whether the tree select is disabled. | +| filter | `(pattern: string, option: TreeOption) => boolean` | - | Filter function. | +| max-tag-count | `number \| 'responsive'` | `undefined` | Max tag count to be shown in multiple mode. Set to `'responsive'` will keep all tags in the same row. | +| multiple | `boolean` | `false` | Whether to support multiple select. | +| options | `TreeOption[]` | `[]` | Options. | +| placeholder | `string` | `'Please Select'` | Placeholder. | +| value | `string \| number \| Array \| null>` | `undefined` | Selected key (or keys when multiple). | +| virtual-scroll | `boolean` | `true` | Whether to enable virtual scroll. | +| on-blur | `(e: FocusEvent) => void` | `undefined` | Callback on blur. | +| on-update:expanded-keys | `(value: Array) => void` | `undefined` | Callback on expanded keys updated. | +| on-focus | `(e: FocusEvent) => void` | `undefined` | Callback on focus. | +| on-update:value | `(value: string \| number \| Array \| null) => void` | `undefined` | Callback on value updated. | + +### TreeOption Properties + +| Name | Type | Description | +| --------- | ------------------ | ------------------------------------ | +| key | `string \| number` | Key of the option. Should be unique. | +| label | `string` | Displayed content of the option. | +| children? | `TreeOption[]` | Child options of the option. | +| disabled? | `boolean` | Whether to disabled the option. | diff --git a/src/tree-select/demos/zhCN/index.demo-entry.md b/src/tree-select/demos/zhCN/index.demo-entry.md index 6c42ac5e48a..7c93f3a53c1 100644 --- a/src/tree-select/demos/zhCN/index.demo-entry.md +++ b/src/tree-select/demos/zhCN/index.demo-entry.md @@ -7,3 +7,40 @@ ```demo basic ``` + +## API + +### TreeSelect Props + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| cascade | `boolean` | `false` | 使用 checkbox 进行多选时是否级联 | +| checkable | `boolean` | `false` | 是否使用 checkbox 进行选择 | +| clearable | `boolean` | `false` | 是否可清除 | +| consistent-menu-width | `boolean` | `true` | 是否使菜单宽度和输入框一致,打开会禁用虚拟滚动 | +| default-value | `string \| number \| Array \| null` | `null` | 默认选中的 key | +| default-expand-all | `boolean` | `false` | 默认展开全部 | +| default-expanded-keys | `Array` | `[]` | 默认展开节点的 key | +| disabled | `boolean` | `false` | 是否禁用 | +| expanded-keys | `Array` | `undefined` | 展开节点的 key | +| filterable | `boolean` | `false` | 是否可过滤 | +| filter | `(pattern: string, option: TreeOption) => boolean` | - | 过滤器函数 | +| max-tag-count | `number \| 'responsive'` | `undefined` | 多选时最多直接显示多少选项,设为 `'responsive'` 会保证最多一行 | +| multiple | `boolean` | `false` | 是否支持多选 | +| options | `TreeOption[]` | `[]` | 选项 | +| placeholder | `string` | `'请选择'` | 占位信息 | +| value | `string \| number \| Array \| null>` | `undefined` | 选中的 key | +| virtual-scroll | `boolean` | `true` | 是否开启虚拟滚动 | +| on-blur | `(e: FocusEvent) => void` | `undefined` | Blur 时的回调 | +| on-update:expanded-keys | `(value: Array) => void` | `undefined` | 展开节点更新的回调 | +| on-focus | `(e: FocusEvent) => void` | `undefined` | Focus 时的回调 | +| on-update:value | `(value: string \| number \| Array \| null) => void` | `undefined` | 更新值的回调 | + +### TreeOption Properties + +| 名称 | 类型 | 说明 | +| --------- | ------------------ | -------------------- | +| key | `string \| number` | 选项的 key,需要唯一 | +| label | `string` | 选项的显示内容 | +| children? | `TreeOption[]` | 节点的子选项 | +| disabled? | `boolean` | 是否禁用选项 | diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index 0a159e59829..bac77aa6761 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -625,7 +625,10 @@ export default defineComponent({ checkable={checkable} cascade={this.mergedCascade} multiple={this.multiple} - virtualScroll={this.virtualScroll} + virtualScroll={ + !this.consistentMenuWidth && + this.virtualScroll + } internalDataTreeMate={this.dataTreeMate} internalDisplayTreeMate={this.displayTreeMate} internalHighlightKeySet={ From 82ba5dc2d3b4b124b8feb662c5656c719ad51593 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 20 Jun 2021 23:57:23 +0800 Subject: [PATCH 18/29] feat(tree-select): stricter type --- .../demos/enUS/index.demo-entry.md | 18 ++++++------ .../demos/zhCN/index.demo-entry.md | 18 ++++++------ src/tree-select/index.ts | 2 ++ src/tree-select/src/TreeSelect.tsx | 18 +++++++----- src/tree-select/src/interface.ts | 9 ++++++ src/tree-select/src/utils.ts | 21 ++++++++------ src/tree-select/tests/TreeSelect.spec.ts | 26 ++++++++++++++++- src/tree/demos/enUS/index.demo-entry.md | 26 ++++++++--------- src/tree/demos/zhCN/index.demo-entry.md | 26 ++++++++--------- src/tree/index.ts | 1 + src/tree/src/interface.ts | 4 ++- src/tree/tests/Tree.spec.ts | 29 +++++++++++++++++++ 12 files changed, 136 insertions(+), 62 deletions(-) diff --git a/src/tree-select/demos/enUS/index.demo-entry.md b/src/tree-select/demos/enUS/index.demo-entry.md index 1fc331eecfb..25df8bcefe8 100644 --- a/src/tree-select/demos/enUS/index.demo-entry.md +++ b/src/tree-select/demos/enUS/index.demo-entry.md @@ -24,10 +24,10 @@ basic | disabled | `boolean` | `false` | Whether to disable the tree select. | | expanded-keys | `Array` | `undefined` | Expanded keys. | | filterable | `boolean` | `false` | Whether the tree select is disabled. | -| filter | `(pattern: string, option: TreeOption) => boolean` | - | Filter function. | +| filter | `(pattern: string, option: TreeSelectOption) => boolean` | - | Filter function. | | max-tag-count | `number \| 'responsive'` | `undefined` | Max tag count to be shown in multiple mode. Set to `'responsive'` will keep all tags in the same row. | | multiple | `boolean` | `false` | Whether to support multiple select. | -| options | `TreeOption[]` | `[]` | Options. | +| options | `TreeSelectOption[]` | `[]` | Options. | | placeholder | `string` | `'Please Select'` | Placeholder. | | value | `string \| number \| Array \| null>` | `undefined` | Selected key (or keys when multiple). | | virtual-scroll | `boolean` | `true` | Whether to enable virtual scroll. | @@ -36,11 +36,11 @@ basic | on-focus | `(e: FocusEvent) => void` | `undefined` | Callback on focus. | | on-update:value | `(value: string \| number \| Array \| null) => void` | `undefined` | Callback on value updated. | -### TreeOption Properties +### TreeSelectOption Properties -| Name | Type | Description | -| --------- | ------------------ | ------------------------------------ | -| key | `string \| number` | Key of the option. Should be unique. | -| label | `string` | Displayed content of the option. | -| children? | `TreeOption[]` | Child options of the option. | -| disabled? | `boolean` | Whether to disabled the option. | +| Name | Type | Description | +| --------- | -------------------- | ------------------------------------ | +| key | `string \| number` | Key of the option. Should be unique. | +| label | `string` | Displayed content of the option. | +| children? | `TreeSelectOption[]` | Child options of the option. | +| disabled? | `boolean` | Whether to disabled the option. | diff --git a/src/tree-select/demos/zhCN/index.demo-entry.md b/src/tree-select/demos/zhCN/index.demo-entry.md index 7c93f3a53c1..2d5ef5649cd 100644 --- a/src/tree-select/demos/zhCN/index.demo-entry.md +++ b/src/tree-select/demos/zhCN/index.demo-entry.md @@ -24,10 +24,10 @@ basic | disabled | `boolean` | `false` | 是否禁用 | | expanded-keys | `Array` | `undefined` | 展开节点的 key | | filterable | `boolean` | `false` | 是否可过滤 | -| filter | `(pattern: string, option: TreeOption) => boolean` | - | 过滤器函数 | +| filter | `(pattern: string, option: TreeSelectOption) => boolean` | - | 过滤器函数 | | max-tag-count | `number \| 'responsive'` | `undefined` | 多选时最多直接显示多少选项,设为 `'responsive'` 会保证最多一行 | | multiple | `boolean` | `false` | 是否支持多选 | -| options | `TreeOption[]` | `[]` | 选项 | +| options | `TreeSelectOption[]` | `[]` | 选项 | | placeholder | `string` | `'请选择'` | 占位信息 | | value | `string \| number \| Array \| null>` | `undefined` | 选中的 key | | virtual-scroll | `boolean` | `true` | 是否开启虚拟滚动 | @@ -36,11 +36,11 @@ basic | on-focus | `(e: FocusEvent) => void` | `undefined` | Focus 时的回调 | | on-update:value | `(value: string \| number \| Array \| null) => void` | `undefined` | 更新值的回调 | -### TreeOption Properties +### TreeSelectOption Properties -| 名称 | 类型 | 说明 | -| --------- | ------------------ | -------------------- | -| key | `string \| number` | 选项的 key,需要唯一 | -| label | `string` | 选项的显示内容 | -| children? | `TreeOption[]` | 节点的子选项 | -| disabled? | `boolean` | 是否禁用选项 | +| 名称 | 类型 | 说明 | +| --------- | -------------------- | -------------------- | +| key | `string \| number` | 选项的 key,需要唯一 | +| label | `string` | 选项的显示内容 | +| children? | `TreeSelectOption[]` | 节点的子选项 | +| disabled? | `boolean` | 是否禁用选项 | diff --git a/src/tree-select/index.ts b/src/tree-select/index.ts index c10f8df6af8..c819a785ee4 100644 --- a/src/tree-select/index.ts +++ b/src/tree-select/index.ts @@ -1 +1,3 @@ export { default as NTreeSelect } from './src/TreeSelect' +export type { TreeSelectProps } from './src/TreeSelect' +export type { TreeSelectOption } from './src/interface' diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index bac77aa6761..30502820cc4 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -22,7 +22,7 @@ import { import { useIsMounted, useMergedState } from 'vooks' import { clickoutside } from 'vdirs' import { createTreeMate } from 'treemate' -import { TreeOptions, Key, InternalTreeInst } from '../../tree/src/interface' +import { Key, InternalTreeInst } from '../../tree/src/interface' import type { SelectBaseOption } from '../../select/src/interface' import { treeMateOptions, treeSharedProps } from '../../tree/src/Tree' import { @@ -41,12 +41,13 @@ import { useAdjustedTo } from '../../_utils' import { treeSelectLight, TreeSelectTheme } from '../styles' -import { +import type { OnUpdateValue, OnUpdateValueImpl, - treeSelectInjectionKey, + TreeSelectOption, Value } from './interface' +import { treeSelectInjectionKey } from './interface' import { treeOption2SelectOption, filterTree } from './utils' import style from './styles/index.cssr' @@ -75,7 +76,7 @@ const props = { maxTagCount: [String, Number] as PropType, multiple: Boolean, options: { - type: Array as PropType, + type: Array as PropType, default: () => [] }, placeholder: String, @@ -139,7 +140,7 @@ export default defineComponent({ const mergedShowRef = useMergedState(controlledShowRef, uncontrolledShowRef) const patternRef = ref('') const filteredTreeInfoRef = computed<{ - filteredTree: TreeOptions + filteredTree: TreeSelectOption[] highlightKeySet: Set | undefined }>(() => { if (!props.filterable) { @@ -159,10 +160,13 @@ export default defineComponent({ }) // used to resolve selected options const dataTreeMateRef = computed(() => - createTreeMate(props.options, treeMateOptions) + createTreeMate(props.options, treeMateOptions) ) const displayTreeMateRef = computed(() => - createTreeMate(filteredTreeInfoRef.value.filteredTree, treeMateOptions) + createTreeMate( + filteredTreeInfoRef.value.filteredTree, + treeMateOptions + ) ) const { value: initMergedValue } = mergedValueRef const pendingNodeKeyRef = ref( diff --git a/src/tree-select/src/interface.ts b/src/tree-select/src/interface.ts index 0558b093f93..081e7b5a3da 100644 --- a/src/tree-select/src/interface.ts +++ b/src/tree-select/src/interface.ts @@ -1,4 +1,13 @@ import { InjectionKey, Ref } from 'vue' +import { TreeOptionBase } from '../../tree/src/interface' + +export type TreeSelectOption = Omit< +TreeOptionBase, +'checkboxDisabled' | 'isLeaf' | 'children' +> & { + children?: TreeSelectOption[] + [k: string]: unknown +} export type OnUpdateValue = ( value: string & diff --git a/src/tree-select/src/utils.ts b/src/tree-select/src/utils.ts index f1c66fe9953..a4ed35f3b3f 100644 --- a/src/tree-select/src/utils.ts +++ b/src/tree-select/src/utils.ts @@ -1,7 +1,10 @@ import { SelectBaseOption } from '../../select/src/interface' -import { TreeOption, TreeOptions, Key } from '../../tree/src/interface' +import { Key } from '../../tree/src/interface' +import { TreeSelectOption } from './interface' -export function treeOption2SelectOption (treeOpt: TreeOption): SelectBaseOption { +export function treeOption2SelectOption ( + treeOpt: TreeSelectOption +): SelectBaseOption { return { ...treeOpt, value: treeOpt.key @@ -9,19 +12,19 @@ export function treeOption2SelectOption (treeOpt: TreeOption): SelectBaseOption } export function filterTree ( - tree: TreeOptions, - filter: (pattern: string, v: TreeOption) => boolean, + tree: TreeSelectOption[], + filter: (pattern: string, v: TreeSelectOption) => boolean, pattern: string ): { - filteredTree: TreeOptions + filteredTree: TreeSelectOption[] highlightKeySet: Set } { const visitedTailKeys = new Set() const visitedNonTailKeys = new Set() const highlightKeySet = new Set() - const filteredTree: TreeOptions = [] - const path: TreeOptions = [] - function visit (t: TreeOptions): void { + const filteredTree: TreeSelectOption[] = [] + const path: TreeSelectOption[] = [] + function visit (t: TreeSelectOption[]): void { t.forEach((n) => { path.push(n) if (filter(pattern, n)) { @@ -46,7 +49,7 @@ export function filterTree ( }) } visit(tree) - function build (t: TreeOptions, sibs: TreeOptions): void { + function build (t: TreeSelectOption[], sibs: TreeSelectOption[]): void { t.forEach((n) => { const { key } = n const isVisitedTail = visitedTailKeys.has(key) diff --git a/src/tree-select/tests/TreeSelect.spec.ts b/src/tree-select/tests/TreeSelect.spec.ts index 126ab2f9279..93db6dcede9 100644 --- a/src/tree-select/tests/TreeSelect.spec.ts +++ b/src/tree-select/tests/TreeSelect.spec.ts @@ -1,8 +1,32 @@ import { mount } from '@vue/test-utils' -import { NTreeSelect } from '../index' +import { NTreeSelect, TreeSelectOption } from '../index' describe('n-tree-select', () => { it('should work with import on demand', () => { mount(NTreeSelect) }) + it('should accept proper options', () => { + mount(NTreeSelect, { + props: { + options: [ + { + label: '1', + key: '1' + } + ] + } + }) + const options: TreeSelectOption[] = [ + { + label: '1', + key: '1', + gogogo: '12' + } + ] + mount(NTreeSelect, { + props: { + options + } + }) + }) }) diff --git a/src/tree/demos/enUS/index.demo-entry.md b/src/tree/demos/enUS/index.demo-entry.md index bebb235623e..8a8128f8151 100644 --- a/src/tree/demos/enUS/index.demo-entry.md +++ b/src/tree/demos/enUS/index.demo-entry.md @@ -17,7 +17,9 @@ async disabled ``` -## Props +## API + +### Tree Props | Name | Type | default | Description | | --- | --- | --- | --- | @@ -28,7 +30,7 @@ disabled | cascade | `boolean` | `false` | Whether to cascade checkboxes. | | checkable | `boolean` | `false` | | | checked-keys | `Array` | `undefined` | If set, checked status will work in controlled manner. | -| data | `Array` | `[]` | The node data of the tree. Reset `data` will cause clearing of some uncontrolled status. If you need to modify data, you'd better make tree work in a controlled manner. | +| data | `Array` | `[]` | The node data of the tree. Reset `data` will cause clearing of some uncontrolled status. If you need to modify data, you'd better make tree work in a controlled manner. | | default-checked-keys | `Array` | `[]` | | | default-expand-all | `boolean` | `false` | | | default-expanded-keys | `Array` | `[]` | | @@ -36,32 +38,30 @@ disabled | draggable | `boolean` | `false` | | | expand-on-dragenter | `boolean` | `true` | Whether to expand nodes after dragenter. | | expanded-keys | `Array` | `undefined` | If set, expanded status will work in controlled manner. | -| filter | `(node: TreeNode) => boolean` | A simple string based filter | | +| filter | `(node: TreeOption) => boolean` | A simple string based filter | | | multiple | `boolean` | `false` | | -| on-load | `(node: TreeNode) => Promise` | `undefined` | | +| on-load | `(node: TreeOption) => Promise` | `undefined` | | | pattern | `string` | `''` | | | remote | `boolean` | `false` | Whether to load nodes async. It should work with `on-load` | | selectable | `boolean` | `true` | | | selected-keys | `Array` | `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. | -| on-dragend | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragenter | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragleave | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragstart | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-drop | `(data: { node: TreeNode, dragNode: TreeNode, dropPosition: 'before' \| 'inside' \| 'after', event: DragEvent }) => void` | `undefined` | | +| on-dragend | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragenter | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragleave | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragstart | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-drop | `(data: { node: TreeOption, dragNode: TreeOption, dropPosition: 'before' \| 'inside' \| 'after', event: DragEvent }) => void` | `undefined` | | | on-update:checked-keys | `(keys: Array) => void` | `undefined` | | | on-update:expanded-keys | `(keys: Array) => void` | `undefined` | | | on-update:selected-keys | `(keys: Array) => void` | `undefined` | | -## API - -### TreeNode Properties +### TreeOption Properties | Name | Type | Description | | --- | --- | --- | | key | `string \| number` | Key of the node, should be unique. | | label | `string` | Label of the node. | | checkboxDisabled? | `boolean` | Whether the checkbox is disabled. | -| children? | `TreeNode[]` | Child nodes of the node. | +| children? | `TreeOption[]` | Child nodes of the node. | | disabled? | `boolean` | Whether the node is disabled. | | isLeaf? | `boolean` | Whether the node is leaf. Required in remote mode. | diff --git a/src/tree/demos/zhCN/index.demo-entry.md b/src/tree/demos/zhCN/index.demo-entry.md index 61b03472596..2fdc63d3816 100644 --- a/src/tree/demos/zhCN/index.demo-entry.md +++ b/src/tree/demos/zhCN/index.demo-entry.md @@ -17,7 +17,9 @@ async disabled ``` -## Props +## API + +### Tree Props | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | @@ -28,7 +30,7 @@ disabled | cascade | `boolean` | `false` | 是否关联选项 | | checkable | `boolean` | `false` | | | checked-keys | `Array` | `undefined` | 如果设定则 checked 状态受控 | -| data | `Array` | `[]` | 树的节点数据。重新设置 data 会将一些非受控状态清空,如果你需要在使用中改动 data,最好以受控的方式控制树 | +| data | `Array` | `[]` | 树的节点数据。重新设置 data 会将一些非受控状态清空,如果你需要在使用中改动 data,最好以受控的方式控制树 | | default-checked-keys | `Array` | `[]` | | | default-expand-all | `boolean` | `false` | | | default-expanded-keys | `Array` | `[]` | | @@ -36,32 +38,30 @@ disabled | draggable | `boolean` | `false` | | | expand-on-dragenter | `boolean` | `true` | 是否在拖入后展开节点 | | expanded-keys | `Array` | `undefined` | 如果设定则展开受控 | -| filter | `(node: TreeNode) => boolean` | 一个简单的字符串过滤算法 | | +| filter | `(node: TreeOption) => boolean` | 一个简单的字符串过滤算法 | | | multiple | `boolean` | `false` | | -| on-load | `(node: TreeNode) => Promise` | `undefined` | | +| on-load | `(node: TreeOption) => Promise` | `undefined` | | | pattern | `string` | `''` | | | remote | `boolean` | `false` | 是否异步获取选项,和 onLoad 配合 | | selectable | `boolean` | `true` | | | selected-keys | `Array` | `undefined` | 如果设定则 selected 状态受控 | | virtual-scroll | `boolean` | `false` | 是否启用虚拟滚动,启用前你需要设定好树的高度样式 | -| on-dragend | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragenter | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragleave | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-dragstart | `(data: { node: TreeNode, event: DragEvent }) => void` | `undefined` | | -| on-drop | `(data: { node: TreeNode, dragNode: TreeNode, dropPosition: 'before' \| 'inside' \| 'after', event: DragEvent }) => void` | `undefined` | | +| on-dragend | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragenter | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragleave | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-dragstart | `(data: { node: TreeOption, event: DragEvent }) => void` | `undefined` | | +| on-drop | `(data: { node: TreeOption, dragNode: TreeOption, dropPosition: 'before' \| 'inside' \| 'after', event: DragEvent }) => void` | `undefined` | | | on-update:checked-keys | `(keys: Array) => void` | `undefined` | | | on-update:expanded-keys | `(keys: Array) => void` | `undefined` | | | on-update:selected-keys | `(keys: Array) => void` | `undefined` | | -## API - -### TreeNode Properties +### TreeOption Properties | 名称 | 类型 | 说明 | | --- | --- | --- | | key | `string \| number` | 节点的 key,需要唯一 | | label | `string` | 节点的内容 | | checkboxDisabled? | `boolean` | 是否禁用节点的 checkbox | -| children? | `TreeNode[]` | 节点的子节点 | +| children? | `TreeOption[]` | 节点的子节点 | | disabled? | `boolean` | 是否禁用节点 | | isLeaf? | `boolean` | 节点是否是叶节点,在 remote 模式下是必须的 | diff --git a/src/tree/index.ts b/src/tree/index.ts index 525c3f8b527..8435fb85086 100644 --- a/src/tree/index.ts +++ b/src/tree/index.ts @@ -1,2 +1,3 @@ export { default as NTree } from './src/Tree' export type { TreeProps } from './src/Tree' +export type { TreeOption } from './src/interface' diff --git a/src/tree/src/interface.ts b/src/tree/src/interface.ts index 94a692202b7..3a8e89d2377 100644 --- a/src/tree/src/interface.ts +++ b/src/tree/src/interface.ts @@ -5,7 +5,7 @@ import type { TreeTheme } from '../styles' export type Key = string | number -export interface TreeOption { +export interface TreeOptionBase { key: Key label: string checkboxDisabled?: boolean @@ -15,6 +15,8 @@ export interface TreeOption { suffix?: () => VNodeChild } +export type TreeOption = TreeOptionBase & { [k: string]: unknown } + export type TreeOptions = TreeOption[] export interface DragInfo { diff --git a/src/tree/tests/Tree.spec.ts b/src/tree/tests/Tree.spec.ts index e3e85a88cab..9e41e075703 100644 --- a/src/tree/tests/Tree.spec.ts +++ b/src/tree/tests/Tree.spec.ts @@ -5,4 +5,33 @@ describe('n-tree', () => { it('should work with import on demand', () => { mount(NTree) }) + it('should accepts proper options', () => { + mount(NTree, { + props: { + options: [ + { + label: '123', + key: '123', + children: [ + { + label: '123', + key: '123' + } + ] + } + ] + } + }) + mount(NTree, { + props: { + options: [ + { + label: '123', + key: '123', + unknown: 'unknown' + } + ] + } + }) + }) }) From d154595dbb6cf3c701c545f41ef5366792a0734c Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 00:00:09 +0800 Subject: [PATCH 19/29] fix: typo --- src/tree/tests/Tree.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree/tests/Tree.spec.ts b/src/tree/tests/Tree.spec.ts index 9e41e075703..eb9db5440aa 100644 --- a/src/tree/tests/Tree.spec.ts +++ b/src/tree/tests/Tree.spec.ts @@ -5,7 +5,7 @@ describe('n-tree', () => { it('should work with import on demand', () => { mount(NTree) }) - it('should accepts proper options', () => { + it('should accept proper options', () => { mount(NTree, { props: { options: [ From 4e2661cc8b51985e9ae26fa958c1532f6b649475 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 12:45:13 +0800 Subject: [PATCH 20/29] fix(select): input blinks in filterable mode when click at menu and input has value --- src/select/src/Select.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/select/src/Select.tsx b/src/select/src/Select.tsx index e09b5d2f514..cfa0436acec 100644 --- a/src/select/src/Select.tsx +++ b/src/select/src/Select.tsx @@ -540,9 +540,7 @@ export default defineComponent({ } } function handleMenuMousedown (e: MouseEvent): void { - if (!happensIn(e, 'action') && props.multiple && props.filterable) { - focusSelection() - } + if (!happensIn(e, 'action')) e.preventDefault() } // scroll events on menu function handleMenuScroll (e: Event): void { From e04eb5e75d8170db504a2af3a706de2c7072e3f9 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 12:45:54 +0800 Subject: [PATCH 21/29] fix(tree): multiple line style --- src/tree/src/Tree.tsx | 2 +- src/tree/src/styles/index.cssr.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index f17411b815d..f4e30e02de7 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -50,7 +50,7 @@ import style from './styles/index.cssr' // During expanding, some node are mis-applied with :active style // Async dnd has bug -const ITEM_SIZE = 30 +const ITEM_SIZE = 30 // 24 + 3 + 3 export const treeMateOptions = { getDisabled (node: TreeOption) { diff --git a/src/tree/src/styles/index.cssr.ts b/src/tree/src/styles/index.cssr.ts index fdfade7e964..f650f9919ae 100644 --- a/src/tree/src/styles/index.cssr.ts +++ b/src/tree/src/styles/index.cssr.ts @@ -181,11 +181,10 @@ export default cB('tree', ` cB('tree-node-content', ` position: relative; display: inline-flex; - height: 24px; + align-items: center; + min-height: 24px; box-sizing: border-box; - border-bottom: 3px solid #0000; - border-top: 3px solid #0000; - line-height: 24px; + line-height: 1.5; align-items: center; vertical-align: bottom; padding: 0 6px; @@ -203,7 +202,6 @@ export default cB('tree', ` marginBottom: 0 }), cE('text', ` - line-height: 1.25; border-bottom: 1px solid #0000; transition: border-color .3s var(--bezier); `) From 905e9661a94010b0cdf7073dea89dffff1eab43c Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 12:55:10 +0800 Subject: [PATCH 22/29] docs(tree-select) --- src/tree-select/demos/enUS/basic.demo.md | 146 ++++++++++++++--- src/tree-select/demos/enUS/checkbox.demo.md | 139 ++++++++++++++++ src/tree-select/demos/enUS/debug.demo.md | 48 ++++++ src/tree-select/demos/enUS/filterable.demo.md | 143 ++++++++++++++++ .../demos/enUS/index.demo-entry.md | 4 + src/tree-select/demos/enUS/multiple.demo.md | 138 ++++++++++++++++ src/tree-select/demos/zhCN/basic.demo.md | 154 ++++++++++++++---- src/tree-select/demos/zhCN/checkbox.demo.md | 139 ++++++++++++++++ src/tree-select/demos/zhCN/debug.demo.md | 48 ++++++ src/tree-select/demos/zhCN/filterable.demo.md | 143 ++++++++++++++++ .../demos/zhCN/index.demo-entry.md | 4 + src/tree-select/demos/zhCN/multiple.demo.md | 138 ++++++++++++++++ src/tree-select/src/TreeSelect.tsx | 9 +- 13 files changed, 1195 insertions(+), 58 deletions(-) create mode 100644 src/tree-select/demos/enUS/checkbox.demo.md create mode 100644 src/tree-select/demos/enUS/debug.demo.md create mode 100644 src/tree-select/demos/enUS/filterable.demo.md create mode 100644 src/tree-select/demos/enUS/multiple.demo.md create mode 100644 src/tree-select/demos/zhCN/checkbox.demo.md create mode 100644 src/tree-select/demos/zhCN/debug.demo.md create mode 100644 src/tree-select/demos/zhCN/filterable.demo.md create mode 100644 src/tree-select/demos/zhCN/multiple.demo.md diff --git a/src/tree-select/demos/enUS/basic.demo.md b/src/tree-select/demos/enUS/basic.demo.md index a4ea74dbb91..39bbd436bee 100644 --- a/src/tree-select/demos/enUS/basic.demo.md +++ b/src/tree-select/demos/enUS/basic.demo.md @@ -1,34 +1,134 @@ # Basic ```html - + ``` ```js -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) - } - }) -} - -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' -} +import { defineComponent } from 'vue' -export default { - options () { +export default defineComponent({ + setup () { return { - options: createData() + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] } } -} +}) ``` diff --git a/src/tree-select/demos/enUS/checkbox.demo.md b/src/tree-select/demos/enUS/checkbox.demo.md new file mode 100644 index 00000000000..8797c51843f --- /dev/null +++ b/src/tree-select/demos/enUS/checkbox.demo.md @@ -0,0 +1,139 @@ +# Use Checkbox + +```html + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/demos/enUS/debug.demo.md b/src/tree-select/demos/enUS/debug.demo.md new file mode 100644 index 00000000000..b6d5585fdb6 --- /dev/null +++ b/src/tree-select/demos/enUS/debug.demo.md @@ -0,0 +1,48 @@ +# Debug + +```html + + + Multiple + Checkable + Cascade + Filterable + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +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: key, + key, + children: createData(level - 1, key) + } + }) +} + +export default defineComponent({ + setup () { + return { + multiple: ref(false), + checkable: ref(false), + cascade: ref(false), + filterable: ref(false), + options: createData() + } + } +}) +``` diff --git a/src/tree-select/demos/enUS/filterable.demo.md b/src/tree-select/demos/enUS/filterable.demo.md new file mode 100644 index 00000000000..ac5b3bf6991 --- /dev/null +++ b/src/tree-select/demos/enUS/filterable.demo.md @@ -0,0 +1,143 @@ +# Filterable + +```html + + + + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/demos/enUS/index.demo-entry.md b/src/tree-select/demos/enUS/index.demo-entry.md index 25df8bcefe8..1e0b283838f 100644 --- a/src/tree-select/demos/enUS/index.demo-entry.md +++ b/src/tree-select/demos/enUS/index.demo-entry.md @@ -6,6 +6,10 @@ It's said that 99% of the people can't distinguish it from cascader. ```demo basic +multiple +checkbox +filterable +debug ``` ## API diff --git a/src/tree-select/demos/enUS/multiple.demo.md b/src/tree-select/demos/enUS/multiple.demo.md new file mode 100644 index 00000000000..a1c6df6ba13 --- /dev/null +++ b/src/tree-select/demos/enUS/multiple.demo.md @@ -0,0 +1,138 @@ +# Multiple + +```html + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/demos/zhCN/basic.demo.md b/src/tree-select/demos/zhCN/basic.demo.md index edf6de5e684..93b7ca96ee6 100644 --- a/src/tree-select/demos/zhCN/basic.demo.md +++ b/src/tree-select/demos/zhCN/basic.demo.md @@ -1,47 +1,133 @@ # 基础用法 ```html - - - Multiple - Checkable - Cascade - Filterable - - - + ``` ```js -import { defineComponent, ref } from 'vue' - -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: key, - key, - children: createData(level - 1, key) - } - }) -} +import { defineComponent } from 'vue' export default defineComponent({ setup () { return { - multiple: ref(false), - checkable: ref(false), - cascade: ref(false), - filterable: ref(false), - options: createData() + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] } } }) diff --git a/src/tree-select/demos/zhCN/checkbox.demo.md b/src/tree-select/demos/zhCN/checkbox.demo.md new file mode 100644 index 00000000000..7fe61e1dd68 --- /dev/null +++ b/src/tree-select/demos/zhCN/checkbox.demo.md @@ -0,0 +1,139 @@ +# 使用 Checkbox 选择 + +```html + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/demos/zhCN/debug.demo.md b/src/tree-select/demos/zhCN/debug.demo.md new file mode 100644 index 00000000000..b6d5585fdb6 --- /dev/null +++ b/src/tree-select/demos/zhCN/debug.demo.md @@ -0,0 +1,48 @@ +# Debug + +```html + + + Multiple + Checkable + Cascade + Filterable + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +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: key, + key, + children: createData(level - 1, key) + } + }) +} + +export default defineComponent({ + setup () { + return { + multiple: ref(false), + checkable: ref(false), + cascade: ref(false), + filterable: ref(false), + options: createData() + } + } +}) +``` diff --git a/src/tree-select/demos/zhCN/filterable.demo.md b/src/tree-select/demos/zhCN/filterable.demo.md new file mode 100644 index 00000000000..48593d97464 --- /dev/null +++ b/src/tree-select/demos/zhCN/filterable.demo.md @@ -0,0 +1,143 @@ +# 可过滤 + +```html + + + + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/demos/zhCN/index.demo-entry.md b/src/tree-select/demos/zhCN/index.demo-entry.md index 2d5ef5649cd..224499c773b 100644 --- a/src/tree-select/demos/zhCN/index.demo-entry.md +++ b/src/tree-select/demos/zhCN/index.demo-entry.md @@ -6,6 +6,10 @@ ```demo basic +multiple +checkbox +filterable +debug ``` ## API diff --git a/src/tree-select/demos/zhCN/multiple.demo.md b/src/tree-select/demos/zhCN/multiple.demo.md new file mode 100644 index 00000000000..a0e0a110bc9 --- /dev/null +++ b/src/tree-select/demos/zhCN/multiple.demo.md @@ -0,0 +1,138 @@ +# 多选 + +```html + +``` + +```js +import { defineComponent } from 'vue' + +export default defineComponent({ + setup () { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: + "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey" + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood' + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man' + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself' + }, + { + label: 'The Word', + key: 'The Word' + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true + }, + { + label: 'What goes on', + key: 'What goes on' + }, + { + label: 'Girl', + key: 'Girl' + }, + { + label: "I'm looking through you", + key: "I'm looking through you" + }, + { + label: 'In My Life', + key: 'In My Life' + }, + { + label: 'Wait', + key: 'Wait' + } + ] + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us' + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony' + }, + { + label: 'Across The Universe', + key: 'Across The Universe' + }, + { + label: 'I Me Mine', + key: 'I Me Mine' + }, + { + label: 'Dig It', + key: 'Dig It' + }, + { + label: 'Let It Be', + key: 'Let It Be' + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae' + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling" + }, + { + label: 'One After 909', + key: 'One After 909' + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road' + }, + { + label: 'For You Blue', + key: 'For You Blue' + }, + { + label: 'Get Back', + key: 'Get Back' + } + ] + } + ] + } + } +}) +``` diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index 30502820cc4..ac95005a725 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -455,6 +455,11 @@ export default defineComponent({ closeMenu() focusSelection() } + function handleMenuMousedown (e: MouseEvent): void { + // If there's an action slot later, we need to check if mousedown happens + // in action panel + e.preventDefault() + } provide(treeSelectInjectionKey, { pendingNodeKeyRef }) @@ -515,6 +520,7 @@ export default defineComponent({ handleKeydown, handleKeyup, handleTabOut, + handleMenuMousedown, cssVars: computed(() => { const { common: { cubicBezierEaseInOut }, @@ -606,6 +612,7 @@ export default defineComponent({ ref="menuElRef" style={this.cssVars as CSSProperties} tabindex={0} + onMousedown={this.handleMenuMousedown} onKeyup={this.handleKeyup} onKeydown={this.handleKeydown} onFocusin={this.handleMenuFocusin} @@ -630,7 +637,7 @@ export default defineComponent({ cascade={this.mergedCascade} multiple={this.multiple} virtualScroll={ - !this.consistentMenuWidth && + this.consistentMenuWidth && this.virtualScroll } internalDataTreeMate={this.dataTreeMate} From 77741d5fd38572aa95b605392fef6d6c73f88214 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 13:42:14 +0800 Subject: [PATCH 23/29] refactor(tree-select): disable tree expand animation on tree select --- src/tree-select/src/TreeSelect.tsx | 1 + src/tree/src/MotionWrapper.tsx | 3 ++- src/tree/src/Tree.tsx | 22 ++++++++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index ac95005a725..97b6af1c128 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -622,6 +622,7 @@ export default defineComponent({ {this.nodes.map((node) => ( diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index f4e30e02de7..25ef047fe3d 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -126,6 +126,10 @@ const treeProps = { type: Function as PropType, default: defaultAllowDrop }, + animated: { + type: Boolean, + default: true + }, virtualScroll: Boolean, onDragenter: [Function, Array] as PropType void>>, onDragleave: [Function, Array] as PropType void>>, @@ -300,6 +304,10 @@ export default defineComponent({ const aipRef = ref(false) // animation in progress const afNodeRef = ref>([]) // animation flattened nodes watch(mergedExpandedKeysRef, (value, prevValue) => { + if (!props.animated) { + void nextTick(syncScrollbar) + return + } const prevVSet = new Set(prevValue) let addedKey: Key | null = null let removedKey: Key | null = null @@ -390,16 +398,18 @@ export default defineComponent({ else return fNodesRef.value }) + function syncScrollbar (): void { + const { value: scrollbarInst } = scrollbarInstRef + if (scrollbarInst) scrollbarInst.sync() + } + function handleAfterEnter (): void { aipRef.value = false if (props.virtualScroll) { // If virtual scroll, we won't listen to resize during animation, so // resize callback of virtual list won't be called and as a result // scrollbar won't sync. We need to sync scrollbar manually. - void nextTick(() => { - const { value: scrollbarInst } = scrollbarInstRef - if (scrollbarInst) scrollbarInst.sync() - }) + void nextTick(syncScrollbar) } } @@ -881,10 +891,10 @@ export default defineComponent({ resetDndState() } function handleScroll (): void { - scrollbarInstRef.value?.sync() + syncScrollbar() } function handleResize (): void { - scrollbarInstRef.value?.sync() + syncScrollbar() } function handleFocusout (e: FocusEvent): void { if (props.virtualScroll || props.internalScrollable) { From f0019b298cbb549a6533d935d04302b261482e96 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 14:02:20 +0800 Subject: [PATCH 24/29] feat(tree-select): change expanded keys on filter --- design-notes/todo.md | 17 +++++++++++++++++ src/tree-select/src/TreeSelect.tsx | 23 +++++++++++++++++++++-- src/tree-select/src/utils.ts | 6 +++++- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 design-notes/todo.md diff --git a/design-notes/todo.md b/design-notes/todo.md new file mode 100644 index 00000000000..8e34e4c6ef8 --- /dev/null +++ b/design-notes/todo.md @@ -0,0 +1,17 @@ +# TODO + +## Tree + +- (moderate) Use set to check if node is checked or selected internally + +## DataTable + +- (moderate) Add fast path for virtual mode if no cell crosses rows + +## Tabs + +- (moderate) Add iOS styled tabs + +## TreeSelect + +- (moderate) Async diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index 97b6af1c128..45fcc1a2b22 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -142,18 +142,21 @@ export default defineComponent({ const filteredTreeInfoRef = computed<{ filteredTree: TreeSelectOption[] highlightKeySet: Set | undefined + expandedKeys: Key[] | undefined }>(() => { if (!props.filterable) { return { filteredTree: props.options, - highlightKeySet: undefined + highlightKeySet: undefined, + expandedKeys: undefined } } const { value: pattern } = patternRef if (!pattern.length || !props.filter) { return { filteredTree: props.options, - highlightKeySet: undefined + highlightKeySet: undefined, + expandedKeys: undefined } } return filterTree(props.options, props.filter, pattern) @@ -470,6 +473,22 @@ export default defineComponent({ if (!mergedShowRef.value) return void nextTick(syncPosition) }) + let memorizedExpandedKeys: Key[] | undefined + watch(patternRef, (value, oldValue) => { + if (!value.length) { + if (memorizedExpandedKeys !== undefined) { + doUpdateExpandedKeys(memorizedExpandedKeys) + } + } else { + if (!oldValue.length) { + memorizedExpandedKeys = mergedExpandedKeysRef.value + } + const { expandedKeys } = filteredTreeInfoRef.value + if (expandedKeys !== undefined) { + doUpdateExpandedKeys(expandedKeys) + } + } + }) const themeRef = useTheme( 'TreeSelect', 'TreeSelect', diff --git a/src/tree-select/src/utils.ts b/src/tree-select/src/utils.ts index a4ed35f3b3f..edbc0c98d44 100644 --- a/src/tree-select/src/utils.ts +++ b/src/tree-select/src/utils.ts @@ -17,11 +17,13 @@ export function filterTree ( pattern: string ): { filteredTree: TreeSelectOption[] + expandedKeys: Key[] highlightKeySet: Set } { const visitedTailKeys = new Set() const visitedNonTailKeys = new Set() const highlightKeySet = new Set() + const expandedKeys: Key[] = [] const filteredTree: TreeSelectOption[] = [] const path: TreeSelectOption[] = [] function visit (t: TreeSelectOption[]): void { @@ -62,6 +64,7 @@ export function filterTree ( sibs.push(n) } else { // It it is not visited path tail, use cloned node + expandedKeys.push(n.key) const clonedNode = { ...n, children: [] } sibs.push(clonedNode) build(children, clonedNode.children) @@ -74,6 +77,7 @@ export function filterTree ( build(tree, filteredTree) return { filteredTree, - highlightKeySet + highlightKeySet, + expandedKeys } } From 8ffd511f668150bcc8fc7b5106d533a9f1a6f28a Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 14:08:44 +0800 Subject: [PATCH 25/29] chore(tree): add comment about animation --- src/tree/src/Tree.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tree/src/Tree.tsx b/src/tree/src/Tree.tsx index 25ef047fe3d..e2ac6a1063c 100644 --- a/src/tree/src/Tree.tsx +++ b/src/tree/src/Tree.tsx @@ -301,8 +301,15 @@ export default defineComponent({ } }) - const aipRef = ref(false) // animation in progress - const afNodeRef = ref>([]) // animation flattened nodes + // animation in progress + const aipRef = ref(false) + // animation flattened nodes + const afNodeRef = ref>([]) + // Note: Since the virtual list depends on min height, if there's a node + // whose height starts from 0, the virtual list will have a wrong height + // during animation. This will seldom cause wired scrollbar status. It is + // fixable and need some changes in vueuc, I've no time so I just leave it + // here. Maybe the bug won't be fixed during the life time of the project. watch(mergedExpandedKeysRef, (value, prevValue) => { if (!props.animated) { void nextTick(syncScrollbar) From 81401ff6850422bab68ada0422560821a86220d4 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 14:31:33 +0800 Subject: [PATCH 26/29] chore --- CHANGELOG.en-US.md | 2 +- src/menu/demos/enUS/render-label.demo.md | 2 +- src/menu/demos/zhCN/render-label.demo.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 30a29dcd39b..6d78230637e 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -3,7 +3,7 @@ ### Feats - `n-dropdown` add `on-clickoutside` prop, closes [#123](https://github.com/TuSimple/naive-ui/issues/123). -- `n-menu` add `renderLabel` prop, closes [#84](https://github.com/TuSimple/naive-ui/issues/84) +- `n-menu` add `render-label` prop, closes [#84](https://github.com/TuSimple/naive-ui/issues/84) ### Feats diff --git a/src/menu/demos/enUS/render-label.demo.md b/src/menu/demos/enUS/render-label.demo.md index 5f158607fd6..a29c79cb1e7 100644 --- a/src/menu/demos/enUS/render-label.demo.md +++ b/src/menu/demos/enUS/render-label.demo.md @@ -1,6 +1,6 @@ # Render Label -The `renderLabel` can be used to batch render menu options. +The `render-label` can be used to batch render menu options. ```html diff --git a/src/menu/demos/zhCN/render-label.demo.md b/src/menu/demos/zhCN/render-label.demo.md index c8d17d0f81f..c00a3e7213b 100644 --- a/src/menu/demos/zhCN/render-label.demo.md +++ b/src/menu/demos/zhCN/render-label.demo.md @@ -1,6 +1,6 @@ # 批量处理菜单渲染 -使用 `renderLabel` 可以批量控制菜单的选项渲染。 +使用 `render-label` 可以批量控制菜单的选项渲染。 ```html From 290cc37a21c05e5945e737217138a8a8e0705c7d Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 14:38:59 +0800 Subject: [PATCH 27/29] feat(tree-select): close on single select --- src/tree-select/src/TreeSelect.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tree-select/src/TreeSelect.tsx b/src/tree-select/src/TreeSelect.tsx index 45fcc1a2b22..31386c8fb96 100644 --- a/src/tree-select/src/TreeSelect.tsx +++ b/src/tree-select/src/TreeSelect.tsx @@ -338,6 +338,12 @@ export default defineComponent({ doUpdateValue(keys) } else { doUpdateValue(keys[0] ?? null) + closeMenu() + if (!props.filterable) { + // Currently it is not necessary. However if there is an action slot, + // it will be useful. So just leave it here. + focusSelection() + } } if (props.filterable) { focusSelectionInput() From 225d2d06d43be333c5937cb75d051e9bac1f7238 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 21 Jun 2021 15:20:58 +0800 Subject: [PATCH 28/29] fix(base-selection): input has useless empty row in multiple filterable mode --- CHANGELOG.en-US.md | 4 +--- CHANGELOG.zh-CN.md | 4 +--- src/_internal/selection/src/Selection.tsx | 15 +++++++++++++++ src/_internal/selection/src/styles/index.cssr.ts | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 6d78230637e..3e0471eb87d 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -4,9 +4,6 @@ - `n-dropdown` add `on-clickoutside` prop, closes [#123](https://github.com/TuSimple/naive-ui/issues/123). - `n-menu` add `render-label` prop, closes [#84](https://github.com/TuSimple/naive-ui/issues/84) - -### Feats - - `n-tree` supports keyboard operations. - Add `n-tree-select` component. @@ -15,6 +12,7 @@ - Fix `n-tree` drag over leaf node causes error, closes [#200](https://github.com/TuSimple/naive-ui/issues/200). - Fix `n-tree` misses `on-update-expanded-keys`, `on-update-selected-keys`, `on-update-checked-keys` prop. - Fix `n-tree`'s `selected-keys` prop influences original array. +- Fix `n-select`'s input has useless empty row in multiple filterable mode. ## 2.12.2 (2021-06-19) diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index ca3adbef103..5ec5ce7b55e 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -4,9 +4,6 @@ - `n-dropdown` 新增 `on-clickoutside` 属性,关闭 [#123](https://github.com/TuSimple/naive-ui/issues/123) - `n-menu` 新增 `render-label` 属性,关闭 [#84](https://github.com/TuSimple/naive-ui/issues/84) - -### Feats - - `n-tree` 支持键盘操作 - 新增 `n-tree-select` 组件 @@ -15,6 +12,7 @@ - 修复 `n-tree` 缺少 `on-update-expanded-keys`、`on-update-selected-keys`、`on-update-checked-keys` 属性 - 修复 `n-tree` 拖拽悬浮叶节点报错,关闭 [#200](https://github.com/TuSimple/naive-ui/issues/200) - 修复 `n-tree` 对 `selected-keys` 属性影响原数组 +- 修复 `n-select` 砸爱 multiple filterable 模式下输入框有无用的空行 ## 2.12.2 (2021-06-19) diff --git a/src/_internal/selection/src/Selection.tsx b/src/_internal/selection/src/Selection.tsx index ab04d868b92..86f7e3f7545 100644 --- a/src/_internal/selection/src/Selection.tsx +++ b/src/_internal/selection/src/Selection.tsx @@ -93,6 +93,7 @@ export default defineComponent({ const counterRef = ref(null) const counterWrapperRef = ref(null) const overflowRef = ref(null) + const inputTagElRef = ref(null) const showTagsPopoverRef = ref(false) const patternInputFocusedRef = ref(false) @@ -141,6 +142,17 @@ export default defineComponent({ } } } + function hideInputTag (): void { + const { value: inputTagEl } = inputTagElRef + if (inputTagEl) inputTagEl.style.display = 'none' + } + function showInputTag (): void { + const { value: inputTagEl } = inputTagElRef + if (inputTagEl) inputTagEl.style.display = 'inline-block' + } + watch(toRef(props, 'active'), (value) => { + if (!value) hideInputTag() + }) watch(toRef(props, 'pattern'), () => { if (props.multiple) { void nextTick(syncMirrorWidth) @@ -247,6 +259,7 @@ export default defineComponent({ function focusInput (): void { const { value: patternInputEl } = patternInputRef if (patternInputEl) { + showInputTag() patternInputEl.focus() } } @@ -316,6 +329,7 @@ export default defineComponent({ singleElRef, patternInputWrapperRef, overflowRef, + inputTagElRef, handleMouseDown, handleFocusin, handleClear, @@ -490,6 +504,7 @@ export default defineComponent({ const input = filterable ? (
Date: Mon, 21 Jun 2021 15:22:15 +0800 Subject: [PATCH 29/29] docs(tree-select): add clearable in filterable demo --- src/tree-select/demos/enUS/filterable.demo.md | 8 +++++++- src/tree-select/demos/zhCN/filterable.demo.md | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tree-select/demos/enUS/filterable.demo.md b/src/tree-select/demos/enUS/filterable.demo.md index ac5b3bf6991..e92ef266a7c 100644 --- a/src/tree-select/demos/enUS/filterable.demo.md +++ b/src/tree-select/demos/enUS/filterable.demo.md @@ -2,13 +2,19 @@ ```html - + ``` diff --git a/src/tree-select/demos/zhCN/filterable.demo.md b/src/tree-select/demos/zhCN/filterable.demo.md index 48593d97464..aa2cc0484b7 100644 --- a/src/tree-select/demos/zhCN/filterable.demo.md +++ b/src/tree-select/demos/zhCN/filterable.demo.md @@ -2,13 +2,19 @@ ```html - + ```