Browse Source

feat: render reactNode

yanzhuoran 1 year ago
parent
commit
f975c2dfb6

+ 289 - 0
packages/semi-ui/hotKeys/__test__/hotkeys.test.js

@@ -0,0 +1,289 @@
+import { Icon, Dropdown, Tag, HotKeys } from '../../index';
+import { string } from 'prop-types';
+import { noop, drop } from 'lodash';
+import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
+import {sleep} from "../../_test_/utils";
+
+function getHK(props) {
+    return mount(<HotKeys {...props}></HotKeys>, {
+        attachTo: document.getElementById('container'),
+    });
+}
+
+describe('HotKeys', () => {
+    beforeEach(() => {
+        document.body.innerHTML = '';
+        // Avoid `attachTo: document.body` Warning
+        const div = document.createElement('div');
+        div.setAttribute('id', 'container');
+        document.body.appendChild(div);
+    });
+
+    afterEach(() => {
+        const div = document.getElementById('container');
+        if (div) {
+            document.body.removeChild(div);
+        }
+    });
+
+    it('HotKeys-custom className & style', () => {
+        let props = {
+            className: 'test',
+            style: {
+                color: 'red',
+            },
+        };
+        const hotkeys = getHK(props);
+        expect(hotkeys.exists(`.${BASE_CLASS_PREFIX}-hotKeys.test`)).toEqual(true);
+        expect(hotkeys.find(`.${BASE_CLASS_PREFIX}-hotKeys`)).toHaveStyle('color', 'red');
+    });
+
+    // Dropdown can't find `.${BASE_CLASS_PREFIX}-portal` (can confirm it't existence through documenty.body.innerHTML)
+    // but find `.${BASE_CLASS_PREFIX}-portal-inner`
+
+    // 由于.${BASE_CLASS_PREFIX}-portal 不是有dropdown或其子元素的render函数渲染出来的dom,而是portal在constructore阶段通过createElement/appendChild 插入的,所以无法使用find来找
+    // 只能通过document.querySelector来获取
+    it('Dropdown-zIndex', () => {
+        let zIndex = 2000;
+        let props = {
+            visible: true,
+            trigger: 'custom',
+            zIndex: zIndex,
+        };
+        const dropdown = getDD(props);
+        expect(Number(document.querySelector(`.${BASE_CLASS_PREFIX}-portal`).style.zIndex)).toEqual(zIndex);
+    });
+
+    it('Dropdown-trigger-hover', async () => {
+        let props = {
+            trigger: 'hover',
+        };
+        const dropdown = getDD(props);
+        // Before hover, dropdown is not displayed
+        expect(dropdown.exists(el_portal_inner)).toEqual(false);
+        // After trigger, dropdown content will show
+        dropdown.find(`.${BASE_CLASS_PREFIX}-tag`).simulate('mouseEnter', {});
+        expect(dropdown.exists(el_portal_inner)).toEqual(true);
+        expect(dropdown.find(el_item)).toHaveLength(3);
+        await sleep(1000);
+        // auto hide
+        dropdown.find(`.${BASE_CLASS_PREFIX}-tag`).simulate('mouseLeave', {});
+        await sleep(1000);
+        expect(dropdown.exists(el_portal_inner)).toEqual(false);
+        expect(dropdown.find(el_item)).toHaveLength(0);
+    });
+
+    it('Dropdown-trigger-click', () => {
+        let props = {
+            trigger: 'click',
+        };
+        const dropdown = getDD(props);
+        // Before click
+        expect(dropdown.exists(el_portal_inner)).toEqual(false);
+        expect(dropdown.exists(el_item)).toEqual(false);
+        // After click
+        dropdown.find(`.${BASE_CLASS_PREFIX}-tag`).simulate('click', {});
+        expect(dropdown.exists(el_portal_inner)).toEqual(true);
+        expect(dropdown.find(el_item)).toHaveLength(3);
+    });
+
+    it('Dropdown-contentClassName', () => {
+        let props = {
+            contentClassName: 'test',
+            trigger: 'custom',
+            visible: true,
+        };
+        const dd = getDD(props);
+        expect(dd.exists(`.${BASE_CLASS_PREFIX}-dropdown.test`)).toEqual(true);
+    });
+
+    // TODO ??? visibleChange在Jest中没有被触发,实际上代码是work的
+    // it('Dropdown-onVisibleChange', () => {
+    //     let onVisibleChange = visible => {
+    //         debugger;
+    //     };
+    //     // let spyVisibleChange = sinon.spy(onVisibleChange);
+    //     let props = {
+    //         trigger: 'hover',
+    //         onVisibleChange: onVisibleChange,
+    //     };
+    //     const dropdown = getDD(props);
+    //     dropdown.find(`.${BASE_CLASS_PREFIX}-tag`).simulate('mouseEnter', {});
+    //     expect(spyVisibleChange.calledOnce).toBe(true);
+    //     expect(spyVisibleChange.calledWithMatch(true)).toBe(true);
+    //     dropdown.find(`.${BASE_CLASS_PREFIX}-tag`).simulate('mouseLeave', {});
+    //     expect(spyVisibleChange.calledWithMatch(false)).toBe(true);
+    // });
+
+    // it('Dropdown-clickToHide', () => {
+    //     let props = {
+    //         clickToHide: true,
+    //     };
+    // });
+
+    it('Dropdown-showTick', () => {
+        let items = [{ children: 'Item 1' }, { active: true, children: 'Item 2' }, { children: 'Item 3' }];
+        let props = {
+            showTick: true,
+            render: getSubMenu(items),
+            visible: true,
+            trigger: 'custom',
+        };
+        let DD = getDD(props);
+        expect(DD.find(`.${BASE_CLASS_PREFIX}-dropdown-item-withTick.${BASE_CLASS_PREFIX}-dropdown-item-active`).text()).toEqual('Item 2');
+    });
+
+    it('Dropdown.Item active', () => {
+        let items = [{ children: 'Item 1' }, { active: true, children: 'Item 2' }, { children: 'Item 3' }];
+        let props = {
+            render: getSubMenu(items),
+            visible: true,
+            trigger: 'custom',
+        };
+        let DD = getDD(props);
+        expect(DD.find(`.${BASE_CLASS_PREFIX}-dropdown-item-active`).text()).toEqual('Item 2');
+    });
+
+    it('Dropdown.Item type', () => {
+        let types = ['primary', 'secondary', 'tertiary', 'warning', 'danger'];
+        let items = types.map(type => {
+            return { type, children: `${type}Item` };
+        });
+        let props = {
+            render: getSubMenu(items),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        items.forEach(item => {
+            expect(DD.find(`.${BASE_CLASS_PREFIX}-dropdown-item-${item.type}`).text()).toEqual(`${item.children}`);
+        });
+    });
+
+    it('Dropdown.Item className & style', () => {
+        let items = [
+            { type: 'primary', children: 'primaryItem', className: 'primary-test', style: { color: 'red' } },
+            { type: 'secondary', children: 'secondaryItem' },
+        ];
+        let props = {
+            render: getSubMenu(items),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        expect(DD.find('li.primary-test')).toHaveStyle('color', 'red');
+    });
+
+    it('Dropdown.Item disabled', () => {
+        let items = [
+            { disabled: true, children: 'Item 1' },
+            { disabled: false, children: 'Item 2' },
+        ];
+        let props = {
+            render: getSubMenu(items),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        expect(DD.find(`.${BASE_CLASS_PREFIX}-dropdown-item-disabled`).text()).toEqual('Item 1');
+    });
+
+    it('Dropdown.Item onClick', () => {
+        let onClick = event => {};
+        let spyItemCLick = sinon.spy(onClick);
+        let items = [{ children: 'A' }, { children: 'B', onClick: spyItemCLick, className: 'test' }];
+        let props = {
+            render: getSubMenu(items),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        let targetItem = DD.find('li.test');
+        let event = {
+            button:0,
+            target: {
+                value: 'B1',
+            },
+        };
+        targetItem.simulate('click', event);
+        expect(spyItemCLick.calledOnce).toEqual(true);
+        expect(spyItemCLick.calledWithMatch(event)).toEqual(true);
+    });
+
+    it('Dropdown.Item onMouseEnter/onMouseLeave', () => {
+        let onMouseEnter = e => {};
+        let spyItemMouseEnter = sinon.spy(onMouseEnter);
+        let spyItemMouseLeave = sinon.spy(e => {});
+        let items = [
+            { children: 'A' },
+            { children: 'B', onMouseEnter: spyItemMouseEnter, onMouseLeave: spyItemMouseLeave, className: 'test' },
+        ];
+        let props = {
+            render: getSubMenu(items),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        let targetItem = DD.find('li.test');
+        let event = {
+            target: {
+                value: 'B1',
+            },
+        };
+        targetItem.simulate('mouseEnter', event);
+        expect(spyItemMouseEnter.calledOnce).toEqual(true);
+        expect(spyItemMouseEnter.calledWithMatch(event)).toEqual(true);
+        targetItem.simulate('mouseLeave', event);
+        expect(spyItemMouseLeave.calledOnce).toEqual(true);
+        expect(spyItemMouseLeave.calledWithMatch(event)).toEqual(true);
+    });
+
+    it('Dropdown.Title className & style', () => {
+        let props = {
+            render: (
+                <Dropdown.Menu>
+                    <Dropdown.Title className="test" style={{ margin: 5 }}>
+                        分组1
+                    </Dropdown.Title>
+                    <Dropdown.Item>primary</Dropdown.Item>
+                    <Dropdown.Item type="secondary">secondary</Dropdown.Item>
+                    <Dropdown.Divider />
+                    <Dropdown.Title>分组2</Dropdown.Title>
+                    <Dropdown.Item type="danger">danger</Dropdown.Item>
+                </Dropdown.Menu>
+            ),
+            trigger: 'custom',
+            visible: true,
+        };
+        let DD = getDD(props);
+        expect(DD.find('div.test')).toHaveStyle('margin', 5);
+    });
+
+
+    it('Dropdown array menu', () => {
+        const menu = [
+            { node: 'title', name: '分组1' },
+            { node: 'item', name: 'primary1', type: 'primary', onClick: () => console.log('click primary') },
+            { node: 'item', name: 'secondary', type: 'secondary' },
+            { node: 'divider', },
+            { node: 'title', name: '分组2' },
+            { node: 'item', name: 'tertiary', type: 'tertiary' },
+            { node: 'item', name: 'warning', type: 'warning', active: true },
+            { node: 'item', name: 'danger', type: 'danger' },
+        ];
+        let DD = mount(<Dropdown menu={menu} trigger="custom" visible ></Dropdown>, {
+            attachTo: document.getElementById('container'),
+        });
+        expect(DD.find('.semi-dropdown-menu').children().length).toEqual(menu.length);
+        const menu2 = [
+            { node: 'title', name: '分组1', iconType: 'menu' },
+            { node: 'item', name: 'secondary', type: 'secondary' },
+            { node: 'divider', },
+            { node: 'title', name: '分组2' },
+            { node: 'invalid node', name: '分组2' },
+        ];
+        DD.setProps({ menu: menu2 })
+        DD.update()
+        expect(DD.find('.semi-dropdown-menu').children().length).toEqual(menu2.length - 1);
+    });
+});

+ 30 - 7
packages/semi-ui/hotKeys/_story/hotKeys.stories.jsx

@@ -15,6 +15,19 @@ export const Demo = () => {
   return (
     <div>
       <HotKeys hotKeys={hotKeys} onClick={onClick}></HotKeys>
+      <pre>{cnt}</pre>
+    </div>
+  );
+}
+
+export const Clickable = () => {
+  const hotKeys = ["Alt","k"]
+  const [cnt, setCnt] = useState(0)
+  const onClick = () => {
+    setCnt(cnt+1)
+  }
+  return (
+    <div>
       <div>clickable</div>
       <HotKeys hotKeys={hotKeys} onClick={onClick} clickable={true}></HotKeys>
       <pre>{cnt}</pre>
@@ -22,11 +35,7 @@ export const Demo = () => {
   );
 }
 
-Demo.story = {
-  name: 'demo',
-};
-
-export const render = () => {
+export const renderButton = () => {
   const hotKeys = ["r"]
   const [cnt, setCnt] = useState(0)
   const onClick = () => {
@@ -42,8 +51,22 @@ export const render = () => {
   return (
     <div>
       <span>{" cnt:" + cnt}</span>
-      <HotKeys hotKeys={hotKeys} onClick={onClick} render={button}
-      ></HotKeys>
+      <HotKeys hotKeys={hotKeys} onClick={onClick} render={button} clickable></HotKeys>
+    </div>
+
+  );
+}
+
+export const renderNull = () => {
+  const hotKeys = ["r"]
+  const [cnt, setCnt] = useState(0)
+  const onClick = () => {
+    setCnt(cnt+1)
+  }
+  return (
+    <div>
+      <span>{" cnt:" + cnt}</span>
+      <HotKeys hotKeys={hotKeys} onClick={onClick} render={null} clickable></HotKeys>
     </div>
 
   );

+ 26 - 17
packages/semi-ui/hotKeys/index.tsx

@@ -1,4 +1,4 @@
-import React, { KeyboardEvent, ReactNode } from 'react';
+import React, { ReactNode } from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import HotKeysFoudation, { HotKeysAdapter } from '@douyinfe/semi-foundation/hotKeys/foundation';
@@ -15,7 +15,7 @@ export interface HotKeysProps {
     onClick?: () => void;
     clickable?: boolean;
     disabled?: boolean;
-    render?: () => ReactNode;
+    render?: () => ReactNode | ReactNode;
     getListenerTarget?: () => HTMLElement;
     className?: string;
     style?: React.CSSProperties
@@ -32,7 +32,7 @@ class HotKeys extends BaseComponent<HotKeysProps, HotKeysState> {
         onClick: PropTypes.func,
         clickable: PropTypes.bool,
         disabled: PropTypes.bool,
-        render: PropTypes.func,
+        render: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
         getListenerTarget: PropTypes.func,
         className: PropTypes.string,
         style: PropTypes.object,
@@ -44,7 +44,7 @@ class HotKeys extends BaseComponent<HotKeysProps, HotKeysState> {
         onClick: noop,
         clickable: false,
         disabled: false,
-        render: null,
+        render: undefined,
         getListenerTarget: () => document.body,
         className: '',
         style: null,
@@ -63,7 +63,6 @@ class HotKeys extends BaseComponent<HotKeysProps, HotKeysState> {
     }
 
     componentDidUpdate(_prevProps: HotKeysProps) {
-        
     }
 
     componentWillUnmount() {
@@ -87,34 +86,44 @@ class HotKeys extends BaseComponent<HotKeysProps, HotKeysState> {
         };
     }
 
-    
+
     render() {
         const { hotKeys, content, onClick, clickable, disabled, render, getListenerTarget, className, style, ...rest } = this.props;
-        
-        if (render !== null) {
-            return render();
+ 
+        if (typeof render !== 'undefined') {
+            if (render === null || (typeof render === 'function' && render() === null)) {
+                return null;
+            }
+            return (
+                <div 
+                    onClick={clickable ? onClick : noop}
+                    className={classNames(prefixCls, className)}
+                    style={style}>
+                    { typeof render === 'function' ? render() : render }
+                </div>
+            );
         }
-        const renderContent = content ?? hotKeys ;
-        
+        const renderContent = content ?? hotKeys;
+
         return (
-            <div 
+            <div
                 onClick={clickable ? onClick : noop}
                 className={classNames(prefixCls, className)}
                 style={style}
             >
-                { renderContent.map((key: KeyboardEvent["key"], index) => {
-                    return index === 0 ? 
+                {renderContent.map((key: KeyboardEvent["key"], index) => {
+                    return index === 0 ?
                         (<span key={index}>
                             <span className={prefixCls + '-content'}>{key}</span>
                         </span>)
-                        : 
+                        :
                         (<span key={index}>
                             <span className={prefixCls + '-split'}>+</span>
                             <span className={prefixCls + '-content'}>{key}</span>
                         </span>);
-                }) }
+                })}
             </div>
-        ); 
+        );
     }
 }