Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): [slider] add mark-snap-percentage #16609

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en-US/component/slider.md
Expand Up @@ -117,6 +117,7 @@ slider/show-marks
| tooltip-class | custom class name for the tooltip | ^[string] | — |
| placement | position of Tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top |
| marks | marks, type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | ^[object]`SliderMarks` | — |
| mark-snap-percentage | while dragging slider, if within this slider percentage of a mark, snap to that mark. Requires `marks` | ^[number] | 0 |
| validate-event | whether to trigger form validation | ^[boolean] | true |

### Events
Expand Down
55 changes: 55 additions & 0 deletions docs/examples/slider/show-marks.vue
@@ -1,7 +1,40 @@
<template>
<div class="slider-demo-block">
<span class="demonstration">No snap</span>
<el-slider v-model="value" range :marks="marks" />
</div>
<div class="slider-demo-block">
<span class="demonstration">With 2% snap</span>
<el-slider
v-model="value2"
range
:marks="marks"
:mark-snap-percentage="2"
/>
</div>
<div class="slider-demo-block">
<span class="demonstration">With 5% snap</span>
<el-slider
v-model="value3"
range
:marks="marks"
:mark-snap-percentage="5"
/>
</div>
<div class="slider-demo-block">
<span class="demonstration">No snap</span>
<el-slider v-model="value4" :min="1000" :max="10000" :marks="marks4" />
</div>
<div class="slider-demo-block">
<span class="demonstration">With 2% snap</span>
<el-slider
v-model="value5"
:min="1000"
:max="10000"
:marks="marks4"
:mark-snap-percentage="2"
/>
</div>
</template>

<script lang="ts" setup>
Expand All @@ -16,6 +49,10 @@ interface Mark {
type Marks = Record<number, Mark | string>

const value = ref([30, 60])
const value2 = ref([30, 60])
const value3 = ref([30, 60])
const value4 = ref(6504)
const value5 = ref(6504)
const marks = reactive<Marks>({
0: '0°C',
8: '8°C',
Expand All @@ -27,16 +64,34 @@ const marks = reactive<Marks>({
label: '50%',
},
})
const marks4 = reactive<Marks>({
6504: '6504K',
})
</script>

<style scoped>
.slider-demo-block {
max-width: 600px;
display: flex;
align-items: center;
justify-content: space-between;
}
.slider-demo-block .el-slider {
margin-top: 0;
margin-left: 12px;
margin-bottom: 20px;
}
.slider-demo-block .demonstration {
font-size: 14px;
color: var(--el-text-color-secondary);
line-height: 44px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
.slider-demo-block .demonstration + .el-slider {
flex: 0 0 70%;
}
</style>
238 changes: 167 additions & 71 deletions packages/components/slider/__tests__/slider.test.tsx
Expand Up @@ -5,6 +5,7 @@ import { EVENT_CODE } from '@element-plus/constants'
import { ElFormItem } from '@element-plus/components/form'
import Slider from '../src/slider.vue'
import type { SliderProps } from '../src/slider'
import type { VueWrapper } from '@vue/test-utils'

vi.mock('lodash-unified', async () => {
return {
Expand All @@ -17,6 +18,60 @@ vi.mock('lodash-unified', async () => {
}
})

type DragState = { x: number; y: number; ctrlKey: boolean }

class MouseDragger {
state: DragState = { x: 0, y: 0, ctrlKey: false }

async down(triggerComponent: VueWrapper<any>, state: Partial<DragState>) {
Object.assign(this.state, { ctrlKey: false }, state)
triggerComponent.trigger('mousedown', {
clientX: this.state.x,
clientY: this.state.y,
ctrlKey: this.state.ctrlKey,
})
await nextTick()
}

async dragTo(state: Partial<DragState>) {
Object.assign(this.state, { ctrlKey: false }, state)
window.dispatchEvent(
new MouseEvent('mousemove', {
screenX: this.state.x,
screenY: this.state.y,
clientX: this.state.x,
clientY: this.state.y,
ctrlKey: this.state.ctrlKey,
})
)
await nextTick()
}

async done() {
window.dispatchEvent(
new MouseEvent('mouseup', {
screenX: this.state.x,
screenY: this.state.y,
clientX: this.state.x,
clientY: this.state.y,
ctrlKey: this.state.ctrlKey,
})
)
await nextTick()
}
}

async function mouseDrag(
triggerComponent: VueWrapper<any>,
from: Partial<DragState>,
to: Partial<DragState>
) {
const dragger = new MouseDragger()
await dragger.down(triggerComponent, from)
await dragger.dragTo(to)
await dragger.done()
}

describe('Slider', () => {
beforeEach(() => {
vi.useFakeTimers()
Expand Down Expand Up @@ -125,25 +180,8 @@ describe('Slider', () => {
'clientWidth',
'get'
).mockImplementation(() => 200)
slider.trigger('mousedown', { clientX: 0 })

const mousemove = new MouseEvent('mousemove', {
screenX: 100,
screenY: 0,
clientX: 100,
clientY: 0,
})
window.dispatchEvent(mousemove)

const mouseup = new MouseEvent('mouseup', {
screenX: 100,
screenY: 0,
clientX: 100,
clientY: 0,
})
window.dispatchEvent(mouseup)

await nextTick()
await mouseDrag(slider, { x: 0 }, { x: 100 })
expect(value.value === 50).toBeTruthy()
})

Expand All @@ -167,28 +205,120 @@ describe('Slider', () => {
'clientHeight',
'get'
).mockImplementation(() => 200)
slider.trigger('mousedown', { clientY: 0 })

const mousemove = new MouseEvent('mousemove', {
screenX: 0,
screenY: -100,
clientX: 0,
clientY: -100,
})
window.dispatchEvent(mousemove)

const mouseup = new MouseEvent('mouseup', {
screenX: 0,
screenY: -100,
clientX: 0,
clientY: -100,
})
window.dispatchEvent(mouseup)
await nextTick()
await mouseDrag(slider, { y: 0 }, { y: -100 })
expect(value.value).toBe(50)
})
})

it('marks percentage snapping (horizontal + vertical)', async () => {
vi.useRealTimers()
const value = ref(0)
const marks = ref({
40: '40',
70: '70',
})
const vertical = ref(false)
const markSnapPercentage = ref(0)
const wrapper = mount(
() => (
<div style="width: 200px;">
<Slider
v-model={value.value}
vertical={vertical.value}
marks={marks.value}
mark-snap-percentage={markSnapPercentage.value}
/>
</div>
),
{
attachTo: document.body,
}
)

const slider = wrapper.findComponent({ name: 'ElSliderButton' })

vi.spyOn(
wrapper.find('.el-slider__runway').element,
'clientWidth',
'get'
).mockImplementation(() => 200)
vi.spyOn(
wrapper.find('.el-slider__runway').element,
'clientHeight',
'get'
).mockImplementation(() => 200)

const directions: Array<[string, number, boolean]> = [
['x', 2, false],
['y', -2, true],
]
for (const [axis, mul, vert] of directions) {
vertical.value = vert
await nextTick()

// no snap
const dragger = new MouseDragger()
await dragger.down(slider, { [axis]: 0 })
expect(value.value).toBe(0)
await dragger.dragTo({ [axis]: mul * 39 })
expect(value.value).toBe(39)

// snap to 5%
markSnapPercentage.value = 5
await nextTick()

await dragger.dragTo({ [axis]: mul * 36 })
expect(value.value).toBe(40)
await dragger.dragTo({ [axis]: mul * 35 })
expect(value.value).toBe(40)
await dragger.dragTo({ [axis]: mul * 34 })
expect(value.value).toBe(34)
await dragger.dragTo({ [axis]: mul * 44 })
expect(value.value).toBe(40)

// ctrlKey disables snap
await dragger.dragTo({ [axis]: mul * 41, ctrlKey: true })
expect(value.value).toBe(41)
await dragger.dragTo({ [axis]: mul * 37, ctrlKey: true })
expect(value.value).toBe(37)

// snap to 2%
markSnapPercentage.value = 2
await nextTick()

await dragger.dragTo({ [axis]: mul * 64 })
expect(value.value).toBe(64)
await dragger.dragTo({ [axis]: mul * 65 })
expect(value.value).toBe(65)
await dragger.dragTo({ [axis]: mul * 68 })
expect(value.value).toBe(70)
await dragger.dragTo({ [axis]: mul * 69 })
expect(value.value).toBe(70)
await dragger.dragTo({ [axis]: mul * 72 })
expect(value.value).toBe(70)
await dragger.dragTo({ [axis]: mul * 73 })
expect(value.value).toBe(73)
await dragger.dragTo({ [axis]: mul * 75 })
expect(value.value).toBe(75)
await dragger.dragTo({ [axis]: mul * 71 })
expect(value.value).toBe(70)

// snap to negative = no snap
markSnapPercentage.value = -99
await nextTick()

await dragger.dragTo({ [axis]: mul * 39 })
expect(value.value).toBe(39)

// reset to zero
await dragger.dragTo({ [axis]: mul * 0 })
expect(value.value).toBe(0)
await dragger.done()
expect(value.value).toBe(0)
}
})

describe('accessibility', () => {
it('left/right arrows', async () => {
const value = ref(0)
Expand Down Expand Up @@ -285,25 +415,7 @@ describe('Slider', () => {
const slider = wrapper.findComponent({ name: 'ElSliderButton' })
await nextTick()

slider.trigger('mousedown', { clientX: 0 })

const mousemove = new MouseEvent('mousemove', {
screenX: 100,
screenY: 0,
clientX: 100,
clientY: 0,
})
window.dispatchEvent(mousemove)

const mouseup = new MouseEvent('mouseup', {
screenX: 100,
screenY: 0,
clientX: 100,
clientY: 0,
})
await nextTick()
window.dispatchEvent(mouseup)
await nextTick()
await mouseDrag(slider, { x: 0 }, { x: 100 })
expect(value.value === 0.5).toBeTruthy()
mockClientWidth.mockRestore()
})
Expand Down Expand Up @@ -399,24 +511,8 @@ describe('Slider', () => {
.spyOn(wrapper.find('.el-slider__runway').element, 'clientWidth', 'get')
.mockImplementation(() => 200)
const slider = wrapper.findComponent({ name: 'ElSliderButton' })
slider.vm.onButtonDown({ clientX: 0 })

const mousemove = new MouseEvent('mousemove', {
screenX: 50,
screenY: 0,
clientX: 50,
clientY: 0,
})
window.dispatchEvent(mousemove)

const mouseup = new MouseEvent('mouseup', {
screenX: 50,
screenY: 0,
clientX: 50,
clientY: 0,
})
window.dispatchEvent(mouseup)
await nextTick()
await mouseDrag(slider, { x: 0 }, { x: 50 })
expect(value.value).toBe(0)
mockClientWidth.mockRestore()
})
Expand Down