mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
PlatformColor lazy fallback: lint rule + RNTester example (#57705)
Summary: Pull Request resolved: https://github.com/react/react-native/pull/57705 An implementation for the RFC in https://github.com/react-native-community/discussions-and-proposals/pull/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 Reviewed By: christophpurrer Differential Revision: D113329138 fbshipit-source-id: 7d38a8d55b54615d1b131591e99d13e6299eb5cb
This commit is contained in:
committed by
meta-codesync[bot]
parent
bf14b94352
commit
31e119a2a1
@@ -19,6 +19,8 @@ eslintTester.run('../platform-colors', rule, {
|
||||
valid: [
|
||||
"const color = PlatformColor('labelColor');",
|
||||
"const color = PlatformColor('controlAccentColor', 'controlColor');",
|
||||
"const color = PlatformColor('labelColor', {fallback: '#FF0000'});",
|
||||
"const color = PlatformColor('controlAccentColor', 'controlColor', {fallback: 'red'});",
|
||||
"const color = DynamicColorIOS({light: 'black', dark: 'white'});",
|
||||
"const color = DynamicColorIOS({light: PlatformColor('black'), dark: PlatformColor('white')});",
|
||||
"const color = DynamicColorIOS({light: PlatformColor('black'), dark: PlatformColor('white'), highContrastLight: PlatformColor('black'), highContrastDark: PlatformColor('white')});",
|
||||
@@ -32,6 +34,26 @@ eslintTester.run('../platform-colors', rule, {
|
||||
code: "const labelColor = 'labelColor'; const color = PlatformColor(labelColor);",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const raw = '#FF0000'; const color = PlatformColor('labelColor', {fallback: raw});",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const color = PlatformColor({fallback: '#FF0000'}, 'labelColor');",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const color = PlatformColor('labelColor', {fallback: '#FF0000', fallback: '#00FF00'});",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const color = PlatformColor('labelColor', {fallback: '#FF0000', extra: 'red'});",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const color = PlatformColor('labelColor', {['fallback']: '#FF0000'});",
|
||||
errors: [{message: rule.meta.messages.platformColorArgTypes}],
|
||||
},
|
||||
{
|
||||
code: "const tuple = {light: 'black', dark: 'white'}; const color = DynamicColorIOS(tuple);",
|
||||
errors: [{message: rule.meta.messages.dynamicColorIOSArg}],
|
||||
|
||||
@@ -33,6 +33,21 @@ module.exports = {
|
||||
CallExpression: function (node) {
|
||||
if (node.callee.name === 'PlatformColor') {
|
||||
const args = node.arguments;
|
||||
// Optional trailing {fallback: <literal>}: exactly one `fallback`
|
||||
// property with a literal value, so it stays statically analyzable.
|
||||
const isFallbackObject = arg =>
|
||||
arg.type === 'ObjectExpression' &&
|
||||
arg.properties.length === 1 &&
|
||||
arg.properties.every(
|
||||
property =>
|
||||
property.type === 'Property' &&
|
||||
// Reject computed keys (e.g. {['fallback']: ...}); only a plain
|
||||
// identifier key keeps the object statically analyzable.
|
||||
property.computed === false &&
|
||||
property.key.type === 'Identifier' &&
|
||||
property.key.name === 'fallback' &&
|
||||
property.value.type === 'Literal',
|
||||
);
|
||||
if (args.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
@@ -40,7 +55,13 @@ module.exports = {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!args.every(arg => arg.type === 'Literal')) {
|
||||
if (
|
||||
!args.every(
|
||||
(arg, index) =>
|
||||
arg.type === 'Literal' ||
|
||||
(index === args.length - 1 && isFallbackObject(arg)),
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'platformColorArgTypes',
|
||||
|
||||
@@ -236,6 +236,154 @@ function FallbackColorsExample() {
|
||||
);
|
||||
}
|
||||
|
||||
function LazyFallbackColorsExample() {
|
||||
// A token that resolves to a real system color on each platform.
|
||||
const validToken = Platform.select({
|
||||
ios: 'systemBlue',
|
||||
android: '?attr/colorAccent',
|
||||
default: 'systemBlue',
|
||||
});
|
||||
// A token that intentionally does not resolve on any platform, so the lazy
|
||||
// raw-string fallback is what actually gets rendered.
|
||||
const invalidToken = Platform.select({
|
||||
ios: 'nonExistentSystemColor',
|
||||
android: '?attr/nonExistentColor',
|
||||
default: 'nonExistentToken',
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.column}>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Valid token '{validToken}' (shows the system color)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(validToken),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Invalid token, NO fallback (miss → transparent, outlined below)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken),
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Invalid token + fallback '#FF0000' → RED (backgroundColor)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken, {fallback: '#FF0000'}),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Invalid token + fallback '#FFFF00' → YELLOW (backgroundColor)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken, {fallback: '#FFFF00'}),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Invalid token + fallback '#00FF00' → GREEN (text color)
|
||||
</RNTesterText>
|
||||
<View style={styles.colorCell}>
|
||||
<RNTesterText
|
||||
style={{
|
||||
color: PlatformColor(invalidToken, {fallback: '#00FF00'}),
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
GREEN
|
||||
</RNTesterText>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
Invalid token + fallback '#0000FF' → BLUE (borderColor)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
borderColor: PlatformColor(invalidToken, {fallback: '#0000FF'}),
|
||||
borderWidth: 3,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<RNTesterText style={styles.note}>
|
||||
The fallback is parsed by each platform's shared native CSS color
|
||||
parser, so hex (#RGB / #RRGGBB / #RRGGBBAA), rgb(), rgba(), hsl(),
|
||||
hsla() and named colors all resolve consistently on every platform. Only
|
||||
a representative subset is demoed below.
|
||||
</RNTesterText>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
fallback 'rgb(255, 0, 128)' → PINK (backgroundColor)
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken, {
|
||||
fallback: 'rgb(255, 0, 128)',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
fallback 'rgba(0, 128, 255, 0.7)' → semi-transparent BLUE
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken, {
|
||||
fallback: 'rgba(0, 128, 255, 0.7)',
|
||||
}),
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/*
|
||||
hsl()/hsla() and named-color fallbacks (e.g. 'cornflowerblue') are
|
||||
intentionally not demoed here. They resolve on every platform, since the
|
||||
fallback is parsed by the shared CSS color parser; they are omitted only
|
||||
to keep this example concise.
|
||||
*/}
|
||||
<View style={styles.row}>
|
||||
<RNTesterText style={styles.labelCell}>
|
||||
fallback '#FF000080' (#RRGGBBAA) → 50% transparent RED
|
||||
</RNTesterText>
|
||||
<View
|
||||
style={{
|
||||
...styles.colorCell,
|
||||
backgroundColor: PlatformColor(invalidToken, {
|
||||
fallback: '#FF000080',
|
||||
}),
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DynamicColorsExample() {
|
||||
return Platform.OS === 'ios' ? (
|
||||
<View style={styles.column}>
|
||||
@@ -372,6 +520,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
colorCell: {flex: 0.25, alignItems: 'stretch'},
|
||||
separator: {height: 8},
|
||||
note: {
|
||||
fontStyle: 'italic',
|
||||
paddingVertical: 8,
|
||||
...Platform.select({
|
||||
ios: {color: PlatformColor('secondaryLabel')},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
exports.title = 'PlatformColor';
|
||||
@@ -392,6 +547,12 @@ exports.examples = [
|
||||
return <FallbackColorsExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Lazy Fallback Colors',
|
||||
render(): React.MixedElement {
|
||||
return <LazyFallbackColorsExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'iOS Dynamic Colors',
|
||||
render(): React.MixedElement {
|
||||
|
||||
Reference in New Issue
Block a user