From 26fbbe531ea0f229795d8fc07c2374cd1417881e Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 26 Apr 2026 00:35:12 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20flight=20ca?=
=?UTF-8?q?rd=20accessibility=20and=20haptics?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Added haptic feedback (Medium impact) to swipe-to-pin gesture.
- Added success haptic feedback upon action completion.
- Implemented comprehensive accessibility labels for flight cards.
- Added custom accessibility actions (togglePin) and hints for screen readers.
- Localized flight notification toggle accessibility label.
- Added necessary translation keys for accessibility in IT and EN.
Co-authored-by: TargetMisser <52361977+TargetMisser@users.noreply.github.com>
---
.jules/palette.md | 3 ++
src/i18n/translations.ts | 8 ++++++
src/screens/FlightScreen.tsx | 54 +++++++++++++++++++++++++++++++-----
3 files changed, 58 insertions(+), 7 deletions(-)
create mode 100644 .jules/palette.md
diff --git a/.jules/palette.md b/.jules/palette.md
new file mode 100644
index 0000000..7373af1
--- /dev/null
+++ b/.jules/palette.md
@@ -0,0 +1,3 @@
+## 2025-05-14 - Flight Card Accessibility & Interaction
+**Learning:** Swipe-based actions are common in mobile UX but are completely inaccessible to screen reader users unless mapped to `accessibilityActions`.
+**Action:** When implementing gestures like swipe-to-action, always include `accessibilityActions` and `onAccessibilityAction` to ensure functional parity for all users.
diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts
index 777bce5..4ffe75a 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',
+ flightAccessibilityPinHint: 'Aggiunge il volo alla sezione pinnati e attiva le notifiche dedicate',
+ flightAccessibilityUnpinHint: 'Rimuove il volo dalla sezione pinnati',
+ flightFrom: 'Da', flightTo: 'Per',
// Phonebook
phonebookTitle: 'Rubrica', contactAdd: 'Aggiungi',
contactSearch: 'Cerca nome o numero...', contactAll: 'Tutti',
@@ -266,6 +270,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 the pinned section and enables dedicated notifications',
+ flightAccessibilityUnpinHint: 'Removes the flight from the pinned section',
+ 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 e200b90..40165b3 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, Alert,
Animated, PanResponder, NativeModules, Platform,
+ AccessibilityActionEvent, ViewProps,
} from 'react-native';
import * as Calendar from 'expo-calendar';
+import * as Haptics from 'expo-haptics';
import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons';
@@ -78,27 +80,41 @@ function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineN
const SWIPE_THRESHOLD = 80;
-function SwipeableFlightCardComponent({
- children, isPinned, onToggle,
-}: {
+interface SwipeableFlightCardProps extends ViewProps {
children: React.ReactNode;
isPinned: boolean;
onToggle: () => void;
-}) {
+}
+
+function SwipeableFlightCardComponent({
+ children, isPinned, onToggle, ...rest
+}: SwipeableFlightCardProps) {
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.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ hasTriggeredHaptic.current = true;
+ } else if (g.dx >= -SWIPE_THRESHOLD && hasTriggeredHaptic.current) {
+ hasTriggeredHaptic.current = false;
+ }
+ }
},
onPanResponderRelease: (_, g) => {
+ hasTriggeredHaptic.current = false;
if (g.dx < -SWIPE_THRESHOLD) {
Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => {
onToggleRef.current();
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
});
} else {
@@ -106,13 +122,14 @@ function SwipeableFlightCardComponent({
}
},
onPanResponderTerminate: () => {
+ hasTriggeredHaptic.current = false;
Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start();
},
}), []);
return (
-
+
{children}
@@ -182,10 +199,33 @@ function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin,
smPool.find(sm => sm.flightNumber === normFn) ??
smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped);
+ const accessibilityLabel = [
+ isPinned ? t('flightPinnedLabel') : '',
+ flightNumber,
+ airline,
+ activeTab === 'arrivals' ? t('flightFrom') : t('flightTo'),
+ originDest,
+ time,
+ statusText,
+ ].filter(Boolean).join(', ');
+
+ const handleAccessibilityAction = (event: AccessibilityActionEvent) => {
+ if (event.nativeEvent.actionName === 'togglePin') {
+ isPinned ? onUnpin() : onPin(item);
+ }
+ };
+
return (
isPinned ? onUnpin() : onPin(item)}
+ accessible={true}
+ accessibilityLabel={accessibilityLabel}
+ accessibilityActions={[
+ { name: 'togglePin', label: isPinned ? t('flightAccessibilityUnpin') : t('flightAccessibilityPin') },
+ ]}
+ accessibilityHint={isPinned ? t('flightAccessibilityUnpinHint') : t('flightAccessibilityPinHint')}
+ onAccessibilityAction={handleAccessibilityAction}
>
{isPinned && {t('flightPinned')}}
@@ -874,7 +914,7 @@ export default function FlightScreen() {
onPress={toggleNotifications}
activeOpacity={0.8}
accessible
- accessibilityLabel={notifsEnabled ? 'Disattiva notifiche voli' : 'Attiva notifiche voli'}
+ accessibilityLabel={notifsEnabled ? t('flightNotifAccessDisable') : t('flightNotifAccessEnable')}
accessibilityRole="button"
>