feat(slider): [slider] marks supported, input range supported (#1429)

* feat(slider): marks supported, input range supported

* feat(slider): slider smb theme

* feat(slider): slider demo

* feat(slider): slider test
This commit is contained in:
yoyo 2024-02-27 10:09:00 +08:00 committed by GitHub
parent 9146bdb554
commit 8900246bea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 375 additions and 48 deletions

View File

@ -111,8 +111,8 @@ export default {
type: 'boolean', type: 'boolean',
defaultValue: 'false', defaultValue: 'false',
desc: { desc: {
'zh-CN': '是否显示输入框,仅在非范围选择时有效', 'zh-CN': '是否显示输入框',
'en-US': 'Indicates whether to display the text box. This parameter is valid only for non-range selection' 'en-US': 'Indicates whether to display the text box.'
}, },
mode: ['pc', 'mobile', 'mobile-first'], mode: ['pc', 'mobile', 'mobile-first'],
pcDemo: 'show-input', pcDemo: 'show-input',
@ -203,6 +203,17 @@ export default {
mode: ['mobile'], mode: ['mobile'],
mobileDemo: '' mobileDemo: ''
}, },
{
name: 'marks',
type: `{ [key:number]: string }`,
defaultValue: '',
desc: {
'zh-CN': '<p>设置滑杆的刻度值</p>',
'en-US': 'Set the scale value of the slide bar'
},
mode: ['pc'],
pcDemo: 'marks'
},
{ {
name: 'vertical', name: 'vertical',
type: 'boolean', type: 'boolean',

View File

@ -0,0 +1,20 @@
<template>
<div>
<div>
<tiny-slider v-model="value" :marks="marks"></tiny-slider>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Slider as TinySlider } from '@opentiny/vue'
const marks = ref({
10: '10%',
40: '40%',
50: '50%'
})
const value = ref(20)
</script>

View File

@ -0,0 +1,12 @@
import { test, expect } from '@playwright/test'
test('刻度标记', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('slider#marks')
const points = page.locator('.tiny-slider-container .tiny-slider__mark-point')
await expect(points.nth(0)).toHaveAttribute('style', /left: 10%/)
await expect(points.nth(1)).toHaveAttribute('style', /left: 40%/)
await expect(points.nth(2)).toHaveAttribute('style', /left: 50%/)
})

View File

@ -0,0 +1,27 @@
<template>
<div>
<div>
<tiny-slider v-model="value" :marks="marks"></tiny-slider>
</div>
</div>
</template>
<script>
import { Slider } from '@opentiny/vue'
export default {
components: {
TinySlider: Slider
},
data() {
return {
value: 40,
marks: {
10: '10%',
40: '40%',
50: '50%'
}
}
}
}
</script>

View File

@ -1,5 +1,6 @@
<template> <template>
<tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider> <tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider>
<tiny-slider v-model="value2" :show-input="true"></tiny-slider>
</template> </template>
<script setup> <script setup>
@ -7,4 +8,5 @@ import { ref } from 'vue'
import { Slider as TinySlider } from '@opentiny/vue' import { Slider as TinySlider } from '@opentiny/vue'
const value = ref(40) const value = ref(40)
const value2 = ref([40, 60])
</script> </script>

View File

@ -1,5 +1,6 @@
<template> <template>
<tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider> <tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider>
<tiny-slider v-model="value2" :show-input="true"></tiny-slider>
</template> </template>
<script> <script>
@ -11,7 +12,8 @@ export default {
}, },
data() { data() {
return { return {
value: 40 value: 40,
value2: [40, 60]
} }
} }
} }

View File

@ -6,11 +6,21 @@ test('输入框模式', async ({ page }) => {
const sliderInput = page.locator('.tiny-slider-container .tiny-slider__input input') const sliderInput = page.locator('.tiny-slider-container .tiny-slider__input input')
const sliderBlock = page.locator('.tiny-slider-container .tiny-slider .tiny-slider__handle') const sliderBlock = page.locator('.tiny-slider-container .tiny-slider .tiny-slider__handle')
const sliderTip = page.locator('.tiny-slider-container .tiny-slider .tiny-slider__tips')
await sliderInput.click() // 单输入框
await sliderInput.fill('60') const singleInput = sliderInput.nth(0)
const singleBlock = sliderBlock.nth(0)
await sliderBlock.hover() await singleInput.nth(0).click()
await singleInput.nth(0).fill('60')
await singleBlock.hover()
await expect(sliderTip.nth(0)).toHaveText('60')
await expect(page.locator('.tiny-slider-container .tiny-slider .tiny-slider__tips')).toHaveText('60') // 双输入框
await sliderInput.nth(1).click()
await sliderInput.nth(1).fill('70')
await expect(sliderInput.nth(1)).toHaveValue('70')
await sliderInput.nth(1).blur()
await expect(sliderInput.nth(1)).toHaveValue('60')
}) })

View File

@ -28,6 +28,18 @@ export default {
}, },
codeFiles: ['vertical-mode.vue'] codeFiles: ['vertical-mode.vue']
}, },
{
demoId: 'marks',
name: {
'zh-CN': '标记',
'en-US': 'marks'
},
desc: {
'zh-CN': '使用marks属性给滑杆的值添加标记。',
'en-US': 'Mark the value of the slider.'
},
codeFiles: ['marks.vue']
},
{ {
demoId: 'max-min', demoId: 'max-min',
name: { name: {

View File

@ -95,6 +95,7 @@ export const bindMouseDown =
const handleEl = event.target const handleEl = event.target
let isClickBar: boolean | undefined = false let isClickBar: boolean | undefined = false
let isClickBtn: boolean | undefined = false let isClickBtn: boolean | undefined = false
let isClickLabel: boolean | undefined = false
if (mode === 'mobile-first') { if (mode === 'mobile-first') {
const role = Array.from(handleEl.attributes).find((attr) => attr.name === 'role') const role = Array.from(handleEl.attributes).find((attr) => attr.name === 'role')
@ -108,8 +109,9 @@ export const bindMouseDown =
hasClass(handleEl, constants.buttonCls(mode)) || hasClass(handleEl, constants.buttonCls(mode)) ||
hasClass(handleEl, constants.leftSvgCls(mode)) || hasClass(handleEl, constants.leftSvgCls(mode)) ||
hasClass(handleEl, constants.rightSvgCls(mode)) hasClass(handleEl, constants.rightSvgCls(mode))
isClickLabel = hasClass(handleEl, constants.PC_LABEL_CLS)
} }
if (state.disabled || (!isClickBtn && !isClickBar)) { if (state.disabled || (!isClickBtn && !isClickBar && !isClickLabel)) {
state.activeIndex = -1 state.activeIndex = -1
return return
} }
@ -122,7 +124,7 @@ export const bindMouseDown =
state.isDrag = isClickBtn state.isDrag = isClickBtn
isClickBtn && (state.activeIndex = api.getActiveButtonIndex(event)) isClickBtn && (state.activeIndex = api.getActiveButtonIndex(event))
if (isClickBar) { if (isClickBar || isClickLabel) {
const currentValue = api.calculateValue(event) const currentValue = api.calculateValue(event)
if (state.isDouble) { if (state.isDouble) {
if (Math.abs(currentValue - state.leftBtnValue) > Math.abs(state.rightBtnValue - currentValue)) { if (Math.abs(currentValue - state.leftBtnValue) > Math.abs(state.rightBtnValue - currentValue)) {
@ -426,7 +428,7 @@ export const changeActiveValue = (state) => (isLeft) => {
export const formatTipValue = (props) => (value) => export const formatTipValue = (props) => (value) =>
props.formatTooltip instanceof Function ? props.formatTooltip(value) : value props.formatTooltip instanceof Function ? props.formatTooltip(value) : value
export const getActiveButtonValue = (state) => () => export const getActiveButtonValue = (state: ISliderRenderlessParams['state']) => (): number | number[] =>
state.isDouble ? [state.leftBtnValue, state.rightBtnValue] : state.leftBtnValue state.isDouble ? [state.leftBtnValue, state.rightBtnValue] : state.leftBtnValue
export const autoSlider = (api) => (value) => { export const autoSlider = (api) => (value) => {
@ -489,11 +491,6 @@ export const watchActiveValue =
} else { } else {
state.activeValue = nNewValue || 0 state.activeValue = nNewValue || 0
} }
// 正在输入时,不应该改变输入的内容
if (!state.isSlotTyping) {
state.slotValue = state.activeValue
}
} }
export const watchModelValue = export const watchModelValue =
@ -511,6 +508,11 @@ export const watchModelValue =
api.setActiveButtonValue(value) api.setActiveButtonValue(value)
} }
} }
// 正在输入时,不应该改变输入的内容
if (!state.isSlotTyping) {
api.updateSlotValue()
}
} }
export const getPoints = export const getPoints =
@ -549,6 +551,41 @@ export const getLabels =
} }
} }
interface IMarkListItem {
value: number
label: string
percent: number
positionStyle: { [key: string]: string }
}
export const getMarkList =
({ props }: Pick<ISliderRenderlessParams, 'props'>) =>
(): IMarkListItem[] => {
const markList: IMarkListItem[] = []
if (!props.marks) {
return markList
}
for (const [key, label] of Object.entries(props.marks)) {
const markValue = Number(key)
if (markValue >= props.min && markValue <= props.max) {
const percent = (markValue - props.min) / (props.max - props.min)
markList.push({
value: markValue,
label,
percent,
positionStyle: {
[props.vertical ? 'bottom' : 'left']: percent * 100 + '%'
}
})
}
}
return markList
}
export const inputValueChange = export const inputValueChange =
({ props, state, api }: Pick<ISliderRenderlessParams, 'api' | 'props' | 'state'>) => ({ props, state, api }: Pick<ISliderRenderlessParams, 'api' | 'props' | 'state'>) =>
($event, pos) => { ($event, pos) => {
@ -569,11 +606,30 @@ export const handleSlotInputFocus = (state: ISliderRenderlessParams['state']) =>
state.isSlotTyping = true state.isSlotTyping = true
} }
export const handleSlotInputBlur = (state: ISliderRenderlessParams['state']) => () => { export const handleSlotInputBlur =
({ state, api }: Pick<ISliderRenderlessParams, 'api' | 'state'>) =>
() => {
state.isSlotTyping = false state.isSlotTyping = false
state.slotValue = state.activeValue api.updateSlotValue()
} }
export const handleSlotInput = (state: ISliderRenderlessParams['state']) => (event: Event) => { export const updateSlotValue =
state.activeValue = Number((event.target as HTMLInputElement).value) ({ state }: Pick<ISliderRenderlessParams, 'state'>) =>
} () => {
if (!state.isDouble) {
state.slotValue = state.activeValue
} else {
state.slotValue =
state.activeIndex === 0 ? [state.activeValue, state.rightBtnValue] : [state.leftBtnValue, state.activeValue]
}
}
export const handleSlotInput =
({ state, api }: Pick<ISliderRenderlessParams, 'api' | 'state'>) =>
(event: Event, isLeftInput: boolean = true): void => {
const inputValue = (event.target as HTMLInputElement).value
api.changeActiveValue(state.isDouble ? isLeftInput : true)
state.activeValue = Number(inputValue)
api.updateSlotValue()
}

View File

@ -41,7 +41,9 @@ import {
inputValueChange, inputValueChange,
handleSlotInputFocus, handleSlotInputFocus,
handleSlotInputBlur, handleSlotInputBlur,
handleSlotInput handleSlotInput,
getMarkList,
updateSlotValue
} from './index' } from './index'
import type { import type {
@ -91,6 +93,7 @@ const initState = ({ reactive, computed, props, api, parent, inject }) => {
moveStyle: [], moveStyle: [],
points: [], points: [],
labels: [], labels: [],
markList: computed(() => api.getMarkList()),
inputValue: [0, 0], inputValue: [0, 0],
isDrag: false, isDrag: false,
sliderSize: 0, sliderSize: 0,
@ -160,8 +163,10 @@ export const renderless = (
getLabels: getLabels({ props, state }), getLabels: getLabels({ props, state }),
inputValueChange: inputValueChange({ props, api, state }), inputValueChange: inputValueChange({ props, api, state }),
handleSlotInputFocus: handleSlotInputFocus(state), handleSlotInputFocus: handleSlotInputFocus(state),
handleSlotInputBlur: handleSlotInputBlur(state), handleSlotInputBlur: handleSlotInputBlur({ state, api }),
handleSlotInput: handleSlotInput(state) handleSlotInput: handleSlotInput({ state, api }),
getMarkList: getMarkList({ props }),
updateSlotValue: updateSlotValue({ state })
}) })
watch( watch(

View File

@ -1,6 +1,14 @@
import type { ExtractPropTypes, ComputedRef } from 'vue' import type { ExtractPropTypes, ComputedRef, CSSProperties } from 'vue'
import type { sliderProps, $constants } from '@/slider/src' import type { sliderProps, $constants } from '@/slider/src'
import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type' import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
import type {
getMarkList,
getActiveButtonValue,
handleSlotInputFocus,
handleSlotInputBlur,
handleSlotInput,
updateSlotValue
} from '../src/slider'
export type ISliderProps = ExtractPropTypes<typeof sliderProps> export type ISliderProps = ExtractPropTypes<typeof sliderProps>
@ -8,7 +16,7 @@ export type ISliderConstants = typeof $constants
export interface ISliderState { export interface ISliderState {
tipStyle: object tipStyle: object
barStyle: object barStyle: CSSProperties
moveStyle: object moveStyle: object
points: object[] points: object[]
labels: object[] labels: object[]
@ -33,11 +41,12 @@ export interface ISliderState {
rangeDiff: ComputedRef<number> rangeDiff: ComputedRef<number>
tipValue: ComputedRef<string> tipValue: ComputedRef<string>
formDisabled: ComputedRef<boolean> formDisabled: ComputedRef<boolean>
disabled: ComputedRef<boolean> disabled: boolean
/** 使用这个值作为插槽中输入的值而不是直接用activeValue来实现在输入时不会被max min属性计算而改变 */ /** 使用这个值作为插槽中输入的值而不是直接用activeValue来实现在输入时不会被max min属性计算而改变 */
slotValue: number slotValue: number | number[] | string
/** 是否正在输入 */ /** 是否正在输入 */
isSlotTyping: boolean isSlotTyping: boolean
markList: ReturnType<ISliderApi['getMarkList']>
} }
export interface ISliderApi { export interface ISliderApi {
@ -49,7 +58,7 @@ export interface ISliderApi {
bindResize: () => void bindResize: () => void
setButtonStyle: () => void setButtonStyle: () => void
calculateValue: (event: Event) => number calculateValue: (event: Event) => number
getActiveButtonValue: () => number getActiveButtonValue: ReturnType<typeof getActiveButtonValue>
getActiveButtonIndex: (event: Event) => number getActiveButtonIndex: (event: Event) => number
setTipStyle: () => void setTipStyle: () => void
customAfterAppearHook: () => void customAfterAppearHook: () => void
@ -63,15 +72,17 @@ export interface ISliderApi {
bindMouseMove: () => void bindMouseMove: () => void
bindMouseDown: () => void bindMouseDown: () => void
setActiveButtonValue: (currentValue: number) => void setActiveButtonValue: (currentValue: number) => void
initSlider: (inputValue: number | [number, number]) => void initSlider: (inputValue: number | number[]) => void
watchModelValue: () => void watchModelValue: () => void
watchActiveValue: () => void watchActiveValue: () => void
getPoints: () => void getPoints: () => void
getLabels: () => void getLabels: () => void
inputValueChange: () => void inputValueChange: () => void
handleSlotInputFocus: () => void handleSlotInputFocus: ReturnType<typeof handleSlotInputFocus>
handleSlotInputBlur: () => void handleSlotInputBlur: ReturnType<typeof handleSlotInputBlur>
handleSlotInput: (event: Event) => void handleSlotInput: ReturnType<typeof handleSlotInput>
getMarkList: ReturnType<typeof getMarkList>
updateSlotValue: ReturnType<typeof updateSlotValue>
} }
export type ISliderRenderlessParams = ISharedRenderlessFunctionParams<ISliderConstants> & { export type ISliderRenderlessParams = ISharedRenderlessFunctionParams<ISliderConstants> & {

View File

@ -57,10 +57,20 @@
&.disabled { &.disabled {
cursor: default; cursor: default;
.@{slider-prefix-cls}__range {
background-color: var(--ti-slider-range-disabled-bg-color);
} }
&.disabled .@{slider-prefix-cls}__handle { .@{slider-prefix-cls}__handle {
cursor: not-allowed; cursor: not-allowed;
border-color: var(--ti-slider-handle-disabled-border-color);
&:hover svg, svg {
fill: var(--ti-slider-handle-icon-disabled-fill-color);
}
}
} }
&__vertical { &__vertical {
@ -94,6 +104,19 @@
margin: var(--ti-slider-margin-vertical) var(--ti-slider-margin-right) var(--ti-slider-margin-vertical) margin: var(--ti-slider-margin-vertical) var(--ti-slider-margin-right) var(--ti-slider-margin-vertical)
var(--ti-slider-margin-left); var(--ti-slider-margin-left);
} }
.@{slider-prefix-cls}__mark-point {
transform: translateY(50%);
height: var(--ti-slider-mark-point-width);
width: var(--ti-slider-height);
}
.@{slider-prefix-cls}__mark-label {
transform: translateY(50%);
margin-left: var(--ti-slider-mark-label-margin-top);
margin-top: 0;
}
} }
&__range { &__range {
@ -169,6 +192,7 @@
border: 1px solid var(--ti-slider-tips-border-color); border: 1px solid var(--ti-slider-tips-border-color);
border-radius: var(--ti-common-border-radius-1); border-radius: var(--ti-common-border-radius-1);
color: var(--ti-slider-tips-text-color); color: var(--ti-slider-tips-text-color);
box-shadow: var(--ti-slider-tips-box-shadow);
&:before { &:before {
margin-left: -6px; margin-left: -6px;
@ -189,13 +213,34 @@
} }
} }
&__mark-point {
position: absolute;
pointer-events: none;
transform: translateX(-50%);
width: var(--ti-slider-mark-point-width);
height: var(--ti-slider-height);
background-color: var(--ti-slider-mark-point-bg-color);
}
&__mark-label {
position: absolute;
transform: translateX(-50%);
margin-top: var(--ti-slider-mark-label-margin-top);
color: var(--ti-slider-mark-label-text-color);
font-size: var(--ti-slider-mark-label-font-size);
}
&__input { &__input {
display: inline-block; display: inline-block;
line-height: var(--ti-slider-input-height); line-height: var(--ti-slider-input-height);
vertical-align: top; vertical-align: top;
margin-left: 12px; margin-left:var(--ti-slider-input-margin-left);
font-size: var(--ti-common-font-size-base); font-size: var(--ti-common-font-size-base);
&__split {
padding: 0 4px;
}
input { input {
width: var(--ti-slider-input-width); width: var(--ti-slider-input-width);
height: var(--ti-slider-input-height); height: var(--ti-slider-input-height);
@ -209,11 +254,11 @@
outline: 0; outline: 0;
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: var(--ti-slider-input-text-align);
} }
span { &__unit {
padding-left: 8px; margin-left: var(--ti-slider-input-unit-margin-left);
} }
} }
} }

View File

@ -1 +1,33 @@
export const tinySliderSmbTheme = {} export const tinySliderSmbTheme = {
'ti-slider-border-radius': 'var(--ti-common-border-radius-1)',
'ti-slider-bg-color': 'var(--ti-common-color-bg-normal)',
'ti-slider-height': 'var(--ti-common-space-2x)',
'ti-slider-margin-left': 'calc(var(--ti-common-space-base) * -1)',
'ti-slider-range-bg-color': 'var(--ti-common-color-data-1)',
'ti-slider-range-hover-bg-color': 'var(--ti-common-color-data-1)',
'ti-slider-range-height': 'var(--ti-common-space-2x)',
'ti-slider-range-margin-top': '0',
'ti-slider-range-border-radius': 'var(--ti-common-border-radius-1)',
'ti-slider-range-disabled-bg-color': 'var(--ti-common-color-bg-secondary)',
'ti-slider-handle-width': 'var(--ti-common-space-4x)',
'ti-slider-handle-height': 'var(--ti-common-space-6x)',
'ti-slider-handle-border-color': 'var(--ti-common-color-data-1)',
'ti-slider-handle-border-color-hover': 'var(--ti-common-color-data-1)',
'ti-slider-handle-margin-top': 'calc(var(--ti-common-space-2x) * -1)',
'ti-slider-handle-icon-fill-color': 'var(--ti-common-color-data-1)',
'ti-slider-handle-border-radius': 'var(--ti-common-border-radius-4)',
'ti-slider-handle-icon-disabled-fill-color': 'var(--ti-common-color-icon-disabled)',
'ti-slider-handle-disabled-border-color': 'var(--ti-common-color-icon-disabled)',
'ti-slider-tips-text-color': 'var(--ti-common-color-dark)',
'ti-slider-tips-box-shadow': 'var(--ti-common-box-shadow-popover)',
'ti-slider-input-margin-left': 'var(--ti-common-space-4x)',
'ti-slider-input-width': 'calc(var(--ti-common-space-10x) * 2)',
'ti-slider-input-text-align': 'left',
'ti-slider-mark-label-font-size': 'var(--ti-common-font-size-1)',
'ti-slider-mark-label-text-color': '#999999'
}

View File

@ -30,6 +30,8 @@
--ti-slider-range-top: calc(-1 * var(--ti-common-space-base)); --ti-slider-range-top: calc(-1 * var(--ti-common-space-base));
// 滑块进度条顶部外边距 // 滑块进度条顶部外边距
--ti-slider-range-margin-top: calc(-1 * var(--ti-common-space-base)); --ti-slider-range-margin-top: calc(-1 * var(--ti-common-space-base));
// 滑块进度条禁用时背景色
--ti-slider-range-disabled-bg-color: var(--ti-common-color-text-disabled);
// 滑块点宽度 // 滑块点宽度
--ti-slider-handle-width: var(--ti-common-size-5x, 20px); --ti-slider-handle-width: var(--ti-common-size-5x, 20px);
@ -70,6 +72,10 @@
--ti-slider-handle-margin-horizontal: calc(var(--ti-common-space-2x, 8px) * -1); --ti-slider-handle-margin-horizontal: calc(var(--ti-common-space-2x, 8px) * -1);
// 滑块点底部外边距 // 滑块点底部外边距
--ti-slider-handle-margin-bottom: var(--ti-common-space-0, 0px); --ti-slider-handle-margin-bottom: var(--ti-common-space-0, 0px);
// 滑块点禁用时图标颜色
--ti-slider-handle-icon-disabled-fill-color: var(--ti-common-color-text-disabled);
// 滑块点禁用时边框颜色
--ti-slider-handle-disabled-border-color: var(--ti-common-color-text-disabled);
// 滑块输入框高度 // 滑块输入框高度
--ti-slider-input-height: var(--ti-common-size-height-normal, 30px); --ti-slider-input-height: var(--ti-common-size-height-normal, 30px);
@ -83,6 +89,19 @@
--ti-slider-input-text-color: var(--ti-common-color-info-normal, #333); --ti-slider-input-text-color: var(--ti-common-color-info-normal, #333);
// 滑块输入框背景色 // 滑块输入框背景色
--ti-slider-input-bg-color: var(--ti-common-color-light, #fff); --ti-slider-input-bg-color: var(--ti-common-color-light, #fff);
// 滑块输入框左边外间距
--ti-slider-input-margin-left: var(--ti-common-size-3x, 12px);
// 滑块输入框单位左边padding
--ti-slider-input-unit-margin-left: var(--ti-common-space-2x, 8px);
// 滑块输入框单位文字对齐方式(hide)
--ti-slider-input-text-align: center;
// 滑块刻度的宽度,竖向模式时作为滑块刻度的高度
--ti-slider-mark-point-width: calc(var(--ti-common-space-base, 4px) / 2);
// 滑块刻度背景色
--ti-slider-mark-point-bg-color: var(--ti-common-color-light, #fff);
// 滑块刻度的label的顶部间距竖向模式时作为左间距
--ti-slider-mark-label-margin-top: calc(var(--ti-common-space-base, 4px) * 2 + var(--ti-slider-height));
// 滑块活动时提示框背景色 // 滑块活动时提示框背景色
--ti-slider-tips-bg-color: var(--ti-common-color-bg-dark-normal, #464c59); --ti-slider-tips-bg-color: var(--ti-common-color-bg-dark-normal, #464c59);

View File

@ -60,6 +60,7 @@ export default {
'time-range': 'time-range', 'time-range': 'time-range',
'time-select': 'time-select', 'time-select': 'time-select',
'scroll-text': 'scroll-text', 'scroll-text': 'scroll-text',
'slider-input': 'slider__input',
'slide-bar': 'slide-bar', 'slide-bar': 'slide-bar',
'slide-img': 'slide-img', 'slide-img': 'slide-img',
'select-dropdown': 'select-dropdown', 'select-dropdown': 'select-dropdown',

View File

@ -52,7 +52,7 @@ describe('PC Mode', () => {
test.todo('step 设置滑块移动时每步位移距离必需是大于0的正整数。') test.todo('step 设置滑块移动时每步位移距离必需是大于0的正整数。')
test('show-input 是否显示输入框,仅在非范围选择时有效', async () => { test('show-input 是否显示输入框', async () => {
const value = ref(60) const value = ref(60)
const wrapper = mount(() => <Slider v-model={value.value} showInput={true} min={0} max={100} />) const wrapper = mount(() => <Slider v-model={value.value} showInput={true} min={0} max={100} />)
@ -60,6 +60,19 @@ describe('PC Mode', () => {
expect(input.exists()).toBe(true) expect(input.exists()).toBe(true)
await input.setValue(110) await input.setValue(110)
expect(value.value).toBe(100) expect(value.value).toBe(100)
// 双输入框
const value2 = ref([40, 60])
const wrapper2 = mount(() => <Slider v-model={value2.value} showInput={true} min={0} max={100} />)
const input1 = wrapper2.find('.tiny-slider__input input')
expect(input1.exists()).toBe(true)
const input2 = wrapper2.find('.tiny-slider__input input:nth-child(3)')
expect(input2.exists()).toBe(true)
await input1.setValue(70)
await input1.trigger('blur')
expect(value2.value).toStrictEqual([60, 60])
}) })
test('show-percent 是否显示百分比仅在show-input为true时有效', async () => { test('show-percent 是否显示百分比仅在show-input为true时有效', async () => {
@ -114,4 +127,22 @@ describe('PC Mode', () => {
test.todo( test.todo(
'Stop 设置滑块滑动结束时,触发该事件;arg:{Number|Array 滑块非范围选择时,是滑块当前值;滑块是范围选择时,是滑块当前值数组}' 'Stop 设置滑块滑动结束时,触发该事件;arg:{Number|Array 滑块非范围选择时,是滑块当前值;滑块是范围选择时,是滑块当前值数组}'
) )
test('marks', async () => {
const marks = {
10: '10%',
40: '40%',
50: '50%'
}
const value = ref(20)
const wrapper = mount(() => <Slider v-model={value.value} marks={marks}></Slider>)
const points = wrapper.findAll('.tiny-slider__mark-point')
const labels = wrapper.findAll('.tiny-slider__mark-label')
Object.entries(marks).forEach(([key], index) => {
expect(points[index].attributes('style')).toContain(`left: ${key}%`)
expect(labels[index].attributes('style')).toContain(`left: ${key}%`)
})
})
}) })

View File

@ -10,6 +10,7 @@
* *
*/ */
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common' import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
import type { PropType } from 'vue'
import template from 'virtual-template?pc|mobile|mobile-first' import template from 'virtual-template?pc|mobile|mobile-first'
export const $constants = { export const $constants = {
@ -20,6 +21,7 @@ export const $constants = {
PC_SLIDER_CLS: 'tiny-slider', PC_SLIDER_CLS: 'tiny-slider',
PC_RANGE_CLS: 'tiny-slider__range', PC_RANGE_CLS: 'tiny-slider__range',
PC_BUTTON_CLS: 'tiny-slider__handle', PC_BUTTON_CLS: 'tiny-slider__handle',
PC_LABEL_CLS: 'tiny-slider__mark-label',
PC_LEFT_SVG_CLS: 'tiny-slider-left-svg', PC_LEFT_SVG_CLS: 'tiny-slider-left-svg',
PC_RIGHT_SVG_CLS: 'tiny-slider-right-svg', PC_RIGHT_SVG_CLS: 'tiny-slider-right-svg',
MOBILE_TIP_CLS: 'tiny-mobile-slider__tips', MOBILE_TIP_CLS: 'tiny-mobile-slider__tips',
@ -123,6 +125,9 @@ export const sliderProps = {
showLabel: { showLabel: {
type: Boolean, type: Boolean,
default: false default: false
},
marks: {
type: Object as PropType<Record<number, string>>
} }
} }

View File

@ -70,18 +70,43 @@
<div ref="sliderTip" class="tiny-slider__tips" v-show="showTip && state.showTip" :style="state.tipStyle"> <div ref="sliderTip" class="tiny-slider__tips" v-show="showTip && state.showTip" :style="state.tipStyle">
{{ state.tipValue }} {{ state.tipValue }}
</div> </div>
<div v-if="state.markList">
<template v-for="mark in state.markList" :key="mark.value">
<div class="tiny-slider__mark-point" :style="mark.positionStyle"></div>
<div class="tiny-slider__mark-label" :style="mark.positionStyle">{{ mark.label }}</div>
</template>
</div> </div>
<template v-if="showInput && !state.isDouble"> </div>
<template v-if="showInput">
<div class="tiny-slider__input"> <div class="tiny-slider__input">
<slot :slot-scope="state.activeValue"> <slot :slot-scope="state.slotValue">
<input <input
v-if="!state.isDouble"
type="text" type="text"
v-model="state.slotValue" :value="state.slotValue"
@focus="handleSlotInputFocus" @focus="handleSlotInputFocus"
@blur="handleSlotInputBlur" @blur="handleSlotInputBlur"
@input="handleSlotInput" @input="handleSlotInput($event)"
:disabled="state.disabled" :disabled="state.disabled"
/><span>{{ unit }}</span> />
<template v-else>
<input
:value="state.slotValue[0]"
@focus="handleSlotInputFocus"
@blur="handleSlotInputBlur"
@input="handleSlotInput($event)"
:disabled="state.disabled"
/>
<span class="tiny-slider__input__split">-</span>
<input
:value="state.slotValue[1]"
@focus="handleSlotInputFocus"
@blur="handleSlotInputBlur"
@input="handleSlotInput($event, false)"
:disabled="state.disabled"
/>
</template>
<span class="tiny-slider__input__unit">{{ unit }}</span>
</slot> </slot>
</div> </div>
</template> </template>
@ -106,6 +131,7 @@ export default defineComponent({
'step', 'step',
'numPages', 'numPages',
'showTip', 'showTip',
'marks',
'showInput', 'showInput',
'unit', 'unit',
'height', 'height',