diff --git a/examples/keyPath.js b/examples/keyPath.js index fa76a1bc..7779e24c 100644 --- a/examples/keyPath.js +++ b/examples/keyPath.js @@ -3,14 +3,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Menu, { SubMenu, Item as MenuItem } from 'rc-menu'; -import createReactClass from 'create-react-class'; import 'rc-menu/assets/index.less'; -const Test = createReactClass({ +class Test extends React.Component { onClick(info) { console.log('click ', info); - }, + } getMenu() { return ( @@ -33,14 +32,14 @@ const Test = createReactClass({ item3 ); - }, + } render() { return (
{this.getMenu()}
); - }, -}); + } +} ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/openKeys.js b/examples/openKeys.js index 13115bd4..6a27f912 100644 --- a/examples/openKeys.js +++ b/examples/openKeys.js @@ -3,27 +3,25 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Menu, { SubMenu, Item as MenuItem } from 'rc-menu'; -import createReactClass from 'create-react-class'; import 'rc-menu/assets/index.less'; -const Test = createReactClass({ - getInitialState() { - return { - openKeys: [], - }; - }, +class Test extends React.Component { + state = { + openKeys: [], + }; onClick(info) { console.log('click ', info); - }, + } - onOpenChange(openKeys) { + onOpenChange = (openKeys) => { console.log('onOpenChange', openKeys); this.setState({ openKeys, }); - }, + } + getMenu() { return ( item3 ); - }, + } render() { return (
{this.getMenu()}
); - }, -}); - + } +} ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/selectedKeys.js b/examples/selectedKeys.js index ba09a076..ae1c1b4c 100644 --- a/examples/selectedKeys.js +++ b/examples/selectedKeys.js @@ -3,38 +3,35 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Menu, { SubMenu, Item as MenuItem } from 'rc-menu'; -import createReactClass from 'create-react-class'; import 'rc-menu/assets/index.less'; -const Test = createReactClass({ - getInitialState() { - return { - destroyed: false, - selectedKeys: [], - openKeys: [], - }; - }, +class Test extends React.Component { + state = { + destroyed: false, + selectedKeys: [], + openKeys: [], + }; - onSelect(info) { + onSelect = (info) => { console.log('selected ', info); this.setState({ selectedKeys: info.selectedKeys, }); - }, + }; onDeselect(info) { console.log('deselect ', info); - }, + } - onOpenChange(openKeys) { + onOpenChange = (openKeys) => { console.log('onOpenChange ', openKeys); this.setState({ openKeys, }); - }, + }; - onCheck(e) { + onCheck = (e) => { const value = e.target.value; if (e.target.checked) { this.setState({ @@ -50,9 +47,9 @@ const Test = createReactClass({ selectedKeys, }); } - }, + }; - onOpenCheck(e) { + onOpenCheck = (e) => { const value = e.target.value; if (e.target.checked) { this.setState({ @@ -68,7 +65,7 @@ const Test = createReactClass({ openKeys, }); } - }, + }; getMenu() { return ( @@ -91,13 +88,13 @@ const Test = createReactClass({ item3 ); - }, + } destroy() { this.setState({ destroyed: true, }); - }, + } render() { if (this.state.destroyed) { @@ -141,8 +138,7 @@ const Test = createReactClass({
{this.getMenu()}
); - }, -}); - + } +} ReactDOM.render(, document.getElementById('__react-content')); diff --git a/package.json b/package.json index 18a15bcd..cc4eaf2c 100644 --- a/package.json +++ b/package.json @@ -75,9 +75,8 @@ "dependencies": { "babel-runtime": "6.x", "classnames": "2.x", - "create-react-class": "^15.5.2", "dom-scroll-into-view": "1.x", - "mini-store": "^1.0.2", + "mini-store": "^1.1.0", "prop-types": "^15.5.6", "rc-animate": "2.x", "rc-trigger": "^2.3.0", diff --git a/src/DOMWrap.jsx b/src/DOMWrap.jsx index e4efc189..6027cf45 100644 --- a/src/DOMWrap.jsx +++ b/src/DOMWrap.jsx @@ -1,22 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -const DOMWrap = createReactClass({ - displayName: 'DOMWrap', - - propTypes: { +export default class DOMWrap extends React.Component { + static propTypes = { tag: PropTypes.string, hiddenClassName: PropTypes.string, visible: PropTypes.bool, - }, + }; - getDefaultProps() { - return { - tag: 'div', - className: '', - }; - }, + static defaultProps = { + tag: 'div', + className: '', + }; render() { const props = { ...this.props }; @@ -28,7 +23,5 @@ const DOMWrap = createReactClass({ delete props.hiddenClassName; delete props.visible; return ; - }, -}); - -export default DOMWrap; + } +} diff --git a/src/Divider.jsx b/src/Divider.jsx index 4dd63357..9360996f 100644 --- a/src/Divider.jsx +++ b/src/Divider.jsx @@ -1,24 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -const Divider = createReactClass({ - displayName: 'Divider', - - propTypes: { +export default class Divider extends React.Component { + static propTypes = { className: PropTypes.string, rootPrefixCls: PropTypes.string, - }, + }; - getDefaultProps() { + static defaultProps = { // To fix keyboard UX. - return { disabled: true }; - }, + disabled: true, + }; render() { const { className = '', rootPrefixCls } = this.props; return
  • ; - }, -}); - -export default Divider; + } +} diff --git a/src/Menu.jsx b/src/Menu.jsx index c8874da8..eb15dc03 100644 --- a/src/Menu.jsx +++ b/src/Menu.jsx @@ -1,15 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import { Provider, create } from 'mini-store'; -import { default as MenuMixin, getActiveKey } from './MenuMixin'; +import { default as SubPopupMenu, getActiveKey } from './SubPopupMenu'; import { noop } from './util'; -const Menu = createReactClass({ - displayName: 'Menu', - - propTypes: { +export default class Menu extends React.Component { + static propTypes = { defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string), + defaultActiveFirst: PropTypes.bool, selectedKeys: PropTypes.arrayOf(PropTypes.string), defaultOpenKeys: PropTypes.arrayOf(PropTypes.string), openKeys: PropTypes.arrayOf(PropTypes.string), @@ -29,29 +27,34 @@ const Menu = createReactClass({ selectable: PropTypes.bool, multiple: PropTypes.bool, children: PropTypes.any, - }, - - mixins: [MenuMixin], + className: PropTypes.string, + style: PropTypes.object, + activeKey: PropTypes.string, + prefixCls: PropTypes.string, + }; + + static defaultProps = { + selectable: true, + onClick: noop, + onSelect: noop, + onOpenChange: noop, + onDeselect: noop, + defaultSelectedKeys: [], + defaultOpenKeys: [], + subMenuOpenDelay: 0.1, + subMenuCloseDelay: 0.1, + triggerSubMenuAction: 'hover', + prefixCls: 'rc-menu', + className: '', + mode: 'vertical', + style: {}, + }; + + constructor(props) { + super(props); + + this.isRootMenu = true; - isRootMenu: true, - - getDefaultProps() { - return { - selectable: true, - onClick: noop, - onSelect: noop, - onOpenChange: noop, - onDeselect: noop, - defaultSelectedKeys: [], - defaultOpenKeys: [], - subMenuOpenDelay: 0.1, - subMenuCloseDelay: 0.1, - triggerSubMenuAction: 'hover', - }; - }, - - getInitialState() { - const props = this.props; let selectedKeys = props.defaultSelectedKeys; let openKeys = props.defaultOpenKeys; if ('selectedKeys' in props) { @@ -66,9 +69,7 @@ const Menu = createReactClass({ openKeys, activeKey: { '0-menu-': getActiveKey(props, props.activeKey) }, }); - - return {}; - }, + } componentWillReceiveProps(nextProps) { if ('selectedKeys' in nextProps) { @@ -81,9 +82,9 @@ const Menu = createReactClass({ openKeys: nextProps.openKeys || [], }); } - }, + } - onSelect(selectInfo) { + onSelect = (selectInfo) => { const props = this.props; if (props.selectable) { // root menu @@ -104,13 +105,20 @@ const Menu = createReactClass({ selectedKeys, }); } - }, + } - onClick(e) { + onClick = (e) => { this.props.onClick(e); - }, + } - onOpenChange(event) { + // onKeyDown needs to be exposed as a instance method + // e.g., in rc-select, we need to navigate menu item while + // current active item is rc-select input box rather than the menu itself + onKeyDown = (e, callback) => { + this.innerMenu.getWrappedInstance().onKeyDown(e, callback); + } + + onOpenChange = (event) => { const props = this.props; const openKeys = this.store.getState().openKeys.concat(); let changed = false; @@ -142,9 +150,9 @@ const Menu = createReactClass({ } props.onOpenChange(openKeys); } - }, + } - onDeselect(selectInfo) { + onDeselect = (selectInfo) => { const props = this.props; if (props.selectable) { const selectedKeys = this.store.getState().selectedKeys.concat(); @@ -163,9 +171,9 @@ const Menu = createReactClass({ selectedKeys, }); } - }, + } - getOpenTransitionName() { + getOpenTransitionName = () => { const props = this.props; let transitionName = props.openTransitionName; const animationName = props.openAnimation; @@ -173,32 +181,24 @@ const Menu = createReactClass({ transitionName = `${props.prefixCls}-open-${animationName}`; } return transitionName; - }, - - renderMenuItem(c, i, subMenuKey) { - /* istanbul ignore if */ - if (!c) { - return null; - } - const state = this.store.getState(); - const extraProps = { - openKeys: state.openKeys, - selectedKeys: state.selectedKeys, - triggerSubMenuAction: this.props.triggerSubMenuAction, - subMenuKey, - }; - return this.renderCommonMenuItem(c, i, extraProps); - }, + } render() { - const props = { ...this.props }; + let { ...props } = this.props; props.className += ` ${props.prefixCls}-root`; + props = { + ...props, + onClick: this.onClick, + onOpenChange: this.onOpenChange, + onDeselect: this.onDeselect, + onSelect: this.onSelect, + openTransitionName: this.getOpenTransitionName(), + parentMenu: this, + }; return ( - {this.renderRoot(props)} + this.innerMenu = c}>{this.props.children} ); - }, -}); - -export default Menu; + } +} diff --git a/src/MenuItem.jsx b/src/MenuItem.jsx index 48f06b61..3920b7b5 100644 --- a/src/MenuItem.jsx +++ b/src/MenuItem.jsx @@ -1,7 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import KeyCode from 'rc-util/lib/KeyCode'; import classNames from 'classnames'; import scrollIntoView from 'dom-scroll-into-view'; @@ -10,10 +9,8 @@ import { noop } from './util'; /* eslint react/no-is-mounted:0 */ -export const MenuItem = createReactClass({ - displayName: 'MenuItem', - - propTypes: { +export class MenuItem extends React.Component { + static propTypes = { rootPrefixCls: PropTypes.string, eventKey: PropTypes.string, active: PropTypes.bool, @@ -29,27 +26,26 @@ export const MenuItem = createReactClass({ onDestroy: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, - }, - - getDefaultProps() { - return { - onSelect: noop, - onMouseEnter: noop, - onMouseLeave: noop, - }; - }, - - componentWillUnmount() { - const props = this.props; - if (props.onDestroy) { - props.onDestroy(props.eventKey); - } - }, + multiple: PropTypes.bool, + isSelected: PropTypes.bool, + manualRef: PropTypes.func, + }; + + static defaultProps = { + onSelect: noop, + onMouseEnter: noop, + onMouseLeave: noop, + manualRef: noop, + }; + + constructor(props) { + super(props); + } componentDidMount() { // invoke customized ref to expose component to mixin this.callRef(); - }, + } componentDidUpdate() { if (this.props.active) { @@ -59,18 +55,24 @@ export const MenuItem = createReactClass({ } this.callRef(); - }, + } + componentWillUnmount() { + const props = this.props; + if (props.onDestroy) { + props.onDestroy(props.eventKey); + } + } - onKeyDown(e) { + onKeyDown = (e) => { const keyCode = e.keyCode; if (keyCode === KeyCode.ENTER) { this.onClick(e); return true; } - }, + }; - onMouseLeave(e) { + onMouseLeave = (e) => { const { eventKey, onItemHover, onMouseLeave } = this.props; onItemHover({ key: eventKey, @@ -80,9 +82,9 @@ export const MenuItem = createReactClass({ key: eventKey, domEvent: e, }); - }, + }; - onMouseEnter(e) { + onMouseEnter = (e) => { const { eventKey, onItemHover, onMouseEnter } = this.props; onItemHover({ key: eventKey, @@ -92,9 +94,9 @@ export const MenuItem = createReactClass({ key: eventKey, domEvent: e, }); - }, + }; - onClick(e) { + onClick = (e) => { const { eventKey, multiple, onClick, onSelect, onDeselect, isSelected } = this.props; const info = { key: eventKey, @@ -112,29 +114,29 @@ export const MenuItem = createReactClass({ } else if (!isSelected) { onSelect(info); } - }, + }; getPrefixCls() { return `${this.props.rootPrefixCls}-item`; - }, + } getActiveClassName() { return `${this.getPrefixCls()}-active`; - }, + } getSelectedClassName() { return `${this.getPrefixCls()}-selected`; - }, + } getDisabledClassName() { return `${this.getPrefixCls()}-disabled`; - }, + } callRef() { if (this.props.manualRef) { this.props.manualRef(this); } - }, + } render() { const props = this.props; @@ -174,12 +176,14 @@ export const MenuItem = createReactClass({ {props.children}
  • ); - }, -}); - -MenuItem.isMenuItem = 1; + } +} -export default connect(({ activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ +const connected = connect(({ activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ active: activeKey[subMenuKey] === eventKey, isSelected: selectedKeys.indexOf(eventKey) !== -1, }))(MenuItem); + +connected.isMenuItem = true; + +export default connected; diff --git a/src/MenuItemGroup.jsx b/src/MenuItemGroup.jsx index bb52fe2a..55ea7d19 100644 --- a/src/MenuItemGroup.jsx +++ b/src/MenuItemGroup.jsx @@ -1,26 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -const MenuItemGroup = createReactClass({ - displayName: 'MenuItemGroup', - - propTypes: { +class MenuItemGroup extends React.Component { + static propTypes = { renderMenuItem: PropTypes.func, index: PropTypes.number, className: PropTypes.string, + subMenuKey: PropTypes.string, rootPrefixCls: PropTypes.string, - }, + }; - getDefaultProps() { - // To fix keyboard UX. - return { disabled: true }; - }, + static defaultProps = { + disabled: true, + }; - renderInnerMenuItem(item) { + renderInnerMenuItem = (item) => { const { renderMenuItem, index } = this.props; return renderMenuItem(item, index, this.props.subMenuKey); - }, + } render() { const props = this.props; @@ -40,8 +37,8 @@ const MenuItemGroup = createReactClass({ ); - }, -}); + } +} MenuItemGroup.isMenuItemGroup = true; diff --git a/src/MenuMixin.js b/src/MenuMixin.js deleted file mode 100644 index d81ad6e7..00000000 --- a/src/MenuMixin.js +++ /dev/null @@ -1,269 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import KeyCode from 'rc-util/lib/KeyCode'; -import createChainedFunction from 'rc-util/lib/createChainedFunction'; -import classNames from 'classnames'; -import { getKeyFromChildrenIndex, loopMenuItem } from './util'; -import DOMWrap from './DOMWrap'; - -function allDisabled(arr) { - if (!arr.length) { - return true; - } - return arr.every(c => !!c.props.disabled); -} - -function updateActiveKey(store, menuId, activeKey) { - const state = store.getState(); - store.setState({ - activeKey: { - ...state.activeKey, - [menuId]: activeKey, - }, - }); -} - -export function getActiveKey(props, originalActiveKey) { - let activeKey = originalActiveKey; - const { children, eventKey } = props; - if (activeKey) { - let found; - loopMenuItem(children, (c, i) => { - if (c && !c.props.disabled && activeKey === getKeyFromChildrenIndex(c, eventKey, i)) { - found = true; - } - }); - if (found) { - return activeKey; - } - } - activeKey = null; - if (props.defaultActiveFirst) { - loopMenuItem(children, (c, i) => { - if (!activeKey && c && !c.props.disabled) { - activeKey = getKeyFromChildrenIndex(c, eventKey, i); - } - }); - return activeKey; - } - return activeKey; -} - -function saveRef(index, c) { - if (c) { - this.instanceArray[index] = c; - } -} - -const MenuMixin = { - propTypes: { - focusable: PropTypes.bool, - multiple: PropTypes.bool, - style: PropTypes.object, - defaultActiveFirst: PropTypes.bool, - visible: PropTypes.bool, - activeKey: PropTypes.string, - selectedKeys: PropTypes.arrayOf(PropTypes.string), - defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string), - defaultOpenKeys: PropTypes.arrayOf(PropTypes.string), - openKeys: PropTypes.arrayOf(PropTypes.string), - children: PropTypes.any, - }, - - getDefaultProps() { - return { - prefixCls: 'rc-menu', - className: '', - mode: 'vertical', - level: 1, - inlineIndent: 24, - visible: true, - focusable: true, - style: {}, - }; - }, - - componentWillReceiveProps(nextProps) { - const originalActiveKey = 'activeKey' in nextProps ? nextProps.activeKey : - this.getStore().getState().activeKey[this.getEventKey()]; - const activeKey = getActiveKey(nextProps, originalActiveKey); - if (activeKey !== originalActiveKey) { - updateActiveKey(this.getStore(), this.getEventKey(), activeKey); - } - }, - - shouldComponentUpdate(nextProps) { - return this.props.visible || nextProps.visible; - }, - - componentWillMount() { - this.instanceArray = []; - }, - - // all keyboard events callbacks run from here at first - // FIXME: callback is currently used by rc-select, should be more explicit - onKeyDown(e, callback) { - const keyCode = e.keyCode; - let handled; - this.getFlatInstanceArray().forEach((obj) => { - if (obj && obj.props.active && obj.onKeyDown) { - handled = obj.onKeyDown(e); - } - }); - if (handled) { - return 1; - } - let activeItem = null; - if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) { - activeItem = this.step(keyCode === KeyCode.UP ? -1 : 1); - } - if (activeItem) { - e.preventDefault(); - updateActiveKey(this.getStore(), this.getEventKey(), activeItem.props.eventKey); - - if (typeof callback === 'function') { - callback(activeItem); - } - - return 1; - } - }, - - onItemHover(e) { - const { key, hover } = e; - updateActiveKey(this.getStore(), this.getEventKey(), hover ? key : null); - }, - - getEventKey() { - // when eventKey not available ,it's menu and return menu id '0-menu-' - return this.props.eventKey || '0-menu-'; - }, - - getStore() { - const store = this.store || this.props.store; - - return store; - }, - - getFlatInstanceArray() { - return this.instanceArray; - }, - - renderCommonMenuItem(child, i, extraProps) { - const state = this.getStore().getState(); - const props = this.props; - const key = getKeyFromChildrenIndex(child, props.eventKey, i); - const childProps = child.props; - const isActive = key === state.activeKey; - const newChildProps = { - mode: props.mode, - level: props.level, - inlineIndent: props.inlineIndent, - renderMenuItem: this.renderMenuItem, - rootPrefixCls: props.prefixCls, - index: i, - parentMenu: this, - // customized ref function, need to be invoked manually in child's componentDidMount - manualRef: childProps.disabled ? undefined : - createChainedFunction(child.ref, saveRef.bind(this, i)), - eventKey: key, - active: !childProps.disabled && isActive, - multiple: props.multiple, - onClick: this.onClick, - onItemHover: this.onItemHover, - openTransitionName: this.getOpenTransitionName(), - openAnimation: props.openAnimation, - subMenuOpenDelay: props.subMenuOpenDelay, - subMenuCloseDelay: props.subMenuCloseDelay, - forceSubMenuRender: props.forceSubMenuRender, - onOpenChange: this.onOpenChange, - onDeselect: this.onDeselect, - onSelect: this.onSelect, - ...extraProps, - }; - if (props.mode === 'inline') { - newChildProps.triggerSubMenuAction = 'click'; - } - return React.cloneElement(child, newChildProps); - }, - - renderRoot(props) { - this.instanceArray = []; - const className = classNames( - props.prefixCls, - props.className, - `${props.prefixCls}-${props.mode}`, - ); - const domProps = { - className, - role: 'menu', - 'aria-activedescendant': '', - }; - if (props.id) { - domProps.id = props.id; - } - if (props.focusable) { - domProps.tabIndex = '0'; - domProps.onKeyDown = this.onKeyDown; - } - return ( - // ESLint is not smart enough to know that the type of `children` was checked. - /* eslint-disable */ - - {React.Children.map( - props.children, - (c, i) => this.renderMenuItem(c, i, props.eventKey || '0-menu-'), - )} - - /*eslint-enable */ - ); - }, - - step(direction) { - let children = this.getFlatInstanceArray(); - const activeKey = this.getStore().getState().activeKey[this.getEventKey()]; - const len = children.length; - if (!len) { - return null; - } - if (direction < 0) { - children = children.concat().reverse(); - } - // find current activeIndex - let activeIndex = -1; - children.every((c, ci) => { - if (c && c.props.eventKey === activeKey) { - activeIndex = ci; - return false; - } - return true; - }); - if (!this.props.defaultActiveFirst && activeIndex !== -1) { - if (allDisabled(children.slice(activeIndex, len - 1))) { - return undefined; - } - } - const start = (activeIndex + 1) % len; - let i = start; - for (; ;) { - const child = children[i]; - if (!child || child.props.disabled) { - i = (i + 1 + len) % len; - // complete a loop - if (i === start) { - return null; - } - } else { - return child; - } - } - }, -}; - -export default MenuMixin; diff --git a/src/SubMenu.jsx b/src/SubMenu.jsx index 75cc9faf..9c9c6bae 100644 --- a/src/SubMenu.jsx +++ b/src/SubMenu.jsx @@ -1,13 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import Trigger from 'rc-trigger'; import KeyCode from 'rc-util/lib/KeyCode'; import classNames from 'classnames'; import { connect } from 'mini-store'; import SubPopupMenu from './SubPopupMenu'; import placements from './placements'; +import Animate from 'rc-animate'; import { noop, loopMenuItemRecursively, @@ -34,10 +34,8 @@ const updateDefaultActiveFirst = (store, eventKey, defaultActiveFirst) => { }); }; -const SubMenu = createReactClass({ - displayName: 'SubMenu', - - propTypes: { +export class SubMenu extends React.Component { + static propTypes = { parentMenu: PropTypes.object, title: PropTypes.node, children: PropTypes.any, @@ -60,27 +58,30 @@ const SubMenu = createReactClass({ onTitleMouseLeave: PropTypes.func, onTitleClick: PropTypes.func, isOpen: PropTypes.bool, - }, - - isRootMenu: false, - - getDefaultProps() { - return { - onMouseEnter: noop, - onMouseLeave: noop, - onTitleMouseEnter: noop, - onTitleMouseLeave: noop, - onTitleClick: noop, - title: '', - }; - }, - - getInitialState() { - this.isSubMenu = 1; - const props = this.props; + store: PropTypes.object, + mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']), + manualRef: PropTypes.func, + }; + + static defaultProps = { + onMouseEnter: noop, + onMouseLeave: noop, + onTitleMouseEnter: noop, + onTitleMouseLeave: noop, + onTitleClick: noop, + manualRef: noop, + mode: 'vertical', + title: '', + }; + + constructor(props) { + super(props); const store = props.store; const eventKey = props.eventKey; const defaultActiveFirst = store.getState().defaultActiveFirst; + + this.isRootMenu = false; + let value = false; if (defaultActiveFirst) { @@ -88,27 +89,11 @@ const SubMenu = createReactClass({ } updateDefaultActiveFirst(store, eventKey, value); - - return {}; - }, + } componentDidMount() { this.componentDidUpdate(); - }, - - adjustWidth() { - /* istanbul ignore if */ - if (!this.subMenuTitle || !this.menuInstance) { - return; - } - const popupMenu = ReactDOM.findDOMNode(this.menuInstance); - if (popupMenu.offsetWidth >= this.subMenuTitle.offsetWidth) { - return; - } - - /* istanbul ignore next */ - popupMenu.style.minWidth = `${this.subMenuTitle.offsetWidth}px`; - }, + } componentDidUpdate() { const { mode, parentMenu, manualRef } = this.props; @@ -123,7 +108,7 @@ const SubMenu = createReactClass({ } this.minWidthTimeout = setTimeout(() => this.adjustWidth(), 0); - }, + } componentWillUnmount() { const { onDestroy, eventKey } = this.props; @@ -140,13 +125,13 @@ const SubMenu = createReactClass({ if (this.mouseenterTimeout) { clearTimeout(this.mouseenterTimeout); } - }, + } - onDestroy(key) { + onDestroy = (key) => { this.props.onDestroy(key); - }, + }; - onKeyDown(e) { + onKeyDown = (e) => { const keyCode = e.keyCode; const menu = this.menuInstance; const { @@ -187,26 +172,26 @@ const SubMenu = createReactClass({ if (isOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) { return menu.onKeyDown(e); } - }, + }; - onOpenChange(e) { + onOpenChange = (e) => { this.props.onOpenChange(e); - }, + }; - onPopupVisibleChange(visible) { + onPopupVisibleChange = (visible) => { this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave'); - }, + }; - onMouseEnter(e) { + onMouseEnter = (e) => { const { eventKey: key, onMouseEnter, store } = this.props; updateDefaultActiveFirst(store, this.props.eventKey, false); onMouseEnter({ key, domEvent: e, }); - }, + }; - onMouseLeave(e) { + onMouseLeave = (e) => { const { parentMenu, eventKey, @@ -217,9 +202,9 @@ const SubMenu = createReactClass({ key: eventKey, domEvent: e, }); - }, + }; - onTitleMouseEnter(domEvent) { + onTitleMouseEnter = (domEvent) => { const { eventKey: key, onItemHover, onTitleMouseEnter } = this.props; onItemHover({ key, @@ -229,9 +214,9 @@ const SubMenu = createReactClass({ key, domEvent, }); - }, + }; - onTitleMouseLeave(e) { + onTitleMouseLeave = (e) => { const { parentMenu, eventKey, onItemHover, onTitleMouseLeave } = this.props; parentMenu.subMenuInstance = this; onItemHover({ @@ -242,9 +227,9 @@ const SubMenu = createReactClass({ key: eventKey, domEvent: e, }); - }, + }; - onTitleClick(e) { + onTitleClick = (e) => { const { props } = this; props.onTitleClick({ key: props.eventKey, @@ -255,53 +240,53 @@ const SubMenu = createReactClass({ } this.triggerOpenChange(!props.isOpen, 'click'); updateDefaultActiveFirst(props.store, this.props.eventKey, false); - }, + }; - onSubMenuClick(info) { + onSubMenuClick = (info) => { this.props.onClick(this.addKeyPath(info)); - }, + }; - onSelect(info) { + onSelect = (info) => { this.props.onSelect(info); - }, + }; - onDeselect(info) { + onDeselect = (info) => { this.props.onDeselect(info); - }, + }; - getPrefixCls() { + getPrefixCls = () => { return `${this.props.rootPrefixCls}-submenu`; - }, + }; - getActiveClassName() { + getActiveClassName = () => { return `${this.getPrefixCls()}-active`; - }, + }; - getDisabledClassName() { + getDisabledClassName = () => { return `${this.getPrefixCls()}-disabled`; - }, + }; - getSelectedClassName() { + getSelectedClassName = () => { return `${this.getPrefixCls()}-selected`; - }, + }; - getOpenClassName() { + getOpenClassName = () => { return `${this.props.rootPrefixCls}-submenu-open`; - }, + }; - saveMenuInstance(c) { + saveMenuInstance = (c) => { // children menu instance this.menuInstance = c; - }, + }; - addKeyPath(info) { + addKeyPath = (info) => { return { ...info, keyPath: (info.keyPath || []).concat(this.props.eventKey), }; - }, + }; - triggerOpenChange(open, type) { + triggerOpenChange = (open, type) => { const key = this.props.eventKey; const openChange = () => { this.onOpenChange({ @@ -319,17 +304,35 @@ const SubMenu = createReactClass({ } else { openChange(); } - }, + } - isChildrenSelected() { + isChildrenSelected = () => { const ret = { find: false }; loopMenuItemRecursively(this.props.children, this.props.selectedKeys, ret); return ret.find; - }, + } - isOpen() { + isOpen = () => { return this.props.openKeys.indexOf(this.props.eventKey) !== -1; - }, + } + + adjustWidth = () => { + /* istanbul ignore if */ + if (!this.subMenuTitle || !this.menuInstance) { + return; + } + const popupMenu = ReactDOM.findDOMNode(this.menuInstance); + if (popupMenu.offsetWidth >= this.subMenuTitle.offsetWidth) { + return; + } + + /* istanbul ignore next */ + popupMenu.style.minWidth = `${this.subMenuTitle.offsetWidth}px`; + }; + + saveSubMenuTitle = (subMenuTitle) => { + this.subMenuTitle = subMenuTitle; + } renderChildren(children) { const props = this.props; @@ -350,6 +353,7 @@ const SubMenu = createReactClass({ openAnimation: props.openAnimation, onOpenChange: this.onOpenChange, subMenuOpenDelay: props.subMenuOpenDelay, + parentMenu: this, subMenuCloseDelay: props.subMenuCloseDelay, forceSubMenuRender: props.forceSubMenuRender, triggerSubMenuAction: props.triggerSubMenuAction, @@ -360,12 +364,44 @@ const SubMenu = createReactClass({ id: this._menuId, manualRef: this.saveMenuInstance, }; - return {children}; - }, - saveSubMenuTitle(subMenuTitle) { - this.subMenuTitle = subMenuTitle; - }, + const haveRendered = this.haveRendered; + this.haveRendered = true; + + this.haveOpened = this.haveOpened || baseProps.visible || baseProps.forceSubMenuRender; + // never rendered not planning to, don't render + if (!this.haveOpened) { + return
    ; + } + + // don't show transition on first rendering (no animation for opened menu) + // show appear transition if it's not visible (not sure why) + // show appear transition if it's not inline mode + const transitionAppear = haveRendered || !baseProps.visible || !baseProps.mode === 'inline'; + + baseProps.className += ` ${baseProps.prefixCls}-sub`; + const animProps = {}; + + if (baseProps.openTransitionName) { + animProps.transitionName = baseProps.openTransitionName; + } else if (typeof baseProps.openAnimation === 'object') { + animProps.animation = { ...baseProps.openAnimation }; + if (!transitionAppear) { + delete animProps.animation.appear; + } + } + + return ( + + {children} + + ); + } render() { const props = this.props; @@ -458,13 +494,15 @@ const SubMenu = createReactClass({ )} ); - }, -}); + } +} -SubMenu.isSubMenu = 1; - -export default connect(({ openKeys, activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ +const connected = connect(({ openKeys, activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ isOpen: openKeys.indexOf(eventKey) > -1, active: activeKey[subMenuKey] === eventKey, selectedKeys, }))(SubMenu); + +connected.isSubMenu = true; + +export default connected; diff --git a/src/SubPopupMenu.js b/src/SubPopupMenu.js index 92974136..7bcecb5b 100644 --- a/src/SubPopupMenu.js +++ b/src/SubPopupMenu.js @@ -1,14 +1,63 @@ import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import Animate from 'rc-animate'; import { connect } from 'mini-store'; -import { default as MenuMixin, getActiveKey } from './MenuMixin'; +import KeyCode from 'rc-util/lib/KeyCode'; +import createChainedFunction from 'rc-util/lib/createChainedFunction'; +import classNames from 'classnames'; +import { getKeyFromChildrenIndex, loopMenuItem, noop } from './util'; +import DOMWrap from './DOMWrap'; -const SubPopupMenu = createReactClass({ - displayName: 'SubPopupMenu', +function allDisabled(arr) { + if (!arr.length) { + return true; + } + return arr.every(c => !!c.props.disabled); +} - propTypes: { +function updateActiveKey(store, menuId, activeKey) { + const state = store.getState(); + store.setState({ + activeKey: { + ...state.activeKey, + [menuId]: activeKey, + }, + }); +} + +export function getActiveKey(props, originalActiveKey) { + let activeKey = originalActiveKey; + const { children, eventKey } = props; + if (activeKey) { + let found; + loopMenuItem(children, (c, i) => { + if (c && !c.props.disabled && activeKey === getKeyFromChildrenIndex(c, eventKey, i)) { + found = true; + } + }); + if (found) { + return activeKey; + } + } + activeKey = null; + if (props.defaultActiveFirst) { + loopMenuItem(children, (c, i) => { + if (!activeKey && c && !c.props.disabled) { + activeKey = getKeyFromChildrenIndex(c, eventKey, i); + } + }); + return activeKey; + } + return activeKey; +} + +function saveRef(index, c) { + if (c) { + this.instanceArray[index] = c; + } +} + +export class SubPopupMenu extends React.Component { + static propTypes = { onSelect: PropTypes.func, onClick: PropTypes.func, onDeselect: PropTypes.func, @@ -19,106 +68,279 @@ const SubPopupMenu = createReactClass({ openKeys: PropTypes.arrayOf(PropTypes.string), visible: PropTypes.bool, children: PropTypes.any, - }, + parentMenu: PropTypes.object, + eventKey: PropTypes.string, + store: PropTypes.shape({ + getState: PropTypes.func, + setState: PropTypes.func, + }), - mixins: [MenuMixin], + // adding in refactor + focusable: PropTypes.bool, + multiple: PropTypes.bool, + style: PropTypes.object, + defaultActiveFirst: PropTypes.bool, + activeKey: PropTypes.string, + selectedKeys: PropTypes.arrayOf(PropTypes.string), + defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string), + defaultOpenKeys: PropTypes.arrayOf(PropTypes.string), + level: PropTypes.number, + mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']), + triggerSubMenuAction: PropTypes.oneOf(['click', 'hover']), + inlineIndent: PropTypes.oneOfType(PropTypes.number, PropTypes.string), + manualRef: PropTypes.func, + }; + + static defaultProps = { + prefixCls: 'rc-menu', + className: '', + mode: 'vertical', + level: 1, + inlineIndent: 24, + visible: true, + focusable: true, + style: {}, + manualRef: noop, + }; + + constructor(props) { + super(props); - getInitialState() { - const props = this.props; props.store.setState({ activeKey: { ...props.store.getState().activeKey, [props.eventKey]: getActiveKey(props, props.activeKey), }, }); + } - return {}; - }, + componentWillMount() { + this.instanceArray = []; + } componentDidMount() { // invoke customized ref to expose component to mixin - this.props.manualRef(this); - }, + if (this.props.manualRef) { + this.props.manualRef(this); + } + } + + componentWillReceiveProps(nextProps) { + const originalActiveKey = 'activeKey' in nextProps ? nextProps.activeKey : + this.getStore().getState().activeKey[this.getEventKey()]; + const activeKey = getActiveKey(nextProps, originalActiveKey); + if (activeKey !== originalActiveKey) { + updateActiveKey(this.getStore(), this.getEventKey(), activeKey); + } + } - onDeselect(selectInfo) { + shouldComponentUpdate(nextProps) { + return this.props.visible || nextProps.visible; + } + + // all keyboard events callbacks run from here at first + onKeyDown = (e, callback) => { + const keyCode = e.keyCode; + let handled; + this.getFlatInstanceArray().forEach((obj) => { + if (obj && obj.props.active && obj.onKeyDown) { + handled = obj.onKeyDown(e); + } + }); + if (handled) { + return 1; + } + let activeItem = null; + if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) { + activeItem = this.step(keyCode === KeyCode.UP ? -1 : 1); + } + if (activeItem) { + e.preventDefault(); + updateActiveKey(this.getStore(), this.getEventKey(), activeItem.props.eventKey); + + if (typeof callback === 'function') { + callback(activeItem); + } + + return 1; + } + }; + + onItemHover = (e) => { + const { key, hover } = e; + updateActiveKey(this.getStore(), this.getEventKey(), hover ? key : null); + }; + + onDeselect = (selectInfo) => { this.props.onDeselect(selectInfo); - }, + }; - onSelect(selectInfo) { + onSelect = (selectInfo) => { this.props.onSelect(selectInfo); - }, + } - onClick(e) { + onClick = (e) => { this.props.onClick(e); - }, + }; - onOpenChange(e) { + onOpenChange = (e) => { this.props.onOpenChange(e); - }, + }; - onDestroy(key) { + onDestroy = (key) => { /* istanbul ignore next */ this.props.onDestroy(key); - }, + }; + + getFlatInstanceArray = () => { + return this.instanceArray; + }; + + getStore = () => { + return this.props.store; + }; - getOpenTransitionName() { + getEventKey = () => { + // when eventKey not available ,it's menu and return menu id '0-menu-' + return this.props.eventKey || '0-menu-'; + }; + + getOpenTransitionName = () => { return this.props.openTransitionName; - }, + }; - renderMenuItem(c, i, subMenuKey) { - /* istanbul ignore next */ - if (!c) { + step = (direction) => { + let children = this.getFlatInstanceArray(); + const activeKey = this.getStore().getState().activeKey[this.getEventKey()]; + const len = children.length; + if (!len) { return null; } + if (direction < 0) { + children = children.concat().reverse(); + } + // find current activeIndex + let activeIndex = -1; + children.every((c, ci) => { + if (c && c.props.eventKey === activeKey) { + activeIndex = ci; + return false; + } + return true; + }); + if ( + !this.props.defaultActiveFirst && activeIndex !== -1 + && + allDisabled(children.slice(activeIndex, len - 1)) + ) { + return undefined; + } + const start = (activeIndex + 1) % len; + let i = start; + + do { + const child = children[i]; + if (!child || child.props.disabled) { + i = (i + 1) % len; + } else { + return child; + } + } while (i !== start); + + return null; + }; + + renderCommonMenuItem = (child, i, extraProps) => { + const state = this.getStore().getState(); const props = this.props; + const key = getKeyFromChildrenIndex(child, props.eventKey, i); + const childProps = child.props; + const isActive = key === state.activeKey; + const newChildProps = { + mode: props.mode, + level: props.level, + inlineIndent: props.inlineIndent, + renderMenuItem: this.renderMenuItem, + rootPrefixCls: props.prefixCls, + index: i, + parentMenu: props.parentMenu, + // customized ref function, need to be invoked manually in child's componentDidMount + manualRef: childProps.disabled ? undefined : + createChainedFunction(child.ref, saveRef.bind(this, i)), + eventKey: key, + active: !childProps.disabled && isActive, + multiple: props.multiple, + onClick: this.onClick, + onItemHover: this.onItemHover, + openTransitionName: this.getOpenTransitionName(), + openAnimation: props.openAnimation, + subMenuOpenDelay: props.subMenuOpenDelay, + subMenuCloseDelay: props.subMenuCloseDelay, + forceSubMenuRender: props.forceSubMenuRender, + onOpenChange: this.onOpenChange, + onDeselect: this.onDeselect, + onSelect: this.onSelect, + ...extraProps, + }; + if (props.mode === 'inline') { + newChildProps.triggerSubMenuAction = 'click'; + } + return React.cloneElement(child, newChildProps); + }; + + renderMenuItem = (c, i, subMenuKey) => { + /* istanbul ignore if */ + if (!c) { + return null; + } + const state = this.getStore().getState(); const extraProps = { - openKeys: props.openKeys, - selectedKeys: props.selectedKeys, - triggerSubMenuAction: props.triggerSubMenuAction, + openKeys: state.openKeys, + selectedKeys: state.selectedKeys, + triggerSubMenuAction: this.props.triggerSubMenuAction, subMenuKey, }; return this.renderCommonMenuItem(c, i, extraProps); - }, + }; render() { - const props = { ...this.props }; - const haveRendered = this.haveRendered; - this.haveRendered = true; - - this.haveOpened = this.haveOpened || props.visible || props.forceSubMenuRender; - // never rendered not planning to, don't render - if (!this.haveOpened) { - return null; + const props = this.props; + this.instanceArray = []; + const className = classNames( + props.prefixCls, + props.className, + `${props.prefixCls}-${props.mode}`, + ); + const domProps = { + className, + role: 'menu', + 'aria-activedescendant': '', + }; + if (props.id) { + domProps.id = props.id; } - - // don't show transition on first rendering (no animation for opened menu) - // show appear transition if it's not visible (not sure why) - // show appear transition if it's not inline mode - const transitionAppear = haveRendered || !props.visible || !props.mode === 'inline'; - - props.className += ` ${props.prefixCls}-sub`; - const animProps = {}; - - if (props.openTransitionName) { - animProps.transitionName = props.openTransitionName; - } else if (typeof props.openAnimation === 'object') { - animProps.animation = { ...props.openAnimation }; - if (!transitionAppear) { - delete animProps.animation.appear; - } + if (props.focusable) { + domProps.tabIndex = '0'; + domProps.onKeyDown = this.onKeyDown; } - return ( - - {this.renderRoot(props)} - + {React.Children.map( + props.children, + (c, i) => this.renderMenuItem(c, i, props.eventKey || '0-menu-'), + )} + + /*eslint-enable */ ); - }, -}); + } +} export default connect()(SubPopupMenu); diff --git a/src/placements.jsx b/src/placements.js similarity index 100% rename from src/placements.jsx rename to src/placements.js diff --git a/tests/SubMenu.spec.js b/tests/SubMenu.spec.js index 8540b4dc..13a14907 100644 --- a/tests/SubMenu.spec.js +++ b/tests/SubMenu.spec.js @@ -256,10 +256,19 @@ describe('SubMenu', () => { describe('horizontal menu', () => { it('should automatically adjust width', () => { - const wrapper = mount(createMenu({ + const props = { mode: 'horizontal', openKeys: ['s1'], - })); + }; + + const wrapper = mount( + + 1 + + 2 + + + ); const subMenuInstance = wrapper.find('SubMenu').first().instance(); const adjustWidthSpy = jest.spyOn(subMenuInstance, 'adjustWidth'); diff --git a/tests/__snapshots__/Menu.spec.js.snap b/tests/__snapshots__/Menu.spec.js.snap index d5de1dad..241f13d0 100644 --- a/tests/__snapshots__/Menu.spec.js.snap +++ b/tests/__snapshots__/Menu.spec.js.snap @@ -188,6 +188,7 @@ exports[`Menu render renders inline menu correctly 1`] = ` class="rc-menu-submenu-arrow" />
    +
    `;