Skip to content

Commit 1a0fc0e

Browse files
committed
refactor: bottom sheet logic
1 parent 614fe17 commit 1a0fc0e

File tree

7 files changed

+273
-157
lines changed

7 files changed

+273
-157
lines changed

packages/machines/bottom-sheet/src/bottom-sheet.connect.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export function connect<T extends PropTypes>(
1212
const { state, send, context, scope, prop } = service
1313

1414
const open = state.hasTag("open")
15-
const dragging = state.hasTag("dragging")
1615
const dragOffset = context.get("dragOffset")
16+
const dragging = dragOffset !== null
1717

1818
const activeSnapPoint = context.get("activeSnapPoint")
1919
const resolvedActiveSnapPoint = context.get("resolvedActiveSnapPoint")
@@ -24,24 +24,44 @@ export function connect<T extends PropTypes>(
2424
const target = getEventTarget<HTMLElement>(event)
2525
if (target?.hasAttribute("data-no-drag") || target?.closest("[data-no-drag]")) return
2626
if (state.matches("closing")) return
27-
const point = getEventPoint(event)
28-
send({ type: "POINTER_DOWN", point })
27+
send({ type: "POINTER_DOWN", point: getEventPoint(event) })
2928
}
3029

3130
return {
3231
open,
33-
activeSnapPoint,
34-
32+
dragging,
3533
setOpen(nextOpen) {
3634
const open = state.hasTag("open")
3735
if (open === nextOpen) return
3836
send({ type: nextOpen ? "OPEN" : "CLOSE" })
3937
},
4038

39+
snapPoints: prop("snapPoints"),
40+
activeSnapPoint,
4141
setActiveSnapPoint(snapPoint) {
4242
const activeSnapPoint = context.get("activeSnapPoint")
4343
if (activeSnapPoint === snapPoint) return
44-
send({ type: "SET_ACTIVE_SNAP_POINT", snapPoint })
44+
send({ type: "ACTIVE_SNAP_POINT.SET", snapPoint })
45+
},
46+
47+
getOpenPercentage() {
48+
if (!open) return 0
49+
50+
const contentHeight = context.get("contentHeight")
51+
if (!contentHeight) return 0
52+
53+
const currentOffset = translate ?? 0
54+
// Inverted: when offset is 0 (fully open), percentage is 1
55+
return Math.max(0, Math.min(1, 1 - currentOffset / contentHeight))
56+
},
57+
58+
getActiveSnapIndex() {
59+
const snapPoints = prop("snapPoints")
60+
return snapPoints.indexOf(activeSnapPoint)
61+
},
62+
63+
getContentHeight() {
64+
return context.get("contentHeight")
4565
},
4666

4767
getContentProps(props = { draggable: true }) {

packages/machines/bottom-sheet/src/bottom-sheet.machine.ts

Lines changed: 76 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import { trackDismissableElement } from "@zag-js/dismissable"
44
import { addDomEvent, getEventPoint, getEventTarget, raf } from "@zag-js/dom-query"
55
import { trapFocus } from "@zag-js/focus-trap"
66
import { preventBodyScroll } from "@zag-js/remove-scroll"
7-
import type { Point } from "@zag-js/types"
87
import * as dom from "./bottom-sheet.dom"
98
import type { BottomSheetSchema, ResolvedSnapPoint } from "./bottom-sheet.types"
10-
import { findClosestSnapPoint } from "./utils/find-closest-snap-point"
11-
import { getScrollInfo } from "./utils/get-scroll-info"
9+
import { DragManager } from "./utils/drag-manager"
1210
import { resolveSnapPoint } from "./utils/resolve-snap-point"
1311

1412
export const machine = createMachine<BottomSheetSchema>({
@@ -26,7 +24,7 @@ export const machine = createMachine<BottomSheetSchema>({
2624
initialFocusEl,
2725
snapPoints: [1],
2826
defaultActiveSnapPoint: 1,
29-
swipeVelocityThreshold: 500,
27+
swipeVelocityThreshold: 700,
3028
closeThreshold: 0.25,
3129
preventDragOnScroll: true,
3230
...props,
@@ -35,9 +33,6 @@ export const machine = createMachine<BottomSheetSchema>({
3533

3634
context({ bindable, prop }) {
3735
return {
38-
pointerStart: bindable<Point | null>(() => ({
39-
defaultValue: null,
40-
})),
4136
dragOffset: bindable<number | null>(() => ({
4237
defaultValue: null,
4338
})),
@@ -54,15 +49,12 @@ export const machine = createMachine<BottomSheetSchema>({
5449
contentHeight: bindable<number | null>(() => ({
5550
defaultValue: null,
5651
})),
57-
lastPoint: bindable<Point | null>(() => ({
58-
defaultValue: null,
59-
})),
60-
lastTimestamp: bindable<number | null>(() => ({
61-
defaultValue: null,
62-
})),
63-
velocity: bindable<number | null>(() => ({
64-
defaultValue: null,
65-
})),
52+
}
53+
},
54+
55+
refs() {
56+
return {
57+
dragManager: new DragManager(),
6658
}
6759
},
6860

@@ -94,7 +86,7 @@ export const machine = createMachine<BottomSheetSchema>({
9486
},
9587

9688
on: {
97-
SET_ACTIVE_SNAP_POINT: {
89+
"ACTIVE_SNAP_POINT.SET": {
9890
actions: ["setActiveSnapPoint"],
9991
},
10092
},
@@ -114,18 +106,28 @@ export const machine = createMachine<BottomSheetSchema>({
114106
"CONTROLLED.CLOSE": {
115107
target: "closed",
116108
},
117-
POINTER_DOWN: [
109+
POINTER_DOWN: {
110+
actions: ["setPointerStart"],
111+
},
112+
POINTER_MOVE: [
118113
{
119-
actions: ["setPointerStart"],
114+
guard: "isDragging",
115+
actions: ["setDragOffset"],
120116
},
121-
],
122-
POINTER_MOVE: [
123117
{
124118
guard: "shouldStartDragging",
125-
target: "open:dragging",
119+
actions: ["setDragOffset"],
126120
},
127121
],
128122
POINTER_UP: [
123+
{
124+
guard: "shouldCloseOnSwipe",
125+
target: "closing",
126+
},
127+
{
128+
guard: "isDragging",
129+
actions: ["setClosestSnapPoint", "clearPointerStart", "clearDragOffset"],
130+
},
129131
{
130132
actions: ["clearPointerStart", "clearDragOffset"],
131133
},
@@ -143,28 +145,6 @@ export const machine = createMachine<BottomSheetSchema>({
143145
},
144146
},
145147

146-
"open:dragging": {
147-
effects: ["trackDismissableElement", "preventScroll", "trapFocus", "hideContentBelow", "trackPointerMove"],
148-
tags: ["open", "dragging"],
149-
on: {
150-
POINTER_MOVE: [
151-
{
152-
actions: ["setDragOffset"],
153-
},
154-
],
155-
POINTER_UP: [
156-
{
157-
guard: "shouldCloseOnSwipe",
158-
target: "closing",
159-
},
160-
{
161-
actions: ["setClosestSnapPoint", "clearPointerStart", "clearDragOffset"],
162-
target: "open",
163-
},
164-
],
165-
},
166-
},
167-
168148
closing: {
169149
effects: ["trackExitAnimation"],
170150
on: {
@@ -207,51 +187,28 @@ export const machine = createMachine<BottomSheetSchema>({
207187
guards: {
208188
isOpenControlled: ({ prop }) => prop("open") !== undefined,
209189

210-
shouldStartDragging({ prop, context, event, scope, send }) {
211-
const pointerStart = context.get("pointerStart")
212-
const container = dom.getContentEl(scope)
213-
if (!pointerStart || !container) return false
214-
215-
const { point, target } = event
216-
217-
if (prop("preventDragOnScroll")) {
218-
const delta = pointerStart.y - point.y
219-
220-
if (Math.abs(delta) < 0.3) return false
221-
222-
const { availableScroll, availableScrollTop } = getScrollInfo(target, container)
223-
224-
if ((delta > 0 && Math.abs(availableScroll) > 1) || (delta < 0 && Math.abs(availableScrollTop) > 0)) {
225-
send({ type: "POINTER_UP", point })
226-
return false
227-
}
228-
}
229-
230-
return true
190+
isDragging({ context }) {
191+
return context.get("dragOffset") !== null
231192
},
232193

233-
shouldCloseOnSwipe({ prop, context, computed }) {
234-
const velocity = context.get("velocity")
235-
const dragOffset = context.get("dragOffset")
236-
const contentHeight = context.get("contentHeight")
237-
const swipeVelocityThreshold = prop("swipeVelocityThreshold")
238-
const closeThreshold = prop("closeThreshold")
239-
const snapPoints = computed("resolvedSnapPoints")
240-
241-
if (dragOffset === null || contentHeight === null || velocity === null) return false
242-
243-
const visibleHeight = contentHeight - dragOffset
244-
const smallestSnapPoint = snapPoints.reduce((acc, curr) => (curr.offset > acc.offset ? curr : acc))
245-
246-
const isFastSwipe = velocity > 0 && velocity >= swipeVelocityThreshold
247-
248-
const closeThresholdInPixels = contentHeight * (1 - closeThreshold)
249-
const isBelowSmallestSnapPoint = visibleHeight < contentHeight - smallestSnapPoint.offset
250-
const isBelowCloseThreshold = visibleHeight < closeThresholdInPixels
251-
252-
const hasEnoughDragToDismiss = (isBelowCloseThreshold && isBelowSmallestSnapPoint) || visibleHeight === 0
194+
shouldStartDragging({ prop, refs, event, scope }) {
195+
const dragManager = refs.get("dragManager")
196+
return dragManager.shouldStartDragging(
197+
event.point,
198+
event.target,
199+
dom.getContentEl(scope),
200+
prop("preventDragOnScroll"),
201+
)
202+
},
253203

254-
return isFastSwipe || hasEnoughDragToDismiss
204+
shouldCloseOnSwipe({ prop, context, computed, refs }) {
205+
const dragManager = refs.get("dragManager")
206+
return dragManager.shouldDismiss(
207+
context.get("contentHeight"),
208+
computed("resolvedSnapPoints"),
209+
prop("swipeVelocityThreshold"),
210+
prop("closeThreshold"),
211+
)
255212
},
256213
},
257214

@@ -268,53 +225,35 @@ export const machine = createMachine<BottomSheetSchema>({
268225
context.set("activeSnapPoint", event.snapPoint)
269226
},
270227

271-
setPointerStart({ event, context }) {
272-
context.set("pointerStart", event.point)
228+
setPointerStart({ event, refs }) {
229+
refs.get("dragManager").setPointerStart(event.point)
273230
},
274231

275-
setDragOffset({ context, event }) {
276-
const pointerStart = context.get("pointerStart")
277-
if (!pointerStart) return
278-
279-
const { point } = event
280-
281-
const currentTimestamp = new Date().getTime()
282-
283-
const lastPoint = context.get("lastPoint")
284-
if (lastPoint) {
285-
const dy = point.y - lastPoint.y
286-
287-
const lastTimestamp = context.get("lastTimestamp")
288-
if (lastTimestamp) {
289-
const dt = currentTimestamp - lastTimestamp
290-
if (dt > 0) {
291-
context.set("velocity", (dy / dt) * 1000)
292-
}
293-
}
294-
}
295-
296-
context.set("lastPoint", point)
297-
context.set("lastTimestamp", currentTimestamp)
298-
299-
let delta = pointerStart.y - point.y - (context.get("resolvedActiveSnapPoint")?.offset || 0)
300-
if (delta > 0) delta = 0
301-
302-
context.set("dragOffset", -delta)
232+
setDragOffset({ context, event, refs }) {
233+
const dragManager = refs.get("dragManager")
234+
dragManager.setDragOffset(event.point, context.get("resolvedActiveSnapPoint")?.offset || 0)
235+
context.set("dragOffset", dragManager.getDragOffset())
303236
},
304237

305-
setClosestSnapPoint({ computed, context }) {
238+
setClosestSnapPoint({ computed, context, refs }) {
306239
const snapPoints = computed("resolvedSnapPoints")
307240
const contentHeight = context.get("contentHeight")
308-
const dragOffset = context.get("dragOffset")
309241

310-
if (!snapPoints || contentHeight === null || dragOffset === null) return
242+
if (!snapPoints.length || contentHeight === null) return
311243

312-
const closestSnapPoint = findClosestSnapPoint(dragOffset, snapPoints)
244+
const dragManager = refs.get("dragManager")
245+
const closestSnapPoint = dragManager.findClosestSnapPoint(snapPoints)
313246

314-
context.set("activeSnapPoint", closestSnapPoint.value)
247+
// Set activeSnapPoint
248+
context.set("activeSnapPoint", closestSnapPoint)
249+
250+
// Also resolve and set immediately to prevent visual snap flash
251+
const resolved = resolveSnapPoint(closestSnapPoint, contentHeight)
252+
context.set("resolvedActiveSnapPoint", resolved)
315253
},
316254

317-
clearDragOffset({ context }) {
255+
clearDragOffset({ context, refs }) {
256+
refs.get("dragManager").clearDragOffset()
318257
context.set("dragOffset", null)
319258
},
320259

@@ -326,18 +265,16 @@ export const machine = createMachine<BottomSheetSchema>({
326265
context.set("resolvedActiveSnapPoint", null)
327266
},
328267

329-
clearPointerStart({ context }) {
330-
context.set("pointerStart", null)
268+
clearPointerStart({ refs }) {
269+
refs.get("dragManager").clearPointerStart()
331270
},
332271

333272
clearContentHeight({ context }) {
334273
context.set("contentHeight", null)
335274
},
336275

337-
clearVelocityTracking({ context }) {
338-
context.set("lastPoint", null)
339-
context.set("lastTimestamp", null)
340-
context.set("velocity", null)
276+
clearVelocityTracking({ refs }) {
277+
refs.get("dragManager").clearVelocityTracking()
341278
},
342279

343280
toggleVisibility({ event, send, prop }) {
@@ -404,10 +341,9 @@ export const machine = createMachine<BottomSheetSchema>({
404341
}
405342

406343
function onPointerUp(event: PointerEvent) {
407-
if (event.pointerType !== "touch") {
408-
const point = getEventPoint(event)
409-
send({ type: "POINTER_UP", point })
410-
}
344+
if (event.pointerType === "touch") return
345+
const point = getEventPoint(event)
346+
send({ type: "POINTER_UP", point })
411347
}
412348

413349
function onTouchStart(event: TouchEvent) {
@@ -428,6 +364,7 @@ export const machine = createMachine<BottomSheetSchema>({
428364
// Prevent overscrolling
429365
const contentEl = dom.getContentEl(scope)
430366
if (!contentEl) return
367+
431368
let el: HTMLElement | null = target
432369
while (el && el !== contentEl && el.scrollHeight <= el.clientHeight) {
433370
el = el.parentElement
@@ -455,12 +392,14 @@ export const machine = createMachine<BottomSheetSchema>({
455392
send({ type: "POINTER_UP", point })
456393
}
457394

395+
const doc = scope.getDoc()
396+
458397
const cleanups = [
459-
addDomEvent(scope.getDoc(), "pointermove", onPointerMove),
460-
addDomEvent(scope.getDoc(), "pointerup", onPointerUp),
461-
addDomEvent(scope.getDoc(), "touchstart", onTouchStart, { passive: false }),
462-
addDomEvent(scope.getDoc(), "touchmove", onTouchMove, { passive: false }),
463-
addDomEvent(scope.getDoc(), "touchend", onTouchEnd),
398+
addDomEvent(doc, "pointermove", onPointerMove),
399+
addDomEvent(doc, "pointerup", onPointerUp),
400+
addDomEvent(doc, "touchstart", onTouchStart, { passive: false }),
401+
addDomEvent(doc, "touchmove", onTouchMove, { passive: false }),
402+
addDomEvent(doc, "touchend", onTouchEnd),
464403
]
465404

466405
return () => {

0 commit comments

Comments
 (0)