-
-
Notifications
You must be signed in to change notification settings - Fork 597
feat(iOS, Tabs): add bottomAccessory support #3288
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
d8eaeae
226954c
28ac025
a71d92b
6a9c2a1
5ea44e8
775ae70
a4a84e6
7e608c4
4df66d2
1f8b2da
35aae34
537adad
7eb5f64
bea9501
9bead07
33bfde5
4ba4a5c
cac1c98
bdaada7
aadf116
1c34da9
31c3bef
a58c723
6b213bd
28ee322
44f4c5f
5ca50ed
2c69f3e
1696be4
5f83151
2f5d7c3
e98390a
946b546
cdb7d84
65524d8
59cee3a
99e2a54
397d175
e19e143
9e2a8f8
0b3af56
b9d1bca
7ab2cb4
fceb14f
3241429
7d8bbaf
48360ce
f366094
0a96a35
f13c54b
eb0dcf3
c8f8d3d
cc84e2a
efba719
6aade86
cf4e24c
0bc2cb8
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,286 @@ | ||
| import React, { | ||
| createContext, | ||
| Dispatch, | ||
| SetStateAction, | ||
| useContext, | ||
| useState, | ||
| } from 'react'; | ||
|
|
||
| import ConfigWrapperContext, { | ||
| type Configuration, | ||
| DEFAULT_GLOBAL_CONFIGURATION, | ||
| } from '../shared/gamma/containers/bottom-tabs/ConfigWrapperContext'; | ||
| import { | ||
| BottomTabsContainer, | ||
| type TabConfiguration, | ||
| } from '../shared/gamma/containers/bottom-tabs/BottomTabsContainer'; | ||
| import { | ||
| ColorValue, | ||
| Pressable, | ||
| PressableProps, | ||
| ScrollView, | ||
| StyleSheet, | ||
| Text, | ||
| View, | ||
| } from 'react-native'; | ||
| import { SettingsPicker, SettingsSwitch } from '../shared'; | ||
| import Colors from '../shared/styling/Colors'; | ||
| import PressableWithFeedback from '../shared/PressableWithFeedback'; | ||
| import { TabBarMinimizeBehavior } from 'react-native-screens'; | ||
| import { BottomTabsAccessoryEnvironment } from 'react-native-screens/components/bottom-tabs/BottomTabsAccessory.types'; | ||
| import { NavigationContainer } from '@react-navigation/native'; | ||
|
|
||
| type BottomAccessoryConfig = { | ||
| shown: boolean; | ||
| backgroundColor: ColorValue; | ||
| shouldAdaptToEnvironment: boolean; | ||
| tabBarMinimizeBehavior: TabBarMinimizeBehavior; | ||
| }; | ||
|
|
||
| type BottomAccessoryContextInterface = { | ||
| config: BottomAccessoryConfig; | ||
| setConfig: Dispatch<SetStateAction<BottomAccessoryConfig>>; | ||
| }; | ||
|
|
||
| const DEFAULT_BOTTOM_ACCESSORY_CONFIG: BottomAccessoryConfig = { | ||
| shown: true, | ||
| backgroundColor: 'transparent', | ||
| shouldAdaptToEnvironment: true, | ||
| tabBarMinimizeBehavior: 'onScrollDown', | ||
| }; | ||
|
|
||
| const BottomAccessoryContext = | ||
| createContext<BottomAccessoryContextInterface | null>(null); | ||
|
|
||
| const useBottomAccessoryContext = () => { | ||
| const bottomAccessoryContext = useContext(BottomAccessoryContext); | ||
|
|
||
| if (!bottomAccessoryContext) { | ||
| throw new Error( | ||
| 'useBottomAccessoryContext has to be used within <BottomAccessoryContext.Provider>', | ||
| ); | ||
| } | ||
|
|
||
| return bottomAccessoryContext; | ||
| }; | ||
|
|
||
| function Config() { | ||
| const { config, setConfig } = useBottomAccessoryContext(); | ||
|
|
||
| return ( | ||
| <ScrollView contentContainerStyle={{ padding: 16, gap: 5 }}> | ||
| <SettingsSwitch | ||
| label="shown" | ||
| value={config.shown} | ||
| onValueChange={value => setConfig({ ...config, shown: value })} | ||
| /> | ||
| <SettingsPicker<string> | ||
| label="backgroundColor" | ||
| value={String(config.backgroundColor)} | ||
| onValueChange={value => | ||
| setConfig({ | ||
| ...config, | ||
| backgroundColor: value, | ||
| }) | ||
| } | ||
| items={['transparent', Colors.NavyLightTransparent, Colors.BlueLight80]} | ||
| /> | ||
| <SettingsSwitch | ||
| label="shouldAdaptToEnvironment" | ||
| value={config.shouldAdaptToEnvironment} | ||
| onValueChange={value => | ||
| setConfig({ ...config, shouldAdaptToEnvironment: value }) | ||
| } | ||
| /> | ||
| <SettingsPicker<TabBarMinimizeBehavior> | ||
| label="tabBarMinimizeBehavior" | ||
| value={config.tabBarMinimizeBehavior} | ||
| onValueChange={value => | ||
| setConfig({ | ||
| ...config, | ||
| tabBarMinimizeBehavior: value, | ||
| }) | ||
| } | ||
| items={['automatic', 'onScrollDown', 'onScrollUp', 'never']} | ||
| /> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| function TestScreen() { | ||
| return ( | ||
| <ScrollView | ||
| contentContainerStyle={{ | ||
| width: '100%', | ||
| height: 'auto', | ||
| gap: 15, | ||
| paddingHorizontal: 30, | ||
| }}> | ||
| {[...Array(50).keys()].map(index => ( | ||
| <PressableWithFeedback | ||
| key={index + 1} | ||
| onPress={() => console.log(`Pressed #${index + 1}`)} | ||
| style={{ | ||
| paddingVertical: 10, | ||
| paddingHorizontal: 20, | ||
| }}> | ||
| <Text>Pressable #{index + 1}</Text> | ||
| </PressableWithFeedback> | ||
| ))} | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| const TAB_CONFIGS: TabConfiguration[] = [ | ||
| { | ||
| tabScreenProps: { | ||
| tabKey: 'Tab1', | ||
| title: 'Config', | ||
| icon: { | ||
| ios: { | ||
| type: 'sfSymbol', | ||
| name: 'gear', | ||
| }, | ||
| }, | ||
| }, | ||
| component: Config, | ||
| }, | ||
| { | ||
| tabScreenProps: { | ||
| tabKey: 'Tab2', | ||
| title: 'Test', | ||
| icon: { | ||
| ios: { | ||
| type: 'sfSymbol', | ||
| name: 'rectangle.stack', | ||
| }, | ||
| }, | ||
| }, | ||
| component: TestScreen, | ||
| }, | ||
| ]; | ||
|
|
||
| function getBottomAccessory( | ||
| environment: BottomTabsAccessoryEnvironment, | ||
| config: BottomAccessoryConfig, | ||
| ) { | ||
| const pressableStyle: PressableProps['style'] = ({ pressed }) => ({ | ||
| shadowColor: '#000', | ||
| shadowOffset: { | ||
| width: 0.5, | ||
| height: 0.5, | ||
| }, | ||
| shadowOpacity: pressed ? 0.9 : 0.0, | ||
| shadowRadius: 2, | ||
| transform: pressed ? [{ scale: 1.1 }] : [], | ||
| }); | ||
| return ( | ||
| <View | ||
| style={[ | ||
| styles.container, | ||
| { | ||
| backgroundColor: config.backgroundColor, | ||
| }, | ||
| ]}> | ||
| <View style={styles.left}> | ||
| <View style={styles.cover} /> | ||
| <View style={styles.data}> | ||
| <Text style={styles.title} numberOfLines={1}> | ||
| Never Gonna Give You Up | ||
| </Text> | ||
| <Text style={styles.author}>Rick Astley</Text> | ||
| </View> | ||
| </View> | ||
|
|
||
| <View style={styles.right}> | ||
| {(environment === 'regular' || !config.shouldAdaptToEnvironment) && ( | ||
| <Pressable | ||
| onPress={() => console.log('You know the rules and so do I')} | ||
| style={pressableStyle}> | ||
| <Text style={{ fontSize: 28 }}>♫</Text> | ||
| </Pressable> | ||
| )} | ||
|
|
||
| <Pressable | ||
| onPress={() => console.log("We're no strangers to love")} | ||
| style={pressableStyle}> | ||
| <Text style={{ fontSize: 30 }}>▶</Text> | ||
| </Pressable> | ||
| </View> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| function Tabs() { | ||
| const [config, setConfig] = React.useState<Configuration>( | ||
| DEFAULT_GLOBAL_CONFIGURATION, | ||
| ); | ||
|
|
||
| const { config: bottomAccessoryConfig } = useBottomAccessoryContext(); | ||
|
|
||
| return ( | ||
| <ConfigWrapperContext.Provider | ||
| value={{ | ||
| config, | ||
| setConfig, | ||
| }}> | ||
| <BottomTabsContainer | ||
| tabConfigs={TAB_CONFIGS} | ||
| tabBarMinimizeBehavior={bottomAccessoryConfig.tabBarMinimizeBehavior} | ||
| bottomAccessory={ | ||
| bottomAccessoryConfig.shown | ||
| ? environment => | ||
| getBottomAccessory(environment, bottomAccessoryConfig) | ||
| : undefined | ||
| } | ||
| /> | ||
| </ConfigWrapperContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| function App() { | ||
| const [bottomAccessoryConfig, setBottomAccessoryConfig] = | ||
| useState<BottomAccessoryConfig>(DEFAULT_BOTTOM_ACCESSORY_CONFIG); | ||
|
|
||
| return ( | ||
| <NavigationContainer> | ||
| <BottomAccessoryContext.Provider | ||
| value={{ | ||
| config: bottomAccessoryConfig, | ||
| setConfig: setBottomAccessoryConfig, | ||
| }}> | ||
| <Tabs /> | ||
| </BottomAccessoryContext.Provider> | ||
| </NavigationContainer> | ||
| ); | ||
| } | ||
|
|
||
| export default App; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| padding: 5, | ||
| }, | ||
| left: { | ||
| flex: 1, | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| gap: 10, | ||
| marginLeft: 10, | ||
| }, | ||
| cover: { backgroundColor: 'pink', width: 30, height: 30 }, | ||
| data: { flex: 1, paddingRight: 5 }, | ||
| title: { fontWeight: 'bold' }, | ||
| author: { fontSize: 10, color: 'gray' }, | ||
| right: { | ||
| flex: 0, | ||
| flexDirection: 'row-reverse', | ||
| alignItems: 'center', | ||
| gap: 12, | ||
| paddingRight: 10, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #pragma once | ||
|
|
||
| #include <react/debug/react_native_assert.h> | ||
| #include <react/renderer/components/rnscreens/Props.h> | ||
| #include <react/renderer/core/ConcreteComponentDescriptor.h> | ||
| #include "RNSBottomTabsAccessoryShadowNode.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| class RNSBottomTabsAccessoryComponentDescriptor final | ||
| : public ConcreteComponentDescriptor<RNSBottomTabsAccessoryShadowNode> { | ||
| public: | ||
| using ConcreteComponentDescriptor::ConcreteComponentDescriptor; | ||
|
|
||
| void adopt(ShadowNode &shadowNode) const override { | ||
| react_native_assert( | ||
| dynamic_cast<RNSBottomTabsAccessoryShadowNode *>(&shadowNode)); | ||
| auto &bottomTabsAccessoryShadowNode = | ||
| static_cast<RNSBottomTabsAccessoryShadowNode &>(shadowNode); | ||
|
|
||
| auto state = std::static_pointer_cast< | ||
| const RNSBottomTabsAccessoryShadowNode::ConcreteState>( | ||
| shadowNode.getState()); | ||
| auto stateData = state->getData(); | ||
|
|
||
| if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need refactor across whole codebase that will extract this particular condition to a separate function. We have too many places where this is stated explicitly & it is hard to change definition of what "empty state" means. Let's create ticket for this, please.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| bottomTabsAccessoryShadowNode.setSize(stateData.frameSize); | ||
| } | ||
|
|
||
| ConcreteComponentDescriptor::adopt(shadowNode); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace facebook::react | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| #include "RNSBottomTabsAccessoryShadowNode.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| extern const char RNSBottomTabsAccessoryComponentName[] = | ||
| "RNSBottomTabsAccessory"; | ||
|
|
||
| Point RNSBottomTabsAccessoryShadowNode::getContentOriginOffset( | ||
| bool /*includeTransform*/) const { | ||
| auto stateData = getStateData(); | ||
| return stateData.contentOffset; | ||
| } | ||
|
|
||
| } // namespace facebook::react |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #pragma once | ||
|
|
||
| #include <jsi/jsi.h> | ||
| #include <react/renderer/components/rnscreens/EventEmitters.h> | ||
| #include <react/renderer/components/rnscreens/Props.h> | ||
| #include <react/renderer/components/view/ConcreteViewShadowNode.h> | ||
| #include "RNSBottomTabsAccessoryState.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| JSI_EXPORT extern const char RNSBottomTabsAccessoryComponentName[]; | ||
|
|
||
| class JSI_EXPORT RNSBottomTabsAccessoryShadowNode final | ||
| : public ConcreteViewShadowNode< | ||
| RNSBottomTabsAccessoryComponentName, | ||
| RNSBottomTabsAccessoryProps, | ||
| RNSBottomTabsAccessoryEventEmitter, | ||
| RNSBottomTabsAccessoryState> { | ||
| public: | ||
| using ConcreteViewShadowNode::ConcreteViewShadowNode; | ||
| using StateData = ConcreteViewShadowNode::ConcreteStateData; | ||
|
|
||
| Point getContentOriginOffset(bool includeTransform) const override; | ||
| }; | ||
|
|
||
| } // namespace facebook::react |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #pragma once | ||
|
|
||
| #include <react/renderer/graphics/Point.h> | ||
| #include <react/renderer/graphics/Size.h> | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| class JSI_EXPORT RNSBottomTabsAccessoryState final { | ||
| public: | ||
| using Shared = std::shared_ptr<const RNSBottomTabsAccessoryState>; | ||
|
|
||
| RNSBottomTabsAccessoryState() {}; | ||
| RNSBottomTabsAccessoryState(Size frameSize_, Point contentOffset_) | ||
| : frameSize(frameSize_), contentOffset(contentOffset_) {}; | ||
|
|
||
| const Size frameSize{}; | ||
| const Point contentOffset{}; | ||
| }; | ||
|
|
||
| } // namespace facebook::react |
Uh oh!
There was an error while loading. Please reload this page.