Skip to content

Commit

Permalink
feat: implement prevent default (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
okwasniewski authored Nov 3, 2024
1 parent 085e979 commit d346a24
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 81 deletions.
12 changes: 4 additions & 8 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
layout(left, top, right, bottom)
}

init {
setOnItemSelectedListener { item ->
onTabSelected(item)
updateTintColors(item)
true
}
}

private fun onTabLongPressed(item: MenuItem) {
val longPressedItem = items?.firstOrNull { it.title == item.title }
longPressedItem?.let {
Expand Down Expand Up @@ -115,6 +107,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
onTabLongPressed(menuItem)
true
}
findViewById<View>(menuItem.itemId).setOnClickListener {
onTabSelected(menuItem)
updateTintColors(menuItem)
}
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ Whether this screens should render the first time it's accessed. Defaults to tru

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

#### `tabPress`

This event is fired when the user presses the tab button for the current screen in the tab bar.

To prevent the default behavior, you can call `event.preventDefault`:

```tsx`
React.useEffect(() => {
const unsubscribe = navigation.addListener('tabPress', (e) => {
// Prevent default behavior
e.preventDefault();

// Do something manually
// ...
});

return unsubscribe;
}, [navigation]);
```
#### `tabLongPress`
This event is fired when the user presses the tab button for the current screen in the tab bar for an extended period.
Expand Down
13 changes: 13 additions & 0 deletions example/src/Examples/NativeBottomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const Tab = createNativeBottomTabNavigator();
function NativeBottomTabs() {
return (
<Tab.Navigator
initialRouteName="Chat"
labeled={true}
hapticFeedbackEnabled={false}
tabBarInactiveTintColor="#C57B57"
tabBarActiveTintColor="#F7DBA7"
Expand Down Expand Up @@ -53,6 +55,12 @@ function NativeBottomTabs() {
<Tab.Screen
name="Contacts"
component={Contacts}
listeners={{
tabPress: (e) => {
e.preventDefault();
console.log('Contacts tab press prevented');
},
}}
options={{
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
tabBarActiveTintColor: 'yellow',
Expand All @@ -61,6 +69,11 @@ function NativeBottomTabs() {
<Tab.Screen
name="Chat"
component={Chat}
listeners={{
tabPress: () => {
console.log('Chat tab pressed');
},
}}
options={{
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
tabBarActiveTintColor: 'white',
Expand Down
100 changes: 100 additions & 0 deletions ios/TabItemEventModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import SwiftUI
import SwiftUIIntrospect

private final class TabBarDelegate: NSObject, UITabBarControllerDelegate {
var onClick: ((_ index: Int) -> Void)? = nil

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let index = tabBarController.viewControllers?.firstIndex(of: viewController) {
onClick?(index)
}
return false
}
}

struct TabItemEventModifier: ViewModifier {
let onTabEvent: (_ key: Int, _ isLongPress: Bool) -> Void
private let delegate = TabBarDelegate()

func body(content: Content) -> some View {
content
.introspect(.tabView, on: .iOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
handle(tabController: tabController)
}
.introspect(.tabView, on: .tvOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
handle(tabController: tabController)
}
}

func handle(tabController: UITabBarController) {
delegate.onClick = { index in
onTabEvent(index, false)
}
tabController.delegate = delegate

// Don't register gesutre recognizer more than one time
if objc_getAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler) != nil {
return
}

// Remove existing long press gestures
if let existingGestures = tabController.tabBar.gestureRecognizers {
for gesture in existingGestures where gesture is UILongPressGestureRecognizer {
tabController.tabBar.removeGestureRecognizer(gesture)
}
}

// Create gesture handler
let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: onTabEvent)
let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:)))
gesture.minimumPressDuration = 0.5

objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN)

tabController.tabBar.addGestureRecognizer(gesture)
}
}

private struct AssociatedKeys {
static var gestureHandler: UInt8 = 0
}

private class LongPressGestureHandler: NSObject {
private weak var tabBar: UITabBar?
private let handler: (Int, Bool) -> Void

init(tabBar: UITabBar, handler: @escaping (Int, Bool) -> Void) {
self.tabBar = tabBar
self.handler = handler
super.init()
}

@objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
guard recognizer.state == .began,
let tabBar = tabBar else { return }

let location = recognizer.location(in: tabBar)

// Get buttons and sort them by frames
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted(by: { $0.frame.minX < $1.frame.minX })

for (index, button) in tabBarButtons.enumerated() {
if button.frame.contains(location) {
handler(index, true)
break
}
}
}

deinit {
if let tabBar {
objc_setAssociatedObject(tabBar, &AssociatedKeys.gestureHandler, nil, .OBJC_ASSOCIATION_RETAIN)
}
}
}

extension View {
func onTabItemEvent(_ handler: @escaping (Int, Bool) -> Void) -> some View {
modifier(TabItemEventModifier(onTabEvent: handler))
}
}
64 changes: 0 additions & 64 deletions ios/TabItemLongPressModifier.swift

This file was deleted.

14 changes: 8 additions & 6 deletions ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,15 @@ struct TabViewImpl: View {
}

}
.onTabItemLongPress({ index in
.onTabItemEvent({ index, isLongPress in
if let key = props.items[safe: index]?.key {
onLongPress(key)
emitHapticFeedback(longPress: true)
if isLongPress {
onLongPress(key)
emitHapticFeedback(longPress: true)
} else {
onSelect(key)
emitHapticFeedback()
}
}
})
.tintColor(props.selectedActiveTintColor)
Expand All @@ -107,9 +112,6 @@ struct TabViewImpl: View {
UIView.setAnimationsEnabled(true)
}
}

onSelect(newValue)
emitHapticFeedback()
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/react-navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type NativeBottomTabNavigationEventMap = {
/**
* Event which fires on tapping on the tab in the tab bar.
*/
tabPress: { data: undefined };
tabPress: { data: undefined; canPreventDefault: true };
/**
* Event which fires on long press on tab bar.
*/
Expand Down
14 changes: 12 additions & 2 deletions src/react-navigation/views/NativeBottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,21 @@ export default function NativeBottomTabView({
return;
}

navigation.emit({
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
navigation.navigate({ key: route.key, name: route.name, merge: true });

if (event.defaultPrevented) {
return;
} else {
navigation.navigate({
key: route.key,
name: route.name,
merge: true,
});
}
}}
/>
);
Expand Down

0 comments on commit d346a24

Please sign in to comment.