Skip to content

Commit c426590

Browse files
feat(FloatingPanel): support inertia factor
1 parent d8a7e82 commit c426590

5 files changed

Lines changed: 140 additions & 6 deletions

File tree

src/components/floating-panel/floating-panel.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ export type FloatingPanelProps = {
2727
onHeightChange?: (height: number, animating: boolean) => void
2828
handleDraggingOfContent?: boolean
2929
placement?: 'bottom' | 'top'
30+
inertiaFactor?: number
3031
} & NativeProps<'--border-radius' | '--z-index' | '--header-height'>
3132

3233
const defaultProps = {
3334
handleDraggingOfContent: true,
35+
inertiaFactor: 50,
3436
}
3537

3638
export const FloatingPanel = forwardRef<FloatingPanelRef, FloatingPanelProps>(
3739
(p, ref) => {
3840
const props = mergeProps(defaultProps, p)
39-
const { anchors, placement = 'bottom' } = props
41+
const { anchors, placement = 'bottom', inertiaFactor } = props
4042
const maxHeight = anchors[anchors.length - 1] ?? window.innerHeight
4143

4244
const isBottomPlacement = placement !== 'top'
@@ -96,7 +98,9 @@ export const FloatingPanel = forwardRef<FloatingPanelRef, FloatingPanelProps>(
9698
if (state.last) {
9799
pullingRef.current = false
98100
setPulling(false)
99-
nextY = nearest(possibles, offsetY)
101+
const [, vy] = state.velocity
102+
const [, dy] = state.direction
103+
nextY = nearest(possibles, offsetY + dy * vy * (inertiaFactor ?? 0))
100104
}
101105
api.start({
102106
y: nextY,

src/components/floating-panel/index.en.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ Users can freely and flexibly slide up and down to browse the content, which is
2020

2121
### Props
2222

23-
| Name | Description | Type | Description | Version |
23+
| Name | Description | Type | Default | Version |
2424
| --- | --- | --- | --- | --- |
2525
| anchors | What height can be dragged to, the unit is `px` | `number[]` | - | |
2626
| handleDraggingOfContent | Whether to handle the drag event of the panel content area. If disabled, only the head area can be dragged | `boolean` | `true` | |
2727
| onHeightChange | Triggered when the height changes, the `animating` parameter indicates whether it is in the process of animation | `(height: number, animating: boolean) => void` | | |
2828
| placement | Specify the direction in which the panel appears. | `'bottom' \| 'top'` | `'bottom'` | 5.39.0 |
29+
| inertiaFactor | Inertia factor for snap behavior. When the user flicks fast, velocity adds extra displacement to the snap target, making it easier to reach distant anchors. Set to 0 to disable. Recommended value: 50~100 | `number` | `50` | 5.42.3 |
2930

3031
### Ref
3132

src/components/floating-panel/index.zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
| handleDraggingOfContent | 是否会处理面板内容区域的拖拽事件,禁用后则只能拖拽头部区域 | `boolean` | `true` | |
2727
| onHeightChange | 当高度变化时触发,`animating` 参数表示是否处于动画过程中 | `(height: number, animating: boolean) => void` | | |
2828
| placement | 指定面板出现的方向 | `'bottom' \| 'top'` | `'bottom'` | 5.39.0 |
29+
| inertiaFactor | 惯性系数,用于吸附判定。快速滑动时速度会附加额外位移,使面板更容易到达较远的锚点。设为 0 可禁用。推荐值:50~100 | `number` | `50` | 5.42.3 |
2930

3031
### Ref
3132

src/components/floating-panel/tests/floating-panel.test.tsx

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, { useRef, forwardRef } from 'react'
2-
import { render, testA11y, fireEvent, waitFor } from 'testing'
3-
import FloatingPanel, { FloatingPanelRef } from '..'
41
import { reduceMotion, restoreMotion } from 'antd-mobile'
2+
import React, { forwardRef, useRef } from 'react'
3+
import { fireEvent, mockTimedDrag, render, testA11y, waitFor } from 'testing'
4+
import FloatingPanel, { FloatingPanelRef } from '..'
5+
import { nearest } from '../../../utils/nearest'
56

67
const classPrefix = `adm-floating-panel`
78

@@ -157,3 +158,91 @@ describe('FloatingPanel', () => {
157158
)
158159
})
159160
})
161+
162+
describe('FloatingPanel inertiaFactor logic', () => {
163+
const possibles = anchors.map(x => -x) // [-100, -200, -400]
164+
165+
test('inertiaFactor=0 ignores velocity', () => {
166+
expect(nearest(possibles, -120 + -1 * 1.5 * 0)).toBe(-100)
167+
})
168+
169+
test('upward flick pushes snap toward higher anchor', () => {
170+
expect(nearest(possibles, -120 + -1 * 1.5 * 50)).toBe(-200)
171+
})
172+
173+
test('downward flick pushes snap toward lower anchor', () => {
174+
expect(nearest(possibles, -280 + 1 * 1.5 * 100)).toBe(-100)
175+
})
176+
177+
test('small inertiaFactor has minimal effect', () => {
178+
expect(nearest(possibles, -120 + -1 * 1.5 * 5)).toBe(-100)
179+
})
180+
181+
test('large inertiaFactor can skip to farthest anchor', () => {
182+
expect(nearest(possibles, -120 + -1 * 1.5 * 200)).toBe(-400)
183+
})
184+
})
185+
186+
describe('FloatingPanel inertiaFactor integration', () => {
187+
const App = forwardRef((props: any, ref) => (
188+
<FloatingPanel anchors={anchors} data-testid='panel' {...props} ref={ref}>
189+
{data.map(item => (
190+
<div key={item} style={{ height: 20 }}>
191+
{item}
192+
</div>
193+
))}
194+
</FloatingPanel>
195+
))
196+
197+
test('without inertiaFactor, fast flick stays at nearest anchor', async () => {
198+
const { getByTestId } = render(<App inertiaFactor={0} />)
199+
const panelEl = getByTestId('panel')
200+
201+
mockTimedDrag(panelEl, 200, 170, 16, 3)
202+
203+
await waitFor(() =>
204+
expect(panelEl.style.transform).toBe(
205+
`translateY(calc(100% + (-${anchors[0]}px)))`
206+
)
207+
)
208+
})
209+
210+
test('with inertiaFactor, fast flick snaps to higher anchor', async () => {
211+
const { getByTestId } = render(<App inertiaFactor={50} />)
212+
const panelEl = getByTestId('panel')
213+
214+
mockTimedDrag(panelEl, 200, 170, 32, 3)
215+
216+
await waitFor(() =>
217+
expect(panelEl.style.transform).toBe(
218+
`translateY(calc(100% + (-${anchors[1]}px)))`
219+
)
220+
)
221+
})
222+
223+
test('with inertiaFactor, slow drag stays at nearest anchor', async () => {
224+
const { getByTestId } = render(<App inertiaFactor={100} />)
225+
const panelEl = getByTestId('panel')
226+
227+
mockTimedDrag(panelEl, 200, 170, 1000, 10)
228+
229+
await waitFor(() =>
230+
expect(panelEl.style.transform).toBe(
231+
`translateY(calc(100% + (-${anchors[0]}px)))`
232+
)
233+
)
234+
})
235+
236+
test('large inertiaFactor with fast flick skips to farthest anchor', async () => {
237+
const { getByTestId } = render(<App inertiaFactor={300} />)
238+
const panelEl = getByTestId('panel')
239+
240+
mockTimedDrag(panelEl, 200, 170, 16, 3)
241+
242+
await waitFor(() =>
243+
expect(panelEl.style.transform).toBe(
244+
`translateY(calc(100% + (-${anchors[2]}px)))`
245+
)
246+
)
247+
})
248+
})

src/tests/testing.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,42 @@ export const mockDrag = async (el: Element, options: any[], time?: number) => {
156156
}
157157
fireEvent.mouseUp(el)
158158
}
159+
160+
/**
161+
* Simulates a vertical drag with velocity — smaller duration means faster flick
162+
*
163+
* @param el Target element
164+
* @param from Starting clientY
165+
* @param to Ending clientY
166+
* @param duration Drag duration in ms
167+
* @param steps Number of mouseMove events
168+
*/
169+
export const mockTimedDrag = (
170+
el: Element,
171+
from: number,
172+
to: number,
173+
duration: number,
174+
steps: number,
175+
) => {
176+
const dispatch = (
177+
type: 'mousedown' | 'mousemove' | 'mouseup',
178+
clientY: number,
179+
timeStamp: number,
180+
) => {
181+
const ev = new MouseEvent(type, {
182+
clientY,
183+
buttons: 1,
184+
bubbles: true,
185+
cancelable: true,
186+
})
187+
Object.defineProperty(ev, 'timeStamp', { value: timeStamp, configurable: true })
188+
fireEvent(el, ev)
189+
}
190+
191+
dispatch('mousedown', from, 1000)
192+
for (let i = 1; i <= steps; i++) {
193+
const progress = i / steps
194+
dispatch('mousemove', from + (to - from) * progress, 1000 + duration * progress)
195+
}
196+
dispatch('mouseup', to, 1000 + duration)
197+
}

0 commit comments

Comments
 (0)