Merge pull request #282 from kagux/feature/customizable-list-2

List Item customization
old
Javi Velasco 2016-01-28 00:26:09 +01:00
commit d0accc0c12
12 changed files with 318 additions and 92 deletions

View File

@ -7,7 +7,7 @@ const FontIcon = ({ children, className, value, ...other}) => {
className
);
return (
<span className={classes} {...other} >
<span className={classes} {...other} data-react-toolbox='font-icon'>
{value}
{children}
</span>

View File

@ -1,30 +1,21 @@
import React from 'react';
import ClassNames from 'classnames';
import FontIcon from '../font_icon';
import ListItemContent from './ListItemContent';
import ListItemLayout from './ListItemLayout';
import Ripple from '../ripple';
import style from './style';
class ListItem extends React.Component {
static propTypes = {
avatar: React.PropTypes.string,
caption: React.PropTypes.string.isRequired,
children: React.PropTypes.any,
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
leftIcon: React.PropTypes.any,
legend: React.PropTypes.string,
onClick: React.PropTypes.func,
rightIcon: React.PropTypes.any,
ripple: React.PropTypes.bool,
selectable: React.PropTypes.bool,
to: React.PropTypes.string
};
static defaultProps = {
disabled: false,
ripple: false,
selectable: false
ripple: false
};
handleClick = (event) => {
@ -33,28 +24,41 @@ class ListItem extends React.Component {
}
};
renderContent () {
const className = ClassNames(style.item, {
[style.withLegend]: this.props.legend,
[style.disabled]: this.props.disabled,
[style.selectable]: this.props.selectable
}, this.props.className);
groupChildren () {
const children = {
leftActions: [],
rightActions: [],
ignored: []
};
return (
<span className={className}>
{this.props.leftIcon ? <FontIcon className={`${style.icon} ${style.left}`} value={this.props.leftIcon} /> : null}
{this.props.avatar ? <img className={style.avatar} src={this.props.avatar} /> : null}
<ListItemContent caption={this.props.caption} legend={this.props.legend} />
{this.props.rightIcon ? <FontIcon className={`${style.icon} ${style.right}`} value={this.props.rightIcon} /> : null}
</span>
);
React.Children.forEach(this.props.children, (child, i) => {
if (!React.isValidElement(child)) {
return;
}
if (child.props.listItemIgnore) {
children.ignored.push(child);
return;
}
if (child.type === ListItemContent) {
children.itemContent = child;
return;
}
const bucket = children.itemContent ? 'rightActions' : 'leftActions';
children[bucket].push({...child, key: i});
});
return children;
}
render () {
const {onMouseDown, to, onClick, ripple, ...other} = this.props;
const children = this.groupChildren();
const content = <ListItemLayout {...children} {...other}/>;
return (
<li className={style.listItem} onClick={this.handleClick} onMouseDown={this.props.onMouseDown}>
{this.props.to ? <a href={this.props.to}>{this.renderContent()}</a> : this.renderContent()}
{this.props.children}
<li className={style.listItem} onClick={this.handleClick} onMouseDown={onMouseDown}>
{to ? <a href={this.props.to}>{content}</a> : content}
{children.ignored}
</li>
);
}
@ -62,6 +66,7 @@ class ListItem extends React.Component {
export default Ripple({
className: style.ripple,
centered: false
centered: false,
listItemIgnore: true
})(ListItem);
export {ListItem as RawListItem};

View File

@ -0,0 +1,21 @@
import React from 'react';
import style from './style';
const ListItemAction = ({action}) => {
const {onClick, onMouseDown} = action.props;
const stopRipple = onClick && !onMouseDown;
const stop = e => e.stopPropagation();
return (
<span className={style.itemAction} onMouseDown={stopRipple && stop} onClick={onClick && stop}>
{action}
</span>
);
};
ListItemAction.propTypes = {
};
ListItemAction.defaultProps = {
};
export default ListItemAction;

View File

@ -0,0 +1,22 @@
import React from 'react';
import style from './style';
import ListItemAction from './ListItemAction';
const ListItemActions = ({type, children}) => {
const validChildren = React.Children.toArray(children).filter(c => (
React.isValidElement(c)
));
return (
<span className={style[type]}>
{validChildren.map((action, i) => <ListItemAction key={i} action={action} />)}
</span>
);
};
ListItemActions.propTypes = {
children: React.PropTypes.any,
type: React.PropTypes.oneOf(['left', 'right'])
};
export default ListItemActions;

View File

@ -1,16 +1,41 @@
import React from 'react';
import style from './style';
import ListItemText from './ListItemText';
const ListItemContent = ({caption, legend}) => (
<span className={style.text}>
<span className={style.caption}>{caption}</span>
<span className={style.legend}>{legend}</span>
</span>
);
const types = ['auto', 'normal', 'large'];
ListItemContent.propTypes = {
caption: React.PropTypes.string.isRequired,
legend: React.PropTypes.any
};
class ListItemContent extends React.Component {
static propTypes = {
caption: React.PropTypes.string,
children: React.PropTypes.any,
legend: React.PropTypes.string,
type: React.PropTypes.oneOf(types)
};
type () {
const {type, children, caption, legend} = this.props;
let count = React.Children.count(children);
[caption, legend].forEach(s => count += s ? 1 : 0);
const typeIndex = Math.min(count, types.length);
return type || types[typeIndex];
}
render () {
const {children, caption, legend} = this.props;
const className = `${style.itemContentRoot} ${style[this.type()]}`;
return (
<span className={className}>
<span className={style.itemContent}>
{caption && <ListItemText primary>{caption}</ListItemText>}
{legend && <ListItemText>{legend}</ListItemText>}
{children}
</span>
</span>
);
}
}
export default ListItemContent;

View File

@ -0,0 +1,56 @@
import React from 'react';
import ClassNames from 'classnames';
import FontIcon from '../font_icon';
import Avatar from '../avatar';
import ListItemContent from './ListItemContent';
import ListItemActions from './ListItemActions';
import style from './style';
const ListItemLayout = (props) => {
const className = ClassNames(style.item, {
[style.disabled]: props.disabled,
[style.selectable]: props.selectable
}, props.className);
const leftActions = [
props.leftIcon && <FontIcon value={props.leftIcon} key='leftIcon'/>,
props.avatar && <Avatar image={props.avatar} key='avatar'/>,
...props.leftActions
];
const rightActions = [
props.rightIcon && <FontIcon value={props.rightIcon} key='rightIcon'/>,
...props.rightActions
];
const content = props.itemContent || <ListItemContent caption={props.caption} legend={props.legend} />;
return (
<span className={className}>
<ListItemActions type='left'>{leftActions}</ListItemActions>
{content}
<ListItemActions type='right'>{rightActions}</ListItemActions>
</span>
);
};
ListItemLayout.propTypes = {
avatar: React.PropTypes.string,
caption: React.PropTypes.string,
children: React.PropTypes.any,
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
itemContent: React.PropTypes.element,
leftActions: React.PropTypes.array,
leftIcon: React.PropTypes.any,
legend: React.PropTypes.string,
rightActions: React.PropTypes.array,
rightIcon: React.PropTypes.any,
selectable: React.PropTypes.bool,
to: React.PropTypes.string
};
ListItemLayout.defaultProps = {
disabled: false,
selectable: false
};
export default ListItemLayout;

View File

@ -0,0 +1,25 @@
import React from 'react';
import ClassNames from 'classnames';
import style from './style';
const ListItemText = ({className, primary, children, ...other}) => {
const _className = ClassNames(style.itemText, {[style.primary]: primary}, className);
return (
<span className={_className} {...other}>
{children}
</span>
);
};
ListItemText.propTypes = {
children: React.PropTypes.any,
className: React.PropTypes.string,
primary: React.PropTypes.bool
};
ListItemText.defaultProps = {
primary: false
};
export default ListItemText;

View File

@ -11,6 +11,8 @@ $list-item-hover-color: $palette-grey-200;
$list-item-legend-margin-top: .3 * $unit;
$list-item-icon-font-size: 2.4 * $unit;
$list-item-icon-size: 1.8 * $unit;
$list-item-right-icon-margin: $list-content-left-spacing - $list-horizontal-padding - $list-item-icon-size;
$list-item-right-icon-margin: $list-content-left-spacing - 2 * $list-horizontal-padding - $list-item-icon-size;
$list-item-right-checkbox-margin: $list-item-right-icon-margin + $list-horizontal-padding;
$list-item-avatar-height: 4 * $unit;
$list-item-avatar-margin: .8 * $unit;
$list-item-child-margin: .8 * $unit;

View File

@ -3,3 +3,5 @@ export ListItem from './ListItem';
export ListDivider from './ListDivider';
export ListCheckbox from './ListCheckbox';
export ListSubHeader from './ListSubHeader';
export ListItemText from './ListItemText';
export ListItemContent from './ListItemContent';

View File

@ -56,9 +56,6 @@
cursor: pointer;
background-color: $list-item-hover-color;
}
&.withLegend {
height: $list-item-min-height-legend;
}
&.disabled {
pointer-events: none;
&:not(.checkboxItem) {
@ -70,15 +67,74 @@
}
}
.left {
[data-react-toolbox='font-icon'] {
width: $list-item-icon-size;
}
& :last-child {
> [data-react-toolbox='font-icon'] {
margin-right: $list-item-right-icon-margin;
}
}
}
.right {
> :last-child {
margin-right: 0;
}
> :first-child {
margin-left: $list-horizontal-padding;
}
}
.left, .right {
display: flex;
flex: 0 0 auto;
align-items: center;
vertical-align: middle;
}
.itemAction {
display: flex;
margin: $list-item-child-margin $list-horizontal-padding $list-item-child-margin 0;
> * {
padding: 0;
}
> [data-react-toolbox='font-icon'] {
font-size: $list-item-icon-font-size;
color: $color-text-secondary;
}
}
.itemContentRoot {
display: block;
flex-grow: 1;
&.large {
height: $list-item-min-height-legend;
.itemContent {
display: block;
position: relative;
top: 50%;
transform: translateY(-50%)
}
}
}
.checkbox {
display: flex;
width: 100%;
height: 100%;
min-height: $list-item-min-height;
align-items: center;
margin: 0;
cursor: pointer;
> [data-role='checkbox'] {
margin-right: $list-item-right-icon-margin;
margin-right: $list-item-right-checkbox-margin;
}
> [data-role='label'] {
padding-left: 0;
@ -89,51 +145,18 @@
color: $color-text-secondary;
}
.text {
flex-grow: 1;
}
.caption {
.itemText {
display: block;
font-size: $font-size-normal;
color: $color-text;
}
.legend {
display: block;
padding-top: $list-item-legend-margin-top;
font-size: $font-size-small;
color: $color-text-secondary;
white-space: normal;
}
.avatar {
display: flex;
width: $list-item-avatar-height;
height: $list-item-avatar-height;
flex: 0 0 auto;
margin: $list-item-avatar-margin $list-horizontal-padding $list-item-avatar-margin 0;
overflow: hidden;
border-radius: 50%;
}
.right, .left {
display: flex;
align-items: center;
vertical-align: middle;
&.icon {
font-size: $list-item-icon-font-size;
&:not(.primary) {
font-size: $font-size-small;
color: $color-text-secondary;
}
}
.right {
margin-left: $list-horizontal-padding;
}
.left {
&.icon {
width: $list-item-icon-size;
margin-right: $list-item-right-icon-margin;
padding-top: $list-item-legend-margin-top;
white-space: normal;
}
&.primary {
font-size: $font-size-normal;
color: $color-text;
}
}

View File

@ -14,7 +14,8 @@ const Ripple = (options = {}) => {
const {
centered: defaultCentered,
className: defaultClassName,
spread: defaultSpread
spread: defaultSpread,
...props
} = {...defaults, ...options};
return ComposedComponent => {
@ -76,6 +77,7 @@ const Ripple = (options = {}) => {
}
handleMouseDown = (event) => {
console.log('ripple');
if (!this.props.disabled) this.start(event);
if (this.props.onMouseDown) this.props.onMouseDown(event);
};
@ -104,11 +106,10 @@ const Ripple = (options = {}) => {
transform: `translate3d(${-width / 2 + left}px, ${-width / 2 + top}px, 0) scale(${scale})`
}, {width, height: width});
return (
<ComposedComponent {...other} onMouseDown={this.handleMouseDown}>
{children ? children : null}
<span data-react-toolbox='ripple' className={style.wrapper}>
<span data-react-toolbox='ripple' className={style.wrapper} {...props}>
<span ref='ripple' role='ripple' className={rippleClassName} style={rippleStyle} />
</span>
</ComposedComponent>

View File

@ -1,5 +1,8 @@
import React from 'react';
import { ListCheckbox, ListSubHeader, List, ListItem, ListDivider } from '../../components/list';
import { ListCheckbox, ListSubHeader, List, ListItem, ListDivider, ListItemText, ListItemContent } from '../../components/list';
import Avatar from '../../components/avatar';
import FontIcon from '../../components/font_icon';
import {Button} from '../../components/button';
const listStyle = {
border: '1px solid #EEE',
@ -138,6 +141,47 @@ class ListTest extends React.Component {
<ListItem caption='Other people' />
</List>
</div>
<h5> List with custom components </h5>
<p> Using custom components in list item </p>
<div style={listStyle}>
<List ripple selectable>
<ListItem leftIcon='send' rightIcon='done' caption='Reference item'/>
<ListItem rightIcon='done' caption='Item with custom left icons'>
<FontIcon value='send' />
<Avatar image='https://pbs.twimg.com/profile_images/459485216499720192/ufS4YGOY_400x400.png'/>
</ListItem>
<ListItem leftIcon='send'>
<ListItemContent caption='custom right icons' legend='ListItemContent acts as a divider'/>
<FontIcon value='done' />
<FontIcon value='undo' />
</ListItem>
<ListItem leftIcon='mail' rightIcon='create'>
<ListItemContent>
<ListItemText primary> Custom Caption </ListItemText>
</ListItemContent>
</ListItem>
<ListItem leftIcon='save' rightIcon='delete'>
<ListItemContent>
<ListItemText primary onClick={() => {console.log('clicked caption');}}>
Custom caption with events
</ListItemText>
<ListItemText> Custom legend with correct height </ListItemText>
</ListItemContent>
</ListItem>
<ListItem caption='Item with a button'>
<Button icon='save' label='save' onClick={() => console.log('clicked button')}/>
</ListItem>
<ListItem caption='Item with overlayed click events' onClick={() => console.log('clicked row')}>
<FontIcon value='send' onClick={() => console.log('clicked icon')}/>
<Avatar
image='https://pbs.twimg.com/profile_images/459485216499720192/ufS4YGOY_400x400.png'
onMouseDown={() => console.log('avatar mouse down, should see ripple')}
onClick={() => console.log('clicked avatar')}
/>
</ListItem>
</List>
</div>
</section>
);
}