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 className
); );
return ( return (
<span className={classes} {...other} > <span className={classes} {...other} data-react-toolbox='font-icon'>
{value} {value}
{children} {children}
</span> </span>

View File

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

View File

@ -56,9 +56,6 @@
cursor: pointer; cursor: pointer;
background-color: $list-item-hover-color; background-color: $list-item-hover-color;
} }
&.withLegend {
height: $list-item-min-height-legend;
}
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
&:not(.checkboxItem) { &: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 { .checkbox {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%;
min-height: $list-item-min-height; min-height: $list-item-min-height;
align-items: center; align-items: center;
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
> [data-role='checkbox'] { > [data-role='checkbox'] {
margin-right: $list-item-right-icon-margin; margin-right: $list-item-right-checkbox-margin;
} }
> [data-role='label'] { > [data-role='label'] {
padding-left: 0; padding-left: 0;
@ -89,51 +145,18 @@
color: $color-text-secondary; color: $color-text-secondary;
} }
.text { .itemText {
flex-grow: 1;
}
.caption {
display: block; display: block;
font-size: $font-size-normal;
color: $color-text;
}
.legend { &:not(.primary) {
display: block; font-size: $font-size-small;
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;
color: $color-text-secondary; color: $color-text-secondary;
} padding-top: $list-item-legend-margin-top;
} white-space: normal;
}
.right {
margin-left: $list-horizontal-padding; &.primary {
} font-size: $font-size-normal;
color: $color-text;
.left {
&.icon {
width: $list-item-icon-size;
margin-right: $list-item-right-icon-margin;
} }
} }

View File

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

View File

@ -1,5 +1,8 @@
import React from 'react'; 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 = { const listStyle = {
border: '1px solid #EEE', border: '1px solid #EEE',
@ -138,6 +141,47 @@ class ListTest extends React.Component {
<ListItem caption='Other people' /> <ListItem caption='Other people' />
</List> </List>
</div> </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> </section>
); );
} }