diff --git a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index 680ff7aa974b..37f4fa6768c3 100644 --- a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -24,7 +24,10 @@ import type {TextInputNativeCommands} from './TextInputNativeCommands'; import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import {colorAttribute} from '../View/ReactNativeStyleAttributes'; +import { + colorAttribute, + fontVariationSettingsAttribute, +} from '../View/ReactNativeStyleAttributes'; export type KeyboardType = // Cross Platform @@ -715,7 +718,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { includeFontPadding: true, fontWeight: true, fontFamily: true, - fontVariationSettings: true, + fontVariationSettings: fontVariationSettingsAttribute, allowFontScaling: true, onSelectionChange: true, mostRecentEventCount: true, diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index cccc623c46ce..ada8aef2247a 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -11,7 +11,10 @@ import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes'; import {ConditionallyIgnoredEventHandlers} from '../../NativeComponent/ViewConfigIgnore'; -import {colorAttribute} from '../View/ReactNativeStyleAttributes'; +import { + colorAttribute, + fontVariationSettingsAttribute, +} from '../View/ReactNativeStyleAttributes'; type PartialViewConfigWithoutName = Omit; @@ -102,6 +105,7 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { }, allowFontScaling: true, fontStyle: true, + fontVariationSettings: fontVariationSettingsAttribute, textTransform: true, textAlign: true, fontFamily: true, diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/RCTTextInputViewConfig-itest.js b/packages/react-native/Libraries/Components/TextInput/__tests__/RCTTextInputViewConfig-itest.js new file mode 100644 index 000000000000..d4b79a57de33 --- /dev/null +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/RCTTextInputViewConfig-itest.js @@ -0,0 +1,56 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import {__INTERNAL_VIEW_CONFIG as AndroidTextInputViewConfig} from '../AndroidTextInputNativeComponent'; +import RCTTextInputViewConfig from '../RCTTextInputViewConfig'; +import nullthrows from 'nullthrows'; + +const { + create, +} = require('../../../ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'); + +const androidValidAttributes = nullthrows( + AndroidTextInputViewConfig.validAttributes, +); +const appleValidAttributes = nullthrows(RCTTextInputViewConfig.validAttributes); + +describe('Android TextInput view config', () => { + it('serializes object font variation settings', () => { + expect( + create( + {fontVariationSettings: {wght: 552.5, opsz: 17.25}}, + androidValidAttributes, + ), + ).toEqual({fontVariationSettings: "'opsz' 17.25, 'wght' 552.5"}); + }); + + it('serializes an empty object as an explicit clear', () => { + expect(create({fontVariationSettings: {}}, androidValidAttributes)).toEqual( + {fontVariationSettings: ''}, + ); + }); +}); + +describe('Apple TextInput view config', () => { + it('serializes object font variation settings', () => { + expect( + create( + {fontVariationSettings: {wght: 552.5, opsz: 17.25}}, + appleValidAttributes, + ), + ).toEqual({fontVariationSettings: "'opsz' 17.25, 'wght' 552.5"}); + }); + + it('serializes an empty object as an explicit clear', () => { + expect(create({fontVariationSettings: {}}, appleValidAttributes)).toEqual({ + fontVariationSettings: '', + }); + }); +}); diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 68d4fa451f56..d3883ea7b859 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -20,6 +20,7 @@ import processBoxShadow from '../../StyleSheet/processBoxShadow'; import processColor from '../../StyleSheet/processColor'; import processFilter from '../../StyleSheet/processFilter'; import processFontVariant from '../../StyleSheet/processFontVariant'; +import processFontVariationSettings from '../../StyleSheet/processFontVariationSettings'; import processTransform from '../../StyleSheet/processTransform'; import processTransformOrigin from '../../StyleSheet/processTransformOrigin'; import sizesDiffer from '../../Utilities/differ/sizesDiffer'; @@ -71,6 +72,10 @@ export const fontVariantAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processFontVariant}; +export const fontVariationSettingsAttribute: AnyAttributeType = { + process: processFontVariationSettings, +}; + export const aspectRatioAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processAspectRatio}; @@ -258,7 +263,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { fontSize: true, fontStyle: true, fontVariant: fontVariantAttribute, - fontVariationSettings: true, + fontVariationSettings: fontVariationSettingsAttribute, fontWeight: true, includeFontPadding: true, letterSpacing: true, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 9df143c6efe1..dfee208d2033 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -999,6 +999,9 @@ export type ____FontVariant_Internal = export type ____FontVariantArray_Internal = ReadonlyArray<____FontVariant_Internal>; +export type ____FontVariationSettings_Internal = + string | Readonly<{[axis: string]: number}>; + type ____TextStyle_InternalBase = Readonly<{ color?: ____ColorValue_Internal, fontFamily?: string, @@ -1012,11 +1015,12 @@ type ____TextStyle_InternalBase = Readonly<{ fontWeight?: ____FontWeight_Internal, fontVariant?: ____FontVariantArray_Internal | string, /** - * Specifies OpenType font variation axis values using CSS syntax. An empty - * string resets inherited variation settings. On Android, this requires API - * level 26 or later. + * Specifies OpenType font variation axis values using CSS syntax or an + * object keyed by four-character axis tags. An empty string or object resets + * inherited variation settings. On Android, this requires API level 26 or + * later. */ - fontVariationSettings?: string, + fontVariationSettings?: ____FontVariationSettings_Internal, textShadowOffset?: Readonly<{ width: number, height: number, diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariationSettings-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariationSettings-itest.js new file mode 100644 index 000000000000..f9c4f2dbe8df --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariationSettings-itest.js @@ -0,0 +1,63 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import processFontVariationSettings from '../processFontVariationSettings'; + +describe('processFontVariationSettings', () => { + it('passes string settings through unchanged', () => { + expect(processFontVariationSettings("'wght' 550, 'opsz' 18")).toBe( + "'wght' 550, 'opsz' 18", + ); + }); + + it('serializes object settings in deterministic axis order', () => { + expect(processFontVariationSettings({wght: 552.5, opsz: 17.25})).toBe( + "'opsz' 17.25, 'wght' 552.5", + ); + }); + + it('serializes an empty object as an explicit clear', () => { + expect(processFontVariationSettings({})).toBe(''); + }); + + it('supports printable four-character tags containing a single quote', () => { + expect(processFontVariationSettings({["a'b "]: 1})).toBe('"a\'b " 1'); + }); + + it('preserves backslashes in printable four-character tags', () => { + expect(processFontVariationSettings({['a\\bc']: 1})).toBe("'a\\bc' 1"); + }); + + it('rejects tags that cannot be delimited without escaping', () => { + expect(() => processFontVariationSettings({[`a'"b`]: 1})).toThrow( + 'Font variation axis tags containing both quote characters must use the string form: "a\'\\"b"', + ); + }); + + it('rejects invalid axis tags', () => { + expect(() => processFontVariationSettings({weight: 550})).toThrow( + 'Font variation axis tags must be exactly four printable ASCII characters: "weight"', + ); + expect(() => processFontVariationSettings({['a\nbc']: 1})).toThrow( + 'Font variation axis tags must be exactly four printable ASCII characters: "a\\nbc"', + ); + }); + + it('rejects non-finite axis values', () => { + expect(() => processFontVariationSettings({wght: NaN})).toThrow( + 'Font variation axis values must be finite numbers: NaN', + ); + expect(() => processFontVariationSettings({wght: Infinity})).toThrow( + 'Font variation axis values must be finite numbers: Infinity', + ); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/processFontVariationSettings.js b/packages/react-native/Libraries/StyleSheet/processFontVariationSettings.js new file mode 100644 index 000000000000..cd59f0192480 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processFontVariationSettings.js @@ -0,0 +1,61 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {____FontVariationSettings_Internal} from './StyleSheetTypes'; + +function quoteAxis(axis: string): string { + if ( + axis.length !== 4 || + axis.split('').some(character => { + const code = character.charCodeAt(0); + return code < 0x20 || code > 0x7e; + }) + ) { + throw new Error( + `Font variation axis tags must be exactly four printable ASCII characters: ${JSON.stringify(axis)}`, + ); + } + + if (!axis.includes("'")) { + return `'${axis}'`; + } + if (!axis.includes('"')) { + return `"${axis}"`; + } + + throw new Error( + `Font variation axis tags containing both quote characters must use the string form: ${JSON.stringify(axis)}`, + ); +} + +function processFontVariationSettings( + settings: ____FontVariationSettings_Internal, +): string { + if (typeof settings === 'string') { + return settings; + } + + return Object.keys(settings) + .sort() + .map(axis => { + const value = settings[axis]; + if (!Number.isFinite(value)) { + throw new Error( + `Font variation axis values must be finite numbers: ${String(value)}`, + ); + } + return `${quoteAxis(axis)} ${String(value)}`; + }) + .join(', '); +} + +export default processFontVariationSettings; diff --git a/packages/react-native/Libraries/Text/__tests__/Text-itest.js b/packages/react-native/Libraries/Text/__tests__/Text-itest.js index a7a50140cd5a..2d19756ff935 100644 --- a/packages/react-native/Libraries/Text/__tests__/Text-itest.js +++ b/packages/react-native/Libraries/Text/__tests__/Text-itest.js @@ -52,6 +52,44 @@ describe('', () => { }); }); + describe('fontVariationSettings', () => { + it('serializes object settings', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + {TEST_TEXT} + , + ); + }); + + expect( + root.getRenderedOutput({props: ['fontVariationSettings']}).toJSX(), + ).toEqual( + + {TEST_TEXT} + , + ); + }); + + it('serializes an empty object as an explicit clear', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + {TEST_TEXT}, + ); + }); + + expect( + root.getRenderedOutput({props: ['fontVariationSettings']}).toJSX(), + ).toEqual( + {TEST_TEXT}, + ); + }); + }); + describe('adjustsFontSizeToFit', () => { it(`can be set to "true"`, () => { const root = Fantom.createRoot(); diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 5220c72e26e6..a04517cbd5a0 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4406ddb15814262a7ccc2b56e9625d67>> + * @generated SignedSource<<44990574a74bcc122718d4f6142db8cd>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -582,6 +582,11 @@ declare type ____FontVariant_Internal = | "tabular-nums" declare type ____FontVariantArray_Internal = ReadonlyArray<____FontVariant_Internal> +declare type ____FontVariationSettings_Internal = + | string + | { + readonly [axis: string]: number + } declare type ____FontWeight_Internal = | "100" | "200" @@ -747,7 +752,7 @@ declare type ____TextStyle_InternalBase = { readonly fontSize?: number readonly fontStyle?: "italic" | "normal" readonly fontVariant?: ____FontVariantArray_Internal | string - readonly fontVariationSettings?: string + readonly fontVariationSettings?: ____FontVariationSettings_Internal readonly fontWeight?: ____FontWeight_Internal readonly includeFontPadding?: boolean readonly letterSpacing?: number @@ -5725,7 +5730,7 @@ export { AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // d74ec583 + Animated, // f23abee5 AppConfig, // 35c0ca70 AppRegistry, // 1e8c5a00 AppState, // 12012be5 @@ -5948,7 +5953,7 @@ export { StatusBarProps, // c2a44d88 StatusBarStyle, // 78f53eea StyleProp, // fa0e9b4a - StyleSheet, // 3c21ec63 + StyleSheet, // daba2860 SubmitBehavior, // c4ddf490 Switch, // f495bab3 SwitchChangeEvent, // 899635b1 @@ -5958,9 +5963,9 @@ export { TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 09e783b9 + Text, // 72e323cd TextContentType, // 239b3ecc - TextInput, // a2cc82d1 + TextInput, // e1821dbc TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b77af40e TextInputChangeEvent, // f55eef98 @@ -5970,13 +5975,13 @@ export { TextInputIOSProps, // fb3c9327 TextInputInstance, // 5a0c0e0d TextInputKeyPressEvent, // 546c5d07 - TextInputProps, // dd91ebaa + TextInputProps, // f6b0ba24 TextInputSelectionChangeEvent, // e58f2abc TextInputSubmitEditingEvent, // 6bcb2aa5 TextInstance, // 05463a96 TextLayoutEvent, // 3f54186f - TextProps, // 4a122c2b - TextStyle, // 6ad4307f + TextProps, // 921f7adc + TextStyle, // 01a21fd1 ToastAndroid, // 88a8969a TouchableHighlight, // 3a6eaed4 TouchableHighlightInstance, // b510c0eb diff --git a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts index 8e17f07e04fc..04b13f46ad90 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -559,6 +559,8 @@ export interface TextStyleAndroid extends ViewStyle { } // @see https://reactnative.dev/docs/text#style +export type FontVariationSettings = string | Readonly>; + export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle { color?: ColorValue | undefined; fontFamily?: string | undefined; @@ -602,11 +604,12 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle { | 'black' | undefined; /** - * Specifies OpenType font variation axis values using CSS syntax. An empty - * string resets inherited variation settings. On Android, this requires API - * level 26 or later. + * Specifies OpenType font variation axis values using CSS syntax or an + * object keyed by four-character axis tags. An empty string or object resets + * inherited variation settings. On Android, this requires API level 26 or + * later. */ - fontVariationSettings?: string | undefined; + fontVariationSettings?: FontVariationSettings | undefined; letterSpacing?: number | undefined; lineHeight?: number | undefined; textAlign?: