Merge pull request #201 from react-toolbox/ripple-decorator

Ripple decorator
old
Javi Velasco 2015-12-07 23:57:59 +01:00
commit fe5f0f8071
20 changed files with 239 additions and 271 deletions

View File

@ -251,24 +251,3 @@
transition-timing-function: $animation-curve-default;
transition-duration: $duration;
}
// The frames are this way to prevent a flicker in Safari
// See https://goo.gl/5luFDk
@mixin ripple-loading($name, $width, $height, $opacity: 0.3) {
@keyframes #{$name} {
0% {
opacity: $opacity;
transform: translate3d(-50%, -50%, 0) scale(0);
}
95% {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(1);
}
100% {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(0);
}
}
}

View File

@ -19,7 +19,6 @@ class Button extends React.Component {
mini: React.PropTypes.bool,
primary: React.PropTypes.bool,
raised: React.PropTypes.bool,
ripple: React.PropTypes.bool,
type: React.PropTypes.string
};
@ -30,13 +29,7 @@ class Button extends React.Component {
floating: false,
mini: false,
primary: false,
raised: false,
ripple: true
};
handleMouseDown = (event) => {
if (this.refs.ripple) this.refs.ripple.start(event);
if (this.props.onMouseDown) this.props.onMouseDown(event);
raised: false
};
handleMouseUp = () => {
@ -45,7 +38,7 @@ class Button extends React.Component {
render () {
const { accent, children, className, flat, floating, href, icon,
inverse, label, mini, primary, raised, ripple, ...others} = this.props;
inverse, label, mini, primary, raised, ...others} = this.props;
const element = href ? 'a' : 'button';
const level = primary ? 'primary' : accent ? 'accent' : 'neutral';
const shape = flat ? 'flat' : raised ? 'raised' : floating ? 'floating' : 'flat';
@ -61,13 +54,11 @@ class Button extends React.Component {
ref: 'button',
className: classes,
disabled: this.props.disabled,
onMouseDown: this.handleMouseDown,
onMouseUp: this.handleMouseUp,
'data-react-toolbox': 'button'
};
return React.createElement(element, props,
ripple ? <Ripple ref='ripple' /> : null,
icon ? <FontIcon className={style.icon} value={icon}/> : null,
label,
children
@ -75,4 +66,4 @@ class Button extends React.Component {
}
}
export default Button;
export default Ripple({centered: false})(Button);

View File

@ -4,7 +4,7 @@ import FontIcon from '../font_icon';
import Ripple from '../ripple';
import style from './style';
class Button extends React.Component {
class IconButton extends React.Component {
static propTypes = {
accent: React.PropTypes.bool,
children: React.PropTypes.node,
@ -14,20 +14,13 @@ class Button extends React.Component {
icon: React.PropTypes.string,
inverse: React.PropTypes.bool,
primary: React.PropTypes.bool,
ripple: React.PropTypes.bool,
type: React.PropTypes.string
};
static defaultProps = {
accent: false,
className: '',
primary: false,
ripple: true
};
handleMouseDown = (event) => {
if (this.refs.ripple) this.refs.ripple.start(event);
if (this.props.onMouseDown) this.props.onMouseDown(event);
primary: false
};
handleMouseUp = () => {
@ -35,7 +28,7 @@ class Button extends React.Component {
};
render () {
const {accent, children, className, href, icon, inverse, primary, ripple, ...others} = this.props;
const {accent, children, className, href, icon, inverse, primary, ...others} = this.props;
const element = href ? 'a' : 'button';
const level = primary ? 'primary' : accent ? 'accent' : 'neutral';
const classes = ClassNames([style.toggle, style[level]], {[style.inverse]: inverse}, className);
@ -46,16 +39,15 @@ class Button extends React.Component {
ref: 'button',
className: classes,
disabled: this.props.disabled,
onMouseDown: this.handleMouseDown,
onMouseUp: this.handleMouseUp,
'data-react-toolbox': 'button'
};
return React.createElement(element, props,
ripple ? <Ripple ref='ripple' centered /> : null,
icon ? <FontIcon className={style.icon} value={icon}/> : children
icon ? <FontIcon className={style.icon} value={icon}/> : null,
children
);
}
}
export default Button;
export default Ripple({centered: true})(IconButton);

View File

@ -0,0 +1,18 @@
import React from 'react';
import ClassNames from 'classnames';
import Ripple from '../ripple';
import style from './style';
const Check = ({checked, children, onMouseDown}) => {
const className = ClassNames(style.check, {
[style.checked]: checked
});
return <div data-role='checkbox' onMouseDown={onMouseDown} className={className}>{children}</div>;
};
export default Ripple({
className: style.ripple,
spread: 2.6,
centered: true
})(Check);

View File

@ -1,7 +1,6 @@
import React from 'react';
import ClassNames from 'classnames';
import Ripple from '../ripple';
import events from '../utils/events';
import Check from './Check';
import style from './style';
class Checkbox extends React.Component {
@ -22,21 +21,10 @@ class Checkbox extends React.Component {
disabled: false
};
handleClick = (event) => {
events.pauseEvent(event);
handleToggle = (event) => {
if (event.pageX !== 0 && event.pageY !== 0) this.blur();
if (!this.props.disabled && this.props.onChange) {
const value = !this.refs.input.checked;
this.props.onChange(value, event);
}
};
handleInputClick = (event) => {
events.pauseEvent(event);
};
handleMouseDown = (event) => {
if (!this.props.disabled) {
this.refs.ripple.start(event);
this.props.onChange(!this.props.checked, event);
}
};
@ -49,30 +37,22 @@ class Checkbox extends React.Component {
}
render () {
const fieldClassName = ClassNames(style.field, {
const { onChange, ...others } = this.props;
const className = ClassNames(style.field, {
[style.disabled]: this.props.disabled
}, this.props.className);
const checkboxClassName = ClassNames(style.check, {
[style.checked]: this.props.checked
});
return (
<label
data-react-toolbox='checkbox'
className={fieldClassName}
onClick={this.handleClick}
>
<label data-react-toolbox='checkbox' className={className}>
<input
ref='input'
{...this.props}
{...others}
className={style.input}
onClick={this.handleInputClick}
onClick={this.handleToggle}
readOnly
ref='input'
type='checkbox'
/>
<span data-role='checkbox' className={checkboxClassName} onMouseDown={this.handleMouseDown}>
<Ripple ref='ripple' data-role='ripple' className={style.ripple} spread={3} centered />
</span>
<Check checked={this.props.checked} disabled={this.props.disabled}/>
{this.props.label ? <span data-role='label' className={style.text}>{this.props.label}</span> : null}
</label>
);

View File

@ -21,7 +21,6 @@ $calendar-primary-contrast-color: $calendar-primary-contrast !default;
$calendar-primary-hover-color: rgba($calendar-primary, 0.21) !default;
$calendar-arrows-color: $palette-grey-600 !default;
$calendar-arrows-font-size: 2 * $unit;
$calendar-arrows-ripple-duration: 450ms;
$calendar-year-font-size: 2.4;
$calendar-day-font-size: 1.3 * $unit;
$calendar-day-disable-opacity: 0.25;

View File

@ -91,8 +91,3 @@
.month {
background-color: $calendar-primary-contrast-color;
}
.ripple {
opacity: .5;
transition-duration: $calendar-arrows-ripple-duration;
}

View File

@ -9,6 +9,7 @@ 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.string,
@ -32,12 +33,6 @@ class ListItem extends React.Component {
}
};
handleMouseDown = (event) => {
if (this.refs.ripple && !this.props.disabled) {
this.refs.ripple.start(event);
}
};
renderContent () {
const className = ClassNames(style.item, {
[style.withLegend]: this.props.legend,
@ -50,7 +45,6 @@ class ListItem extends React.Component {
{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.ripple ? <Ripple ref='ripple' className={style.ripple} spread={2} /> : null}
{this.props.rightIcon ? <FontIcon className={`${style.icon} ${style.right}`} value={this.props.rightIcon} /> : null}
</span>
);
@ -58,11 +52,15 @@ class ListItem extends React.Component {
render () {
return (
<li className={style['list-item']} onClick={this.handleClick} onMouseDown={this.handleMouseDown}>
<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>
);
}
}
export default ListItem;
export default Ripple({
className: style.ripple,
centered: false
})(ListItem);

View File

@ -32,11 +32,16 @@
.list + & {
margin-top: - $list-vertical-padding;
}
.list-item ~ & {
.listItem ~ & {
margin: $list-vertical-padding 0;
}
}
.listItem {
position: relative;
overflow: hidden;
}
.item {
position: relative;
display: flex;
@ -79,7 +84,7 @@
}
.ripple {
opacity: .1;
color: $color-text-secondary;
}
.text {

View File

@ -1,16 +1,17 @@
import React from 'react';
import FontIcon from '../font_icon';
import ClassNames from 'classnames';
import Ripple from '../ripple';
import style from './style.menu_item';
class MenuItem extends React.Component {
static propTypes = {
caption: React.PropTypes.string.isRequired,
children: React.PropTypes.any,
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
icon: React.PropTypes.string,
onClick: React.PropTypes.func,
ripple: React.PropTypes.bool,
selected: React.PropTypes.bool,
shortcut: React.PropTypes.string
};
@ -18,7 +19,6 @@ class MenuItem extends React.Component {
static defaultProps = {
className: '',
disabled: false,
ripple: false,
selected: false
};
@ -28,32 +28,24 @@ class MenuItem extends React.Component {
}
};
handleMouseDown = (event) => {
if (this.props.ripple && !this.props.disabled) {
this.refs.ripple.start(event);
}
};
render () {
let className = style.root;
if (this.props.selected) className += ` ${style.selected}`;
if (this.props.disabled) className += ` ${style.disabled}`;
if (this.props.className) className += ` ${this.props.className}`;
const {icon, caption, children, shortcut, selected, disabled, ...others} = this.props;
const className = ClassNames(style.root, {
[style.selected]: selected,
[style.disabled]: disabled
}, this.props.className);
return (
<li
data-react-toolbox='menu-item'
className={className}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
>
{this.props.icon ? <FontIcon value={this.props.icon} className={style.icon}/> : null}
<span className={style.caption}>{this.props.caption}</span>
{this.props.shortcut ? <small className={style.shortcut}>{this.props.shortcut}</small> : null}
{this.props.ripple ? <Ripple ref='ripple' className={style.ripple} spread={2.5} /> : null}
<li {...others} data-react-toolbox='menu-item' className={className} onClick={this.handleClick}>
{icon ? <FontIcon value={icon} className={style.icon}/> : null}
<span className={style.caption}>{caption}</span>
{shortcut ? <small className={style.shortcut}>{shortcut}</small> : null}
{children}
</li>
);
}
}
export default MenuItem;
export default Ripple({
className: style.ripple
})(MenuItem);

View File

@ -38,5 +38,5 @@
}
.ripple {
opacity: .1;
color: $color-text-secondary;
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import Ripple from '../ripple';
import style from './style';
const Radio = ({checked, children, onMouseDown}) => {
const className = style[checked ? 'radio-checked' : 'radio'];
return <div data-role='radio' onMouseDown={onMouseDown} className={className}>{children}</div>;
};
export default Ripple({
className: style.ripple,
spread: 2.6,
centered: true
})(Radio);

View File

@ -1,8 +1,7 @@
import React from 'react';
import ClassNames from 'classnames';
import Ripple from '../ripple';
import Radio from './Radio';
import style from './style';
import events from '../utils/events';
class RadioButton extends React.Component {
static propTypes = {
@ -23,23 +22,10 @@ class RadioButton extends React.Component {
disabled: false
};
handleChange = (event) => {
if (!this.props.checked && this.props.onChange) {
this.props.onChange(event, this);
}
};
handleClick = (event) => {
events.pauseEvent(event);
if (!this.props.disabled) this.handleChange(event);
};
handleInputClick = (event) => {
events.pauseEvent(event);
};
handleMouseDown = (event) => {
if (!this.props.disabled) this.refs.ripple.start(event);
const {checked, disabled, onChange} = this.props;
if (event.pageX !== 0 && event.pageY !== 0) this.blur();
if (!disabled && !checked && onChange) onChange(event, this);
};
blur () {
@ -51,22 +37,20 @@ class RadioButton extends React.Component {
}
render () {
const labelClassName = ClassNames(style[this.props.disabled ? 'disabled' : 'field'], this.props.className);
const radioClassName = style[this.props.checked ? 'radio-checked' : 'radio'];
const className = ClassNames(style[this.props.disabled ? 'disabled' : 'field'], this.props.className);
const { onChange, ...others } = this.props;
return (
<label className={labelClassName} onClick={this.handleClick}>
<label className={className}>
<input
{...this.props}
ref='input'
{...others}
className={style.input}
onChange={this.handleChange}
onClick={this.handleInputClick}
onClick={this.handleClick}
readOnly
ref='input'
type='radio'
/>
<span role='radio' className={radioClassName} onMouseDown={this.handleMouseDown}>
<Ripple ref='ripple' role='ripple' className={style.ripple} spread={3} centered />
</span>
<Radio checked={this.props.checked} disabled={this.props.disabled}/>
{this.props.label ? <span className={style.text}>{this.props.label}</span> : null}
</label>
);

View File

@ -28,15 +28,12 @@
margin: 0;
border: 0;
opacity: 0;
appearance: none;
&:focus:not(&:active) {
+ .radio {
box-shadow: 0 0 0 $unit $radio-focus-color;
}
+ .radio-checked {
box-shadow: 0 0 0 $unit $radio-checked-focus-color;
}
&:focus ~ .radio {
box-shadow: 0 0 0 $unit $radio-focus-color;
}
&:focus ~ .radio-checked {
box-shadow: 0 0 0 $unit $radio-checked-focus-color;
}
}

View File

@ -1,84 +1,122 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ClassNames from 'classnames';
import prefixer from '../utils/prefixer';
import style from './style';
import prefixer from '../utils/prefixer';
class Ripple extends React.Component {
static propTypes = {
centered: React.PropTypes.bool,
className: React.PropTypes.string,
loading: React.PropTypes.bool,
spread: React.PropTypes.number
};
const defaults = {
centered: false,
className: '',
spread: 2
};
static defaultProps = {
centered: false,
className: '',
loading: false,
spread: 2
};
const Ripple = (options = {}) => {
const {
centered: defaultCentered,
className: defaultClassName,
spread: defaultSpread
} = {...defaults, ...options};
state = {
active: false,
left: null,
restarting: false,
top: null,
width: null
};
return ComposedComponent => {
return class RippledComponent extends React.Component {
static propTypes = {
children: React.PropTypes.any,
disabled: React.PropTypes.bool,
ripple: React.PropTypes.bool,
rippleCentered: React.PropTypes.bool,
rippleClassName: React.PropTypes.string,
rippleSpread: React.PropTypes.number
};
handleEnd = () => {
document.removeEventListener(this.touch ? 'touchend' : 'mouseup', this.handleEnd);
this.setState({active: false});
};
static defaultProps = {
disabled: false,
ripple: true,
rippleCentered: defaultCentered,
rippleClassName: defaultClassName,
rippleSpread: defaultSpread
};
start = ({pageX, pageY}, touch = false) => {
if (!this._isTouchRippleReceivingMouseEvent(touch)) {
this.touch = touch;
document.addEventListener(this.touch ? 'touchend' : 'mouseup', this.handleEnd);
const {top, left, width} = this._getDescriptor(pageX, pageY);
this.setState({active: false, restarting: true, top, left, width}, () => {
this.refs.ripple.offsetWidth; //eslint-disable-line no-unused-expressions
this.setState({active: true, restarting: false});
});
}
};
state = {
active: false,
left: null,
restarting: false,
top: null,
width: null
};
_isTouchRippleReceivingMouseEvent (touch) {
return this.touch && !touch;
}
handleEnd = () => {
document.removeEventListener(this.touch ? 'touchend' : 'mouseup', this.handleEnd);
this.setState({active: false});
};
_getDescriptor (pageX, pageY) {
const {left, top, height, width} = ReactDOM.findDOMNode(this).getBoundingClientRect();
return {
left: this.props.centered ? 0 : pageX - left - width / 2 - window.scrollX,
top: this.props.centered ? 0 : pageY - top - height / 2 - window.scrollY,
width: width * this.props.spread
start = ({pageX, pageY}, touch = false) => {
if (!this._isTouchRippleReceivingMouseEvent(touch)) {
this.touch = touch;
document.addEventListener(this.touch ? 'touchend' : 'mouseup', this.handleEnd);
const {top, left, width} = this._getDescriptor(pageX, pageY);
this.setState({active: false, restarting: true, top, left, width}, () => {
this.refs.ripple.offsetWidth; //eslint-disable-line no-unused-expressions
this.setState({active: true, restarting: false});
});
}
};
_isTouchRippleReceivingMouseEvent (touch) {
return this.touch && !touch;
}
_getDescriptor (pageX, pageY) {
const {left, top, height, width} = ReactDOM.findDOMNode(this).getBoundingClientRect();
const {rippleCentered: centered, rippleSpread: spread} = this.props;
return {
left: centered ? 0 : pageX - left - width / 2 - window.scrollX,
top: centered ? 0 : pageY - top - height / 2 - window.scrollY,
width: width * spread
};
}
handleMouseDown = (event) => {
if (!this.props.disabled) this.start(event);
if (this.props.onMouseDown) this.props.onMouseDown(event);
};
render () {
if (!this.props.ripple) {
return <ComposedComponent {...this.props} />;
} else {
const {
children,
ripple,
rippleClassName: className,
rippleCentered: centered,
rippleSpread: spread,
...other
} = this.props;
const rippleClassName = ClassNames(style.normal, {
[style.active]: this.state.active,
[style.restarting]: this.state.restarting
}, className);
const { left, top, width } = this.state;
const scale = this.state.restarting ? 0 : 1;
const rippleStyle = prefixer({
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 ref='ripple' role='ripple' className={rippleClassName} style={rippleStyle} />
</span>
</ComposedComponent>
);
}
}
};
}
render () {
const { left, top, width } = this.state;
const scale = this.state.restarting ? 0 : 1;
let rippleStyle = {width, height: width};
if (!this.props.loading) {
rippleStyle = prefixer({
transform: `translate3d(${-width / 2 + left}px, ${-width / 2 + top}px, 0) scale(${scale})`
}, rippleStyle);
}
const className = ClassNames(style[this.props.loading ? 'loading' : 'normal'], {
[style.active]: this.state.active,
[style.restarting]: this.state.restarting
}, this.props.className);
return (
<span data-react-toolbox='ripple' className={style.wrapper}>
<span ref='ripple' role='ripple' className={className} style={rippleStyle} />
</span>
);
}
}
};
};
export default Ripple;

View File

@ -1,3 +1,3 @@
$ripple-duration: 1.2s;
$ripple-final-opacity: .3;
$ripple-size: 15 * $unit;
$ripple-duration: 800ms !default;
$ripple-final-opacity: .3 !default;
$ripple-size: 15 * $unit !default;

View File

@ -38,15 +38,3 @@
transition-property: opacity, transform;
}
}
.loading {
@extend %ripple;
@include ripple-loading(ripple, $ripple-size, $ripple-size);
width: $ripple-size;
height: $ripple-size;
opacity: $ripple-final-opacity;
animation-name: ripple;
animation-duration: $ripple-duration;
animation-timing-function: $animation-curve-linear-out-slow-in;
animation-iteration-count: infinite;
}

View File

@ -1,7 +1,6 @@
import React from 'react';
import Ripple from '../ripple';
import Thumb from './Thumb';
import style from './style';
import events from '../utils/events';
class Switch extends React.Component {
static propTypes = {
@ -21,22 +20,13 @@ class Switch extends React.Component {
disabled: false
};
handleChange = (event) => {
events.pauseEvent(event);
if (this.props.onChange && !this.props.disabled) {
const value = !this.refs.input.checked;
this.props.onChange(value, event);
handleToggle = (event) => {
if (event.pageX !== 0 && event.pageY !== 0) this.blur();
if (!this.props.disabled && this.props.onChange) {
this.props.onChange(!this.props.checked, event);
}
};
handleInputClick = (event) => {
events.pauseEvent(event);
};
handleMouseDown = (event) => {
if (!this.props.disabled) this.refs.ripple.start(event);
};
blur () {
this.refs.input.blur();
}
@ -46,29 +36,24 @@ class Switch extends React.Component {
}
render () {
let labelClassName = style[this.props.disabled ? 'disabled' : 'field'];
let className = style[this.props.disabled ? 'disabled' : 'field'];
const switchClassName = style[this.props.checked ? 'on' : 'off'];
if (this.props.className) labelClassName += ` ${this.props.className}`;
const { onChange, ...others } = this.props;
if (this.props.className) className += ` ${this.props.className}`;
return (
<label
data-react-toolbox='checkbox'
className={labelClassName}
onClick={this.handleChange}
>
<label data-react-toolbox='checkbox' className={className}>
<input
{...this.props}
ref='input'
{...others}
checked={this.props.checked}
className={style.input}
onChange={this.handleChange}
onClick={this.handleInputClick}
onClick={this.handleToggle}
readOnly
ref='input'
type='checkbox'
/>
<span role='switch' className={switchClassName}>
<span role='thumb' className={style.thumb} onMouseDown={this.handleMouseDown}>
<Ripple ref='ripple' role='ripple' className={style.ripple} spread={2.4} centered />
</span>
<Thumb disabled={this.props.disabled} />
</span>
{this.props.label ? <span className={style.text}>{this.props.label}</span> : null}
</label>

View File

@ -0,0 +1,13 @@
import React from 'react';
import Ripple from '../ripple';
import style from './style';
const Check = ({children, onMouseDown}) => (
<span role='thumb' className={style.thumb} onMouseDown={onMouseDown}>{children}</span>
);
export default Ripple({
className: style.ripple,
spread: 2.6,
centered: true
})(Check);

View File

@ -109,7 +109,7 @@
.text {
color: $switch-disabled-text-color;
}
.switch-on, .switch-off {
.on, .off {
cursor: auto;
background: $switch-disabled-track-color;
}