From 16989d818759e87d61e72e6852f41559635f9d71 Mon Sep 17 00:00:00 2001
From: opengraphica <57385187+opengraphica@users.noreply.github.com>
Date: Wed, 27 Apr 2022 09:38:47 -0400
Subject: [PATCH] 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
---
docs/en-US/component/slider.md | 3 +
.../slider/__tests__/slider.test.ts | 268 ++++++++++++------
packages/components/slider/src/button.vue | 13 +-
packages/components/slider/src/index.vue | 108 +++++--
packages/components/slider/src/slider.type.ts | 27 +-
packages/components/slider/src/useSlide.ts | 87 ++++--
.../components/slider/src/useSliderButton.ts | 101 +++++--
packages/locale/lang/en.ts | 5 +
8 files changed, 432 insertions(+), 180 deletions(-)
diff --git a/docs/en-US/component/slider.md b/docs/en-US/component/slider.md
index 986d035060..e10b84ebf4 100644
--- a/docs/en-US/component/slider.md
+++ b/docs/en-US/component/slider.md
@@ -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 | — | — |
diff --git a/packages/components/slider/__tests__/slider.test.ts b/packages/components/slider/__tests__/slider.test.ts
index 44bc7f7927..b83dc9620a 100644
--- a/packages/components/slider/__tests__/slider.test.ts
+++ b/packages/components/slider/__tests__/slider.test.ts
@@ -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: `
-
-
-
- `,
- 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: `
+
+
+
+ `,
+ 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: `
+
+
+
+ `,
+ 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: `
+
+
+
+ `,
+ 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: `
+
+
+
+ `,
+ 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: `
@@ -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: `
@@ -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: `
@@ -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: `
@@ -552,6 +654,7 @@ describe('Slider', () => {
})
it('show input', (done) => {
+ vi.useRealTimers()
const wrapper = mount({
template: `
@@ -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: `
@@ -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: `
@@ -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: `
@@ -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)
})
})
})
diff --git a/packages/components/slider/src/button.vue b/packages/components/slider/src/button.vue
index 2adab65425..ad1c812849 100644
--- a/packages/components/slider/src/button.vue
+++ b/packages/components/slider/src/button.vue
@@ -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"
>
@@ -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
(() => {
+ return (
+ props.label ||
+ t('el.slider.defaultLabel', {
+ min: props.min,
+ max: props.max,
+ })
+ )
+ })
+
+ const firstButtonLabel = computed(() => {
+ if (props.range) {
+ return props.rangeStartLabel || t('el.slider.defaultRangeStartLabel')
+ } else {
+ return groupLabel.value
+ }
+ })
+
+ const firstValueText = computed(() => {
+ return props.formatValueText
+ ? props.formatValueText(firstValue.value)
+ : `${firstValue.value}`
+ })
+
+ const secondButtonLabel = computed(() => {
+ return props.rangeEndLabel || t('el.slider.defaultRangeEndLabel')
+ })
+
+ const secondValueText = computed(() => {
+ 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> = ref(null)
+ const sliderWrapper = ref()
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 {
diff --git a/packages/components/slider/src/slider.type.ts b/packages/components/slider/src/slider.type.ts
index 76c4843213..74d13a1fd0 100644
--- a/packages/components/slider/src/slider.type.ts
+++ b/packages/components/slider/src/slider.type.ts
@@ -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
}
export interface ISliderInitData {
- firstValue: Nullable
- secondValue: Nullable
- oldValue: Nullable
+ 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>
+ [s in 'firstButton' | 'secondButton']: Ref
}
export interface ISliderButtonProps {
@@ -95,15 +102,15 @@ export interface ISliderButtonInitData {
oldValue: number
}
-export interface ISliderButton {
- tooltip: Ref>
+export interface ISliderButton extends ComponentPublicInstance {
+ tooltip: Ref
showTooltip: Ref
wrapperStyle: ComputedRef
formatValue: ComputedRef
+ dragging: boolean
handleMouseEnter: () => void
handleMouseLeave: () => void
onButtonDown: (event: MouseEvent | TouchEvent) => void
- onLeftKeyDown: () => void
- onRightKeyDown: () => void
+ onKeyDown: (event: KeyboardEvent) => void
setPosition: (newPosition: number) => void
}
diff --git a/packages/components/slider/src/useSlide.ts b/packages/components/slider/src/useSlide.ts
index e541eec3c9..1ec879c972 100644
--- a/packages/components/slider/src/useSlide.ts
+++ b/packages/components/slider/src/useSlide.ts
@@ -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>(null)
+ const slider = shallowRef()
- const firstButton = ref(null)
+ const firstButton = ref()
- const secondButton = ref(null)
+ const secondButton = ref()
const buttonRefs: ButtonRefs = {
firstButton,
@@ -80,13 +84,14 @@ export const useSlide = (
}
}
- const setPosition = (percent: number) => {
+ const getButtonRefByPercent = (
+ percent: number
+ ): Ref => {
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 => {
+ 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 | 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,
}
diff --git a/packages/components/slider/src/useSliderButton.ts b/packages/components/slider/src/useSliderButton.ts
index e91c87e742..114339d683 100644
--- a/packages/components/slider/src/useSliderButton.ts
+++ b/packages/components/slider/src/useSliderButton.ts
@@ -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
) => {
- const tooltip = ref(null)
+ const tooltip = ref()
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('SliderProvider')
+ } = inject('SliderProvider')!
const { tooltip, tooltipVisible, formatValue, displayTooltip, hideTooltip } =
useTooltip(props, formatTooltip, showTooltip)
+ const button = ref()
+
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,
}
}
diff --git a/packages/locale/lang/en.ts b/packages/locale/lang/en.ts
index d85760f4df..0590bf67a2 100644
--- a/packages/locale/lang/en.ts
+++ b/packages/locale/lang/en.ts
@@ -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',