diff --git a/apps/fluent-tester/ios/Podfile b/apps/fluent-tester/ios/Podfile index 1126346941..8ac79cbc3f 100644 --- a/apps/fluent-tester/ios/Podfile +++ b/apps/fluent-tester/ios/Podfile @@ -24,6 +24,9 @@ use_test_app! do |target| target.app do platform :ios, '14.0' + # There is a bug where autolinking isn't working, do specify these manually. + pod 'FRNFontMetrics', :path => '../../../packages/experimental/NativeFontMetrics/FRNFontMetrics.podspec' + script_phase name: 'Start Packager', script: start_packager_script, execution_position: :before_compile diff --git a/apps/fluent-tester/ios/Podfile.lock b/apps/fluent-tester/ios/Podfile.lock index 002c72f636..53a9106022 100644 --- a/apps/fluent-tester/ios/Podfile.lock +++ b/apps/fluent-tester/ios/Podfile.lock @@ -10,12 +10,14 @@ PODS: - React-jsi (= 0.68.5) - ReactCommon/turbomodule/core (= 0.68.5) - fmt (6.2.1) - - FRNAvatar (0.16.20): + - FRNAvatar (0.16.24): - MicrosoftFluentUI (= 0.8.3) - React - FRNDatePicker (0.7.3): - MicrosoftFluentUI (= 0.8.3) - React + - FRNFontMetrics (0.2.0): + - React - glog (0.3.5) - MicrosoftFluentUI (0.8.3): - MicrosoftFluentUI/ActivityIndicator_ios (= 0.8.3) @@ -384,7 +386,7 @@ PODS: - glog - react-native-menu (0.1.2): - React - - react-native-slider (4.3.2): + - react-native-slider (4.3.3): - React-Core - React-perflogger (0.68.5) - React-RCTActionSheet (0.68.5): @@ -468,6 +470,7 @@ DEPENDENCIES: - FBReactNativeSpec (from `../../../node_modules/react-native/React/FBReactNativeSpec`) - FRNAvatar (from `../../../packages/experimental/Avatar`) - FRNDatePicker (from `../../../packages/experimental/NativeDatePicker`) + - FRNFontMetrics (from `../../../packages/experimental/NativeFontMetrics/FRNFontMetrics.podspec`) - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../../../node_modules/react-native/Libraries/RCTRequired`) @@ -522,6 +525,8 @@ EXTERNAL SOURCES: :path: "../../../packages/experimental/Avatar" FRNDatePicker: :path: "../../../packages/experimental/NativeDatePicker" + FRNFontMetrics: + :path: "../../../packages/experimental/NativeFontMetrics/FRNFontMetrics.podspec" glog: :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" RCT-Folly: @@ -595,8 +600,9 @@ SPEC CHECKSUMS: FBLazyVector: 2b47ff52037bd9ae07cc9b051c9975797814b736 FBReactNativeSpec: dd89c4a5591e20015aa55c6efbf9c7740a83efbf fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - FRNAvatar: de1aec8a9011ade478f2148677b4f2c076d77110 + FRNAvatar: 3911021ed95a08f19e0ee696712aa48b6dff010a FRNDatePicker: 241cd55b8d2b63d4427d782951f31504f09fbe1a + FRNFontMetrics: 472e7952e454ece364a91babd8bb2a32219676e7 glog: 476ee3e89abb49e07f822b48323c51c57124b572 MicrosoftFluentUI: e30487dd18aba04beeed4caf1ce1988073f8b03a RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8 @@ -613,7 +619,7 @@ SPEC CHECKSUMS: React-jsinspector: eb202e43b3879aba9a14f3f65788aec85d4e1ea9 React-logger: 98f663b292a60967ebbc6d803ae96c1381183b6d react-native-menu: 9fe07f72e075b250295eeae25425490cc9608951 - react-native-slider: e540525ea731783850802b7af457d8551edb0711 + react-native-slider: 7d19220da2f2ae7cbb9aa80127cb73c597fa221f React-perflogger: 0458a87ea9a7342079e7a31b0d32b3734fb8415f React-RCTActionSheet: 22538001ea2926dea001111dd2846c13a0730bc9 React-RCTAnimation: 732ce66878d4aa151d56a0d142b1105aa12fd313 @@ -632,6 +638,6 @@ SPEC CHECKSUMS: RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8 Yoga: c4d61225a466f250c35c1ee78d2d0b3d41fe661c -PODFILE CHECKSUM: eeba196fb25cf059c631787109cecd08a4ac85a6 +PODFILE CHECKSUM: 819f14a4e3e6e335a0b1993fe37edad50db02d86 COCOAPODS: 1.11.3 diff --git a/change/@fluentui-react-native-experimental-native-font-metrics-a7673fa7-cc45-446e-83e0-02ce3ab92578.json b/change/@fluentui-react-native-experimental-native-font-metrics-a7673fa7-cc45-446e-83e0-02ce3ab92578.json new file mode 100644 index 0000000000..91e3edda09 --- /dev/null +++ b/change/@fluentui-react-native-experimental-native-font-metrics-a7673fa7-cc45-446e-83e0-02ce3ab92578.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Send new font metrics information through a JS event", + "packageName": "@fluentui-react-native/experimental-native-font-metrics", + "email": "adgleitm@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tester-955d714f-c8eb-47c4-bec6-5a032f9171d2.json b/change/@fluentui-react-native-tester-955d714f-c8eb-47c4-bec6-5a032f9171d2.json new file mode 100644 index 0000000000..0fb5c5404a --- /dev/null +++ b/change/@fluentui-react-native-tester-955d714f-c8eb-47c4-bec6-5a032f9171d2.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add Dynamic Type support", + "packageName": "@fluentui-react-native/tester", + "email": "adgleitm@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-text-234cd2bf-f18b-4347-8a20-cfe76b66a60d.json b/change/@fluentui-react-native-text-234cd2bf-f18b-4347-8a20-cfe76b66a60d.json new file mode 100644 index 0000000000..56ea4c9b65 --- /dev/null +++ b/change/@fluentui-react-native-text-234cd2bf-f18b-4347-8a20-cfe76b66a60d.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add Dynamic Type support", + "packageName": "@fluentui-react-native/text", + "email": "adgleitm@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/package.nuspec b/package.nuspec index 036df5f17b..bb28b9b883 100644 --- a/package.nuspec +++ b/package.nuspec @@ -12,10 +12,12 @@ + + diff --git a/packages/components/text/package.json b/packages/components/text/package.json index e281b32af0..50e1f55f3e 100644 --- a/packages/components/text/package.json +++ b/packages/components/text/package.json @@ -28,6 +28,7 @@ "dependencies": { "@uifabricshared/foundation-compose": "^1.12.22", "@fluentui-react-native/adapters": ">=0.10.0 <1.0.0", + "@fluentui-react-native/experimental-native-font-metrics": "^0.2.0", "@fluentui-react-native/framework": "0.8.21", "@fluentui-react-native/interactive-hooks": ">=0.21.2 <1.0.0", "@fluentui-react-native/theme-tokens": ">=0.21.3 <1.0.0", diff --git a/packages/components/text/src/Text.tsx b/packages/components/text/src/Text.tsx index c0e6c3f625..3fb19246eb 100644 --- a/packages/components/text/src/Text.tsx +++ b/packages/components/text/src/Text.tsx @@ -15,6 +15,7 @@ import { I18nManager, Platform, Text as RNText } from 'react-native'; import { textName, TextProps, TextTokens } from './Text.types'; import { useTextTokens } from './TextTokens'; import React from 'react'; +import { useFontMetricsScaleFactors } from '@fluentui-react-native/experimental-native-font-metrics'; const emptyProps = {}; export const Text = compressible((props: TextProps, useTokens: UseTokens) => { @@ -49,6 +50,9 @@ export const Text = compressible((props: TextProps, useTo // get the tokens from the theme let [tokens, cache] = useTokens(theme); + // TODO(#2268): Remove once RN Core properly supports Dynamic Type scaling + const fontMetricsScaleFactors = useFontMetricsScaleFactors(); + const textAlign = I18nManager.isRTL ? align === 'start' ? 'right' @@ -79,6 +83,9 @@ export const Text = compressible((props: TextProps, useTo [onPress, onAccessibilityTap], ); + // TODO(#2268): Remove once RN Core properly supports Dynamic Type scaling + const dynamicTypeVariant = Platform.OS === 'ios' ? tokens.dynamicTypeRamp : undefined; + // override tokens from props [tokens, cache] = patchTokens(tokens, cache, { color, @@ -106,6 +113,19 @@ export const Text = compressible((props: TextProps, useTo ['color', 'fontStyle', 'textAlign', 'textDecorationLine', ...fontStyles.keys], ); + // [TODO(#2268): Remove once RN Core properly supports Dynamic Type scaling + let scaleStyleAdjustments: TextTokens = emptyProps; + // tokenStyle.fontSize and tokenStyle.lineHeight can also be strings (e.g., "14px"). + // Therefore, we only support scaling for number-based size values in order to avoid any messy calculations. + if (dynamicTypeVariant !== undefined && typeof tokenStyle.fontSize === 'number' && typeof tokenStyle.lineHeight === 'number') { + const scaleFactor = fontMetricsScaleFactors[dynamicTypeVariant] ?? 1; + scaleStyleAdjustments = { + fontSize: tokenStyle.fontSize * scaleFactor, + lineHeight: tokenStyle.lineHeight * scaleFactor, + }; + } + // ]TODO(#2268) + const isWinPlatform = Platform.OS === (('win32' as any) || 'windows'); const filteredProps = { onKeyUp: isWinPlatform ? onKeyUp : undefined, @@ -124,9 +144,10 @@ export const Text = compressible((props: TextProps, useTo ...keyProps, ...filteredProps, ...extra, + ...(dynamicTypeVariant !== undefined && { allowFontScaling: false }), // TODO(#2268): Remove once RN Core properly supports Dynamic Type scaling onPress, numberOfLines: truncate || !wrap ? 1 : 0, - style: mergeStyles(tokenStyle, props.style, extra?.style), + style: mergeStyles(tokenStyle, props.style, extra?.style, scaleStyleAdjustments), }; return ( diff --git a/packages/components/text/src/Text.types.ts b/packages/components/text/src/Text.types.ts index f2c2ea3af2..ef75c24911 100644 --- a/packages/components/text/src/Text.types.ts +++ b/packages/components/text/src/Text.types.ts @@ -8,7 +8,11 @@ export const textName = 'Text'; * Text tokens, these are the internally configurable values for Text elements. In particular these * drive decisions on how to build the styles */ -export type TextTokens = Omit & IForegroundColorTokens & Omit; +export type TextTokens = Omit & + IForegroundColorTokens & + Omit & { + dynamicTypeRamp?: string; // TODO(#2268): Remove once RN Core properly supports Dynamic Type scaling + }; export type TextAlign = 'start' | 'center' | 'end' | 'justify'; export type TextFont = 'base' | 'monospace' | 'numeric'; diff --git a/packages/components/text/src/Variants.ios.ts b/packages/components/text/src/Variants.ios.ts index 592238c71f..5c1142547c 100644 --- a/packages/components/text/src/Variants.ios.ts +++ b/packages/components/text/src/Variants.ios.ts @@ -1,43 +1,57 @@ import { Text } from './Text'; +// TODO(#2268): Remove "as any" designations once RN Core properly supports Dynamic Type scaling + export const Caption1 = Text.customize({ variant: 'caption1', -}); + dynamicTypeRamp: 'footnote', +} as any); export const Caption1Strong = Text.customize({ variant: 'caption1Strong', -}); + dynamicTypeRamp: 'footnote', +} as any); export const Caption2 = Text.customize({ variant: 'caption2', -}); + dynamicTypeRamp: 'caption1', +} as any); export const Body1 = Text.customize({ variant: 'body1', -}); + dynamicTypeRamp: 'body', +} as any); export const Body1Strong = Text.customize({ variant: 'body1Strong', -}); + dynamicTypeRamp: 'body', +} as any); export const Body2 = Text.customize({ variant: 'body2', -}); + dynamicTypeRamp: 'subheadline', +} as any); export const Body2Strong = Text.customize({ variant: 'body2Strong', -}); + dynamicTypeRamp: 'subheadline', +} as any); export const Subtitle1 = null; // Not supported on iOS export const Subtitle1Strong = null; // Not supported on iOS export const Subtitle2 = null; // Not supported on iOS export const Subtitle2Strong = null; // Not supported on iOS export const Title1 = Text.customize({ variant: 'title1', -}); + dynamicTypeRamp: 'title1', +} as any); export const Title1Strong = null; // Not supported on iOS export const Title2 = Text.customize({ variant: 'title2', -}); + dynamicTypeRamp: 'title2', +} as any); export const Title3 = Text.customize({ variant: 'title3', -}); + dynamicTypeRamp: 'title3', +} as any); export const LargeTitle = Text.customize({ variant: 'largeTitle', -}); + dynamicTypeRamp: 'largeTitle', +} as any); export const Display = Text.customize({ variant: 'display', -}); + dynamicTypeRamp: 'largeTitle', +} as any); diff --git a/packages/experimental/NativeFontMetrics/ios/FRNFontMetrics.m b/packages/experimental/NativeFontMetrics/ios/FRNFontMetrics.m index 543b60f425..d266b0e706 100644 --- a/packages/experimental/NativeFontMetrics/ios/FRNFontMetrics.m +++ b/packages/experimental/NativeFontMetrics/ios/FRNFontMetrics.m @@ -85,7 +85,7 @@ + (BOOL)requiresMainQueueSetup return YES; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(allScaleFactors) +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(currentScaleFactors) { NSMutableDictionary *result = [NSMutableDictionary new]; [FRNRecognizedTextStyles() enumerateKeysAndObjectsUsingBlock:^(NSString * styleString, __unused NSNumber * boxedTextStyle, __unused BOOL * stop) { @@ -129,7 +129,7 @@ - (void)stopObserving - (void)onFontMetricsChanged:(NSNotification *)notification { if (_hasListeners) { - [self sendEventWithName:@"onFontMetricsChanged" body:@{@"newScaleFactors": [self allScaleFactors]}]; + [self sendEventWithName:@"onFontMetricsChanged" body:@{@"newScaleFactors": [self currentScaleFactors]}]; } } diff --git a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ios.ts b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ios.ts index 250b15d964..d53e52f2ba 100644 --- a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ios.ts +++ b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ios.ts @@ -4,7 +4,8 @@ import { ScaleFactors, TextStyle } from './NativeFontMetrics.types'; export const NativeFontMetrics = NativeModules.FRNFontMetrics; interface NativeFontMetricsInterface { - allScaleFactors(): ScaleFactors; + currentScaleFactors(): ScaleFactors; scaleFactorForStyle(style: TextStyle): number; } + export default NativeFontMetrics as NativeFontMetricsInterface; diff --git a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ts b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ts index 709bceb520..6bac794156 100644 --- a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ts +++ b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.ts @@ -1,12 +1,11 @@ -import { ScaleFactors, TextStyle } from './NativeFontMetrics.types'; +import { TextStyle } from './NativeFontMetrics.types'; -interface NativeFontMetricsInterface { - allScaleFactors(): ScaleFactors; - scaleFactorForStyle(style: TextStyle): number; -} - -const NativeFontMetrics: NativeFontMetricsInterface = { - allScaleFactors: () => { +const NativeFontMetrics = { + // eslint-disable-next-line @typescript-eslint/no-empty-function + addListener: (_: string) => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + removeListeners: (_: number) => {}, + currentScaleFactors: () => { console.warn('NativeFontMetrics is only available on iOS'); return {}; }, diff --git a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.types.ts b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.types.ts index 3c00d12e7a..30b518bec9 100644 --- a/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.types.ts +++ b/packages/experimental/NativeFontMetrics/src/NativeFontMetrics.types.ts @@ -1,3 +1,9 @@ +export interface FontMetrics { + readonly scaleFactors: ScaleFactors; +} + +export type ScaleFactors = { [K in TextStyle]?: number }; + export type TextStyle = | 'caption2' | 'caption1' @@ -10,5 +16,3 @@ export type TextStyle = | 'title2' | 'title1' | 'largeTitle'; - -export type ScaleFactors = { [K in TextStyle]?: number }; diff --git a/packages/experimental/NativeFontMetrics/src/fontMetrics.ios.ts b/packages/experimental/NativeFontMetrics/src/fontMetrics.ios.ts new file mode 100644 index 0000000000..8177540e44 --- /dev/null +++ b/packages/experimental/NativeFontMetrics/src/fontMetrics.ios.ts @@ -0,0 +1,25 @@ +import { NativeEventEmitter } from 'react-native'; +import NativeFontMetrics from './NativeFontMetrics'; +import { FontMetrics, ScaleFactors } from './NativeFontMetrics.types'; + +class FontMetricsImpl implements FontMetrics { + _scaleFactors: ScaleFactors; + + constructor() { + if (NativeFontMetrics) { + this._scaleFactors = NativeFontMetrics.currentScaleFactors(); + const eventEmitter = new NativeEventEmitter(NativeFontMetrics as any); + eventEmitter.addListener('onFontMetricsChanged', ({ newScaleFactors }) => { + this._scaleFactors = newScaleFactors; + }); + } else { + this._scaleFactors = {}; + } + } + + get scaleFactors(): ScaleFactors { + return this._scaleFactors; + } +} + +export const fontMetrics = new FontMetricsImpl() as FontMetrics; diff --git a/packages/experimental/NativeFontMetrics/src/fontMetrics.ts b/packages/experimental/NativeFontMetrics/src/fontMetrics.ts new file mode 100644 index 0000000000..24f180d1b1 --- /dev/null +++ b/packages/experimental/NativeFontMetrics/src/fontMetrics.ts @@ -0,0 +1,3 @@ +import { FontMetrics } from './NativeFontMetrics.types'; + +export const fontMetrics = { scaleFactors: {} } as FontMetrics; diff --git a/packages/experimental/NativeFontMetrics/src/index.ts b/packages/experimental/NativeFontMetrics/src/index.ts index cefd8c0449..1ec967c30b 100644 --- a/packages/experimental/NativeFontMetrics/src/index.ts +++ b/packages/experimental/NativeFontMetrics/src/index.ts @@ -1,2 +1 @@ -export * from './NativeFontMetrics'; export * from './useFontMetrics'; diff --git a/packages/experimental/NativeFontMetrics/src/useFontMetrics.ios.ts b/packages/experimental/NativeFontMetrics/src/useFontMetrics.ios.ts new file mode 100644 index 0000000000..e2564312da --- /dev/null +++ b/packages/experimental/NativeFontMetrics/src/useFontMetrics.ios.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; +import { NativeEventEmitter } from 'react-native'; +import { useSubscription } from 'use-subscription'; +import { fontMetrics } from './fontMetrics'; +import NativeFontMetrics from './NativeFontMetrics'; +import { ScaleFactors } from './NativeFontMetrics.types'; + +const eventEmitter = NativeFontMetrics ? new NativeEventEmitter(NativeFontMetrics as any) : undefined; + +export function useFontMetricsScaleFactors(): ScaleFactors { + if (!eventEmitter) { + return {}; + } + + const subscription = useMemo( + () => ({ + getCurrentValue: () => fontMetrics.scaleFactors, + subscribe: (callback) => { + const appearanceSubscription = eventEmitter.addListener('onFontMetricsChanged', callback); + return () => { + appearanceSubscription.remove(); + }; + }, + }), + [], + ); + + return useSubscription(subscription); +} diff --git a/packages/experimental/NativeFontMetrics/src/useFontMetrics.ts b/packages/experimental/NativeFontMetrics/src/useFontMetrics.ts index c391dd29b5..28e7664b11 100644 --- a/packages/experimental/NativeFontMetrics/src/useFontMetrics.ts +++ b/packages/experimental/NativeFontMetrics/src/useFontMetrics.ts @@ -1,29 +1,6 @@ -import { useMemo } from 'react'; -import { NativeEventEmitter, Platform } from 'react-native'; -import { useSubscription } from 'use-subscription'; -import NativeFontMetrics from './NativeFontMetrics'; import { ScaleFactors } from './NativeFontMetrics.types'; -const eventEmitter = new NativeEventEmitter(NativeFontMetrics as any); - -export function useFontMetrics(): ScaleFactors { - if (Platform.OS !== 'ios') { - console.warn('NativeFontMetrics is only available on iOS'); - return {}; - } - - const subscription = useMemo( - () => ({ - getCurrentValue: () => NativeFontMetrics.allScaleFactors(), - subscribe: (callback) => { - const appearanceSubscription = eventEmitter.addListener('onFontMetricsChanged', callback); - return () => { - appearanceSubscription.remove(); - }; - }, - }), - [], - ); - - return useSubscription(subscription); +export function useFontMetricsScaleFactors(): ScaleFactors { + // Stubbed out for non-iOS platforms + return {}; }