Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d8eaeae
add skeleton
kligarski Sep 30, 2025
226954c
WIP
kligarski Oct 2, 2025
28ac025
controlers
kligarski Oct 2, 2025
a71d92b
helper and wrapper, invalidating display link
kligarski Oct 2, 2025
6a9c2a1
refactor
kligarski Oct 2, 2025
5ea44e8
refactor 2
kligarski Oct 2, 2025
775ae70
invalidating skeleton
kligarski Oct 6, 2025
a4a84e6
don't update if tab controllers did not change
kligarski Oct 6, 2025
7e608c4
use correct type in RCTInvalidating's invalidate
kligarski Oct 6, 2025
4df66d2
add environment change events
kligarski Oct 6, 2025
1f8b2da
ios 26 availability, typo
kligarski Oct 6, 2025
35aae34
refactor JS API
kligarski Oct 6, 2025
537adad
use KVO for native bottom accessory view's frame
kligarski Oct 7, 2025
7eb5f64
Paper implementation
kligarski Oct 8, 2025
bea9501
add test screen
kligarski Oct 8, 2025
9bead07
revert changes to TestBottomTabs
kligarski Oct 9, 2025
33bfde5
remove custom wrapper
kligarski Oct 9, 2025
4ba4a5c
fix inserting and removing react subviews in host component
kligarski Oct 9, 2025
cac1c98
restore App.tsx
kligarski Oct 13, 2025
bdaada7
Merge branch 'main' into @kligarski/bottom-accessory
kligarski Oct 13, 2025
aadf116
in-code docs refinement
kligarski Oct 13, 2025
1c34da9
add iOS version guards
kligarski Oct 13, 2025
31c3bef
fix guards
kligarski Oct 13, 2025
a58c723
fix build on Paper
kligarski Oct 13, 2025
6b213bd
Merge branch 'main' into @kligarski/bottom-accessory
kligarski Nov 3, 2025
28ee322
adapt test screen to new icons API
kligarski Nov 3, 2025
44f4c5f
suggestions from code review part 1
kligarski Nov 3, 2025
5ca50ed
fix renamed prop for Paper
kligarski Nov 3, 2025
2c69f3e
extract UITabAccessoryEnvironment conversion
kligarski Nov 3, 2025
1696be4
remove duplication in mounting/unmounting code
kligarski Nov 3, 2025
5f83151
remove redundant flex
kligarski Nov 3, 2025
2f5d7c3
use self-closing tag
kligarski Nov 3, 2025
e98390a
unify naming for content offset
kligarski Nov 3, 2025
946b546
use ifNeeded suffix
kligarski Nov 3, 2025
cdb7d84
import type JS
kligarski Nov 3, 2025
65524d8
refactor handling subview change in bottom tabs host
kligarski Nov 3, 2025
59cee3a
add missing guards
kligarski Nov 4, 2025
99e2a54
Merge branch 'main' into @kligarski/bottom-accessory
kligarski Nov 5, 2025
397d175
view swap approach PoC
kligarski Nov 5, 2025
e19e143
cleanup
kligarski Nov 5, 2025
9e2a8f8
maybe fix build
kligarski Nov 6, 2025
0b3af56
cleanup
kligarski Nov 6, 2025
b9d1bca
update docs
kligarski Nov 6, 2025
7ab2cb4
ifdef RCT_NEW_ARCH_ENABLED -> if RCT_NEW_ARCH_ENABLED
kligarski Nov 6, 2025
fceb14f
Merge branch 'main' into @kligarski/bottom-accessory
kligarski Nov 10, 2025
3241429
use weak pointer from helper to accessory view
kligarski Nov 10, 2025
7d8bbaf
rename notifyFrameUpdate -> notifyWrapperViewFrameHasChanged
kligarski Nov 10, 2025
48360ce
move state to component view, update invalidate fn
kligarski Nov 10, 2025
f366094
simplify guards
kligarski Nov 10, 2025
0a96a35
legacy arch only view manager
kligarski Nov 10, 2025
f13c54b
add BOTTOM_ACCESSORY_AVAILABLE macro
kligarski Nov 10, 2025
eb0dcf3
log error if child is not screen or accessory
kligarski Nov 10, 2025
c8f8d3d
add hasModifiedReactSubviewsInCurrentTransaction as property in exten…
kligarski Nov 10, 2025
cc84e2a
rename react subview validation method
kligarski Nov 10, 2025
efba719
use StyleSheet.absoluteFill for bottom accessory JS components
kligarski Nov 10, 2025
6aade86
fix build on Paper
kligarski Nov 10, 2025
cf4e24c
handle _reactSubviews inside validateAndHandleReactSubview
kligarski Nov 10, 2025
0bc2cb8
extract shadow updates to separate class
kligarski Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions apps/src/tests/Test3288.tsx
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,
},
});
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export { default as Test3248 } from './Test3248';
export { default as Test3265 } from './Test3265';
export { default as Test3271 } from './Test3271';
export { default as Test3282 } from './Test3282';
export { default as Test3288 } from './Test3288';
export { default as Test3342 } from './Test3342';
export { default as Test3346 } from './Test3346';
export { default as Test3369 } from './Test3369';
Expand Down
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) {
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Loading
Loading