Skip to content

Commit

Permalink
feat: Add track dragging functionality (#91)
Browse files Browse the repository at this point in the history
* Add track dragging functionality
* Stop slider drag from affecting track drag
* Allow clicks on tracks outside of range for draggable track
* Prevent track drag while slider is dragging
  • Loading branch information
leobauza authored and davidchin committed Jul 9, 2017
1 parent 7813540 commit 4a8ca26
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 2 deletions.
14 changes: 14 additions & 0 deletions example/js/example-app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export default class ExampleApp extends React.Component {
min: 5,
max: 10,
},
value5: {
min: 3,
max: 7,
},
};
}

Expand Down Expand Up @@ -52,6 +56,16 @@ export default class ExampleApp extends React.Component {
value={this.state.value4}
onChange={value => this.setState({ value4: value })}
onChangeComplete={value => console.log(value)} />

<InputRange
draggableTrack
labelSuffix="kg"
maxValue={20}
minValue={0}
onChange={value => this.setState({ value5: value })}
onChangeComplete={value => console.log(value)}
value={this.state.value5} />

</form>
);
}
Expand Down
64 changes: 62 additions & 2 deletions src/js/input-range/input-range.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class InputRange extends React.Component {
ariaControls: PropTypes.string,
classNames: PropTypes.objectOf(PropTypes.string),
disabled: PropTypes.bool,
draggableTrack: PropTypes.bool,
formatLabel: PropTypes.func,
maxValue: rangePropType,
minValue: rangePropType,
Expand Down Expand Up @@ -90,6 +91,12 @@ export default class InputRange extends React.Component {
* @type {?Component}
*/
this.trackNode = null;

/**
* @private
* @type {bool}
*/
this.isSliderDragging = false;
}

/**
Expand Down Expand Up @@ -353,10 +360,46 @@ export default class InputRange extends React.Component {
}

const position = valueTransformer.getPositionFromEvent(event, this.getTrackClientRect());

this.isSliderDragging = true;
requestAnimationFrame(() => this.updatePosition(key, position));
}

/**
* Handle any "mousemove" event received by the track
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
@autobind
handleTrackDrag(event, prevEvent) {
if (this.props.disabled || !this.props.draggableTrack || this.isSliderDragging) {
return;
}

const {
maxValue,
minValue,
value: { max, min },
} = this.props;

const position = valueTransformer.getPositionFromEvent(event, this.getTrackClientRect());
const value = valueTransformer.getValueFromPosition(position, minValue, maxValue, this.getTrackClientRect());
const stepValue = valueTransformer.getStepValueFromValue(value, this.props.step);

const prevPosition = valueTransformer.getPositionFromEvent(prevEvent, this.getTrackClientRect());
const prevValue = valueTransformer.getValueFromPosition(prevPosition, minValue, maxValue, this.getTrackClientRect());
const prevStepValue = valueTransformer.getStepValueFromValue(prevValue, this.props.step);

const offset = prevStepValue - stepValue;

const transformedValues = {
min: min - offset,
max: max - offset,
};

this.updateValues(transformedValues);
}

/**
* Handle any "keydown" event received by the slider
* @private
Expand Down Expand Up @@ -401,9 +444,20 @@ export default class InputRange extends React.Component {
return;
}

const {
maxValue,
minValue,
value: { max, min },
} = this.props;

event.preventDefault();

this.updatePosition(this.getKeyByPosition(position), position);
const value = valueTransformer.getValueFromPosition(position, minValue, maxValue, this.getTrackClientRect());
const stepValue = valueTransformer.getStepValueFromValue(value, this.props.step);

if (!this.props.draggableTrack || stepValue > max || stepValue < min) {
this.updatePosition(this.getKeyByPosition(position), position);
}
}

/**
Expand Down Expand Up @@ -437,6 +491,10 @@ export default class InputRange extends React.Component {
this.props.onChangeComplete(this.props.value);
}

if (this.isSliderDragging) {
this.isSliderDragging = false;
}

this.startValue = null;
}

Expand Down Expand Up @@ -600,8 +658,10 @@ export default class InputRange extends React.Component {

<Track
classNames={this.props.classNames}
draggableTrack={this.props.draggableTrack}
ref={(trackNode) => { this.trackNode = trackNode; }}
percentages={percentages}
onTrackDrag={this.handleTrackDrag}
onTrackMouseDown={this.handleTrackMouseDown}>

{this.renderSliders()}
Expand Down
81 changes: 81 additions & 0 deletions src/js/input-range/track.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ export default class Track extends React.Component {
* @return {Object}
* @property {Function} children
* @property {Function} classNames
* @property {Boolean} draggableTrack
* @property {Function} onTrackDrag
* @property {Function} onTrackMouseDown
* @property {Function} percentages
*/
static get propTypes() {
return {
children: PropTypes.node.isRequired,
classNames: PropTypes.objectOf(PropTypes.string).isRequired,
draggableTrack: PropTypes.bool,
onTrackDrag: PropTypes.func,
onTrackMouseDown: PropTypes.func.isRequired,
percentages: PropTypes.objectOf(PropTypes.number).isRequired,
};
Expand All @@ -26,6 +30,8 @@ export default class Track extends React.Component {
/**
* @param {Object} props
* @param {InputRangeClassNames} props.classNames
* @param {Boolean} props.draggableTrack
* @param {Function} props.onTrackDrag
* @param {Function} props.onTrackMouseDown
* @param {number} props.percentages
*/
Expand All @@ -37,6 +43,7 @@ export default class Track extends React.Component {
* @type {?Component}
*/
this.node = null;
this.trackDragEvent = null;
}

/**
Expand All @@ -58,6 +65,75 @@ export default class Track extends React.Component {
return { left, width };
}

/**
* Listen to mousemove event
* @private
* @return {void}
*/
addDocumentMouseMoveListener() {
this.removeDocumentMouseMoveListener();
this.node.ownerDocument.addEventListener('mousemove', this.handleMouseMove);
}

/**
* Listen to mouseup event
* @private
* @return {void}
*/
addDocumentMouseUpListener() {
this.removeDocumentMouseUpListener();
this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
}

/**
* @private
* @return {void}
*/
removeDocumentMouseMoveListener() {
this.node.ownerDocument.removeEventListener('mousemove', this.handleMouseMove);
}

/**
* @private
* @return {void}
*/
removeDocumentMouseUpListener() {
this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
}

/**
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
@autobind
handleMouseMove(event) {
if (!this.props.draggableTrack) {
return;
}

if (this.trackDragEvent !== null) {
this.props.onTrackDrag(event, this.trackDragEvent);
}

this.trackDragEvent = event;
}

/**
* @private
* @return {void}
*/
@autobind
handleMouseUp() {
if (!this.props.draggableTrack) {
return;
}

this.removeDocumentMouseMoveListener();
this.removeDocumentMouseUpListener();
this.trackDragEvent = null;
}

/**
* @private
* @param {SyntheticEvent} event - User event
Expand All @@ -72,6 +148,11 @@ export default class Track extends React.Component {
};

this.props.onTrackMouseDown(event, position);

if (this.props.draggableTrack) {
this.addDocumentMouseMoveListener();
this.addDocumentMouseUpListener();
}
}

/**
Expand Down

0 comments on commit 4a8ca26

Please sign in to comment.