Use FRNFontMetrics module in `Text` (#2269)

* Add FRNFontMetrics module

* Remove debugging `console.log` statements

* Use a hook to access font metrics

* Add NativeFontMetrics package

* Remove unneeded API

* Fix spacing

* Change files

* Add @types/use-subscription dependency

* Update yarn.lock

* Add use-subscription as a dependency

* Update yarn.lock again

* NativeFontMetrics.tsx -> NativeFontMetrics.ts

* Make NativeFontMetrics iOS only, part 1

* Stub out NativeFontMetrics for non-iOS platforms

* Keep use-subscription version consistent with react-native and react-native-macos

* Remove core-android capability

* id -> UIFontTextStyle

* Delete old changefile

* Regenerate lockfiles

* Delete duplicate NativeFontMetrics.tsx file

* Change files

* allScaleFactors -> currentScaleFactors

* Use pure events to update font metrics

* Update package.nuspec

* Use pure JS hook to make V2 Texts rerender

* Change files

* Update snapshot

* Remove unneeded dependency

* Move fontMetrics to iOS specific file

* Better isolation of iOS-specific code

* Add useFontMetrics.ios.ts

* Remove useFontMetricsScaleFactors warning on non-iOS platforms

* Move fontMetrics accesses outside of continuation, and improve typing

* Fix Text rerender shallow equality test

* Handle case when NativeFontMetrics isn't defined

* IFontMetrics -> FontMetrics

* Keep `mergedProps` spreads next to each other

Co-authored-by: Adam Gleitman <adgleitm@microsoft.com>
This commit is contained in:
Adam Gleitman 2022-11-28 11:29:22 -08:00 коммит произвёл GitHub
Родитель bc62b6fb1d
Коммит 1b0f4b66f9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 168 добавлений и 59 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add Dynamic Type support",
"packageName": "@fluentui-react-native/tester",
"email": "adgleitm@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add Dynamic Type support",
"packageName": "@fluentui-react-native/text",
"email": "adgleitm@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -12,10 +12,12 @@
<!-- iOS device ship -->
<file src="DerivedData\Build\Products\Release-iphoneos\FRNAvatar\libFRNAvatar.a" target="Ship-iphoneos"/>
<file src="DerivedData\Build\Products\Release-iphoneos\FRNDatePicker\libFRNDatePicker.a" target="Ship-iphoneos"/>
<file src="DerivedData\Build\Products\Release-iphoneos\FRNFontMetrics\libFRNFontMetrics.a" target="Ship-iphoneos"/>
<!-- iOS simulator ship -->
<file src="DerivedData\Build\Products\Release-iphonesimulator\FRNAvatar\libFRNAvatar.a" target="Ship-iphonesimulator"/>
<file src="DerivedData\Build\Products\Release-iphonesimulator\FRNDatePicker\libFRNDatePicker.a" target="Ship-iphonesimulator"/>
<file src="DerivedData\Build\Products\Release-iphonesimulator\FRNFontMetrics\libFRNFontMetrics.a" target="Ship-iphoneos"/>
<!-- macOS ship -->
<file src="DerivedData\Build\Products\Release\FRNAvatar\libFRNAvatar.a" target="Ship-macosx"/>

Просмотреть файл

@ -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",

Просмотреть файл

@ -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<TextProps, TextTokens>((props: TextProps, useTokens: UseTokens<TextTokens>) => {
@ -49,6 +50,9 @@ export const Text = compressible<TextProps, TextTokens>((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<TextProps, TextTokens>((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<TextProps, TextTokens>((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<TextProps, TextTokens>((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 (
<RNText ellipsizeMode={!wrap && !truncate ? 'clip' : 'tail'} {...mergedProps}>

Просмотреть файл

@ -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<FontTokens, 'fontFamily'> & IForegroundColorTokens & Omit<TextStyle, 'fontSize' | 'fontWeight' | 'color'>;
export type TextTokens = Omit<FontTokens, 'fontFamily'> &
IForegroundColorTokens &
Omit<TextStyle, 'fontSize' | 'fontWeight' | 'color'> & {
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';

Просмотреть файл

@ -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);

Просмотреть файл

@ -85,7 +85,7 @@ CGFloat FRNBaseSizeForTextStyle(FRNTextStyle textStyle) {
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 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(scaleFactorForStyle:(NSString *)styleStri
- (void)onFontMetricsChanged:(NSNotification *)notification {
if (_hasListeners) {
[self sendEventWithName:@"onFontMetricsChanged" body:@{@"newScaleFactors": [self allScaleFactors]}];
[self sendEventWithName:@"onFontMetricsChanged" body:@{@"newScaleFactors": [self currentScaleFactors]}];
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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 {};
},

Просмотреть файл

@ -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 };

Просмотреть файл

@ -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;

Просмотреть файл

@ -0,0 +1,3 @@
import { FontMetrics } from './NativeFontMetrics.types';
export const fontMetrics = { scaleFactors: {} } as FontMetrics;

Просмотреть файл

@ -1,2 +1 @@
export * from './NativeFontMetrics';
export * from './useFontMetrics';

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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 {};
}