diff --git a/.changeset/tidy-unicode-tabs.md b/.changeset/tidy-unicode-tabs.md new file mode 100644 index 00000000..ddb17323 --- /dev/null +++ b/.changeset/tidy-unicode-tabs.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': patch +--- + +Fix an iOS Fabric crash when selecting or long-pressing a tab whose key contains non-ASCII characters. diff --git a/packages/example-shared/e2e/unicode-tab-key.yaml b/packages/example-shared/e2e/unicode-tab-key.yaml new file mode 100644 index 00000000..2771d0d4 --- /dev/null +++ b/packages/example-shared/e2e/unicode-tab-key.yaml @@ -0,0 +1,19 @@ +appId: ${APP_ID} +name: Press and long-press tabs with Unicode keys +--- +- launchApp +- scrollUntilVisible: + element: + text: ^Native Bottom Tabs with Unicode keys$ + direction: DOWN + timeout: 15000 +- tapOn: + text: ^Native Bottom Tabs with Unicode keys$ +- assertVisible: Press me +- tapOn: + text: ^アルバム$ +- longPressOn: + text: ^기사$ +- tapOn: + text: ^Чат$ +- assertVisible: Press me diff --git a/packages/example-shared/src/App.tsx b/packages/example-shared/src/App.tsx index 420d3427..0611d023 100644 --- a/packages/example-shared/src/App.tsx +++ b/packages/example-shared/src/App.tsx @@ -31,6 +31,7 @@ import SFSymbols from './Examples/SFSymbols'; import Roles from './Examples/Roles'; import LabeledTabs from './Examples/Labeled'; import NativeBottomTabs from './Examples/NativeBottomTabs'; +import NativeBottomTabsUnicode from './Examples/NativeBottomTabsUnicode'; import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; @@ -199,6 +200,10 @@ const examples = [ name: 'Native Bottom Tabs with original icons', }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, + { + component: NativeBottomTabsUnicode, + name: 'Native Bottom Tabs with Unicode keys', + }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { component: JSBottomTabs, diff --git a/packages/example-shared/src/Examples/NativeBottomTabsUnicode.tsx b/packages/example-shared/src/Examples/NativeBottomTabsUnicode.tsx new file mode 100644 index 00000000..76f599d0 --- /dev/null +++ b/packages/example-shared/src/Examples/NativeBottomTabsUnicode.tsx @@ -0,0 +1,100 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; +import { Platform } from 'react-native'; +import { useState } from 'react'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsUnicode() { + const [label, setLabel] = useState('기사'); + return ( + { + console.log( + `${Platform.OS}: Long press detected on tab with key ${data.target} at the navigator level.` + ); + }, + }} + > + { + console.log( + `${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.` + ); + setLabel('새 기사'); + }, + }} + options={{ + tabBarButtonTestID: 'articleTestID', + tabBarBadge: '10', + tabBarLabel: label, + tabBarIcon: ({ focused }) => + focused + ? require('../../assets/icons/person_dark.png') + : require('../../assets/icons/article_dark.png'), + }} + /> + require('../../assets/icons/grid_dark.png'), + }} + /> + { + console.log('Contacts tab press prevented'); + }, + }} + options={{ + tabBarButtonTestID: 'contactsTestID', + tabBarIcon: () => require('../../assets/icons/person_dark.png'), + tabBarActiveTintColor: 'yellow', + preventsDefault: true, + }} + /> + { + console.log('Chat tab pressed'); + }, + }} + options={{ + tabBarButtonTestID: 'chatTestID', + tabBarIcon: () => require('../../assets/icons/chat_dark.png'), + tabBarActiveTintColor: 'white', + }} + /> + + ); +} + +export default NativeBottomTabsUnicode; diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm index 8e9cae58..ccb09ea6 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm @@ -265,7 +265,7 @@ - (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag { auto eventEmitter = std::static_pointer_cast(_eventEmitter); if (eventEmitter) { eventEmitter->onPageSelected(RNCTabViewEventEmitter::OnPageSelected{ - .key = [key cStringUsingEncoding:kCFStringEncodingUTF8] + .key = std::string([key UTF8String] ?: "") }); } } @@ -274,7 +274,7 @@ - (void)onLongPressWithKey:(NSString *)key reactTag:(NSNumber *)reactTag { auto eventEmitter = std::static_pointer_cast(_eventEmitter); if (eventEmitter) { eventEmitter->onTabLongPress(RNCTabViewEventEmitter::OnTabLongPress { - .key = [key cStringUsingEncoding:kCFStringEncodingUTF8] + .key = std::string([key UTF8String] ?: "") }); } }