Skip to content

Commit d346a24

Browse files
authored
feat: implement prevent default (#116)
1 parent 085e979 commit d346a24

File tree

8 files changed

+158
-81
lines changed

8 files changed

+158
-81
lines changed

android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
4747
layout(left, top, right, bottom)
4848
}
4949

50-
init {
51-
setOnItemSelectedListener { item ->
52-
onTabSelected(item)
53-
updateTintColors(item)
54-
true
55-
}
56-
}
57-
5850
private fun onTabLongPressed(item: MenuItem) {
5951
val longPressedItem = items?.firstOrNull { it.title == item.title }
6052
longPressedItem?.let {
@@ -115,6 +107,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
115107
onTabLongPressed(menuItem)
116108
true
117109
}
110+
findViewById<View>(menuItem.itemId).setOnClickListener {
111+
onTabSelected(menuItem)
112+
updateTintColors(menuItem)
113+
}
118114
}
119115
}
120116
}

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ Whether this screens should render the first time it's accessed. Defaults to tru
178178

179179
The navigator can emit events on certain actions. Supported events are:
180180

181+
#### `tabPress`
182+
183+
This event is fired when the user presses the tab button for the current screen in the tab bar.
184+
185+
To prevent the default behavior, you can call `event.preventDefault`:
186+
187+
```tsx`
188+
React.useEffect(() => {
189+
const unsubscribe = navigation.addListener('tabPress', (e) => {
190+
// Prevent default behavior
191+
e.preventDefault();
192+
193+
// Do something manually
194+
// ...
195+
});
196+
197+
return unsubscribe;
198+
}, [navigation]);
199+
```
200+
181201
#### `tabLongPress`
182202
183203
This event is fired when the user presses the tab button for the current screen in the tab bar for an extended period.

example/src/Examples/NativeBottomTabs.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const Tab = createNativeBottomTabNavigator();
1111
function NativeBottomTabs() {
1212
return (
1313
<Tab.Navigator
14+
initialRouteName="Chat"
15+
labeled={true}
1416
hapticFeedbackEnabled={false}
1517
tabBarInactiveTintColor="#C57B57"
1618
tabBarActiveTintColor="#F7DBA7"
@@ -53,6 +55,12 @@ function NativeBottomTabs() {
5355
<Tab.Screen
5456
name="Contacts"
5557
component={Contacts}
58+
listeners={{
59+
tabPress: (e) => {
60+
e.preventDefault();
61+
console.log('Contacts tab press prevented');
62+
},
63+
}}
5664
options={{
5765
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
5866
tabBarActiveTintColor: 'yellow',
@@ -61,6 +69,11 @@ function NativeBottomTabs() {
6169
<Tab.Screen
6270
name="Chat"
6371
component={Chat}
72+
listeners={{
73+
tabPress: () => {
74+
console.log('Chat tab pressed');
75+
},
76+
}}
6477
options={{
6578
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
6679
tabBarActiveTintColor: 'white',

ios/TabItemEventModifier.swift

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import SwiftUI
2+
import SwiftUIIntrospect
3+
4+
private final class TabBarDelegate: NSObject, UITabBarControllerDelegate {
5+
var onClick: ((_ index: Int) -> Void)? = nil
6+
7+
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
8+
if let index = tabBarController.viewControllers?.firstIndex(of: viewController) {
9+
onClick?(index)
10+
}
11+
return false
12+
}
13+
}
14+
15+
struct TabItemEventModifier: ViewModifier {
16+
let onTabEvent: (_ key: Int, _ isLongPress: Bool) -> Void
17+
private let delegate = TabBarDelegate()
18+
19+
func body(content: Content) -> some View {
20+
content
21+
.introspect(.tabView, on: .iOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
22+
handle(tabController: tabController)
23+
}
24+
.introspect(.tabView, on: .tvOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
25+
handle(tabController: tabController)
26+
}
27+
}
28+
29+
func handle(tabController: UITabBarController) {
30+
delegate.onClick = { index in
31+
onTabEvent(index, false)
32+
}
33+
tabController.delegate = delegate
34+
35+
// Don't register gesutre recognizer more than one time
36+
if objc_getAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler) != nil {
37+
return
38+
}
39+
40+
// Remove existing long press gestures
41+
if let existingGestures = tabController.tabBar.gestureRecognizers {
42+
for gesture in existingGestures where gesture is UILongPressGestureRecognizer {
43+
tabController.tabBar.removeGestureRecognizer(gesture)
44+
}
45+
}
46+
47+
// Create gesture handler
48+
let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: onTabEvent)
49+
let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:)))
50+
gesture.minimumPressDuration = 0.5
51+
52+
objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN)
53+
54+
tabController.tabBar.addGestureRecognizer(gesture)
55+
}
56+
}
57+
58+
private struct AssociatedKeys {
59+
static var gestureHandler: UInt8 = 0
60+
}
61+
62+
private class LongPressGestureHandler: NSObject {
63+
private weak var tabBar: UITabBar?
64+
private let handler: (Int, Bool) -> Void
65+
66+
init(tabBar: UITabBar, handler: @escaping (Int, Bool) -> Void) {
67+
self.tabBar = tabBar
68+
self.handler = handler
69+
super.init()
70+
}
71+
72+
@objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
73+
guard recognizer.state == .began,
74+
let tabBar = tabBar else { return }
75+
76+
let location = recognizer.location(in: tabBar)
77+
78+
// Get buttons and sort them by frames
79+
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted(by: { $0.frame.minX < $1.frame.minX })
80+
81+
for (index, button) in tabBarButtons.enumerated() {
82+
if button.frame.contains(location) {
83+
handler(index, true)
84+
break
85+
}
86+
}
87+
}
88+
89+
deinit {
90+
if let tabBar {
91+
objc_setAssociatedObject(tabBar, &AssociatedKeys.gestureHandler, nil, .OBJC_ASSOCIATION_RETAIN)
92+
}
93+
}
94+
}
95+
96+
extension View {
97+
func onTabItemEvent(_ handler: @escaping (Int, Bool) -> Void) -> some View {
98+
modifier(TabItemEventModifier(onTabEvent: handler))
99+
}
100+
}

ios/TabItemLongPressModifier.swift

Lines changed: 0 additions & 64 deletions
This file was deleted.

ios/TabViewImpl.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,15 @@ struct TabViewImpl: View {
9191
}
9292

9393
}
94-
.onTabItemLongPress({ index in
94+
.onTabItemEvent({ index, isLongPress in
9595
if let key = props.items[safe: index]?.key {
96-
onLongPress(key)
97-
emitHapticFeedback(longPress: true)
96+
if isLongPress {
97+
onLongPress(key)
98+
emitHapticFeedback(longPress: true)
99+
} else {
100+
onSelect(key)
101+
emitHapticFeedback()
102+
}
98103
}
99104
})
100105
.tintColor(props.selectedActiveTintColor)
@@ -107,9 +112,6 @@ struct TabViewImpl: View {
107112
UIView.setAnimationsEnabled(true)
108113
}
109114
}
110-
111-
onSelect(newValue)
112-
emitHapticFeedback()
113115
}
114116
}
115117

src/react-navigation/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type NativeBottomTabNavigationEventMap = {
1515
/**
1616
* Event which fires on tapping on the tab in the tab bar.
1717
*/
18-
tabPress: { data: undefined };
18+
tabPress: { data: undefined; canPreventDefault: true };
1919
/**
2020
* Event which fires on long press on tab bar.
2121
*/

src/react-navigation/views/NativeBottomTabView.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,21 @@ export default function NativeBottomTabView({
6868
return;
6969
}
7070

71-
navigation.emit({
71+
const event = navigation.emit({
7272
type: 'tabPress',
7373
target: route.key,
74+
canPreventDefault: true,
7475
});
75-
navigation.navigate({ key: route.key, name: route.name, merge: true });
76+
77+
if (event.defaultPrevented) {
78+
return;
79+
} else {
80+
navigation.navigate({
81+
key: route.key,
82+
name: route.name,
83+
merge: true,
84+
});
85+
}
7686
}}
7787
/>
7888
);

0 commit comments

Comments
 (0)