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',
|
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',
|
||||||
|
|
|
@ -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>
|
<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>
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
})
|
})
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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> & {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-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);
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}%`)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue