Skip to content

Commit 08eb113

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
PlatformColor lazy fallback: lint rule + RNTester example
Summary: An implementation for the RFC in react-native-community/discussions-and-proposals#1008 Rounds out the lazy `PlatformColor` fallback with tooling and a demo. - The `react-native/platform-colors` ESLint rule now permits an optional trailing `{fallback: <literal>}` options object so `PlatformColor('token', {fallback: '#RRGGBB'})` is lint-clean, while still requiring every other argument to be a literal. The options object must have exactly one `fallback` property whose value is a literal, so it stays statically analyzable. - A new "Lazy Fallback Colors" section in the RNTester `PlatformColor` example demonstrates valid tokens, misses with no fallback (transparent), and misses with hex / `rgb()` / `rgba()` / `#RRGGBBAA` fallbacks across `backgroundColor`, text `color`, and `borderColor`. Changelog: [Internal] - PlatformColor: ESLint support and RNTester example for the lazy raw-color fallback Differential Revision: D113329138
1 parent f3fa39e commit 08eb113

3 files changed

Lines changed: 205 additions & 1 deletion

File tree

packages/eslint-plugin-react-native/__tests__/platform-colors-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ eslintTester.run('../platform-colors', rule, {
1919
valid: [
2020
"const color = PlatformColor('labelColor');",
2121
"const color = PlatformColor('controlAccentColor', 'controlColor');",
22+
"const color = PlatformColor('labelColor', {fallback: '#FF0000'});",
23+
"const color = PlatformColor('controlAccentColor', 'controlColor', {fallback: 'red'});",
2224
"const color = DynamicColorIOS({light: 'black', dark: 'white'});",
2325
"const color = DynamicColorIOS({light: PlatformColor('black'), dark: PlatformColor('white')});",
2426
"const color = DynamicColorIOS({light: PlatformColor('black'), dark: PlatformColor('white'), highContrastLight: PlatformColor('black'), highContrastDark: PlatformColor('white')});",
@@ -32,6 +34,26 @@ eslintTester.run('../platform-colors', rule, {
3234
code: "const labelColor = 'labelColor'; const color = PlatformColor(labelColor);",
3335
errors: [{message: rule.meta.messages.platformColorArgTypes}],
3436
},
37+
{
38+
code: "const raw = '#FF0000'; const color = PlatformColor('labelColor', {fallback: raw});",
39+
errors: [{message: rule.meta.messages.platformColorArgTypes}],
40+
},
41+
{
42+
code: "const color = PlatformColor({fallback: '#FF0000'}, 'labelColor');",
43+
errors: [{message: rule.meta.messages.platformColorArgTypes}],
44+
},
45+
{
46+
code: "const color = PlatformColor('labelColor', {fallback: '#FF0000', fallback: '#00FF00'});",
47+
errors: [{message: rule.meta.messages.platformColorArgTypes}],
48+
},
49+
{
50+
code: "const color = PlatformColor('labelColor', {fallback: '#FF0000', extra: 'red'});",
51+
errors: [{message: rule.meta.messages.platformColorArgTypes}],
52+
},
53+
{
54+
code: "const color = PlatformColor('labelColor', {['fallback']: '#FF0000'});",
55+
errors: [{message: rule.meta.messages.platformColorArgTypes}],
56+
},
3557
{
3658
code: "const tuple = {light: 'black', dark: 'white'}; const color = DynamicColorIOS(tuple);",
3759
errors: [{message: rule.meta.messages.dynamicColorIOSArg}],

packages/eslint-plugin-react-native/platform-colors.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,35 @@ module.exports = {
3333
CallExpression: function (node) {
3434
if (node.callee.name === 'PlatformColor') {
3535
const args = node.arguments;
36+
// Optional trailing {fallback: <literal>}: exactly one `fallback`
37+
// property with a literal value, so it stays statically analyzable.
38+
const isFallbackObject = arg =>
39+
arg.type === 'ObjectExpression' &&
40+
arg.properties.length === 1 &&
41+
arg.properties.every(
42+
property =>
43+
property.type === 'Property' &&
44+
// Reject computed keys (e.g. {['fallback']: ...}); only a plain
45+
// identifier key keeps the object statically analyzable.
46+
property.computed === false &&
47+
property.key.type === 'Identifier' &&
48+
property.key.name === 'fallback' &&
49+
property.value.type === 'Literal',
50+
);
3651
if (args.length === 0) {
3752
context.report({
3853
node,
3954
messageId: 'platformColorArgsLength',
4055
});
4156
return;
4257
}
43-
if (!args.every(arg => arg.type === 'Literal')) {
58+
if (
59+
!args.every(
60+
(arg, index) =>
61+
arg.type === 'Literal' ||
62+
(index === args.length - 1 && isFallbackObject(arg)),
63+
)
64+
) {
4465
context.report({
4566
node,
4667
messageId: 'platformColorArgTypes',

packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,154 @@ function FallbackColorsExample() {
236236
);
237237
}
238238

239+
function LazyFallbackColorsExample() {
240+
// A token that resolves to a real system color on each platform.
241+
const validToken = Platform.select({
242+
ios: 'systemBlue',
243+
android: '?attr/colorAccent',
244+
default: 'systemBlue',
245+
});
246+
// A token that intentionally does not resolve on any platform, so the lazy
247+
// raw-string fallback is what actually gets rendered.
248+
const invalidToken = Platform.select({
249+
ios: 'nonExistentSystemColor',
250+
android: '?attr/nonExistentColor',
251+
default: 'nonExistentToken',
252+
});
253+
254+
return (
255+
<View style={styles.column}>
256+
<View style={styles.row}>
257+
<RNTesterText style={styles.labelCell}>
258+
Valid token '{validToken}' (shows the system color)
259+
</RNTesterText>
260+
<View
261+
style={{
262+
...styles.colorCell,
263+
backgroundColor: PlatformColor(validToken),
264+
}}
265+
/>
266+
</View>
267+
<View style={styles.row}>
268+
<RNTesterText style={styles.labelCell}>
269+
Invalid token, NO fallback (miss → transparent, outlined below)
270+
</RNTesterText>
271+
<View
272+
style={{
273+
...styles.colorCell,
274+
backgroundColor: PlatformColor(invalidToken),
275+
borderColor: 'black',
276+
borderWidth: 1,
277+
}}
278+
/>
279+
</View>
280+
<View style={styles.row}>
281+
<RNTesterText style={styles.labelCell}>
282+
Invalid token + fallback '#FF0000' → RED (backgroundColor)
283+
</RNTesterText>
284+
<View
285+
style={{
286+
...styles.colorCell,
287+
backgroundColor: PlatformColor(invalidToken, {fallback: '#FF0000'}),
288+
}}
289+
/>
290+
</View>
291+
<View style={styles.row}>
292+
<RNTesterText style={styles.labelCell}>
293+
Invalid token + fallback '#FFFF00' → YELLOW (backgroundColor)
294+
</RNTesterText>
295+
<View
296+
style={{
297+
...styles.colorCell,
298+
backgroundColor: PlatformColor(invalidToken, {fallback: '#FFFF00'}),
299+
}}
300+
/>
301+
</View>
302+
<View style={styles.row}>
303+
<RNTesterText style={styles.labelCell}>
304+
Invalid token + fallback '#00FF00' → GREEN (text color)
305+
</RNTesterText>
306+
<View style={styles.colorCell}>
307+
<RNTesterText
308+
style={{
309+
color: PlatformColor(invalidToken, {fallback: '#00FF00'}),
310+
fontWeight: 'bold',
311+
}}>
312+
GREEN
313+
</RNTesterText>
314+
</View>
315+
</View>
316+
<View style={styles.row}>
317+
<RNTesterText style={styles.labelCell}>
318+
Invalid token + fallback '#0000FF' → BLUE (borderColor)
319+
</RNTesterText>
320+
<View
321+
style={{
322+
...styles.colorCell,
323+
borderColor: PlatformColor(invalidToken, {fallback: '#0000FF'}),
324+
borderWidth: 3,
325+
}}
326+
/>
327+
</View>
328+
<RNTesterText style={styles.note}>
329+
The fallback is parsed by each platform's shared native CSS color
330+
parser, so hex (#RGB / #RRGGBB / #RRGGBBAA), rgb(), rgba(), hsl(),
331+
hsla() and named colors all resolve consistently on every platform. Only
332+
a representative subset is demoed below.
333+
</RNTesterText>
334+
<View style={styles.row}>
335+
<RNTesterText style={styles.labelCell}>
336+
fallback 'rgb(255, 0, 128)' → PINK (backgroundColor)
337+
</RNTesterText>
338+
<View
339+
style={{
340+
...styles.colorCell,
341+
backgroundColor: PlatformColor(invalidToken, {
342+
fallback: 'rgb(255, 0, 128)',
343+
}),
344+
}}
345+
/>
346+
</View>
347+
<View style={styles.row}>
348+
<RNTesterText style={styles.labelCell}>
349+
fallback 'rgba(0, 128, 255, 0.7)' → semi-transparent BLUE
350+
</RNTesterText>
351+
<View
352+
style={{
353+
...styles.colorCell,
354+
backgroundColor: PlatformColor(invalidToken, {
355+
fallback: 'rgba(0, 128, 255, 0.7)',
356+
}),
357+
borderColor: 'black',
358+
borderWidth: 1,
359+
}}
360+
/>
361+
</View>
362+
{/*
363+
hsl()/hsla() and named-color fallbacks (e.g. 'cornflowerblue') are
364+
intentionally not demoed here. They resolve on every platform, since the
365+
fallback is parsed by the shared CSS color parser; they are omitted only
366+
to keep this example concise.
367+
*/}
368+
<View style={styles.row}>
369+
<RNTesterText style={styles.labelCell}>
370+
fallback '#FF000080' (#RRGGBBAA) → 50% transparent RED
371+
</RNTesterText>
372+
<View
373+
style={{
374+
...styles.colorCell,
375+
backgroundColor: PlatformColor(invalidToken, {
376+
fallback: '#FF000080',
377+
}),
378+
borderColor: 'black',
379+
borderWidth: 1,
380+
}}
381+
/>
382+
</View>
383+
</View>
384+
);
385+
}
386+
239387
function DynamicColorsExample() {
240388
return Platform.OS === 'ios' ? (
241389
<View style={styles.column}>
@@ -372,6 +520,13 @@ const styles = StyleSheet.create({
372520
},
373521
colorCell: {flex: 0.25, alignItems: 'stretch'},
374522
separator: {height: 8},
523+
note: {
524+
fontStyle: 'italic',
525+
paddingVertical: 8,
526+
...Platform.select({
527+
ios: {color: PlatformColor('secondaryLabel')},
528+
}),
529+
},
375530
});
376531

377532
exports.title = 'PlatformColor';
@@ -392,6 +547,12 @@ exports.examples = [
392547
return <FallbackColorsExample />;
393548
},
394549
},
550+
{
551+
title: 'Lazy Fallback Colors',
552+
render(): React.MixedElement {
553+
return <LazyFallbackColorsExample />;
554+
},
555+
},
395556
{
396557
title: 'iOS Dynamic Colors',
397558
render(): React.MixedElement {

0 commit comments

Comments
 (0)