refactor: slider

pull/4606/head
tangjinzhou 2021-08-27 22:05:21 +08:00
parent 0535f09edf
commit db6acc5199
16 changed files with 541 additions and 244 deletions

View File

@ -3,6 +3,7 @@ import classNames from '../classNames';
import { isVNode, Fragment, Comment, Text, h } from 'vue';
import { camelize, hyphenate, isOn, resolvePropValue } from '../util';
import isValid from '../isValid';
import initDefaultProps from './initDefaultProps';
// function getType(fn) {
// const match = fn && fn.toString().match(/^\s*function (\w+)/);
// return match ? match[1] : '';
@ -367,16 +368,6 @@ export function filterEmpty(children = []) {
});
return res.filter(c => !isEmptyElement(c));
}
const initDefaultProps = (propTypes, defaultProps) => {
Object.keys(defaultProps).forEach(k => {
if (propTypes[k]) {
propTypes[k].def && (propTypes[k] = propTypes[k].def(defaultProps[k]));
} else {
throw new Error(`not have ${k} prop`);
}
});
return propTypes;
};
export function mergeProps() {
const args = [].slice.call(arguments, 0);
@ -401,7 +392,6 @@ function isValidElement(element) {
function getPropsSlot(slots, props, prop = 'default') {
return props[prop] ?? slots[prop]?.();
}
export {
splitAttrs,
hasProp,

View File

@ -0,0 +1,50 @@
import { onBeforeUnmount, watch } from 'vue';
import { onActivated } from 'vue';
import { defineComponent, ref } from 'vue';
import Tooltip, { tooltipProps } from '../tooltip';
import raf from '../_util/raf';
export default defineComponent({
props: tooltipProps,
name: 'SliderTooltip',
inheritAttrs: false,
setup(props, { attrs, slots }) {
const innerRef = ref<any>(null);
const rafRef = ref<number | null>(null);
function cancelKeepAlign() {
raf.cancel(rafRef.value!);
rafRef.value = null;
}
function keepAlign() {
rafRef.value = raf(() => {
innerRef.value?.forcePopupAlign();
rafRef.value = null;
});
}
const align = () => {
cancelKeepAlign();
if (props.visible) {
keepAlign();
}
};
watch(
[() => props.visible, () => props.title],
() => {
align();
},
{ flush: 'post', immediate: true },
);
onActivated(() => {
align();
});
onBeforeUnmount(() => {
cancelKeepAlign();
});
return () => {
return <Tooltip ref={innerRef} {...props} {...attrs} v-slots={slots} />;
};
},
});

View File

@ -1,56 +1,76 @@
import type { VNodeTypes } from 'vue';
import { defineComponent, inject } from 'vue';
import PropTypes from '../_util/vue-types';
import { computed, CSSProperties, ref, VNodeTypes } from 'vue';
import { defineComponent } from 'vue';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps } from '../_util/props-util';
import VcSlider from '../vc-slider/src/Slider';
import VcRange from '../vc-slider/src/Range';
import VcHandle from '../vc-slider/src/Handle';
import Tooltip from '../tooltip';
import { defaultConfigProvider } from '../config-provider';
import abstractTooltipProps from '../tooltip/abstractTooltipProps';
import { withInstall } from '../_util/type';
import { VueNode, withInstall } from '../_util/type';
import { PropType } from 'vue';
import { TooltipPlacement } from '../tooltip/Tooltip';
import useConfigInject from '../_util/hooks/useConfigInject';
import SliderTooltip from './SliderTooltip';
import classNames from '../_util/classNames';
export type SliderValue = number | [number, number];
interface HandleGeneratorInfo {
value: number;
dragging: boolean;
index: number;
rest: any[];
interface SliderMarks {
[key: number]:
| VueNode
| {
style: CSSProperties;
label: any;
};
}
interface HandleGeneratorInfo {
value?: number;
dragging?: boolean;
index: number;
rest?: any[];
}
interface SliderRange {
draggableTrack?: boolean;
}
export type HandleGeneratorFn = (config: {
tooltipPrefixCls?: string;
prefixCls?: string;
info: HandleGeneratorInfo;
}) => VNodeTypes;
type Value = [number, number] | number;
const tooltipProps = abstractTooltipProps();
const defaultTipFormatter = (value: number) => (typeof value === 'number' ? value.toString() : '');
export const SliderProps = () => ({
prefixCls: PropTypes.string,
tooltipPrefixCls: PropTypes.string,
range: PropTypes.looseBool,
reverse: PropTypes.looseBool,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.any,
marks: PropTypes.object,
dots: PropTypes.looseBool,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]),
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]),
included: PropTypes.looseBool,
disabled: PropTypes.looseBool,
vertical: PropTypes.looseBool,
tipFormatter: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
tooltipVisible: PropTypes.looseBool,
tooltipPlacement: tooltipProps.placement,
getTooltipPopupContainer: PropTypes.func,
onChange: PropTypes.func,
onAfterChange: PropTypes.func,
prefixCls: String,
tooltipPrefixCls: String,
range: { type: [Boolean, Object] as PropType<boolean | SliderRange>, default: undefined },
reverse: { type: Boolean, default: undefined },
min: Number,
max: Number,
step: { type: [Number, Object] as PropType<null | number> },
marks: { type: Object as PropType<SliderMarks> },
dots: { type: Boolean, default: undefined },
value: { type: [Number, Array] as PropType<Value> },
defaultValue: { type: [Number, Array] as PropType<Value> },
included: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
vertical: { type: Boolean, default: undefined },
tipFormatter: {
type: Function as PropType<(value?: number) => any>,
default: defaultTipFormatter,
},
tooltipVisible: { type: Boolean, default: undefined },
tooltipPlacement: { type: String as PropType<TooltipPlacement> },
getTooltipPopupContainer: {
type: Function as PropType<(triggerNode: HTMLElement) => HTMLElement>,
},
autofocus: { type: Boolean, default: undefined },
onChange: { type: Function as PropType<(value: Value) => void> },
onAfterChange: { type: Function as PropType<(value: Value) => void> },
handleStyle: { type: [Object, Array] as PropType<CSSProperties[] | CSSProperties> },
trackStyle: { type: [Object, Array] as PropType<CSSProperties[] | CSSProperties> },
});
const defaultTipFormatter = (value: number) => value.toString();
export type Visibles = { [index: number]: boolean };
const Slider = defineComponent({
name: 'ASlider',
@ -59,109 +79,125 @@ const Slider = defineComponent({
props: {
...SliderProps(),
},
emits: ['update:value', 'change'],
setup() {
return {
vcSlider: null,
configProvider: inject('configProvider', defaultConfigProvider),
emits: ['update:value', 'change', 'afterChange'],
slots: ['mark'],
setup(props, { attrs, slots, emit, expose }) {
const { prefixCls, rootPrefixCls, direction, getPopupContainer, configProvider } =
useConfigInject('slider', props);
const sliderRef = ref();
const visibles = ref<Visibles>({});
const toggleTooltipVisible = (index: number, visible: boolean) => {
visibles.value[index] = visible;
};
},
data() {
return {
visibles: {},
const tooltipPlacement = computed(() => {
if (props.tooltipPlacement) {
return props.tooltipPlacement;
}
if (!props.vertical) {
return 'top';
}
return direction.value === 'rtl' ? 'left' : 'right';
});
const focus = () => {
sliderRef.value?.focus();
};
},
methods: {
toggleTooltipVisible(index: number, visible: boolean) {
this.setState(({ visibles }) => ({
visibles: {
...visibles,
[index]: visible,
},
}));
},
handleWithTooltip(
tooltipPrefixCls: string,
prefixCls: string,
{ value, dragging, index, ...restProps }: HandleGeneratorInfo,
): VNodeTypes {
const {
tipFormatter = defaultTipFormatter,
tooltipVisible,
tooltipPlacement,
getTooltipPopupContainer,
} = this.$props;
const { visibles } = this;
const isTipFormatter = tipFormatter ? visibles[index] || dragging : false;
const visible = tooltipVisible || (tooltipVisible === undefined && isTipFormatter);
const tooltipProps = {
prefixCls: tooltipPrefixCls,
title: tipFormatter ? tipFormatter(value) : '',
visible,
placement: tooltipPlacement || 'top',
transitionName: 'zoom-down',
overlayClassName: `${prefixCls}-tooltip`,
getPopupContainer: getTooltipPopupContainer || (() => document.body),
key: index,
};
const handleProps = {
value,
...restProps,
onMouseenter: () => this.toggleTooltipVisible(index, true),
onMouseleave: () => this.toggleTooltipVisible(index, false),
};
return (
<Tooltip {...tooltipProps}>
<VcHandle {...handleProps} />
</Tooltip>
);
},
saveSlider(node: any) {
this.vcSlider = node;
},
focus() {
this.vcSlider.focus();
},
blur() {
this.vcSlider.blur();
},
handleChange(val: SliderValue) {
this.$emit('update:value', val);
this.$emit('change', val);
},
},
render() {
const {
range,
prefixCls: customizePrefixCls,
tooltipPrefixCls: customizeTooltipPrefixCls,
...restProps
} = { ...getOptionProps(this), ...this.$attrs } as any;
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('slider', customizePrefixCls);
const tooltipPrefixCls = getPrefixCls('tooltip', customizeTooltipPrefixCls);
if (range) {
const vcRangeProps = {
...restProps,
prefixCls,
tooltipPrefixCls,
handle: (info: HandleGeneratorInfo) =>
this.handleWithTooltip(tooltipPrefixCls, prefixCls, info),
ref: this.saveSlider,
onChange: this.handleChange,
};
return <VcRange {...vcRangeProps} />;
}
const vcSliderProps = {
...restProps,
prefixCls,
const blur = () => {
sliderRef.value?.blur();
};
const handleChange = (val: SliderValue) => {
emit('update:value', val);
emit('change', val);
};
expose({
focus,
blur,
});
const handleWithTooltip: HandleGeneratorFn = ({
tooltipPrefixCls,
handle: (info: HandleGeneratorInfo) =>
this.handleWithTooltip(tooltipPrefixCls, prefixCls, info),
ref: this.saveSlider,
onChange: this.handleChange,
info: { value, dragging, index, ...restProps },
}) => {
const { tipFormatter, tooltipVisible, getTooltipPopupContainer } = props;
const isTipFormatter = tipFormatter ? visibles.value[index] || dragging : false;
const visible = tooltipVisible || (tooltipVisible === undefined && isTipFormatter);
return (
<SliderTooltip
prefixCls={tooltipPrefixCls}
title={tipFormatter ? tipFormatter(value) : ''}
visible={visible}
placement={tooltipPlacement.value}
transitionName={`${rootPrefixCls.value}-zoom-down`}
key={index}
overlayClassName={`${prefixCls.value}-tooltip`}
getPopupContainer={getTooltipPopupContainer || getPopupContainer.value}
>
<VcHandle
{...restProps}
value={value}
onMouseenter={() => toggleTooltipVisible(index, true)}
onMouseleave={() => toggleTooltipVisible(index, false)}
/>
</SliderTooltip>
);
};
return () => {
const { tooltipPrefixCls: customizeTooltipPrefixCls, range, ...restProps } = props;
const tooltipPrefixCls = configProvider.getPrefixCls('tooltip', customizeTooltipPrefixCls);
const cls = classNames(attrs.class, {
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
});
// make reverse default on rtl direction
if (direction.value === 'rtl' && !restProps.vertical) {
restProps.reverse = !restProps.reverse;
}
// extrack draggableTrack from range={{ ... }}
let draggableTrack: boolean | undefined;
if (typeof range === 'object') {
draggableTrack = range.draggableTrack;
}
if (range) {
return (
<VcRange
{...restProps}
step={restProps.step!}
draggableTrack={draggableTrack}
class={cls}
ref={ref}
handle={(info: HandleGeneratorInfo) =>
handleWithTooltip({
tooltipPrefixCls,
prefixCls: prefixCls.value,
info,
})
}
prefixCls={prefixCls.value}
onChange={handleChange}
v-slots={{ mark: slots.mark }}
/>
);
}
return (
<VcSlider
{...restProps}
step={restProps.step!}
class={cls}
ref={ref}
handle={(info: HandleGeneratorInfo) =>
handleWithTooltip({
tooltipPrefixCls,
prefixCls: prefixCls.value,
info,
})
}
prefixCls={prefixCls.value}
onChange={handleChange}
v-slots={{ mark: slots.mark }}
/>
);
};
return <VcSlider {...vcSliderProps} />;
},
});

View File

@ -0,0 +1,70 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@slider-prefix-cls: ~'@{ant-prefix}-slider';
.@{slider-prefix-cls} {
&-rtl {
direction: rtl;
}
&-mark {
.@{slider-prefix-cls}-rtl & {
right: 0;
left: auto;
}
}
&-dot {
.@{slider-prefix-cls}-rtl & {
margin-right: -4px;
margin-left: 0;
}
&:first-child {
.@{slider-prefix-cls}-rtl & {
margin-right: -4px;
margin-left: 0;
}
}
&:last-child {
.@{slider-prefix-cls}-rtl & {
margin-right: -4px;
margin-left: 0;
}
}
}
}
.vertical() {
&-vertical {
.@{slider-prefix-cls}-handle {
.@{slider-prefix-cls}-rtl& {
margin-right: -5px;
margin-left: 0;
}
}
.@{slider-prefix-cls}-mark {
.@{slider-prefix-cls}-rtl& {
right: 12px;
left: auto;
}
}
.@{slider-prefix-cls}-mark-text {
.@{slider-prefix-cls}-rtl& {
right: 4px;
left: auto;
}
}
.@{slider-prefix-cls}-dot {
.@{slider-prefix-cls}-rtl& {
right: 2px;
left: auto;
}
}
}
}

View File

@ -15,7 +15,7 @@ import getPlacements, { AdjustOverflow, PlacementsConfig } from './placements';
export { AdjustOverflow, PlacementsConfig };
export type TooltipPlacement = typeof placementTypes;
export type TooltipPlacement = typeof placementTypes[number];
// https://github.com/react-component/tooltip
// https://github.com/yiminghe/dom-align
@ -44,7 +44,7 @@ const props = abstractTooltipProps();
const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`);
const tooltipProps = {
export const tooltipProps = {
...props,
title: PropTypes.VNodeChild,
};
@ -88,8 +88,11 @@ export default defineComponent({
};
const handleVisibleChange = (val: boolean) => {
visible.value = isNoTitle() ? false : val;
if (!isNoTitle()) {
const noTitle = isNoTitle();
if (props.visible === undefined) {
visible.value = noTitle ? false : val;
}
if (!noTitle) {
emit('update:visible', val);
emit('visibleChange', val);
}
@ -99,7 +102,7 @@ export default defineComponent({
return tooltip.value.getPopupDomNode();
};
expose({ getPopupDomNode, visible });
expose({ getPopupDomNode, visible, forcePopupAlign: () => tooltip.value?.forcePopupAlign() });
const tooltipPlacements = computed(() => {
const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = props;

View File

@ -1,5 +1,5 @@
import { withInstall } from '../_util/type';
import ToolTip from './Tooltip';
import ToolTip, { tooltipProps } from './Tooltip';
export type {
TooltipProps,
@ -9,4 +9,6 @@ export type {
PlacementTypes,
} from './Tooltip';
export { tooltipProps };
export default withInstall(ToolTip);

View File

@ -19,6 +19,9 @@ export default defineComponent({
value: PropTypes.number,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
reverse: PropTypes.looseBool,
ariaLabel: String,
ariaLabelledBy: String,
ariaValueTextFormatter: Function,
// handleFocus: PropTypes.func.def(noop),
// handleBlur: PropTypes.func.def(noop),
},
@ -68,13 +71,26 @@ export default defineComponent({
},
// when click can not focus in vue, use mousedown trigger focus
handleMousedown(e) {
e.preventDefault();
this.focus();
this.__emit('mousedown', e);
},
},
render() {
const { prefixCls, vertical, reverse, offset, disabled, min, max, value, tabindex } =
getOptionProps(this);
const {
prefixCls,
vertical,
reverse,
offset,
disabled,
min,
max,
value,
tabindex,
ariaLabel,
ariaLabelledBy,
ariaValueTextFormatter,
} = getOptionProps(this);
const className = classNames(this.$attrs.class, {
[`${prefixCls}-handle-click-focused`]: this.clickFocused,
});
@ -83,7 +99,7 @@ export default defineComponent({
? {
[reverse ? 'top' : 'bottom']: `${offset}%`,
[reverse ? 'bottom' : 'top']: 'auto',
transform: `translateY(+50%)`,
transform: reverse ? null : `translateY(+50%)`,
}
: {
[reverse ? 'right' : 'left']: `${offset}%`,
@ -101,15 +117,20 @@ export default defineComponent({
...this.$attrs.style,
...positionStyle,
};
let _tabIndex = tabindex || 0;
let mergedTabIndex = tabindex || 0;
if (disabled || tabindex === null) {
_tabIndex = null;
mergedTabIndex = null;
}
let ariaValueText;
if (ariaValueTextFormatter) {
ariaValueText = ariaValueTextFormatter(value);
}
const handleProps = {
...this.$attrs,
role: 'slider',
tabindex: _tabIndex,
tabindex: mergedTabIndex,
...ariaProps,
class: className,
onBlur: this.handleBlur,
@ -118,6 +139,13 @@ export default defineComponent({
ref: this.setHandleRef,
style: elStyle,
};
return <div {...handleProps} />;
return (
<div
{...handleProps}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
aria-valuetext={ariaValueText}
/>
);
},
});

View File

@ -1,10 +1,11 @@
import classNames from '../../_util/classNames';
import PropTypes, { withUndefined } from '../../_util/vue-types';
import BaseMixin from '../../_util/BaseMixin';
import { initDefaultProps, hasProp } from '../../_util/props-util';
import { hasProp } from '../../_util/props-util';
import Track from './common/Track';
import createSlider from './common/createSlider';
import * as utils from './utils';
import initDefaultProps from '../../_util/props-util/initDefaultProps';
const trimAlignValue = ({ value, handle, bounds, props }) => {
const { allowCross, pushable } = props;
@ -35,6 +36,10 @@ const rangeProps = {
min: PropTypes.number,
max: PropTypes.number,
autofocus: PropTypes.looseBool,
ariaLabelGroupForHandles: Array,
ariaLabelledByGroupForHandles: Array,
ariaValueTextFormatterGroupForHandles: Array,
draggableTrack: PropTypes.looseBool,
};
const Range = {
name: 'Range',
@ -46,6 +51,10 @@ const Range = {
allowCross: true,
pushable: false,
tabindex: [],
draggableTrack: false,
ariaLabelGroupForHandles: [],
ariaLabelledByGroupForHandles: [],
ariaValueTextFormatterGroupForHandles: [],
}),
data() {
const { count, min, max } = this;
@ -89,7 +98,7 @@ const Range = {
methods: {
setChangeValue(value) {
const { bounds } = this;
const nextBounds = value.map((v, i) =>
let nextBounds = value.map((v, i) =>
trimAlignValue({
value: v,
handle: i,
@ -97,8 +106,19 @@ const Range = {
props: this.$props,
}),
);
if (nextBounds.length === bounds.length && nextBounds.every((v, i) => v === bounds[i]))
return;
if (bounds.length === nextBounds.length) {
if (nextBounds.every((v, i) => v === bounds[i])) {
return null;
}
} else {
nextBounds = value.map((v, i) =>
trimAlignValue({
value: v,
handle: i,
props: this.$props,
}),
);
}
this.setState({ bounds: nextBounds });
@ -131,6 +151,19 @@ const Range = {
const changedValue = data.bounds;
this.__emit('change', changedValue);
},
positionGetValue(position) {
const bounds = this.getValue();
const value = this.calcValueByPos(position);
const closestBound = this.getClosestBound(value);
const index = this.getBoundNeedMoving(value, closestBound);
const prevValue = bounds[index];
if (value === prevValue) return null;
const nextBounds = [...bounds];
nextBounds[index] = value;
return nextBounds;
},
onStart(position) {
const { bounds } = this;
this.__emit('beforeChange', bounds);
@ -156,13 +189,35 @@ const Range = {
onEnd(force) {
const { sHandle } = this;
this.removeDocumentEvents();
if (!sHandle) {
this.dragTrack = false;
}
if (sHandle !== null || force) {
this.__emit('afterChange', this.bounds);
}
this.setState({ sHandle: null });
},
onMove(e, position) {
onMove(e, position, dragTrack, startBounds) {
utils.pauseEvent(e);
const { $data: state, $props: props } = this;
const maxValue = props.max || 100;
const minValue = props.min || 0;
if (dragTrack) {
let pos = props.vertical ? -position : position;
pos = props.reverse ? -pos : pos;
const max = maxValue - Math.max(...startBounds);
const min = minValue - Math.min(...startBounds);
const ratio = Math.min(Math.max(pos / (this.getSliderLength() / 100), min), max);
const nextBounds = startBounds.map(v =>
Math.floor(Math.max(Math.min(v + ratio, maxValue), minValue)),
);
if (state.bounds.map((c, i) => c === nextBounds[i]).some(c => !c)) {
this.onChange({
bounds: nextBounds,
});
}
return;
}
const { bounds, sHandle } = this;
const value = this.calcValueByPos(position);
const oldValue = bounds[sHandle];
@ -193,8 +248,8 @@ const Range = {
getClosestBound(value) {
const { bounds } = this;
let closestBound = 0;
for (let i = 1; i < bounds.length - 1; ++i) {
if (value > bounds[i]) {
for (let i = 1; i < bounds.length - 1; i += 1) {
if (value >= bounds[i]) {
closestBound = i;
}
}
@ -230,7 +285,7 @@ const Range = {
*/
getPoints() {
const { marks, step, min, max } = this;
const cache = this._getPointsCache;
const cache = this.internalPointsCache;
if (!cache || cache.marks !== marks || cache.step !== step) {
const pointsObject = { ...marks };
if (step !== null) {
@ -240,9 +295,9 @@ const Range = {
}
const points = Object.keys(pointsObject).map(parseFloat);
points.sort((a, b) => a - b);
this._getPointsCache = { marks, step, points };
this.internalPointsCache = { marks, step, points };
}
return this._getPointsCache.points;
return this.internalPointsCache.points;
},
moveTo(value, isFromKeyboardEvent) {
@ -277,8 +332,8 @@ const Range = {
pushSurroundingHandles(bounds, handle) {
const value = bounds[handle];
let { pushable: threshold } = this;
threshold = Number(threshold);
let { pushable } = this;
const threshold = Number(pushable);
let direction = 0;
if (bounds[handle + 1] - value < threshold) {
@ -324,7 +379,8 @@ const Range = {
}
const nextHandle = handle + direction;
const nextValue = points[nextPointIndex];
const { pushable: threshold } = this;
const { pushable } = this;
const threshold = Number(pushable);
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
@ -397,28 +453,33 @@ const Range = {
trackStyle,
handleStyle,
tabindex,
ariaLabelGroupForHandles,
ariaLabelledByGroupForHandles,
ariaValueTextFormatterGroupForHandles,
} = this;
const handleGenerator = handle || defaultHandle;
const offsets = bounds.map(v => this.calcOffset(v));
const handleClassName = `${prefixCls}-handle`;
const handles = bounds.map((v, i) => {
let _tabIndex = tabindex[i] || 0;
let mergedTabIndex = tabindex[i] || 0;
if (disabled || tabindex[i] === null) {
_tabIndex = null;
mergedTabIndex = null;
}
const dragging = sHandle === i;
return handleGenerator({
class: classNames({
[handleClassName]: true,
[`${handleClassName}-${i + 1}`]: true,
[`${handleClassName}-dragging`]: dragging,
}),
prefixCls,
vertical,
dragging,
offset: offsets[i],
value: v,
dragging: sHandle === i,
index: i,
tabindex: _tabIndex,
tabindex: mergedTabIndex,
min,
max,
reverse,
@ -427,6 +488,9 @@ const Range = {
ref: h => this.saveHandle(i, h),
onFocus: this.onFocus,
onBlur: this.onBlur,
ariaLabel: ariaLabelGroupForHandles[i],
ariaLabelledBy: ariaLabelledByGroupForHandles[i],
ariaValueTextFormatter: ariaValueTextFormatterGroupForHandles[i],
});
});

View File

@ -19,6 +19,10 @@ const Slider = defineComponent({
reverse: PropTypes.looseBool,
min: PropTypes.number,
max: PropTypes.number,
ariaLabelForHandle: String,
ariaLabelledByForHandle: String,
ariaValueTextFormatterForHandle: String,
startPoint: Number,
},
data() {
const defaultValue = this.defaultValue !== undefined ? this.defaultValue : this.min;
@ -111,10 +115,14 @@ const Slider = defineComponent({
}
},
getLowerBound() {
return this.min;
const minPoint = this.$props.startPoint || this.$props.min;
return this.$data.sValue > minPoint ? minPoint : this.$data.sValue;
},
getUpperBound() {
return this.sValue;
if (this.$data.sValue < this.$props.startPoint) {
return this.$props.startPoint;
}
return this.$data.sValue;
},
trimAlignValue(v, nextProps = {}) {
if (v === null) {
@ -124,18 +132,27 @@ const Slider = defineComponent({
const val = utils.ensureValueInRange(v, mergedProps);
return utils.ensureValuePrecision(val, mergedProps);
},
getTrack({ prefixCls, reverse, vertical, included, offset, minimumTrackStyle, _trackStyle }) {
getTrack({
prefixCls,
reverse,
vertical,
included,
minimumTrackStyle,
mergedTrackStyle,
length,
offset,
}) {
return (
<Track
class={`${prefixCls}-track`}
vertical={vertical}
included={included}
offset={0}
offset={offset}
reverse={reverse}
length={offset}
length={length}
style={{
...minimumTrackStyle,
..._trackStyle,
...mergedTrackStyle,
}}
/>
);
@ -150,8 +167,12 @@ const Slider = defineComponent({
trackStyle,
handleStyle,
tabindex,
ariaLabelForHandle,
ariaLabelledByForHandle,
ariaValueTextFormatterForHandle,
min,
max,
startPoint,
reverse,
handle,
defaultHandle,
@ -172,22 +193,26 @@ const Slider = defineComponent({
reverse,
index: 0,
tabindex,
ariaLabel: ariaLabelForHandle,
ariaLabelledBy: ariaLabelledByForHandle,
ariaValueTextFormatter: ariaValueTextFormatterForHandle,
style: handleStyle[0] || handleStyle,
ref: h => this.saveHandle(0, h),
onFocus: this.onFocus,
onBlur: this.onBlur,
});
const _trackStyle = trackStyle[0] || trackStyle;
const trackOffset = startPoint !== undefined ? this.calcOffset(startPoint) : 0;
const mergedTrackStyle = trackStyle[0] || trackStyle;
return {
tracks: this.getTrack({
prefixCls,
reverse,
vertical,
included,
offset,
offset: trackOffset,
minimumTrackStyle,
_trackStyle,
mergedTrackStyle,
length: offset - trackOffset,
}),
handles,
};

View File

@ -2,7 +2,7 @@ import supportsPassive from '../../../_util/supportsPassive';
import classNames from '../../../_util/classNames';
import { isValidElement } from '../../../_util/props-util';
const Marks = (_, { attrs }) => {
const Marks = (_, { attrs, slots }) => {
const {
class: className,
vertical,
@ -16,19 +16,21 @@ const Marks = (_, { attrs }) => {
onClickLabel,
} = attrs;
const marksKeys = Object.keys(marks);
const customMark = slots.mark;
const range = max - min;
const elements = marksKeys
.map(parseFloat)
.sort((a, b) => a - b)
.map(point => {
const markPoint = typeof marks[point] === 'function' ? marks[point](h) : marks[point];
const markPoint = typeof marks[point] === 'function' ? marks[point]() : marks[point];
const markPointIsObject = typeof markPoint === 'object' && !isValidElement(markPoint);
const markLabel = markPointIsObject ? markPoint.label : markPoint;
let markLabel = markPointIsObject ? markPoint.label : markPoint;
if (!markLabel && markLabel !== 0) {
return null;
}
if (customMark) {
markLabel = customMark({ point, label: markLabel });
}
const isActive =
(!included && point === upperBound) ||
(included && point <= upperBound && point >= lowerBound);
@ -43,11 +45,9 @@ const Marks = (_, { attrs }) => {
};
const leftStyle = {
transform: `translateX(-50%)`,
msTransform: `translateX(-50%)`,
[reverse ? 'right' : 'left']: reverse
? `${((point - min / 4) / range) * 100}%`
: `${((point - min) / range) * 100}%`,
transform: `translateX(${reverse ? `50%` : `-50%`})`,
msTransform: `translateX(${reverse ? `50%` : `-50%`})`,
[reverse ? 'right' : 'left']: `${((point - min) / range) * 100}%`,
};
const style = vertical ? bottomStyle : leftStyle;

View File

@ -1,7 +1,12 @@
/* eslint-disable */
const Track = (_, { attrs }) => {
const { included, vertical, offset, length, reverse, style, class: className } = attrs;
const { included, vertical, style, class: className } = attrs;
let { length, offset, reverse } = attrs;
if (length < 0) {
reverse = !reverse;
length = Math.abs(length);
offset = 100 - offset;
}
const positonStyle = vertical
? {
[reverse ? 'top' : 'bottom']: `${offset}%`,

View File

@ -35,15 +35,13 @@ export default function createSlider(Component) {
dotStyle: PropTypes.object,
activeDotStyle: PropTypes.object,
autofocus: PropTypes.looseBool,
draggableTrack: PropTypes.looseBool,
};
return defineComponent({
name: 'CreateSlider',
mixins: [BaseMixin, Component],
inheritAttrs: false,
// model: {
// prop: 'value',
// event: 'change',
// },
slots: ['mark'],
props: initDefaultProps(propTypes, {
prefixCls: 'rc-slider',
min: 0,
@ -66,10 +64,7 @@ export default function createSlider(Component) {
const isPointDiffEven = isFinite(max - min) ? (max - min) % step === 0 : true; // eslint-disable-line
warning(
step && Math.floor(step) === step ? isPointDiffEven : true,
'Slider',
'Slider[max] - Slider[min] (%s) should be a multiple of Slider[step] (%s)',
max - min,
step,
`Slider[max] - Slider[min] (${max - min}) should be a multiple of Slider[step] (${step})`,
);
this.handlesRefs = {};
return {};
@ -105,43 +100,62 @@ export default function createSlider(Component) {
};
return <Handle {...handleProps} />;
},
onDown(e, position) {
let p = position;
const { draggableTrack, vertical: isVertical } = this.$props;
const { bounds } = this.$data;
const value = draggableTrack && this.positionGetValue ? this.positionGetValue(p) || [] : [];
const inPoint = utils.isEventFromHandle(e, this.handlesRefs);
this.dragTrack =
draggableTrack &&
bounds.length >= 2 &&
!inPoint &&
!value
.map((n, i) => {
const v = !i ? n >= bounds[i] : true;
return i === value.length - 1 ? n <= bounds[i] : v;
})
.some(c => !c);
if (this.dragTrack) {
this.dragOffset = p;
this.startBounds = [...bounds];
} else {
if (!inPoint) {
this.dragOffset = 0;
} else {
const handlePosition = utils.getHandleCenterPosition(isVertical, e.target);
this.dragOffset = p - handlePosition;
p = handlePosition;
}
this.onStart(p);
}
},
onMouseDown(e) {
if (e.button !== 0) {
return;
}
const isVertical = this.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);
const isVertical = this.$props.vertical;
const position = utils.getMousePosition(isVertical, e);
this.onDown(e, position);
this.addDocumentMouseEvents();
utils.pauseEvent(e);
},
onTouchStart(e) {
if (utils.isNotTouchEvent(e)) return;
const isVertical = this.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);
const position = utils.getTouchPosition(isVertical, e);
this.onDown(e, position);
this.addDocumentTouchEvents();
utils.pauseEvent(e);
},
onFocus(e) {
const { vertical } = this;
if (utils.isEventFromHandle(e, this.handlesRefs)) {
if (utils.isEventFromHandle(e, this.handlesRefs) && !this.dragTrack) {
const handlePosition = utils.getHandleCenterPosition(vertical, e.target);
this.dragOffset = 0;
this.onStart(handlePosition);
@ -150,7 +164,9 @@ export default function createSlider(Component) {
}
},
onBlur(e) {
this.onEnd();
if (!this.dragTrack) {
this.onEnd();
}
this.__emit('blur', e);
},
onMouseUp() {
@ -164,7 +180,7 @@ export default function createSlider(Component) {
return;
}
const position = utils.getMousePosition(this.vertical, e);
this.onMove(e, position - this.dragOffset);
this.onMove(e, position - this.dragOffset, this.dragTrack, this.startBounds);
},
onTouchMove(e) {
if (utils.isNotTouchEvent(e) || !this.sliderRef) {
@ -173,7 +189,7 @@ export default function createSlider(Component) {
}
const position = utils.getTouchPosition(this.vertical, e);
this.onMove(e, position - this.dragOffset);
this.onMove(e, position - this.dragOffset, this.dragTrack, this.startBounds);
},
onKeyDown(e) {
if (this.sliderRef && utils.isEventFromHandle(e, this.handlesRefs)) {
@ -222,18 +238,19 @@ export default function createSlider(Component) {
/* eslint-enable no-unused-expressions */
},
focus() {
if (!this.disabled) {
this.handlesRefs[0].focus();
if (this.$props.disabled) {
return;
}
this.handlesRefs[0]?.focus();
},
blur() {
if (!this.disabled) {
Object.keys(this.handlesRefs).forEach(key => {
if (this.handlesRefs[key] && this.handlesRefs[key].blur) {
this.handlesRefs[key].blur();
}
});
if (this.$props.disabled) {
return;
}
Object.keys(this.handlesRefs).forEach(key => {
this.handlesRefs[key]?.blur?.();
});
},
calcValue(offset) {
const { vertical, min, max } = this;
@ -250,7 +267,7 @@ export default function createSlider(Component) {
calcOffset(value) {
const { min, max } = this;
const ratio = (value - min) / (max - min);
return ratio * 100;
return Math.max(0, ratio * 100);
},
saveSlider(slider) {
this.sliderRef = slider;
@ -339,7 +356,7 @@ export default function createSlider(Component) {
activeDotStyle={activeDotStyle}
/>
{handles}
<Marks {...markProps} />
<Marks {...markProps} v-slots={{ mark: this.$slots.mark }} />
{getSlot(this)}
</div>
);

View File

@ -16,6 +16,7 @@ export default function createSliderWithTooltip(Component) {
}),
handleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]),
tipProps: PropTypes.object.def(() => ({})),
getTooltipContainer: PropTypes.func.def(node => node.parentNode),
},
data() {
return {
@ -34,7 +35,7 @@ export default function createSliderWithTooltip(Component) {
});
},
handleWithTooltip({ value, dragging, index, disabled, ...restProps }) {
const { tipFormatter, tipProps, handleStyle } = this.$props;
const { tipFormatter, tipProps, handleStyle, getTooltipContainer } = this.$props;
const {
prefixCls = 'rc-slider-tooltip',
@ -53,6 +54,7 @@ export default function createSliderWithTooltip(Component) {
const tooltipProps = {
...restTooltipProps,
getTooltipContainer,
prefixCls,
overlay,
placement,

View File

@ -22,8 +22,8 @@ export function isNotTouchEvent(e) {
export function getClosestPoint(val, { marks, step, min, max }) {
const points = Object.keys(marks).map(parseFloat);
if (step !== null) {
const base = 10 ** getPrecision(step);
const maxSteps = Math.floor((max * base - min * base) / (step * base));
const baseNum = 10 ** getPrecision(step);
const maxSteps = Math.floor((max * baseNum - min * baseNum) / (step * baseNum));
const steps = Math.min((val - min) / step, maxSteps);
const closestStep = Math.round(steps) * step + min;
points.push(closestStep);
@ -96,7 +96,8 @@ export function calculateNextValue(func, value, props) {
if (props.step) {
return operations[func](value, props.step);
} else if (!!Object.keys(props.marks).length && !!props.marks[keyToGet]) {
}
if (!!Object.keys(props.marks).length && !!props.marks[keyToGet]) {
return props.marks[keyToGet];
}
return value;

View File

@ -55,7 +55,11 @@ export default defineComponent({
return triggerDOM.value.getPopupDomNode();
};
expose({ getPopupDomNode, triggerDOM });
expose({
getPopupDomNode,
triggerDOM,
forcePopupAlign: () => triggerDOM.value?.forcePopupAlign(),
});
const destroyTooltip = ref(false);
const autoDestroy = ref(false);