diff --git a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
index 0bda4dc3f..d69ff368f 100644
--- a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
@@ -5,7 +5,7 @@ import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-rout
import { useState, useEffect, useRef, useCallback } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingView, Platform, Pressable } from 'react-native';
+import { StyleSheet, View, Alert, Keyboard, KeyboardAvoidingView, Platform } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import Toast from 'react-native-toast-message';
@@ -28,6 +28,7 @@ import LoadingOverlay from '@/components/LoadingOverlay';
import { ThemedContainer } from '@/components/themed/ThemedContainer';
import { ThemedText } from '@/components/themed/ThemedText';
import { AliasVaultToast } from '@/components/Toast';
+import { RobustPressable } from '@/components/ui/RobustPressable';
import { useAuth } from '@/context/AuthContext';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
@@ -536,51 +537,44 @@ export default function AddEditCredentialScreen() : React.ReactNode {
// Set header buttons
useEffect(() => {
- if (Platform.OS === 'ios') {
- navigation.setOptions({
+ navigation.setOptions({
+ /**
+ * Header left button (iOS only).
+ */
+ ...(Platform.OS === 'ios' && {
/**
- * Header left button.
+ *
*/
headerLeft: () => (
- router.back()}
style={styles.headerLeftButton}
+ accessibilityRole="button"
+ accessibilityLabel="Cancel"
>
{t('common.cancel')}
-
+
),
- /**
- * Header right button.
- */
- headerRight: () => (
-
-
-
- ),
- });
- } else {
- navigation.setOptions({
- /**
- * Header right button.
- */
- headerRight: () => (
-
-
-
- ),
- });
- }
+ }),
+ /**
+ * Header right button.
+ */
+ headerRight: () => (
+
+
+
+ ),
+ });
}, [navigation, mode, handleSubmit, onSubmit, colors.primary, isEditMode, router, styles.headerLeftButton, styles.headerLeftButtonText, styles.headerRightButton, styles.headerRightButtonDisabled, isSaveDisabled, t]);
return (
@@ -602,9 +596,11 @@ export default function AddEditCredentialScreen() : React.ReactNode {
>
{!isEditMode && (
- setMode('random')}
+ accessibilityRole="button"
+ accessibilityState={{ selected: mode === 'random' }}
>
{t('credentials.randomAlias')}
-
-
+ setMode('manual')}
+ accessibilityRole="button"
+ accessibilityState={{ selected: mode === 'manual' }}
>
{t('credentials.manual')}
-
+
)}
@@ -702,10 +700,15 @@ export default function AddEditCredentialScreen() : React.ReactNode {
{t('credentials.alias')}
-
+
{t('credentials.generateRandomAlias')}
-
+
{isEditMode && (
-
{t('credentials.deleteCredential')}
-
+
)}
>
)}
diff --git a/apps/mobile-app/components/ui/RobustPressable.tsx b/apps/mobile-app/components/ui/RobustPressable.tsx
new file mode 100644
index 000000000..eea60ddab
--- /dev/null
+++ b/apps/mobile-app/components/ui/RobustPressable.tsx
@@ -0,0 +1,105 @@
+import React, { useRef, useCallback } from 'react';
+import { Pressable, PressableProps, GestureResponderEvent, Platform } from 'react-native';
+
+interface IRobustPressableProps extends Omit {
+ onPress?: (event: GestureResponderEvent) => void;
+ children: React.ReactNode;
+ activeOpacity?: number;
+ style?: PressableProps['style'];
+}
+
+/**
+ * A more robust Pressable component that better handles Magic Keyboard trackpad interactions.
+ * This component ensures clicks register even when the cursor is moving slightly during tap,
+ * while maintaining TouchableOpacity-like activeOpacity behavior.
+ */
+export const RobustPressable: React.FC = ({
+ onPress,
+ onPressIn,
+ onPressOut,
+ hitSlop,
+ pressRetentionOffset,
+ delayLongPress,
+ disabled,
+ activeOpacity = 0.7,
+ style,
+ ...props
+}) => {
+ const pressStartTime = useRef(0);
+ const pressStartLocation = useRef<{ x: number; y: number } | null>(null);
+ const isPressing = useRef(false);
+ const hasMoved = useRef(false);
+
+ const handlePressIn = useCallback((event: GestureResponderEvent) => {
+ pressStartTime.current = Date.now();
+ pressStartLocation.current = {
+ x: event.nativeEvent.pageX,
+ y: event.nativeEvent.pageY
+ };
+ isPressing.current = true;
+ hasMoved.current = false;
+
+ onPressIn?.(event);
+ }, [onPressIn]);
+
+ const handlePressOut = useCallback((event: GestureResponderEvent) => {
+ const pressDuration = Date.now() - pressStartTime.current;
+
+ // Check if the press moved too much (for trackpad detection)
+ if (pressStartLocation.current) {
+ const moveDistance = Math.sqrt(
+ Math.pow(event.nativeEvent.pageX - pressStartLocation.current.x, 2) +
+ Math.pow(event.nativeEvent.pageY - pressStartLocation.current.y, 2)
+ );
+
+ // Allow up to 15 pixels of movement for trackpad taps
+ if (moveDistance > 15) {
+ hasMoved.current = true;
+ }
+ }
+
+ /**
+ * For iPad with trackpad/mouse: be more lenient with tap detection
+ * Accept taps up to 600ms and allow small movements
+ */
+ if (isPressing.current && pressDuration < 600 && !hasMoved.current) {
+ onPress?.(event);
+ }
+
+ isPressing.current = false;
+ pressStartLocation.current = null;
+ onPressOut?.(event);
+ }, [onPress, onPressOut]);
+
+ // Increase hit slop for better trackpad interaction
+ const enhancedHitSlop = Platform.select({
+ ios: hitSlop ?? { top: 15, bottom: 15, left: 15, right: 15 },
+ default: hitSlop ?? { top: 12, bottom: 12, left: 12, right: 12 }
+ });
+
+ // Increase press retention offset to handle cursor movement during tap
+ const enhancedPressRetentionOffset = pressRetentionOffset ?? {
+ top: 25,
+ bottom: 25,
+ left: 25,
+ right: 25
+ };
+
+ return (
+ [
+ typeof style === 'function' ? style({ pressed, hovered }) : style,
+ pressed && { opacity: activeOpacity },
+ hovered && Platform.OS !== 'ios' && { opacity: Math.max(activeOpacity, 0.8) }
+ ]}
+ onPress={undefined} // We handle onPress in onPressOut for better trackpad support
+ onPressIn={handlePressIn}
+ onPressOut={handlePressOut}
+ hitSlop={enhancedHitSlop}
+ pressRetentionOffset={enhancedPressRetentionOffset}
+ delayLongPress={delayLongPress ?? 500}
+ disabled={disabled}
+ />
+ );
+};