Repository: ant-design/ant-design-mobile
Pull request #5524 (2022-07-29 09:25:41)

fix(Rate): rate value will reset when drag stop on PC
在PC上拖动的时候有个不太符合预期的行为= =，单个Star的`onClick`会后于`drag`事件执行，导致value在拖动结束时会变成开始点击时对应的value，这里全部改到`drag`事件中用拖动的逻辑计算了= =。

用`Math.round`点击触发的体验不是很棒= =，点左半边的话会被省略= =。改成`Math.ceil`的话拖动的时候就又有点灵敏= =。

加了一个test，把对应test中的click改成了`mockDrag`的原地拖动。


Patch:
diff --git a/src/components/rate/rate.less b/src/components/rate/rate.less
index a7a59ced5..20ecee385 100644
--- a/src/components/rate/rate.less
+++ b/src/components/rate/rate.less
@@ -6,6 +6,7 @@
   --inactive-color: var(--adm-color-border);
   display: inline-flex;
   touch-action: pan-y;
+  user-select: none;
   &-box {
     position: relative;
   }
@@ -19,6 +20,7 @@
     overflow: hidden;
     cursor: pointer;
     box-sizing: border-box;
+    transition: all 0.3s;
     &-half {
       padding-right: 0;
       width: 50%;
diff --git a/src/components/rate/rate.tsx b/src/components/rate/rate.tsx
index 6eb27bb75..a7ead7b34 100644
--- a/src/components/rate/rate.tsx
+++ b/src/components/rate/rate.tsx
@@ -44,14 +44,6 @@ export const Rate: FC<RateProps> = p => {
           [`${classPrefix}-star-half`]: half,
           [`${classPrefix}-star-readonly`]: props.readOnly,
         })}
-        onClick={() => {
-          if (props.readOnly) return
-          if (props.allowClear && value === v) {
-            setValue(0)
-          } else {
-            setValue(v)
-          }
-        }}
         role='radio'
         aria-checked={value >= v}
         aria-label={'' + v}
@@ -66,28 +58,36 @@ export const Rate: FC<RateProps> = p => {
       if (props.readOnly) return
       const {
         xy: [clientX],
+        tap,
       } = state
       const container = containerRef.current
       if (!container) return
       const rect = container.getBoundingClientRect()
-
       const rawValue = ((clientX - rect.left) / rect.width) * props.count
 
-      const roundedValue = props.allowHalf
-        ? Math.round(rawValue * 2) / 2
-        : Math.round(rawValue)
+      const ceiledValue = props.allowHalf
+        ? Math.ceil(rawValue * 2) / 2
+        : Math.ceil(rawValue)
+
+      const boundValue = bound(ceiledValue, 0, props.count)
 
-      setValue(bound(roundedValue, 0, props.count))
+      if (tap) {
+        if (props.allowClear && boundValue === value) {
+          setValue(0)
+          return
+        }
+      }
+
+      setValue(boundValue)
     },
     {
       axis: 'x',
-      preventScroll: true,
       pointer: {
         touch: true,
       },
+      filterTaps: true,
     }
   )
-
   return withNativeProps(
     props,
     <div
diff --git a/src/components/swiper/swiper.tsx b/src/components/swiper/swiper.tsx
index 1a6fd2426..85ab8d1c8 100644
--- a/src/components/swiper/swiper.tsx
+++ b/src/components/swiper/swiper.tsx
@@ -256,7 +256,7 @@ export const Swiper = forwardRef<SwiperRef, SwiperProps>(
         return () => {
           window.clearInterval(interval)
         }
-      }, [autoplay, autoplayInterval, dragging, count])
+      }, [autoplay, autoplayInterval, dragging])
 
       function renderTrackInner() {
         if (loop) {


Test patch:
diff --git a/src/components/rate/tests/rate.test.tsx b/src/components/rate/tests/rate.test.tsx
index 92c5e581e..d40e11438 100644
--- a/src/components/rate/tests/rate.test.tsx
+++ b/src/components/rate/tests/rate.test.tsx
@@ -1,10 +1,22 @@
 import React from 'react'
-import { render, testA11y, fireEvent, screen } from 'testing'
+import { render, testA11y, fireEvent, screen, mockDrag } from 'testing'
 import Rate from '..'
 
 const classPrefix = `adm-rate`
 
 describe('Rate', () => {
+  const getBoundingClientRectMock = jest.spyOn(
+    HTMLElement.prototype,
+    'getBoundingClientRect'
+  )
+
+  beforeAll(() => {
+    getBoundingClientRectMock.mockReturnValue({
+      left: 12,
+      width: 155.9375,
+    } as DOMRect)
+  })
+
   test('a11y', async () => {
     await testA11y(<Rate />)
   })
@@ -12,8 +24,16 @@ describe('Rate', () => {
   test('readOnly should be work', () => {
     const onChange = jest.fn()
     render(<Rate value={4} readOnly onChange={onChange} />)
+    const radioGroup = screen.getByRole('radiogroup')
     const radio = screen.getByRole('radio', { name: '1' })
-    fireEvent.click(radio)
+    mockDrag(radioGroup, [
+      {
+        clientX: 0,
+      },
+      {
+        clientX: 100,
+      },
+    ])
     expect(radio).toHaveClass(`${classPrefix}-star-readonly`)
     expect(onChange).not.toBeCalled()
   })
@@ -21,11 +41,29 @@ describe('Rate', () => {
   test('allowHalf should be work', () => {
     render(<Rate allowHalf />)
     const radio = screen.getByRole('radio', { name: '1.5' })
-    fireEvent.click(radio)
+    const shouldNotActiveRadio = screen.getByRole('radio', { name: '2' })
+    const radioGroup = screen.getByRole('radiogroup')
+
+    mockDrag(radioGroup, [
+      {
+        clientX: 54,
+      },
+      {
+        clientX: 54,
+      },
+    ])
+
     expect(radio).toHaveClass(`${classPrefix}-star-half`)
     expect(radio).toHaveClass(`${classPrefix}-star-active`)
-
-    fireEvent.click(radio)
+    expect(shouldNotActiveRadio).not.toHaveClass(`${classPrefix}-star-active`)
+    mockDrag(radioGroup, [
+      {
+        clientX: 54,
+      },
+      {
+        clientX: 54,
+      },
+    ])
     expect(
       document.querySelectorAll(`.${classPrefix}-star-active`)
     ).toHaveLength(0)
@@ -34,8 +72,9 @@ describe('Rate', () => {
   test('can not clear', () => {
     render(<Rate allowClear={false} defaultValue={1} />)
     const radio = screen.getByRole('radio', { name: '1' })
+    const radioGroup = screen.getByRole('radiogroup')
     expect(radio).toHaveClass(`${classPrefix}-star-active`)
-    fireEvent.click(radio)
+    mockDrag(radioGroup, [{ clientX: 40 }, { clientX: 40 }])
     expect(radio).toHaveClass(`${classPrefix}-star-active`)
   })
 })