feat(components): [slider] aria keyboard controls and attrs (#7389)

* feat(components): [slider] aria keyboard controls and attrs

fix #7350

* feat(components): [slider] fix lint error in slider types

* feat(components): [slider] change start value for home/end unit test

* feat(components): prevent scrolling on touch screen for runway

* feat(components): [slider] type-o in locale prop

* fix(components): [slider] PR comments r1

* fix(components): [slider] linting errors
This commit is contained in:
opengraphica 2022-04-27 09:38:47 -04:00 committed by GitHub
parent dc98974db9
commit 16989d8187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 432 additions and 180 deletions

View File

@ -91,6 +91,9 @@ slider/show-marks
| vertical | vertical mode | boolean | — | false |
| height | Slider height, required in vertical mode | string | — | — |
| label | label for screen reader | string | — | — |
| range-start-label | when `range` is true, screen reader label for the start of the range | string | — | — |
| range-end-label | when `range` is true, screen reader label for the end of the range | string | — | — |
| format-value-text | format to display the `aria-valuenow` attribute for screen readers | function(value) | — | — |
| debounce | debounce delay when typing, in milliseconds, works when `show-input` is true | number | — | 300 |
| tooltip-class | custom class name for the tooltip | string | — | — |
| marks | marks type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | object | — | — |

View File

@ -1,7 +1,7 @@
import { h, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import sleep from '@element-plus/test-utils/sleep'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EVENT_CODE } from '@element-plus/constants'
import Slider from '../src/index.vue'
vi.mock('lodash-unified', async () => {
@ -16,6 +16,14 @@ vi.mock('lodash-unified', async () => {
})
describe('Slider', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('create', () => {
const wrapper = mount(Slider)
expect(wrapper.props().modelValue).toBe(0)
@ -34,7 +42,7 @@ describe('Slider', () => {
},
})
await sleep(10)
await nextTick()
wrapper.vm.value = 40
await nextTick()
expect(wrapper.vm.value).toBe(50)
@ -118,7 +126,7 @@ describe('Slider', () => {
}
},
methods: {
formatTooltip(val) {
formatTooltip(val: number) {
return `$${val}`
},
},
@ -131,6 +139,7 @@ describe('Slider', () => {
describe('drag', () => {
it('horizontal', async () => {
vi.useRealTimers()
const wrapper = mount(
{
template: `
@ -151,9 +160,11 @@ describe('Slider', () => {
)
const slider = wrapper.findComponent({ name: 'ElSliderButton' })
const mockClientWidth = vi
.spyOn(wrapper.find('.el-slider__runway').element, 'clientWidth', 'get')
.mockImplementation(() => 200)
vi.spyOn(
wrapper.find('.el-slider__runway').element,
'clientWidth',
'get'
).mockImplementation(() => 200)
slider.trigger('mousedown', { clientX: 0 })
const mousemove = document.createEvent('MouseEvent')
@ -198,10 +209,10 @@ describe('Slider', () => {
await nextTick()
expect(wrapper.vm.value === 50).toBeTruthy()
mockClientWidth.mockRestore()
})
it('vertical', async () => {
vi.useRealTimers()
const wrapper = mount(
{
template: `
@ -221,14 +232,11 @@ describe('Slider', () => {
}
)
const slider = wrapper.findComponent({ name: 'ElSliderButton' })
const mockClientHeight = vi
.spyOn(
wrapper.find('.el-slider__runway').element,
'clientHeight',
'get'
)
.mockImplementation(() => 200)
vi.spyOn(
wrapper.find('.el-slider__runway').element,
'clientHeight',
'get'
).mockImplementation(() => 200)
slider.trigger('mousedown', { clientY: 0 })
const mousemove = document.createEvent('MouseEvent')
mousemove.initMouseEvent(
@ -270,37 +278,123 @@ describe('Slider', () => {
window.dispatchEvent(mouseup)
await nextTick()
expect(wrapper.vm.value).toBe(50)
mockClientHeight.mockRestore()
})
})
it('accessibility', (done) => {
const wrapper = mount({
template: `
<div>
<slider v-model="value"></slider>
</div>
`,
components: { Slider },
data() {
return {
value: 0.1,
}
},
})
const slider: any = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onRightKeyDown()
setTimeout(() => {
describe('accessibility', () => {
it('left/right arrows', async () => {
const wrapper = mount({
template: `
<div>
<slider v-model="value"></slider>
</div>
`,
components: { Slider },
data() {
return {
value: 0.1,
}
},
})
const slider: any = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.right })
)
await nextTick()
expect(wrapper.vm.value).toBe(1)
slider.vm.onLeftKeyDown()
setTimeout(() => {
expect(wrapper.vm.value).toBe(0)
done()
}, 10)
}, 10)
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.left })
)
await nextTick()
expect(wrapper.vm.value).toBe(0)
})
it('up/down arrows', async () => {
const wrapper = mount({
template: `
<div>
<slider v-model="value"></slider>
</div>
`,
components: { Slider },
data() {
return {
value: 0.1,
}
},
})
const slider: any = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onKeyDown(new KeyboardEvent('keydown', { key: EVENT_CODE.up }))
await nextTick()
expect(wrapper.vm.value).toBe(1)
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.down })
)
await nextTick()
expect(wrapper.vm.value).toBe(0)
})
it('page up/down keys', async () => {
const wrapper = mount({
template: `
<div>
<slider v-model="value" :min="-5" :max="10"></slider>
</div>
`,
components: { Slider },
data() {
return {
value: -1,
}
},
})
const slider: any = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.pageUp })
)
await nextTick()
expect(wrapper.vm.value).toBe(3)
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.pageDown })
)
await nextTick()
expect(wrapper.vm.value).toBe(-1)
})
it('home/end keys', async () => {
const wrapper = mount({
template: `
<div>
<slider v-model="value" :min="-5" :max="10"></slider>
</div>
`,
components: { Slider },
data() {
return {
value: 0,
}
},
})
const slider: any = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onKeyDown(
new KeyboardEvent('keydown', { key: EVENT_CODE.home })
)
await nextTick()
expect(wrapper.vm.value).toBe(-5)
slider.vm.onKeyDown(new KeyboardEvent('keydown', { key: EVENT_CODE.end }))
await nextTick()
expect(wrapper.vm.value).toBe(10)
})
})
it('step', (done) => {
vi.useRealTimers()
const wrapper = mount(
{
template: `
@ -372,6 +466,7 @@ describe('Slider', () => {
})
it('click', (done) => {
vi.useRealTimers()
const wrapper = mount({
template: `
<div>
@ -385,17 +480,22 @@ describe('Slider', () => {
}
},
})
const mockClientWidth = vi
.spyOn(wrapper.find('.el-slider__runway').element, 'clientWidth', 'get')
.mockImplementation(() => 200)
const slider: any = wrapper.findComponent({ name: 'ElSlider' })
setTimeout(() => {
slider.vm.onSliderClick({ clientX: 100 })
slider.vm.onSliderClick(new MouseEvent('mousedown', { clientX: 100 }))
setTimeout(() => {
expect(wrapper.vm.value > 0).toBeTruthy()
done()
mockClientWidth.mockRestore()
}, 10)
}, 10)
})
it('change event', (done) => {
vi.useRealTimers()
const wrapper = mount({
template: `
<div style="width: 200px">
@ -411,7 +511,7 @@ describe('Slider', () => {
}
},
methods: {
onChange(val) {
onChange(val: number) {
this.data = val
},
},
@ -432,7 +532,7 @@ describe('Slider', () => {
.mockImplementation(() => 200)
setTimeout(() => {
expect(wrapper.vm.data).toBe(0)
slider.vm.onSliderClick({ clientX: 100 })
slider.vm.onSliderClick(new MouseEvent('mousedown', { clientX: 100 }))
setTimeout(() => {
expect(wrapper.vm.data === 50).toBeTruthy()
mockRectLeft.mockRestore()
@ -443,6 +543,7 @@ describe('Slider', () => {
})
it('input event', async (done) => {
vi.useRealTimers()
const wrapper = mount({
template: `
<div style="width: 200px">
@ -458,7 +559,7 @@ describe('Slider', () => {
}
},
methods: {
onInput(val) {
onInput(val: number) {
this.data = val
},
},
@ -479,7 +580,7 @@ describe('Slider', () => {
.mockImplementation(() => 200)
await nextTick()
expect(wrapper.vm.data).toBe(0)
slider.vm.onSliderClick({ clientX: 100 })
slider.vm.onSliderClick(new MouseEvent('mousedown', { clientX: 100 }))
await nextTick()
expect(wrapper.vm.data === 50).toBeTruthy()
mockRectLeft.mockRestore()
@ -488,6 +589,7 @@ describe('Slider', () => {
})
it('disabled', (done) => {
vi.useRealTimers()
const wrapper = mount({
template: `
<div>
@ -552,6 +654,7 @@ describe('Slider', () => {
})
it('show input', (done) => {
vi.useRealTimers()
const wrapper = mount({
template: `
<div>
@ -585,6 +688,7 @@ describe('Slider', () => {
})
it('vertical mode', (done) => {
vi.useRealTimers()
const wrapper = mount(
{
template: `
@ -618,7 +722,7 @@ describe('Slider', () => {
.mockImplementation(() => 200)
const slider: any = wrapper.getComponent({ name: 'ElSlider' })
setTimeout(() => {
slider.vm.onSliderClick({ clientY: 100 })
slider.vm.onSliderClick(new MouseEvent('mousedown', { clientX: 100 }))
setTimeout(() => {
expect(wrapper.vm.value > 0).toBeTruthy()
mockRectBottom.mockRestore()
@ -665,7 +769,7 @@ describe('Slider', () => {
expect(sliders.length).toBe(2)
})
it('should not exceed min and max', (done) => {
it('should not exceed min and max', async () => {
const wrapper = mount({
template: `
<div>
@ -680,20 +784,19 @@ describe('Slider', () => {
}
},
})
setTimeout(() => {
wrapper.vm.value = [40, 60]
setTimeout(() => {
expect(wrapper.vm.value).toStrictEqual([50, 60])
wrapper.vm.value = [50, 120]
setTimeout(() => {
expect(wrapper.vm.value).toStrictEqual([50, 100])
done()
}, 10)
}, 10)
}, 10)
await nextTick()
wrapper.vm.value = [40, 60]
await nextTick()
expect(wrapper.vm.value).toStrictEqual([50, 60])
wrapper.vm.value = [50, 120]
await nextTick()
expect(wrapper.vm.value).toStrictEqual([50, 100])
})
it('click', (done) => {
vi.useRealTimers()
const wrapper = mount(
{
template: `
@ -727,7 +830,7 @@ describe('Slider', () => {
.mockImplementation(() => 200)
const slider: any = wrapper.getComponent({ name: 'ElSlider' })
setTimeout(() => {
slider.vm.onSliderClick({ clientX: 100 })
slider.vm.onSliderClick(new MouseEvent('mousedown', { clientX: 100 }))
setTimeout(() => {
// Because mock the clientWidth, so the targetValue is 50.
// The behavior of the setPosition method in the useSlider.ts file should be that the value of the second button is 50
@ -740,7 +843,7 @@ describe('Slider', () => {
}, 10)
})
it('responsive to dynamic min and max', (done) => {
it('responsive to dynamic min and max', async () => {
const wrapper = mount({
template: `
<div>
@ -757,21 +860,19 @@ describe('Slider', () => {
}
},
})
setTimeout(() => {
wrapper.vm.min = 60
setTimeout(() => {
expect(wrapper.vm.value).toStrictEqual([60, 80])
wrapper.vm.min = 30
wrapper.vm.max = 40
setTimeout(() => {
expect(wrapper.vm.value).toStrictEqual([40, 40])
done()
}, 10)
}, 10)
}, 10)
await nextTick()
wrapper.vm.min = 60
await nextTick()
expect(wrapper.vm.value).toStrictEqual([60, 80])
wrapper.vm.min = 30
wrapper.vm.max = 40
await nextTick()
expect(wrapper.vm.value).toStrictEqual([40, 40])
})
it('show stops', (done) => {
it('show stops', async () => {
const wrapper = mount({
template: `
<div>
@ -790,11 +891,9 @@ describe('Slider', () => {
}
},
})
setTimeout(() => {
const stops = wrapper.findAll('.el-slider__stop')
expect(stops.length).toBe(5)
done()
}, 10)
await nextTick()
const stops = wrapper.findAll('.el-slider__stop')
expect(stops.length).toBe(5)
})
it('marks', async () => {
@ -830,14 +929,11 @@ describe('Slider', () => {
},
})
nextTick().then(() => {
const stops = wrapper.findAll('.el-slider__marks-stop.el-slider__stop')
const marks = wrapper.findAll(
'.el-slider__marks .el-slider__marks-text'
)
expect(marks.length).toBe(2)
expect(stops.length).toBe(2)
})
await nextTick()
const stops = wrapper.findAll('.el-slider__marks-stop.el-slider__stop')
const marks = wrapper.findAll('.el-slider__marks .el-slider__marks-text')
expect(marks.length).toBe(2)
expect(stops.length).toBe(2)
})
})
})

View File

@ -10,10 +10,7 @@
@touchstart="onButtonDown"
@focus="handleMouseEnter"
@blur="handleMouseLeave"
@keydown.left="onLeftKeyDown"
@keydown.right="onRightKeyDown"
@keydown.down.prevent="onLeftKeyDown"
@keydown.up.prevent="onRightKeyDown"
@keydown="onKeyDown"
>
<el-tooltip
ref="tooltip"
@ -79,6 +76,7 @@ export default defineComponent({
})
const {
button,
tooltip,
showTooltip,
tooltipVisible,
@ -87,8 +85,7 @@ export default defineComponent({
handleMouseEnter,
handleMouseLeave,
onButtonDown,
onLeftKeyDown,
onRightKeyDown,
onKeyDown,
setPosition,
} = useSliderButton(props, initData, emit)
@ -96,6 +93,7 @@ export default defineComponent({
return {
ns,
button,
tooltip,
tooltipVisible,
showTooltip,
@ -104,8 +102,7 @@ export default defineComponent({
handleMouseEnter,
handleMouseLeave,
onButtonDown,
onLeftKeyDown,
onRightKeyDown,
onKeyDown,
setPosition,
hovering,

View File

@ -2,11 +2,10 @@
<div
ref="sliderWrapper"
:class="sliderKls"
role="slider"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
:aria-disabled="sliderDisabled"
:role="range ? 'group' : undefined"
:aria-label="range ? groupLabel : undefined"
@touchstart="onSliderWrapperPrevent"
@touchmove="onSliderWrapperPrevent"
>
<div
ref="slider"
@ -16,7 +15,8 @@
ns.is('disabled', sliderDisabled),
]"
:style="runwayStyle"
@click="onSliderClick"
@mousedown="onSliderDown"
@touchstart="onSliderDown"
>
<div :class="ns.e('bar')" :style="barStyle" />
<slider-button
@ -24,6 +24,14 @@
:model-value="firstValue"
:vertical="vertical"
:tooltip-class="tooltipClass"
role="slider"
:aria-label="firstButtonLabel"
:aria-valuemin="min"
:aria-valuemax="range ? secondValue : max"
:aria-valuenow="firstValue"
:aria-valuetext="firstValueText"
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
:aria-disabled="sliderDisabled"
@update:model-value="setFirstValue"
/>
<slider-button
@ -32,6 +40,14 @@
:model-value="secondValue"
:vertical="vertical"
:tooltip-class="tooltipClass"
role="slider"
:aria-label="secondButtonLabel"
:aria-valuemin="firstValue"
:aria-valuemax="max"
:aria-valuenow="secondValue"
:aria-valuetext="secondValueText"
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
:aria-disabled="sliderDisabled"
@update:model-value="setSecondValue"
/>
<div v-if="showStops">
@ -101,20 +117,17 @@ import {
import {
debugWarn,
isValidComponentSize,
off,
on,
throwError,
} from '@element-plus/utils'
import { useNamespace, useSize } from '@element-plus/hooks'
import { useLocale, useNamespace, useSize } from '@element-plus/hooks'
import SliderButton from './button.vue'
import SliderMarker from './marker.vue'
import { useMarks } from './useMarks'
import { useSlide } from './useSlide'
import { useStops } from './useStops'
import type { PropType, Ref } from 'vue'
import type { PropType } from 'vue'
import type { ComponentSize } from '@element-plus/constants'
import type { Nullable } from '@element-plus/utils'
export default defineComponent({
name: 'ElSlider',
@ -194,6 +207,18 @@ export default defineComponent({
type: String,
default: undefined,
},
rangeStartLabel: {
type: String,
default: undefined,
},
rangeEndLabel: {
type: String,
default: undefined,
},
formatValueText: {
type: Function as PropType<(val: number) => string>,
default: undefined,
},
tooltipClass: {
type: String,
default: undefined,
@ -205,6 +230,7 @@ export default defineComponent({
setup(props, { emit }) {
const ns = useNamespace('slider')
const { t } = useLocale()
const initData = reactive({
firstValue: 0,
secondValue: 0,
@ -225,7 +251,9 @@ export default defineComponent({
barStyle,
resetSize,
emitChange,
onSliderWrapperPrevent,
onSliderClick,
onSliderDown,
setFirstValue,
setSecondValue,
} = useSlide(props, initData, emit)
@ -242,6 +270,40 @@ export default defineComponent({
() => props.inputSize || sliderWrapperSize.value
)
const groupLabel = computed<string>(() => {
return (
props.label ||
t('el.slider.defaultLabel', {
min: props.min,
max: props.max,
})
)
})
const firstButtonLabel = computed<string>(() => {
if (props.range) {
return props.rangeStartLabel || t('el.slider.defaultRangeStartLabel')
} else {
return groupLabel.value
}
})
const firstValueText = computed<string>(() => {
return props.formatValueText
? props.formatValueText(firstValue.value)
: `${firstValue.value}`
})
const secondButtonLabel = computed<string>(() => {
return props.rangeEndLabel || t('el.slider.defaultRangeEndLabel')
})
const secondValueText = computed<string>(() => {
return props.formatValueText
? props.formatValueText(secondValue.value)
: `${secondValue.value}`
})
const sliderKls = computed(() => [
ns.b(),
ns.m(sliderWrapperSize.value),
@ -289,13 +351,20 @@ export default defineComponent({
sliderSize,
slider,
groupLabel,
firstButton,
firstButtonLabel,
firstValueText,
secondButton,
secondButtonLabel,
secondValueText,
sliderDisabled,
runwayStyle,
barStyle,
emitChange,
onSliderClick,
onSliderWrapperPrevent,
onSliderDown,
getStopStyle,
setFirstValue,
setSecondValue,
@ -405,10 +474,9 @@ const useWatch = (props, initData, minValue, maxValue, emit, elFormItem) => {
}
const useLifecycle = (props, initData, resetSize) => {
const sliderWrapper: Ref<Nullable<HTMLElement>> = ref(null)
const sliderWrapper = ref<HTMLElement>()
onMounted(async () => {
let valuetext
if (props.range) {
if (Array.isArray(props.modelValue)) {
initData.firstValue = Math.max(props.min, props.modelValue[0])
@ -418,7 +486,6 @@ const useLifecycle = (props, initData, resetSize) => {
initData.secondValue = props.max
}
initData.oldValue = [initData.firstValue, initData.secondValue]
valuetext = `${initData.firstValue}-${initData.secondValue}`
} else {
if (
typeof props.modelValue !== 'number' ||
@ -432,25 +499,16 @@ const useLifecycle = (props, initData, resetSize) => {
)
}
initData.oldValue = initData.firstValue
valuetext = initData.firstValue
}
sliderWrapper.value.setAttribute('aria-valuetext', valuetext)
// label screen reader
sliderWrapper.value.setAttribute(
'aria-label',
props.label ? props.label : `slider between ${props.min} and ${props.max}`
)
on(window, 'resize', resetSize)
window.addEventListener('resize', resetSize)
await nextTick()
resetSize()
})
onBeforeUnmount(() => {
off(window, 'resize', resetSize)
window.removeEventListener('resize', resetSize)
})
return {

View File

@ -1,5 +1,9 @@
import type { CSSProperties, ComputedRef, Ref } from 'vue'
import type { Nullable } from '@element-plus/utils'
import type {
CSSProperties,
ComponentPublicInstance,
ComputedRef,
Ref,
} from 'vue'
export interface ISliderProps {
modelValue: number | number[]
@ -18,14 +22,17 @@ export interface ISliderProps {
height: string
debounce: number
label: string
rangeStartLabel: string
rangeEndLabel: string
formatValueText: (val: number) => string
tooltipClass: string
marks?: Record<number, any>
}
export interface ISliderInitData {
firstValue: Nullable<number>
secondValue: Nullable<number>
oldValue: Nullable<number>
firstValue: number
secondValue: number
oldValue?: number
dragging: boolean
sliderSize: number
}
@ -73,7 +80,7 @@ export type Slide = {
}
export type ButtonRefs = {
[s in 'firstButton' | 'secondButton']: Ref<Nullable<ISliderButton>>
[s in 'firstButton' | 'secondButton']: Ref<ISliderButton | undefined>
}
export interface ISliderButtonProps {
@ -95,15 +102,15 @@ export interface ISliderButtonInitData {
oldValue: number
}
export interface ISliderButton {
tooltip: Ref<Nullable<HTMLElement>>
export interface ISliderButton extends ComponentPublicInstance {
tooltip: Ref<HTMLElement | undefined>
showTooltip: Ref<boolean>
wrapperStyle: ComputedRef<CSSProperties>
formatValue: ComputedRef<number | string>
dragging: boolean
handleMouseEnter: () => void
handleMouseLeave: () => void
onButtonDown: (event: MouseEvent | TouchEvent) => void
onLeftKeyDown: () => void
onRightKeyDown: () => void
onKeyDown: (event: KeyboardEvent) => void
setPosition: (newPosition: number) => void
}

View File

@ -5,24 +5,28 @@ import {
UPDATE_MODEL_EVENT,
} from '@element-plus/constants'
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
import type { CSSProperties } from 'vue'
import type { ButtonRefs, ISliderInitData, ISliderProps } from './slider.type'
import type { CSSProperties, ComponentInternalInstance, Ref } from 'vue'
import type {
ButtonRefs,
ISliderButton,
ISliderInitData,
ISliderProps,
} from './slider.type'
import type { FormContext, FormItemContext } from '@element-plus/tokens'
import type { Nullable } from '@element-plus/utils'
export const useSlide = (
props: ISliderProps,
initData: ISliderInitData,
emit
emit: ComponentInternalInstance['emit']
) => {
const elForm = inject(formContextKey, {} as FormContext)
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
const slider = shallowRef<Nullable<HTMLElement>>(null)
const slider = shallowRef<HTMLElement>()
const firstButton = ref(null)
const firstButton = ref<ISliderButton>()
const secondButton = ref(null)
const secondButton = ref<ISliderButton>()
const buttonRefs: ButtonRefs = {
firstButton,
@ -80,13 +84,14 @@ export const useSlide = (
}
}
const setPosition = (percent: number) => {
const getButtonRefByPercent = (
percent: number
): Ref<ISliderButton | undefined> => {
const targetValue = props.min + (percent * (props.max - props.min)) / 100
if (!props.range) {
firstButton.value.setPosition(percent)
return
return firstButton
}
let buttonRefName: string
let buttonRefName: 'firstButton' | 'secondButton'
if (
Math.abs(minValue.value - targetValue) <
Math.abs(maxValue.value - targetValue)
@ -101,7 +106,13 @@ export const useSlide = (
? 'firstButton'
: 'secondButton'
}
buttonRefs[buttonRefName].value.setPosition(percent)
return buttonRefs[buttonRefName]
}
const setPosition = (percent: number): Ref<ISliderButton | undefined> => {
const buttonRef = getButtonRefByPercent(percent)
buttonRef.value!.setPosition(percent)
return buttonRef
}
const setFirstValue = (firstValue: number) => {
@ -130,21 +141,51 @@ export const useSlide = (
)
}
const onSliderClick = (event: MouseEvent) => {
const handleSliderPointerEvent = (
event: MouseEvent | TouchEvent
): Ref<ISliderButton | undefined> | undefined => {
if (sliderDisabled.value || initData.dragging) return
resetSize()
let newPercent = 0
if (props.vertical) {
const sliderOffsetBottom = slider.value.getBoundingClientRect().bottom
setPosition(
((sliderOffsetBottom - event.clientY) / initData.sliderSize) * 100
)
const clientY =
(event as TouchEvent).touches?.item(0)?.clientY ??
(event as MouseEvent).clientY
const sliderOffsetBottom = slider.value!.getBoundingClientRect().bottom
newPercent = ((sliderOffsetBottom - clientY) / initData.sliderSize) * 100
} else {
const sliderOffsetLeft = slider.value.getBoundingClientRect().left
setPosition(
((event.clientX - sliderOffsetLeft) / initData.sliderSize) * 100
)
const clientX =
(event as TouchEvent).touches?.item(0)?.clientX ??
(event as MouseEvent).clientX
const sliderOffsetLeft = slider.value!.getBoundingClientRect().left
newPercent = ((clientX - sliderOffsetLeft) / initData.sliderSize) * 100
}
if (newPercent < 0 || newPercent > 100) return
return setPosition(newPercent)
}
const onSliderWrapperPrevent = (event: TouchEvent) => {
if (
buttonRefs['firstButton'].value?.dragging ||
buttonRefs['secondButton'].value?.dragging
) {
event.preventDefault()
}
}
const onSliderDown = async (event: MouseEvent | TouchEvent) => {
const buttonRef = handleSliderPointerEvent(event)
if (buttonRef) {
await nextTick()
buttonRef.value!.onButtonDown(event)
}
}
const onSliderClick = (event: MouseEvent | TouchEvent) => {
const buttonRef = handleSliderPointerEvent(event)
if (buttonRef) {
emitChange()
}
emitChange()
}
return {
@ -160,7 +201,9 @@ export const useSlide = (
resetSize,
setPosition,
emitChange,
onSliderWrapperPrevent,
onSliderClick,
onSliderDown,
setFirstValue,
setSecondValue,
}

View File

@ -1,21 +1,22 @@
import { computed, inject, nextTick, ref, watch } from 'vue'
import { debounce } from 'lodash-unified'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { off, on } from '@element-plus/utils'
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import type { CSSProperties, ComputedRef } from 'vue'
import type { CSSProperties, ComponentInternalInstance, ComputedRef } from 'vue'
import type {
ISliderButtonInitData,
ISliderButtonProps,
ISliderProvider,
} from './slider.type'
const { left, down, right, up, home, end, pageUp, pageDown } = EVENT_CODE
const useTooltip = (
props: ISliderButtonProps,
formatTooltip: ComputedRef<(value: number) => number | string>,
showTooltip: ComputedRef<boolean>
) => {
const tooltip = ref(null)
const tooltip = ref<any>()
const tooltipVisible = ref(false)
@ -50,7 +51,7 @@ const useTooltip = (
export const useSliderButton = (
props: ISliderButtonProps,
initData: ISliderButtonInitData,
emit
emit: ComponentInternalInstance['emit']
) => {
const {
disabled,
@ -64,11 +65,13 @@ export const useSliderButton = (
emitChange,
resetSize,
updateDragging,
} = inject<ISliderProvider>('SliderProvider')
} = inject<ISliderProvider>('SliderProvider')!
const { tooltip, tooltipVisible, formatValue, displayTooltip, hideTooltip } =
useTooltip(props, formatTooltip, showTooltip)
const button = ref<any>()
const currentPosition = computed(() => {
return `${
((props.modelValue - min.value) / (max.value - min.value)) * 100
@ -99,31 +102,71 @@ export const useSliderButton = (
if (disabled.value) return
event.preventDefault()
onDragStart(event)
on(window, 'mousemove', onDragging)
on(window, 'touchmove', onDragging)
on(window, 'mouseup', onDragEnd)
on(window, 'touchend', onDragEnd)
on(window, 'contextmenu', onDragEnd)
window.addEventListener('mousemove', onDragging)
window.addEventListener('touchmove', onDragging)
window.addEventListener('mouseup', onDragEnd)
window.addEventListener('touchend', onDragEnd)
window.addEventListener('contextmenu', onDragEnd)
button.value.focus()
}
const onLeftKeyDown = () => {
if (disabled.value) return
initData.newPosition =
Number.parseFloat(currentPosition.value) -
(step.value / (max.value - min.value)) * 100
setPosition(initData.newPosition)
emitChange()
}
const onRightKeyDown = () => {
const incrementPosition = (amount: number) => {
if (disabled.value) return
initData.newPosition =
Number.parseFloat(currentPosition.value) +
(step.value / (max.value - min.value)) * 100
(amount / (max.value - min.value)) * 100
setPosition(initData.newPosition)
emitChange()
}
const onLeftKeyDown = () => {
incrementPosition(-step.value)
}
const onRightKeyDown = () => {
incrementPosition(step.value)
}
const onPageDownKeyDown = () => {
incrementPosition(-step.value * 4)
}
const onPageUpKeyDown = () => {
incrementPosition(step.value * 4)
}
const onHomeKeyDown = () => {
if (disabled.value) return
setPosition(0)
emitChange()
}
const onEndKeyDown = () => {
if (disabled.value) return
setPosition(100)
emitChange()
}
const onKeyDown = (event: KeyboardEvent) => {
let isPreventDefault = true
if ([left, down].includes(event.key)) {
onLeftKeyDown()
} else if ([right, up].includes(event.key)) {
onRightKeyDown()
} else if (event.key === home) {
onHomeKeyDown()
} else if (event.key === end) {
onEndKeyDown()
} else if (event.key === pageDown) {
onPageDownKeyDown()
} else if (event.key === pageUp) {
onPageUpKeyDown()
} else {
isPreventDefault = false
}
isPreventDefault && event.preventDefault()
}
const getClientXY = (event: MouseEvent | TouchEvent) => {
let clientX: number
let clientY: number
@ -188,11 +231,11 @@ export const useSliderButton = (
emitChange()
}
}, 0)
off(window, 'mousemove', onDragging)
off(window, 'touchmove', onDragging)
off(window, 'mouseup', onDragEnd)
off(window, 'touchend', onDragEnd)
off(window, 'contextmenu', onDragEnd)
window.removeEventListener('mousemove', onDragging)
window.removeEventListener('touchmove', onDragging)
window.removeEventListener('mouseup', onDragEnd)
window.removeEventListener('touchend', onDragEnd)
window.removeEventListener('contextmenu', onDragEnd)
}
}
@ -227,6 +270,7 @@ export const useSliderButton = (
)
return {
button,
tooltip,
tooltipVisible,
showTooltip,
@ -235,8 +279,7 @@ export const useSliderButton = (
handleMouseEnter,
handleMouseLeave,
onButtonDown,
onLeftKeyDown,
onRightKeyDown,
onKeyDown,
setPosition,
}
}

View File

@ -91,6 +91,11 @@ export default {
preview: 'Preview',
continue: 'Continue',
},
slider: {
defaultLabel: 'slider between {min} and {max}',
defaultRangeStartLabel: 'pick start value',
defaultRangeEndLabel: 'pick end value',
},
table: {
emptyText: 'No Data',
confirmFilter: 'Confirm',