Merged in update-dropwdown-component (pull request #15)

Update Menu and dropwdown component
This commit is contained in:
Ivan Diaz 2016-06-22 18:52:03 -03:00
commit e039162025
12 changed files with 318 additions and 79 deletions

View File

@ -1,16 +1,23 @@
'use strict';
import React from 'react';
import {Link} from 'react-router';
import DocumentTitle from 'react-document-title';
const React = require('react');
const DocumentTitle = require('react-document-title');
import Button from 'core-components/button';
import Input from 'core-components/input';
import Checkbox from 'core-components/checkbox';
import Widget from 'core-components/widget';
import DropDown from 'core-components/drop-down';
const Button = require('core-components/button');
const Input = require('core-components/input');
const Checkbox = require('core-components/checkbox');
const Widget = require('core-components/widget');
const DropDown = require('core-components/drop-down');
const Menu = require('core-components/menu');
let dropDownItems = [{content: 'English'}, {content: 'Spanish'}, {content: 'German'}, {content: 'Portuguese'}, {content: 'Japanese'}];
let secondaryMenuItems = [
{content: 'My Tickets', icon: 'file-text'},
{content: 'New Ticket', icon: 'plus'},
{content: 'Articles', icon: 'book'},
{content: 'Edit Profile', icon: 'pencil'},
{content: 'Close Session', icon: 'lock'}
];
let DemoPage = React.createClass({
@ -58,13 +65,25 @@ let DemoPage = React.createClass({
render: (
<DropDown items={dropDownItems} onChange={function (index) { console.log('changed to ' + index); }} />
)
},
{
title: 'Primary Menu',
render: (
<Menu items={dropDownItems} />
)
},
{
title: 'Secondary Menu',
render: (
<Menu items={secondaryMenuItems} type="secondary"/>
)
}
],
render() {
return (
<DocumentTitle title="Demo Page">
<section className="home-page">
<section className="demo-page">
{this.renderElements()}
</section>
</DocumentTitle>
@ -74,7 +93,7 @@ let DemoPage = React.createClass({
renderElements: function () {
return this.elements.map((element) => {
return (
<div className="demo-element">
<div className="demo-element col-md-3">
<h4>
{element.title}
</h4>

View File

@ -32,13 +32,10 @@ let MainLayoutHeader = React.createClass({
},
getLanguageList() {
return languageList.map((item) => {
return languageList.map((language) => {
return {
content: (
<span>
<Icon name={codeLanguages[item]} />{item}
</span>
)
content: language,
icon: codeLanguages[language]
};
});
},

View File

@ -19,9 +19,5 @@
position: relative;
top: 5px;
left: -6px;
.language-icon {
margin-right: 10px;
}
}
}

View File

@ -0,0 +1,82 @@
// LIBS
const _ = require('lodash');
// MOCKS
const Icon = ReactMock();
// COMPONENTS
const Menu = requireUnit('core-components/menu', {
'core-components/icon': Icon
});
describe('Menu component', function () {
let menu, items, icons;
function renderMenu(props) {
let defaultProps = {
items: [
{content: 'First Item', icon: 'ICON_1'},
{content: 'Second Item', icon: 'ICON_2'},
{content: 'Third Item', icon: 'ICON_3'},
{content: 'Fourth Item', icon: 'ICON_4'}
]
};
menu = TestUtils.renderIntoDocument(
<Menu {..._.extend(defaultProps, props)} />
);
items = TestUtils.scryRenderedDOMComponentsWithTag(menu, 'li');
icons = TestUtils.scryRenderedComponentsWithType(menu, Icon);
}
it('should render items with icons', function () {
renderMenu({
items: [
{content: 'First Item', icon: 'ICON_1'},
{content: 'Second Item', icon: 'ICON_2'},
{content: 'Third Item', icon: 'ICON_3'},
{content: 'Fourth Item', icon: 'ICON_4'}
]
});
expect(items.length).to.equal(4);
expect(items[0].textContent).to.equal('First Item');
expect(items[1].textContent).to.equal('Second Item');
expect(items[2].textContent).to.equal('Third Item');
expect(items[3].textContent).to.equal('Fourth Item');
items.forEach((item, index) => {
expect(item.className).to.contain('menu__list-item');
expect(item.childNodes[0]).to.equal(ReactDOM.findDOMNode(icons[index]));
});
});
it('should add custom class if passsed', function () {
renderMenu({
className: 'CUSTOM_CLASSNAME'
});
expect(ReactDOM.findDOMNode(menu).className).to.contain('CUSTOM_CLASSNAME');
});
it('should add selected class to selected index', function () {
renderMenu({
selectedIndex: 2
});
expect(ReactDOM.findDOMNode(items[2]).className).to.contain('menu__list-item_selected')
});
it('should call onItemClick if an item is clicked', function () {
let callback = stub();
renderMenu({
onItemClick: callback
});
TestUtils.Simulate.click(ReactDOM.findDOMNode(items[2]));
expect(callback).to.have.been.calledWith(2);
});
});

View File

@ -1,20 +1,18 @@
import React from 'react';
import classNames from 'classnames';
import _ from 'lodash';
import {Motion, spring} from 'react-motion';
const React = require('react');
const classNames = require('classnames');
const _ = require('lodash');
const {Motion, spring} = require('react-motion');
const callback = require('lib-core/callback');
import callback from 'lib-core/callback';
const Menu = require('core-components/menu');
const Icon = require('core-components/icon');
let DropDown = React.createClass({
const DropDown = React.createClass({
propTypes: {
defaultSelectedIndex: React.PropTypes.number,
selectedIndex: React.PropTypes.number,
items: React.PropTypes.arrayOf(React.PropTypes.shape({
content: React.PropTypes.node.isRequired,
icon: React.PropTypes.string
})).isRequired
items: Menu.propTypes.items
},
getDefaultProps() {
@ -48,12 +46,11 @@ let DropDown = React.createClass({
render() {
let animation = this.getAnimationStyles();
let selectedItem = this.props.items[this.getSelectedIndex()];
return (
<div className={this.getClass()}>
<div className="drop-down--current" onBlur={this.handleBlur} onClick={this.handleClick} tabIndex="0" ref="current">
{this.props.items[this.getSelectedIndex()].content}
</div>
{this.renderCurrentItem(selectedItem)}
<Motion defaultStyle={animation.defaultStyle} style={animation.style}>
{this.renderList}
</Motion>
@ -63,27 +60,37 @@ let DropDown = React.createClass({
renderList({opacity, translateY}) {
let style = { opacity: opacity, transform: `translateY(${translateY}px)`};
let menuProps = {
items: this.props.items,
onItemClick: this.handleItemClick,
onMouseDown: this.handleListMouseDown
};
return (
<div className="drop-down--list-container" style={style}>
<ul className="drop-down--list">
{this.props.items.map(this.renderItem)}
</ul>
<Menu {...menuProps} />
</div>
);
},
renderItem(item, index) {
renderCurrentItem(item) {
var iconNode = null;
if (item.icon) {
iconNode = <Icon className="drop-down--current-item-icon" name={item.icon} />;
}
return (
<li {...this.getItemProps(index)}>
{item.content}
</li>
<div className="drop-down--current-item" onBlur={this.handleBlur} onClick={this.handleClick} tabIndex="0">
{iconNode}{item.content}
</div>
);
},
getClass() {
let classes = {
'drop-down': true,
'drop-down_closed': !this.state.opened,
[this.props.className]: (this.props.className)
};
@ -91,15 +98,6 @@ let DropDown = React.createClass({
return classNames(classes);
},
getItemProps(index) {
return {
className: 'drop-down--list-item',
onClick: this.handleItemClick.bind(this, index),
onMouseDown: this.handleItemMouseDown,
key: index
};
},
handleBlur() {
this.setState({
opened: false
@ -125,7 +123,7 @@ let DropDown = React.createClass({
}
},
handleItemMouseDown(event) {
handleListMouseDown(event) {
event.preventDefault();
},

View File

@ -7,32 +7,28 @@
user-select: none;
cursor: pointer;
&--current {
&--current-item {
background-color: $light-grey;
border-radius: 4px 4px 0 0;
color: $primary-black;
padding: 6px;
}
&--list-container {
background-color: white;
position: absolute;
width: 150px;
&--current-item-icon {
margin-right: 8px;
margin-bottom: 2px;
}
&--list {
color: $dark-grey;
margin: 0;
padding: 0;
list-style-type: none;
&--list-container {
position: absolute;
width: 150px;
z-index: 5;
}
&-item {
padding: 8px;
&_closed {
&:hover {
background-color: $primary-red;
color: white;
}
.drop-down--list-container {
pointer-events: none;
}
}
}

View File

@ -1,17 +1,45 @@
import React from 'react';
const React = require('react');
const classNames = require('classnames');
let Icon = React.createClass({
const Icon = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired
name: React.PropTypes.string.isRequired,
size: React.PropTypes.number
},
getDefaultProps() {
return {
size: 0
};
},
render() {
return (
<img className="language-icon" src={`../images/icons/${this.props.name}.png`} />
);
}
return (this.props.name.length > 2) ? this.renderFontIcon() : this.renderFlag();
},
renderFontIcon() {
return (
<span className={this.getFontIconClass()} aria-hidden="true" />
);
},
renderFlag() {
return (
<img className={this.props.className} src={`../images/icons/${this.props.name}.png`} aria-hidden="true" />
);
},
getFontIconClass() {
let classes = {
'fa': true,
['fa-' + this.props.name]: true,
['fa-' + this.props.size]: true,
[this.props.className]: true
};
return classNames(classes);
}
});
export default Icon;

View File

@ -0,0 +1,91 @@
const React = require('react');
const _ = require('lodash');
const classNames = require('classnames');
const Icon = require('core-components/icon');
const Menu = React.createClass({
propTypes: {
type: React.PropTypes.oneOf(['primary', 'secondary']),
items: React.PropTypes.arrayOf(React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
icon: React.PropTypes.string
})).isRequired,
selectedIndex: React.PropTypes.number
},
getDefaultProps() {
return {
type: 'primary',
selectedIndex: 0
};
},
render() {
return (
<ul {...this.getProps()}>
{this.props.items.map(this.renderListItem)}
</ul>
)
},
renderListItem(item, index) {
let iconNode = null;
if (item.icon) {
iconNode = <Icon className="menu__icon" name={item.icon} />;
}
return (
<li {...this.getItemProps(index)}>
{iconNode}{item.content}
</li>
);
},
getProps() {
var props = _.clone(this.props);
props.className = this.getClass();
props.type = null;
return props;
},
getClass() {
let classes = {
'menu': true,
'menu_secondary': (this.props.type === 'secondary')
};
classes[this.props.className] = true;
return classNames(classes);
},
getItemProps(index) {
return {
className: this.getItemClass(index),
onClick: this.handleItemClick.bind(this, index),
key: index
};
},
getItemClass(index) {
let classes = {
'menu__list-item': true,
'menu__list-item_selected': (this.props.selectedIndex === index)
};
return classNames(classes);
},
handleItemClick(index) {
if (this.props.onItemClick) {
this.props.onItemClick(index);
}
}
});
export default Menu;

View File

@ -0,0 +1,30 @@
@import "../scss/vars";
.menu {
background-color: white;
color: $dark-grey;
margin: 0;
padding: 0;
list-style-type: none;
cursor: pointer;
&__list-item {
padding: 8px;
&:hover {
background-color: $primary-red;
color: white;
}
}
&__icon {
margin-right: 8px;
margin-bottom: 2px;
}
&_secondary {
.menu__list-item:hover {
background-color: $secondary-blue;
}
}
}

View File

@ -1,9 +1,10 @@
const React = require('react');
const _ = require('lodash');
module.exports = function () {
return React.createClass({
module.exports = function (options) {
return React.createClass(_.extend({
render() {
return <div {...this.props} />;
return <div {...this.props}></div>;
}
});
}, options));
};

View File

@ -5,4 +5,4 @@
@import 'scss/font_awesome/font-awesome';
@import 'core-components/*';
@import 'app/main/*';
@import 'app/*';

View File

@ -3,6 +3,7 @@ $primary-red: #DD5555;
$secondary-red: #FB6362;
$primary-blue: #414A59;
$secondary-blue: #20B8c5;
$light-grey: #EEEEEE;
$grey: #E7E7E7;