diff --git a/src-next/slider/README.md b/src-next/slider/README.md new file mode 100644 index 000000000..0a342c50d --- /dev/null +++ b/src-next/slider/README.md @@ -0,0 +1,123 @@ +# Slider + +### Install + +```js +import Vue from 'vue'; +import { Slider } from 'vant'; + +Vue.use(Slider); +``` + +## Usage + +### Basic Usage + +```html + +``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + value: 50, + }; + }, + methods: { + onChange(value) { + Toast('Current value:' + value); + }, + }, +}; +``` + +### Range + +```html + +``` + +### Disabled + +```html + +``` + +### Step size + +```html + +``` + +### Custom style + +```html + +``` + +### Custom button + +```html + + + + + +``` + +### Vertical + +```html +
+ +
+``` + +## API + +### Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| value | Current value | _number_ | `0` | +| max | Max value | _number \| string_ | `100` | +| min | Min value | _number \| string_ | `0` | +| step | Step size | _number \| string_ | `1` | +| bar-height | Height of bar | _number \| string_ | `2px` | +| button-size `v2.4.5` | Button size | _number \| string_ | `24px` | +| active-color | Active color of bar | _string_ | `#1989fa` | +| inactive-color | Inactive color of bar | _string_ | `#e5e5e5` | +| disabled | Whether to disable slider | _boolean_ | `false` | +| vertical | Whether to display slider vertically | _boolean_ | `false` | + +### Events + +| Event | Description | Arguments | +| ---------- | ------------------------------------ | ------------------- | +| input | Instant triggered when value changed | value: current rate | +| change | Triggered after value changed | value: current rate | +| drag-start | Triggered when start drag | - | +| drag-end | Triggered when end drag | - | + +### Slots + +| Name | Description | +| ------ | ------------- | +| button | Custom button | diff --git a/src-next/slider/README.zh-CN.md b/src-next/slider/README.zh-CN.md new file mode 100644 index 000000000..99c43e4eb --- /dev/null +++ b/src-next/slider/README.zh-CN.md @@ -0,0 +1,125 @@ +# Slider 滑块 + +### 引入 + +```js +import Vue from 'vue'; +import { Slider } from 'vant'; + +Vue.use(Slider); +``` + +## 代码演示 + +### 基础用法 + +```html + +``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + value: 50, + }; + }, + methods: { + onChange(value) { + Toast('当前值:' + value); + }, + }, +}; +``` + +### 指定选择范围 + +```html + +``` + +### 禁用 + +```html + +``` + +### 指定步长 + +```html + +``` + +### 自定义样式 + +```html + +``` + +### 自定义按钮 + +```html + + + + + +``` + +### 垂直方向 + +Slider 垂直展示时,高度为 100% 父元素高度 + +```html +
+ +
+``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前进度百分比 | _number_ | `0` | +| max | 最大值 | _number \| string_ | `100` | +| min | 最小值 | _number \| string_ | `0` | +| step | 步长 | _number \| string_ | `1` | +| bar-height | 进度条高度,默认单位为`px` | _number \| string_ | `2px` | +| button-size `v2.4.5` | 滑块按钮大小,默认单位为`px` | _number \| string_ | `24px` | +| active-color | 进度条激活态颜色 | _string_ | `#1989fa` | +| inactive-color | 进度条非激活态颜色 | _string_ | `#e5e5e5` | +| disabled | 是否禁用滑块 | _boolean_ | `false` | +| vertical | 是否垂直展示 | _boolean_ | `false` | + +### Events + +| 事件名 | 说明 | 回调参数 | +| ---------- | ------------------------ | --------------- | +| input | 进度变化时实时触发 | value: 当前进度 | +| change | 进度变化且结束拖动后触发 | value: 当前进度 | +| drag-start | 开始拖动时触发 | - | +| drag-end | 结束拖动时触发 | - | + +### Slots + +| 名称 | 说明 | +| ------ | -------------- | +| button | 自定义滑动按钮 | diff --git a/src-next/slider/demo/index.vue b/src-next/slider/demo/index.vue new file mode 100644 index 000000000..a510eefc6 --- /dev/null +++ b/src-next/slider/demo/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src-next/slider/index.js b/src-next/slider/index.js new file mode 100644 index 000000000..d0e3e0b6a --- /dev/null +++ b/src-next/slider/index.js @@ -0,0 +1,195 @@ +import { createNamespace, addUnit } from '../utils'; +import { preventDefault } from '../utils/dom/event'; +import { TouchMixin } from '../mixins/touch'; +import { FieldMixin } from '../mixins/field'; + +const [createComponent, bem] = createNamespace('slider'); + +export default createComponent({ + mixins: [TouchMixin, FieldMixin], + + props: { + disabled: Boolean, + vertical: Boolean, + barHeight: [Number, String], + buttonSize: [Number, String], + activeColor: String, + inactiveColor: String, + min: { + type: [Number, String], + default: 0, + }, + max: { + type: [Number, String], + default: 100, + }, + step: { + type: [Number, String], + default: 1, + }, + modelValue: { + type: Number, + default: 0, + }, + }, + + data() { + return { + dragStatus: '', + }; + }, + + computed: { + range() { + return this.max - this.min; + }, + + buttonStyle() { + if (this.buttonSize) { + const size = addUnit(this.buttonSize); + return { + width: size, + height: size, + }; + } + }, + }, + + created() { + // format initial value + this.updateValue(this.modelValue); + }, + + mounted() { + this.bindTouchEvent(this.$refs.wrapper); + }, + + methods: { + onTouchStart(event) { + if (this.disabled) { + return; + } + + this.touchStart(event); + this.startValue = this.format(this.modelValue); + this.dragStatus = 'start'; + }, + + onTouchMove(event) { + if (this.disabled) { + return; + } + + if (this.dragStatus === 'start') { + this.$emit('drag-start'); + } + + preventDefault(event, true); + this.touchMove(event); + this.dragStatus = 'draging'; + + const rect = this.$el.getBoundingClientRect(); + const delta = this.vertical ? this.deltaY : this.deltaX; + const total = this.vertical ? rect.height : rect.width; + const diff = (delta / total) * this.range; + + this.newValue = this.startValue + diff; + this.updateValue(this.newValue); + }, + + onTouchEnd() { + if (this.disabled) { + return; + } + + if (this.dragStatus === 'draging') { + this.updateValue(this.newValue, true); + this.$emit('drag-end'); + } + + this.dragStatus = ''; + }, + + onClick(event) { + event.stopPropagation(); + + if (this.disabled) return; + + const rect = this.$el.getBoundingClientRect(); + const delta = this.vertical + ? event.clientY - rect.top + : event.clientX - rect.left; + const total = this.vertical ? rect.height : rect.width; + const value = +this.min + (delta / total) * this.range; + + this.startValue = this.modelValue; + this.updateValue(value, true); + }, + + updateValue(value, end) { + value = this.format(value); + + if (value !== this.modelValue) { + this.$emit('update:modelValue', value); + } + + if (end && value !== this.startValue) { + this.$emit('change', value); + } + }, + + format(value) { + return ( + Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * + this.step + ); + }, + }, + + render() { + const { vertical } = this; + const mainAxis = vertical ? 'height' : 'width'; + const crossAxis = vertical ? 'width' : 'height'; + + const wrapperStyle = { + background: this.inactiveColor, + [crossAxis]: addUnit(this.barHeight), + }; + + const barStyle = { + [mainAxis]: `${((this.modelValue - this.min) * 100) / this.range}%`, + background: this.activeColor, + }; + + if (this.dragStatus) { + barStyle.transition = 'none'; + } + + return ( +
+
+
+ {this.$slots.button ? ( + this.$slots.button() + ) : ( +
+ )} +
+
+
+ ); + }, +}); diff --git a/src-next/slider/index.less b/src-next/slider/index.less new file mode 100644 index 000000000..6919ffe0c --- /dev/null +++ b/src-next/slider/index.less @@ -0,0 +1,75 @@ +@import '../style/var'; + +.van-slider { + position: relative; + width: 100%; + height: @slider-bar-height; + background-color: @slider-inactive-background-color; + border-radius: @border-radius-max; + cursor: pointer; + + // use pseudo element to expand click area + &::before { + position: absolute; + top: -@padding-xs; + right: 0; + bottom: -@padding-xs; + left: 0; + content: ''; + } + + &__bar { + position: relative; + width: 100%; + height: 100%; + background-color: @slider-active-background-color; + border-radius: inherit; + transition: width @animation-duration-fast; + } + + &__button { + width: @slider-button-width; + height: @slider-button-height; + background-color: @slider-button-background-color; + border-radius: @slider-button-border-radius; + box-shadow: @slider-button-box-shadow; + + &-wrapper { + position: absolute; + top: 50%; + right: 0; + transform: translate3d(50%, -50%, 0); + cursor: grab; + } + } + + &--disabled { + cursor: not-allowed; + opacity: @slider-disabled-opacity; + + .van-slider__button-wrapper { + cursor: not-allowed; + } + } + + &--vertical { + display: inline-block; + width: @slider-bar-height; + height: 100%; + + .van-slider__button-wrapper { + top: auto; + right: 50%; + bottom: 0; + transform: translate3d(50%, 50%, 0); + } + + // use pseudo element to expand click area + &::before { + top: 0; + right: -@padding-xs; + bottom: 0; + left: -@padding-xs; + } + } +} diff --git a/src-next/slider/test/__snapshots__/demo.spec.js.snap b/src-next/slider/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..e1a65f6e5 --- /dev/null +++ b/src-next/slider/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
50
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src-next/slider/test/__snapshots__/index.spec.js.snap b/src-next/slider/test/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..e63f4f6d0 --- /dev/null +++ b/src-next/slider/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bar-height prop 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`button-size prop 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`click bar 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`click bar 2`] = ` +
+
+
+
+
+
+
+`; + +exports[`click vertical 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`drag button 1`] = ` +
+
+
+
+
+
+
+`; + +exports[`drag button 2`] = ` +
+
+
+
+
+
+
+`; + +exports[`drag button vertical 1`] = ` +
+
+
+
+
+
+
+`; diff --git a/src-next/slider/test/demo.spec.js b/src-next/slider/test/demo.spec.js new file mode 100644 index 000000000..5c70922b5 --- /dev/null +++ b/src-next/slider/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/src-next/slider/test/index.spec.js b/src-next/slider/test/index.spec.js new file mode 100644 index 000000000..53333b201 --- /dev/null +++ b/src-next/slider/test/index.spec.js @@ -0,0 +1,166 @@ +import Slider from '..'; +import { + mount, + trigger, + triggerDrag, + mockGetBoundingClientRect, +} from '../../../test'; + +function mockRect(vertical) { + return mockGetBoundingClientRect({ + width: vertical ? 0 : 100, + height: vertical ? 100 : 0, + top: vertical ? 0 : 100, + left: vertical ? 100 : 0, + }); +} + +test('drag button', () => { + const restoreMock = mockRect(); + + const wrapper = mount(Slider, { + propsData: { + value: 50, + disabled: true, + }, + }); + + wrapper.vm.$on('input', (value) => { + wrapper.setProps({ value }); + }); + + const button = wrapper.find('.van-slider__button'); + triggerDrag(button, 50, 0); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.emitted('drag-start')).toBeFalsy(); + expect(wrapper.emitted('drag-end')).toBeFalsy(); + + wrapper.setData({ disabled: false }); + trigger(button, 'touchstart', 0, 0); + trigger(button, 'touchend', 0, 0); + triggerDrag(button, 50, 0); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.emitted('drag-start')).toBeTruthy(); + expect(wrapper.emitted('drag-end')).toBeTruthy(); + + restoreMock(); +}); + +test('click bar', () => { + const restoreMock = mockRect(); + + const wrapper = mount(Slider, { + propsData: { + value: 50, + disabled: true, + }, + }); + + wrapper.vm.$on('input', (value) => { + wrapper.setProps({ value }); + }); + + trigger(wrapper, 'click', 100, 0); + expect(wrapper).toMatchSnapshot(); + + wrapper.setData({ disabled: false }); + trigger(wrapper, 'click', 100, 0); + expect(wrapper).toMatchSnapshot(); + + restoreMock(); +}); + +test('drag button vertical', () => { + const restoreMock = mockRect(true); + + const wrapper = mount(Slider, { + propsData: { + value: 50, + vertical: true, + }, + }); + + wrapper.vm.$on('input', (value) => { + wrapper.setProps({ value }); + }); + + const button = wrapper.find('.van-slider__button'); + triggerDrag(button, 0, 50); + expect(wrapper).toMatchSnapshot(); + + restoreMock(); +}); + +test('click vertical', () => { + const restoreMock = mockRect(true); + + const wrapper = mount(Slider, { + propsData: { + value: 50, + vertical: true, + }, + }); + + wrapper.vm.$on('input', (value) => { + wrapper.setProps({ value }); + }); + + trigger(wrapper, 'click', 0, 100); + expect(wrapper).toMatchSnapshot(); + + restoreMock(); +}); + +test('bar-height prop', () => { + const wrapper = mount(Slider, { + propsData: { + value: 50, + barHeight: 10, + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('button-size prop', () => { + const wrapper = mount(Slider, { + propsData: { + value: 50, + buttonSize: 10, + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('should not emit change event when value not changed', () => { + const wrapper = mount(Slider, { + propsData: { + value: 50, + }, + listeners: { + input(value) { + wrapper.setProps({ value }); + }, + }, + }); + + trigger(wrapper, 'click', 100, 0); + trigger(wrapper, 'click', 100, 0); + + expect(wrapper.emitted('change').length).toEqual(1); +}); + +test('should format initial value', (done) => { + mount(Slider, { + propsData: { + value: null, + }, + listeners: { + input(value) { + expect(value).toEqual(0); + done(); + }, + }, + }); +}); diff --git a/vant.config.js b/vant.config.js index bd5cc7bfe..f0e8c944e 100644 --- a/vant.config.js +++ b/vant.config.js @@ -159,10 +159,10 @@ module.exports = { // path: 'search', // title: 'Search 搜索', // }, - // { - // path: 'slider', - // title: 'Slider 滑块', - // }, + { + path: 'slider', + title: 'Slider 滑块', + }, // { // path: 'stepper', // title: 'Stepper 步进器', @@ -493,10 +493,10 @@ module.exports = { // path: 'search', // title: 'Search', // }, - // { - // path: 'slider', - // title: 'Slider', - // }, + { + path: 'slider', + title: 'Slider', + }, // { // path: 'stepper', // title: 'Stepper',