Repository: ant-design/ant-design-mobile
Pull request #5718 (2022-10-10 07:15:17)

feat: (FloatingBubble) add drag end Callback event and default drag o…
FloatingBubble 支持提供拖动后的偏移值，支持刷新页面回绑
#5717 

github-actions[bot]: PR preview has been successfully built and deployed to https://antd-mobile-preview-pr-5718.surge.sh
codecov[bot]: # [Codecov](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) Report
Base: **90.02**% // Head: **89.92**% // Decreases project coverage by **`-0.10%`** :warning:
> Coverage data is based on head [(`9e4e973`)](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) compared to base [(`2c54423`)](https://codecov.io/gh/ant-design/ant-design-mobile/commit/2c544239be002f157acf9b9d4731b6b5c74ae907?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5718      +/-   ##
==========================================
- Coverage   90.02%   89.92%   -0.11%     
==========================================
  Files         297      297              
  Lines        6339     6341       +2     
  Branches     1554     1556       +2     
==========================================
- Hits         5707     5702       -5     
- Misses        584      590       +6     
- Partials       48       49       +1     
```


| [Impacted Files](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) | Coverage Δ | |
|---|---|---|
| [src/components/floating-bubble/floating-bubble.tsx](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design#diff-c3JjL2NvbXBvbmVudHMvZmxvYXRpbmctYnViYmxlL2Zsb2F0aW5nLWJ1YmJsZS50c3g=) | `100.00% <100.00%> (ø)` | |
| [src/components/image-uploader/image-uploader.tsx](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design#diff-c3JjL2NvbXBvbmVudHMvaW1hZ2UtdXBsb2FkZXIvaW1hZ2UtdXBsb2FkZXIudHN4) | `86.36% <0.00%> (-6.37%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ant-design/ant-design-mobile/pull/5718?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).

liangskyli: 我把onDragEnd也加上了，多一个方法使用，避免没办法在离开页面的时候去读取 ref 的值
miracles1919: `dragTo` 的时候应该也要触发 `onDragEnd` 的

我在你的基础上调整了下代码和 demo
liangskyli: 好，幸苦了





------------------ Original ------------------
From: miracles1919 ***@***.***&gt;
Date: Wed,Nov 9,2022 5:02 PM
To: ant-design/ant-design-mobile ***@***.***&gt;
Cc: liangsky ***@***.***&gt;, Author ***@***.***&gt;
Subject: Re: [ant-design/ant-design-mobile] feat: (FloatingBubble) add drag end Callback event and default drag o… (PR #5718)





 
dragTo 的时候应该也要触发 onDragEnd 的
 
我在你的基础上调整了下代码和 demo
 
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***&gt;
zombieJ: 我还是比较认同受控这个：https://github.com/ant-design/ant-design-mobile/issues/5717#issuecomment-1278660676

cc @miracles1919 @afc163  @awmleer
awmleer: 是指 x y 做成受控嘛？如果是的话，那可能存在性能问题，因为现在 x y 并不是 React state，而是 react-spring 里的 SpringValue，现在每次 x y 改变的时候，并不会触发 React 组件的重渲染的
zombieJ: > 是指 x y 做成受控嘛？如果是的话，那可能存在性能问题，因为现在 x y 并不是 React state，而是 react-spring 里的 SpringValue，现在每次 x y 改变的时候，并不会触发 React 组件的重渲染的

这块是否可以调成 transition 实现？本身位移不是一个复杂动画。另外，其实我也不太建议因为性能去打 ref 的孔，性能可以优化的，如果未来依赖库解决了性能问题或者换了一个库不再有性能问题。这个孔却不得不一直兼容下去了。
miracles1919: 尝试支持受控模式，大佬们帮我 cc 

Patch:
diff --git a/src/components/floating-bubble/demos/demo4.tsx b/src/components/floating-bubble/demos/demo4.tsx
new file mode 100644
index 000000000..1e084fc82
--- /dev/null
+++ b/src/components/floating-bubble/demos/demo4.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react'
+import { FloatingBubble, Divider } from 'antd-mobile'
+import { DemoDescription } from 'demos'
+import { MessageFill } from 'antd-mobile-icons'
+
+export default () => {
+  const [offset, setOffset] = useState({ x: -24, y: -24 })
+
+  return (
+    <div
+      style={{
+        textAlign: 'center',
+        padding: '50vh 32px 0',
+      }}
+    >
+      <DemoDescription>受控模式</DemoDescription>
+      <Divider />
+
+      <DemoDescription>
+        <div>x: {offset.x}</div>
+        <div>y: {offset.y}</div>
+      </DemoDescription>
+
+      <FloatingBubble
+        axis='xy'
+        style={{
+          '--initial-position-bottom': '0',
+          '--initial-position-right': '0',
+        }}
+        onOffsetChange={offset => {
+          setOffset(offset)
+        }}
+        offset={offset}
+      >
+        <MessageFill fontSize={32} />
+      </FloatingBubble>
+    </div>
+  )
+}
diff --git a/src/components/floating-bubble/floating-bubble.tsx b/src/components/floating-bubble/floating-bubble.tsx
index 046988858..e77e44b22 100644
--- a/src/components/floating-bubble/floating-bubble.tsx
+++ b/src/components/floating-bubble/floating-bubble.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useRef } from 'react'
+import React, { FC, useRef, useEffect, useState } from 'react'
 import { useSpring, animated, to } from '@react-spring/web'
 import { useDrag } from '@use-gesture/react'
 import { mergeProps } from '../../utils/with-default-props'
@@ -6,11 +6,16 @@ import { NativeProps, withNativeProps } from '../../utils/native-props'
 
 const classPrefix = `adm-floating-bubble`
 
+type Offset = { x: number; y: number }
+
 export type FloatingBubbleProps = {
   onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
   axis?: 'x' | 'y' | 'xy' | 'lock'
   magnetic?: 'x' | 'y'
   children?: React.ReactNode
+  offset?: Offset
+  defaultOffset?: Offset
+  onOffsetChange?: (offset: Offset) => void
 } & NativeProps<
   | '--initial-position-left'
   | '--initial-position-right'
@@ -25,6 +30,7 @@ export type FloatingBubbleProps = {
 
 const defaultProps = {
   axis: 'y',
+  defaultOffset: { x: 0, y: 0 },
 }
 
 export const FloatingBubble: FC<FloatingBubbleProps> = p => {
@@ -33,20 +39,31 @@ export const FloatingBubble: FC<FloatingBubbleProps> = p => {
   const boundaryRef = useRef<HTMLDivElement>(null)
   const buttonRef = useRef<HTMLDivElement>(null)
 
+  const [innerValue, setInnerValue] = useState<Offset>(
+    props.offset === undefined ? props.defaultOffset : props.offset
+  )
+
+  useEffect(() => {
+    if (props.offset === undefined) return
+    api.start({ x: props.offset.x, y: props.offset.y })
+  }, [props.offset])
+
   /**
    * memoize the `to` function
    * inside a component that renders frequently
    * to prevent an unintended restart
    */
   const [{ x, y, opacity }, api] = useSpring(() => ({
-    x: 0,
-    y: 0,
+    x: innerValue.x,
+    y: innerValue.y,
     opacity: 1,
   }))
+
   const bind = useDrag(
     state => {
       let nextX = state.offset[0]
       let nextY = state.offset[1]
+
       if (state.last && props.magnetic) {
         const boundary = boundaryRef.current
         const button = buttonRef.current
@@ -76,10 +93,16 @@ export const FloatingBubble: FC<FloatingBubbleProps> = p => {
           }
         }
       }
-      api.start({
-        x: nextX,
-        y: nextY,
-      })
+
+      const nextOffest = { x: nextX, y: nextY }
+      if (props.offset === undefined) {
+        // Uncontrolled mode
+        api.start(nextOffest)
+      } else {
+        setInnerValue(nextOffest)
+      }
+      props.onOffsetChange?.(nextOffest)
+
       // active status
       api.start({
         opacity: state.active ? 0.8 : 1,
diff --git a/src/components/floating-bubble/index.en.md b/src/components/floating-bubble/index.en.md
index 71c627351..bf373a044 100644
--- a/src/components/floating-bubble/index.en.md
+++ b/src/components/floating-bubble/index.en.md
@@ -14,15 +14,20 @@ Suitable for providing extra functionality to the current page.
 
 <code src="./demos/demo3.tsx"></code>
 
+<code src="./demos/demo4.tsx"></code>
+
 ## FloatingBubble
 
 ### Props
 
-| Name     | Description                                                                                                                                                       | Type                           | Default |
-| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------- |
-| axis     | The direction in which the drag can be performed, `'xy'` means free movement, `'lock'` means movement is only allowed in the direction in which the drag started. | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'`   |
-| children | Content of the bubble.                                                                                                                                            | ReactNode                      | -----   |
-| magnetic | Automatic magnetic attraction to the boundary.                                                                                                                    | `'x' \| 'y'`                   | -       |
+| Name           | Description                                                                                                                                                       | Type                                        | Default          |
+| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------- |
+| axis           | The direction in which the drag can be performed, `'xy'` means free movement, `'lock'` means movement is only allowed in the direction in which the drag started. | `'x' \| 'y' \| 'xy' \| 'lock'`              | `'y'`            |
+| children       | Content of the bubble.                                                                                                                                            | ReactNode                                   | -----            |
+| magnetic       | Automatic magnetic attraction to the boundary.                                                                                                                    | `'x' \| 'y'`                                | -                |
+| defaultOffset  | Default offset position                                                                                                                                           | `{ x: number; y: number }`                  | `{ x: 0, y: 0 }` |
+| offset         | Offset position                                                                                                                                                   | `{ x: number; y: number }`                  |                  |
+| onOffsetChange | Callback function when changing                                                                                                                                   | `(offset:{ x: number; y: number }) => void` | -                |
 
 ### CSS Variables
 
diff --git a/src/components/floating-bubble/index.zh.md b/src/components/floating-bubble/index.zh.md
index 4e1d4db1b..8b1ec751c 100644
--- a/src/components/floating-bubble/index.zh.md
+++ b/src/components/floating-bubble/index.zh.md
@@ -14,15 +14,20 @@
 
 <code src="./demos/demo3.tsx"></code>
 
+<code src="./demos/demo4.tsx"></code>
+
 ## FloatingBubble
 
 ### 属性
 
-| 属性     | 描述                                                                                 | 类型                           | 默认值 |
-| -------- | ------------------------------------------------------------------------------------ | ------------------------------ | ------ |
-| axis     | 可以进行拖动的方向，`'xy'` 表示自由移动，`'lock'` 表示只允许在拖拽开始时的方向上移动 | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'`  |
-| children | 气泡的内容                                                                           | ReactNode                      | -      |
-| magnetic | 自动磁吸到边界                                                                       | `'x' \| 'y'`                   | -      |
+| 属性           | 描述                                                                                 | 类型                                        | 默认值           |
+| -------------- | ------------------------------------------------------------------------------------ | ------------------------------------------- | ---------------- |
+| axis           | 可以进行拖动的方向，`'xy'` 表示自由移动，`'lock'` 表示只允许在拖拽开始时的方向上移动 | `'x' \| 'y' \| 'xy' \| 'lock'`              | `'y'`            |
+| children       | 气泡的内容                                                                           | ReactNode                                   | -                |
+| magnetic       | 自动磁吸到边界                                                                       | `'x' \| 'y'`                                | -                |
+| defaultOffset  | 默认偏移位置                                                                         | `{ x: number; y: number }`                  | `{ x: 0, y: 0 }` |
+| offset         | 偏移位置                                                                             | `{ x: number; y: number }`                  |                  |
+| onOffsetChange | 偏移位置变化时的回调函数                                                             | `(offset:{ x: number; y: number }) => void` | -                |
 
 ### CSS 变量
 


Test patch:
diff --git a/src/components/floating-bubble/tests/floating-bubble.test.tsx b/src/components/floating-bubble/tests/floating-bubble.test.tsx
index 140da43fc..3cd9e1ceb 100644
--- a/src/components/floating-bubble/tests/floating-bubble.test.tsx
+++ b/src/components/floating-bubble/tests/floating-bubble.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useState } from 'react'
 import { render, fireEvent, waitFor, sleep } from 'testing'
 import FloatingBubble from '..'
 
@@ -146,4 +146,37 @@ describe('FloatingBubble', () => {
     await sleep(0)
     expect(btn).toHaveStyle('transform: translate(0px, 0px)')
   })
+
+  test('controlled mode', async () => {
+    const App = () => {
+      const [offset, setOffset] = useState({ x: -24, y: -24 })
+      return (
+        <FloatingBubble
+          onOffsetChange={offset => {
+            setOffset(offset)
+          }}
+          offset={offset}
+          axis='xy'
+        />
+      )
+    }
+    render(<App />)
+    const btn = document.querySelectorAll(`.${classPrefix}-button`)[0]
+    expect(btn).toHaveStyle('transform: translate(-24px, -24px)')
+
+    mockBoundaryRect()
+    fireEvent.mouseDown(btn, {
+      buttons: 1,
+      clientX: -27,
+      clientY: -27,
+    })
+    fireEvent.mouseMove(btn, {
+      buttons: 1,
+      clientX: 0,
+      clientY: 0,
+    })
+    mockButtonRect({ x: 0, y: 0 })
+    fireEvent.mouseUp(btn)
+    expect(btn).toHaveStyle('transform: translate(0px, 0px)')
+  })
 })