Skip to content

Commit e4cbd9c

Browse files
poc
1 parent 91e3f77 commit e4cbd9c

15 files changed

Lines changed: 733 additions & 5 deletions

File tree

packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
281281
resizeMode: true,
282282
tintColor: colorAttribute,
283283
objectFit: true,
284+
/**
285+
* Media Queries
286+
*/
287+
__mediaQueries: true,
284288
};
285289

286290
export default ReactNativeStyleAttributes;

packages/react-native/Libraries/StyleSheet/StyleSheetExports.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,71 @@ import composeStyles from '../../src/private/styles/composeStyles';
1616
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
1717
import flatten from './flattenStyle';
1818

19+
const MEDIA_PREFIX = '@media ';
20+
21+
function _processStyleValue(key: string, value: mixed): mixed {
22+
const config = ReactNativeStyleAttributes[key];
23+
if (
24+
config != null &&
25+
typeof config === 'object' &&
26+
typeof config.process === 'function'
27+
) {
28+
return config.process(value);
29+
}
30+
return value;
31+
}
32+
33+
/**
34+
* Extract @media keys from a style object and convert them into a
35+
* __mediaQueries array.
36+
*/
37+
function _processMediaQueries(style: {[string]: mixed}): {[string]: mixed} {
38+
let hasMedia = false;
39+
for (const key in style) {
40+
if (typeof key === 'string' && key.startsWith(MEDIA_PREFIX)) {
41+
hasMedia = true;
42+
break;
43+
}
44+
}
45+
if (!hasMedia) {
46+
return style;
47+
}
48+
49+
const result: {[string]: mixed} = {};
50+
const queries: Array<{query: string, styles: {[string]: mixed}}> = [];
51+
const baseStyles: {[string]: mixed} = {};
52+
53+
for (const key in style) {
54+
if (typeof key === 'string' && key.startsWith(MEDIA_PREFIX)) {
55+
const rawStyles = style[key];
56+
const processed: {[string]: mixed} = {};
57+
if (rawStyles != null && typeof rawStyles === 'object') {
58+
for (const styleKey in rawStyles) {
59+
processed[styleKey] = _processStyleValue(
60+
styleKey,
61+
rawStyles[styleKey],
62+
);
63+
// Capture base value for each MQ-touched key.
64+
if (!(styleKey in baseStyles)) {
65+
baseStyles[styleKey] = _processStyleValue(
66+
styleKey,
67+
style[styleKey],
68+
);
69+
}
70+
}
71+
}
72+
queries.push({
73+
query: key.slice(MEDIA_PREFIX.length),
74+
styles: processed,
75+
});
76+
} else {
77+
result[key] = style[key];
78+
}
79+
}
80+
result.__mediaQueries = {baseStyles, queries};
81+
return result;
82+
}
83+
1984
let _hairlineWidth: ?number;
2085

2186
const absoluteFill = {
@@ -183,9 +248,10 @@ export default {
183248
// TODO: This should return S as the return type. But first,
184249
// we need to codemod all the callsites that are typing this
185250
// return value as a number (even though it was opaque).
186-
if (__DEV__) {
187-
for (const key in obj) {
188-
if (obj[key]) {
251+
for (const key in obj) {
252+
if (obj[key]) {
253+
obj[key] = _processMediaQueries(obj[key]);
254+
if (__DEV__) {
189255
Object.freeze(obj[key]);
190256
}
191257
}

packages/react-native/Libraries/StyleSheet/flattenStyle.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import type {
1616
____FlattenStyleProp_Internal,
1717
} from './StyleSheetTypes';
1818

19+
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
20+
1921
type NonAnimatedNodeObject<TStyleProp> = TStyleProp extends AnimatedNode
2022
? empty
2123
: TStyleProp;
@@ -49,6 +51,23 @@ function flattenStyle<
4951
}
5052
}
5153
}
54+
55+
const mq: $FlowFixMe = result.__mediaQueries;
56+
if (mq != null && typeof mq === 'object' && mq.baseStyles != null) {
57+
const updatedBase: {[string]: $FlowFixMe} = {};
58+
for (const key in mq.baseStyles) {
59+
const val = result[key];
60+
const config = ReactNativeStyleAttributes[key];
61+
updatedBase[key] =
62+
config != null &&
63+
typeof config === 'object' &&
64+
typeof config.process === 'function'
65+
? config.process(val)
66+
: val;
67+
}
68+
result.__mediaQueries = {...mq, baseStyles: updatedBase};
69+
}
70+
5271
// $FlowFixMe[incompatible-type]
5372
return result;
5473
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <vector>
11+
12+
#include <react/renderer/core/MediaQueryEnvironment.h>
13+
14+
namespace facebook::react {
15+
16+
enum class MediaFeature : uint8_t {
17+
MinWidth,
18+
MaxWidth,
19+
MinHeight,
20+
MaxHeight,
21+
PrefersColorScheme,
22+
};
23+
24+
struct MediaCondition {
25+
MediaFeature feature{};
26+
Float dimensionValue{0};
27+
ColorScheme colorSchemeValue{ColorScheme::Light};
28+
29+
bool evaluate(const MediaQueryEnvironment& env) const {
30+
switch (feature) {
31+
case MediaFeature::MinWidth:
32+
return env.viewportWidth >= dimensionValue;
33+
case MediaFeature::MaxWidth:
34+
return env.viewportWidth <= dimensionValue;
35+
case MediaFeature::MinHeight:
36+
return env.viewportHeight >= dimensionValue;
37+
case MediaFeature::MaxHeight:
38+
return env.viewportHeight <= dimensionValue;
39+
case MediaFeature::PrefersColorScheme:
40+
return env.colorScheme == colorSchemeValue;
41+
}
42+
return false;
43+
}
44+
};
45+
46+
struct MediaQuery {
47+
std::vector<MediaCondition> conditions;
48+
49+
bool evaluate(const MediaQueryEnvironment& env) const {
50+
for (const auto& condition : conditions) {
51+
if (!condition.evaluate(env)) {
52+
return false;
53+
}
54+
}
55+
return true;
56+
}
57+
};
58+
59+
} // namespace facebook::react
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <react/renderer/graphics/Float.h>
11+
12+
namespace facebook::react {
13+
14+
enum class ColorScheme : uint8_t {
15+
Light = 0,
16+
Dark = 1,
17+
};
18+
19+
/**
20+
* Captures the current environment state used to evaluate CSS media queries.
21+
* Viewport dimensions are in React Native points (logical pixels).
22+
*/
23+
struct MediaQueryEnvironment {
24+
Float viewportWidth{0};
25+
Float viewportHeight{0};
26+
ColorScheme colorScheme{ColorScheme::Light};
27+
28+
bool operator==(const MediaQueryEnvironment& other) const = default;
29+
bool operator!=(const MediaQueryEnvironment& other) const = default;
30+
};
31+
32+
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/core/Props.cpp

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,62 @@
88
#include "Props.h"
99

1010
#include <react/renderer/core/propsConversions.h>
11+
#include <react/renderer/css/CSSMediaQuery.h>
1112

1213
#include <react/featureflags/ReactNativeFeatureFlags.h>
1314
#include <react/renderer/debug/debugStringConvertibleUtils.h>
1415
#include "DynamicPropsUtilities.h"
1516

1617
namespace facebook::react {
1718

19+
namespace {
20+
21+
std::optional<MediaQueryData> parseMediaQueryData(
22+
const folly::dynamic& dynamic) {
23+
if (!dynamic.isObject()) {
24+
return std::nullopt;
25+
}
26+
27+
auto queriesIt = dynamic.find("queries");
28+
auto baseIt = dynamic.find("baseStyles");
29+
if (queriesIt == dynamic.items().end() ||
30+
!queriesIt->second.isArray()) {
31+
return std::nullopt;
32+
}
33+
34+
MediaQueryData data;
35+
if (baseIt != dynamic.items().end() && baseIt->second.isObject()) {
36+
data.baseStyles = baseIt->second;
37+
}
38+
39+
for (const auto& entry : queriesIt->second) {
40+
if (!entry.isObject()) {
41+
continue;
42+
}
43+
auto queryIt = entry.find("query");
44+
auto stylesIt = entry.find("styles");
45+
if (queryIt == entry.items().end() ||
46+
stylesIt == entry.items().end() ||
47+
!queryIt->second.isString() ||
48+
!stylesIt->second.isObject()) {
49+
continue;
50+
}
51+
auto parsed = parseMediaQuery(queryIt->second.getString());
52+
if (parsed.has_value()) {
53+
data.queries.push_back(
54+
MediaQueryData::Query{std::move(parsed.value()), stylesIt->second});
55+
}
56+
}
57+
58+
if (data.queries.empty()) {
59+
return std::nullopt;
60+
}
61+
62+
return data;
63+
}
64+
65+
} // namespace
66+
1867
Props::Props(
1968
const PropsParserContext& context,
2069
const Props& sourceProps,
@@ -29,13 +78,16 @@ void Props::initialize(
2978
const RawProps& rawProps,
3079
[[maybe_unused]] const std::function<bool(const std::string&)>&
3180
filterObjectKeys) {
81+
// Inherit from source so it survives cloneProps calls that don't
82+
// include __mediaQueries in rawProps.
83+
mediaQueryData_ = sourceProps.mediaQueryData_;
84+
3285
nativeId = ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
3386
? sourceProps.nativeId
3487
: convertRawProp(context, rawProps, "nativeID", sourceProps.nativeId, {});
3588

3689
if (!ReactNativeFeatureFlags::enableCppPropsIteratorSetter()) {
3790
if (ReactNativeFeatureFlags::enableNativeViewPropTransformations()) {
38-
// id -> nativeId
3991
auto* idValue = rawProps.at("id", nullptr, nullptr);
4092
if (idValue != nullptr) {
4193
if (idValue->hasValue()) {
@@ -45,6 +97,16 @@ void Props::initialize(
4597
}
4698
}
4799
}
100+
101+
auto* mqValue = rawProps.at("__mediaQueries", nullptr, nullptr);
102+
if (mqValue != nullptr) {
103+
if (mqValue->hasValue()) {
104+
mediaQueryData_ = parseMediaQueryData((folly::dynamic)(*mqValue));
105+
} else {
106+
// Explicitly removed (sent as null from JS diff).
107+
mediaQueryData_.reset();
108+
}
109+
}
48110
}
49111
#ifdef RN_SERIALIZABLE_STATE
50112
if (!ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid()) {
@@ -68,6 +130,14 @@ void Props::setProp(
68130
}
69131
fromRawValue(context, value, nativeId, {});
70132
return;
133+
case CONSTEXPR_RAW_PROPS_KEY_HASH("__mediaQueries"): {
134+
if (!value.hasValue()) {
135+
mediaQueryData_.reset();
136+
return;
137+
}
138+
mediaQueryData_ = parseMediaQueryData((folly::dynamic)value);
139+
return;
140+
}
71141
}
72142
}
73143

packages/react-native/ReactCommon/react/renderer/core/Props.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,34 @@
77

88
#pragma once
99

10+
#include <folly/dynamic.h>
1011
#include <react/renderer/core/PropsMacros.h>
1112
#include <react/renderer/core/PropsParserContext.h>
1213
#include <react/renderer/core/RawProps.h>
1314
#include <react/renderer/core/ReactPrimitives.h>
1415
#include <react/renderer/core/Sealable.h>
16+
#include <react/renderer/core/MediaQuery.h>
1517
#include <react/renderer/debug/DebugStringConvertible.h>
1618

1719
#ifdef ANDROID
18-
#include <folly/dynamic.h>
1920
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
2021
#endif
2122

2223
namespace facebook::react {
2324

25+
/**
26+
* Parsed media query data for a node: ordered list of queries
27+
* with their override styles.
28+
*/
29+
struct MediaQueryData {
30+
struct Query {
31+
MediaQuery condition;
32+
folly::dynamic styles;
33+
};
34+
folly::dynamic baseStyles; // base values for MQ-touched keys
35+
std::vector<Query> queries; // ordered, later wins
36+
};
37+
2438
/*
2539
* Represents the most generic props object.
2640
*/
@@ -59,6 +73,12 @@ class Props : public virtual Sealable, public virtual DebugStringConvertible {
5973

6074
std::string nativeId;
6175

76+
std::optional<MediaQueryData> mediaQueryData_;
77+
78+
bool hasMediaQueries() const {
79+
return mediaQueryData_.has_value();
80+
}
81+
6282
#ifdef RN_SERIALIZABLE_STATE
6383
folly::dynamic rawProps = folly::dynamic::object();
6484

0 commit comments

Comments
 (0)