Skip to content

Commit

Permalink
fix slider implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lipp committed Sep 21, 2016
1 parent 7858096 commit 2aa6fe9
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 99 deletions.
124 changes: 75 additions & 49 deletions components/slider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,86 @@ import React from 'react'
/**
* Slider
*/

const clip = (v, min, max) => {
if (v < min) {
return min
}
if (v > max) {
return max
}
return v
}

export default class Slider extends React.Component {

static propTypes = {
value: React.PropTypes.number,
min: React.PropTypes.number.isRequired,
max: React.PropTypes.number.isRequired,
step: React.PropTypes.number,
onChange: React.PropTypes.func,
onMouseUp: React.PropTypes.func
onMove: React.PropTypes.func
}

static defaultProps = {
step: 1
defaultProps = {
value: 0
}

state = {
mouseDown: false,
isAnimating: false,
value: 50,
// ratio between actual dom element width and range value (0 -> 100)
ratio: 1
update = (value, func) => {
let newValue
if (this.props.step) {
const steps = (value - this.state.prevValue) / this.props.step
if (Math.round(steps)) {
newValue = this.state.prevValue + Math.round(steps) * this.props.step
}
} else {
newValue = value
}
if (newValue !== undefined && newValue !== this.state.prevValue) {
this.setState({value: newValue, prevValue: newValue})
if (func) {
func(newValue)
}
}
}

onChange = (event) => {
const value = parseFloat(event.target.value, 10)
this.setState({
value: value
})
this.setState({value, prevValue: value})
if (this.props.onChange) {
this.props.onChange(value)
}
if (this.props.onMove) {
this.props.onMove(value)
}
}

constructor (props) {
super(props)
this.state = {
mouseDown: false,
isAnimating: false,
value: props.value || 0,
prevValue: props.value || 0,
step: 1,
// ratio between actual dom element width and range value (0 -> 100)
ratio: 1
}
}

componentWillReceiveProps (nextProps) {
if (nextProps.value !== undefined) {
this.setState({value: nextProps.value, prevValue: nextProps.value})
}
}

// get initial width of dom element
componentDidMount () {
var {slider} = this.refs
var width = slider.getBoundingClientRect().width - 16
const {slider} = this.refs
const width = slider.getBoundingClientRect().width - 16
this.setState({
ratio: width / 100
ratio: width / (this.props.max - this.props.min)
})
}

Expand All @@ -51,25 +95,21 @@ export default class Slider extends React.Component {
document.removeEventListener('touchmove', this.onMouseMove)
document.removeEventListener('mouseup', this.onMouseUp)
document.removeEventListener('touchend', this.onMouseUp)
if (this.props.onMouseUp) {
this.props.onMouseUp(this.state.value)
if (this.props.onChange) {
this.props.onChange(this.state.value)
}
}

getValueFromSlider = (event) => {
const {slider} = this.refs
const pageX = event.pageX || event.touches[0].pageX
const x = pageX - 8 - slider.getBoundingClientRect().left
return clip(x / this.state.ratio + this.props.min, this.props.min, this.props.max)
}

onMouseDown = (event) => {
event.preventDefault()
var {slider} = this.refs
var pageX = event.pageX || event.touches[0].pageX
var x = pageX - 8 - slider.getBoundingClientRect().left
var value = x / this.state.ratio
if (value > 100) {
value = 100
}
if (value < 0) {
value = 0
}
this.setState({
value: value,
mouseDown: true
})
document.addEventListener('mousemove', this.onMouseMove)
Expand All @@ -80,38 +120,24 @@ export default class Slider extends React.Component {

onMouseMove = (event) => {
if (this.state.mouseDown) {
var {slider} = this.refs
var pageX = event.pageX || event.touches[0].pageX
var x = pageX - 8 - slider.getBoundingClientRect().left
var value = x / this.state.ratio
if (value > 100) {
value = 100
}
if (value < 0) {
value = 0
}
this.setState({
value: value
})
if (this.props.onChange) {
this.props.onChange(value)
}
const value = this.getValueFromSlider(event)
this.update(value, this.props.onMove)
}
}

render () {
var position = (this.state.value * this.state.ratio)
const position = (this.state.value - this.props.min) * this.state.ratio

return (
<label className='Slider'>
<input
className='Slider-input'
max='100'
min='0'
max={this.props.max}
min={this.props.min}
onChange={this.onChange}
step={this.props.step}
type='range'
value={this.state.value}
step={this.props.step}
/>
<div
className='Slider-track'
Expand All @@ -124,7 +150,7 @@ export default class Slider extends React.Component {
<div className='Slider-track-on' style={{width: `${position}px`}} />
</div>
<div
className={`Slider-thumb${this.state.value === 0 ? ' is-zero' : ''}`}
className={`Slider-thumb${this.state.value === this.props.min ? ' is-zero' : ''}`}
style={{left: `${position}px`}}
/>
</div>
Expand Down
47 changes: 21 additions & 26 deletions components/slider/test/test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
/* global it, describe, KeyboardEvent */
/* global it, describe */

import assert from 'assert'
import React from 'react'
import Slider from '../'
import {mount} from 'enzyme'

describe('Slider', () => {
it('should have a default value of 50', () => {
const wrapper = mount(<Slider />)
assert.equal(wrapper.find('.Slider-input').node.value, '50')
it('should have a default value of 0', () => {
const wrapper = mount(<Slider min={0} max={3} />)
assert.equal(wrapper.find('.Slider-input').node.value, '0')
})

it('should reflect changes made to input type range', () => {
const wrapper = mount(<Slider />)
const wrapper = mount(<Slider min={0} max={3} />)
wrapper.find('.Slider-input').simulate('change', {
target: {
value: 45
Expand All @@ -21,44 +21,39 @@ describe('Slider', () => {
assert.equal(wrapper.find('.Slider-input').node.value, '45')
})

it('should fade out the thumb when value is zero', () => {
const wrapper = mount(<Slider />)
it('should fade out the thumb when value is min', () => {
const wrapper = mount(<Slider min={-1} max={3} />)
wrapper.find('.Slider-input').simulate('change', {
target: {
value: 0
value: -1
}
})
assert(wrapper.find('.Slider-thumb').hasClass('is-zero'))
})

it.skip('should work with keyboard arrow keys', () => {
const wrapper = mount(<Slider />)
// make sure default value is 50
assert.equal(wrapper.find('.Slider-input').node.value, '50')
// simulate keyboard event
var event = new KeyboardEvent('keypress', {
const wrapper = mount(<Slider min={3} max={7} value={5} />)
// make sure default value is 5
assert.equal(wrapper.find('.Slider-input').node.value, '5')
wrapper.find('.Slider-input').simulate('keypress', {
key: 'ArrowLeft',
keyCode: 37
})
wrapper.find('.Slider-input').node.dispatchEvent(event)
assert.equal(wrapper.find('.Slider-input').node.value, '49')
// simulate keyboard event
event = new KeyboardEvent('keypress', {
assert.equal(wrapper.find('.Slider-input').node.value, '4')
wrapper.find('.Slider-input').simulate('keypress', {
key: 'ArrowRight',
keyCode: 39
})
wrapper.find('.Slider-input').node.dispatchEvent(event)
assert.equal(wrapper.find('.Slider-input').node.value, '50')
assert.equal(wrapper.find('.Slider-input').node.value, '5')
})

it.skip('should work with custom step', () => {
const wrapper = mount(<Slider step={10} />)
assert.equal(wrapper.find('.Slider-input').node.value, '50')
let event = new KeyboardEvent('keypress', {
key: 'ArrowLeft',
keyCode: 37
const wrapper = mount(<Slider step={10.1} min={1.3} max={20} value={1.3} />)
assert.equal(wrapper.find('.Slider-input').node.value, '1.3')
wrapper.find('.Slider-input').simulate('keypress', {
key: 'ArrowRight',
keyCode: 39
})
wrapper.find('.Slider-input').node.dispatchEvent(event)
assert.equal(wrapper.find('.Slider-input').node.value, '40')
assert.equal(wrapper.find('.Slider-input').node.value, '11.4')
})
})
42 changes: 20 additions & 22 deletions examples/js/components/sliderRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,46 @@ const defaultSliderWithStepsComponent =
/>`

const sliderComponent =
`class App extends React.Component {
`const min = -20
const max = 110
const step = 10
class App extends React.Component {
state = {
value: 0,
settledValue: 0
}
onChange = (value) => {
onMove = (value) => {
this.setState({value})
}
onMouseUp = (settledValue) => {
onChange = (settledValue) => {
this.setState({settledValue})
}
render () {
return (
<div>
<h4>onChange: {Math.round(this.state.value)}</h4>
<h4>onMouseUp: {Math.round(this.state.settledValue)}</h4>
<Slider onChange={this.onChange} onMouseUp={this.onMouseUp} />
<Slider
onChange={this.onChange}
onMove={this.onMove}
value={this.state.value}
step={step}
min={min}
max={max}
/>
<h4>onMove: {this.state.value === undefined ? '-' : this.state.value}</h4>
<h4>onChange: {this.state.settledValue === undefined ? '-' : this.state.settledValue}</h4>
<h4>min: {min}</h4>
<h4>max: {max}</h4>
<h4>step: {step}</h4>
</div>
)
}
}
ReactDOM.render(<App />, mountNode)`

const sliderWithStepsComponent =
`<Slider
step={10}
/>`

export default class SliderRoute extends React.Component {

render () {
Expand All @@ -67,15 +74,6 @@ export default class SliderRoute extends React.Component {
collapsableCode
/>
</section>
<section>
<h2>Slider with steps</h2>
<Playground
codeText={sliderWithStepsComponent}
noRender={false}
scope={{React, ReactDOM, Slider}}
collapsableCode
/>
</section>
<section>
<h2>Default slider</h2>
<Playground
Expand Down
2 changes: 0 additions & 2 deletions examples/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import CardRoute from './components/cardRoute'
import CheckboxRoute from './components/checkboxRoute'
import ChipRoute from './components/chipRoute'
import HomeRoute from './components/homeRoute'
import IconRoute from './components/iconRoute'
import ListRoute from './components/listRoute'
import MenuRoute from './components/menuRoute'
import ModalRoute from './components/modalRoute'
Expand Down Expand Up @@ -62,7 +61,6 @@ ReactDOM.render((
<Route path='card' component={CardRoute} />
<Route path='checkbox' component={CheckboxRoute} />
<Route path='chip' component={ChipRoute} />
<Route path='icon' component={IconRoute} />
<Route path='list' component={ListRoute} />
<Route path='menu' component={MenuRoute} />
<Route path='modal' component={ModalRoute} />
Expand Down

0 comments on commit 2aa6fe9

Please sign in to comment.