From 14303f4ccb398008289272b5a3431a88e24e43dc Mon Sep 17 00:00:00 2001 From: genffy <1506972+genffy@users.noreply.github.com> Date: Tue, 2 Nov 2021 19:15:17 +0800 Subject: [PATCH] feat: support picker can scroll on desktop browser (#9713) * feat: support picker can scroll on desktop browser fix: change mousewheel event and fix pr review opt: optimize wheel on first item * chore: use deltaY directly * fix: direction and unit test Co-authored-by: genffy --- src/picker/PickerColumn.js | 55 +++++++++++++++++++++++++++-- src/picker/test/index.spec.js | 66 ++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/picker/PickerColumn.js b/src/picker/PickerColumn.js index 8534f5d31..bc2e65b9f 100644 --- a/src/picker/PickerColumn.js +++ b/src/picker/PickerColumn.js @@ -1,7 +1,7 @@ import { deepClone } from '../utils/deep-clone'; import { createNamespace, isObject } from '../utils'; import { range } from '../utils/format/number'; -import { preventDefault } from '../utils/dom/event'; +import { preventDefault, on, off } from '../utils/dom/event'; import { TouchMixin } from '../mixins/touch'; const DEFAULT_DURATION = 200; @@ -9,8 +9,8 @@ const DEFAULT_DURATION = 200; // 惯性滑动思路: // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move // 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动 -const MOMENTUM_LIMIT_TIME = 300; -const MOMENTUM_LIMIT_DISTANCE = 15; +export const MOMENTUM_LIMIT_TIME = 300; +export const MOMENTUM_LIMIT_DISTANCE = 15; const [createComponent, bem] = createNamespace('picker-column'); @@ -25,6 +25,10 @@ function getElementTranslateY(element) { function isOptionDisabled(option) { return isObject(option) && option.disabled; } +// use standard WheelEvent: +// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent +const supportMousewheel = 'onwheel' in window; +let mousewheelTimer = null; export default createComponent({ mixins: [TouchMixin], @@ -63,6 +67,9 @@ export default createComponent({ mounted() { this.bindTouchEvent(this.$el); + if (supportMousewheel) { + on(this.$el, 'wheel', this.onMouseWheel, false); + } }, destroyed() { @@ -71,6 +78,10 @@ export default createComponent({ if (children) { children.splice(children.indexOf(this), 1); } + + if (supportMousewheel) { + off(this.$el, 'wheel'); + } }, watch: { @@ -172,6 +183,44 @@ export default createComponent({ }, 0); }, + onMouseWheel(event) { + if (this.readonly) { + return; + } + preventDefault(event, true); + // simply combine touchstart and touchmove + const translateY = getElementTranslateY(this.$refs.wrapper); + this.startOffset = Math.min(0, translateY - this.baseOffset); + this.momentumOffset = this.startOffset; + this.transitionEndTrigger = null; + + // directly use deltaY, see https://caniuse.com/?search=deltaY + // use deltaY to detect direction for not special setting device + // https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event + const { deltaY } = event; + if (this.startOffset === 0 && deltaY < 0) { + return; + } + + // get offset + // if necessary, can adjust distance value to make scrolling smoother + const distance = -deltaY; + this.offset = range( + this.startOffset + distance, + -(this.count * this.itemHeight), + this.itemHeight + ); + + if (mousewheelTimer) { + clearTimeout(mousewheelTimer); + } + + mousewheelTimer = setTimeout(() => { + this.onTouchEnd(); + this.touchStartTime = 0; + }, MOMENTUM_LIMIT_TIME); + }, + onTransitionEnd() { this.stopMomentum(); }, diff --git a/src/picker/test/index.spec.js b/src/picker/test/index.spec.js index 9d9d97fb2..9d6164846 100644 --- a/src/picker/test/index.spec.js +++ b/src/picker/test/index.spec.js @@ -1,6 +1,7 @@ import Picker from '..'; -import PickerColumn from '../PickerColumn'; +import PickerColumn, { MOMENTUM_LIMIT_TIME, MOMENTUM_LIMIT_DISTANCE} from '../PickerColumn'; import { mount, triggerDrag, later } from '../../../test'; +import { DEFAULT_ITEM_HEIGHT } from '../shared'; const simpleColumn = ['1990', '1991', '1992', '1993', '1994', '1995']; const columns = [ @@ -336,3 +337,66 @@ test('readonly prop', () => { expect(wrapper.emitted('change')).toBeFalsy(); }); + +test('wheel event on columns is detected', async () => { + const onMouseWheel = jest.spyOn(PickerColumn.methods, 'onMouseWheel'); + + const wrapper = mount(PickerColumn, { + propsData: { + initialOptions: [...simpleColumn], + }, + }); + + await wrapper.trigger('wheel'); + + expect(onMouseWheel).toHaveBeenCalled(); +}); + +test('wheel scroll on columns', async () => { + const fakeScroll = (translateY, deltaY)=> { + // mock getComputedStyle + // see: https://github.com/jsdom/jsdom/issues/2588 + const originGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = (ele) => { + const style = originGetComputedStyle(ele); + return { + ...style, + transform: `matrix(1, 0, 0, 1, 0, ${translateY})`, + }; + }; + return new Promise(resolve => { + const wrapper = mount(Picker, { + propsData: { + columns: simpleColumn, + }, + }); + + wrapper.find('.van-picker-column').trigger('wheel', { + deltaY, + }); + + return later(MOMENTUM_LIMIT_TIME + 10).then(()=>{ + wrapper.find('.van-picker-column ul').trigger('transitionend'); + resolve(wrapper.emitted('change')); + }).finally(()=>{ + window.getComputedStyle = originGetComputedStyle; + }); + }); + } + + const topToDown = await fakeScroll(110, -MOMENTUM_LIMIT_DISTANCE); + expect(topToDown).toEqual(undefined); + + const topToUp = await fakeScroll(110, MOMENTUM_LIMIT_DISTANCE + 10); + expect(topToUp[0][1]).toEqual('1991'); + + const bottomToUp = await fakeScroll(-110, MOMENTUM_LIMIT_DISTANCE + 5); + expect(bottomToUp[0][1]).toEqual('1995'); + + const bottomToDown = await fakeScroll(-110, -(MOMENTUM_LIMIT_DISTANCE - 5)); + expect(bottomToDown[0][1]).toEqual('1995'); + + const pos1992 = simpleColumn.indexOf('1992') + const momentum = await fakeScroll(-110 + (pos1992 + 1) * DEFAULT_ITEM_HEIGHT, MOMENTUM_LIMIT_DISTANCE + 10); + expect(momentum[0][1]).toEqual(simpleColumn[pos1992+1]); +});