From b0678d79ee00dc01cf75dc3e639d6531cd5dc14a Mon Sep 17 00:00:00 2001
From: tangjinzhou <415800467@qq.com>
Date: Fri, 20 Aug 2021 23:21:49 +0800
Subject: [PATCH] refactor: tree-select

---
 components/tree/Tree.tsx                 | 102 +---------
 components/vc-select/OptionList.tsx      |   4 +-
 components/vc-tree-select/Context.tsx    |   1 +
 components/vc-tree-select/OptionList.tsx | 241 ++++++++++++++++++++++-
 components/vc-tree-select/props.ts       |   9 +-
 components/vc-tree/Tree.tsx              |   4 +-
 components/vc-tree/props.ts              |   2 +-
 7 files changed, 249 insertions(+), 114 deletions(-)

diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx
index 879546b86..8a668500c 100644
--- a/components/tree/Tree.tsx
+++ b/components/tree/Tree.tsx
@@ -123,6 +123,7 @@ export const treeProps = () => {
      */
     replaceFields: { type: Object as PropType<FieldNames> },
     blockNode: { type: Boolean, default: undefined },
+    openAnimation: PropTypes.any,
   };
 };
 
@@ -182,6 +183,7 @@ export default defineComponent({
         selectable,
         fieldNames,
         replaceFields,
+        motion = props.openAnimation,
       } = props;
       const newProps = {
         ...attrs,
@@ -197,6 +199,7 @@ export default defineComponent({
           itemHeight={20}
           virtual={virtual.value}
           {...newProps}
+          motion={motion}
           ref={treeRef}
           prefixCls={prefixCls.value}
           class={classNames(
@@ -226,103 +229,4 @@ export default defineComponent({
       );
     };
   },
-  // methods: {
-  //   renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) {
-  //     const { showLine } = this.$props;
-  //     if (loading) {
-  //       return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
-  //     }
-
-  //     if (isLeaf) {
-  //       return showLine ? <FileOutlined class={`${prefixCls}-switcher-line-icon`} /> : null;
-  //     }
-  //     const switcherCls = `${prefixCls}-switcher-icon`;
-  //     if (switcherIcon) {
-  //       return cloneElement(switcherIcon, {
-  //         class: switcherCls,
-  //       });
-  //     }
-  //     return showLine ? (
-  //       expanded ? (
-  //         <MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
-  //       ) : (
-  //         <PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
-  //       )
-  //     ) : (
-  //       <CaretDownFilled class={switcherCls} />
-  //     );
-  //   },
-  //   updateTreeData(treeData: TreeDataItem[]) {
-  //     const { $slots } = this;
-  //     const defaultFields = { children: 'children', title: 'title', key: 'key' };
-  //     const replaceFields = { ...defaultFields, ...this.$props.replaceFields };
-  //     return treeData.map(item => {
-  //       const key = item[replaceFields.key];
-  //       const children = item[replaceFields.children];
-  //       const { slots = {}, class: cls, style, ...restProps } = item;
-  //       const treeNodeProps = {
-  //         ...restProps,
-  //         icon: $slots[slots.icon] || restProps.icon,
-  //         switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon,
-  //         title: $slots[slots.title] || $slots.title || restProps[replaceFields.title],
-  //         dataRef: item,
-  //         key,
-  //         class: cls,
-  //         style,
-  //       };
-  //       if (children) {
-  //         return { ...treeNodeProps, children: this.updateTreeData(children) };
-  //       }
-  //       return treeNodeProps;
-  //     });
-  //   },
-  //   setTreeRef(node: VNode) {
-  //     this.tree = node;
-  //   },
-  //   handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) {
-  //     this.$emit('update:checkedKeys', checkedObj);
-  //     this.$emit('check', checkedObj, eventObj);
-  //   },
-  //   handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) {
-  //     this.$emit('update:expandedKeys', expandedKeys);
-  //     this.$emit('expand', expandedKeys, eventObj);
-  //   },
-  //   handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) {
-  //     this.$emit('update:selectedKeys', selectedKeys);
-  //     this.$emit('select', selectedKeys, eventObj);
-  //   },
-  // },
-  // render() {
-  //   const props = getOptionProps(this);
-  //   const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props;
-  //   const getPrefixCls = this.configProvider.getPrefixCls;
-  //   const prefixCls = getPrefixCls('tree', customizePrefixCls);
-  //   const switcherIcon = getComponent(this, 'switcherIcon');
-  //   const checkable = props.checkable;
-  //   let treeData = props.treeData || treeNodes;
-  //   if (treeData) {
-  //     treeData = this.updateTreeData(treeData);
-  //   }
-  //   const { class: className, ...restAttrs } = this.$attrs;
-  //   const vcTreeProps = {
-  //     ...props,
-  //     prefixCls,
-  //     checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
-  //     children: getSlot(this),
-  //     switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps),
-  //     ref: this.setTreeRef,
-  //     ...restAttrs,
-  //     class: classNames(className, {
-  //       [`${prefixCls}-icon-hide`]: !showIcon,
-  //       [`${prefixCls}-block-node`]: blockNode,
-  //     }),
-  //     onCheck: this.handleCheck,
-  //     onExpand: this.handleExpand,
-  //     onSelect: this.handleSelect,
-  //   } as Record<string, any>;
-  //   if (treeData) {
-  //     vcTreeProps.treeData = treeData;
-  //   }
-  //   return <VcTree {...vcTreeProps} />;
-  // },
 });
diff --git a/components/vc-select/OptionList.tsx b/components/vc-select/OptionList.tsx
index 634240f9e..750faed2f 100644
--- a/components/vc-select/OptionList.tsx
+++ b/components/vc-select/OptionList.tsx
@@ -18,8 +18,8 @@ import type { RawValueType, FlattenOptionsType } from './interface/generator';
 import useMemo from '../_util/hooks/useMemo';
 
 export interface RefOptionListProps {
-  onKeydown: KeyboardEvent;
-  onKeyup: KeyboardEvent;
+  onKeydown: (e?: KeyboardEvent) => void;
+  onKeyup: (e?: KeyboardEvent) => void;
   scrollTo?: (index: number) => void;
 }
 export interface OptionListProps {
diff --git a/components/vc-tree-select/Context.tsx b/components/vc-tree-select/Context.tsx
index 63275013d..52db64a55 100644
--- a/components/vc-tree-select/Context.tsx
+++ b/components/vc-tree-select/Context.tsx
@@ -12,6 +12,7 @@ import {
 
 interface ContextProps {
   checkable: boolean;
+  customCheckable: () => any;
   checkedKeys: Key[];
   halfCheckedKeys: Key[];
   treeExpandedKeys: Key[];
diff --git a/components/vc-tree-select/OptionList.tsx b/components/vc-tree-select/OptionList.tsx
index 2e81cc9ca..653b870d7 100644
--- a/components/vc-tree-select/OptionList.tsx
+++ b/components/vc-tree-select/OptionList.tsx
@@ -1,9 +1,14 @@
-import type { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface';
-import { SelectContext } from './Context';
+import type { DataNode, TreeDataNode, Key } from './interface';
+import { useInjectSelectContext } from './Context';
 import { RefOptionListProps } from '../vc-select/OptionList';
 import { ScrollTo } from '../vc-virtual-list/List';
-import { defineComponent } from 'vue';
+import { computed, defineComponent, nextTick, ref, watch } from 'vue';
 import { optionListProps } from './props';
+import useMemo from '../_util/hooks/useMemo';
+import { EventDataNode } from '../tree';
+import KeyCode from '../_util/KeyCode';
+import Tree from '../vc-tree/Tree';
+import { TreeProps } from '../vc-tree/props';
 
 const HIDDEN_STYLE = {
   width: 0,
@@ -26,6 +31,234 @@ type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollT
 
 export default defineComponent({
   name: 'OptionList',
-  props: optionListProps(),
+  inheritAttrs: false,
+  props: optionListProps<DataNode>(),
   slots: ['notFoundContent', 'menuItemSelectedIcon'],
+  expose: ['scrollTo', 'onKeydown', 'onKeyup'],
+  setup(props, { slots, expose }) {
+    const context = useInjectSelectContext();
+
+    const treeRef = ref();
+    const memoOptions = useMemo(
+      () => props.options,
+      [() => props.open, () => props.options],
+      (next, prev) => next[0] && prev[1] !== next[1],
+    );
+
+    const valueKeys = computed(() => {
+      const { checkedKeys, getEntityByValue } = context.value;
+      return checkedKeys.map(val => {
+        const entity = getEntityByValue(val);
+        return entity ? entity.key : null;
+      });
+    });
+
+    const mergedCheckedKeys = computed(() => {
+      const { checkable, halfCheckedKeys } = context.value;
+      if (!checkable) {
+        return null;
+      }
+
+      return {
+        checked: valueKeys.value,
+        halfChecked: halfCheckedKeys,
+      };
+    });
+
+    watch(
+      () => props.open,
+      () => {
+        nextTick(() => {
+          if (props.open && !props.multiple && valueKeys.value.length) {
+            treeRef.value?.scrollTo({ key: valueKeys[0] });
+          }
+        });
+      },
+      { immediate: true, flush: 'post' },
+    );
+
+    // ========================== Search ==========================
+    const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase());
+    const filterTreeNode = (treeNode: EventDataNode) => {
+      if (!lowerSearchValue.value) {
+        return false;
+      }
+      return String(treeNode[context.value.treeNodeFilterProp])
+        .toLowerCase()
+        .includes(lowerSearchValue.value);
+    };
+
+    // =========================== Keys ===========================
+    const expandedKeys = ref<Key[]>(context.value.treeDefaultExpandedKeys);
+    const searchExpandedKeys = ref<Key[]>(null);
+
+    watch(
+      () => props.searchValue,
+      () => {
+        if (props.searchValue) {
+          searchExpandedKeys.value = props.flattenOptions.map(o => o.key);
+        }
+      },
+      {
+        immediate: true,
+      },
+    );
+    const mergedExpandedKeys = computed(() => {
+      if (context.value.treeExpandedKeys) {
+        return [...context.value.treeExpandedKeys];
+      }
+      return props.searchValue ? searchExpandedKeys.value : expandedKeys.value;
+    });
+
+    const onInternalExpand = (keys: Key[]) => {
+      expandedKeys.value = keys;
+      searchExpandedKeys.value = keys;
+
+      context.value.onTreeExpand?.(keys);
+    };
+
+    // ========================== Events ==========================
+    const onListMouseDown = (event: MouseEvent) => {
+      event.preventDefault();
+    };
+
+    const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => {
+      const { getEntityByKey, checkable, checkedKeys } = context.value;
+      const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select');
+      if (entity !== null) {
+        props.onSelect?.(entity.data.value, {
+          selected: !checkedKeys.includes(entity.data.value),
+        });
+      }
+
+      if (!props.multiple) {
+        props.onToggleOpen?.(false);
+      }
+    };
+
+    // ========================= Keyboard =========================
+    const activeKey = ref<Key>(null);
+    const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value));
+
+    const setActiveKey = (key: Key) => {
+      activeKey.value = key;
+    };
+    expose({
+      scrollTo: treeRef.value?.scrollTo as ScrollTo,
+      onKeydown: (event: KeyboardEvent) => {
+        const { which } = event;
+        switch (which) {
+          // >>> Arrow keys
+          case KeyCode.UP:
+          case KeyCode.DOWN:
+          case KeyCode.LEFT:
+          case KeyCode.RIGHT:
+            treeRef.value?.onKeyDown(event);
+            break;
+
+          // >>> Select item
+          case KeyCode.ENTER: {
+            const { selectable, value } = activeEntity.value?.data.node || {};
+            if (selectable !== false) {
+              onInternalSelect(null, {
+                node: { key: activeKey.value },
+                selected: !context.value.checkedKeys.includes(value),
+              });
+            }
+            break;
+          }
+
+          // >>> Close
+          case KeyCode.ESC: {
+            props.onToggleOpen(false);
+          }
+        }
+      },
+      onKeyup: () => {},
+    } as ReviseRefOptionListProps);
+
+    return () => {
+      const {
+        prefixCls,
+        height,
+        itemHeight,
+        virtual,
+        multiple,
+        searchValue,
+        open,
+        notFoundContent = slots.notFoundContent?.(),
+        onMouseenter,
+      } = props;
+      const {
+        checkable,
+        treeDefaultExpandAll,
+        treeIcon,
+        showTreeIcon,
+        switcherIcon,
+        treeLine,
+        loadData,
+        treeLoadedKeys,
+        treeMotion,
+        onTreeLoad,
+      } = context.value;
+      // ========================== Render ==========================
+      if (memoOptions.value.length === 0) {
+        return (
+          <div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}>
+            {notFoundContent}
+          </div>
+        );
+      }
+
+      const treeProps: Partial<TreeProps> = {};
+      if (treeLoadedKeys) {
+        treeProps.loadedKeys = treeLoadedKeys;
+      }
+      if (mergedExpandedKeys) {
+        treeProps.expandedKeys = mergedExpandedKeys.value;
+      }
+
+      return (
+        <div onMousedown={onListMouseDown} onMouseenter={onMouseenter}>
+          {activeEntity && open && (
+            <span style={HIDDEN_STYLE} aria-live="assertive">
+              {activeEntity.value.data.value}
+            </span>
+          )}
+
+          <Tree
+            ref={treeRef}
+            focusable={false}
+            prefixCls={`${prefixCls}-tree`}
+            treeData={memoOptions.value as TreeDataNode[]}
+            height={height}
+            itemHeight={itemHeight}
+            virtual={virtual}
+            multiple={multiple}
+            icon={treeIcon}
+            showIcon={showTreeIcon}
+            switcherIcon={switcherIcon}
+            showLine={treeLine}
+            loadData={searchValue ? null : (loadData as any)}
+            motion={treeMotion}
+            // We handle keys by out instead tree self
+            checkable={checkable}
+            checkStrictly
+            checkedKeys={mergedCheckedKeys.value}
+            selectedKeys={!checkable ? valueKeys.value : []}
+            defaultExpandAll={treeDefaultExpandAll}
+            {...treeProps}
+            // Proxy event out
+            onActiveChange={setActiveKey}
+            onSelect={onInternalSelect}
+            onCheck={onInternalSelect as any}
+            onExpand={onInternalExpand}
+            onLoad={onTreeLoad}
+            filterTreeNode={filterTreeNode}
+            v-slots={{ checkable: context.value.customCheckable }}
+          />
+        </div>
+      );
+    };
+  },
 });
diff --git a/components/vc-tree-select/props.ts b/components/vc-tree-select/props.ts
index 7ed250429..f154d86da 100644
--- a/components/vc-tree-select/props.ts
+++ b/components/vc-tree-select/props.ts
@@ -33,9 +33,6 @@ export function optionListProps<OptionsType>() {
   };
 }
 
-class Helper<T> {
-  Return = optionListProps<T>();
-}
-type FuncReturnType<T> = Helper<T>['Return'];
-
-export type OptionListProps = Partial<ExtractPropTypes<FuncReturnType<DataNode>>>;
+export type OptionListProps = Partial<
+  Omit<ExtractPropTypes<ReturnType<typeof optionListProps>>, 'options'> & { options: DataNode[] }
+>;
diff --git a/components/vc-tree/Tree.tsx b/components/vc-tree/Tree.tsx
index af5ccfc5e..251c06176 100644
--- a/components/vc-tree/Tree.tsx
+++ b/components/vc-tree/Tree.tsx
@@ -986,7 +986,7 @@ export default defineComponent({
         checkable,
         checkStrictly,
         disabled,
-        openAnimation,
+        motion,
         loadData,
         filterTreeNode,
         height,
@@ -1063,7 +1063,7 @@ export default defineComponent({
               disabled={disabled}
               selectable={selectable}
               checkable={!!checkable}
-              motion={openAnimation}
+              motion={motion}
               dragging={dragging}
               height={height}
               itemHeight={itemHeight}
diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts
index 42d293f50..cacaa58e3 100644
--- a/components/vc-tree/props.ts
+++ b/components/vc-tree/props.ts
@@ -225,7 +225,7 @@ export const treeProps = () => ({
    */
   onActiveChange: { type: Function as PropType<(key: Key) => void> },
   filterTreeNode: { type: Function as PropType<(treeNode: EventDataNode) => boolean> },
-  openAnimation: PropTypes.any,
+  motion: PropTypes.any,
   switcherIcon: PropTypes.any,
 
   // Virtual List