diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 3a09508a6b4..2031c312b64 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -13,6 +13,8 @@ - `n-menu` add a color distinction between selected and unselected arrow, closes [#1535](https://github.com/TuSimple/naive-ui/issues/1535). - `n-menu` 's `defaultExpandedKeys` use watchEffect initialize, closes [#1536](https://github.com/TuSimple/naive-ui/issues/1536). - `n-date-picker`'s `type` prop support `year` option. +- `n-slider` add `vertical` prop, closes [#1468](https://github.com/TuSimple/naive-ui/issues/1468). +- `n-slider` add `reverse` prop. ### i18n @@ -54,6 +56,10 @@ - `n-image`'s `toolbar` add close icon, closes [#1412](https://github.com/TuSimple/naive-ui/issues/1412). - `n-tree`'s `on-load` prop is triggered when the `expanded-keys` prop changes in `remote` mode, closes [#1339](https://github.com/TuSimple/naive-ui/issues/1339). +### Feats + +- `n-slider` add `vertical` prop, closes [#1468](https://github.com/TuSimple/naive-ui/issues/1468). + ## 2.20.0 (2021-10-28) ### Breaking Changes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 0191bca6d42..b85c4ac355e 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -13,6 +13,8 @@ - `n-menu` 添加箭头颜色区分选中未选中,关闭 [#1535](https://github.com/TuSimple/naive-ui/issues/1535) - `n-menu` 的 `defaultExpandedKeys` 使用 watchEffect 初始化,关闭 [#1536](https://github.com/TuSimple/naive-ui/issues/1536) - `n-date-picker` 属性 `type` 支持 `year` 选项 +- `n-slider` 新增 `vertical` 属性,关闭 [#1468](https://github.com/TuSimple/naive-ui/issues/1468) +- `n-slider` 新增 `reverse` 属性 ### i18n @@ -54,6 +56,10 @@ - `n-image` 的 `toolbar` 增加关闭图标,关闭 [#1412](https://github.com/TuSimple/naive-ui/issues/1412) - `n-tree` 的 `on-load` 属性在 `remote` 模式下 `expanded-keys` 属性改变时被触发,关闭 [#1339](https://github.com/TuSimple/naive-ui/issues/1339) +### Feats + +- `n-slider` 新增 `vertical` 属性,关闭 [#1468](https://github.com/TuSimple/naive-ui/issues/1468) + ## 2.20.0 (2021-10-28) ### Breaking Changes diff --git a/src/slider/demos/enUS/index.demo-entry.md b/src/slider/demos/enUS/index.demo-entry.md index f529b705f98..f13d50686f6 100644 --- a/src/slider/demos/enUS/index.demo-entry.md +++ b/src/slider/demos/enUS/index.demo-entry.md @@ -11,6 +11,8 @@ mark disabled disable-tooltip format +reverse +vertical ``` ## API @@ -28,5 +30,7 @@ format | range | `boolean` | `false` | Whether the slider uses range value. | | step | `number` | `1` | Step of the slider. | | tooltip | `boolean` | `true` | Whether to show tooltip. | +| reverse | `boolean` | `false` | Whether to reverse the track. | +| vertical | `boolean` | `false` | Whether to enable vertical mode. | | value | `number \| [number, number] \| null` | `undefined` | Value of the slider. | | on-update:value | `(value: number \| [number, number]) => void` | `undefined` | Callback on value update. | diff --git a/src/slider/demos/enUS/reverse.demo.md b/src/slider/demos/enUS/reverse.demo.md new file mode 100644 index 00000000000..ee77d46e91d --- /dev/null +++ b/src/slider/demos/enUS/reverse.demo.md @@ -0,0 +1,22 @@ +# Reverse + +Set `reverse` to invert the track. + +```html + + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + value: ref(0) + } + } +}) +``` diff --git a/src/slider/demos/enUS/vertical.demo.md b/src/slider/demos/enUS/vertical.demo.md new file mode 100644 index 00000000000..2039b210ff6 --- /dev/null +++ b/src/slider/demos/enUS/vertical.demo.md @@ -0,0 +1,29 @@ +# Vertical + +Set `vertical` to enable the vertical mode. Its height depends on the height of the container by default, and you can also customize the height. + +```html + + + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + value: ref([20, 70]), + marks: { + 0: '0°C', + 20: '20°C', + 37: '37°C', + 100: '100°C' + } + } + } +}) +``` \ No newline at end of file diff --git a/src/slider/demos/zhCN/index.demo-entry.md b/src/slider/demos/zhCN/index.demo-entry.md index 818999f56a7..0ab6cf450f0 100644 --- a/src/slider/demos/zhCN/index.demo-entry.md +++ b/src/slider/demos/zhCN/index.demo-entry.md @@ -11,6 +11,8 @@ mark disabled disable-tooltip format +reverse +vertical ``` ## API @@ -28,5 +30,7 @@ format | range | `boolean` | `false` | 是否选择范围值 | | step | `number` | `1` | 步长 | | tooltip | `boolean` | `true` | 是否展示 tooltip | +| reverse | `boolean` | `false` | 是否倒转轨道 | +| vertical | `boolean` | `false` | 是否启用垂直模式 | | value | `number \| [number, number] \| null` | `undefined` | 值 | | on-update:value | `(value: number \| [number, number]) => void` | `undefined` | 值更新的回调 | diff --git a/src/slider/demos/zhCN/reverse.demo.md b/src/slider/demos/zhCN/reverse.demo.md new file mode 100644 index 00000000000..326e0bd9e94 --- /dev/null +++ b/src/slider/demos/zhCN/reverse.demo.md @@ -0,0 +1,22 @@ +# 倒转 + +设定 `reverse` 可以将轨道倒转过来。 + +```html + + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + value: ref(0) + } + } +}) +``` diff --git a/src/slider/demos/zhCN/vertical.demo.md b/src/slider/demos/zhCN/vertical.demo.md new file mode 100644 index 00000000000..648d799e86c --- /dev/null +++ b/src/slider/demos/zhCN/vertical.demo.md @@ -0,0 +1,30 @@ +# 垂直 + +设定 `vertical` 来启用垂直模式,它的高度默认依赖于容器的高度,你也可以自定义高度。 + +```html + + + + + + +``` + +```js +import { defineComponent, ref } from 'vue' + +export default defineComponent({ + setup () { + return { + value: ref([20, 70]), + marks: { + 0: '0°C', + 20: '20°C', + 37: '37°C', + 100: '100°C' + } + } + } +}) +``` \ No newline at end of file diff --git a/src/slider/src/Slider.tsx b/src/slider/src/Slider.tsx index d1768d7046b..a7bf316e885 100644 --- a/src/slider/src/Slider.tsx +++ b/src/slider/src/Slider.tsx @@ -9,7 +9,9 @@ import { Transition, PropType, onBeforeUnmount, - CSSProperties + CSSProperties, + onBeforeUpdate, + onBeforeMount } from 'vue' import { VBinder, @@ -20,20 +22,32 @@ import { } from 'vueuc' import { useIsMounted, useMergedState } from 'vooks' import { on, off } from 'evtd' -import { useTheme, useFormItem, useConfig } from '../../_mixins' -import type { ThemeProps } from '../../_mixins' -import { warn, call, useAdjustedTo } from '../../_utils' -import type { MaybeArray, ExtractPublicPropTypes } from '../../_utils' +import { useTheme, useFormItem, useConfig, ThemeProps } from '../../_mixins' +import { + warn, + call, + useAdjustedTo, + MaybeArray, + ExtractPublicPropTypes +} from '../../_utils' import { sliderLight, SliderTheme } from '../styles' -import style from './styles/index.cssr' import { OnUpdateValueImpl } from './interface' import { isTouchEvent } from './utils' +import style from './styles/index.cssr' + +export interface ClosestMark { + value: number + distance: number + index: number +} + +const MouseButtonLeft = 0 const sliderProps = { ...(useTheme.props as ThemeProps), to: useAdjustedTo.propTo, defaultValue: { - type: [Number, Array] as PropType, + type: [Number, Array] as PropType, default: 0 }, marks: Object as PropType>, @@ -55,11 +69,8 @@ const sliderProps = { default: 1 }, range: Boolean, - value: [Number, Array] as PropType, - placement: { - type: String as PropType, - default: 'top' - }, + value: [Number, Array] as PropType, + placement: String as PropType, showTooltip: { type: Boolean as PropType, default: undefined @@ -68,16 +79,18 @@ const sliderProps = { type: Boolean, default: true }, + vertical: Boolean, + reverse: Boolean, 'onUpdate:value': [Function, Array] as PropType< - MaybeArray<(value: T) => void> + MaybeArray<(value: T) => void> >, onUpdateValue: [Function, Array] as PropType< - MaybeArray<(value: T) => void> + MaybeArray<(value: T) => void> >, // deprecated onChange: { type: [Function, Array] as PropType< - MaybeArray<(value: T) => void> + MaybeArray<(value: T) => void> >, validator: () => { if (__DEV__) { @@ -109,18 +122,19 @@ export default defineComponent({ ) const formItem = useFormItem(props) const { mergedDisabledRef } = formItem - const handleRef1 = ref(null) - const handleRef2 = ref(null) - const railRef = ref(null) - const followerRef1 = ref(null) - const followerRef2 = ref(null) + const handleRailRef = ref(null) const precisionRef = computed(() => { - const precisions = [props.min, props.max, props.step].map((item) => { - const fraction = String(item).split('.')[1] - return fraction ? fraction.length : 0 - }) - return Math.max(...precisions) + const { step } = props + if (!step) return 0 + const stepString = step.toString() + let precision = 0 + if (stepString.includes('.')) { + precision = stepString.length - stepString.indexOf('.') - 1 + } + return precision }) + const handleRefs = ref([]) + const followerRefs = ref([]) const uncontrolledValueRef = ref(props.defaultValue) const controlledValueRef = toRef(props, 'value') @@ -128,110 +142,119 @@ export default defineComponent({ controlledValueRef, uncontrolledValueRef ) + const mergedValuesRef = computed(() => { + return ((props.range + ? mergedValueRef.value + : [mergedValueRef.value]) as number[]).map(clampValue) + }) - const memoziedOtherValueRef = ref(0) - const changeSourceRef = ref<'click' | 'keyboard' | null>(null) + const mergedPlacementRef = computed(() => { + return props.placement === undefined + ? props.vertical + ? 'right' + : 'top' + : props.placement + }) - const handleActive1Ref = ref(false) - const handleActive2Ref = ref(false) - const handleClicked1Ref = ref(false) - const handleClicked2Ref = ref(false) + const propMarkValues = computed(() => { + const { marks } = props + return marks ? Object.keys(marks).map(parseFloat) : null + }) - const controlledShowTooltipRef = toRef(props, 'showTooltip') - const mergedShowTooltip1Ref = useMergedState( - controlledShowTooltipRef, - handleActive1Ref - ) - const mergedShowTooltip2Ref = useMergedState( - controlledShowTooltipRef, - handleActive2Ref - ) - const dotTransitionDisabledRef = ref(false) + const activeIndexRef = ref(-1) + const previousIndexRef = ref(-1) + const hoverIndexRef = ref(-1) + const draggingRef = ref(false) - const activeRef = computed(() => { - return handleActive1Ref.value || handleActive2Ref.value + const styleDirectionRef = computed(() => { + const { vertical, reverse } = props + const left = reverse ? 'right' : 'left' + const bottom = reverse ? 'top' : 'bottom' + return vertical ? bottom : left }) - const prevActiveRef = ref(activeRef.value) - const clickedRef = computed(() => { - return handleClicked1Ref.value || handleClicked2Ref.value + + const fillStyleRef = computed(() => { + const values = mergedValuesRef.value + const start = props.range ? Math.min.apply(null, values) : props.min + const end = props.range ? Math.max.apply(null, values) : values[0] + const { value: styleDirection } = styleDirectionRef + return props.vertical + ? { + [styleDirection]: `${valueToPercentage(start)}%`, + height: `${valueToPercentage(end - start)}%` + } + : { + [styleDirection]: `${valueToPercentage(start)}%`, + width: `${valueToPercentage(end - start)}%` + } }) + + const dotTransitionDisabledRef = ref(false) + const markInfosRef = computed(() => { const mergedMarks = [] - const { marks, max, min } = props + const { marks } = props if (marks) { - const { value: mergedValue } = mergedValueRef + const orderValues = mergedValuesRef.value.slice() + orderValues.sort((a, b) => a - b) + const { value: styleDirection } = styleDirectionRef + const { range } = props for (const key of Object.keys(marks)) { const num = Number(key) mergedMarks.push({ - active: Array.isArray(mergedValue) - ? mergedValue[0] <= num && mergedValue[1] >= num - : mergedValue !== null - ? mergedValue >= num - : false, + active: range + ? num >= orderValues[0] && + num <= orderValues[orderValues.length - 1] + : num >= orderValues[0], label: marks[key], style: { - left: `${((num - min) / (max - min)) * 100}%` + [styleDirection]: `${valueToPercentage(num)}%` } }) } } return mergedMarks }) - const fillStyleRef = computed(() => { - const { max, min, range } = props - if (range) { - return { - left: `${((handleValue1Ref.value - min) / (max - min)) * 100}%`, - width: `${ - ((handleValue2Ref.value - handleValue1Ref.value) / (max - min)) * - 100 - }%` - } - } else { - return { - left: 0, - width: `${((handleValue1Ref.value - min) / (max - min)) * 100}%` - } - } - }) - const handleValue1Ref = computed(() => { - const { value: mergedValue } = mergedValueRef - if (Array.isArray(mergedValue)) { - return sanitizeValue(mergedValue[0]) - } else { - return sanitizeValue(mergedValue) - } - }) - const handleValue2Ref = computed(() => { - const { value: mergedValue } = mergedValueRef - if (Array.isArray(mergedValue)) { - return sanitizeValue(mergedValue[1]) - } - return 0 - }) - const firstHandleStyleRef = computed(() => { - const { value: handleValue1 } = handleValue1Ref - const { value: handleClicked1 } = handleClicked1Ref - const { max, min } = props - const percentage = ((handleValue1 - min) / (max - min)) * 100 - return { - left: `${percentage}%`, - transform: `translateX(${-percentage}%)`, - zIndex: handleClicked1 ? 1 : 0 + + function isShowTooltip (index: number): boolean { + return ( + props.showTooltip || + hoverIndexRef.value === index || + activeIndexRef.value === index + ) + } + + function isSkipCSSDetection (index: number): boolean { + return !( + activeIndexRef.value === index && previousIndexRef.value === index + ) + } + + function focusActiveHandle (index = activeIndexRef.value): void { + if (~index) { + handleRefs.value?.[index].focus() } - }) - const secondHandleStyleRef = computed(() => { - const { value: handleValue2 } = handleValue2Ref - const { value: handleClicked2 } = handleClicked2Ref - const { max, min } = props - const percentage = ((handleValue2 - min) / (max - min)) * 100 + } + + function syncPosition (): void { + followerRefs.value.forEach((inst, index) => { + if (isShowTooltip(index)) inst.syncPosition() + }) + } + + function getHandleStyle ( + value: number, + index: number + ): Record { + const percentage = valueToPercentage(value) + const { value: styleDirection } = styleDirectionRef return { - left: `${percentage}%`, - transform: `translateX(${-percentage}%)`, - zIndex: handleClicked2 ? 1 : 0 + [styleDirection]: `${percentage}%`, + zIndex: index === activeIndexRef.value ? 1 : 0 } - }) - function doUpdateValue (value: number | [number, number]): void { + } + + function doUpdateValue (value: number | number[]): void { const { onChange, 'onUpdate:value': _onUpdateValue, @@ -245,392 +268,237 @@ export default defineComponent({ nTriggerFormInput() nTriggerFormChange() } - function doUpdateShow (show1?: boolean, show2?: boolean): void { - if (show1 !== undefined) { - handleActive1Ref.value = show1 - } - if (show2 !== undefined) { - handleActive2Ref.value = show2 - } - } - function syncPosition (): void { - followerRef1.value?.syncPosition() - followerRef2.value?.syncPosition() - } - function handleHandleFocus1 (): void { - if (clickedRef.value) return - doUpdateShow(true, false) - } - function handleHandleFocus2 (): void { - if (clickedRef.value) return - doUpdateShow(false, true) - } - function handleHandleBlur1 (): void { - if (clickedRef.value) return - doUpdateShow(false, false) - } - function handleHandleBlur2 (): void { - if (clickedRef.value) return - doUpdateShow(false, false) - } - function handleRailClick (e: MouseEvent): void { - if (mergedDisabledRef.value) return - const { value: railEl } = railRef - if (!railEl) return - const railRect = railEl.getBoundingClientRect() - const offsetRatio = (e.clientX - railRect.left) / railRect.width - const newValue = props.min + (props.max - props.min) * offsetRatio - if (!props.range) { - dispatchValueUpdate(newValue, { source: 'click' }) - handleRef1.value?.focus() - } else { - if (Array.isArray(mergedValueRef.value)) { - if ( - Math.abs(handleValue1Ref.value - newValue) < - Math.abs(handleValue2Ref.value - newValue) - ) { - dispatchValueUpdate([newValue, handleValue2Ref.value], { - source: 'click' - }) - handleRef1.value?.focus() - } else { - dispatchValueUpdate([handleValue1Ref.value, newValue], { - source: 'click' - }) - handleRef2.value?.focus() - } - } else { - dispatchValueUpdate([newValue, newValue], { source: 'click' }) - handleRef1.value?.focus() - } - } - } - function handleHandleMouseMove ( - e: MouseEvent | TouchEvent, - handleIndex: 0 | 1 - ): void { - if (!handleRef1.value || !railRef.value) return - const x = 'touches' in e ? e.touches[0].clientX : e.clientX - const { width: handleWidth } = handleRef1.value.getBoundingClientRect() - const { width: railWidth, left: railLeft } = - railRef.value.getBoundingClientRect() - const { min, max, range } = props - const offsetRatio = - (x - railLeft - handleWidth / 2) / (railWidth - handleWidth) - const newValue = min + (max - min) * offsetRatio + + function dispatchValueUpdate (value: number | number[]): void { + const { range } = props if (range) { - if (handleIndex === 0) { - dispatchValueUpdate([memoziedOtherValueRef.value, newValue]) - } else { - dispatchValueUpdate([newValue, memoziedOtherValueRef.value]) - } - } else { - dispatchValueUpdate(newValue) - } - } - function handleKeyDown (e: KeyboardEvent): void { - if (mergedDisabledRef.value) return - switch (e.code) { - case 'ArrowRight': - handleKeyDownRight() - break - case 'ArrowLeft': - handleKeyDownLeft() - break - } - } - function handleKeyDownRight (): void { - if (clickedRef.value) return - let firstHandleFocused = false - let handleValue = null - if (document.activeElement === handleRef1.value) { - firstHandleFocused = true - handleValue = handleValue1Ref.value - } else { - handleValue = handleValue2Ref.value - } - const { step, marks } = props - let nextValue = Math.floor(handleValue / step) * step + step - if (marks) { - for (const key of Object.keys(marks)) { - const numberKey = Number(key) - if (numberKey > handleValue && numberKey < nextValue) { - nextValue = numberKey + if (Array.isArray(value)) { + const { value: oldValues } = mergedValuesRef + if (value.join() !== oldValues.join()) { + doUpdateValue(value) } } - } - if (props.range) { - if (firstHandleFocused) { - dispatchValueUpdate([nextValue, handleValue2Ref.value], { - source: 'keyboard' - }) - } else { - dispatchValueUpdate([handleValue1Ref.value, nextValue], { - source: 'keyboard' - }) + } else if (!Array.isArray(value)) { + const oldValue = mergedValuesRef.value[0] + if (oldValue !== value) { + doUpdateValue(value) } - } else { - dispatchValueUpdate(nextValue, { source: 'keyboard' }) } } - function handleKeyDownLeft (): void { - if (clickedRef.value) return - let firstHandleFocused = false - let handleValue = null - if (document.activeElement === handleRef1.value) { - firstHandleFocused = true - handleValue = handleValue1Ref.value - } else if (document.activeElement === handleRef2.value) { - handleValue = handleValue2Ref.value - } else { - return - } - const { step, marks } = props - let nextValue = Math.ceil(handleValue / step) * step - step - if (marks) { - for (const key of Object.keys(marks)) { - const numberKey = Number(key) - if (numberKey < handleValue && numberKey > nextValue) { - nextValue = numberKey - } - } - } + + function doDispatchValue (value: number, index: number): void { if (props.range) { - if (firstHandleFocused) { - dispatchValueUpdate([nextValue, handleValue2Ref.value], { - source: 'keyboard' - }) - } else { - dispatchValueUpdate([handleValue1Ref.value, nextValue], { - source: 'keyboard' - }) - } + const values = mergedValuesRef.value.slice() + values.splice(index, 1, value) + dispatchValueUpdate(values) } else { - dispatchValueUpdate(nextValue, { source: 'keyboard' }) + dispatchValueUpdate(value) } } - function switchFocus (): void { - if (props.range) { - const firstHandle = handleRef1.value - const secondHandle = handleRef2.value - if (firstHandle && secondHandle) { + + function getClosestMark ( + currentValue: number, + markValues = propMarkValues.value, + buffer?: number + ): ClosestMark | null { + if (markValues) { + let closestMark = null + let index = -1 + while (++index < markValues.length) { + const diff = markValues[index] - currentValue + const distance = Math.abs(diff) if ( - handleActive1Ref.value && - document.activeElement === secondHandle - ) { - disableTransitionOneTick() - firstHandle.focus() - if (handleClicked2Ref.value) { - handleClicked2Ref.value = false - handleClicked1Ref.value = true - } - } else if ( - handleActive2Ref.value && - document.activeElement === firstHandle + (buffer === undefined || diff * buffer > 0) && + (closestMark === null || distance < closestMark.distance) ) { - disableTransitionOneTick() - secondHandle.focus() - if (handleClicked1Ref.value) { - handleClicked1Ref.value = false - handleClicked2Ref.value = true - } - } - } - } - } - function getClosestMarkValue (currentValue: number): number | null { - const { marks } = props - if (marks) { - const markValues = Object.keys(marks).map((key) => Number(key)) - let diff: number | null = null - let closestValue: number | null = null - for (const value of markValues) { - if (closestValue === null) { - closestValue = value - diff = Math.abs(value - currentValue) - } else { - const newDiff = Math.abs(value - currentValue) - if (newDiff < (diff as number)) { - closestValue = value - diff = newDiff + closestMark = { + value: markValues[index], + distance, + index } } } - return closestValue + return closestMark } return null } - function sanitizeValue (value: number): number { - let justifiedValue = value - const { min, max, marks, step } = props - justifiedValue = Math.max(min, justifiedValue) - justifiedValue = Math.min(max, justifiedValue) - justifiedValue = Math.round((justifiedValue - min) / step) * step + min - justifiedValue = parseFloat(justifiedValue.toFixed(precisionRef.value)) - if (marks) { - const closestMarkValue = getClosestMarkValue(value) - if ( - closestMarkValue !== null && - Math.abs(justifiedValue - value) > Math.abs(closestMarkValue - value) - ) { - justifiedValue = closestMarkValue - } - } - return justifiedValue + + function sanitizeValue ( + value: number, + currentValue: number, + stepBuffer?: number + ): number { + const stepping = stepBuffer !== undefined + if (!stepBuffer) { + stepBuffer = value - currentValue > 0 ? 1 : -1 + } + const markValues = propMarkValues.value || [] + const { min, max, step } = props + if (!step) { + const closestMark = getClosestMark( + value, + markValues.concat(currentValue), + stepBuffer + ) as ClosestMark + return closestMark.value + } + const roundValue = getRoundValue(value) + // ensure accurate step + const stepValue = new Array(Math.floor((max - min) / step) + 1) + .fill('') + .map((_, index) => step * index + min) + // If it is a stepping, priority will be given to the marks + // on the rail,otherwise take the nearest one + const closestMark = stepping + ? getClosestMark(currentValue, stepValue.concat(markValues), stepBuffer) + : getClosestMark(value, markValues.concat(roundValue)) + return closestMark ? clampValue(closestMark.value) : currentValue } - function handleFirstHandleMouseDown (e: MouseEvent | TouchEvent): void { - if (mergedDisabledRef.value) return - if (isTouchEvent(e)) e.preventDefault() - if (props.range) { - memoziedOtherValueRef.value = handleValue2Ref.value - } - doUpdateShow(true, false) - handleClicked1Ref.value = true - on('touchend', document, handleHandleMouseUp) - on('mouseup', document, handleHandleMouseUp) - on('touchmove', document, handleFirstHandleMouseMove) - on('mousemove', document, handleFirstHandleMouseMove) + + function clampValue (value: number): number { + return Math.min(props.max, Math.max(props.min, value)) } - function handleSecondHandleMouseDown (e: MouseEvent | TouchEvent): void { - if (mergedDisabledRef.value) return - if (isTouchEvent(e)) e.preventDefault() - if (props.range) { - memoziedOtherValueRef.value = handleValue1Ref.value - } - doUpdateShow(false, true) - handleClicked2Ref.value = true - on('touchend', document, handleHandleMouseUp) - on('mouseup', document, handleHandleMouseUp) - on('touchmove', document, handleSecondHandleMouseMove) - on('mousemove', document, handleSecondHandleMouseMove) + + function valueToPercentage (value: number): number { + const { max, min } = props + return ((value - min) / (max - min)) * 100 } - function handleHandleMouseUp (e: MouseEvent | TouchEvent): void { - if ( - isTouchEvent(e) || - (!handleRef1.value?.contains(e.target as Node) && - (props.range ? !handleRef2.value?.contains(e.target as Node) : true)) - ) { - doUpdateShow(false, false) - } - handleClicked2Ref.value = false - handleClicked1Ref.value = false - off('touchend', document, handleHandleMouseUp) - off('mouseup', document, handleHandleMouseUp) - off('touchmove', document, handleFirstHandleMouseMove) - off('touchmove', document, handleSecondHandleMouseMove) - off('mousemove', document, handleFirstHandleMouseMove) - off('mousemove', document, handleSecondHandleMouseMove) + + function percentageToValue (percentage: number): number { + const { max, min } = props + return min + (max - min) * percentage } - function dispatchValueUpdate ( - value: number | [number, number], - options: { source: 'keyboard' | 'click' | null } = { source: null } - ): void { - const { source } = options - const { range } = props - if (range) { - if (Array.isArray(value)) { - if (value[0] > value[1]) { - value = [sanitizeValue(value[1]), sanitizeValue(value[0])] - } else { - value = [sanitizeValue(value[0]), sanitizeValue(value[1])] - } - const { value: oldValue } = mergedValueRef - if ( - !Array.isArray(oldValue) || - oldValue[0] !== value[0] || - oldValue[1] !== value[1] - ) { - changeSourceRef.value = source - doUpdateValue(value) - } - } + + function getRoundValue (value: number): number { + const { step, min } = props + const newValue = Math.round((value - min) / step) * step + min + return Number(newValue.toFixed(precisionRef.value)) + } + + function getPointValue ( + event: MouseEvent | TouchEvent + ): number | undefined { + const railEl = handleRailRef.value + if (!railEl) return + + const touchEvent = isTouchEvent(event) ? event.touches[0] : event + const railRect = railEl.getBoundingClientRect() + + let percentage: number + if (props.vertical) { + percentage = (railRect.bottom - touchEvent.clientY) / railRect.height } else { - if (!Array.isArray(value)) { - const { max, min } = props - const { value: oldValue } = mergedValueRef - if (value > max) { - if (oldValue !== max) { - changeSourceRef.value = source - doUpdateValue(max) - } - } else if (value < min) { - if (oldValue !== min) { - changeSourceRef.value = source - doUpdateValue(min) - } - } else { - const newValue = sanitizeValue(value) - if (oldValue !== newValue) { - changeSourceRef.value = source - doUpdateValue(newValue) - } - } - } + percentage = (touchEvent.clientX - railRect.left) / railRect.width + } + + if (props.reverse) { + percentage = 1 - percentage } + + return percentageToValue(percentage) } - function handleFirstHandleMouseMove (e: MouseEvent | TouchEvent): void { - handleHandleMouseMove(e, 0) + + function handleRailKeyDown (e: KeyboardEvent): void { + if (mergedDisabledRef.value) return + const { vertical, reverse } = props + switch (e.code) { + case 'ArrowUp': + e.preventDefault() + handleStepValue(vertical && reverse ? -1 : 1) + break + case 'ArrowRight': + e.preventDefault() + handleStepValue(!vertical && reverse ? -1 : 1) + break + case 'ArrowDown': + e.preventDefault() + handleStepValue(vertical && reverse ? 1 : -1) + break + case 'ArrowLeft': + e.preventDefault() + handleStepValue(!vertical && reverse ? 1 : -1) + break + } } - function handleSecondHandleMouseMove (e: MouseEvent | TouchEvent): void { - handleHandleMouseMove(e, 1) + + function handleStepValue (ratio: number): void { + const activeIndex = activeIndexRef.value + if (activeIndex === -1) return + const { step } = props + const currentValue = mergedValuesRef.value[activeIndex] + const nextValue = currentValue + step * ratio + doDispatchValue( + // Avoid the number of value does not change when `step` is null + sanitizeValue(nextValue, currentValue, ratio > 0 ? 1 : -1), + activeIndex + ) } - function handleFirstHandleMouseEnter (): void { - if (!activeRef.value) { - doUpdateShow(true, undefined) - void nextTick(() => { - syncPosition() - }) + + function handleRailMouseDown (event: MouseEvent | TouchEvent): void { + if (mergedDisabledRef.value) return + if (!isTouchEvent(event) && event.button !== MouseButtonLeft) { + return } - } - function handleFirstHandleMouseLeave (): void { - if (changeSourceRef.value === 'keyboard') return - if (!activeRef.value) { - doUpdateShow(false, false) - } else if (!clickedRef.value) { - doUpdateShow(false, false) + const pointValue = getPointValue(event) + if (pointValue === undefined) return + + const values = mergedValuesRef.value.slice() + const activeIndex = props.range + ? getClosestMark(pointValue, values)?.index ?? -1 + : 0 + activeIndexRef.value = activeIndex + + if (activeIndex !== -1) { + // avoid triggering scrolling on touch + event.preventDefault() + draggingRef.value = true + focusActiveHandle(activeIndex) + doDispatchValue( + sanitizeValue(pointValue, mergedValuesRef.value[activeIndex]), + activeIndex + ) } } - function handleSecondHandleMouseEnter (): void { - if (!activeRef.value) { - doUpdateShow(undefined, true) - void nextTick(() => { - syncPosition() - }) - } + + function handleMouseMove (event: MouseEvent | TouchEvent): void { + const activeIndex = activeIndexRef.value + if (!draggingRef.value || activeIndex === -1) return + + const pointValue = getPointValue(event) as number + doDispatchValue( + sanitizeValue(pointValue, mergedValuesRef.value[activeIndex]), + activeIndex + ) } - function handleSecondHandleMouseLeave (): void { - if (changeSourceRef.value === 'keyboard') return - if (!activeRef.value) { - doUpdateShow(false, false) - } else if (!clickedRef.value) { - doUpdateShow(false, false) - } + + function handleMouseUp (): void { + draggingRef.value = false } - function disableTransitionOneTick (): void { - if (handleRef1.value) { - handleRef1.value.style.transition = 'none' - void nextTick(() => { - if (handleRef1.value) { - handleRef1.value.style.transition = '' - } - }) + + function handleHandleFocus (index: number): void { + activeIndexRef.value = index + } + + function handleHandleBlur (index: number): void { + if (activeIndexRef.value === index) { + activeIndexRef.value = -1 + draggingRef.value = false } - if (handleRef2.value) { - handleRef2.value.style.transition = 'none' - void nextTick(() => { - if (handleRef2.value) { - handleRef2.value.style.transition = '' - } - }) + } + + function handleHandleMouseEnter (index: number): void { + hoverIndexRef.value = index + } + + function handleHandleMouseLeave (index: number): void { + if (hoverIndexRef.value === index) { + hoverIndexRef.value = -1 } } - watch(activeRef, (value) => { - void nextTick(() => { - prevActiveRef.value = value - }) - }) - watch(mergedValueRef, (newValue, oldValue) => { - const { value: changeSource } = changeSourceRef + + watch(activeIndexRef, (_, previous) => (previousIndexRef.value = previous)) + + watch(mergedValueRef, () => { if (props.marks) { if (dotTransitionDisabledRef.value) return dotTransitionDisabledRef.value = true @@ -638,98 +506,52 @@ export default defineComponent({ dotTransitionDisabledRef.value = false }) } - if (props.range && Array.isArray(newValue) && Array.isArray(oldValue)) { - if (oldValue && oldValue[1] !== newValue[1]) { - void nextTick(() => { - if (!(changeSource === 'click')) { - doUpdateShow(false, true) - } - switchFocus() - }) - } else if (oldValue && oldValue[0] !== newValue[0]) { - void nextTick(() => { - if (!(changeSource === 'click')) { - doUpdateShow(true, false) - } - switchFocus() - }) - } else if (newValue[0] === newValue[1]) { - void nextTick(() => { - if (!(changeSource === 'click')) { - doUpdateShow(false, true) - } - switchFocus() - }) - } - } - void nextTick(() => { - // dom has changed but event is not fired, use marco task to make sure - // relevant event handler is called - setTimeout(() => { - changeSourceRef.value = null - }, 0) - if (props.range) { - if (Array.isArray(newValue) && Array.isArray(oldValue)) { - if (newValue[0] !== oldValue[0] || newValue[1] !== oldValue[1]) { - syncPosition() - } - } - } else { - syncPosition() - } - }) + void nextTick(syncPosition) + }) + + onBeforeUpdate(() => { + handleRefs.value.length = 0 + followerRefs.value.length = 0 + }) + onBeforeMount(() => { + on('touchend', document, handleMouseUp) + on('mouseup', document, handleMouseUp) + on('touchmove', document, handleMouseMove) + on('mousemove', document, handleMouseMove) }) onBeforeUnmount(() => { - off('touchmove', document, handleFirstHandleMouseMove) - off('touchmove', document, handleSecondHandleMouseMove) - off('mousemove', document, handleFirstHandleMouseMove) - off('mousemove', document, handleSecondHandleMouseMove) - off('touchend', document, handleHandleMouseUp) - off('mouseup', document, handleHandleMouseUp) + off('touchend', document, handleMouseUp) + off('mouseup', document, handleMouseUp) + off('touchmove', document, handleMouseMove) + off('mousemove', document, handleMouseMove) }) + return { mergedClsPrefix: mergedClsPrefixRef, namespace: namespaceRef, uncontrolledValue: uncontrolledValueRef, mergedValue: mergedValueRef, mergedDisabled: mergedDisabledRef, + mergedPlacement: mergedPlacementRef, isMounted: useIsMounted(), adjustedTo: useAdjustedTo(props), - handleValue1: handleValue1Ref, - handleValue2: handleValue2Ref, - mergedShowTooltip1: mergedShowTooltip1Ref, - mergedShowTooltip2: mergedShowTooltip2Ref, - handleActive1: handleActive1Ref, - handleActive2: handleActive2Ref, - handleClicked1: handleClicked1Ref, - handleClicked2: handleClicked2Ref, - memoziedOtherValue: memoziedOtherValueRef, - active: activeRef, - prevActive: prevActiveRef, - clicked: clickedRef, dotTransitionDisabled: dotTransitionDisabledRef, markInfos: markInfosRef, - // https://github.com/vuejs/vue-next/issues/2283 - handleRef1, - handleRef2, - railRef, - followerRef1, - followerRef2, - firstHandleStyle: firstHandleStyleRef, - secondHandleStyle: secondHandleStyleRef, + isShowTooltip, + isSkipCSSDetection, + handleRailRef, + handleRefs, + followerRefs, fillStyle: fillStyleRef, - handleKeyDown, - handleRailClick, - handleHandleFocus1, - handleHandleBlur1, - handleFirstHandleMouseDown, - handleFirstHandleMouseEnter, - handleFirstHandleMouseLeave, - handleHandleFocus2, - handleHandleBlur2, - handleSecondHandleMouseDown, - handleSecondHandleMouseEnter, - handleSecondHandleMouseLeave, + getHandleStyle, + activeIndex: activeIndexRef, + mergedValues: mergedValuesRef, + handleRailMouseDown, + handleHandleFocus, + handleHandleBlur, + handleHandleMouseEnter, + handleHandleMouseLeave, + handleRailKeyDown, indicatorCssVars: computed(() => { const { self: { @@ -766,6 +588,7 @@ export default defineComponent({ dotBorder, dotBoxShadow, railHeight, + railWidthVertical, handleSize, dotHeight, dotWidth, @@ -799,7 +622,8 @@ export default defineComponent({ '--opacity-disabled': opacityDisabled, '--rail-color': railColor, '--rail-color-hover': railColorHover, - '--rail-height': railHeight + '--rail-height': railHeight, + '--rail-width-vertical': railWidthVertical } }) } @@ -812,15 +636,18 @@ export default defineComponent({ `${mergedClsPrefix}-slider`, { [`${mergedClsPrefix}-slider--disabled`]: this.mergedDisabled, - [`${mergedClsPrefix}-slider--active`]: this.active, - [`${mergedClsPrefix}-slider--with-mark`]: this.marks + [`${mergedClsPrefix}-slider--active`]: this.activeIndex !== -1, + [`${mergedClsPrefix}-slider--with-mark`]: this.marks, + [`${mergedClsPrefix}-slider--vertical`]: this.vertical, + [`${mergedClsPrefix}-slider--reverse`]: this.reverse } ]} style={this.cssVars as CSSProperties} - onKeydown={this.handleKeyDown} - onClick={this.handleRailClick} + onKeydown={this.handleRailKeyDown} + onMousedown={this.handleRailMouseDown} + onTouchstart={this.handleRailMouseDown} > -
+
- {this.markInfos.map((mark) => ( + {this.markInfos.map(mark => (
) : null} -
- - {{ - default: () => [ - - {{ - default: () => ( -
- ) - }} - , - this.tooltip && ( - +
+ {this.mergedValues.map((value, index) => { + const showTooltip = this.isShowTooltip(index) + return ( + {{ - default: () => ( - + default: () => [ + {{ - default: () => - this.mergedShowTooltip1 ? ( -
- {typeof formatTooltip === 'function' - ? formatTooltip(this.handleValue1) - : this.handleValue1} -
- ) : null + default: () => ( +
{ + if (el) { + this.handleRefs[index] = el + } + }) as () => void + } + class={`${mergedClsPrefix}-slider-handle`} + tabindex={this.mergedDisabled ? -1 : 0} + style={this.getHandleStyle(value, index)} + onFocus={() => this.handleHandleFocus(index)} + onBlur={() => this.handleHandleBlur(index)} + onMouseenter={() => + this.handleHandleMouseEnter(index) + } + onMouseleave={() => + this.handleHandleMouseLeave(index) + } + /> + ) }} - - ) - }} - - ) - ] - }} - - {this.tooltip && this.range ? ( - - {{ - default: () => [ - - {{ - default: () => ( -
- ) - }} - , - - {{ - default: () => ( - - {{ - default: () => - this.mergedShowTooltip2 ? ( -
, + this.tooltip && ( + { + if (inst) { + this.followerRefs[index] = inst + } + }) as () => void + } + show={showTooltip} + to={this.adjustedTo} + teleportDisabled={ + this.adjustedTo === useAdjustedTo.tdkey + } + placement={this.mergedPlacement} + containerClass={this.namespace} + > + {{ + default: () => ( + - {typeof formatTooltip === 'function' - ? formatTooltip(this.handleValue2) - : this.handleValue2} -
- ) : null - }} -
- ) + {{ + default: () => + showTooltip ? ( +
+ {typeof formatTooltip === 'function' + ? formatTooltip(value) + : value} +
+ ) : null + }} + + ) + }} +
+ ) + ] }} - - ] - }} - - ) : null} - {this.marks ? ( -
- {this.markInfos.map((mark) => ( -
- {mark.label} -
- ))} + + ) + })}
- ) : null} + {this.marks ? ( +
+ {this.markInfos.map(mark => ( +
+ {mark.label} +
+ ))} +
+ ) : null} +
) } diff --git a/src/slider/src/interface.ts b/src/slider/src/interface.ts index 31b77a82da8..5a73b75c07a 100644 --- a/src/slider/src/interface.ts +++ b/src/slider/src/interface.ts @@ -1 +1 @@ -export type OnUpdateValueImpl = (value: number | [number, number]) => void +export type OnUpdateValueImpl = (value: number | number[]) => void diff --git a/src/slider/src/styles/index.cssr.ts b/src/slider/src/styles/index.cssr.ts index 19e894a8828..f9ac9587e7d 100644 --- a/src/slider/src/styles/index.cssr.ts +++ b/src/slider/src/styles/index.cssr.ts @@ -28,6 +28,7 @@ import fadeInScaleUpTransition from '../../../_styles/transitions/fade-in-scale- // --rail-color // --rail-color-hover // --rail-height +// --rail-width-vertical export default c([ cB('slider', ` display: block; @@ -36,17 +37,89 @@ export default c([ z-index: 0; width: 100%; cursor: pointer; + user-select: none; `, [ - cB('slider-marks', ` - position: absolute; - top: 18px; - left: calc(var(--handle-size) / 2); - right: calc(var(--handle-size) / 2); + cM('reverse', [ + cB('slider-handles', [ + cB('slider-handle', ` + transform: translate(50%, -50%); + `) + ]), + cB('slider-dots', [ + cB('slider-dot', ` + transform: translateX(50%, -50%); + `) + ]), + cM('vertical', [ + cB('slider-handles', [ + cB('slider-handle', ` + transform: translate(-50%, -50%); + `) + ]), + cB('slider-marks', [ + cB('slider-mark', ` + transform: translateY(calc(-50% + var(--dot-height) / 2)); + `) + ]), + cB('slider-dots', [ + cB('slider-dot', ` + transform: translateX(-50%) translateY(0); + `) + ]) + ]) + ]), + cM('vertical', ` + padding: 0 calc((var(--handle-size) - var(--rail-height)) / 2); + width: var(--rail-width-vertical); + height: 100%; `, [ - cB('slider-mark', { - position: 'absolute', - transform: 'translateX(-50%)' - }) + cB('slider-handles', ` + top: calc(var(--handle-size) / 2); + right: 0; + bottom: calc(var(--handle-size) / 2); + left: 0; + `, [ + cB('slider-handle', ` + top: unset; + left: 50%; + transform: translate(-50%, 50%); + `) + ]), + cB('slider-rail', ` + height: 100%; + `, [ + cE('fill', ` + top: unset; + right: 0; + bottom: unset; + left: 0; + `) + ]), + cM('with-mark', ` + width: var(--rail-width-vertical); + margin: 0 32px 0 8px; + `), + cB('slider-marks', ` + top: calc(var(--handle-size) / 2); + right: unset; + bottom: calc(var(--handle-size) / 2); + left: 22px; + `, [ + cB('slider-mark', ` + transform: translateY(50%); + white-space: nowrap; + `) + ]), + cB('slider-dots', ` + top: calc(var(--handle-size) / 2); + right: unset; + bottom: calc(var(--handle-size) / 2); + left: 50%; + `, [ + cB('slider-dot', ` + transform: translateX(-50%) translateY(50%); + `) + ]) ]), cM('disabled', ` cursor: not-allowed; @@ -84,6 +157,17 @@ export default c([ boxShadow: 'var(--handle-box-shadow-hover)' }) ]), + cB('slider-marks', ` + position: absolute; + top: 18px; + left: calc(var(--handle-size) / 2); + right: calc(var(--handle-size) / 2); + `, [ + cB('slider-mark', { + position: 'absolute', + transform: 'translateX(-50%)' + }) + ]), cB('slider-rail', ` width: 100%; position: relative; @@ -101,29 +185,37 @@ export default c([ background-color: var(--fill-color); `) ]), - cB('slider-handle', ` - outline: none; - height: var(--handle-size); - width: var(--handle-size); - border-radius: 50%; - transition: box-shadow .2s var(--bezier), background-color .3s var(--bezier); + cB('slider-handles', ` position: absolute; top: 0; - transform: translateX(-50%); - overflow: hidden; - cursor: pointer; - background-color: var(--handle-color); - box-shadow: var(--handle-box-shadow); + right: calc(var(--handle-size) / 2); + bottom: 0; + left: calc(var(--handle-size) / 2); `, [ - c('&:hover', { - boxShadow: 'var(--handle-box-shadow-hover)' - }), - c('&:hover:focus', { - boxShadow: 'var(--handle-box-shadow-active)' - }), - c('&:focus', { - boxShadow: 'var(--handle-box-shadow-focus)' - }) + cB('slider-handle', ` + outline: none; + height: var(--handle-size); + width: var(--handle-size); + border-radius: 50%; + transition: box-shadow .2s var(--bezier), background-color .3s var(--bezier); + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + overflow: hidden; + cursor: pointer; + background-color: var(--handle-color); + box-shadow: var(--handle-box-shadow); + `, [ + c('&:hover', { + boxShadow: 'var(--handle-box-shadow-hover)' + }), + c('&:hover:focus', { + boxShadow: 'var(--handle-box-shadow-active)' + }), + c('&:focus', { + boxShadow: 'var(--handle-box-shadow-focus)' + }) + ]) ]), cB('slider-dots', ` position: absolute; @@ -142,7 +234,7 @@ export default c([ box-shadow .3s var(--bezier), background-color .3s var(--bezier); position: absolute; - transform: translateX(-50%) translateY(-50%); + transform: translate(-50%, -50%); height: var(--dot-height); width: var(--dot-width); border-radius: var(--dot-border-radius); @@ -160,7 +252,6 @@ export default c([ cB('slider-handle-indicator', ` font-size: var(--font-size); padding: 6px 10px; - margin-bottom: 12px; border-radius: var(--indicator-border-radius); color: var(--indicator-text-color); background-color: var(--indicator-color); @@ -168,6 +259,28 @@ export default c([ `, [ fadeInScaleUpTransition() ]), + cB('slider-handle-indicator', ` + font-size: var(--font-size); + padding: 6px 10px; + border-radius: var(--indicator-border-radius); + color: var(--indicator-text-color); + background-color: var(--indicator-color); + box-shadow: var(--indicator-box-shadow); + `, [ + cM('top', ` + margin-bottom: 12px; + `), + cM('right', ` + margin-left: 12px; + `), + cM('bottom', ` + margin-top: 12px; + `), + cM('left', ` + margin-right: 12px; + `), + fadeInScaleUpTransition() + ]), insideModal( cB('slider', [ cB('slider-dot', { diff --git a/src/slider/src/utils.ts b/src/slider/src/utils.ts index 72fc2186471..bc8cde5a930 100644 --- a/src/slider/src/utils.ts +++ b/src/slider/src/utils.ts @@ -1,3 +1,3 @@ -export function isTouchEvent (e: MouseEvent | TouchEvent): boolean { +export function isTouchEvent (e: MouseEvent | TouchEvent): e is TouchEvent { return window.TouchEvent && e instanceof window.TouchEvent } diff --git a/src/slider/styles/_common.ts b/src/slider/styles/_common.ts index 0e03892d62f..5c24f16be07 100644 --- a/src/slider/styles/_common.ts +++ b/src/slider/styles/_common.ts @@ -1,5 +1,6 @@ export default { railHeight: '4px', + railWidthVertical: '4px', handleSize: '18px', dotHeight: '8px', dotWidth: '8px', diff --git a/src/slider/tests/Slider.spec.ts b/src/slider/tests/Slider.spec.ts index aeea6e41f17..7da354bb47c 100644 --- a/src/slider/tests/Slider.spec.ts +++ b/src/slider/tests/Slider.spec.ts @@ -44,7 +44,7 @@ describe('n-slider', () => { it('accept correct callback types', () => { function onUpdateValue1 (value: number): void {} - function onUpdateValue2 (value: [number, number]): void {} + function onUpdateValue2 (value: number[]): void {} mount(NSlider, { props: { onUpdateValue: onUpdateValue1 @@ -100,4 +100,39 @@ describe('n-slider', () => { expect(element.style.left).toEqual('24%') expect(element.style.width).toEqual('25%') }) + + it('should work with `vertical` prop', async () => { + const wrapper = mount(NSlider, { + props: { + defaultValue: 77, + vertical: true + } + }) + + const sliderRailFill = wrapper.find('.n-slider-rail__fill') + const firstHandle = wrapper.find('.n-slider-handle') + expect((sliderRailFill.element as HTMLElement).style.height).toEqual('77%') + expect((firstHandle.element as HTMLElement).style.bottom).toEqual('77%') + }) + + it('should work with `range` & `vertical` prop', async () => { + const wrapper = mount(NSlider, { + props: { + range: true, + defaultValue: [24, 49], + vertical: true + } + }) + + const sliderRailFill = wrapper.find('.n-slider-rail__fill') + const element = sliderRailFill.element as HTMLElement + expect(element.style.bottom).toEqual('24%') + expect(element.style.height).toEqual('25%') + expect(wrapper.findAll('.n-slider-handle')[0].attributes('style')).toContain( + 'bottom: 24%' + ) + expect(wrapper.findAll('.n-slider-handle')[1].attributes('style')).toContain( + 'bottom: 49%' + ) + }) })