diff --git a/components/vc-slider/assets/index.less b/components/vc-slider/assets/index.less
new file mode 100644
index 000000000..1777d2cc8
--- /dev/null
+++ b/components/vc-slider/assets/index.less
@@ -0,0 +1,302 @@
+@prefixClass: rc-slider;
+
+@disabledColor: #ccc;
+@border-radius-base: 6px;
+@primary-color: #2db7f5;
+@tooltip-color: #fff;
+@tooltip-bg: tint(#666, 4%);
+@tooltip-arrow-width: 4px;
+@tooltip-distance: @tooltip-arrow-width+4;
+@tooltip-arrow-color: @tooltip-bg;
+@ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1);
+@ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06);
+
+.borderBox() {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // remove tap highlight color for mobile safari
+
+ * {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // remove tap highlight color for mobile safari
+ }
+}
+
+.@{prefixClass} {
+ position: relative;
+ height: 14px;
+ padding: 5px 0;
+ width: 100%;
+ border-radius: @border-radius-base;
+ touch-action: none;
+ .borderBox();
+
+ &-rail {
+ position: absolute;
+ width: 100%;
+ background-color: #e9e9e9;
+ height: 4px;
+ border-radius: @border-radius-base;
+ }
+
+ &-track {
+ position: absolute;
+ left: 0;
+ height: 4px;
+ border-radius: @border-radius-base;
+ background-color: tint(@primary-color, 60%);
+ }
+
+ &-handle {
+ position: absolute;
+ margin-left: -7px;
+ margin-top: -5px;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ cursor: -webkit-grab;
+ cursor: grab;
+ border-radius: 50%;
+ border: solid 2px tint(@primary-color, 50%);
+ background-color: #fff;
+ touch-action: pan-x;
+
+ &:focus {
+ border-color: tint(@primary-color, 20%);
+ box-shadow: 0 0 0 5px tint(@primary-color, 50%);
+ outline: none;
+ }
+
+ &-click-focused:focus {
+ border-color: tint(@primary-color, 50%);
+ box-shadow: unset;
+ }
+
+ &:hover {
+ border-color: tint(@primary-color, 20%);
+ }
+
+ &:active {
+ border-color: tint(@primary-color, 20%);
+ box-shadow: 0 0 5px tint(@primary-color, 20%);
+ cursor: -webkit-grabbing;
+ cursor: grabbing;
+ }
+ }
+
+ &-mark {
+ position: absolute;
+ top: 18px;
+ left: 0;
+ width: 100%;
+ font-size: 12px;
+ }
+
+ &-mark-text {
+ position: absolute;
+ display: inline-block;
+ vertical-align: middle;
+ text-align: center;
+ cursor: pointer;
+ color: #999;
+
+ &-active {
+ color: #666;
+ }
+ }
+
+ &-step {
+ position: absolute;
+ width: 100%;
+ height: 4px;
+ background: transparent;
+ }
+
+ &-dot {
+ position: absolute;
+ bottom: -2px;
+ margin-left: -4px;
+ width: 8px;
+ height: 8px;
+ border: 2px solid #e9e9e9;
+ background-color: #fff;
+ cursor: pointer;
+ border-radius: 50%;
+ vertical-align: middle;
+ &-active {
+ border-color: tint(@primary-color, 50%);
+ }
+ }
+
+ &-disabled {
+ background-color: #e9e9e9;
+
+ .@{prefixClass}-track {
+ background-color: @disabledColor;
+ }
+
+ .@{prefixClass}-handle, .@{prefixClass}-dot {
+ border-color: @disabledColor;
+ box-shadow: none;
+ background-color: #fff;
+ cursor: not-allowed;
+ }
+
+ .@{prefixClass}-mark-text, .@{prefixClass}-dot {
+ cursor: not-allowed!important;
+ }
+ }
+}
+
+.@{prefixClass}-vertical {
+ width: 14px;
+ height: 100%;
+ padding: 0 5px;
+
+ .@{prefixClass} {
+ &-rail {
+ height: 100%;
+ width: 4px;
+ }
+
+ &-track {
+ left: 5px;
+ bottom: 0;
+ width: 4px;
+ }
+
+ &-handle {
+ margin-left: -5px;
+ margin-bottom: -7px;
+ touch-action: pan-y;
+ }
+
+ &-mark {
+ top: 0;
+ left: 18px;
+ height: 100%;
+ }
+
+ &-step {
+ height: 100%;
+ width: 4px;
+ }
+
+ &-dot {
+ left: 2px;
+ margin-bottom: -4px;
+ &:first-child {
+ margin-bottom: -4px;
+ }
+ &:last-child {
+ margin-bottom: -4px;
+ }
+ }
+ }
+}
+
+.motion-common() {
+ animation-duration: .3s;
+ animation-fill-mode: both;
+ display: block !important;
+}
+
+.make-motion(@className, @keyframeName) {
+ .@{className}-enter, .@{className}-appear {
+ .motion-common();
+ animation-play-state: paused;
+ }
+ .@{className}-leave {
+ .motion-common();
+ animation-play-state: paused;
+ }
+ .@{className}-enter.@{className}-enter-active, .@{className}-appear.@{className}-appear-active {
+ animation-name: ~"@{keyframeName}In";
+ animation-play-state: running;
+ }
+ .@{className}-leave.@{className}-leave-active {
+ animation-name: ~"@{keyframeName}Out";
+ animation-play-state: running;
+ }
+}
+.zoom-motion(@className, @keyframeName) {
+ .make-motion(@className, @keyframeName);
+ .@{className}-enter, .@{className}-appear {
+ transform: scale(0, 0); // need this by yiminghe
+ animation-timing-function: @ease-out-quint;
+ }
+ .@{className}-leave {
+ animation-timing-function: @ease-in-quint;
+ }
+}
+.zoom-motion(rc-slider-tooltip-zoom-down, rcSliderTooltipZoomDown);
+
+@keyframes rcSliderTooltipZoomDownIn {
+ 0% {
+ opacity: 0;
+ transform-origin: 50% 100%;
+ transform: scale(0, 0);
+ }
+ 100% {
+ transform-origin: 50% 100%;
+ transform: scale(1, 1);
+ }
+}
+
+@keyframes rcSliderTooltipZoomDownOut {
+ 0% {
+ transform-origin: 50% 100%;
+ transform: scale(1, 1);
+ }
+ 100% {
+ opacity: 0;
+ transform-origin: 50% 100%;
+ transform: scale(0, 0);
+ }
+}
+
+.@{prefixClass}-tooltip {
+ position: absolute;
+ left: -9999px;
+ top: -9999px;
+ visibility: visible;
+
+ .borderBox();
+
+ &-hidden {
+ display: none;
+ }
+
+ &-placement-top {
+ padding: @tooltip-arrow-width 0 @tooltip-distance 0;
+ }
+
+ &-inner {
+ padding: 6px 2px;
+ min-width: 24px;
+ height: 24px;
+ font-size: 12px;
+ line-height: 1;
+ color: @tooltip-color;
+ text-align: center;
+ text-decoration: none;
+ background-color: @tooltip-bg;
+ border-radius: @border-radius-base;
+ box-shadow: 0 0 4px #d9d9d9;
+ }
+
+ &-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ }
+
+ &-placement-top &-arrow {
+ bottom: @tooltip-distance - @tooltip-arrow-width;
+ left: 50%;
+ margin-left: -@tooltip-arrow-width;
+ border-width: @tooltip-arrow-width @tooltip-arrow-width 0;
+ border-top-color: @tooltip-arrow-color;
+ }
+}
diff --git a/components/vc-slider/src/Handle.vue b/components/vc-slider/src/Handle.vue
new file mode 100644
index 000000000..918ccc1e7
--- /dev/null
+++ b/components/vc-slider/src/Handle.vue
@@ -0,0 +1,109 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import addEventListener from 'rc-util/lib/Dom/addEventListener';
+
+export default class Handle extends React.Component {
+ state = {
+ clickFocused: false,
+ }
+
+ componentDidMount() {
+ // mouseup won't trigger if mouse moved out of handle,
+ // so we listen on document here.
+ this.onMouseUpListener = addEventListener(document, 'mouseup', this.handleMouseUp);
+ }
+
+ componentWillUnmount() {
+ if (this.onMouseUpListener) {
+ this.onMouseUpListener.remove();
+ }
+ }
+
+ setClickFocus(focused) {
+ this.setState({ clickFocused: focused });
+ }
+
+ handleMouseUp = () => {
+ if (document.activeElement === this.handle) {
+ this.setClickFocus(true);
+ }
+ }
+
+ handleBlur = () => {
+ this.setClickFocus(false);
+ }
+
+ handleKeyDown = () => {
+ this.setClickFocus(false);
+ }
+
+ clickFocus() {
+ this.setClickFocus(true);
+ this.focus();
+ }
+
+ focus() {
+ this.handle.focus();
+ }
+
+ blur() {
+ this.handle.blur();
+ }
+
+ render() {
+ const {
+ prefixCls, vertical, offset, style, disabled, min, max, value, tabIndex, ...restProps,
+ } = this.props;
+
+ const className = classNames(
+ this.props.className,
+ {
+ [`${prefixCls}-handle-click-focused`]: this.state.clickFocused,
+ }
+ );
+
+ const postionStyle = vertical ? { bottom: `${offset}%` } : { left: `${offset}%` };
+ const elStyle = {
+ ...style,
+ ...postionStyle,
+ };
+ let ariaProps = {};
+ if (value !== undefined) {
+ ariaProps = {
+ ...ariaProps,
+ 'aria-valuemin': min,
+ 'aria-valuemax': max,
+ 'aria-valuenow': value,
+ 'aria-disabled': !!disabled,
+ };
+ }
+
+ return (
+
(this.handle = node)}
+ role="slider"
+ tabIndex= {disabled ? null : (tabIndex || 0)}
+ {...ariaProps}
+ {...restProps}
+ className={className}
+ style={elStyle}
+ onBlur={this.handleBlur}
+ onKeyDown={this.handleKeyDown}
+ />
+ );
+ }
+}
+
+Handle.propTypes = {
+ prefixCls: PropTypes.string,
+ className: PropTypes.string,
+ vertical: PropTypes.bool,
+ offset: PropTypes.number,
+ style: PropTypes.object,
+ disabled: PropTypes.bool,
+ min: PropTypes.number,
+ max: PropTypes.number,
+ value: PropTypes.number,
+ tabIndex: PropTypes.number,
+};
diff --git a/components/vc-slider/src/Range.vue b/components/vc-slider/src/Range.vue
new file mode 100644
index 000000000..03876b052
--- /dev/null
+++ b/components/vc-slider/src/Range.vue
@@ -0,0 +1,382 @@
+/* eslint-disable react/prop-types */
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import shallowEqual from 'shallowequal';
+import Track from './common/Track';
+import createSlider from './common/createSlider';
+import * as utils from './utils';
+
+class Range extends React.Component {
+ static displayName = 'Range';
+ static propTypes = {
+ defaultValue: PropTypes.arrayOf(PropTypes.number),
+ value: PropTypes.arrayOf(PropTypes.number),
+ count: PropTypes.number,
+ pushable: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.number,
+ ]),
+ allowCross: PropTypes.bool,
+ disabled: PropTypes.bool,
+ tabIndex: PropTypes.arrayOf(PropTypes.number),
+ };
+
+ static defaultProps = {
+ count: 1,
+ allowCross: true,
+ pushable: false,
+ tabIndex: [],
+ };
+
+ constructor(props) {
+ super(props);
+
+ const { count, min, max } = props;
+ const initialValue = Array.apply(null, Array(count + 1))
+ .map(() => min);
+ const defaultValue = 'defaultValue' in props ?
+ props.defaultValue : initialValue;
+ const value = props.value !== undefined ?
+ props.value : defaultValue;
+ const bounds = value.map((v, i) => this.trimAlignValue(v, i));
+ const recent = bounds[0] === max ? 0 : bounds.length - 1;
+
+ this.state = {
+ handle: null,
+ recent,
+ bounds,
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!('value' in nextProps || 'min' in nextProps || 'max' in nextProps)) return;
+ if (this.props.min === nextProps.min &&
+ this.props.max === nextProps.max &&
+ shallowEqual(this.props.value, nextProps.value)) {
+ return;
+ }
+
+ const { bounds } = this.state;
+ const value = nextProps.value || bounds;
+ const nextBounds = value.map((v, i) => this.trimAlignValue(v, i, nextProps));
+ if (nextBounds.length === bounds.length && nextBounds.every((v, i) => v === bounds[i])) return;
+
+ this.setState({ bounds: nextBounds });
+
+ if (bounds.some(v => utils.isValueOutOfRange(v, nextProps))) {
+ const newValues = value.map((v) => {
+ return utils.ensureValueInRange(v, nextProps);
+ });
+ this.props.onChange(newValues);
+ }
+ }
+
+ onChange(state) {
+ const props = this.props;
+ const isNotControlled = !('value' in props);
+ if (isNotControlled) {
+ this.setState(state);
+ } else if (state.handle !== undefined) {
+ this.setState({ handle: state.handle });
+ }
+
+ const data = { ...this.state, ...state };
+ const changedValue = data.bounds;
+ props.onChange(changedValue);
+ }
+
+ onStart(position) {
+ const props = this.props;
+ const state = this.state;
+ const bounds = this.getValue();
+ props.onBeforeChange(bounds);
+
+ const value = this.calcValueByPos(position);
+ this.startValue = value;
+ this.startPosition = position;
+
+ const closestBound = this.getClosestBound(value);
+ this.prevMovedHandleIndex = this.getBoundNeedMoving(value, closestBound);
+
+ this.setState({
+ handle: this.prevMovedHandleIndex,
+ recent: this.prevMovedHandleIndex,
+ });
+
+ const prevValue = bounds[this.prevMovedHandleIndex];
+ if (value === prevValue) return;
+
+ const nextBounds = [...state.bounds];
+ nextBounds[this.prevMovedHandleIndex] = value;
+ this.onChange({ bounds: nextBounds });
+ }
+
+ onEnd = () => {
+ this.removeDocumentEvents();
+ this.props.onAfterChange(this.getValue());
+ }
+
+ onMove(e, position) {
+ utils.pauseEvent(e);
+ const state = this.state;
+
+ const value = this.calcValueByPos(position);
+ const oldValue = state.bounds[state.handle];
+ if (value === oldValue) return;
+
+ this.moveTo(value);
+ }
+
+ onKeyboard(e) {
+ const valueMutator = utils.getKeyboardValueMutator(e);
+
+ if (valueMutator) {
+ utils.pauseEvent(e);
+ const { state, props } = this;
+ const { bounds, handle } = state;
+ const oldValue = bounds[handle];
+ const mutatedValue = valueMutator(oldValue, props);
+ const value = this.trimAlignValue(mutatedValue);
+ if (value === oldValue) return;
+ const isFromKeyboardEvent = true;
+ this.moveTo(value, isFromKeyboardEvent);
+ }
+ }
+
+ getValue() {
+ return this.state.bounds;
+ }
+
+ getClosestBound(value) {
+ const { bounds } = this.state;
+ let closestBound = 0;
+ for (let i = 1; i < bounds.length - 1; ++i) {
+ if (value > bounds[i]) { closestBound = i; }
+ }
+ if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) {
+ closestBound = closestBound + 1;
+ }
+ return closestBound;
+ }
+
+ getBoundNeedMoving(value, closestBound) {
+ const { bounds, recent } = this.state;
+ let boundNeedMoving = closestBound;
+ const isAtTheSamePoint = (bounds[closestBound + 1] === bounds[closestBound]);
+
+ if (isAtTheSamePoint && bounds[recent] === bounds[closestBound]) {
+ boundNeedMoving = recent;
+ }
+
+ if (isAtTheSamePoint && (value !== bounds[closestBound + 1])) {
+ boundNeedMoving = value < bounds[closestBound + 1] ? closestBound : closestBound + 1;
+ }
+ return boundNeedMoving;
+ }
+
+ getLowerBound() {
+ return this.state.bounds[0];
+ }
+
+ getUpperBound() {
+ const { bounds } = this.state;
+ return bounds[bounds.length - 1];
+ }
+
+ /**
+ * Returns an array of possible slider points, taking into account both
+ * `marks` and `step`. The result is cached.
+ */
+ getPoints() {
+ const { marks, step, min, max } = this.props;
+ const cache = this._getPointsCache;
+ if (!cache || cache.marks !== marks || cache.step !== step) {
+ const pointsObject = { ...marks };
+ if (step !== null) {
+ for (let point = min; point <= max; point += step) {
+ pointsObject[point] = point;
+ }
+ }
+ const points = Object.keys(pointsObject).map(parseFloat);
+ points.sort((a, b) => a - b);
+ this._getPointsCache = { marks, step, points };
+ }
+ return this._getPointsCache.points;
+ }
+
+ moveTo(value, isFromKeyboardEvent) {
+ const { state, props } = this;
+ const nextBounds = [...state.bounds];
+ nextBounds[state.handle] = value;
+ let nextHandle = state.handle;
+ if (props.pushable !== false) {
+ this.pushSurroundingHandles(nextBounds, nextHandle);
+ } else if (props.allowCross) {
+ nextBounds.sort((a, b) => a - b);
+ nextHandle = nextBounds.indexOf(value);
+ }
+ this.onChange({
+ handle: nextHandle,
+ bounds: nextBounds,
+ });
+ if (isFromKeyboardEvent) {
+ // known problem: because setState is async,
+ // so trigger focus will invoke handler's onEnd and another handler's onStart too early,
+ // cause onBeforeChange and onAfterChange receive wrong value.
+ // here use setState callback to hack,but not elegant
+ this.setState({}, () => {
+ this.handlesRefs[nextHandle].focus();
+ });
+ }
+ }
+
+ pushSurroundingHandles(bounds, handle) {
+ const value = bounds[handle];
+ let { pushable: threshold } = this.props;
+ threshold = Number(threshold);
+
+ let direction = 0;
+ if (bounds[handle + 1] - value < threshold) {
+ direction = +1; // push to right
+ }
+ if (value - bounds[handle - 1] < threshold) {
+ direction = -1; // push to left
+ }
+
+ if (direction === 0) { return; }
+
+ const nextHandle = handle + direction;
+ const diffToNext = direction * (bounds[nextHandle] - value);
+ if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) {
+ // revert to original value if pushing is impossible
+ bounds[handle] = bounds[nextHandle] - (direction * threshold);
+ }
+ }
+
+ pushHandle(bounds, handle, direction, amount) {
+ const originalValue = bounds[handle];
+ let currentValue = bounds[handle];
+ while (direction * (currentValue - originalValue) < amount) {
+ if (!this.pushHandleOnePoint(bounds, handle, direction)) {
+ // can't push handle enough to create the needed `amount` gap, so we
+ // revert its position to the original value
+ bounds[handle] = originalValue;
+ return false;
+ }
+ currentValue = bounds[handle];
+ }
+ // the handle was pushed enough to create the needed `amount` gap
+ return true;
+ }
+
+ pushHandleOnePoint(bounds, handle, direction) {
+ const points = this.getPoints();
+ const pointIndex = points.indexOf(bounds[handle]);
+ const nextPointIndex = pointIndex + direction;
+ if (nextPointIndex >= points.length || nextPointIndex < 0) {
+ // reached the minimum or maximum available point, can't push anymore
+ return false;
+ }
+ const nextHandle = handle + direction;
+ const nextValue = points[nextPointIndex];
+ const { pushable: threshold } = this.props;
+ const diffToNext = direction * (bounds[nextHandle] - nextValue);
+ if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) {
+ // couldn't push next handle, so we won't push this one either
+ return false;
+ }
+ // push the handle
+ bounds[handle] = nextValue;
+ return true;
+ }
+
+ trimAlignValue(v, handle, nextProps = {}) {
+ const mergedProps = { ...this.props, ...nextProps };
+ const valInRange = utils.ensureValueInRange(v, mergedProps);
+ const valNotConflict = this.ensureValueNotConflict(handle, valInRange, mergedProps);
+ return utils.ensureValuePrecision(valNotConflict, mergedProps);
+ }
+
+ ensureValueNotConflict(handle, val, { allowCross, pushable: thershold }) {
+ const state = this.state || {};
+ const { bounds } = state;
+ handle = handle === undefined ? state.handle : handle;
+ thershold = Number(thershold);
+ /* eslint-disable eqeqeq */
+ if (!allowCross && handle != null && bounds !== undefined) {
+ if (handle > 0 && val <= (bounds[handle - 1] + thershold)) {
+ return bounds[handle - 1] + thershold;
+ }
+ if (handle < bounds.length - 1 && val >= (bounds[handle + 1] - thershold)) {
+ return bounds[handle + 1] - thershold;
+ }
+ }
+ /* eslint-enable eqeqeq */
+ return val;
+ }
+
+ render() {
+ const {
+ handle,
+ bounds,
+ } = this.state;
+ const {
+ prefixCls,
+ vertical,
+ included,
+ disabled,
+ min,
+ max,
+ handle: handleGenerator,
+ trackStyle,
+ handleStyle,
+ tabIndex,
+ } = this.props;
+
+ const offsets = bounds.map(v => this.calcOffset(v));
+
+ const handleClassName = `${prefixCls}-handle`;
+ const handles = bounds.map((v, i) => handleGenerator({
+ className: classNames({
+ [handleClassName]: true,
+ [`${handleClassName}-${i + 1}`]: true,
+ }),
+ prefixCls,
+ vertical,
+ offset: offsets[i],
+ value: v,
+ dragging: handle === i,
+ index: i,
+ tabIndex: tabIndex[i] || 0,
+ min,
+ max,
+ disabled,
+ style: handleStyle[i],
+ ref: h => this.saveHandle(i, h),
+ }));
+
+ const tracks = bounds.slice(0, -1).map((_, index) => {
+ const i = index + 1;
+ const trackClassName = classNames({
+ [`${prefixCls}-track`]: true,
+ [`${prefixCls}-track-${i}`]: true,
+ });
+ return (
+
+ );
+ });
+
+ return { tracks, handles };
+ }
+}
+
+export default createSlider(Range);
diff --git a/components/vc-slider/src/Slider.vue b/components/vc-slider/src/Slider.vue
new file mode 100644
index 000000000..b0af9d600
--- /dev/null
+++ b/components/vc-slider/src/Slider.vue
@@ -0,0 +1,191 @@
+/* eslint-disable react/prop-types */
+import React from 'react'
+import PropTypes from 'prop-types'
+import warning from 'warning'
+import Track from './common/Track'
+import createSlider from './common/createSlider'
+import * as utils from './utils'
+
+class Slider extends React.Component {
+ static propTypes = {
+ defaultValue: PropTypes.number,
+ value: PropTypes.number,
+ disabled: PropTypes.bool,
+ autoFocus: PropTypes.bool,
+ tabIndex: PropTypes.number,
+ };
+
+ constructor (props) {
+ super(props)
+
+ const defaultValue = props.defaultValue !== undefined
+ ? props.defaultValue : props.min
+ const value = props.value !== undefined
+ ? props.value : defaultValue
+
+ this.state = {
+ value: this.trimAlignValue(value),
+ dragging: false,
+ }
+ if (process.env.NODE_ENV !== 'production') {
+ warning(
+ !('minimumTrackStyle' in props),
+ 'minimumTrackStyle will be deprecate, please use trackStyle instead.'
+ )
+ warning(
+ !('maximumTrackStyle' in props),
+ 'maximumTrackStyle will be deprecate, please use railStyle instead.'
+ )
+ }
+ }
+
+ componentDidMount () {
+ const { autoFocus, disabled } = this.props
+ if (autoFocus && !disabled) {
+ this.focus()
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (!('value' in nextProps || 'min' in nextProps || 'max' in nextProps)) return
+
+ const prevValue = this.state.value
+ const value = nextProps.value !== undefined
+ ? nextProps.value : prevValue
+ const nextValue = this.trimAlignValue(value, nextProps)
+ if (nextValue === prevValue) return
+
+ this.setState({ value: nextValue })
+ if (utils.isValueOutOfRange(value, nextProps)) {
+ this.props.onChange(nextValue)
+ }
+ }
+
+ onChange (state) {
+ const props = this.props
+ const isNotControlled = !('value' in props)
+ if (isNotControlled) {
+ this.setState(state)
+ }
+
+ const changedValue = state.value
+ props.onChange(changedValue)
+ }
+
+ onStart (position) {
+ this.setState({ dragging: true })
+ const props = this.props
+ const prevValue = this.getValue()
+ props.onBeforeChange(prevValue)
+
+ const value = this.calcValueByPos(position)
+ this.startValue = value
+ this.startPosition = position
+
+ if (value === prevValue) return
+
+ this.prevMovedHandleIndex = 0
+
+ this.onChange({ value })
+ }
+
+ onEnd = () => {
+ this.setState({ dragging: false })
+ this.removeDocumentEvents()
+ this.props.onAfterChange(this.getValue())
+ }
+
+ onMove (e, position) {
+ utils.pauseEvent(e)
+ const { value: oldValue } = this.state
+ const value = this.calcValueByPos(position)
+ if (value === oldValue) return
+
+ this.onChange({ value })
+ }
+
+ onKeyboard (e) {
+ const valueMutator = utils.getKeyboardValueMutator(e)
+
+ if (valueMutator) {
+ utils.pauseEvent(e)
+ const state = this.state
+ const oldValue = state.value
+ const mutatedValue = valueMutator(oldValue, this.props)
+ const value = this.trimAlignValue(mutatedValue)
+ if (value === oldValue) return
+
+ this.onChange({ value })
+ }
+ }
+
+ getValue () {
+ return this.state.value
+ }
+
+ getLowerBound () {
+ return this.props.min
+ }
+
+ getUpperBound () {
+ return this.state.value
+ }
+
+ trimAlignValue (v, nextProps = {}) {
+ const mergedProps = { ...this.props, ...nextProps }
+ const val = utils.ensureValueInRange(v, mergedProps)
+ return utils.ensureValuePrecision(val, mergedProps)
+ }
+
+ render () {
+ const {
+ prefixCls,
+ vertical,
+ included,
+ disabled,
+ minimumTrackStyle,
+ trackStyle,
+ handleStyle,
+ tabIndex,
+ min,
+ max,
+ handle: handleGenerator,
+ } = this.props
+ const { value, dragging } = this.state
+ const offset = this.calcOffset(value)
+ const handle = handleGenerator({
+ className: `${prefixCls}-handle`,
+ prefixCls,
+ vertical,
+ offset,
+ value,
+ dragging,
+ disabled,
+ min,
+ max,
+ index: 0,
+ tabIndex,
+ style: handleStyle[0] || handleStyle,
+ ref: h => this.saveHandle(0, h),
+ })
+
+ const _trackStyle = trackStyle[0] || trackStyle
+ const track = (
+
+ )
+
+ return { tracks: track, handles: handle }
+ }
+}
+
+export default createSlider(Slider)
diff --git a/components/vc-slider/src/common/Marks.vue b/components/vc-slider/src/common/Marks.vue
new file mode 100644
index 000000000..f869ca143
--- /dev/null
+++ b/components/vc-slider/src/common/Marks.vue
@@ -0,0 +1,66 @@
+import React from 'react'
+import classNames from 'classnames'
+
+const Marks = ({
+ className,
+ vertical,
+ marks,
+ included,
+ upperBound,
+ lowerBound,
+ max, min,
+ onClickLabel,
+}) => {
+ const marksKeys = Object.keys(marks)
+ const marksCount = marksKeys.length
+ const unit = marksCount > 1 ? 100 / (marksCount - 1) : 100
+ const markWidth = unit * 0.9
+
+ const range = max - min
+ const elements = marksKeys.map(parseFloat).sort((a, b) => a - b).map(point => {
+ const markPoint = marks[point]
+ const markPointIsObject = typeof markPoint === 'object' &&
+ !React.isValidElement(markPoint)
+ const markLabel = markPointIsObject ? markPoint.label : markPoint
+ if (!markLabel && markLabel !== 0) {
+ return null
+ }
+
+ const isActive = (!included && point === upperBound) ||
+ (included && point <= upperBound && point >= lowerBound)
+ const markClassName = classNames({
+ [`${className}-text`]: true,
+ [`${className}-text-active`]: isActive,
+ })
+
+ const bottomStyle = {
+ marginBottom: '-50%',
+ bottom: `${(point - min) / range * 100}%`,
+ }
+
+ const leftStyle = {
+ width: `${markWidth}%`,
+ marginLeft: `${-markWidth / 2}%`,
+ left: `${(point - min) / range * 100}%`,
+ }
+
+ const style = vertical ? bottomStyle : leftStyle
+ const markStyle = markPointIsObject
+ ? { ...style, ...markPoint.style } : style
+ return (
+
onClickLabel(e, point)}
+ onTouchStart={(e) => onClickLabel(e, point)}
+ >
+ {markLabel}
+
+ )
+ })
+
+ return
{elements}
+};
+
+export default Marks
diff --git a/components/vc-slider/src/common/Steps.vue b/components/vc-slider/src/common/Steps.vue
new file mode 100644
index 000000000..2859a9582
--- /dev/null
+++ b/components/vc-slider/src/common/Steps.vue
@@ -0,0 +1,44 @@
+import React from 'react'
+import classNames from 'classnames'
+import warning from 'warning'
+
+const calcPoints = (vertical, marks, dots, step, min, max) => {
+ warning(
+ dots ? step > 0 : true,
+ '`Slider[step]` should be a positive number in order to make Slider[dots] work.'
+ )
+ const points = Object.keys(marks).map(parseFloat)
+ if (dots) {
+ for (let i = min; i <= max; i = i + step) {
+ if (points.indexOf(i) >= 0) continue
+ points.push(i)
+ }
+ }
+ return points
+}
+
+const Steps = ({ prefixCls, vertical, marks, dots, step, included,
+ lowerBound, upperBound, max, min, dotStyle, activeDotStyle }) => {
+ const range = max - min
+ const elements = calcPoints(vertical, marks, dots, step, min, max).map((point) => {
+ const offset = `${Math.abs(point - min) / range * 100}%`
+
+ const isActived = (!included && point === upperBound) ||
+ (included && point <= upperBound && point >= lowerBound)
+ let style = vertical ? { bottom: offset, ...dotStyle } : { left: offset, ...dotStyle }
+ if (isActived) {
+ style = { ...style, ...activeDotStyle }
+ }
+
+ const pointClassName = classNames({
+ [`${prefixCls}-dot`]: true,
+ [`${prefixCls}-dot-active`]: isActived,
+ })
+
+ return
+ })
+
+ return
{elements}
+}
+
+export default Steps
diff --git a/components/vc-slider/src/common/Track.vue b/components/vc-slider/src/common/Track.vue
new file mode 100644
index 000000000..6fb6910ac
--- /dev/null
+++ b/components/vc-slider/src/common/Track.vue
@@ -0,0 +1,22 @@
+/* eslint-disable react/prop-types */
+import React from 'react'
+
+const Track = (props) => {
+ const { className, included, vertical, offset, length, style } = props
+
+ const positonStyle = vertical ? {
+ bottom: `${offset}%`,
+ height: `${length}%`,
+ } : {
+ left: `${offset}%`,
+ width: `${length}%`,
+ }
+
+ const elStyle = {
+ ...style,
+ ...positonStyle,
+ }
+ return included ?
: null
+}
+
+export default Track
diff --git a/components/vc-slider/src/common/createSlider.vue b/components/vc-slider/src/common/createSlider.vue
new file mode 100644
index 000000000..9ef8ddcbe
--- /dev/null
+++ b/components/vc-slider/src/common/createSlider.vue
@@ -0,0 +1,343 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import addEventListener from 'rc-util/lib/Dom/addEventListener'
+import classNames from 'classnames'
+import warning from 'warning'
+import Steps from './Steps'
+import Marks from './Marks'
+import Handle from '../Handle'
+import * as utils from '../utils'
+
+function noop () {}
+
+export default function createSlider (Component) {
+ return class ComponentEnhancer extends Component {
+ static displayName = `ComponentEnhancer(${Component.displayName})`;
+ static propTypes = {
+ ...Component.propTypes,
+ min: PropTypes.number,
+ max: PropTypes.number,
+ step: PropTypes.number,
+ marks: PropTypes.object,
+ included: PropTypes.bool,
+ className: PropTypes.string,
+ prefixCls: PropTypes.string,
+ disabled: PropTypes.bool,
+ children: PropTypes.any,
+ onBeforeChange: PropTypes.func,
+ onChange: PropTypes.func,
+ onAfterChange: PropTypes.func,
+ handle: PropTypes.func,
+ dots: PropTypes.bool,
+ vertical: PropTypes.bool,
+ style: PropTypes.object,
+ minimumTrackStyle: PropTypes.object, // just for compatibility, will be deperecate
+ maximumTrackStyle: PropTypes.object, // just for compatibility, will be deperecate
+ handleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]),
+ trackStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]),
+ railStyle: PropTypes.object,
+ dotStyle: PropTypes.object,
+ activeDotStyle: PropTypes.object,
+ autoFocus: PropTypes.bool,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+ };
+
+ static defaultProps = {
+ ...Component.defaultProps,
+ prefixCls: 'rc-slider',
+ className: '',
+ min: 0,
+ max: 100,
+ step: 1,
+ marks: {},
+ handle ({ index, ...restProps }) {
+ delete restProps.dragging
+ return
+ },
+ onBeforeChange: noop,
+ onChange: noop,
+ onAfterChange: noop,
+ included: true,
+ disabled: false,
+ dots: false,
+ vertical: false,
+ trackStyle: [{}],
+ handleStyle: [{}],
+ railStyle: {},
+ dotStyle: {},
+ activeDotStyle: {},
+ };
+
+ constructor (props) {
+ super(props)
+
+ if (process.env.NODE_ENV !== 'production') {
+ const { step, max, min } = props
+ warning(
+ step && Math.floor(step) === step ? (max - min) % step === 0 : true,
+ 'Slider[max] - Slider[min] (%s) should be a multiple of Slider[step] (%s)',
+ max - min,
+ step
+ )
+ }
+ this.handlesRefs = {}
+ }
+
+ componentWillUnmount () {
+ if (super.componentWillUnmount) super.componentWillUnmount()
+ this.removeDocumentEvents()
+ }
+
+ componentDidMount () {
+ // Snapshot testing cannot handle refs, so be sure to null-check this.
+ this.document = this.sliderRef && this.sliderRef.ownerDocument
+ }
+
+ onMouseDown = (e) => {
+ if (e.button !== 0) { return }
+
+ const isVertical = this.props.vertical
+ let position = utils.getMousePosition(isVertical, e)
+ if (!utils.isEventFromHandle(e, this.handlesRefs)) {
+ this.dragOffset = 0
+ } else {
+ const handlePosition = utils.getHandleCenterPosition(isVertical, e.target)
+ this.dragOffset = position - handlePosition
+ position = handlePosition
+ }
+ this.removeDocumentEvents()
+ this.onStart(position)
+ this.addDocumentMouseEvents()
+ }
+
+ onTouchStart = (e) => {
+ if (utils.isNotTouchEvent(e)) return
+
+ const isVertical = this.props.vertical
+ let position = utils.getTouchPosition(isVertical, e)
+ if (!utils.isEventFromHandle(e, this.handlesRefs)) {
+ this.dragOffset = 0
+ } else {
+ const handlePosition = utils.getHandleCenterPosition(isVertical, e.target)
+ this.dragOffset = position - handlePosition
+ position = handlePosition
+ }
+ this.onStart(position)
+ this.addDocumentTouchEvents()
+ utils.pauseEvent(e)
+ }
+
+ onFocus = (e) => {
+ const { onFocus, vertical } = this.props
+ if (utils.isEventFromHandle(e, this.handlesRefs)) {
+ const handlePosition = utils.getHandleCenterPosition(vertical, e.target)
+ this.dragOffset = 0
+ this.onStart(handlePosition)
+ utils.pauseEvent(e)
+ if (onFocus) {
+ onFocus(e)
+ }
+ }
+ }
+
+ onBlur = (e) => {
+ const { onBlur } = this.props
+ this.onEnd(e)
+ if (onBlur) {
+ onBlur(e)
+ }
+ };
+
+ addDocumentTouchEvents () {
+ // just work for Chrome iOS Safari and Android Browser
+ this.onTouchMoveListener = addEventListener(this.document, 'touchmove', this.onTouchMove)
+ this.onTouchUpListener = addEventListener(this.document, 'touchend', this.onEnd)
+ }
+
+ addDocumentMouseEvents () {
+ this.onMouseMoveListener = addEventListener(this.document, 'mousemove', this.onMouseMove)
+ this.onMouseUpListener = addEventListener(this.document, 'mouseup', this.onEnd)
+ }
+
+ removeDocumentEvents () {
+ /* eslint-disable no-unused-expressions */
+ this.onTouchMoveListener && this.onTouchMoveListener.remove()
+ this.onTouchUpListener && this.onTouchUpListener.remove()
+
+ this.onMouseMoveListener && this.onMouseMoveListener.remove()
+ this.onMouseUpListener && this.onMouseUpListener.remove()
+ /* eslint-enable no-unused-expressions */
+ }
+
+ onMouseUp = () => {
+ if (this.handlesRefs[this.prevMovedHandleIndex]) {
+ this.handlesRefs[this.prevMovedHandleIndex].clickFocus()
+ }
+ }
+
+ onMouseMove = (e) => {
+ if (!this.sliderRef) {
+ this.onEnd()
+ return;
+ }
+ const position = utils.getMousePosition(this.props.vertical, e)
+ this.onMove(e, position - this.dragOffset)
+ }
+
+ onTouchMove = (e) => {
+ if (utils.isNotTouchEvent(e) || !this.sliderRef) {
+ this.onEnd()
+ return;
+ }
+
+ const position = utils.getTouchPosition(this.props.vertical, e)
+ this.onMove(e, position - this.dragOffset)
+ }
+
+ onKeyDown = (e) => {
+ if (this.sliderRef && utils.isEventFromHandle(e, this.handlesRefs)) {
+ this.onKeyboard(e)
+ }
+ }
+
+ focus () {
+ if (!this.props.disabled) {
+ this.handlesRefs[0].focus()
+ }
+ }
+
+ blur () {
+ if (!this.props.disabled) {
+ this.handlesRefs[0].blur()
+ }
+ }
+
+ getSliderStart () {
+ const slider = this.sliderRef
+ const rect = slider.getBoundingClientRect()
+
+ return this.props.vertical ? rect.top : rect.left
+ }
+
+ getSliderLength () {
+ const slider = this.sliderRef
+ if (!slider) {
+ return 0
+ }
+
+ const coords = slider.getBoundingClientRect()
+ return this.props.vertical ? coords.height : coords.width
+ }
+
+ calcValue (offset) {
+ const { vertical, min, max } = this.props
+ const ratio = Math.abs(Math.max(offset, 0) / this.getSliderLength())
+ const value = vertical ? (1 - ratio) * (max - min) + min : ratio * (max - min) + min
+ return value
+ }
+
+ calcValueByPos (position) {
+ const pixelOffset = position - this.getSliderStart()
+ const nextValue = this.trimAlignValue(this.calcValue(pixelOffset))
+ return nextValue
+ }
+
+ calcOffset (value) {
+ const { min, max } = this.props
+ const ratio = (value - min) / (max - min)
+ return ratio * 100
+ }
+
+ saveSlider = (slider) => {
+ this.sliderRef = slider
+ }
+
+ saveHandle (index, handle) {
+ this.handlesRefs[index] = handle
+ }
+
+ onClickMarkLabel = (e, value) => {
+ e.stopPropagation()
+ this.onChange({ value })
+ }
+
+ render () {
+ const {
+ prefixCls,
+ className,
+ marks,
+ dots,
+ step,
+ included,
+ disabled,
+ vertical,
+ min,
+ max,
+ children,
+ maximumTrackStyle,
+ style,
+ railStyle,
+ dotStyle,
+ activeDotStyle,
+ } = this.props
+ const { tracks, handles } = super.render()
+
+ const sliderClassName = classNames(prefixCls, {
+ [`${prefixCls}-with-marks`]: Object.keys(marks).length,
+ [`${prefixCls}-disabled`]: disabled,
+ [`${prefixCls}-vertical`]: vertical,
+ [className]: className,
+ })
+ return (
+
+
+ {tracks}
+
+ {handles}
+
+ {children}
+
+ )
+ }
+ }
+}
diff --git a/components/vc-slider/src/createSliderWithTooltip.vue b/components/vc-slider/src/createSliderWithTooltip.vue
new file mode 100644
index 000000000..002357a48
--- /dev/null
+++ b/components/vc-slider/src/createSliderWithTooltip.vue
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Tooltip from 'rc-tooltip';
+import Handle from './Handle';
+
+export default function createSliderWithTooltip(Component) {
+ return class ComponentWrapper extends React.Component {
+ static propTypes = {
+ tipFormatter: PropTypes.func,
+ handleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]),
+ tipProps: PropTypes.object,
+ };
+ static defaultProps = {
+ tipFormatter(value) { return value; },
+ handleStyle: [{}],
+ tipProps: {},
+ };
+ constructor(props) {
+ super(props);
+ this.state = { visibles: {} };
+ }
+ handleTooltipVisibleChange = (index, visible) => {
+ this.setState((prevState) => {
+ return {
+ visibles: {
+ ...prevState.visibles,
+ [index]: visible,
+ },
+ };
+ });
+ }
+ handleWithTooltip = ({ value, dragging, index, disabled, ...restProps }) => {
+ const {
+ tipFormatter,
+ tipProps,
+ handleStyle,
+ } = this.props;
+
+ const {
+ prefixCls = 'rc-slider-tooltip',
+ overlay = tipFormatter(value),
+ placement = 'top',
+ visible = visible || false,
+ ...restTooltipProps,
+ } = tipProps;
+
+ let handleStyleWithIndex;
+ if (Array.isArray(handleStyle)) {
+ handleStyleWithIndex = handleStyle[index] || handleStyle[0];
+ } else {
+ handleStyleWithIndex = handleStyle;
+ }
+
+ return (
+
+
+ this.handleTooltipVisibleChange(index, true)}
+ onMouseLeave={() => this.handleTooltipVisibleChange(index, false)}
+ />
+
+ );
+ }
+ render() {
+ return
;
+ }
+ };
+}
diff --git a/components/vc-slider/src/index.js b/components/vc-slider/src/index.js
new file mode 100644
index 000000000..9ad6d20d7
--- /dev/null
+++ b/components/vc-slider/src/index.js
@@ -0,0 +1,10 @@
+import Slider from './Slider'
+import Range from './Range'
+import Handle from './Handle'
+import createSliderWithTooltip from './createSliderWithTooltip'
+
+Slider.Range = Range
+Slider.Handle = Handle
+Slider.createSliderWithTooltip = createSliderWithTooltip
+export default Slider
+export { Range, Handle, createSliderWithTooltip }
diff --git a/components/vc-slider/src/utils.js b/components/vc-slider/src/utils.js
new file mode 100644
index 000000000..1b2f4a036
--- /dev/null
+++ b/components/vc-slider/src/utils.js
@@ -0,0 +1,92 @@
+import { findDOMNode } from 'react-dom'
+import keyCode from '../../_util/KeyCode'
+
+export function isEventFromHandle (e, handles) {
+ return Object.keys(handles)
+ .some(key => e.target === findDOMNode(handles[key]))
+}
+
+export function isValueOutOfRange (value, { min, max }) {
+ return value < min || value > max
+}
+
+export function isNotTouchEvent (e) {
+ return e.touches.length > 1 ||
+ (e.type.toLowerCase() === 'touchend' && e.touches.length > 0)
+}
+
+export function getClosestPoint (val, { marks, step, min }) {
+ const points = Object.keys(marks).map(parseFloat)
+ if (step !== null) {
+ const closestStep =
+ Math.round((val - min) / step) * step + min
+ points.push(closestStep)
+ }
+ const diffs = points.map(point => Math.abs(val - point))
+ return points[diffs.indexOf(Math.min(...diffs))]
+}
+
+export function getPrecision (step) {
+ const stepString = step.toString()
+ let precision = 0
+ if (stepString.indexOf('.') >= 0) {
+ precision = stepString.length - stepString.indexOf('.') - 1
+ }
+ return precision
+}
+
+export function getMousePosition (vertical, e) {
+ return vertical ? e.clientY : e.pageX
+}
+
+export function getTouchPosition (vertical, e) {
+ return vertical ? e.touches[0].clientY : e.touches[0].pageX
+}
+
+export function getHandleCenterPosition (vertical, handle) {
+ const coords = handle.getBoundingClientRect()
+ return vertical
+ ? coords.top + (coords.height * 0.5)
+ : coords.left + (coords.width * 0.5)
+}
+
+export function ensureValueInRange (val, { max, min }) {
+ if (val <= min) {
+ return min
+ }
+ if (val >= max) {
+ return max
+ }
+ return val
+}
+
+export function ensureValuePrecision (val, props) {
+ const { step } = props
+ const closestPoint = getClosestPoint(val, props)
+ return step === null ? closestPoint
+ : parseFloat(closestPoint.toFixed(getPrecision(step)))
+}
+
+export function pauseEvent (e) {
+ e.stopPropagation()
+ e.preventDefault()
+}
+
+export function getKeyboardValueMutator (e) {
+ switch (e.keyCode) {
+ case keyCode.UP:
+ case keyCode.RIGHT:
+ return (value, props) => value + props.step
+
+ case keyCode.DOWN:
+ case keyCode.LEFT:
+ return (value, props) => value - props.step
+
+ case keyCode.END: return (value, props) => props.max
+ case keyCode.HOME: return (value, props) => props.min
+ case keyCode.PAGE_UP: return (value, props) => value + props.step * 2
+ case keyCode.PAGE_DOWN: return (value, props) => value - props.step * 2
+
+ default: return undefined
+ }
+}
diff --git a/components/vc-steps/Step.vue b/components/vc-steps/Step.vue
index 7f2942811..e150f4126 100644
--- a/components/vc-steps/Step.vue
+++ b/components/vc-steps/Step.vue
@@ -71,8 +71,8 @@ export default {
adjustMarginRight,
} = getOptionProps(this)
- const title = this.title || this.$slots.title
- const description = this.description || this.$slots.description
+ const title = getComponentFromProp(this, 'title')
+ const description = getComponentFromProp(this, 'description')
const classString = {
[`${prefixCls}-item`]: true,
diff --git a/contributors.md b/contributors.md
index bd37f3959..36a3f2161 100644
--- a/contributors.md
+++ b/contributors.md
@@ -41,7 +41,7 @@ TreeSelect
##王
Rate | done
-Pagination | done|select完成后补全
+Pagination | done
Avatar | done
Badge | done
Breadcrumb | done
@@ -49,7 +49,7 @@ Card | done
Collapse | done
Spin | done
Switch | done
-Steps
+Steps | done
Progress
Slider
Table
diff --git a/examples/routes.js b/examples/routes.js
index c798a71bd..928891f4b 100644
--- a/examples/routes.js
+++ b/examples/routes.js
@@ -3,7 +3,7 @@ const AsyncComp = () => {
const hashs = window.location.hash.split('/')
const d = hashs[hashs.length - 1]
return {
- component: import(`../components/pagination/demo/${d}`),
+ component: import(`../components/steps/demo/${d}`),
}
}
export default [