diff --git a/README.md b/README.md index a0b9fa1b..f4d52730 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,18 @@ ReactDOM.render( see placements.jsx Describes how the popup menus should be positioned + + itemIcon + ReactNode | (props: MenuItemProps) => ReactNode + + Specify the menu item icon. + + + expandIcon + ReactNode | (props: SubMenuProps & { isSubMenu: boolean }) => ReactNode + + Specify the menu item icon. + @@ -237,6 +249,12 @@ ReactDOM.render( + + itemIcon + ReactNode | (props: MenuItemProps) => ReactNode + + Specify the menu item icon. + @@ -313,6 +331,18 @@ ReactDOM.render( The offset of the popup submenu, in an x, y coordinate array. e.g.: `[0,15]` + + expandIcon + ReactNode | (props: SubMenuProps) => ReactNode + + Specify the menu item icon. + + + itemIcon + ReactNode | (props: SubMenuProps & { isSubMenu: boolean }) => ReactNode + + Specify the menu item icon. + diff --git a/examples/custom-icon.html b/examples/custom-icon.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/custom-icon.js b/examples/custom-icon.js new file mode 100644 index 00000000..015c5a56 --- /dev/null +++ b/examples/custom-icon.js @@ -0,0 +1,174 @@ +/* eslint no-console:0 */ +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import Menu, { SubMenu, Item as MenuItem, Divider } from 'rc-menu'; +import 'rc-menu/assets/index.less'; +import animate from 'css-animation'; + +const getSvgIcon = (style = {}, text) => { + if (text) { + return ( + + {text} + + ); + } + const path = 'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h' + + '-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v' + + '60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91' + + '.5c1.9 0 3.8-0.7 5.2-2L869 536.2c14.7-12.8 14.7-35.6 0-48.4z'; + return ( + + + + + + ); +}; + +function itemIcon(props) { + return getSvgIcon({ + position: 'absolute', + right: '1rem', + color: props.isSelected ? 'pink' : 'inherit', + }); +} + +function expandIcon(props) { + return getSvgIcon({ + position: 'absolute', + right: '1rem', + color: 'lightblue', + transform: `rotate(${props.isOpen ? 90 : 0}deg)`, + }); +} + +const animation = { + enter(node, done) { + let height; + return animate(node, 'rc-menu-collapse', { + start() { + height = node.offsetHeight; + node.style.height = 0; + }, + active() { + node.style.height = `${height}px`; + }, + end() { + node.style.height = ''; + done(); + }, + }); + }, + + appear() { + return this.enter.apply(this, arguments); + }, + + leave(node, done) { + return animate(node, 'rc-menu-collapse', { + start() { + node.style.height = `${node.offsetHeight}px`; + }, + active() { + node.style.height = 0; + }, + end() { + node.style.height = ''; + done(); + }, + }); + }, +}; + +class Demo extends React.Component { + + onOpenChange = (value) => { + console.log('onOpenChange', value); + } + + handleClick = (info) => { + console.log(`clicked ${info.key}`); + console.log(info); + } + + renderNestSubMenu = (props = {}) => { + return ( + offset sub menu 2} key="4" popupOffset={[10, 15]} {...props}> + inner inner + + sub menu 3} + > + + inner inner + inner inner2 + + inn + sub menu 4} key="4-2-2"> + inner inner + inner inner2 + + + inner inner + inner inner2 + + + + ); + } + + renderCommonMenu = (props = {}) => { + return ( + + sub menu} key="1"> + 0-1 + 0-2 + + {this.renderNestSubMenu()} + 1 + outer + disabled + outer3 + + ); + } + + render() { + const verticalMenu = this.renderCommonMenu({ + mode: 'vertical', + openAnimation: 'zoom', + itemIcon, + expandIcon, + }); + + const inlineMenu = this.renderCommonMenu({ + mode: 'inline', + defaultOpenKeys: ['1'], + openAnimation: animation, + itemIcon, + expandIcon, + }); + + return ( +
+

Antd menu - Custom icon

+
+

vertical

+
{verticalMenu}
+

inline

+
{inlineMenu}
+
+
+ ); + } +} + +ReactDOM.render(, document.getElementById('__react-content')); diff --git a/src/Menu.jsx b/src/Menu.jsx index 5d6d6df6..c3bcfa0d 100644 --- a/src/Menu.jsx +++ b/src/Menu.jsx @@ -32,6 +32,8 @@ class Menu extends React.Component { activeKey: PropTypes.string, prefixCls: PropTypes.string, builtinPlacements: PropTypes.object, + itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + expandIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; static defaultProps = { diff --git a/src/MenuItem.jsx b/src/MenuItem.jsx index beec9d6f..194caada 100644 --- a/src/MenuItem.jsx +++ b/src/MenuItem.jsx @@ -30,6 +30,7 @@ export class MenuItem extends React.Component { multiple: PropTypes.bool, isSelected: PropTypes.bool, manualRef: PropTypes.func, + itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; static defaultProps = { @@ -182,6 +183,10 @@ export class MenuItem extends React.Component { style.paddingLeft = props.inlineIndent * props.level; } menuAllProps.forEach(key => delete props[key]); + let icon = this.props.itemIcon; + if (typeof this.props.itemIcon === 'function') { + icon = React.createElement(this.props.itemIcon, this.props); + } return (
  • {props.children} + {icon}
  • ); } diff --git a/src/SubMenu.jsx b/src/SubMenu.jsx index d086587e..d3a6d84c 100644 --- a/src/SubMenu.jsx +++ b/src/SubMenu.jsx @@ -63,6 +63,8 @@ export class SubMenu extends React.Component { store: PropTypes.object, mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']), manualRef: PropTypes.func, + itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + expandIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; static defaultProps = { @@ -366,6 +368,8 @@ export class SubMenu extends React.Component { prefixCls: props.rootPrefixCls, id: this._menuId, manualRef: this.saveMenuInstance, + itemIcon: props.itemIcon, + expandIcon: props.expandIcon, }; const haveRendered = this.haveRendered; @@ -461,6 +465,18 @@ export class SubMenu extends React.Component { }; } + // expand custom icon should NOT be displayed in menu with horizontal mode. + let icon = null; + if (props.mode !== 'horizontal') { + icon = this.props.expandIcon; // ReactNode + if (typeof this.props.expandIcon === 'function') { + icon = React.createElement( + this.props.expandIcon, + { ...this.props } + ); + } + } + const title = (
    {props.title} - + {icon || }
    ); const children = this.renderChildren(props.children); diff --git a/src/SubPopupMenu.js b/src/SubPopupMenu.js index 8f96a77e..6ed02679 100644 --- a/src/SubPopupMenu.js +++ b/src/SubPopupMenu.js @@ -101,6 +101,8 @@ export class SubPopupMenu extends React.Component { triggerSubMenuAction: PropTypes.oneOf(['click', 'hover']), inlineIndent: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), manualRef: PropTypes.func, + itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + expandIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; static defaultProps = { @@ -286,6 +288,8 @@ export class SubPopupMenu extends React.Component { onDeselect: this.onDeselect, onSelect: this.onSelect, builtinPlacements: props.builtinPlacements, + itemIcon: childProps.itemIcon || this.props.itemIcon, + expandIcon: childProps.expandIcon || this.props.expandIcon, ...extraProps, }; if (props.mode === 'inline') { diff --git a/src/util.js b/src/util.js index eeaae638..51f8a1c9 100644 --- a/src/util.js +++ b/src/util.js @@ -104,4 +104,6 @@ export const menuAllProps = [ 'inlineCollapsed', 'menu', 'theme', + 'itemIcon', + 'expandIcon', ]; diff --git a/tests/MenuItem.spec.js b/tests/MenuItem.spec.js index d4d1cef1..b0b9a883 100644 --- a/tests/MenuItem.spec.js +++ b/tests/MenuItem.spec.js @@ -7,6 +7,40 @@ import Menu, { MenuItem, MenuItemGroup, SubMenu } from '../src'; import { MenuItem as NakedMenuItem } from '../src/MenuItem'; describe('MenuItem', () => { + const subMenuIconText = 'SubMenuIcon'; + const menuItemIconText = 'MenuItemIcon'; + function itemIcon() { + return {menuItemIconText}; + } + + function expandIcon() { + return {subMenuIconText}; + } + + describe('custom icon', () => { + it('should render custom arrow icon correctly.', () => { + const wrapper = mount( + + 1 + + ); + const menuItemText = wrapper.find('.rc-menu-item').first().text(); + expect(menuItemText).toEqual(`1${menuItemIconText}`); + }); + + it('should render custom arrow icon correctly (with children props).', () => { + const targetText = 'target'; + const wrapper = mount( + + {targetText}}>1 + 2 + + ); + const menuItemText = wrapper.find('.rc-menu-item').first().text(); + expect(menuItemText).toEqual(`1${targetText}`); + }); + }); + describe('disabled', () => { it('can not be active by key down', () => { const wrapper = mount( diff --git a/tests/SubMenu.spec.js b/tests/SubMenu.spec.js index 6accc365..79f9f2cc 100644 --- a/tests/SubMenu.spec.js +++ b/tests/SubMenu.spec.js @@ -29,6 +29,10 @@ describe('SubMenu', () => { ); } + function itemIcon() { + return MenuItemIcon; + } + it('don\'t show submenu when disabled', () => { const wrapper = mount( @@ -54,6 +58,58 @@ describe('SubMenu', () => { expect(popupAlign).toEqual({ offset: [0, 15] }); }); + it('should render custom arrow icon correctly.', () => { + const wrapper = mount( + SubMenuIconNode} + > + + 1 + 2 + + + ); + + const wrapperWithExpandIconFunction = mount( + SubMenuIconNode} + > + + 1 + 2 + + + ); + + const subMenuText = wrapper.find('.rc-menu-submenu-title').first().text(); + const subMenuTextWithExpandIconFunction = + wrapperWithExpandIconFunction.find('.rc-menu-submenu-title').first().text(); + expect(subMenuText).toEqual('submenuSubMenuIconNode'); + expect(subMenuTextWithExpandIconFunction).toEqual('submenuSubMenuIconNode'); + }); + + it('should Not render custom arrow icon in horizontal mode.', () => { + const wrapper = mount( + + SubMenuIconNode} + > + 1 + + + ); + + const childText = wrapper.find('.rc-menu-submenu-title').first().text(); + expect(childText).toEqual('submenu'); + }); + describe('openSubMenuOnMouseEnter and closeSubMenuOnMouseLeave are true', () => { it('toggles when mouse enter and leave', () => { const wrapper = mount(createMenu());