diff --git a/.maestro/enrichedInput/flows/custom_style_fonts_visual.yaml b/.maestro/enrichedInput/flows/custom_style_fonts_visual.yaml
new file mode 100644
index 000000000..ab8dacbf7
--- /dev/null
+++ b/.maestro/enrichedInput/flows/custom_style_fonts_visual.yaml
@@ -0,0 +1,116 @@
+appId: swmansion.enriched.example
+---
+# Validates custom font size and font family on plain text, inline styles, and paragraph styles.
+- launchApp
+
+- tapOn:
+ id: 'toggle-screen-button'
+
+- tapOn:
+ id: 'editor-input'
+
+# Section 1: Plain text with custom fonts
+
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-24'
+- inputText: '24px plain'
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-clear'
+- inputText: ' '
+
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-Nunito-Bold'
+- inputText: 'Bold family'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-clear'
+- inputText: ' '
+
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-32'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-Courier'
+- inputText: '32 Courier'
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-clear'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-clear'
+- inputText: ' '
+
+# Section 2: Inline styles + custom fonts
+
+- tapOn:
+ id: 'toolbar-bold'
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-20'
+- inputText: 'Bold 20px'
+- tapOn:
+ id: 'toolbar-bold'
+- tapOn:
+ id: 'toolbar-font-size'
+- tapOn:
+ id: 'font-size-clear'
+- inputText: ' '
+
+- tapOn:
+ id: 'toolbar-italic'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-Nunito-Italic'
+- inputText: 'Italic family'
+- tapOn:
+ id: 'toolbar-italic'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-clear'
+- inputText: ' '
+
+- tapOn:
+ id: 'toolbar-inline-code'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-CascadiaCode-Bold'
+- inputText: 'Code bold'
+- tapOn:
+ id: 'toolbar-inline-code'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-clear'
+- inputText: ' '
+- pressKey: Enter
+
+# Section 3: Paragraph styles + custom fonts
+
+- tapOn:
+ id: 'toolbar-quote'
+- tapOn:
+ id: 'toolbar-font-family'
+- tapOn:
+ id: 'font-family-CascadiaCode-Regular'
+- inputText: 'Quote code family'
+
+- runFlow:
+ file: '../subflows/capture_or_assert_screenshot.yaml'
+ env:
+ SCREENSHOT_NAME: 'custom_style_fonts'
diff --git a/.maestro/enrichedInput/screenshots/ios/custom_style_fonts.png b/.maestro/enrichedInput/screenshots/ios/custom_style_fonts.png
new file mode 100644
index 000000000..26a41abb7
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/ios/custom_style_fonts.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_1.png b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_1.png
index 7085daf9e..650394951 100644
Binary files a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_1.png and b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_1.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_2.png b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_2.png
index 9859c8102..a50059814 100644
Binary files a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_2.png and b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_2.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_3.png b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_3.png
index b2f8fc378..dd2d16428 100644
Binary files a/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_3.png and b/.maestro/enrichedInput/screenshots/ios/mention_popup_closing_on_cursor_travel_3.png differ
diff --git a/.maestro/enrichedText/flows/custom_style_fonts_visual.yaml b/.maestro/enrichedText/flows/custom_style_fonts_visual.yaml
new file mode 100644
index 000000000..e5c9b62a4
--- /dev/null
+++ b/.maestro/enrichedText/flows/custom_style_fonts_visual.yaml
@@ -0,0 +1,29 @@
+appId: swmansion.enriched.example
+---
+# Validates that custom style fonts are displayed correctly
+- launchApp
+
+- tapOn:
+ id: 'toggle-screen-button'
+
+- tapOn:
+ id: 'toggle-enriched-text-screen-button'
+
+- runFlow:
+ file: '../subflows/set_enriched_text_value.yaml'
+ env:
+ VALUE: >
+
+
Regular family
+ 24px plain
+ Bold family
+ 32 Courier
+ Bold 20px Italic family Code bold
+ H5 40px
+ Quote code family
+
+
+- runFlow:
+ file: '../subflows/capture_or_assert_screenshot.yaml'
+ env:
+ SCREENSHOT_NAME: 'custom_style_fonts_visual'
diff --git a/.maestro/enrichedText/screenshots/ios/custom_style_fonts_visual.png b/.maestro/enrichedText/screenshots/ios/custom_style_fonts_visual.png
new file mode 100644
index 000000000..03f364cc3
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/custom_style_fonts_visual.png differ
diff --git a/apps/example/src/components/FontFamilyPickerRow.tsx b/apps/example/src/components/FontFamilyPickerRow.tsx
new file mode 100644
index 000000000..f4f907748
--- /dev/null
+++ b/apps/example/src/components/FontFamilyPickerRow.tsx
@@ -0,0 +1,100 @@
+import { type FC } from 'react';
+import { Pressable, ScrollView, StyleSheet, Text } from 'react-native';
+
+export interface FontFamilyOption {
+ label: string;
+ value: string;
+}
+
+interface Props {
+ families: FontFamilyOption[];
+ activeFamily: string;
+ onSelectFamily: (family: string) => void;
+ onClear: () => void;
+}
+
+export const FontFamilyPickerRow: FC = ({
+ families,
+ activeFamily,
+ onSelectFamily,
+ onClear,
+}) => {
+ return (
+
+
+ ✕
+
+ {families.map(({ label, value }) => {
+ const isActive = value === activeFamily;
+ return (
+ onSelectFamily(value)}
+ style={[styles.familyButton, isActive && styles.familyButtonActive]}
+ >
+
+ {label}
+
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ backgroundColor: 'rgba(0, 26, 114, 0.9)',
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ gap: 8,
+ },
+ clearButton: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: 'rgba(255,255,255,0.15)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ clearText: {
+ color: 'white',
+ fontSize: 14,
+ lineHeight: 16,
+ },
+ familyButton: {
+ minWidth: 36,
+ height: 28,
+ paddingHorizontal: 8,
+ borderRadius: 14,
+ backgroundColor: 'rgba(255,255,255,0.15)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ familyButtonActive: {
+ backgroundColor: 'rgb(0, 26, 114)',
+ borderWidth: 2,
+ borderColor: 'white',
+ },
+ familyText: {
+ color: 'white',
+ fontSize: 14,
+ fontWeight: '600',
+ lineHeight: 16,
+ },
+});
diff --git a/apps/example/src/components/FontSizePickerRow.tsx b/apps/example/src/components/FontSizePickerRow.tsx
new file mode 100644
index 000000000..c22896909
--- /dev/null
+++ b/apps/example/src/components/FontSizePickerRow.tsx
@@ -0,0 +1,93 @@
+import { type FC } from 'react';
+import { Pressable, ScrollView, StyleSheet, Text } from 'react-native';
+
+interface Props {
+ sizes: number[];
+ activeSize: number;
+ onSelectSize: (size: number) => void;
+ onClear: () => void;
+}
+
+export const FontSizePickerRow: FC = ({
+ sizes,
+ activeSize,
+ onSelectSize,
+ onClear,
+}) => {
+ return (
+
+
+ ✕
+
+ {sizes.map((size) => {
+ const isActive = size === activeSize;
+ return (
+ onSelectSize(size)}
+ style={[styles.sizeButton, isActive && styles.sizeButtonActive]}
+ >
+ {size}
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ backgroundColor: 'rgba(0, 26, 114, 0.9)',
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ gap: 8,
+ },
+ clearButton: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: 'rgba(255,255,255,0.15)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ clearText: {
+ color: 'white',
+ fontSize: 14,
+ lineHeight: 16,
+ },
+ sizeButton: {
+ minWidth: 36,
+ height: 28,
+ paddingHorizontal: 8,
+ borderRadius: 14,
+ backgroundColor: 'rgba(255,255,255,0.15)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ sizeButtonActive: {
+ backgroundColor: 'rgb(0, 26, 114)',
+ borderWidth: 2,
+ borderColor: 'white',
+ },
+ sizeText: {
+ color: 'white',
+ fontSize: 14,
+ fontWeight: '600',
+ lineHeight: 16,
+ },
+});
diff --git a/apps/example/src/components/Toolbar.tsx b/apps/example/src/components/Toolbar.tsx
index c44d4d5d8..4d997779b 100644
--- a/apps/example/src/components/Toolbar.tsx
+++ b/apps/example/src/components/Toolbar.tsx
@@ -9,6 +9,8 @@ import {
} from 'react-native';
import { ToolbarButton } from './ToolbarButton';
import { ColorPickerRow } from './ColorPickerRow';
+import { FontSizePickerRow } from './FontSizePickerRow';
+import { FontFamilyPickerRow } from './FontFamilyPickerRow';
import type {
OnChangeStateEvent,
EnrichedTextInputInstance,
@@ -36,6 +38,16 @@ const COLORS = [
'#ADD8E6',
];
+const FONT_SIZES = [12, 16, 20, 24, 28, 32, 36, 40];
+
+const FONT_FAMILIES = [
+ { label: 'Bold', value: 'Nunito-Bold' },
+ { label: 'Italic', value: 'Nunito-Italic' },
+ { label: 'Code', value: 'CascadiaCode-Regular' },
+ { label: 'Code Bold', value: 'CascadiaCode-Bold' },
+ { label: 'Courier', value: 'Courier' },
+] as const;
+
const STYLE_ITEMS = [
{
name: 'bold',
@@ -133,11 +145,24 @@ const STYLE_ITEMS = [
name: 'bg-color',
text: 'BG',
},
+ {
+ name: 'font-size',
+ text: 'Aa',
+ },
+ {
+ name: 'font-family',
+ text: 'Ff',
+ },
] as const;
type Item = (typeof STYLE_ITEMS)[number];
type StylesState = OnChangeStateEvent;
-type OpenPicker = 'text-color' | 'bg-color' | null;
+type OpenPicker =
+ | 'text-color'
+ | 'bg-color'
+ | 'font-size'
+ | 'font-family'
+ | null;
export interface ToolbarProps {
stylesState: StylesState;
@@ -158,6 +183,11 @@ export const Toolbar: FC = ({
const activeFgColor = stylesState.customStyle?.foregroundColor ?? '';
const activeBgColor = stylesState.customStyle?.backgroundColor ?? '';
+ const activeFontSize = stylesState.customStyle?.fontSize ?? 0;
+ const activeFontFamily = stylesState.customStyle?.fontFamily ?? '';
+ const activeFontFamilyLabel =
+ FONT_FAMILIES.find((family) => family.value === activeFontFamily)?.label ??
+ '';
const fgIndicatorColor =
activeFgColor.length > 0 ? activeFgColor : 'transparent';
@@ -395,6 +425,48 @@ export const Toolbar: FC = ({
);
}
+ if (item.name === 'font-size') {
+ const fontSizeLabel = activeFontSize > 0 ? String(activeFontSize) : 'Aa';
+ return (
+
+ setOpenPicker((prev) => (prev === 'font-size' ? null : 'font-size'))
+ }
+ style={[
+ styles.colorButton,
+ layout === 'grid' ? styles.gridItem : undefined,
+ (openPicker === 'font-size' || activeFontSize > 0) &&
+ styles.colorButtonActive,
+ ]}
+ >
+ {fontSizeLabel}
+
+ );
+ }
+
+ if (item.name === 'font-family') {
+ const fontFamilyLabel = activeFontFamilyLabel || 'Ff';
+ return (
+
+ setOpenPicker((prev) =>
+ prev === 'font-family' ? null : 'font-family'
+ )
+ }
+ style={[
+ styles.colorButton,
+ layout === 'grid' ? styles.gridItem : undefined,
+ (openPicker === 'font-family' || activeFontFamily.length > 0) &&
+ styles.colorButtonActive,
+ ]}
+ >
+ {fontFamilyLabel}
+
+ );
+ }
+
return (
= ({
setOpenPicker(null);
};
+ const handleSelectFontSize = (size: number) => {
+ editorRef?.current?.setStyle({ fontSize: size });
+ setOpenPicker(null);
+ };
+
+ const handleClearFontSize = () => {
+ editorRef?.current?.setStyle({ fontSize: null });
+ setOpenPicker(null);
+ };
+
+ const handleSelectFontFamily = (family: string) => {
+ editorRef?.current?.setStyle({ fontFamily: family });
+ setOpenPicker(null);
+ };
+
+ const handleClearFontFamily = () => {
+ editorRef?.current?.setStyle({ fontFamily: null });
+ setOpenPicker(null);
+ };
+
return (
= ({
onClear={handleClearBgColor}
/>
)}
+ {openPicker === 'font-size' && (
+
+ )}
+ {openPicker === 'font-family' && (
+
+ )}
);
};
diff --git a/apps/example/src/constants/editorConfig.ts b/apps/example/src/constants/editorConfig.ts
index 092911f9e..f29ac11c0 100644
--- a/apps/example/src/constants/editorConfig.ts
+++ b/apps/example/src/constants/editorConfig.ts
@@ -36,6 +36,8 @@ export const DEFAULT_STYLES: StylesState = {
customStyle: {
foregroundColor: '',
backgroundColor: '',
+ fontSize: 0,
+ fontFamily: '',
},
};
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 5e6123cd5..f78ce6b0c 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1168,7 +1168,12 @@ - (void)tryUpdatingActiveStyles {
?: "",
.backgroundColor =
[[currentCustomStyle.backgroundColor hexString] UTF8String]
- ?: ""}});
+ ?: "",
+ .fontSize = currentCustomStyle.fontSize
+ ? [currentCustomStyle.fontSize doubleValue]
+ : 0.0,
+ .fontFamily =
+ [currentCustomStyle.fontFamily UTF8String] ?: ""}});
}
}
@@ -1568,6 +1573,19 @@ - (void)setStyle:(NSString *)styleJSON {
: [RCTConvert UIColor:bgRaw];
}
+ id fsRaw = dict[@"fontSize"];
+ if (fsRaw != nil) {
+ processedDict[@"fontSize"] =
+ [fsRaw isKindOfClass:[NSNull class]] ? [NSNull null] : fsRaw;
+ }
+
+ id ffRaw = dict[@"fontFamily"];
+ if (ffRaw != nil) {
+ processedDict[@"fontFamily"] = [ffRaw isKindOfClass:[NSNull class]]
+ ? [NSNull null]
+ : [RCTConvert NSString:ffRaw];
+ }
+
[customStyleClass applyStyleFromDict:processedDict
selectedRange:selectedRange];
[self anyTextMayHaveBeenModified];
@@ -1976,7 +1994,12 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText {
.backgroundColor =
[[contextCustomStyleData.backgroundColor hexString]
UTF8String]
- ?: ""}}});
+ ?: "",
+ .fontSize = contextCustomStyleData.fontSize
+ ? [contextCustomStyleData.fontSize doubleValue]
+ : 0.0,
+ .fontFamily =
+ [contextCustomStyleData.fontFamily UTF8String] ?: ""}}});
}
}
diff --git a/ios/config/EnrichedConfig.h b/ios/config/EnrichedConfig.h
index d39248baf..e72f58f6b 100644
--- a/ios/config/EnrichedConfig.h
+++ b/ios/config/EnrichedConfig.h
@@ -89,6 +89,7 @@
- (CGFloat)codeBlockBorderRadius;
- (void)setCodeBlockBorderRadius:(CGFloat)newValue;
- (void)invalidateFonts;
+- (CGFloat)scaledFontSizeForValue:(CGFloat)fontSize;
- (NSNumber *)scaledPrimaryFontSize;
- (BOOL)allowFontScaling;
- (void)setAllowFontScaling:(BOOL)newValue;
diff --git a/ios/config/EnrichedConfig.mm b/ios/config/EnrichedConfig.mm
index 05d0e675e..6a8f146ae 100644
--- a/ios/config/EnrichedConfig.mm
+++ b/ios/config/EnrichedConfig.mm
@@ -162,11 +162,7 @@ - (void)setPrimaryLineHeight:(CGFloat)newValue {
}
- (CGFloat)scaledPrimaryLineHeight {
- if (!_allowFontScaling) {
- return [self primaryLineHeight];
- }
- return [[UIFontMetrics defaultMetrics]
- scaledValueForValue:[self primaryLineHeight]];
+ return [self scaledFontSizeForValue:[self primaryLineHeight]];
}
- (NSString *)primaryFontWeight {
@@ -235,10 +231,7 @@ - (void)setMentionIndicators:(NSSet *)newValue {
}
- (CGFloat)h1FontSize {
- if (!_allowFontScaling) {
- return _h1FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h1FontSize];
+ return [self scaledFontSizeForValue:_h1FontSize];
}
- (void)setH1FontSize:(CGFloat)newValue {
@@ -254,10 +247,7 @@ - (void)setH1Bold:(BOOL)newValue {
}
- (CGFloat)h2FontSize {
- if (!_allowFontScaling) {
- return _h2FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h2FontSize];
+ return [self scaledFontSizeForValue:_h2FontSize];
}
- (void)setH2FontSize:(CGFloat)newValue {
@@ -273,10 +263,7 @@ - (void)setH2Bold:(BOOL)newValue {
}
- (CGFloat)h3FontSize {
- if (!_allowFontScaling) {
- return _h3FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h3FontSize];
+ return [self scaledFontSizeForValue:_h3FontSize];
}
- (void)setH3FontSize:(CGFloat)newValue {
@@ -292,10 +279,7 @@ - (void)setH3Bold:(BOOL)newValue {
}
- (CGFloat)h4FontSize {
- if (!_allowFontScaling) {
- return _h4FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h4FontSize];
+ return [self scaledFontSizeForValue:_h4FontSize];
}
- (void)setH4FontSize:(CGFloat)newValue {
@@ -311,10 +295,7 @@ - (void)setH4Bold:(BOOL)newValue {
}
- (CGFloat)h5FontSize {
- if (!_allowFontScaling) {
- return _h5FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h5FontSize];
+ return [self scaledFontSizeForValue:_h5FontSize];
}
- (void)setH5FontSize:(CGFloat)newValue {
@@ -330,10 +311,7 @@ - (void)setH5Bold:(BOOL)newValue {
}
- (CGFloat)h6FontSize {
- if (!_allowFontScaling) {
- return _h6FontSize;
- }
- return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h6FontSize];
+ return [self scaledFontSizeForValue:_h6FontSize];
}
- (void)setH6FontSize:(CGFloat)newValue {
@@ -560,13 +538,15 @@ - (void)setAllowFontScaling:(BOOL)newValue {
}
}
-- (NSNumber *)scaledPrimaryFontSize {
+- (CGFloat)scaledFontSizeForValue:(CGFloat)fontSize {
if (!_allowFontScaling) {
- return [self primaryFontSize];
+ return fontSize;
}
- CGFloat scaledSize = [[UIFontMetrics defaultMetrics]
- scaledValueForValue:[[self primaryFontSize] floatValue]];
- return @(scaledSize);
+ return [[UIFontMetrics defaultMetrics] scaledValueForValue:fontSize];
+}
+
+- (NSNumber *)scaledPrimaryFontSize {
+ return @([self scaledFontSizeForValue:[[self primaryFontSize] floatValue]]);
}
- (CGFloat)checkboxListBoxSize {
diff --git a/ios/customStyleData/CustomStyleData.h b/ios/customStyleData/CustomStyleData.h
index 7ee405f8e..c6ac30e40 100644
--- a/ios/customStyleData/CustomStyleData.h
+++ b/ios/customStyleData/CustomStyleData.h
@@ -5,6 +5,8 @@
@property(nonatomic, strong, nullable) UIColor *foregroundColor;
@property(nonatomic, strong, nullable) UIColor *backgroundColor;
+@property(nonatomic, strong, nullable) NSNumber *fontSize;
+@property(nonatomic, copy, nullable) NSString *fontFamily;
- (BOOL)isEmpty;
diff --git a/ios/customStyleData/CustomStyleData.mm b/ios/customStyleData/CustomStyleData.mm
index c08f1fd54..cfdd0dc81 100644
--- a/ios/customStyleData/CustomStyleData.mm
+++ b/ios/customStyleData/CustomStyleData.mm
@@ -3,7 +3,8 @@
@implementation CustomStyleData
- (BOOL)isEmpty {
- return _foregroundColor == nil && _backgroundColor == nil;
+ return _foregroundColor == nil && _backgroundColor == nil &&
+ _fontSize == nil && _fontFamily == nil;
}
- (void)mergeFromDict:(NSDictionary *)dict {
@@ -17,6 +18,20 @@ - (void)mergeFromDict:(NSDictionary *)dict {
self.backgroundColor =
[bgVal isKindOfClass:[UIColor class]] ? (UIColor *)bgVal : nil;
}
+ id fsVal = dict[@"fontSize"];
+ if (fsVal != nil) {
+ if ([fsVal isKindOfClass:[NSNumber class]] &&
+ [(NSNumber *)fsVal doubleValue] > 0) {
+ self.fontSize = (NSNumber *)fsVal;
+ } else {
+ self.fontSize = nil;
+ }
+ }
+ id ffVal = dict[@"fontFamily"];
+ if (ffVal != nil) {
+ self.fontFamily =
+ [ffVal isKindOfClass:[NSString class]] ? (NSString *)ffVal : nil;
+ }
}
- (BOOL)isEqual:(id)object {
@@ -29,17 +44,25 @@ - (BOOL)isEqual:(id)object {
[_foregroundColor isEqual:other.foregroundColor];
BOOL bgEqual = (_backgroundColor == other.backgroundColor) ||
[_backgroundColor isEqual:other.backgroundColor];
- return fgEqual && bgEqual;
+ BOOL fsEqual = (_fontSize == other.fontSize) ||
+ (_fontSize != nil && other.fontSize != nil &&
+ [_fontSize isEqualToNumber:other.fontSize]);
+ BOOL ffEqual = (_fontFamily == other.fontFamily) ||
+ [_fontFamily isEqualToString:other.fontFamily];
+ return fgEqual && bgEqual && fsEqual && ffEqual;
}
- (NSUInteger)hash {
- return [_foregroundColor hash] ^ [_backgroundColor hash];
+ return [_foregroundColor hash] ^ [_backgroundColor hash] ^ [_fontSize hash] ^
+ [_fontFamily hash];
}
- (id)copyWithZone:(NSZone *)zone {
CustomStyleData *copy = [[CustomStyleData allocWithZone:zone] init];
copy.foregroundColor = self.foregroundColor;
copy.backgroundColor = self.backgroundColor;
+ copy.fontSize = self.fontSize;
+ copy.fontFamily = self.fontFamily;
return copy;
}
diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm
index c6b778a73..db0847e52 100644
--- a/ios/htmlParser/HtmlParser.mm
+++ b/ios/htmlParser/HtmlParser.mm
@@ -1495,6 +1495,22 @@ + (NSString *)tagContentForStyle:(NSNumber *)style
[cssProps appendString:@" "];
[cssProps appendFormat:@"background-color: %@;", bg];
}
+ if (data.fontSize != nil) {
+ if (cssProps.length > 0)
+ [cssProps appendString:@" "];
+ [cssProps
+ appendFormat:@"font-size: %gpx;", [data.fontSize doubleValue]];
+ }
+ if (data.fontFamily.length > 0) {
+ if (cssProps.length > 0)
+ [cssProps appendString:@" "];
+ if ([data.fontFamily rangeOfString:@" "].location != NSNotFound) {
+ // font family contains spaces, wrap in single quotes
+ [cssProps appendFormat:@"font-family: '%@';", data.fontFamily];
+ } else {
+ [cssProps appendFormat:@"font-family: %@;", data.fontFamily];
+ }
+ }
if (cssProps.length > 0) {
return [NSString stringWithFormat:@"span style=\"%@\"", cssProps];
}
@@ -1533,6 +1549,8 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams:
static NSRegularExpression *styleAttrRegex;
static NSRegularExpression *fgRegex;
static NSRegularExpression *bgRegex;
+ static NSRegularExpression *fontSizeRegex;
+ static NSRegularExpression *fontFamilyRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
styleAttrRegex = [NSRegularExpression
@@ -1551,6 +1569,17 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams:
regularExpressionWithPattern:@"background-color\\s*:\\s*([^;]+)"
options:NSRegularExpressionCaseInsensitive
error:nil];
+
+ fontSizeRegex = [NSRegularExpression
+ regularExpressionWithPattern:
+ @"font-size\\s*:\\s*([0-9.]+)(?:\\s*px)?(?=\\s*;|\\s*$)"
+ options:NSRegularExpressionCaseInsensitive
+ error:nil];
+
+ fontFamilyRegex = [NSRegularExpression
+ regularExpressionWithPattern:@"font-family\\s*:\\s*([^;]+)"
+ options:NSRegularExpressionCaseInsensitive
+ error:nil];
});
NSTextCheckingResult *attrMatch =
@@ -1581,6 +1610,50 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams:
colorFromCSSString:[css substringWithRange:[bgMatch rangeAtIndex:1]]];
}
+ NSTextCheckingResult *fontSizeMatch =
+ [fontSizeRegex firstMatchInString:css
+ options:0
+ range:NSMakeRange(0, css.length)];
+ if (fontSizeMatch) {
+ NSString *fontSizeString =
+ [css substringWithRange:[fontSizeMatch rangeAtIndex:1]];
+ double parsedFontSize = [fontSizeString doubleValue];
+ if (parsedFontSize > 0) {
+ data.fontSize = @(parsedFontSize);
+ }
+ }
+
+ NSTextCheckingResult *fontFamilyMatch =
+ [fontFamilyRegex firstMatchInString:css
+ options:0
+ range:NSMakeRange(0, css.length)];
+ if (fontFamilyMatch) {
+ NSString *fontFamilyString = [[css
+ substringWithRange:[fontFamilyMatch rangeAtIndex:1]]
+ stringByTrimmingCharactersInSet:[NSCharacterSet
+ whitespaceAndNewlineCharacterSet]];
+
+ // If font family contains a comma, use the first part e.g. "Nunito,
+ // sans-serif" -> "Nunito"
+ NSRange commaRange = [fontFamilyString rangeOfString:@","];
+ if (commaRange.location != NSNotFound) {
+ fontFamilyString =
+ [[fontFamilyString substringToIndex:commaRange.location]
+ stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ }
+ if (([fontFamilyString hasPrefix:@"'"] &&
+ [fontFamilyString hasSuffix:@"'"]) ||
+ ([fontFamilyString hasPrefix:@"\""] &&
+ [fontFamilyString hasSuffix:@"\""])) {
+ fontFamilyString = [fontFamilyString
+ substringWithRange:NSMakeRange(1, fontFamilyString.length - 2)];
+ }
+ if (fontFamilyString.length > 0) {
+ data.fontFamily = fontFamilyString;
+ }
+ }
+
return data.isEmpty ? nil : data;
}
diff --git a/ios/styles/CustomStyle.mm b/ios/styles/CustomStyle.mm
index b79f767db..14f186837 100644
--- a/ios/styles/CustomStyle.mm
+++ b/ios/styles/CustomStyle.mm
@@ -1,8 +1,10 @@
#import "ColorExtension.h"
#import "CustomStyleData.h"
#import "EnrichedTextInputView.h"
+#import "FontExtension.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
+#import
static NSString *const CustomStyleAttributeName = @"EnrichedCustomStyle";
@@ -24,6 +26,46 @@ - (NSInteger)stylePriority {
return 1;
}
+- (UIFont *)resolveFont:(UIFont *)font forData:(CustomStyleData *)data {
+ UIFont *resolvedFont = font ?: [self.host.config primaryFont];
+ UIFont *originalFont = resolvedFont;
+
+ if (data.fontFamily.length > 0) {
+ resolvedFont = [RCTFont updateFont:resolvedFont
+ withFamily:data.fontFamily
+ size:0
+ weight:nullptr
+ style:nullptr
+ variant:nullptr
+ scaleMultiplier:1];
+ resolvedFont = [resolvedFont withFontTraits:originalFont];
+ }
+
+ if (data.fontSize != nil) {
+ CGFloat pointSize =
+ [self.host.config scaledFontSizeForValue:[data.fontSize doubleValue]];
+ resolvedFont = [resolvedFont setSize:pointSize];
+ }
+
+ return resolvedFont;
+}
+
+- (BOOL)appliesStylingToTyping {
+ CustomStyleData *data =
+ [self getCustomStyleDataAt:self.host.textView.selectedRange.location];
+ return data.fontSize != nil || data.fontFamily.length > 0;
+}
+
+- (void)applyStylingToTypingAttrs:(NSMutableDictionary *)attributes {
+ CustomStyleData *data =
+ [self getCustomStyleDataAt:self.host.textView.selectedRange.location];
+ if (data.fontSize == nil && data.fontFamily.length == 0)
+ return;
+
+ UIFont *currentFont = attributes[NSFontAttributeName];
+ attributes[NSFontAttributeName] = [self resolveFont:currentFont forData:data];
+}
+
- (BOOL)styleCondition:(id)value range:(NSRange)range {
if (![value isKindOfClass:[CustomStyleData class]])
return NO;
@@ -65,17 +107,37 @@ - (void)applyStyling:(NSRange)range {
attrs[NSBackgroundColorAttributeName] =
[data.backgroundColor colorWithResolvedAlpha];
}
- if (attrs.count == 0)
- return;
+
+ BOOL hasFontOverride =
+ data.fontSize != nil || data.fontFamily.length > 0;
// Skip newline characters so background color doesn't bleed.
NSArray *nonNewlineRanges =
[RangeUtils getNonNewlineRangesIn:self.host.textView
range:subRange];
for (NSValue *rangeVal in nonNewlineRanges) {
- [self.host.textView.textStorage
- addAttributes:attrs
- range:[rangeVal rangeValue]];
+ NSRange applyRange = [rangeVal rangeValue];
+ if (attrs.count > 0) {
+ [self.host.textView.textStorage addAttributes:attrs
+ range:applyRange];
+ }
+ if (hasFontOverride) {
+ [self.host.textView.textStorage
+ enumerateAttribute:NSFontAttributeName
+ inRange:applyRange
+ options:0
+ usingBlock:^(id _Nullable value,
+ NSRange fontRange,
+ BOOL *_Nonnull stop) {
+ UIFont *font = (UIFont *)value;
+ UIFont *newFont = [self resolveFont:font
+ forData:data];
+ [self.host.textView.textStorage
+ addAttribute:NSFontAttributeName
+ value:newFont
+ range:fontRange];
+ }];
+ }
}
}];
}
diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx
index 0b102b700..9f3374dc4 100644
--- a/src/native/EnrichedTextInput.tsx
+++ b/src/native/EnrichedTextInput.tsx
@@ -295,10 +295,14 @@ export const EnrichedTextInput = ({
setStyle: (customStyle: {
foregroundColor?: ColorValue | null;
backgroundColor?: ColorValue | null;
+ fontSize?: number | null;
+ fontFamily?: string | null;
}) => {
const payload: {
foregroundColor?: number | null;
backgroundColor?: number | null;
+ fontSize?: number | null;
+ fontFamily?: string | null;
} = {};
if (customStyle.foregroundColor !== undefined) {
payload.foregroundColor = getSafeColorInt(customStyle.foregroundColor);
@@ -306,6 +310,12 @@ export const EnrichedTextInput = ({
if (customStyle.backgroundColor !== undefined) {
payload.backgroundColor = getSafeColorInt(customStyle.backgroundColor);
}
+ if (customStyle.fontSize !== undefined) {
+ payload.fontSize = customStyle.fontSize;
+ }
+ if (customStyle.fontFamily !== undefined) {
+ payload.fontFamily = customStyle.fontFamily;
+ }
Commands.setStyle(nullthrows(nativeRef.current), JSON.stringify(payload));
},
}));
diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts
index 3467acadb..eea2099d1 100644
--- a/src/spec/EnrichedTextInputNativeComponent.ts
+++ b/src/spec/EnrichedTextInputNativeComponent.ts
@@ -127,6 +127,8 @@ export interface OnChangeStateEvent {
customStyle: {
foregroundColor: string;
backgroundColor: string;
+ fontSize: Float;
+ fontFamily: string;
};
}
@@ -287,6 +289,8 @@ export interface OnContextMenuItemPressEvent {
customStyle: {
foregroundColor: string;
backgroundColor: string;
+ fontSize: Float;
+ fontFamily: string;
};
};
}
diff --git a/src/types.ts b/src/types.ts
index 80bf14ba0..3454d70d5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -383,6 +383,8 @@ export interface OnChangeStateEvent {
customStyle: {
foregroundColor: string;
backgroundColor: string;
+ fontSize: number;
+ fontFamily: string;
};
}
@@ -579,6 +581,8 @@ export interface EnrichedTextInputInstance extends NativeMethods {
setStyle: (customStyle: {
foregroundColor?: ColorValue | null;
backgroundColor?: ColorValue | null;
+ fontSize?: number | null;
+ fontFamily?: string | null;
}) => void;
}
diff --git a/src/web/useOnChangeState.ts b/src/web/useOnChangeState.ts
index 3affc3536..2015f86e2 100644
--- a/src/web/useOnChangeState.ts
+++ b/src/web/useOnChangeState.ts
@@ -104,6 +104,8 @@ function buildState(
editor.getAttributes('customStyle').foregroundColor ?? '',
backgroundColor:
editor.getAttributes('customStyle').backgroundColor ?? '',
+ fontSize: 0,
+ fontFamily: '',
},
};
}