diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts
index 26c05cb..f3982df 100644
--- a/src/i18n/translations.ts
+++ b/src/i18n/translations.ts
@@ -129,6 +129,8 @@ const it = {
flightNotifMsg1: 'Programmate {count} notifiche per il turno di oggi.',
flightNotifMsg0: 'Nessuna notifica futura trovata con i filtri attivi.',
flightNotifAccessEnable: 'Attiva notifiche voli', flightNotifAccessDisable: 'Disattiva notifiche voli',
+ flightAccessibilityPin: 'Pinna volo', flightAccessibilityUnpin: 'Rimuovi pin',
+ flightFrom: 'da', flightTo: 'per',
flightNotifSettingsTitle: 'Impostazioni notifiche',
flightNotifSettingsSub: 'Decidi cosa ricevere, sticky e minutaggi.',
flightNotifOnlyTracked: 'Solo compagnie monitorate',
@@ -328,6 +330,8 @@ const en: typeof it = {
flightNotifMsg1: '{count} notifications scheduled for today’s shift.',
flightNotifMsg0: 'No upcoming notifications found with current filters.',
flightNotifAccessEnable: 'Enable flight notifications', flightNotifAccessDisable: 'Disable flight notifications',
+ flightAccessibilityPin: 'Pin flight', flightAccessibilityUnpin: 'Unpin flight',
+ flightFrom: 'from', flightTo: 'to',
flightNotifSettingsTitle: 'Notification settings',
flightNotifSettingsSub: 'Choose what to receive, sticky behavior and timing.',
flightNotifOnlyTracked: 'Only tracked airlines',
diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx
index 09dbe5d..bc103ea 100644
--- a/src/screens/FlightScreen.tsx
+++ b/src/screens/FlightScreen.tsx
@@ -3,8 +3,10 @@ import {
View, Text, StyleSheet, ActivityIndicator, Modal, ScrollView,
FlatList, TouchableOpacity, RefreshControl, Image,
Animated, PanResponder, NativeModules, Platform, Switch,
+ type ViewProps, type AccessibilityActionEvent,
} from 'react-native';
import { Easing } from 'react-native';
+import * as Haptics from 'expo-haptics';
import * as Calendar from 'expo-calendar';
import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -302,15 +304,17 @@ const SWIPE_MAX_TRANSLATE = 96;
const SWIPE_DRAG_RESISTANCE = 0.82;
function SwipeableFlightCardComponent({
- children, isPinned, onToggle,
+ children, isPinned, onToggle, ...rest
}: {
children: React.ReactNode;
isPinned: boolean;
onToggle: () => void;
-}) {
+} & ViewProps) {
const translateX = useRef(new Animated.Value(0)).current;
const onToggleRef = useRef(onToggle);
onToggleRef.current = onToggle;
+
+ const hasTriggeredHaptic = useRef(false);
const dragScale = useMemo(() => translateX.interpolate({
inputRange: [-SWIPE_MAX_TRANSLATE, 0],
outputRange: [0.985, 1],
@@ -336,9 +340,17 @@ function SwipeableFlightCardComponent({
? Math.max(g.dx * SWIPE_DRAG_RESISTANCE, -SWIPE_MAX_TRANSLATE)
: g.dx * 0.08;
translateX.setValue(nextTranslate);
+
+ if (nextTranslate <= -SWIPE_THRESHOLD && !hasTriggeredHaptic.current) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ hasTriggeredHaptic.current = true;
+ } else if (nextTranslate > -SWIPE_THRESHOLD && hasTriggeredHaptic.current) {
+ hasTriggeredHaptic.current = false;
+ }
},
onPanResponderRelease: (_, g) => {
if (g.dx < -SWIPE_THRESHOLD || g.vx < -SWIPE_TRIGGER_VELOCITY) {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Animated.timing(translateX, {
toValue: -SWIPE_MAX_TRANSLATE,
duration: 170,
@@ -351,6 +363,7 @@ function SwipeableFlightCardComponent({
} else {
animateBack(g.vx);
}
+ hasTriggeredHaptic.current = false;
},
onPanResponderTerminate: () => {
animateBack();
@@ -359,7 +372,11 @@ function SwipeableFlightCardComponent({
return (
-
+
{children}
@@ -515,12 +532,29 @@ function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin,
return () => clearInterval(interval);
}, []);
+ const aLabel = useMemo(() => {
+ const direction = activeTab === 'arrivals' ? t('flightFrom') : t('flightTo');
+ const pinnedState = isPinned ? `${t('flightPinnedLabel')}. ` : '';
+ return `${pinnedState}${flightNumber}, ${airline}, ${direction} ${originDest}, ${time}, ${statusText}`;
+ }, [activeTab, airline, flightNumber, isPinned, originDest, statusText, t, time]);
+
+ const handleAccessibilityAction = useCallback((event: AccessibilityActionEvent) => {
+ if (event.nativeEvent.actionName === 'togglePin') {
+ if (isPinned) onUnpin();
+ else onPin(item);
+ }
+ }, [isPinned, onPin, onUnpin, item]);
+
return (
isPinned ? onUnpin() : onPin(item)}
+ accessible
+ accessibilityLabel={aLabel}
+ accessibilityActions={[{ name: 'togglePin', label: isPinned ? t('flightAccessibilityUnpin') : t('flightAccessibilityPin') }]}
+ onAccessibilityAction={handleAccessibilityAction}
>
-
+
{isPinned && {t('flightPinned')}}
{/* Header */}