Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro

// `role` wins over the legacy `accessibilityRole` when both are set, matching
// how the traits themselves are resolved.
if (props.role != Role::None) {
if (props.role.value_or(Role::None) != Role::None) {
static const std::unordered_set<Role> interactiveRoles{
Role::Button,
Role::Checkbox,
Expand All @@ -82,7 +82,7 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro
Role::Switch,
Role::Tab,
Role::Treeitem};
return interactiveRoles.contains(props.role);
return interactiveRoles.contains(*props.role);
}

static const std::unordered_set<std::string_view> interactiveAccessibilityRoles{
Expand Down Expand Up @@ -1678,8 +1678,9 @@ - (NSString *)accessibilityValue
}

NSMutableArray *valueComponents = [NSMutableArray new];
NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()]
: [NSString stringWithUTF8String:props.accessibilityRole.c_str()];
NSString *roleString = (props.role.value_or(Role::None) != Role::None)
? [NSString stringWithUTF8String:toString(*props.role).c_str()]
: [NSString stringWithUTF8String:props.accessibilityRole.c_str()];

// In iOS, checkbox and radio buttons aren't recognized as traits. However,
// because our apps use checkbox and radio buttons often, we should announce
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,12 @@ public class ReactImageView(
// no worth marking as dirty if it already rendered.
}

/** Sets the request headers. A change rebuilds the image request on the next update. */
public fun setHeaders(headers: ReadableMap?) {
this.headers = headers
if (this.headers != headers) {
this.headers = headers
isDirty = true
}
}

// Disable rasterizing to offscreen layer in order to preserve background effects like box-shadow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import android.graphics.Color
import android.util.DisplayMetrics
import com.facebook.common.logging.FLog
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder
import com.facebook.drawee.drawable.ScalingUtils
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
import com.facebook.react.common.ReactConstants
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.LengthPercentage
Expand All @@ -40,12 +43,15 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockedStatic
import org.mockito.Mockito.RETURNS_SELF
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mockStatic
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.reset
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
Expand Down Expand Up @@ -281,4 +287,27 @@ class ReactImagePropertyTest {
viewManager.setHeaders(mockView, headers)
verify(mockView).setHeaders(headers)
}

@Test
fun testHeadersChangeRebuildsImageRequest() {
val controllerBuilder = mock<PipelineDraweeControllerBuilder>(defaultAnswer = RETURNS_SELF)
val viewManager = ReactImageManager(controllerBuilder)
val view = viewManager.createViewInstance(themeContext)
val sources = Arguments.createArray()
sources.pushMap(JavaOnlyMap.of("uri", "https://example.com/image.png"))
viewManager.setSource(view, sources)
viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-1"))
view.maybeUpdateView()

viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2"))
view.maybeUpdateView()
viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2"))
view.maybeUpdateView()

val requests = argumentCaptor<ImageRequest>()
verify(controllerBuilder, times(2)).setImageRequest(requests.capture())
assertThat(requests.firstValue.sourceUri).isEqualTo(requests.secondValue.sourceUri)
assertThat((requests.secondValue as ReactNetworkImageRequest).headers)
.isEqualTo(JavaOnlyMap.of("Authorization", "token-2"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include <react/renderer/debug/debugStringConvertibleUtils.h>
#include <react/renderer/graphics/Color.h>

#include <algorithm>
#include <tuple>

namespace facebook::react {

ImageProps::ImageProps(
Expand Down Expand Up @@ -145,6 +148,30 @@ static folly::dynamic convertEdgeInsets(const EdgeInsets& edgeInsets) {
return edgeInsetsResult;
}

// ImageSource::operator== compares only the request identity: type and URI.
static bool hasSamePayload(const ImageSource& lhs, const ImageSource& rhs) {
return std::tie(
lhs.type,
lhs.uri,
lhs.bundle,
lhs.scale,
lhs.size,
lhs.body,
lhs.method,
lhs.cache,
lhs.headers) ==
std::tie(
rhs.type,
rhs.uri,
rhs.bundle,
rhs.scale,
rhs.size,
rhs.body,
rhs.method,
rhs.cache,
rhs.headers);
}

ComponentName ImageProps::getDiffPropsImplementationTarget() const {
return "Image";
}
Expand All @@ -158,11 +185,14 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const {

folly::dynamic result = ViewProps::getDiffProps(oldProps);

if (sources != oldProps->sources) {
if (!std::ranges::equal(sources, oldProps->sources, hasSamePayload)) {
auto sourcesArray = folly::dynamic::array();
for (const auto& source : sources) {
sourcesArray.push_back(toDynamic(source));
}
if (!sources.empty()) {
result["headers"] = sourcesArray[0]["headers"];
}
result["source"] = sourcesArray;
}

Expand Down Expand Up @@ -221,7 +251,8 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const {
SharedColor prevTintColorValue =
oldProps->tintColor.value_or(SharedColor{});
if (tintColorValue != prevTintColorValue) {
result["tintColor"] = *tintColorValue;
result["tintColor"] = tintColorValue ? folly::dynamic(*tintColorValue)
: folly::dynamic(nullptr);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <gtest/gtest.h>

#include <react/renderer/components/image/ImageProps.h>
#include <react/test_utils/PropsPayloads.h>

#ifdef RN_SERIALIZABLE_STATE

namespace facebook::react {

namespace {

using folly::dynamic;

// Props 2.0 serializes every field of a source, JS only the ones it sets.
void expectPayloads(const std::vector<PropsCommit>& commits) {
expectPayloadsMatchRawProps<ImageProps>(commits, {"source"});
}

} // namespace

// ReactImageView.setSource reads width and height at the top level of each
// source when there are several, and ReactImageManager reads the request
// headers from the top-level headers prop, which JS derives from the first
// source.
TEST(ImagePropsDiffTest, multipleSourcesCarrySizesAndHeaders) {
dynamic headers = dynamic::object("Authorization", "token");
expectPayloads({{dynamic::object(
"source",
dynamic::array(
dynamic::object("uri", "https://a")("width", 10)("height", 20)(
"headers", headers),
dynamic::object("uri", "https://b")("width", 30)("height", 40)))(
"headers", headers)}});
}

// ReactImageManager ignores a null headers prop, so only an empty map clears
// the headers of the previous source.
TEST(ImagePropsDiffTest, clearsHeadersWhenTheNewSourceHasNone) {
dynamic headers = dynamic::object("Authorization", "old");
expectPayloads({
{dynamic::object(
"source",
dynamic::array(
dynamic::object("uri", "https://a")("headers", headers)))(
"headers", headers)},
{dynamic::object(
"source", dynamic::array(dynamic::object("uri", "https://b")))(
"headers", nullptr),
dynamic::object("headers", dynamic::object())},
});
}

// ReactImageManager clears the tint only for null; 0 is a transparent SRC_IN
// tint that hides the image.
TEST(ImagePropsDiffTest, removingTintColorEmitsNull) {
expectPayloads({
{dynamic::object("tintColor", static_cast<int32_t>(0xFFFF0000))},
{dynamic::object("tintColor", nullptr)},
});
}

// ReactImageView reads the cache policy and the sizes of each source, and
// ReactImageManager the headers; a source with the same URI can change them.
TEST(ImagePropsDiffTest, emitsASameUriSourceWhenItsOtherFieldsChange) {
dynamic headers = dynamic::object("Authorization", "old");
dynamic source = dynamic::object("uri", "https://a")("width", 10)(
"height", 20)("scale", 1)("cache", "default")("headers", headers);
std::vector<PropsCommit> commits{
{dynamic::object("source", dynamic::array(source))("headers", headers)}};
dynamic changes = dynamic::object("cache", "reload")("width", 30)("scale", 2);
for (const auto& [field, value] : changes.items()) {
source[field] = value;
// Props 2.0 sends the first source's headers whenever the sources change.
commits.push_back(
{dynamic::object("source", dynamic::array(source)),
dynamic::object("headers", headers)});
}
source["headers"] = dynamic::object("Authorization", "new");
commits.push_back({dynamic::object("source", dynamic::array(source))(
"headers", source["headers"])});
expectPayloads(commits);
}

} // namespace facebook::react

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ namespace facebook::react {
// members. This makes accessibilityTraits the single source of truth.
// See: github.com/facebook/react-native/issues/57515
static AccessibilityTraits deriveAccessibilityTraits(
Role role,
std::optional<Role> role,
const std::string& accessibilityRole,
const std::optional<AccessibilityState>& accessibilityState) {
AccessibilityTraits traits = AccessibilityTraits::None;
if (role != Role::None) {
fromString(toString(role), traits);
if (role.value_or(Role::None) != Role::None) {
fromString(toString(*role), traits);
} else if (!accessibilityRole.empty()) {
fromString(accessibilityRole, traits);
}
Expand Down Expand Up @@ -179,16 +179,15 @@ AccessibilityProps::AccessibilityProps(
auto* accessibilityRoleValue = rawProps.at("accessibilityRole");
auto* roleValue = rawProps.at("role");

if (accessibilityRoleValue == nullptr ||
!accessibilityRoleValue->hasValue()) {
if (accessibilityRoleValue == nullptr) {
accessibilityRole = sourceProps.accessibilityRole;
} else {
} else if (accessibilityRoleValue->hasValue()) {
fromRawValue(context, *accessibilityRoleValue, accessibilityRole);
}

if (roleValue == nullptr || !roleValue->hasValue()) {
if (roleValue == nullptr) {
role = sourceProps.role;
} else {
} else if (roleValue->hasValue()) {
fromRawValue(context, *roleValue, role);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AccessibilityProps {
bool onAccessibilityEscape{};
bool onAccessibilityAction{};
ImportantForAccessibility importantForAccessibility{ImportantForAccessibility::Auto};
Role role{Role::None};
std::optional<Role> role{};
std::string testId;

#pragma mark - DebugStringConvertible
Expand Down
Loading
Loading