From 0e1e222ab219930bd0893771d893fde036f6611e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:31:01 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20flight=20ca?= =?UTF-8?q?rd=20UX=20with=20haptics=20and=20accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added haptic feedback to `SwipeableFlightCard` for threshold crossing and action success. - Implemented comprehensive `accessibilityLabel` for flight cards. - Added `accessibilityActions` for pinning/unpinning flights to support screen reader users. - Standardized localized strings for accessibility labels and actions. Co-authored-by: TargetMisser <52361977+TargetMisser@users.noreply.github.com> --- .jules/palette.md | 3 +++ src/i18n/translations.ts | 8 ++++++++ src/screens/FlightScreen.tsx | 31 ++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..6049e9d --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2025-05-14 - Enhancing Custom Gestures with Haptics and Accessibility +**Learning:** Custom gesture interactions, like swiping on a flight card, can be opaque to users if they lack sensory feedback (haptics) or a programmatic way to be triggered (accessibility actions). Adding haptics provides a tactile confirmation of threshold crossing, while accessibility actions make the same functionality available to screen reader users. +**Action:** When implementing custom PanResponders, always include haptic feedback for threshold crossing and define equivalent accessibilityActions for non-visual navigation. diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 8ccd1e2..288b714 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -106,6 +106,10 @@ const it = { flightNotifMsg1: 'Programmate {count} notifiche: arrivi voli (15 min prima) + fine turno.', flightNotifMsg0: 'Nessun volo futuro trovato, ma riceverai la notifica di fine turno.', flightNotifAccessEnable: 'Attiva notifiche voli', flightNotifAccessDisable: 'Disattiva notifiche voli', + flightAccessibilityPin: 'Pinna volo', flightAccessibilityUnpin: 'Rimuovi pin volo', + flightAccessibilityPinHint: 'Aggiunge il volo alle notifiche e alla visualizzazione rapida', + flightAccessibilityUnpinHint: 'Rimuove il volo dalle notifiche e dalla visualizzazione rapida', + flightFrom: 'da', flightTo: 'per', // Phonebook phonebookTitle: 'Rubrica', contactAdd: 'Aggiungi', contactSearch: 'Cerca nome o numero...', contactAll: 'Tutti', @@ -264,6 +268,10 @@ const en: typeof it = { flightNotifMsg1: '{count} notifications scheduled: flight arrivals (15 min before) + end of shift.', flightNotifMsg0: 'No future flights found, but you will receive the end-of-shift notification.', flightNotifAccessEnable: 'Enable flight notifications', flightNotifAccessDisable: 'Disable flight notifications', + flightAccessibilityPin: 'Pin flight', flightAccessibilityUnpin: 'Unpin flight', + flightAccessibilityPinHint: 'Adds the flight to notifications and quick view', + flightAccessibilityUnpinHint: 'Removes the flight from notifications and quick view', + flightFrom: 'from', flightTo: 'to', // Phonebook phonebookTitle: 'Phonebook', contactAdd: 'Add', contactSearch: 'Search name or number...', contactAll: 'All', diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 7b37ca6..782ce03 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -3,7 +3,9 @@ import { View, Text, StyleSheet, ActivityIndicator, Modal, FlatList, TouchableOpacity, RefreshControl, Image, Alert, Animated, PanResponder, NativeModules, Platform, + AccessibilityActionEvent, } 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'; @@ -61,24 +63,36 @@ function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineN const SWIPE_THRESHOLD = 80; function SwipeableFlightCardComponent({ - children, isPinned, onToggle, + children, isPinned, onToggle, ...rest }: { children: React.ReactNode; isPinned: boolean; onToggle: () => void; + [key: string]: any; }) { const translateX = useRef(new Animated.Value(0)).current; const onToggleRef = useRef(onToggle); onToggleRef.current = onToggle; + const hasTriggeredHaptic = useRef(false); + const panResponder = useMemo(() => PanResponder.create({ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 15 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5, onPanResponderMove: (_, g) => { - if (g.dx < 0) translateX.setValue(g.dx); + if (g.dx < 0) { + translateX.setValue(g.dx); + if (g.dx < -SWIPE_THRESHOLD && !hasTriggeredHaptic.current) { + Haptics.selectionAsync(); + hasTriggeredHaptic.current = true; + } else if (g.dx >= -SWIPE_THRESHOLD && hasTriggeredHaptic.current) { + hasTriggeredHaptic.current = false; + } + } }, onPanResponderRelease: (_, g) => { if (g.dx < -SWIPE_THRESHOLD) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => { onToggleRef.current(); Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start(); @@ -86,15 +100,17 @@ function SwipeableFlightCardComponent({ } else { Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start(); } + hasTriggeredHaptic.current = false; }, onPanResponderTerminate: () => { Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start(); + hasTriggeredHaptic.current = false; }, }), []); return ( - + {children} @@ -605,10 +621,19 @@ export default function FlightScreen() { console.log(`[FlightScreen] No staffMonitor match for "${normFn}" (stripped: "${normFnStripped}") in ${activeTab}`); } + const accessibilityLabel = `${isPinned ? t('flightPinnedLabel') + ', ' : ''}${flightNumber}, ${airline}, ${activeTab === 'arrivals' ? t('flightFrom') : t('flightTo')} ${originDest}, ${time}, ${statusText}`; + return ( isPinned ? unpinFlight() : pinFlight(item)} + accessible + accessibilityLabel={accessibilityLabel} + accessibilityActions={[{ name: 'toggle_pin', label: isPinned ? t('flightAccessibilityUnpin') : t('flightAccessibilityPin') }]} + onAccessibilityAction={(e: AccessibilityActionEvent) => { + if (e.nativeEvent.actionName === 'toggle_pin') isPinned ? unpinFlight() : pinFlight(item); + }} + accessibilityHint={isPinned ? t('flightAccessibilityUnpinHint') : t('flightAccessibilityPinHint')} > {isPinned && {t('flightPinned')}}