Skip to content

Commit

Permalink
feat(components): [slider] add mark-snap-percentage
Browse files Browse the repository at this point in the history
* while dragging slider; will snap value to the closest mark if value is within mark-snap-percentage (+/- in slider percent)
* Snap percentage defaults to zero
* can hold down control key while dragging to disable snapping behavior
* added additional show-marks examples
* generalized mouseDrag in slider tests
  • Loading branch information
adapt0 committed Apr 25, 2024
1 parent c46a21a commit e8b4abe
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 77 deletions.
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

0 comments on commit e8b4abe

Please sign in to comment.