diff --git a/blocks/Menu/index.js b/blocks/Menu/index.js new file mode 100644 index 0000000..ef053b4 --- /dev/null +++ b/blocks/Menu/index.js @@ -0,0 +1,135 @@ +import React, {Children} from 'react'; +import bem from 'b_'; +import BemComponent, {BemControl} from '../BemComponent'; + +const b = bem.with('menu'); + +export default class Menu extends BemComponent { + constructor(props) { + super(props); + + this._lastHoveredItem = null; + this._items = []; + + this.listeners = { + onClick: this.onClick.bind(this), + onKeyDown: this.onKeyDown.bind(this) + }; + + this.onItemHover = this.onItemHover.bind(this); + this.onItemInit = this.onItemInit.bind(this); + this.onItemDestroy = this.onItemDestroy.bind(this); + } + + render() { + const {disabled, focused} = this.state; + const {theme, size} = this.props; + + const className = b({ + theme, + size, + disabled, + focused + }); + + return this.renderMenu(className, this.listeners); + } + + renderMenu(className, listeners) { + const {children} = this.props; + const {disabled} = this.state; + + Children.forEach(children, (item) => { + // disable menu items + item.props.disabled = disabled ? disabled : item.props.disabled; + item.props.onHover = this.onItemHover; + // collect renderedMenuItems + item.props.onInit = this.onItemInit; + item.props.onDestroy = this.onItemDestroy; + }); + + const tabIndex = disabled ? -1 : 0; + + return ( + +
{this.props.children}
+
+ ); + } + + onClick(e) { + if (this.state.disabled) { + e.preventDefault(); + } else { + this.props.onClick(); + } + } + + onItemHover(menuItem, hovered) { + if (hovered) { + this._lastHoveredItem = menuItem; + } else { + if (this._lastHoveredItem && this._lastHoveredItem.state.hovered) { + this._lastHoveredItem.setState({hovered: false}); + } + + this._lastHoveredItem = null; + } + } + + onItemInit(menuItem) { + this._items.push(menuItem); + } + + onItemDestroy(menuItem) { + const index = this._items.indexOf(menuItem); + if (index >= 0) { + this._items.splice(index, 1); + } + } + + onKeyDown(e) { + if (this.state.disabled || !this.state.focused) { + return; + } + + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + + const dir = e.key === 'ArrowDown' ? 1 : -1; + const items = this._items; + const len = items.length; + const hoveredIdx = this._lastHoveredItem ? Math.max(items.indexOf(this._lastHoveredItem), 0) : 0; + let nextIdx = hoveredIdx; + let i = 0; + + do { + nextIdx += dir; + if (nextIdx < 0) { + nextIdx = len - 1; + } + if (nextIdx >= len) { + nextIdx = 0; + } + if (++i === len) { + // overflow + return; + } + } while (items[nextIdx].props.disabled); + + this._lastHoveredItem.setState({hovered: false}); + + items[nextIdx].setState({hovered: true}); + this._lastHoveredItem = items[nextIdx]; + } + } + +} + +Menu.defaultProps = { + onClick() {} +}; diff --git a/blocks/Menu2/index.js b/blocks/Menu2/index.js new file mode 100644 index 0000000..f4e56f9 --- /dev/null +++ b/blocks/Menu2/index.js @@ -0,0 +1,159 @@ +import React, {Children} from 'react'; +import bem from 'b_'; +import BemComponent, {BemControl} from '../BemComponent'; + +const b = bem.with('menu'); + +/** + * Example render through props.renderItem + * + * } + * items={[ + * {value: '1', text: 'menu-item2 1'}, + * {value: '2', text: 'menu-item2 2'}, + * {value: '3', text: 'menu-item2 3'}, + * {value: '4', text: 'menu-item2 4'} + * ]} + * + */ +export default class Menu2 extends BemComponent { + constructor(props) { + super(props); + + this._lastHoveredItem = null; + this._items = []; + + this.listeners = { + onClick: this.onClick.bind(this), + onKeyDown: this.onKeyDown.bind(this) + }; + + this.onItemHover = this.onItemHover.bind(this); + this.onItemInit = this.onItemInit.bind(this); + this.onItemDestroy = this.onItemDestroy.bind(this); + } + + render() { + const {disabled, focused} = this.state; + const {theme, size} = this.props; + + const className = b({ + theme, + size, + disabled, + focused + }); + + return this.renderMenu(className, this.listeners); + } + + renderMenu(className, listeners) { + const {disabled} = this.state; + + const tabIndex = disabled ? -1 : 0; + + const items = this.props.items.map((item) => { + return React.createElement( + this.props.renderItem.type, + Object.assign({ + key: item.value, + // disable menu items + disabled: disabled, + theme: this.props.theme, + onHover: this.onItemHover, + // collect renderedMenuItems + onInit: this.onItemInit, + onDestroy: this.onItemDestroy + }, item) + ); + }); + + return ( + +
{items}
+
+ ); + } + + onClick(e) { + if (this.state.disabled) { + e.preventDefault(); + } else { + this.props.onClick(); + } + } + + onItemHover(menuItem, hovered) { + if (hovered) { + this._lastHoveredItem = menuItem; + } else { + if (this._lastHoveredItem && this._lastHoveredItem.state.hovered) { + this._lastHoveredItem.setState({hovered: false}); + } + + this._lastHoveredItem = null; + } + } + + onItemInit(menuItem) { + this._items.push(menuItem); + } + + onItemDestroy(menuItem) { + const index = this._items.indexOf(menuItem); + if (index >= 0) { + this._items.splice(index, 1); + } + } + + onKeyDown(e) { + if (this.state.disabled || !this.state.focused) { + return; + } + + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + + const dir = e.key === 'ArrowDown' ? 1 : -1; + const items = this._items; + const len = items.length; + const hoveredIdx = this._lastHoveredItem ? Math.max(items.indexOf(this._lastHoveredItem), 0) : 0; + let nextIdx = hoveredIdx; + let i = 0; + + do { + nextIdx += dir; + if (nextIdx < 0) { + nextIdx = len - 1; + } + if (nextIdx >= len) { + nextIdx = 0; + } + if (++i === len) { + // overflow + return; + } + } while (items[nextIdx].props.disabled); + + this._lastHoveredItem.setState({hovered: false}); + + items[nextIdx].setState({hovered: true}); + this._lastHoveredItem = items[nextIdx]; + } + } + +} + +Menu2.propTypes = { + items: React.PropTypes.array +}; + +Menu2.defaultProps = { + onClick() {} +}; diff --git a/blocks/MenuItem/index.js b/blocks/MenuItem/index.js new file mode 100644 index 0000000..1f32fe3 --- /dev/null +++ b/blocks/MenuItem/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import bem from 'b_'; +import BemComponent, { BemControl } from '../BemComponent'; + +const b = bem.with('menu-item'); + +export default class MenuItem extends BemComponent { + constructor(props) { + super(props); + + this.listeners = { + onClick: this.onClick.bind(this) + }; + } + + componentWillMount() { + this.props.onInit(this); + } + + componentWillUnmount() { + this.props.onDestroy(this); + } + + render() { + const { disabled, hovered } = this.state; + const { theme, size } = this.props; + + const className = b({ + theme, + size, + + disabled, + hovered + }); + + return this.renderMenuItem(className, this.listeners) + } + + renderMenuItem(className, listeners) { + return ( + +
{this.props.children}
+
+ ); + } + + onClick(e) { + if (this.state.disabled) { + e.preventDefault(); + } else { + this.props.onClick(); + } + } + + onControlMouseEnter() { + if (this.state.disabled) { + return; + } + + super.onControlMouseEnter(); + + this.props.onHover(this, true); + } + + onControlMouseLeave() { + if (this.state.disabled) { + return; + } + + super.onControlMouseLeave(); + + this.props.onHover(this, false); + } +} + +MenuItem.defaultProps = { + disabled: false, + onClick() {}, + onDestroy() {}, + onInit() {}, + onHover() {} +}; diff --git a/blocks/MenuItem2/index.js b/blocks/MenuItem2/index.js new file mode 100644 index 0000000..fe8ef6d --- /dev/null +++ b/blocks/MenuItem2/index.js @@ -0,0 +1,87 @@ +import React from 'react'; +import bem from 'b_'; +import BemComponent, { BemControl } from '../BemComponent'; + +const b = bem.with('menu-item'); + +export default class MenuItem2 extends BemComponent { + constructor(props) { + super(props); + + this.listeners = { + onClick: this.onClick.bind(this) + }; + } + + componentWillMount() { + this.props.onInit(this); + } + + componentWillUnmount() { + this.props.onDestroy(this); + } + + render() { + const { disabled, hovered } = this.state; + const { theme, size } = this.props; + + const className = b({ + theme, + size, + + disabled, + hovered + }); + + return this.renderMenuItem(className, this.listeners) + } + + renderMenuItem(className, listeners) { + return ( + +
{this.props.text}
+
+ ); + } + + onClick(e) { + if (this.state.disabled) { + e.preventDefault(); + } else { + this.props.onClick(); + } + } + + onControlMouseEnter() { + console.log('onControlMouseEnter') + if (this.state.disabled) { + return; + } + + super.onControlMouseEnter(); + + this.props.onHover(this, true); + } + + onControlMouseLeave() { + if (this.state.disabled) { + return; + } + + super.onControlMouseLeave(); + + this.props.onHover(this, false); + } +} + +MenuItem2.defaultProps = { + disabled: false, + onClick() {}, + onDestroy() {}, + onInit() {}, + onHover() {} +}; diff --git a/bundles/MenuDisabledSwitch.js b/bundles/MenuDisabledSwitch.js new file mode 100644 index 0000000..271613c --- /dev/null +++ b/bundles/MenuDisabledSwitch.js @@ -0,0 +1,36 @@ +import React from 'react'; +import Button from '../blocks/Button'; +import Menu from '../blocks/Menu'; +import MenuItem from '../blocks/MenuItem'; + +export default class MenuDisabledSwitch extends React.Component { + constructor(props) { + super(props); + + this.state = { + disabled: true + }; + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.setState({ + disabled: !this.state.disabled + }) + } + + render() { + return ( +
+ + + menu-item1 + menu-item2 + menu-item3 + +
+ ); + } +} diff --git a/bundles/index.js b/bundles/index.js index d4ee674..4e2d045 100644 --- a/bundles/index.js +++ b/bundles/index.js @@ -2,9 +2,15 @@ import React from 'react'; import { render } from 'react-dom'; import Button from '../blocks/Button'; import Link from '../blocks/Link'; +import Menu from '../blocks/Menu'; +import Menu2 from '../blocks/Menu2'; +import MenuItem from '../blocks/MenuItem'; +import MenuItem2 from '../blocks/MenuItem2'; import Popup from '../blocks/Popup'; import TextInput from '../blocks/TextInput'; +import MenuDisabledSwitch from './MenuDisabledSwitch'; + class Example extends React.Component { constructor(...args) { super(...args); @@ -43,6 +49,7 @@ class Example extends React.Component { {this.renderLink()} {this.renderTextInput()} {this.renderPopup()} + {this.renderMenu()} ); } @@ -99,6 +106,63 @@ class Example extends React.Component { ) } + + renderMenu() { + return ( +
+
+
Simple Menu2
+ } + items={[ + {value: '1', text: 'menu-item2 1'}, + {value: '2', text: 'menu-item2 2'}, + {value: '3', text: 'menu-item2 3'}, + {value: '4', text: 'menu-item2 4'} + ]} + > + +
+
+
simple
+ + menu-item1 + menu-item2 + menu-item3 + menu-item4 + menu-item5 + menu-item6 + menu-item7 + +
+
+
with disabled item
+ + menu-item1 + menu-item2 + menu-item3 + +
+
+
with disabled menu
+ +
+
+ ); + } } +const styles = { + example: { + border: '1px solid', + margin: '10px' + }, + + exampleTitle: { + fontWeight: 'bold', + marginBottom: '20px' + } +}; + render(React.createElement(Example), document.getElementById('root'));