From d346a24bd8244f44a19bd5a0556b6eb15e0a1803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwa=C5=9Bniewski?= Date: Sun, 3 Nov 2024 18:25:31 +0100 Subject: [PATCH] feat: implement prevent default (#116) --- .../main/java/com/rcttabview/RCTTabView.kt | 12 +-- .../guides/usage-with-react-navigation.mdx | 20 ++++ example/src/Examples/NativeBottomTabs.tsx | 13 +++ ios/TabItemEventModifier.swift | 100 ++++++++++++++++++ ios/TabItemLongPressModifier.swift | 64 ----------- ios/TabViewImpl.swift | 14 +-- src/react-navigation/types.ts | 2 +- .../views/NativeBottomTabView.tsx | 14 ++- 8 files changed, 158 insertions(+), 81 deletions(-) create mode 100644 ios/TabItemEventModifier.swift delete mode 100644 ios/TabItemLongPressModifier.swift diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 9bff768..d6f2eee 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -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 { @@ -115,6 +107,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context onTabLongPressed(menuItem) true } + findViewById(menuItem.itemId).setOnClickListener { + onTabSelected(menuItem) + updateTintColors(menuItem) + } } } } diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index e5b5c3e..26fd82c 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -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. diff --git a/example/src/Examples/NativeBottomTabs.tsx b/example/src/Examples/NativeBottomTabs.tsx index ea2747d..0de6ea8 100644 --- a/example/src/Examples/NativeBottomTabs.tsx +++ b/example/src/Examples/NativeBottomTabs.tsx @@ -11,6 +11,8 @@ const Tab = createNativeBottomTabNavigator(); function NativeBottomTabs() { return ( { + e.preventDefault(); + console.log('Contacts tab press prevented'); + }, + }} options={{ tabBarIcon: () => require('../../assets/icons/person_dark.png'), tabBarActiveTintColor: 'yellow', @@ -61,6 +69,11 @@ function NativeBottomTabs() { { + console.log('Chat tab pressed'); + }, + }} options={{ tabBarIcon: () => require('../../assets/icons/chat_dark.png'), tabBarActiveTintColor: 'white', diff --git a/ios/TabItemEventModifier.swift b/ios/TabItemEventModifier.swift new file mode 100644 index 0000000..9f6b6ab --- /dev/null +++ b/ios/TabItemEventModifier.swift @@ -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)) + } +} diff --git a/ios/TabItemLongPressModifier.swift b/ios/TabItemLongPressModifier.swift deleted file mode 100644 index 71900cc..0000000 --- a/ios/TabItemLongPressModifier.swift +++ /dev/null @@ -1,64 +0,0 @@ -import SwiftUI -import SwiftUIIntrospect - -struct TabItemLongPressModifier: ViewModifier { - let onLongPress: (Int) -> Void - - func body(content: Content) -> some View { - content.introspect(.tabView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { tabController in - // 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: onLongPress) - 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) -> Void - - init(tabBar: UITabBar, handler: @escaping (Int) -> 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) - break - } - } - } -} - -extension View { - func onTabItemLongPress(_ handler: @escaping (Int) -> Void) -> some View { - modifier(TabItemLongPressModifier(onLongPress: handler)) - } -} diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift index 3f54104..de8b844 100644 --- a/ios/TabViewImpl.swift +++ b/ios/TabViewImpl.swift @@ -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) @@ -107,9 +112,6 @@ struct TabViewImpl: View { UIView.setAnimationsEnabled(true) } } - - onSelect(newValue) - emitHapticFeedback() } } diff --git a/src/react-navigation/types.ts b/src/react-navigation/types.ts index 1d26653..0dc2194 100644 --- a/src/react-navigation/types.ts +++ b/src/react-navigation/types.ts @@ -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. */ diff --git a/src/react-navigation/views/NativeBottomTabView.tsx b/src/react-navigation/views/NativeBottomTabView.tsx index 43f74ed..997be9c 100644 --- a/src/react-navigation/views/NativeBottomTabView.tsx +++ b/src/react-navigation/views/NativeBottomTabView.tsx @@ -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, + }); + } }} /> );