Skip to content
This repository was archived by the owner on Sep 11, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions blocks/Menu/index.js
Original file line number Diff line number Diff line change
@@ -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) => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не вдаваясь в подробности про производительность, разве мутировать props пришедшие «снаружи», это не опасно? Я думал, что в этом случае рекомендуется клонировать чайлда, через React.cloneElement(item, {/* additional props */}):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Еще пока обсуждали с @pasaran, вспомнили, что для массива однородных детей, нижно задавать key. Может давай его тут подставлять из счетчика в цикла?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я тут все же переделал.
А key нужен при динамической генерации списка, что Menu2 и делает

// 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 (
<BemControl>
<div
className={className}
tabIndex={tabIndex}
{...listeners}
>{this.props.children}</div>
</BemControl>
);
}

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() {}
};
159 changes: 159 additions & 0 deletions blocks/Menu2/index.js
Original file line number Diff line number Diff line change
@@ -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
*
* <Menu2
* theme="islands"
* renderItem={<MenuItem2/>}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

По-моему — адЪ. Я бы совсем не хотел, на проекте так описывать компоненты.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

У этого подхода есть несколько плюсов (хотя минусов тоже хватает :) ).

Почти любой нестандартный MenuItem разработчик рано или поздно завернет в компонент, а значит он из раза в раз будет писать одну и ту же балалайку в своем коде

<Menu>
{data.map((item) => <MyMenuItem/>)}
</Menu>

Плюс такой подход скрывает реализацию Menu и еще приближает к более удобной схеме генерации: массив + render fn = список

* 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'}
* ]}
* </Menu2>
*/
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 (
<BemControl>
<div
className={className}
tabIndex={tabIndex}
{...listeners}
>{items}</div>
</BemControl>
);
}

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() {}
};
85 changes: 85 additions & 0 deletions blocks/MenuItem/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<BemControl>
<div
className={className}
{...listeners}
>{this.props.children}</div>
</BemControl>
);
}

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() {}
};
Loading