diff --git a/src/rate/index.js b/src/rate/index.js
new file mode 100644
index 000000000..4430683ea
--- /dev/null
+++ b/src/rate/index.js
@@ -0,0 +1,199 @@
+import { createNamespace, addUnit } from '../utils';
+import { preventDefault } from '../utils/dom/event';
+import { TouchMixin } from '../mixins/touch';
+import Icon from '../icon';
+
+const [createComponent, bem] = createNamespace('rate');
+
+function getRateStatus(value, index, allowHalf) {
+ if (value >= index) {
+ return 'full';
+ }
+
+ if (value + 0.5 >= index && allowHalf) {
+ return 'half';
+ }
+
+ return 'void';
+}
+
+export default createComponent({
+ mixins: [TouchMixin],
+
+ props: {
+ size: [Number, String],
+ gutter: [Number, String],
+ readonly: Boolean,
+ disabled: Boolean,
+ allowHalf: Boolean,
+ value: {
+ type: Number,
+ default: 0
+ },
+ icon: {
+ type: String,
+ default: 'star'
+ },
+ voidIcon: {
+ type: String,
+ default: 'star-o'
+ },
+ color: {
+ type: String,
+ default: '#ffd21e'
+ },
+ voidColor: {
+ type: String,
+ default: '#c7c7c7'
+ },
+ disabledColor: {
+ type: String,
+ default: '#bdbdbd'
+ },
+ count: {
+ type: Number,
+ default: 5
+ }
+ },
+
+ computed: {
+ list() {
+ const list = [];
+ for (let i = 1; i <= this.count; i++) {
+ list.push(getRateStatus(this.value, i, this.allowHalf));
+ }
+
+ return list;
+ },
+
+ sizeWithUnit() {
+ return addUnit(this.size);
+ },
+
+ gutterWithUnit() {
+ return addUnit(this.gutter);
+ }
+ },
+
+ methods: {
+ select(index) {
+ if (!this.disabled && !this.readonly && index !== this.value) {
+ this.$emit('input', index);
+ this.$emit('change', index);
+ }
+ },
+
+ onTouchStart(event) {
+ if (this.readonly || this.disabled) {
+ return;
+ }
+
+ this.touchStart(event);
+
+ const rects = this.$refs.items.map(item => item.getBoundingClientRect());
+ const ranges = [];
+
+ rects.forEach((rect, index) => {
+ if (this.allowHalf) {
+ ranges.push(
+ { score: index + 0.5, left: rect.left },
+ { score: index + 1, left: rect.left + rect.width / 2 }
+ );
+ } else {
+ ranges.push({ score: index + 1, left: rect.left });
+ }
+ });
+
+ this.ranges = ranges;
+ },
+
+ onTouchMove(event) {
+ if (this.readonly || this.disabled) {
+ return;
+ }
+
+ this.touchMove(event);
+
+ if (this.direction === 'horizontal') {
+ preventDefault(event);
+
+ const { clientX } = event.touches[0];
+ this.select(this.getScoreByPosition(clientX));
+ }
+ },
+
+ getScoreByPosition(x) {
+ for (let i = this.ranges.length - 1; i > 0; i--) {
+ if (x > this.ranges[i].left) {
+ return this.ranges[i].score;
+ }
+ }
+
+ return this.allowHalf ? 0.5 : 1;
+ },
+
+ renderStar(status, index) {
+ const { icon, color, count, voidIcon, disabled, voidColor, disabledColor } = this;
+ const score = index + 1;
+ const isFull = status === 'full';
+ const isVoid = status === 'void';
+
+ let style;
+ if (this.gutterWithUnit && score !== count) {
+ style = { paddingRight: this.gutterWithUnit };
+ }
+
+ return (
+
+ {
+ this.select(score);
+ }}
+ />
+ {this.allowHalf && (
+ {
+ this.select(score - 0.5);
+ }}
+ />
+ )}
+
+ );
+ }
+ },
+
+ render() {
+ return (
+
+ {this.list.map((status, index) => this.renderStar(status, index))}
+
+ );
+ }
+});
diff --git a/src/rate/index.tsx b/src/rate/index.tsx
deleted file mode 100644
index e271e5e09..000000000
--- a/src/rate/index.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { createNamespace, addUnit } from '../utils';
-import { emit, inherit } from '../utils/functional';
-import { preventDefault } from '../utils/dom/event';
-import Icon from '../icon';
-
-// Types
-import { CreateElement, RenderContext } from 'vue/types';
-import { DefaultSlots } from '../utils/types';
-
-const [createComponent, bem] = createNamespace('rate');
-
-export type RateProps = {
- size: number;
- icon: string;
- count: number;
- color: string;
- value: number;
- gutter?: string | number;
- voidIcon: string;
- voidColor: string;
- readonly?: boolean;
- disabled?: boolean;
- allowHalf?: boolean;
- disabledColor: string;
-};
-
-export type RateEvents = {
- input(index: number): void;
- change(index: number): void;
-};
-
-type RateStatus = 'full' | 'half' | 'void';
-
-function getRateStatus(value: number, index: number, allowHalf?: boolean): RateStatus {
- if (value >= index) {
- return 'full';
- }
- if (value + 0.5 >= index && allowHalf) {
- return 'half';
- }
- return 'void';
-}
-
-function Rate(
- h: CreateElement,
- props: RateProps,
- slots: DefaultSlots,
- ctx: RenderContext
-) {
- const {
- icon,
- color,
- count,
- voidIcon,
- readonly,
- disabled,
- voidColor,
- disabledColor
- } = props;
-
- const list: RateStatus[] = [];
- for (let i = 1; i <= count; i++) {
- list.push(getRateStatus(props.value, i, props.allowHalf));
- }
-
- function onSelect(index: number) {
- if (!disabled && !readonly) {
- emit(ctx, 'input', index);
- emit(ctx, 'change', index);
- }
- }
-
- function onTouchMove(event: TouchEvent) {
- if (readonly || disabled || !document.elementFromPoint) {
- return;
- }
-
- preventDefault(event);
- const { clientX, clientY } = event.touches[0];
- const target = document.elementFromPoint(clientX, clientY) as HTMLElement;
-
- if (target && target.dataset) {
- const { score } = target.dataset;
-
- /* istanbul ignore else */
- if (score) {
- onSelect(+score);
- }
- }
- }
-
- const gutter = addUnit(props.gutter);
-
- function renderStar(status: RateStatus, index: number) {
- const isFull = status === 'full';
- const isVoid = status === 'void';
- const score = index + 1;
- const size = addUnit(props.size);
-
- let style;
- if (gutter && score !== count) {
- style = { paddingRight: gutter };
- }
-
- return (
-
- {
- onSelect(score);
- }}
- />
- {props.allowHalf && (
- {
- onSelect(score - 0.5);
- }}
- />
- )}
-
- );
- }
-
- return (
-
- {list.map((status, index) => renderStar(status, index))}
-
- );
-}
-
-Rate.props = {
- size: [Number, String],
- gutter: [Number, String],
- readonly: Boolean,
- disabled: Boolean,
- allowHalf: Boolean,
- value: {
- type: Number,
- default: 0
- },
- icon: {
- type: String,
- default: 'star'
- },
- voidIcon: {
- type: String,
- default: 'star-o'
- },
- color: {
- type: String,
- default: '#ffd21e'
- },
- voidColor: {
- type: String,
- default: '#c7c7c7'
- },
- disabledColor: {
- type: String,
- default: '#bdbdbd'
- },
- count: {
- type: Number,
- default: 5
- }
-};
-
-export default createComponent(Rate);
diff --git a/src/rate/test/index.spec.js b/src/rate/test/index.spec.js
index 5cc8229ce..b55315c31 100644
--- a/src/rate/test/index.spec.js
+++ b/src/rate/test/index.spec.js
@@ -1,23 +1,38 @@
import Rate from '..';
import { mount, triggerDrag } from '../../../test/utils';
+function mockGetBoundingClientRect(items) {
+ items.filter((icon, index) => {
+ icon.element.getBoundingClientRect = () => ({
+ left: index * 25,
+ width: 25
+ });
+ return true;
+ });
+}
+
test('change event', () => {
const onInput = jest.fn();
const onChange = jest.fn();
const wrapper = mount(Rate, {
- context: {
- on: {
- input: onInput,
- change: onChange
- }
+ listeners: {
+ input: value => {
+ onInput(value);
+ wrapper.setProps({ value });
+ },
+ change: onChange
}
});
const item4 = wrapper.findAll('.van-rate__icon').at(3);
item4.trigger('click');
+ item4.trigger('click');
+
expect(onInput).toHaveBeenCalledWith(4);
+ expect(onInput).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(4);
+ expect(onChange).toHaveBeenCalledTimes(1);
});
test('allow half', () => {
@@ -28,11 +43,9 @@ test('allow half', () => {
propsData: {
allowHalf: true
},
- context: {
- on: {
- input: onInput,
- change: onChange
- }
+ listeners: {
+ input: onInput,
+ change: onChange
}
});
const item4 = wrapper.findAll('.van-rate__icon--half').at(3);
@@ -50,43 +63,59 @@ test('disabled', () => {
propsData: {
disabled: true
},
- context: {
- on: {
- input: onInput,
- change: onChange
- }
+ listeners: {
+ input: onInput,
+ change: onChange
}
});
const item4 = wrapper.findAll('.van-rate__item').at(3);
+ triggerDrag(wrapper, 100, 0);
item4.trigger('click');
+
expect(onInput).toHaveBeenCalledTimes(0);
expect(onChange).toHaveBeenCalledTimes(0);
});
-test('touchmove', () => {
+test('touchmove to select item', () => {
const onChange = jest.fn();
const wrapper = mount(Rate, {
- context: {
- on: {
- change: onChange
- }
+ listeners: {
+ change: onChange
}
});
+
+ const icons = wrapper.findAll('.van-rate__item');
+
+ mockGetBoundingClientRect(icons);
triggerDrag(wrapper, 100, 0);
- const icons = wrapper.findAll('.van-icon');
- document.elementFromPoint = function (x) {
- const index = Math.round(x / 20);
- if (index < icons.length) {
- return icons.at(index).element;
+ expect(onChange).toHaveBeenNthCalledWith(1, 1);
+ expect(onChange).toHaveBeenNthCalledWith(2, 2);
+ expect(onChange).toHaveBeenNthCalledWith(3, 2);
+ expect(onChange).toHaveBeenNthCalledWith(4, 4);
+});
+
+test('touchmove to select half item', () => {
+ const onChange = jest.fn();
+ const wrapper = mount(Rate, {
+ propsData: {
+ allowHalf: true
+ },
+ listeners: {
+ change: onChange
}
- };
+ });
+ const icons = wrapper.findAll('.van-rate__item');
+
+ mockGetBoundingClientRect(icons);
triggerDrag(wrapper, 100, 0);
- expect(onChange).toHaveBeenNthCalledWith(1, 2);
- expect(onChange).toHaveBeenNthCalledWith(2, 3);
- expect(onChange).toHaveBeenNthCalledWith(3, 4);
+
+ expect(onChange).toHaveBeenNthCalledWith(1, 1);
+ expect(onChange).toHaveBeenNthCalledWith(2, 1.5);
+ expect(onChange).toHaveBeenNthCalledWith(3, 2);
+ expect(onChange).toHaveBeenNthCalledWith(4, 4);
});
test('gutter prop', () => {