forked from opentiny/tiny-vue
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:
parent
9146bdb554
commit
8900246bea
|
@ -111,8 +111,8 @@ export default {
|
|||
type: 'boolean',
|
||||
defaultValue: 'false',
|
||||
desc: {
|
||||
'zh-CN': '是否显示输入框,仅在非范围选择时有效',
|
||||
'en-US': 'Indicates whether to display the text box. This parameter is valid only for non-range selection'
|
||||
'zh-CN': '是否显示输入框',
|
||||
'en-US': 'Indicates whether to display the text box.'
|
||||
},
|
||||
mode: ['pc', 'mobile', 'mobile-first'],
|
||||
pcDemo: 'show-input',
|
||||
|
@ -203,6 +203,17 @@ export default {
|
|||
mode: ['mobile'],
|
||||
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',
|
||||
type: 'boolean',
|
||||
|
|
|
@ -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>
|
|
@ -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%/)
|
||||
})
|
|
@ -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>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider>
|
||||
<tiny-slider v-model="value2" :show-input="true"></tiny-slider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
@ -7,4 +8,5 @@ import { ref } from 'vue'
|
|||
import { Slider as TinySlider } from '@opentiny/vue'
|
||||
|
||||
const value = ref(40)
|
||||
const value2 = ref([40, 60])
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<tiny-slider v-model="value" :show-input="true" unit="%"></tiny-slider>
|
||||
<tiny-slider v-model="value2" :show-input="true"></tiny-slider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -11,7 +12,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
value: 40
|
||||
value: 40,
|
||||
value2: [40, 60]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,21 @@ test('输入框模式', async ({ page }) => {
|
|||
|
||||
const sliderInput = page.locator('.tiny-slider-container .tiny-slider__input input')
|
||||
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')
|
||||
})
|
||||
|
|
|
@ -28,6 +28,18 @@ export default {
|
|||
},
|
||||
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',
|
||||
name: {
|
||||
|
|
|
@ -95,6 +95,7 @@ export const bindMouseDown =
|
|||
const handleEl = event.target
|
||||
let isClickBar: boolean | undefined = false
|
||||
let isClickBtn: boolean | undefined = false
|
||||
let isClickLabel: boolean | undefined = false
|
||||
|
||||
if (mode === 'mobile-first') {
|
||||
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.leftSvgCls(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
|
||||
return
|
||||
}
|
||||
|
@ -122,7 +124,7 @@ export const bindMouseDown =
|
|||
state.isDrag = isClickBtn
|
||||
isClickBtn && (state.activeIndex = api.getActiveButtonIndex(event))
|
||||
|
||||
if (isClickBar) {
|
||||
if (isClickBar || isClickLabel) {
|
||||
const currentValue = api.calculateValue(event)
|
||||
if (state.isDouble) {
|
||||
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) =>
|
||||
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
|
||||
|
||||
export const autoSlider = (api) => (value) => {
|
||||
|
@ -489,11 +491,6 @@ export const watchActiveValue =
|
|||
} else {
|
||||
state.activeValue = nNewValue || 0
|
||||
}
|
||||
|
||||
// 正在输入时,不应该改变输入的内容
|
||||
if (!state.isSlotTyping) {
|
||||
state.slotValue = state.activeValue
|
||||
}
|
||||
}
|
||||
|
||||
export const watchModelValue =
|
||||
|
@ -511,6 +508,11 @@ export const watchModelValue =
|
|||
api.setActiveButtonValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// 正在输入时,不应该改变输入的内容
|
||||
if (!state.isSlotTyping) {
|
||||
api.updateSlotValue()
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
({ props, state, api }: Pick<ISliderRenderlessParams, 'api' | 'props' | 'state'>) =>
|
||||
($event, pos) => {
|
||||
|
@ -569,11 +606,30 @@ export const handleSlotInputFocus = (state: ISliderRenderlessParams['state']) =>
|
|||
state.isSlotTyping = true
|
||||
}
|
||||
|
||||
export const handleSlotInputBlur = (state: ISliderRenderlessParams['state']) => () => {
|
||||
export const handleSlotInputBlur =
|
||||
({ state, api }: Pick<ISliderRenderlessParams, 'api' | 'state'>) =>
|
||||
() => {
|
||||
state.isSlotTyping = false
|
||||
state.slotValue = state.activeValue
|
||||
}
|
||||
api.updateSlotValue()
|
||||
}
|
||||
|
||||
export const handleSlotInput = (state: ISliderRenderlessParams['state']) => (event: Event) => {
|
||||
state.activeValue = Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
export const updateSlotValue =
|
||||
({ 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()
|
||||
}
|
||||
|
|
|
@ -41,7 +41,9 @@ import {
|
|||
inputValueChange,
|
||||
handleSlotInputFocus,
|
||||
handleSlotInputBlur,
|
||||
handleSlotInput
|
||||
handleSlotInput,
|
||||
getMarkList,
|
||||
updateSlotValue
|
||||
} from './index'
|
||||
|
||||
import type {
|
||||
|
@ -91,6 +93,7 @@ const initState = ({ reactive, computed, props, api, parent, inject }) => {
|
|||
moveStyle: [],
|
||||
points: [],
|
||||
labels: [],
|
||||
markList: computed(() => api.getMarkList()),
|
||||
inputValue: [0, 0],
|
||||
isDrag: false,
|
||||
sliderSize: 0,
|
||||
|
@ -160,8 +163,10 @@ export const renderless = (
|
|||
getLabels: getLabels({ props, state }),
|
||||
inputValueChange: inputValueChange({ props, api, state }),
|
||||
handleSlotInputFocus: handleSlotInputFocus(state),
|
||||
handleSlotInputBlur: handleSlotInputBlur(state),
|
||||
handleSlotInput: handleSlotInput(state)
|
||||
handleSlotInputBlur: handleSlotInputBlur({ state, api }),
|
||||
handleSlotInput: handleSlotInput({ state, api }),
|
||||
getMarkList: getMarkList({ props }),
|
||||
updateSlotValue: updateSlotValue({ state })
|
||||
})
|
||||
|
||||
watch(
|
||||
|
|
|
@ -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 { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
|
||||
import type {
|
||||
getMarkList,
|
||||
getActiveButtonValue,
|
||||
handleSlotInputFocus,
|
||||
handleSlotInputBlur,
|
||||
handleSlotInput,
|
||||
updateSlotValue
|
||||
} from '../src/slider'
|
||||
|
||||
export type ISliderProps = ExtractPropTypes<typeof sliderProps>
|
||||
|
||||
|
@ -8,7 +16,7 @@ export type ISliderConstants = typeof $constants
|
|||
|
||||
export interface ISliderState {
|
||||
tipStyle: object
|
||||
barStyle: object
|
||||
barStyle: CSSProperties
|
||||
moveStyle: object
|
||||
points: object[]
|
||||
labels: object[]
|
||||
|
@ -33,11 +41,12 @@ export interface ISliderState {
|
|||
rangeDiff: ComputedRef<number>
|
||||
tipValue: ComputedRef<string>
|
||||
formDisabled: ComputedRef<boolean>
|
||||
disabled: ComputedRef<boolean>
|
||||
disabled: boolean
|
||||
/** 使用这个值作为插槽中输入的值,而不是直接用activeValue,来实现在输入时不会被max min属性计算而改变 */
|
||||
slotValue: number
|
||||
slotValue: number | number[] | string
|
||||
/** 是否正在输入 */
|
||||
isSlotTyping: boolean
|
||||
markList: ReturnType<ISliderApi['getMarkList']>
|
||||
}
|
||||
|
||||
export interface ISliderApi {
|
||||
|
@ -49,7 +58,7 @@ export interface ISliderApi {
|
|||
bindResize: () => void
|
||||
setButtonStyle: () => void
|
||||
calculateValue: (event: Event) => number
|
||||
getActiveButtonValue: () => number
|
||||
getActiveButtonValue: ReturnType<typeof getActiveButtonValue>
|
||||
getActiveButtonIndex: (event: Event) => number
|
||||
setTipStyle: () => void
|
||||
customAfterAppearHook: () => void
|
||||
|
@ -63,15 +72,17 @@ export interface ISliderApi {
|
|||
bindMouseMove: () => void
|
||||
bindMouseDown: () => void
|
||||
setActiveButtonValue: (currentValue: number) => void
|
||||
initSlider: (inputValue: number | [number, number]) => void
|
||||
initSlider: (inputValue: number | number[]) => void
|
||||
watchModelValue: () => void
|
||||
watchActiveValue: () => void
|
||||
getPoints: () => void
|
||||
getLabels: () => void
|
||||
inputValueChange: () => void
|
||||
handleSlotInputFocus: () => void
|
||||
handleSlotInputBlur: () => void
|
||||
handleSlotInput: (event: Event) => void
|
||||
handleSlotInputFocus: ReturnType<typeof handleSlotInputFocus>
|
||||
handleSlotInputBlur: ReturnType<typeof handleSlotInputBlur>
|
||||
handleSlotInput: ReturnType<typeof handleSlotInput>
|
||||
getMarkList: ReturnType<typeof getMarkList>
|
||||
updateSlotValue: ReturnType<typeof updateSlotValue>
|
||||
}
|
||||
|
||||
export type ISliderRenderlessParams = ISharedRenderlessFunctionParams<ISliderConstants> & {
|
||||
|
|
|
@ -57,10 +57,20 @@
|
|||
|
||||
&.disabled {
|
||||
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;
|
||||
border-color: var(--ti-slider-handle-disabled-border-color);
|
||||
|
||||
&:hover svg, svg {
|
||||
fill: var(--ti-slider-handle-icon-disabled-fill-color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__vertical {
|
||||
|
@ -94,6 +104,19 @@
|
|||
margin: var(--ti-slider-margin-vertical) var(--ti-slider-margin-right) var(--ti-slider-margin-vertical)
|
||||
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 {
|
||||
|
@ -169,6 +192,7 @@
|
|||
border: 1px solid var(--ti-slider-tips-border-color);
|
||||
border-radius: var(--ti-common-border-radius-1);
|
||||
color: var(--ti-slider-tips-text-color);
|
||||
box-shadow: var(--ti-slider-tips-box-shadow);
|
||||
|
||||
&:before {
|
||||
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 {
|
||||
display: inline-block;
|
||||
line-height: var(--ti-slider-input-height);
|
||||
vertical-align: top;
|
||||
margin-left: 12px;
|
||||
margin-left:var(--ti-slider-input-margin-left);
|
||||
font-size: var(--ti-common-font-size-base);
|
||||
|
||||
&__split {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: var(--ti-slider-input-width);
|
||||
height: var(--ti-slider-input-height);
|
||||
|
@ -209,11 +254,11 @@
|
|||
outline: 0;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
text-align: var(--ti-slider-input-text-align);
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 8px;
|
||||
&__unit {
|
||||
margin-left: var(--ti-slider-input-unit-margin-left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
--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-disabled-bg-color: var(--ti-common-color-text-disabled);
|
||||
|
||||
// 滑块点宽度
|
||||
--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-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);
|
||||
|
@ -83,6 +89,19 @@
|
|||
--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-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);
|
||||
|
|
|
@ -60,6 +60,7 @@ export default {
|
|||
'time-range': 'time-range',
|
||||
'time-select': 'time-select',
|
||||
'scroll-text': 'scroll-text',
|
||||
'slider-input': 'slider__input',
|
||||
'slide-bar': 'slide-bar',
|
||||
'slide-img': 'slide-img',
|
||||
'select-dropdown': 'select-dropdown',
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('PC Mode', () => {
|
|||
|
||||
test.todo('step 设置滑块移动时,每步位移距离,必需是大于0的正整数。')
|
||||
|
||||
test('show-input 是否显示输入框,仅在非范围选择时有效', async () => {
|
||||
test('show-input 是否显示输入框', async () => {
|
||||
const value = ref(60)
|
||||
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)
|
||||
await input.setValue(110)
|
||||
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 () => {
|
||||
|
@ -114,4 +127,22 @@ describe('PC Mode', () => {
|
|||
test.todo(
|
||||
'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}%`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
*
|
||||
*/
|
||||
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
|
||||
import type { PropType } from 'vue'
|
||||
import template from 'virtual-template?pc|mobile|mobile-first'
|
||||
|
||||
export const $constants = {
|
||||
|
@ -20,6 +21,7 @@ export const $constants = {
|
|||
PC_SLIDER_CLS: 'tiny-slider',
|
||||
PC_RANGE_CLS: 'tiny-slider__range',
|
||||
PC_BUTTON_CLS: 'tiny-slider__handle',
|
||||
PC_LABEL_CLS: 'tiny-slider__mark-label',
|
||||
PC_LEFT_SVG_CLS: 'tiny-slider-left-svg',
|
||||
PC_RIGHT_SVG_CLS: 'tiny-slider-right-svg',
|
||||
MOBILE_TIP_CLS: 'tiny-mobile-slider__tips',
|
||||
|
@ -123,6 +125,9 @@ export const sliderProps = {
|
|||
showLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
marks: {
|
||||
type: Object as PropType<Record<number, string>>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,18 +70,43 @@
|
|||
<div ref="sliderTip" class="tiny-slider__tips" v-show="showTip && state.showTip" :style="state.tipStyle">
|
||||
{{ state.tipValue }}
|
||||
</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>
|
||||
<template v-if="showInput && !state.isDouble">
|
||||
</div>
|
||||
<template v-if="showInput">
|
||||
<div class="tiny-slider__input">
|
||||
<slot :slot-scope="state.activeValue">
|
||||
<slot :slot-scope="state.slotValue">
|
||||
<input
|
||||
v-if="!state.isDouble"
|
||||
type="text"
|
||||
v-model="state.slotValue"
|
||||
:value="state.slotValue"
|
||||
@focus="handleSlotInputFocus"
|
||||
@blur="handleSlotInputBlur"
|
||||
@input="handleSlotInput"
|
||||
@input="handleSlotInput($event)"
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -106,6 +131,7 @@ export default defineComponent({
|
|||
'step',
|
||||
'numPages',
|
||||
'showTip',
|
||||
'marks',
|
||||
'showInput',
|
||||
'unit',
|
||||
'height',
|
||||
|
|
Loading…
Reference in New Issue