-
Notifications
You must be signed in to change notification settings - Fork 0
Add pull-down gesture dismissal for mobile modals #1683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from "react"; | |||||||
| import { useEffect, useMemo, useState } from "react"; | ||||||||
| import type { PressableProps } from "react-native"; | ||||||||
| import { Modal, Platform, View } from "react-native"; | ||||||||
| import { Gesture, GestureDetector } from "react-native-gesture-handler"; | ||||||||
| import Reanimated, { | ||||||||
| Easing, | ||||||||
| Extrapolation, | ||||||||
|
|
@@ -13,8 +14,10 @@ import Reanimated, { | |||||||
| withSpring, | ||||||||
| withTiming, | ||||||||
| } from "react-native-reanimated"; | ||||||||
| import { runOnJS } from "react-native-worklets"; | ||||||||
|
|
||||||||
| import { useEventCallback } from "../hooks/useEventCallback"; | ||||||||
| import { hapticImpactIfMobile } from "../hooks/hapticImpactIfMobile"; | ||||||||
| import { ReanimatedPressable } from "./ReanimatedPressable"; | ||||||||
|
|
||||||||
| export type PageSheetChild = (options: { dismiss: () => void }) => ReactNode; | ||||||||
|
|
@@ -214,6 +217,8 @@ const IosImpl = ({ onDismiss, children }: ImplProps) => { | |||||||
| [onDismiss], | ||||||||
| ); | ||||||||
|
|
||||||||
| // iOS pageSheet already supports native pull-to-dismiss, | ||||||||
| // so we rely on that instead of adding custom gestures | ||||||||
| return ( | ||||||||
| <Modal | ||||||||
| animationType="slide" | ||||||||
|
|
@@ -243,20 +248,80 @@ const IosImpl = ({ onDismiss, children }: ImplProps) => { | |||||||
| }; | ||||||||
|
|
||||||||
| const DefaultImpl = ({ children, onDismiss }: ImplProps) => { | ||||||||
| const translateY = useSharedValue(0); | ||||||||
| const [dismissing, setDismissing] = useState(false); | ||||||||
|
|
||||||||
| const api = useMemo( | ||||||||
| () => ({ | ||||||||
| dismiss: onDismiss, | ||||||||
| dismiss: () => { | ||||||||
| setDismissing(true); | ||||||||
| }, | ||||||||
| }), | ||||||||
| [onDismiss], | ||||||||
| [], | ||||||||
| ); | ||||||||
|
|
||||||||
| const handleDismiss = useEventCallback(() => { | ||||||||
| hapticImpactIfMobile(); | ||||||||
| onDismiss(); | ||||||||
| }); | ||||||||
|
|
||||||||
| // Pan gesture for pull-to-dismiss | ||||||||
| const panGesture = Gesture.Pan() | ||||||||
| .activeOffsetY(10) // Only activate when dragging down more than 10px | ||||||||
| .onUpdate((event) => { | ||||||||
| // Only allow downward movement | ||||||||
| if (event.translationY > 0) { | ||||||||
| // Add resistance effect - movement becomes harder as you drag further | ||||||||
| const resistance = Math.min(event.translationY / 3, 200); | ||||||||
| translateY.set(resistance); | ||||||||
| } | ||||||||
| }) | ||||||||
| .onEnd((event) => { | ||||||||
| const shouldDismiss = | ||||||||
| event.translationY > 100 || // Dragged far enough | ||||||||
| (event.translationY > 40 && event.velocityY > 800); // Or sufficient velocity | ||||||||
|
||||||||
| (event.translationY > 40 && event.velocityY > 800); // Or sufficient velocity | |
| event.translationY > DISMISS_DISTANCE_THRESHOLD || // Dragged far enough | |
| (event.translationY > VELOCITY_DISMISS_DISTANCE_THRESHOLD && event.velocityY > VELOCITY_THRESHOLD); // Or sufficient velocity |
Copilot
AI
Aug 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The animation value 600 appears in multiple places (lines 287 and 301) and should be extracted as a named constant like DISMISS_ANIMATION_DISTANCE = 600 to avoid duplication and make it easier to adjust if needed.
| withTiming(600, { duration: 250 }, () => { | |
| withTiming(DISMISS_ANIMATION_DISTANCE, { duration: 250 }, () => { |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,30 @@ | ||
| import { WikiHanziWordModal } from "@/client/ui/WikiHanziWordModal"; | ||
| import { useState } from "react"; | ||
| import { RectButton } from "./RectButton"; | ||
|
|
||
| export default () => { | ||
| const [showModal, setShowModal] = useState(false); | ||
|
|
||
| return ( | ||
| <WikiHanziWordModal | ||
| devUiSnapshotMode | ||
| hanziWord={`你好:hello`} | ||
| onDismiss={() => null} | ||
| /> | ||
| <> | ||
| <RectButton | ||
| variant="filled" | ||
| onPress={() => { | ||
| setShowModal(true); | ||
| }} | ||
| > | ||
| Open Wiki Modal (Test Pull-Down Gesture) | ||
| </RectButton> | ||
|
|
||
| {showModal && ( | ||
| <WikiHanziWordModal | ||
| devUiSnapshotMode={false} | ||
| hanziWord={`你好:hello`} | ||
| onDismiss={() => { | ||
| setShowModal(false); | ||
| }} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic numbers
3and200should be extracted as named constants to improve code readability and maintainability. Consider definingRESISTANCE_FACTOR = 3andMAX_RESISTANCE_OFFSET = 200at the top of the component.