UNPKG

@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
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 };