diff --git a/packages/address-edit/test/__snapshots__/index.spec.js.snap b/packages/address-edit/test/__snapshots__/index.spec.js.snap
index 9623de848..6e7aa9c0e 100644
--- a/packages/address-edit/test/__snapshots__/index.spec.js.snap
+++ b/packages/address-edit/test/__snapshots__/index.spec.js.snap
@@ -40,14 +40,15 @@ exports[`create a AddressEdit 1`] = `
@@ -109,14 +110,15 @@ exports[`create a AddressEdit with props 1`] = `
diff --git a/packages/area/test/__snapshots__/demo.spec.js.snap b/packages/area/test/__snapshots__/demo.spec.js.snap
index 840df29d2..2b7e32edb 100644
--- a/packages/area/test/__snapshots__/demo.spec.js.snap
+++ b/packages/area/test/__snapshots__/demo.spec.js.snap
@@ -11,69 +11,70 @@ exports[`renders demo correctly 1`] = `
-
- - 北京市
- - 天津市
- - 河北省
- - 山西省
- - 内蒙古自治区
- - 辽宁省
- - 吉林省
- - 黑龙江省
- - 上海市
- - 江苏省
- - 浙江省
- - 安徽省
- - 福建省
- - 江西省
- - 山东省
- - 河南省
- - 湖北省
- - 湖南省
- - 广东省
- - 广西壮族自治区
- - 海南省
- - 重庆市
- - 四川省
- - 贵州省
- - 云南省
- - 西藏自治区
- - 陕西省
- - 甘肃省
- - 青海省
- - 宁夏回族自治区
- - 新疆维吾尔自治区
- - 台湾省
- - 香港特别行政区
- - 澳门特别行政区
- - 海外
+
+ - 北京市
+ - 天津市
+ - 河北省
+ - 山西省
+ - 内蒙古自治区
+ - 辽宁省
+ - 吉林省
+ - 黑龙江省
+ - 上海市
+ - 江苏省
+ - 浙江省
+ - 安徽省
+ - 福建省
+ - 江西省
+ - 山东省
+ - 河南省
+ - 湖北省
+ - 湖南省
+ - 广东省
+ - 广西壮族自治区
+ - 海南省
+ - 重庆市
+ - 四川省
+ - 贵州省
+ - 云南省
+ - 西藏自治区
+ - 陕西省
+ - 甘肃省
+ - 青海省
+ - 宁夏回族自治区
+ - 新疆维吾尔自治区
+ - 台湾省
+ - 香港特别行政区
+ - 澳门特别行政区
+ - 海外
-
- - 东城区
- - 西城区
- - 朝阳区
- - 丰台区
- - 石景山区
- - 海淀区
- - 门头沟区
- - 房山区
- - 通州区
- - 顺义区
- - 昌平区
- - 大兴区
- - 怀柔区
- - 平谷区
- - 密云区
- - 延庆区
+
+ - 东城区
+ - 西城区
+ - 朝阳区
+ - 丰台区
+ - 石景山区
+ - 海淀区
+ - 门头沟区
+ - 房山区
+ - 通州区
+ - 顺义区
+ - 昌平区
+ - 大兴区
+ - 怀柔区
+ - 平谷区
+ - 密云区
+ - 延庆区
+
@@ -87,74 +88,75 @@ exports[`renders demo correctly 1`] = `
@@ -169,49 +171,50 @@ exports[`renders demo correctly 1`] = `
-
- - 北京市
- - 天津市
- - 河北省
- - 山西省
- - 内蒙古自治区
- - 辽宁省
- - 吉林省
- - 黑龙江省
- - 上海市
- - 江苏省
- - 浙江省
- - 安徽省
- - 福建省
- - 江西省
- - 山东省
- - 河南省
- - 湖北省
- - 湖南省
- - 广东省
- - 广西壮族自治区
- - 海南省
- - 重庆市
- - 四川省
- - 贵州省
- - 云南省
- - 西藏自治区
- - 陕西省
- - 甘肃省
- - 青海省
- - 宁夏回族自治区
- - 新疆维吾尔自治区
- - 台湾省
- - 香港特别行政区
- - 澳门特别行政区
- - 海外
+
+ - 北京市
+ - 天津市
+ - 河北省
+ - 山西省
+ - 内蒙古自治区
+ - 辽宁省
+ - 吉林省
+ - 黑龙江省
+ - 上海市
+ - 江苏省
+ - 浙江省
+ - 安徽省
+ - 福建省
+ - 江西省
+ - 山东省
+ - 河南省
+ - 湖北省
+ - 湖南省
+ - 广东省
+ - 广西壮族自治区
+ - 海南省
+ - 重庆市
+ - 四川省
+ - 贵州省
+ - 云南省
+ - 西藏自治区
+ - 陕西省
+ - 甘肃省
+ - 青海省
+ - 宁夏回族自治区
+ - 新疆维吾尔自治区
+ - 台湾省
+ - 香港特别行政区
+ - 澳门特别行政区
+ - 海外
+
diff --git a/packages/area/test/__snapshots__/index.spec.js.snap b/packages/area/test/__snapshots__/index.spec.js.snap
index 1ebee1c5b..6fa0b5ee5 100644
--- a/packages/area/test/__snapshots__/index.spec.js.snap
+++ b/packages/area/test/__snapshots__/index.spec.js.snap
@@ -9,23 +9,24 @@ exports[`change option 1`] = `
@@ -40,23 +41,24 @@ exports[`change option 2`] = `
@@ -71,23 +73,24 @@ exports[`change option 3`] = `
@@ -102,22 +105,23 @@ exports[`reset method 1`] = `
@@ -132,23 +136,24 @@ exports[`reset method 2`] = `
@@ -163,23 +168,24 @@ exports[`watch areaList & code 1`] = `
@@ -194,23 +200,24 @@ exports[`watch areaList & code 2`] = `
@@ -225,23 +232,24 @@ exports[`watch areaList & code 3`] = `
diff --git a/packages/area/test/index.spec.js b/packages/area/test/index.spec.js
index 052f4b8d8..229fddbeb 100644
--- a/packages/area/test/index.spec.js
+++ b/packages/area/test/index.spec.js
@@ -68,10 +68,15 @@ test('change option', () => {
const columns = wrapper.findAll('.van-picker-column');
expect(wrapper).toMatchSnapshot();
+
triggerDrag(columns.at(0), 0, -100);
+ columns.at(0).find('ul').trigger('transitionend');
expect(wrapper).toMatchSnapshot();
+
triggerDrag(columns.at(2), 0, -100);
+ columns.at(2).find('ul').trigger('transitionend');
expect(wrapper).toMatchSnapshot();
+
expect(onChange.mock.calls[0][1]).toEqual(secondOption);
});
diff --git a/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap b/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap
index a7fd73288..162ea47d2 100644
--- a/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap
+++ b/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap
@@ -11,154 +11,155 @@ exports[`renders demo correctly 1`] = `
@@ -172,72 +173,73 @@ exports[`renders demo correctly 1`] = `
@@ -251,37 +253,38 @@ exports[`renders demo correctly 1`] = `
-
- - 2018年
- - 2019年
- - 2020年
- - 2021年
- - 2022年
- - 2023年
- - 2024年
- - 2025年
- - 2026年
- - 2027年
- - 2028年
- - 2029年
+
+ - 2018年
+ - 2019年
+ - 2020年
+ - 2021年
+ - 2022年
+ - 2023年
+ - 2024年
+ - 2025年
+ - 2026年
+ - 2027年
+ - 2028年
+ - 2029年
-
- - 01月
- - 02月
- - 03月
- - 04月
- - 05月
- - 06月
- - 07月
- - 08月
- - 09月
- - 10月
- - 11月
- - 12月
+
+ - 01月
+ - 02月
+ - 03月
+ - 04月
+ - 05月
+ - 06月
+ - 07月
+ - 08月
+ - 09月
+ - 10月
+ - 11月
+ - 12月
+
@@ -295,84 +298,85 @@ exports[`renders demo correctly 1`] = `
-
- - 10
- - 11
- - 12
- - 13
- - 14
- - 15
- - 16
- - 17
- - 18
- - 19
- - 20
+
+ - 10
+ - 11
+ - 12
+ - 13
+ - 14
+ - 15
+ - 16
+ - 17
+ - 18
+ - 19
+ - 20
+
diff --git a/packages/picker/PickerColumn.js b/packages/picker/PickerColumn.js
index 382482f4a..d427d54d6 100644
--- a/packages/picker/PickerColumn.js
+++ b/packages/picker/PickerColumn.js
@@ -1,11 +1,22 @@
import { deepClone } from '../utils/deep-clone';
import { use, isObj, range } from '../utils';
import { preventDefault } from '../utils/event';
+import { TouchMixin } from '../mixins/touch';
const DEFAULT_DURATION = 200;
+
+// 惯性滑动思路:
+// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move 距
+// 离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动,持续 `MOMENTUM_DURATION`
+const MOMENTUM_DURATION = 1500;
+const MOMENTUM_LIMIT_TIME = 300;
+const MOMENTUM_LIMIT_DISTANCE = 15;
+
const [sfc, bem] = use('picker-column');
export default sfc({
+ mixins: [TouchMixin],
+
props: {
valueKey: String,
className: String,
@@ -17,10 +28,12 @@ export default sfc({
data() {
return {
- startY: 0,
offset: 0,
duration: 0,
startOffset: 0,
+ momentumOffset: 0,
+ touchTimestamp: 0,
+ moving: false,
options: deepClone(this.initialOptions),
currentIndex: this.defaultIndex
};
@@ -50,29 +63,73 @@ export default sfc({
methods: {
onTouchStart(event) {
- this.startY = event.touches[0].clientY;
- this.startOffset = this.offset;
+ this.touchStart(event);
+
+ if (this.moving) {
+ const { translateY } = this.getEleTransform(this.$refs.wrapper);
+ this.startOffset = Math.min(0, translateY);
+ } else {
+ this.startOffset = this.offset;
+ }
+
this.duration = 0;
+ this.moving = false;
+ this.transitionEndTrigger = null;
+ this.touchTimestamp = Date.now();
+ this.momentumOffset = this.startOffset;
},
onTouchMove(event) {
preventDefault(event);
- const deltaY = event.touches[0].clientY - this.startY;
+ this.moving = true;
+ this.touchMove(event);
this.offset = range(
- this.startOffset + deltaY,
+ this.startOffset + this.deltaY,
-(this.count * this.itemHeight),
this.itemHeight
);
+
+ const now = Date.now();
+ if (now - this.touchTimestamp > MOMENTUM_LIMIT_TIME) {
+ this.touchTimestamp = now;
+ this.momentumOffset = this.offset;
+ }
},
onTouchEnd() {
+ const distance = this.offset - this.momentumOffset;
+ const duration = Date.now() - this.touchTimestamp;
+ const allowMomentum =
+ duration < MOMENTUM_LIMIT_TIME &&
+ Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
+
+ if (allowMomentum) {
+ this.momentum(distance, duration);
+ return;
+ }
+
if (this.offset !== this.startOffset) {
this.duration = DEFAULT_DURATION;
- const index = range(Math.round(-this.offset / this.itemHeight), 0, this.count - 1);
+ const index = this.getIndexByOffset(this.offset);
this.setIndex(index, true);
}
},
+ onTransitionEnd() {
+ this.moving = false;
+
+ if (this.transitionEndTrigger) {
+ this.transitionEndTrigger();
+ this.transitionEndTrigger = null;
+ }
+ },
+
+ onClickItem(e) {
+ const index = Number(e.currentTarget.getAttribute('data-index'));
+ this.duration = DEFAULT_DURATION;
+ this.setIndex(index, true);
+ },
+
adjustIndex(index) {
index = range(index, 0, this.count);
for (let i = index; i < this.count; i++) {
@@ -95,9 +152,19 @@ export default sfc({
index = this.adjustIndex(index) || 0;
this.offset = -index * this.itemHeight;
- if (index !== this.currentIndex) {
- this.currentIndex = index;
- userAction && this.$emit('change', index);
+ const trigger = () => {
+ if (index !== this.currentIndex) {
+ this.currentIndex = index;
+ userAction && this.$emit('change', index);
+ }
+ };
+
+ // 若有触发过 `touchmove` 事件,那应该
+ // 在 `transitionend` 后再触发 `change` 事件
+ if (this.moving) {
+ this.transitionEndTrigger = trigger;
+ } else {
+ trigger();
}
},
@@ -112,7 +179,43 @@ export default sfc({
getValue() {
return this.options[this.currentIndex];
- }
+ },
+
+ getIndexByOffset(offset) {
+ return range(
+ Math.round(-offset / this.itemHeight),
+ 0,
+ this.count - 1
+ );
+ },
+
+ getEleTransform(ele) {
+ const { transform } = window.getComputedStyle(ele);
+ const matrix = transform
+ .slice(7, transform.length - 1)
+ .split(', ')
+ .map(val => Number(val));
+
+ return {
+ scaleX: matrix[0],
+ skewY: matrix[1],
+ skewX: matrix[2],
+ scaleY: matrix[3],
+ translateX: matrix[4],
+ translateY: matrix[5]
+ };
+ },
+
+ momentum(distance, duration) {
+ const speed = Math.abs(distance / duration);
+
+ distance = this.offset + speed / 0.0015 * (distance < 0 ? -1 : 1);
+
+ const index = this.getIndexByOffset(distance);
+
+ this.duration = MOMENTUM_DURATION;
+ this.setIndex(index, true);
+ },
},
render(h) {
@@ -125,8 +228,9 @@ export default sfc({
const baseOffset = (itemHeight * (visibleItemCount - 1)) / 2;
const wrapperStyle = {
- transition: `${this.duration}ms`,
transform: `translate3d(0, ${this.offset + baseOffset}px, 0)`,
+ transitionTimingFunction: 'cubic-bezier(0.23, 1, 0.32, 1)',
+ transitionDuration: `${this.duration}ms`,
lineHeight: `${itemHeight}px`
};
@@ -143,7 +247,11 @@ export default sfc({
onTouchend={this.onTouchEnd}
onTouchcancel={this.onTouchEnd}
>
-
+
{this.options.map((option, index) => (
- {
- this.setIndex(index, true);
- }}
+ data-index={index}
+ onClick={this.onClickItem}
/>
))}
diff --git a/packages/picker/index.js b/packages/picker/index.js
index def4258e6..0fe86c795 100644
--- a/packages/picker/index.js
+++ b/packages/picker/index.js
@@ -141,6 +141,7 @@ export default sfc({
render(h) {
const { itemHeight } = this;
+ const wrapHeight = itemHeight * this.visibleItemCount;
const columns = this.simple ? [this.columns] : this.columns;
const frameStyle = {
@@ -148,7 +149,11 @@ export default sfc({
};
const columnsStyle = {
- height: `${itemHeight * this.visibleItemCount}px`
+ height: `${wrapHeight}px`
+ };
+
+ const maskStyle = {
+ backgroundSize: `100% ${(wrapHeight - itemHeight) / 2}px`
};
const Toolbar = this.showToolbar && (
@@ -186,6 +191,7 @@ export default sfc({
}}
/>
))}
+
diff --git a/packages/picker/index.less b/packages/picker/index.less
index c6da47a2f..aa04139e6 100644
--- a/packages/picker/index.less
+++ b/packages/picker/index.less
@@ -55,12 +55,26 @@
position: absolute;
top: 50%;
left: 0;
- z-index: 1;
+ z-index: 3;
width: 100%;
transform: translateY(-50%);
pointer-events: none;
}
+ &__mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ background-image: linear-gradient(180deg, hsla(0, 0%, 100%, .8), hsla(0, 0%, 100%, .6)),
+ linear-gradient(0deg, hsla(0, 0%, 100%, .8), hsla(0, 0%, 100%, .6));
+ background-repeat: no-repeat;
+ background-position: top, bottom;
+ pointer-events: none;
+ }
+
&-column {
flex: 1;
overflow: hidden;
@@ -69,10 +83,9 @@
&__item {
padding: 0 5px;
- color: @picker-option-text-color;
+ color: @picker-option-selected-text-color;
&--selected {
- color: @picker-option-selected-text-color;
font-weight: 500;
}
diff --git a/packages/picker/test/__snapshots__/demo.spec.js.snap b/packages/picker/test/__snapshots__/demo.spec.js.snap
index 7d5c821d3..17109cabd 100644
--- a/packages/picker/test/__snapshots__/demo.spec.js.snap
+++ b/packages/picker/test/__snapshots__/demo.spec.js.snap
@@ -7,14 +7,15 @@ exports[`renders demo correctly 1`] = `
-
- - 杭州
- - 宁波
- - 温州
- - 嘉兴
- - 湖州
+
+
@@ -24,14 +25,15 @@ exports[`renders demo correctly 1`] = `
-
- - 杭州
- - 宁波
- - 温州
- - 嘉兴
- - 湖州
+
+
@@ -46,14 +48,15 @@ exports[`renders demo correctly 1`] = `
-
- - 杭州
- - 宁波
- - 温州
- - 嘉兴
- - 湖州
+
+
@@ -63,12 +66,13 @@ exports[`renders demo correctly 1`] = `
@@ -78,20 +82,21 @@ exports[`renders demo correctly 1`] = `
-
- - 杭州
- - 宁波
- - 温州
- - 嘉兴
- - 湖州
+
+
@@ -101,20 +106,21 @@ exports[`renders demo correctly 1`] = `
-
- - 杭州
- - 宁波
- - 温州
- - 嘉兴
- - 湖州
+
+
diff --git a/packages/picker/test/__snapshots__/index.spec.js.snap b/packages/picker/test/__snapshots__/index.spec.js.snap
index dac8778fb..d4d0fd849 100644
--- a/packages/picker/test/__snapshots__/index.spec.js.snap
+++ b/packages/picker/test/__snapshots__/index.spec.js.snap
@@ -2,28 +2,28 @@
exports[`column watch default index 1`] = `
-
- - 1
- - 1990
- - 1991
- - 1992
- - 1993
- - 1994
- - 1995
+
+ - 1
+ - 1990
+ - 1991
+ - 1992
+ - 1993
+ - 1994
+ - 1995
`;
exports[`column watch default index 2`] = `
-
- - 1
- - 1990
- - 1991
- - 1992
- - 1993
- - 1994
- - 1995
+
+ - 1
+ - 1990
+ - 1991
+ - 1992
+ - 1993
+ - 1994
+ - 1995
`;
@@ -35,6 +35,7 @@ exports[`render title slot 1`] = `
diff --git a/packages/picker/test/index.spec.js b/packages/picker/test/index.spec.js
index 6813813bc..7702aadc4 100644
--- a/packages/picker/test/index.spec.js
+++ b/packages/picker/test/index.spec.js
@@ -80,25 +80,32 @@ test('set picker values', () => {
expect(vm.getColumnValue(2)).toEqual(undefined);
});
-test('drag columns', () => {
+test('drag columns', async () => {
const wrapper = mount(Picker, {
propsData: {
columns
}
});
- triggerDrag(wrapper.find('.van-picker-column'), 0, 0);
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
+ wrapper.find('.van-picker-column ul').trigger('transitionend');
+
+ // 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
+ // 会得到一个很大的值,导致会滚动到且选中列表的最后一项
expect(wrapper.emitted('change')[0][1]).toEqual(['normal', '1990']);
});
-test('drag simple columns', () => {
+test('drag simple columns', async () => {
const wrapper = mount(Picker, {
propsData: {
columns: simpleColumn
}
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
- expect(wrapper.emitted('change')[0][1]).toEqual('1992');
+ wrapper.find('.van-picker-column ul').trigger('transitionend');
+
+ // 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
+ // 会得到一个很大的值,导致会滚动到且选中列表的最后一项
+ expect(wrapper.emitted('change')[0][1]).toEqual('1995');
});
test('column watch default index', async () => {
@@ -132,3 +139,45 @@ test('render title slot', () => {
expect(wrapper).toMatchSnapshot();
});
+
+test('simulation finger swipe again before transitionend', () => {
+ // 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, -5)'
+ };
+ };
+
+ const wrapper = mount(Picker, {
+ propsData: {
+ columns: simpleColumn
+ }
+ });
+
+ triggerDrag(wrapper.find('.van-picker-column'), 0, -5);
+ triggerDrag(wrapper.find('.van-picker-column'), -5, -100);
+ wrapper.find('.van-picker-column ul').trigger('transitionend');
+ expect(wrapper.emitted('change')[0][1]).toEqual('1995');
+});
+
+test('click column\'s item', () => {
+ const columns = [
+ { text: '杭州' },
+ { text: '宁波' },
+ { text: '温州', disabled: true },
+ { text: '嘉兴', disabled: true }
+ ];
+ const wrapper = mount(Picker, {
+ propsData: {
+ columns
+ }
+ });
+
+ wrapper.findAll('.van-picker-column__item').at(3).trigger('click');
+ expect(wrapper.emitted('change')[0][1]).toEqual(columns[1]);
+});