Skip to content

Swipe button doesn't work on True Sheet #713

Description

@vicovictor

Before submitting a new issue

  • I tested using the latest version of the library.
  • I tested using a supported version of React Native.
  • I checked for existing issues that might answer my question.

Bug Summary

Using Expo 55. Android. I have a swipe button (like iOS swipe button) on True Sheet. It has 3 detents: low, default, full. Only in full detent swipe button can be swiped, the rest would revert itself to its original position half way, or won't register at all when it reaches the end. Claude had to create this patch for it to work.

Added horizontal drag detection in onInterceptTouchEvent that calls parent.requestDisallowInterceptTouchEvent(true) when it detects the user is swiping horizontally (|dx| > touchSlop && |dx| > |dy| × 1.5). This propagates FLAG_DISALLOW_INTERCEPT up through footer → container → sheet → CoordinatorLayout, preventing BottomSheetBehavior from ever seeing the ACTION_MOVE events.

Affected Platforms

  • iOS
  • Android
  • Web
  • Other

Library Version

3.10.1

Environment Info

Expo 55

Steps to Reproduce

See sample code below

Repro

NA

Additional Context

Here's the patch:

+diff --git a/node_modules/@lodev09/react-native-true-sheet/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt b/node_modules/@lodev09/react-native-true-sheet/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt
+index b42f46d..81c1b40 100644
+--- a/node_modules/@lodev09/react-native-true-sheet/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt
++++ b/node_modules/@lodev09/react-native-true-sheet/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt
+@@ -3,6 +3,7 @@ package com.lodev09.truesheet
+ import android.annotation.SuppressLint
+ import android.view.MotionEvent
+ import android.view.View
++import android.view.ViewConfiguration
+ import com.facebook.react.uimanager.JSPointerDispatcher
+ import com.facebook.react.uimanager.JSTouchDispatcher
+ import com.facebook.react.uimanager.PointerEvents
+@@ -42,6 +43,13 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
+   private val jsTouchDispatcher = JSTouchDispatcher(this)
+   private var jsPointerDispatcher: JSPointerDispatcher? = null
+ 
++  // Horizontal drag detection to prevent BottomSheetBehavior from intercepting
++  // swipe gestures in the footer (e.g. swipe-to-confirm buttons)
++  private val touchSlop: Int = ViewConfiguration.get(reactContext).scaledTouchSlop
++  private var footerInitialX = 0f
++  private var footerInitialY = 0f
++  private var horizontalDragClaimed = false
++
+   init {
+     jsPointerDispatcher = JSPointerDispatcher(this)
+   }
+@@ -59,6 +67,31 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
+   // ==================== RootView Implementation ====================
+ 
+   override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
++    // Detect horizontal drags and prevent BottomSheetBehavior from intercepting.
++    // This propagates FLAG_DISALLOW_INTERCEPT up through the view hierarchy
++    // (footer → container → sheet → CoordinatorLayout), preventing the
++    // BottomSheetBehavior from stealing touch events during horizontal swipes.
++    when (event.actionMasked) {
++      MotionEvent.ACTION_DOWN -> {
++        horizontalDragClaimed = false
++        footerInitialX = event.x
++        footerInitialY = event.y
++      }
++      MotionEvent.ACTION_MOVE -> {
++        if (!horizontalDragClaimed) {
++          val dx = event.x - footerInitialX
++          val dy = event.y - footerInitialY
++          if (kotlin.math.abs(dx) > touchSlop && kotlin.math.abs(dx) > kotlin.math.abs(dy) * 1.5f) {
++            horizontalDragClaimed = true
++            parent?.requestDisallowInterceptTouchEvent(true)
++          }
++        }
++      }
++      MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
++        horizontalDragClaimed = false
++      }
++    }
++
+     eventDispatcher?.let { dispatcher ->
+       jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
+       jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
+diff --git a/node_modules/@lodev09/react-native-true-sheet/lib/module/TrueSheet.js b/node_modules/@lodev09/react-native-true-sheet/lib/module/TrueSheet.js
+index 6b090ff..c997926 100644
+--- a/node_modules/@lodev09/react-native-true-sheet/lib/module/TrueSheet.js
++++ b/node_modules/@lodev09/react-native-true-sheet/lib/module/TrueSheet.js
+@@ -258,8 +258,16 @@ export class TrueSheet extends PureComponent {
+   }
+   handleBackPress() {
+     if (!this.isPresented || !this.isSheetVisible) return false;
+-    TrueSheetModule?.handleBackPress(this.handle);
+-    return this.props.onBackPress?.() ?? true;
++    const backPressResult = this.props.onBackPress?.();
++    if (backPressResult === false) return false;
++    if (typeof TrueSheetModule?.handleBackPress === 'function') {
++      TrueSheetModule.handleBackPress(this.handle);
++    } else if (this.props.dismissible !== false) {
++      void this.dismiss().catch(error => {
++        console.warn('TrueSheet: failed to handle back press', error);
++      });
++    }
++    return backPressResult ?? true;
+   }
+ 
+   /**
+diff --git a/node_modules/@lodev09/react-native-true-sheet/src/TrueSheet.tsx b/node_modules/@lodev09/react-native-true-sheet/src/TrueSheet.tsx
+index 8843824..406e27e 100644
+--- a/node_modules/@lodev09/react-native-true-sheet/src/TrueSheet.tsx
++++ b/node_modules/@lodev09/react-native-true-sheet/src/TrueSheet.tsx
+@@ -352,8 +352,18 @@ export class TrueSheet extends PureComponent<TrueSheetProps, TrueSheetState> {
+   private handleBackPress(): boolean {
+     if (!this.isPresented || !this.isSheetVisible) return false;
+ 
+-    TrueSheetModule?.handleBackPress(this.handle);
+-    return this.props.onBackPress?.() ?? true;
++    const backPressResult = this.props.onBackPress?.();
++    if (backPressResult === false) return false;
++
++    if (typeof TrueSheetModule?.handleBackPress === 'function') {
++      TrueSheetModule.handleBackPress(this.handle);
++    } else if (this.props.dismissible !== false) {
++      void this.dismiss().catch((error) => {
++        console.warn('TrueSheet: failed to handle back press', error);
++      });
++    }
++
++    return backPressResult ?? true;
+   }
+ 
+   /**

Here's my swipe button:

import { LoadingView } from '@/components/common/LoadingView';
import { PlatformIcon } from '@/components/common/PlatformIcon';
import MaskedView from '@react-native-masked-view/masked-view';
import { SystemColor } from '@/design';
import { color } from '@/design/colors';
import { tokens } from '@/design/tokens';
import { typography } from '@/design/typography';
import * as Haptics from 'expo-haptics';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { getAppLocale } from '@/i18n';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Animated, Platform, Text, View } from 'react-native';

const TRACK_HEIGHT = 62;
const TRACK_EDGE_INSET = 2;
const TRACK_HORIZONTAL_PADDING = TRACK_EDGE_INSET;
const TRACK_VERTICAL_PADDING = TRACK_EDGE_INSET;
const HANDLE_SIZE = TRACK_HEIGHT - (TRACK_EDGE_INSET * 2);
const HANDLE_BORDER_RADIUS = HANDLE_SIZE / 2;
const PROGRESS_VERTICAL_INSET = TRACK_VERTICAL_PADDING;
const TRACK_RIGHT_INSET = 2;
const HANDLE_SPRING_SPEED = 24;
const HANDLE_SUBMIT_SPRING_SPEED = 28;
const HANDLE_SPRING_BOUNCINESS = 0;
const TRACK_DIM_MAX_OPACITY = 0.09;
const SUBMIT_FLASH_MAX_OPACITY = 0.14;
const SUBMIT_FLASH_IN_DURATION_MS = 150;
const SUBMIT_FLASH_OUT_DURATION_MS = 100;
const GRADIENT_LABEL_FADE_END_RATIO = 0.78;
const LIGHT_LABEL_FADE_START_RATIO = 0.18;
const SUBMIT_CALLBACK_DELAY_MS = 180;
const SUCCESS_ICON_DURATION_MS = 220;
const FALLBACK_SUCCESS_DELAY_MS = 900;

const BUTTON_CONTENT_MIN_HEIGHT = Math.max(
  24,
  typeof typography.h2.lineHeight === 'number' ? typography.h2.lineHeight : 24
);

const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

export interface SwipeActionButtonProps {
  label: string;
  onSwipeComplete: () => void;
  loading?: boolean;
  variant?: 'primary' | 'tertiary';
  danger?: boolean;
  accessibilityLabel?: string;
  onSwipeInteractionChange?: (isInteracting: boolean) => void;
}

export function SwipeActionButton({
  label,
  onSwipeComplete,
  loading = false,
  variant = 'primary',
  danger = false,
  accessibilityLabel,
  onSwipeInteractionChange,
}: SwipeActionButtonProps) {
  const locale = getAppLocale();
  const needsComplexScriptPadding = locale === 'km' || locale === 'my';
  const scriptVerticalInset = needsComplexScriptPadding ? (locale === 'my' ? 4 : 2) : 0;
  const labelTextStyle = locale === 'my'
    ? {
      fontFamily: Platform.select({
        ios: 'Myanmar Sangam MN',
        android: 'sans-serif',
        default: undefined,
      }),
      fontWeight: tokens.fontWeight.regular,
    }
    : undefined;

  const translateX = useRef(new Animated.Value(0)).current;
  const handleScale = useRef(new Animated.Value(1)).current;
  const submitFlash = useRef(new Animated.Value(0)).current;
  const dragStartX = useRef(0);
  const submitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const fallbackSuccessTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const isDraggingRef = useRef(false);
  const interactionNotifiedRef = useRef(false);
  const didTriggerEndHapticRef = useRef(false);
  const loadingSeenRef = useRef(false);
  const submittedRef = useRef(false);
  const [isSubmittingVisual, setIsSubmittingVisual] = useState(false);
  const [showSuccessIcon, setShowSuccessIcon] = useState(false);
  const [trackWidth, setTrackWidth] = useState(0);

  const maxTranslate = Math.max(
    trackWidth - HANDLE_SIZE - (TRACK_HORIZONTAL_PADDING * 2) - TRACK_RIGHT_INSET,
    0
  );
  const progressLeftInset = TRACK_HORIZONTAL_PADDING;
  const restingProgressWidth = HANDLE_SIZE;
  const progressMax = Math.max(maxTranslate, 1);
  const gradientFadeEnd = Math.max(progressMax * GRADIENT_LABEL_FADE_END_RATIO, 1);
  const lightFadeStart = progressMax * LIGHT_LABEL_FADE_START_RATIO;
  const isPrimary = variant === 'primary';
  const gradientLabelOpacity = translateX.interpolate({
    inputRange: [0, gradientFadeEnd],
    outputRange: [1, 0],
    extrapolate: 'clamp',
  });
  const lightLabelOpacity = translateX.interpolate({
    inputRange: [lightFadeStart, progressMax],
    outputRange: [0, 1],
    extrapolate: 'clamp',
  });
  const progressWidth = translateX.interpolate({
    inputRange: [0, progressMax],
    outputRange: [
      restingProgressWidth,
      maxTranslate + restingProgressWidth,
    ],
    extrapolate: 'clamp',
  });
  const dimOpacity = translateX.interpolate({
    inputRange: [0, progressMax],
    outputRange: [0, TRACK_DIM_MAX_OPACITY],
    extrapolate: 'clamp',
  });
  const submitFlashOpacity = submitFlash.interpolate({
    inputRange: [0, 1],
    outputRange: [0, SUBMIT_FLASH_MAX_OPACITY],
    extrapolate: 'clamp',
  });

  const animateHandle = useCallback((value: number) => {
    Animated.spring(translateX, {
      toValue: value,
      useNativeDriver: false,
      speed: HANDLE_SPRING_SPEED,
      bounciness: HANDLE_SPRING_BOUNCINESS,
    }).start();
  }, [translateX]);

  const clearSubmitTimers = useCallback(() => {
    if (submitTimeoutRef.current) {
      clearTimeout(submitTimeoutRef.current);
      submitTimeoutRef.current = null;
    }
    if (successTimeoutRef.current) {
      clearTimeout(successTimeoutRef.current);
      successTimeoutRef.current = null;
    }
    if (fallbackSuccessTimeoutRef.current) {
      clearTimeout(fallbackSuccessTimeoutRef.current);
      fallbackSuccessTimeoutRef.current = null;
    }
  }, []);

  const setSwipeInteractionState = useCallback((isInteracting: boolean) => {
    if (interactionNotifiedRef.current === isInteracting) return;
    interactionNotifiedRef.current = isInteracting;
    onSwipeInteractionChange?.(isInteracting);
  }, [onSwipeInteractionChange]);

  const fireHaptic = useCallback((style: Haptics.ImpactFeedbackStyle) => {
    Promise.resolve(Haptics.impactAsync(style)).catch(() => undefined);
  }, []);

  useEffect(() => {
    if (!loading && !isSubmittingVisual && !showSuccessIcon && !submittedRef.current) {
      handleScale.setValue(1);
      submitFlash.setValue(0);
      animateHandle(0);
    }
  }, [animateHandle, handleScale, isSubmittingVisual, loading, maxTranslate, showSuccessIcon, submitFlash]);

  const transitionToSuccess = useCallback(() => {
    if (!submittedRef.current) return;

    clearSubmitTimers();
    setIsSubmittingVisual(false);
    setShowSuccessIcon(true);
    loadingSeenRef.current = false;

    Animated.timing(submitFlash, {
      toValue: 0,
      duration: SUBMIT_FLASH_OUT_DURATION_MS,
      useNativeDriver: false,
    }).start();

    successTimeoutRef.current = setTimeout(() => {
      setShowSuccessIcon(false);
      submittedRef.current = false;
      loadingSeenRef.current = false;

      Animated.parallel([
        Animated.spring(translateX, {
          toValue: 0,
          useNativeDriver: false,
          speed: HANDLE_SPRING_SPEED,
          bounciness: HANDLE_SPRING_BOUNCINESS,
        }),
        Animated.spring(handleScale, {
          toValue: 1,
          useNativeDriver: false,
          speed: HANDLE_SPRING_SPEED,
          bounciness: HANDLE_SPRING_BOUNCINESS,
        }),
      ]).start();
    }, SUCCESS_ICON_DURATION_MS);
  }, [clearSubmitTimers, handleScale, submitFlash, translateX]);

  const resetFromInterruptedGesture = useCallback(() => {
    clearSubmitTimers();
    submittedRef.current = false;
    loadingSeenRef.current = false;
    isDraggingRef.current = false;
    didTriggerEndHapticRef.current = false;
    setSwipeInteractionState(false);
    setIsSubmittingVisual(false);
    setShowSuccessIcon(false);

    Animated.parallel([
      Animated.spring(translateX, {
        toValue: 0,
        useNativeDriver: false,
        speed: HANDLE_SPRING_SPEED,
        bounciness: HANDLE_SPRING_BOUNCINESS,
      }),
      Animated.spring(handleScale, {
        toValue: 1,
        useNativeDriver: false,
        speed: HANDLE_SPRING_SPEED,
        bounciness: HANDLE_SPRING_BOUNCINESS,
      }),
      Animated.timing(submitFlash, {
        toValue: 0,
        duration: SUBMIT_FLASH_OUT_DURATION_MS,
        useNativeDriver: false,
      }),
    ]).start();
  }, [clearSubmitTimers, handleScale, setSwipeInteractionState, submitFlash, translateX]);

  useEffect(() => {
    if (!isSubmittingVisual) return;

    if (loading) {
      loadingSeenRef.current = true;
      return;
    }

    if (loadingSeenRef.current) {
      transitionToSuccess();
      return;
    }

    fallbackSuccessTimeoutRef.current = setTimeout(() => {
      transitionToSuccess();
    }, FALLBACK_SUCCESS_DELAY_MS);

    return () => {
      if (fallbackSuccessTimeoutRef.current) {
        clearTimeout(fallbackSuccessTimeoutRef.current);
        fallbackSuccessTimeoutRef.current = null;
      }
    };
  }, [isSubmittingVisual, loading, transitionToSuccess]);

  useEffect(() => {
    return () => {
      clearSubmitTimers();
      didTriggerEndHapticRef.current = false;
      setSwipeInteractionState(false);
    };
  }, [clearSubmitTimers, setSwipeInteractionState]);

  const handleRelease = useCallback((dx: number) => {
    isDraggingRef.current = false;
    didTriggerEndHapticRef.current = false;
    setSwipeInteractionState(false);
    if (loading || submittedRef.current || isSubmittingVisual) {
      return;
    }

    const clampedX = clamp(dx, 0, maxTranslate);

    if (maxTranslate > 0 && clampedX >= maxTranslate) {
      submittedRef.current = true;
      loadingSeenRef.current = false;
      setIsSubmittingVisual(true);
      setShowSuccessIcon(false);

      clearSubmitTimers();
      const settleTranslateX = clampedX >= (maxTranslate - 1) ? clampedX : maxTranslate;

      Animated.parallel([
        Animated.spring(translateX, {
          toValue: settleTranslateX,
          useNativeDriver: false,
          speed: HANDLE_SUBMIT_SPRING_SPEED,
          bounciness: HANDLE_SPRING_BOUNCINESS,
        }),
        Animated.timing(submitFlash, {
          toValue: 1,
          duration: SUBMIT_FLASH_IN_DURATION_MS,
          useNativeDriver: false,
        }),
      ]).start();

      submitTimeoutRef.current = setTimeout(() => {
        onSwipeComplete();
      }, SUBMIT_CALLBACK_DELAY_MS);
      return;
    }

    clearSubmitTimers();
    submittedRef.current = false;
    loadingSeenRef.current = false;
    setIsSubmittingVisual(false);
    setShowSuccessIcon(false);
    Animated.parallel([
      Animated.spring(translateX, {
        toValue: 0,
        useNativeDriver: false,
        speed: HANDLE_SPRING_SPEED,
        bounciness: HANDLE_SPRING_BOUNCINESS,
      }),
      Animated.spring(handleScale, {
        toValue: 1,
        useNativeDriver: false,
        speed: HANDLE_SPRING_SPEED,
        bounciness: HANDLE_SPRING_BOUNCINESS,
      }),
      Animated.timing(submitFlash, {
        toValue: 0,
        duration: SUBMIT_FLASH_OUT_DURATION_MS,
        useNativeDriver: false,
      }),
    ]).start();
  }, [
    clearSubmitTimers,
    handleScale,
    isSubmittingVisual,
    loading,
    maxTranslate,
    onSwipeComplete,
    submitFlash,
    setSwipeInteractionState,
    translateX,
  ]);

  const handleGrant = useCallback((event: any) => {
    if (loading || submittedRef.current) return;
    isDraggingRef.current = true;
    didTriggerEndHapticRef.current = false;
    setSwipeInteractionState(true);
    fireHaptic(Haptics.ImpactFeedbackStyle.Light);
    dragStartX.current = event?.nativeEvent?.pageX ?? 0;
    handleScale.stopAnimation(() => {
      handleScale.setValue(1);
    });
    translateX.stopAnimation();
  }, [fireHaptic, handleScale, loading, setSwipeInteractionState, translateX]);

  const handleMove = useCallback((event: any) => {
    if (loading || submittedRef.current) return;
    const pageX = event?.nativeEvent?.pageX ?? dragStartX.current;
    const dx = pageX - dragStartX.current;
    const clampedDx = clamp(dx, 0, maxTranslate);
    translateX.setValue(clampedDx);
    if (
      maxTranslate > 0 &&
      clampedDx >= maxTranslate &&
      !didTriggerEndHapticRef.current
    ) {
      didTriggerEndHapticRef.current = true;
      fireHaptic(Haptics.ImpactFeedbackStyle.Heavy);
    }
  }, [fireHaptic, loading, maxTranslate, translateX]);

  const handleResponderEnd = useCallback((event: any) => {
    const pageX = event?.nativeEvent?.pageX ?? dragStartX.current;
    handleRelease(pageX - dragStartX.current);
  }, [handleRelease]);

  const handleResponderTerminate = useCallback(() => {
    if (isSubmittingVisual) return;
    resetFromInterruptedGesture();
  }, [isSubmittingVisual, resetFromInterruptedGesture]);

  const labelLineHeight = BUTTON_CONTENT_MIN_HEIGHT + (scriptVerticalInset * 2);
  const sharedLabelTextStyle = {
    ...typography.h2,
    ...(labelTextStyle || {}),
    textAlign: 'center' as const,
    lineHeight: labelLineHeight,
    paddingTop: scriptVerticalInset,
    paddingBottom: scriptVerticalInset,
  };

  const renderLabel = () => {
    if (!isPrimary) {
      return (
        <Text
          numberOfLines={1}
          style={{
            ...sharedLabelTextStyle,
            color: danger ? SystemColor('systemRed') : SystemColor('label'),
          }}
        >
          {label}
        </Text>
      );
    }

    const gradientTextMask = (
      <Text
        numberOfLines={1}
        style={{
          ...sharedLabelTextStyle,
          color: 'black',
        }}
      >
        {label}
      </Text>
    );

    const gradientLayer = (
      <MaskedView
        testID="swipe-action-label-gradient"
        style={{ minHeight: labelLineHeight }}
        maskElement={(
          <View style={{ minHeight: labelLineHeight, justifyContent: 'center' }}>
            {gradientTextMask}
          </View>
        )}
      >
        <LinearGradient
          colors={color.brand.colors}
          start={color.brand.start}
          end={color.brand.end}
        >
          <View style={{ opacity: 0, minHeight: labelLineHeight, justifyContent: 'center' }}>
            {gradientTextMask}
          </View>
        </LinearGradient>
      </MaskedView>
    );

    return (
      <View
        style={{
          minHeight: labelLineHeight,
          justifyContent: 'center',
          alignItems: 'center',
          position: 'relative',
        }}
      >
        <Animated.View
          testID="swipe-action-label-gradient-layer"
          style={{ opacity: gradientLabelOpacity }}
        >
          {gradientLayer}
        </Animated.View>
        <Animated.Text
          testID="swipe-action-label-light"
          numberOfLines={1}
          style={{
            ...sharedLabelTextStyle,
            color: '#fff',
            position: 'absolute',
            left: 0,
            right: 0,
            opacity: lightLabelOpacity,
          }}
        >
          {label}
        </Animated.Text>
      </View>
    );
  };

  const iconColor = SystemColor('lightText');

  const renderHandleIcon = () => {
    if (loading || isSubmittingVisual) {
      return <LoadingView size="small" color={iconColor as any} />;
    }

    if (showSuccessIcon) {
      return (
        <PlatformIcon
          name="checkmark"
          androidName="check"
          size={24}
          color={iconColor as any}
        />
      );
    }

    return (
      <PlatformIcon
        name="chevron-forward-outline"
        androidName="chevron-right"
        size={24}
        color={iconColor as any}
      />
    );
  };

  return (
    <GlassView
      testID="swipe-action-track"
      accessibilityRole="button"
      accessibilityLabel={accessibilityLabel || label}
      glassEffectStyle="regular"
      style={{
        width: '100%',
        minHeight: TRACK_HEIGHT,
        borderRadius: tokens.radius.full,
        backgroundColor: !isLiquidGlassAvailable() ? SystemColor('secondarySystemFill') : undefined,
        borderColor: SystemColor('separator'),
        borderWidth: !isLiquidGlassAvailable() ? 0.5 : 0,
        justifyContent: 'center',
        paddingHorizontal: TRACK_HORIZONTAL_PADDING,
        paddingVertical: TRACK_VERTICAL_PADDING,
        overflow: 'hidden',
      }}
      onLayout={(event) => {
        const width = event.nativeEvent.layout.width;
        if (width !== trackWidth) {
          setTrackWidth(width);
        }
      }}
    >
      <Animated.View
        testID="swipe-action-progress"
        pointerEvents="none"
        style={{
          position: 'absolute',
          top: PROGRESS_VERTICAL_INSET,
          bottom: PROGRESS_VERTICAL_INSET,
          left: progressLeftInset,
          width: progressWidth,
          borderRadius: tokens.radius.full,
          opacity: 1,
          overflow: 'hidden',
        }}
      >
        <LinearGradient
          colors={color.brand.colors}
          start={color.brand.start}
          end={color.brand.end}
          style={{ flex: 1 }}
        />
      </Animated.View>

      <Animated.View
        testID="swipe-action-dim"
        pointerEvents="none"
        style={{
          position: 'absolute',
          top: 0,
          bottom: 0,
          left: 0,
          right: 0,
          backgroundColor: '#000',
          opacity: dimOpacity,
        }}
      />

      <Animated.View
        testID="swipe-action-submit-flash"
        pointerEvents="none"
        style={{
          position: 'absolute',
          top: 0,
          bottom: 0,
          left: 0,
          right: 0,
          backgroundColor: '#fff',
          opacity: submitFlashOpacity,
        }}
      />

      <Animated.View
        testID="swipe-action-label"
        pointerEvents="none"
        style={{
          position: 'absolute',
          left: tokens.spacing.lg,
          right: tokens.spacing.lg,
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {renderLabel()}
      </Animated.View>

      <Animated.View
        testID="swipe-action-handle"
        onStartShouldSetResponder={() => !loading && !submittedRef.current}
        onMoveShouldSetResponder={() => !loading && !submittedRef.current}
        onResponderTerminationRequest={() => !isDraggingRef.current}
        onResponderGrant={handleGrant}
        onResponderMove={handleMove}
        onResponderRelease={handleResponderEnd}
        onResponderTerminate={handleResponderTerminate}
        style={{
          width: HANDLE_SIZE,
          height: HANDLE_SIZE,
          borderRadius: HANDLE_BORDER_RADIUS,
          transform: [{ scale: handleScale }, { translateX }],
        }}
      >
        <View
          testID="swipe-action-handle-glass"
          style={{
            flex: 1,
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: HANDLE_BORDER_RADIUS,
            overflow: 'hidden',
          }}
        >
          {renderHandleIcon()}
        </View>
      </Animated.View>
    </GlassView>
  );
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds reproNeed to replicate this issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions