mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
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:
parent
dc98974db9
commit
16989d8187
@ -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 | — | — |
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user