Skip to content

Commit 4e30cca

Browse files
authored
Merge branch 'master' into feat/Custom-bottom-bar-buttons
2 parents f8a79b8 + a25439a commit 4e30cca

6 files changed

Lines changed: 539 additions & 22 deletions

File tree

.cursor/skills/ios26-navigation/SKILL.md

Lines changed: 433 additions & 0 deletions
Large diffs are not rendered by default.

android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import android.annotation.SuppressLint;
44
import android.content.Context;
5+
import android.util.TypedValue;
56
import android.view.View;
67

78
import com.facebook.react.ReactInstanceManager;
@@ -10,7 +11,6 @@
1011
import com.reactnativenavigation.react.ReactView;
1112

1213
import static android.view.View.MeasureSpec.EXACTLY;
13-
import static android.view.View.MeasureSpec.UNSPECIFIED;
1414
import static android.view.View.MeasureSpec.makeMeasureSpec;
1515
import static com.reactnativenavigation.utils.UiUtils.dpToPx;
1616

@@ -39,7 +39,19 @@ private int createSpec(int measureSpec, Number dimension) {
3939
if (dimension.hasValue()) {
4040
return makeMeasureSpec(MeasureSpec.getSize(dpToPx(getContext(), dimension.get())), EXACTLY);
4141
} else {
42-
return makeMeasureSpec(MeasureSpec.getSize(measureSpec), UNSPECIFIED);
42+
// When JS doesn't pass width/height, default to the theme's actionBarSize (48dp on Material).
43+
// Yoga's intrinsic measurement of the React view collapses `paddingHorizontal` on the
44+
// trailing edge in RTL (RN/Fabric measurement quirk), so we cannot trust UNSPECIFIED here -
45+
// it produces a 0dp visible inset against the screen edge in RTL.
46+
return makeMeasureSpec(resolveActionBarSize(), EXACTLY);
4347
}
4448
}
49+
50+
private int resolveActionBarSize() {
51+
TypedValue tv = new TypedValue();
52+
if (getContext().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
53+
return TypedValue.complexToDimensionPixelSize(tv.data, getContext().getResources().getDisplayMetrics());
54+
}
55+
return (int) dpToPx(getContext(), 48f);
56+
}
4557
}

ios/RNNReactButtonView.mm

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
#import "RNNReactButtonView.h"
2+
#import <React/RCTSurface.h>
23

3-
@implementation RNNReactButtonView
4+
@implementation RNNReactButtonView {
5+
NSLayoutConstraint *_widthConstraint;
6+
NSLayoutConstraint *_heightConstraint;
7+
BOOL _didCenter;
8+
}
49

510
- (instancetype)initWithHost:(RCTHost *)host
611
moduleName:(NSString *)moduleName
@@ -10,15 +15,67 @@ - (instancetype)initWithHost:(RCTHost *)host
1015
reactViewReadyBlock:(RNNReactViewReadyCompletionBlock)reactViewReadyBlock {
1116
self = [super initWithHost:host moduleName:moduleName initialProperties:initialProperties eventEmitter:eventEmitter sizeMeasureMode:convertToSurfaceSizeMeasureMode(RCTRootViewSizeFlexibilityWidthAndHeight) reactViewReadyBlock:reactViewReadyBlock];
1217
[host.surfacePresenter addObserver:self];
13-
self.backgroundColor = UIColor.clearColor;
18+
self.backgroundColor = [UIColor clearColor];
19+
20+
if (@available(iOS 26.0, *)) {
21+
if (![self designRequiresCompatibility]) {
22+
self.translatesAutoresizingMaskIntoConstraints = NO;
23+
_widthConstraint = [self.widthAnchor constraintEqualToConstant:0];
24+
_heightConstraint = [self.heightAnchor constraintEqualToConstant:0];
25+
_widthConstraint.priority = UILayoutPriorityDefaultHigh;
26+
_heightConstraint.priority = UILayoutPriorityDefaultHigh;
27+
_widthConstraint.active = YES;
28+
_heightConstraint.active = YES;
29+
_didCenter = NO;
30+
}
31+
}
1432

1533
return self;
1634
}
1735

36+
- (BOOL)designRequiresCompatibility {
37+
static BOOL checked = NO;
38+
static BOOL result = NO;
39+
if (!checked) {
40+
checked = YES;
41+
result = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"] boolValue];
42+
}
43+
return result;
44+
}
45+
1846
- (void)didMountComponentsWithRootTag:(NSInteger)rootTag {
1947
if (self.surface.rootTag == rootTag) {
2048
[super didMountComponentsWithRootTag:rootTag];
2149
[self sizeToFit];
50+
if (@available(iOS 26.0, *)) {
51+
if (![self designRequiresCompatibility]) {
52+
[self updateConstraintsToFitSize];
53+
}
54+
}
55+
}
56+
}
57+
58+
- (void)updateConstraintsToFitSize {
59+
CGSize size = self.frame.size;
60+
if (size.width > 0 && size.height > 0) {
61+
_widthConstraint.constant = size.width;
62+
_heightConstraint.constant = size.height;
63+
}
64+
}
65+
66+
- (void)layoutSubviews {
67+
[super layoutSubviews];
68+
if (@available(iOS 26.0, *)) {
69+
if ([self designRequiresCompatibility]) return;
70+
if (!_didCenter && self.superview && self.frame.size.width > 0) {
71+
CGFloat wrapperWidth = self.superview.bounds.size.width;
72+
CGFloat selfWidth = self.frame.size.width;
73+
if (wrapperWidth > selfWidth) {
74+
_didCenter = YES;
75+
CGFloat tx = (wrapperWidth - selfWidth) / 2.0;
76+
self.layer.affineTransform = CGAffineTransformMakeTranslation(tx, 0);
77+
}
78+
}
2279
}
2380
}
2481

playground/e2e/Buttons.test.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Platform } from 'react-native';
12
import Utils from './Utils';
23
import TestIDs from '../src/testIDs';
34

@@ -22,14 +23,21 @@ describe('Buttons', () => {
2223
});
2324

2425
it(':android: should not effect left buttons when hiding back button', async () => {
25-
await elementById(TestIDs.TOGGLE_BACK).tap();
26-
await expect(elementById(TestIDs.LEFT_BUTTON)).toBeVisible();
27-
await expect(elementById(TestIDs.TEXTUAL_LEFT_BUTTON)).toBeVisible();
28-
await expect(elementById(TestIDs.BACK_BUTTON)).toBeVisible();
29-
30-
await elementById(TestIDs.TOGGLE_BACK).tap();
31-
await expect(elementById(TestIDs.LEFT_BUTTON)).toBeVisible();
32-
await expect(elementById(TestIDs.TEXTUAL_LEFT_BUTTON)).toBeVisible();
26+
// Jest mock runs with Platform.OS === 'ios'; this test asserts Android-only topBar behavior.
27+
const platform = Platform.OS;
28+
Platform.OS = 'android';
29+
try {
30+
await elementById(TestIDs.TOGGLE_BACK).tap();
31+
await expect(elementById(TestIDs.LEFT_BUTTON)).toBeVisible();
32+
await expect(elementById(TestIDs.TEXTUAL_LEFT_BUTTON)).toBeVisible();
33+
await expect(elementById(TestIDs.BACK_BUTTON)).toBeVisible();
34+
35+
await elementById(TestIDs.TOGGLE_BACK).tap();
36+
await expect(elementById(TestIDs.LEFT_BUTTON)).toBeVisible();
37+
await expect(elementById(TestIDs.TEXTUAL_LEFT_BUTTON)).toBeVisible();
38+
} finally {
39+
Platform.OS = platform;
40+
}
3341
});
3442
it('sets right buttons', async () => {
3543
await expect(elementById(TestIDs.BUTTON_ONE)).toBeVisible();
6 Bytes
Loading

playground/src/screens/ButtonsScreen.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable prettier/prettier */
22
import React from 'react';
3+
import { Platform } from 'react-native';
34
import { NavigationComponent, Options, OptionsTopBarButton } from 'react-native-navigation';
45
import Root from '../components/Root';
56
import Button from '../components/Button';
@@ -139,17 +140,23 @@ export default class ButtonOptions extends NavigationComponent {
139140
);
140141
}
141142

142-
toggleBack= ()=> {
143+
toggleBack = () => {
143144
this.backButtonVisibile = !this.backButtonVisibile;
144-
Navigation.mergeOptions(this.props.componentId,{
145-
topBar:{
146-
backButton:{
147-
testID:BACK_BUTTON,
148-
visible:this.backButtonVisibile
149-
}
150-
}
151-
})
152-
}
145+
Navigation.mergeOptions(this.props.componentId, {
146+
topBar: {
147+
backButton: {
148+
testID: BACK_BUTTON,
149+
visible: this.backButtonVisibile,
150+
},
151+
// iOS: leftButtons replace the back chevron slot, so the back button
152+
// can't render unless leftButtons are cleared. Android's back
153+
// affordance is independent of leftButtons.
154+
...(Platform.OS === 'ios' && this.backButtonVisibile
155+
? { leftButtons: [] }
156+
: {}),
157+
},
158+
});
159+
};
153160

154161
setRightButtons = () =>
155162
Navigation.mergeOptions(this, {

0 commit comments

Comments
 (0)