-
Notifications
You must be signed in to change notification settings - Fork 0
π¨ Palette: Add tactile feedback to FlightScreen #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ## 2025-03-05 - [Haptic Feedback for Gestures] | ||
| **Learning:** When adding haptics to continuous gestures (like `PanResponder`), it's critical to use a `ref` to ensure the haptic triggers exactly once when crossing a threshold. Otherwise, it triggers on every frame, creating an unpleasant "buzzing" effect. | ||
| **Action:** Always use a "hasTriggered" ref gated by the threshold logic in gesture handlers. | ||
|
|
||
| ## 2025-03-05 - [Micro-UX: Tactile Tabs] | ||
| **Learning:** Light haptic impact on segmented control/tab switches makes the digital interface feel more mechanical and responsive, especially when visual transitions are subtle. | ||
| **Action:** Add `ImpactFeedbackStyle.Light` to tab-like navigation elements. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { | |||||
| } from 'react-native'; | ||||||
| import * as Calendar from 'expo-calendar'; | ||||||
| import * as Notifications from 'expo-notifications'; | ||||||
| import * as Haptics from 'expo-haptics'; | ||||||
| import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
| import { MaterialIcons } from '@expo/vector-icons'; | ||||||
| import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; | ||||||
|
|
@@ -70,14 +71,24 @@ function SwipeableFlightCard({ | |||||
| 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) => { | ||||||
| hasTriggeredHaptic.current = false; | ||||||
| if (g.dx < -SWIPE_THRESHOLD) { | ||||||
| Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => { | ||||||
| onToggleRef.current(); | ||||||
|
|
@@ -88,6 +99,7 @@ function SwipeableFlightCard({ | |||||
| } | ||||||
| }, | ||||||
| onPanResponderTerminate: () => { | ||||||
| hasTriggeredHaptic.current = false; | ||||||
| Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start(); | ||||||
| }, | ||||||
| }), []); | ||||||
|
|
@@ -493,6 +505,7 @@ export default function FlightScreen() { | |||||
| const tab = activeTab; | ||||||
| await AsyncStorage.setItem(PINNED_FLIGHT_KEY, JSON.stringify({ ...item, _pinTab: tab, _pinnedAt: Date.now() })); | ||||||
| setPinnedFlightId(id); | ||||||
| Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); | ||||||
|
||||||
| Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); | |
| void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haptics.selectionAsync() is fire-and-forget here; if it rejects on unsupported platforms it can surface as an unhandled promise rejection. Consider using void Haptics.selectionAsync().catch(() => {}) (and/or gating with Haptics.isAvailableAsync() / Platform.OS !== 'web').
| Haptics.selectionAsync(); | |
| void Haptics.selectionAsync().catch(() => {}); |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haptics.impactAsync(...) returns a Promise but isnβt awaited/handled; on unsupported platforms this can generate unhandled promise rejections. Consider wrapping haptics in a small helper that gates by platform/availability and uses void ...catch(() => {}).
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above: Haptics.impactAsync(...) is not awaited/handled, which can lead to unhandled promise rejections on platforms where haptics are unavailable (notably web). Consider gating/handling via a shared helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasTriggeredHapticis reset when the swipe moves back above the threshold (dx >= -SWIPE_THRESHOLD), so a single swipe gesture can triggerHaptics.selectionAsync()multiple times if the user jitters across the threshold. If the intent is βexactly once per gestureβ, keep the reftrueuntilonPanResponderRelease/Terminateand remove the mid-gesture reset (or update the gating logic accordingly).