@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
700 lines • 25.2 kB
JavaScript
import _noop from "lodash/noop";
import _isEqual from "lodash/isEqual";
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import cls from 'classnames';
import { cssClasses } from '@douyinfe/semi-foundation/lib/es/slider/constants';
import BaseComponent from '../_base/baseComponent';
import SliderFoundation from '@douyinfe/semi-foundation/lib/es/slider/foundation';
import Tooltip from '../tooltip/index';
import '@douyinfe/semi-foundation/lib/es/slider/slider.css';
const prefixCls = cssClasses.PREFIX;
function domIsInRenderTree(e) {
if (!e) {
return false;
}
return Boolean(e.offsetWidth || e.offsetHeight || e.getClientRects().length);
}
export default class Slider extends BaseComponent {
constructor(props) {
super(props);
this.renderHandle = () => {
var _a, _b, _c, _d, _e;
const {
vertical,
range,
tooltipVisible,
tipFormatter,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
'aria-valuetext': ariaValueText,
getAriaValueText,
disabled
} = this.props;
const {
chooseMovePos,
isDrag,
isInRenderTree,
firstDotFocusVisible,
secondDotFocusVisible
} = this.state;
const stylePos = vertical ? 'top' : 'left';
const percentInfo = this.foundation.getMinAndMaxPercent(this.state.currentValue);
const minPercent = percentInfo.min;
const maxPercent = percentInfo.max;
const {
tipVisible,
tipChildren
} = this.foundation.computeHandleVisibleVal(tooltipVisible && isInRenderTree, tipFormatter, range);
const minClass = cls(cssClasses.HANDLE, {
[`${cssClasses.HANDLE}-clicked`]: chooseMovePos === 'min' && isDrag
});
const maxClass = cls(cssClasses.HANDLE, {
[`${cssClasses.HANDLE}-clicked`]: chooseMovePos === 'max' && isDrag
});
const {
min,
max,
currentValue
} = this.state;
const commonAria = {
'aria-label': ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : disabled ? 'Disabled Slider' : undefined,
'aria-labelledby': ariaLabelledby,
'aria-disabled': disabled
};
vertical && Object.assign(commonAria, {
'aria-orientation': 'vertical'
});
const handleDot = this.props.handleDot;
const handleContents = !range ? (/*#__PURE__*/React.createElement(Tooltip, {
content: tipChildren.min,
showArrow: this.props.showArrow,
position: "top",
trigger: "custom",
rePosKey: minPercent,
visible: isInRenderTree && (tipVisible.min || firstDotFocusVisible),
className: `${cssClasses.HANDLE}-tooltip`
}, /*#__PURE__*/React.createElement("span", Object.assign({
onMouseOver: this.foundation.checkAndUpdateIsInRenderTreeState,
ref: this.minHanleEl,
className: minClass,
style: {
[stylePos]: `${minPercent * 100}%`,
zIndex: chooseMovePos === 'min' && isDrag ? 2 : 1
},
onMouseDown: e => {
this.foundation.onHandleDown(e, 'min');
},
onMouseEnter: () => {
this.foundation.onHandleEnter('min');
},
onTouchStart: e => {
this.foundation.onHandleTouchStart(e, 'min');
},
onMouseLeave: () => {
this.foundation.onHandleLeave();
},
onKeyUp: e => {
this.foundation.onHandleUp(e);
},
onTouchEnd: e => {
this.foundation.onHandleUp(e);
},
onKeyDown: e => {
this.foundation.handleKeyDown(e, 'min');
},
onFocus: e => {
this.foundation.onFocus(e, 'min');
},
onBlur: e => {
this.foundation.onBlur(e, 'min');
},
role: "slider",
"aria-valuetext": getAriaValueText ? getAriaValueText(currentValue, 0) : ariaValueText,
tabIndex: disabled ? -1 : 0
}, commonAria, {
"aria-valuenow": currentValue,
"aria-valuemax": max,
"aria-valuemin": min
}), handleDot && /*#__PURE__*/React.createElement("div", {
className: cssClasses.HANDLE_DOT,
style: Object.assign(Object.assign({}, (handleDot === null || handleDot === void 0 ? void 0 : handleDot.size) ? {
width: handleDot.size,
height: handleDot.size
} : {}), (handleDot === null || handleDot === void 0 ? void 0 : handleDot.color) ? {
backgroundColor: handleDot.color
} : {})
})))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Tooltip, {
content: tipChildren.min,
position: "top",
trigger: "custom",
rePosKey: minPercent,
visible: isInRenderTree && (tipVisible.min || firstDotFocusVisible),
className: `${cssClasses.HANDLE}-tooltip`
}, /*#__PURE__*/React.createElement("span", Object.assign({
ref: this.minHanleEl,
className: minClass,
style: {
[stylePos]: `${minPercent * 100}%`,
zIndex: chooseMovePos === 'min' ? 2 : 1
},
onMouseDown: e => {
this.foundation.onHandleDown(e, 'min');
},
onMouseEnter: () => {
this.foundation.onHandleEnter('min');
},
onTouchStart: e => {
this.foundation.onHandleTouchStart(e, 'min');
},
onMouseLeave: () => {
this.foundation.onHandleLeave();
},
onKeyUp: e => {
this.foundation.onHandleUp(e);
},
onTouchEnd: e => {
this.foundation.onHandleUp(e);
},
onKeyDown: e => {
this.foundation.handleKeyDown(e, 'min');
},
onFocus: e => {
this.foundation.onFocus(e, 'min');
},
onBlur: e => {
this.foundation.onBlur(e, 'min');
},
role: "slider",
tabIndex: disabled ? -1 : 0
}, commonAria, {
"aria-valuetext": getAriaValueText ? getAriaValueText(currentValue[0], 0) : ariaValueText,
"aria-valuenow": currentValue[0],
"aria-valuemax": currentValue[1],
"aria-valuemin": min
}), (handleDot === null || handleDot === void 0 ? void 0 : handleDot[0]) && /*#__PURE__*/React.createElement("div", {
className: cssClasses.HANDLE_DOT,
style: Object.assign(Object.assign({}, ((_a = handleDot[0]) === null || _a === void 0 ? void 0 : _a.size) ? {
width: handleDot[0].size,
height: handleDot[0].size
} : {}), ((_b = handleDot[0]) === null || _b === void 0 ? void 0 : _b.color) ? {
backgroundColor: handleDot[0].color
} : {})
}))), /*#__PURE__*/React.createElement(Tooltip, {
content: tipChildren.max,
position: "top",
trigger: "custom",
rePosKey: maxPercent,
visible: isInRenderTree && (tipVisible.max || secondDotFocusVisible),
className: `${cssClasses.HANDLE}-tooltip`
}, /*#__PURE__*/React.createElement("span", Object.assign({
ref: this.maxHanleEl,
className: maxClass,
style: {
[stylePos]: `${maxPercent * 100}%`,
zIndex: chooseMovePos === 'max' ? 2 : 1
},
onMouseDown: e => {
this.foundation.onHandleDown(e, 'max');
},
onMouseEnter: () => {
this.foundation.onHandleEnter('max');
},
onMouseLeave: () => {
this.foundation.onHandleLeave();
},
onKeyUp: e => {
this.foundation.onHandleUp(e);
},
onTouchStart: e => {
this.foundation.onHandleTouchStart(e, 'max');
},
onTouchEnd: e => {
this.foundation.onHandleUp(e);
},
onKeyDown: e => {
this.foundation.handleKeyDown(e, 'max');
},
onFocus: e => {
this.foundation.onFocus(e, 'max');
},
onBlur: e => {
this.foundation.onBlur(e, 'max');
},
role: "slider",
tabIndex: disabled ? -1 : 0
}, commonAria, {
"aria-valuetext": getAriaValueText ? getAriaValueText(currentValue[1], 1) : ariaValueText,
"aria-valuenow": currentValue[1],
"aria-valuemax": max,
"aria-valuemin": currentValue[0]
}), ((_c = this.props.handleDot) === null || _c === void 0 ? void 0 : _c[1]) && /*#__PURE__*/React.createElement("div", {
className: cssClasses.HANDLE_DOT,
style: Object.assign(Object.assign({}, ((_d = this.props.handleDot[1]) === null || _d === void 0 ? void 0 : _d.size) ? {
width: this.props.handleDot[1].size,
height: this.props.handleDot[1].size
} : {}), ((_e = this.props.handleDot[1]) === null || _e === void 0 ? void 0 : _e.color) ? {
backgroundColor: this.props.handleDot[1].color
} : {})
})))));
return handleContents;
};
this.renderTrack = () => {
const {
range,
included,
vertical
} = this.props;
const percentInfo = this.foundation.getMinAndMaxPercent(this.state.currentValue);
const minPercent = percentInfo.min;
const maxPercent = percentInfo.max;
let trackStyle = !vertical ? {
width: range ? `${Math.abs(maxPercent - minPercent) * 100}%` : `${minPercent * 100}%`,
left: range ? `${Math.min(minPercent, maxPercent) * 100}%` : 0
} : {
height: range ? `${Math.abs(maxPercent - minPercent) * 100}%` : `${minPercent * 100}%`,
top: range ? `${Math.min(minPercent, maxPercent) * 100}%` : 0
};
trackStyle = included ? trackStyle : {};
return (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
React.createElement("div", {
className: cssClasses.TRACK,
style: trackStyle,
onClick: this.foundation.handleWrapClick
})
);
};
this.renderStepDot = () => {
const {
min,
max,
vertical,
marks
} = this.props;
const stylePos = vertical ? 'top' : 'left';
const labelContent = marks && Object.keys(marks).length > 0 ? (/*#__PURE__*/React.createElement("div", {
className: cssClasses.DOTS
}, Object.keys(marks).map(mark => {
const activeResult = this.foundation.isMarkActive(Number(mark));
const markClass = cls(`${prefixCls}-dot`, {
[`${prefixCls}-dot-active`]: this.foundation.isMarkActive(Number(mark)) === 'active'
});
const markPercent = (Number(mark) - min) / (max - min);
const dotDOM =
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
React.createElement("span", {
key: mark,
onClick: this.foundation.handleWrapClick,
className: markClass,
style: {
[stylePos]: `calc(${markPercent * 100}% - 2px)`
}
});
return activeResult ? this.props.tooltipOnMark ? /*#__PURE__*/React.createElement(Tooltip, {
content: marks[mark]
}, dotDOM) : dotDOM : null;
}))) : null;
return labelContent;
};
this.renderLabel = () => {
if (!this.props.showMarkLabel) {
return null;
}
const {
min,
max,
vertical,
marks,
verticalReverse
} = this.props;
const stylePos = vertical ? 'top' : 'left';
const labelContent = marks && Object.keys(marks).length > 0 ? (/*#__PURE__*/React.createElement("div", {
className: cssClasses.MARKS + (vertical && verticalReverse ? '-reverse' : '')
}, Object.keys(marks).map(mark => {
const activeResult = this.foundation.isMarkActive(Number(mark));
const markPercent = (Number(mark) - min) / (max - min);
return activeResult ? (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
React.createElement("span", {
key: mark,
className: cls(`${prefixCls}-mark${vertical && verticalReverse ? '-reverse' : ''}`),
style: {
[stylePos]: `${markPercent * 100}%`
},
onClick: this.foundation.handleWrapClick
}, marks[mark])) : null;
}))) : null;
return labelContent;
};
this._getAriaValueText = (value, index) => {
const {
getAriaValueText
} = this.props;
return getAriaValueText ? getAriaValueText(value, index) : value;
};
let {
value
} = this.props;
if (!value) {
value = this.props.defaultValue;
}
this.state = {
currentValue: value ? value : this.props.range ? [0, 0] : 0,
min: this.props.min || 0,
max: this.props.max || 0,
focusPos: '',
onChange: this.props.onChange,
disabled: this.props.disabled || false,
chooseMovePos: '',
isDrag: false,
clickValue: 0,
showBoundary: false,
isInRenderTree: true,
firstDotFocusVisible: false,
secondDotFocusVisible: false
};
this.sliderEl = /*#__PURE__*/React.createRef();
this.minHanleEl = /*#__PURE__*/React.createRef();
this.maxHanleEl = /*#__PURE__*/React.createRef();
this.dragging = [false, false];
this.foundation = new SliderFoundation(this.adapter);
this.eventListenerSet = new Set();
this.handleDownEventListenerSet = new Set();
}
get adapter() {
var _this = this;
return Object.assign(Object.assign({}, super.adapter), {
getSliderLengths: () => {
var _a;
if (this.sliderEl && this.sliderEl.current) {
const rect = this.sliderEl.current.getBoundingClientRect();
const offsetParentRect = (_a = this.sliderEl.current.offsetParent) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
const offset = {
x: offsetParentRect ? rect.left - offsetParentRect.left : this.sliderEl.current.offsetLeft,
y: offsetParentRect ? rect.top - offsetParentRect.top : this.sliderEl.current.offsetTop
};
return {
sliderX: offset.x,
sliderY: offset.y,
sliderWidth: rect.width,
sliderHeight: rect.height
};
}
return {
sliderX: 0,
sliderY: 0,
sliderWidth: 0,
sliderHeight: 0
};
},
getParentRect: () => {
const parentObj = this.sliderEl && this.sliderEl.current && this.sliderEl.current.offsetParent;
if (!parentObj) {
return undefined;
}
return parentObj.getBoundingClientRect();
},
getScrollParentVal: () => {
const scrollParent = this.foundation.getScrollParent(this.sliderEl.current);
return {
scrollTop: scrollParent.scrollTop,
scrollLeft: scrollParent.scrollLeft
};
},
isEventFromHandle: e => {
const handles = [this.minHanleEl, this.maxHanleEl];
let flag = false;
handles.forEach(handle => {
if (!handle) {
return;
}
const handleInstance = handle && handle.current;
const handleDom = ReactDOM.findDOMNode(handleInstance);
if (handleDom && handleDom.contains(e.target)) {
flag = true;
}
});
return flag;
},
getOverallVars: () => ({
dragging: this.dragging
}),
updateDisabled: disabled => {
this.setState({
disabled
});
},
transNewPropsToState(stateObj) {
let callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _noop;
this.setState(stateObj, callback);
},
notifyChange: cbValue => {
this.props.onChange(Array.isArray(cbValue) ? [...cbValue].sort((a, b) => a - b) : cbValue);
},
setDragging: value => {
this.dragging = value;
},
updateCurrentValue: value => {
const {
currentValue
} = this.state;
if (value !== currentValue) {
this.setState({
currentValue: value
});
}
},
setOverallVars: (key, value) => {
this[key] = value;
},
getMinHandleEl: () => this.minHanleEl.current,
getMaxHandleEl: () => this.maxHanleEl.current,
onHandleDown: e => {
this.handleDownEventListenerSet.add(this._addEventListener(document.body, 'mousemove', this.foundation.onHandleMove, false));
this.handleDownEventListenerSet.add(this._addEventListener(window, 'mouseup', this.foundation.onHandleUp, false));
this.handleDownEventListenerSet.add(this._addEventListener(document.body, 'touchmove', this.foundation.onHandleTouchMove, false));
},
onHandleMove: function (mousePos, isMin) {
let stateChangeCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _noop;
let clickTrack = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
let outPutValue = arguments.length > 4 ? arguments[4] : undefined;
const sliderDOMIsInRenderTree = _this.foundation.checkAndUpdateIsInRenderTreeState();
if (!sliderDOMIsInRenderTree) {
return;
}
const {
value
} = _this.props;
let finalOutPutValue = outPutValue;
if (finalOutPutValue === undefined) {
const moveValue = _this.foundation.transPosToValue(mousePos, isMin);
if (moveValue === false) {
return;
}
finalOutPutValue = _this.foundation.outPutValue(moveValue);
}
const {
currentValue
} = _this.state;
if (!_isEqual(_this.foundation.outPutValue(currentValue), finalOutPutValue)) {
if (!clickTrack && _this.foundation.valueFormatIsCorrect(value)) {
// still require afterChangeCallback when click on the track directly, need skip here
return false;
}
_this.setState({
currentValue: finalOutPutValue
}, stateChangeCallback);
}
},
setEventDefault: e => {
e.stopPropagation();
e.preventDefault();
},
setStateVal: (name, val) => {
this.setState({
[name]: val
});
},
checkAndUpdateIsInRenderTreeState: () => {
const sliderDOMIsInRenderTree = domIsInRenderTree(this.sliderEl.current);
if (sliderDOMIsInRenderTree !== this.state.isInRenderTree) {
this.setState({
isInRenderTree: sliderDOMIsInRenderTree
});
}
return sliderDOMIsInRenderTree;
},
onHandleEnter: pos => {
this.setState({
focusPos: pos
});
},
onHandleLeave: () => {
this.setState({
focusPos: ''
});
},
onHandleUpBefore: e => {
var _a, _b;
(_b = (_a = this.props).onMouseUp) === null || _b === void 0 ? void 0 : _b.call(_a, e);
e.stopPropagation();
e.preventDefault();
Array.from(this.handleDownEventListenerSet).forEach(clear => clear());
this.handleDownEventListenerSet.clear();
},
onHandleUpAfter: () => {
const {
currentValue
} = this.state;
const value = this.foundation.outPutValue(currentValue);
this.props.onAfterChange(value);
},
unSubscribeEventListener: () => {
Array.from(this.eventListenerSet).forEach(clear => clear());
}
});
}
componentDidMount() {
this.foundation.init();
}
componentDidUpdate(prevProps, prevState) {
const hasPropValueChange = !_isEqual(this.props.value, prevProps.value);
const hasPropDisabledChange = this.props.disabled !== prevProps.disabled;
if (hasPropDisabledChange) {
this.foundation.handleDisabledChange(this.props.disabled);
}
if (hasPropValueChange) {
const nextValue = this.props.value;
const prevValue = this.state.currentValue;
this.foundation.handleValueChange(prevValue, nextValue);
// trigger onAfterChange when value is controlled and changed
this.props.onAfterChange(this.props.value);
}
}
componentWillUnmount() {
this.foundation.destroy();
}
render() {
const {
disabled,
currentValue,
min,
max
} = this.state;
const _a = this.props,
{
vertical,
verticalReverse,
style,
railStyle,
range,
className
} = _a,
rest = __rest(_a, ["vertical", "verticalReverse", "style", "railStyle", "range", "className"]);
const wrapperClass = cls(`${prefixCls}-wrapper`, {
[`${prefixCls}-disabled`]: disabled,
[`${cssClasses.VERTICAL}-wrapper`]: vertical,
[`${prefixCls}-reverse`]: vertical && verticalReverse
}, className);
const boundaryClass = cls(`${prefixCls}-boundary`, {
[`${prefixCls}-boundary-show`]: this.props.showBoundary && this.state.showBoundary
});
const sliderCls = cls({
[`${prefixCls}`]: !vertical,
[cssClasses.VERTICAL]: vertical
});
const fixedCurrentValue = Array.isArray(currentValue) ? [...currentValue].sort() : currentValue;
const ariaLabel = range ? `Range: ${this._getAriaValueText(fixedCurrentValue[0], 0)} to ${this._getAriaValueText(fixedCurrentValue[1], 1)}` : undefined;
const slider = /*#__PURE__*/React.createElement("div", Object.assign({
className: wrapperClass,
style: style,
ref: this.sliderEl,
"aria-label": ariaLabel,
onMouseEnter: () => this.foundation.handleWrapperEnter(),
onMouseLeave: () => this.foundation.handleWrapperLeave()
}, this.getDataAttr(rest)), /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-rail`,
onClick: this.foundation.handleWrapClick,
style: railStyle
}), this.renderTrack(), this.renderStepDot(), /*#__PURE__*/React.createElement("div", null, this.renderHandle()), this.renderLabel(), /*#__PURE__*/React.createElement("div", {
className: boundaryClass
}, /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-boundary-min`
}, min), /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-boundary-max`
}, max)));
if (!vertical) {
return /*#__PURE__*/React.createElement("div", {
className: sliderCls
}, slider);
}
return slider;
}
_addEventListener(target, eventName, callback) {
if (target.addEventListener) {
for (var _len = arguments.length, rests = new Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
rests[_key - 3] = arguments[_key];
}
target.addEventListener(eventName, callback, ...rests);
const clearSelf = () => {
target === null || target === void 0 ? void 0 : target.removeEventListener(eventName, callback);
Promise.resolve().then(() => {
this.eventListenerSet.delete(clearSelf);
});
};
this.eventListenerSet.add(clearSelf);
return clearSelf;
} else {
return _noop;
}
}
}
Slider.propTypes = {
// allowClear: PropTypes.bool,
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
disabled: PropTypes.bool,
showMarkLabel: PropTypes.bool,
included: PropTypes.bool,
marks: PropTypes.object,
max: PropTypes.number,
min: PropTypes.number,
range: PropTypes.bool,
step: PropTypes.number,
tipFormatter: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
vertical: PropTypes.bool,
onAfterChange: PropTypes.func,
onChange: PropTypes.func,
onMouseUp: PropTypes.func,
tooltipOnMark: PropTypes.bool,
tooltipVisible: PropTypes.bool,
showArrow: PropTypes.bool,
style: PropTypes.object,
className: PropTypes.string,
showBoundary: PropTypes.bool,
railStyle: PropTypes.object,
verticalReverse: PropTypes.bool,
getAriaValueText: PropTypes.func,
handleDot: PropTypes.oneOfType([PropTypes.shape({
size: PropTypes.string,
color: PropTypes.string
}), PropTypes.arrayOf(PropTypes.shape({
size: PropTypes.string,
color: PropTypes.string
}))])
};
Slider.defaultProps = {
// allowClear: false,
disabled: false,
showMarkLabel: true,
tooltipOnMark: false,
included: true,
max: 100,
min: 0,
range: false,
showArrow: true,
step: 1,
tipFormatter: value => value,
vertical: false,
showBoundary: false,
onAfterChange: value => {
// console.log(value);
},
onChange: value => {
// console.log(value);
},
verticalReverse: false
};