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 (
+
+
+
+
+ );
+ }
+}
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
+
+
+
+
with disabled item
+
+
+
+
+ );
+ }
}
+const styles = {
+ example: {
+ border: '1px solid',
+ margin: '10px'
+ },
+
+ exampleTitle: {
+ fontWeight: 'bold',
+ marginBottom: '20px'
+ }
+};
+
render(React.createElement(Example), document.getElementById('root'));