Skip to content

Commit f6cb575

Browse files
evankatz14meta-codesync[bot]
authored andcommitted
Accept object font variation settings
Summary: Allow `fontVariationSettings` to accept either the existing CSS-compatible string or an object keyed by four-character OpenType axis tags. Normalize the object at the shared style boundary so Text and TextInput retain the existing native string representation on every platform. Object serialization is deterministic, supports fractional finite values, and preserves the current unset versus explicit-clear behavior. Changelog: [General][Added] - Add object syntax for `fontVariationSettings` Differential Revision: D115640672
1 parent aeee662 commit f6cb575

10 files changed

Lines changed: 263 additions & 21 deletions

File tree

packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import type {TextInputNativeCommands} from './TextInputNativeCommands';
2424

2525
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
2626
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
27-
import {colorAttribute} from '../View/ReactNativeStyleAttributes';
27+
import {
28+
colorAttribute,
29+
fontVariationSettingsAttribute,
30+
} from '../View/ReactNativeStyleAttributes';
2831

2932
export type KeyboardType =
3033
// Cross Platform
@@ -715,7 +718,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
715718
includeFontPadding: true,
716719
fontWeight: true,
717720
fontFamily: true,
718-
fontVariationSettings: true,
721+
fontVariationSettings: fontVariationSettingsAttribute,
719722
allowFontScaling: true,
720723
onSelectionChange: true,
721724
mostRecentEventCount: true,

packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
1212

1313
import {ConditionallyIgnoredEventHandlers} from '../../NativeComponent/ViewConfigIgnore';
14-
import {colorAttribute} from '../View/ReactNativeStyleAttributes';
14+
import {
15+
colorAttribute,
16+
fontVariationSettingsAttribute,
17+
} from '../View/ReactNativeStyleAttributes';
1518

1619
type PartialViewConfigWithoutName = Omit<PartialViewConfig, 'uiViewClassName'>;
1720

@@ -102,6 +105,7 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = {
102105
},
103106
allowFontScaling: true,
104107
fontStyle: true,
108+
fontVariationSettings: fontVariationSettingsAttribute,
105109
textTransform: true,
106110
textAlign: true,
107111
fontFamily: true,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import {__INTERNAL_VIEW_CONFIG as AndroidTextInputViewConfig} from '../AndroidTextInputNativeComponent';
12+
import RCTTextInputViewConfig from '../RCTTextInputViewConfig';
13+
import nullthrows from 'nullthrows';
14+
15+
const {
16+
create,
17+
} = require('../../../ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload');
18+
19+
const androidValidAttributes = nullthrows(
20+
AndroidTextInputViewConfig.validAttributes,
21+
);
22+
const appleValidAttributes = nullthrows(RCTTextInputViewConfig.validAttributes);
23+
24+
describe('Android TextInput view config', () => {
25+
it('serializes object font variation settings', () => {
26+
expect(
27+
create(
28+
{fontVariationSettings: {wght: 552.5, opsz: 17.25}},
29+
androidValidAttributes,
30+
),
31+
).toEqual({fontVariationSettings: "'opsz' 17.25, 'wght' 552.5"});
32+
});
33+
34+
it('serializes an empty object as an explicit clear', () => {
35+
expect(create({fontVariationSettings: {}}, androidValidAttributes)).toEqual(
36+
{fontVariationSettings: ''},
37+
);
38+
});
39+
});
40+
41+
describe('Apple TextInput view config', () => {
42+
it('serializes object font variation settings', () => {
43+
expect(
44+
create(
45+
{fontVariationSettings: {wght: 552.5, opsz: 17.25}},
46+
appleValidAttributes,
47+
),
48+
).toEqual({fontVariationSettings: "'opsz' 17.25, 'wght' 552.5"});
49+
});
50+
51+
it('serializes an empty object as an explicit clear', () => {
52+
expect(create({fontVariationSettings: {}}, appleValidAttributes)).toEqual({
53+
fontVariationSettings: '',
54+
});
55+
});
56+
});

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import processBoxShadow from '../../StyleSheet/processBoxShadow';
2020
import processColor from '../../StyleSheet/processColor';
2121
import processFilter from '../../StyleSheet/processFilter';
2222
import processFontVariant from '../../StyleSheet/processFontVariant';
23+
import processFontVariationSettings from '../../StyleSheet/processFontVariationSettings';
2324
import processTransform from '../../StyleSheet/processTransform';
2425
import processTransformOrigin from '../../StyleSheet/processTransformOrigin';
2526
import sizesDiffer from '../../Utilities/differ/sizesDiffer';
@@ -71,6 +72,10 @@ export const fontVariantAttribute: AnyAttributeType = nativeCSSParsing
7172
? true
7273
: {process: processFontVariant};
7374

75+
export const fontVariationSettingsAttribute: AnyAttributeType = {
76+
process: processFontVariationSettings,
77+
};
78+
7479
export const aspectRatioAttribute: AnyAttributeType = nativeCSSParsing
7580
? true
7681
: {process: processAspectRatio};
@@ -258,7 +263,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
258263
fontSize: true,
259264
fontStyle: true,
260265
fontVariant: fontVariantAttribute,
261-
fontVariationSettings: true,
266+
fontVariationSettings: fontVariationSettingsAttribute,
262267
fontWeight: true,
263268
includeFontPadding: true,
264269
letterSpacing: true,

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,9 @@ export type ____FontVariant_Internal =
999999
export type ____FontVariantArray_Internal =
10001000
ReadonlyArray<____FontVariant_Internal>;
10011001

1002+
export type ____FontVariationSettings_Internal =
1003+
string | Readonly<{[axis: string]: number}>;
1004+
10021005
type ____TextStyle_InternalBase = Readonly<{
10031006
color?: ____ColorValue_Internal,
10041007
fontFamily?: string,
@@ -1012,11 +1015,12 @@ type ____TextStyle_InternalBase = Readonly<{
10121015
fontWeight?: ____FontWeight_Internal,
10131016
fontVariant?: ____FontVariantArray_Internal | string,
10141017
/**
1015-
* Specifies OpenType font variation axis values using CSS syntax. An empty
1016-
* string resets inherited variation settings. On Android, this requires API
1017-
* level 26 or later.
1018+
* Specifies OpenType font variation axis values using CSS syntax or an
1019+
* object keyed by four-character axis tags. An empty string or object resets
1020+
* inherited variation settings. On Android, this requires API level 26 or
1021+
* later.
10181022
*/
1019-
fontVariationSettings?: string,
1023+
fontVariationSettings?: ____FontVariationSettings_Internal,
10201024
textShadowOffset?: Readonly<{
10211025
width: number,
10221026
height: number,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import processFontVariationSettings from '../processFontVariationSettings';
14+
15+
describe('processFontVariationSettings', () => {
16+
it('passes string settings through unchanged', () => {
17+
expect(processFontVariationSettings("'wght' 550, 'opsz' 18")).toBe(
18+
"'wght' 550, 'opsz' 18",
19+
);
20+
});
21+
22+
it('serializes object settings in deterministic axis order', () => {
23+
expect(processFontVariationSettings({wght: 552.5, opsz: 17.25})).toBe(
24+
"'opsz' 17.25, 'wght' 552.5",
25+
);
26+
});
27+
28+
it('serializes an empty object as an explicit clear', () => {
29+
expect(processFontVariationSettings({})).toBe('');
30+
});
31+
32+
it('supports printable four-character tags containing a single quote', () => {
33+
expect(processFontVariationSettings({["a'b "]: 1})).toBe('"a\'b " 1');
34+
});
35+
36+
it('preserves backslashes in printable four-character tags', () => {
37+
expect(processFontVariationSettings({['a\\bc']: 1})).toBe("'a\\bc' 1");
38+
});
39+
40+
it('rejects tags that cannot be delimited without escaping', () => {
41+
expect(() => processFontVariationSettings({[`a'"b`]: 1})).toThrow(
42+
'Font variation axis tags containing both quote characters must use the string form: "a\'\\"b"',
43+
);
44+
});
45+
46+
it('rejects invalid axis tags', () => {
47+
expect(() => processFontVariationSettings({weight: 550})).toThrow(
48+
'Font variation axis tags must be exactly four printable ASCII characters: "weight"',
49+
);
50+
expect(() => processFontVariationSettings({['a\nbc']: 1})).toThrow(
51+
'Font variation axis tags must be exactly four printable ASCII characters: "a\\nbc"',
52+
);
53+
});
54+
55+
it('rejects non-finite axis values', () => {
56+
expect(() => processFontVariationSettings({wght: NaN})).toThrow(
57+
'Font variation axis values must be finite numbers: NaN',
58+
);
59+
expect(() => processFontVariationSettings({wght: Infinity})).toThrow(
60+
'Font variation axis values must be finite numbers: Infinity',
61+
);
62+
});
63+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {____FontVariationSettings_Internal} from './StyleSheetTypes';
14+
15+
function quoteAxis(axis: string): string {
16+
if (
17+
axis.length !== 4 ||
18+
axis.split('').some(character => {
19+
const code = character.charCodeAt(0);
20+
return code < 0x20 || code > 0x7e;
21+
})
22+
) {
23+
throw new Error(
24+
`Font variation axis tags must be exactly four printable ASCII characters: ${JSON.stringify(axis)}`,
25+
);
26+
}
27+
28+
if (!axis.includes("'")) {
29+
return `'${axis}'`;
30+
}
31+
if (!axis.includes('"')) {
32+
return `"${axis}"`;
33+
}
34+
35+
throw new Error(
36+
`Font variation axis tags containing both quote characters must use the string form: ${JSON.stringify(axis)}`,
37+
);
38+
}
39+
40+
function processFontVariationSettings(
41+
settings: ____FontVariationSettings_Internal,
42+
): string {
43+
if (typeof settings === 'string') {
44+
return settings;
45+
}
46+
47+
return Object.keys(settings)
48+
.sort()
49+
.map(axis => {
50+
const value = settings[axis];
51+
if (!Number.isFinite(value)) {
52+
throw new Error(
53+
`Font variation axis values must be finite numbers: ${String(value)}`,
54+
);
55+
}
56+
return `${quoteAxis(axis)} ${String(value)}`;
57+
})
58+
.join(', ');
59+
}
60+
61+
export default processFontVariationSettings;

packages/react-native/Libraries/Text/__tests__/Text-itest.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,44 @@ describe('<Text>', () => {
5252
});
5353
});
5454

55+
describe('fontVariationSettings', () => {
56+
it('serializes object settings', () => {
57+
const root = Fantom.createRoot();
58+
59+
Fantom.runTask(() => {
60+
root.render(
61+
<Text style={{fontVariationSettings: {wght: 552.5, opsz: 17.25}}}>
62+
{TEST_TEXT}
63+
</Text>,
64+
);
65+
});
66+
67+
expect(
68+
root.getRenderedOutput({props: ['fontVariationSettings']}).toJSX(),
69+
).toEqual(
70+
<rn-paragraph fontVariationSettings="'opsz' 17.25, 'wght' 552.5">
71+
{TEST_TEXT}
72+
</rn-paragraph>,
73+
);
74+
});
75+
76+
it('serializes an empty object as an explicit clear', () => {
77+
const root = Fantom.createRoot();
78+
79+
Fantom.runTask(() => {
80+
root.render(
81+
<Text style={{fontVariationSettings: {}}}>{TEST_TEXT}</Text>,
82+
);
83+
});
84+
85+
expect(
86+
root.getRenderedOutput({props: ['fontVariationSettings']}).toJSX(),
87+
).toEqual(
88+
<rn-paragraph fontVariationSettings="">{TEST_TEXT}</rn-paragraph>,
89+
);
90+
});
91+
});
92+
5593
describe('adjustsFontSizeToFit', () => {
5694
it(`can be set to "true"`, () => {
5795
const root = Fantom.createRoot();

0 commit comments

Comments
 (0)