Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/android-system-font-weight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ui-kitten/components": patch
---

On Android, text with the default `System` font family now keeps its exact `fontWeight`. React Native Android treats any `fontFamily` as a custom family and rounds the weight to regular or bold, so `500` / `600` text (subtitles, labels, radio and checkbox text, avatar initials) rendered regular. Styles resolved from the theme now leave the family unset when it is `System` on Android; iOS and custom families are unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ const hasCompleteTextStyle = (props: TextProps): boolean => {
}
const style: TextStyle = StyleSheet.flatten(props.style) || {};

// `fontFamily` may be present with `undefined`: the mapping resolves `System` to no family on
// Android (see `resolvePlatformFontFamily`), which still means the style set it.
return style.color !== undefined
&& style.fontFamily !== undefined
&& 'fontFamily' in style
&& style.fontSize !== undefined
&& style.fontWeight !== undefined;
};
Expand Down
29 changes: 29 additions & 0 deletions src/components/theme/style/platformFontFamily.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { resolvePlatformFontFamily } from './platformFontFamily';

describe('@platform-font-family: resolvePlatformFontFamily', () => {

it('should drop the System family on Android for font family keys', () => {
expect(resolvePlatformFontFamily('fontFamily', 'System', 'android')).toBeUndefined();
expect(resolvePlatformFontFamily('textFontFamily', 'System', 'android')).toBeUndefined();
expect(resolvePlatformFontFamily('titleFontfamily', 'System', 'android')).toBeUndefined();
});

it('should keep the System family on iOS and web', () => {
expect(resolvePlatformFontFamily('fontFamily', 'System', 'ios')).toEqual('System');
expect(resolvePlatformFontFamily('textFontFamily', 'System', 'web')).toEqual('System');
});

it('should keep a custom family on Android', () => {
expect(resolvePlatformFontFamily('fontFamily', 'Roboto-Medium', 'android')).toEqual('Roboto-Medium');
});

it('should keep System under a key that is not a font family', () => {
expect(resolvePlatformFontFamily('accessibilityLabel', 'System', 'android')).toEqual('System');
expect(resolvePlatformFontFamily('fontWeight', 'System', 'android')).toEqual('System');
});

it('should keep non-string values', () => {
expect(resolvePlatformFontFamily('fontFamily', undefined, 'android')).toBeUndefined();
expect(resolvePlatformFontFamily('fontFamily', 16, 'android')).toEqual(16);
});
});
37 changes: 37 additions & 0 deletions src/components/theme/style/platformFontFamily.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright Akveo. All Rights Reserved.
* Copyright (c) 2024-2026 Vlad Bataev and UI Kitten Contributors.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import {
Platform,
PlatformOSType,
} from 'react-native';

const FONT_FAMILY_KEY = /fontFamily$/i;
const SYSTEM_FONT_FAMILY = 'System';

/**
* Resolves a themed style value for the current platform's font handling.
*
* `System` names the platform font. iOS resolves it as such, but React Native Android treats any
* `fontFamily` as a custom family: it rounds `fontWeight` to normal or bold (the asset lookup only
* knows `_bold` files) and falls back to the default typeface, so 500 and 600 render as 400.
* Without a `fontFamily` Android keeps the numeric weight on the default typeface.
*
* So on Android a `System` value of a `*fontFamily` key resolves to `undefined`. Callers keep the key
* (with `undefined`) so a flattened style still reports that the mapping set a font family; see
* `FalsyText`. Every other value, key and platform passes through unchanged.
*/
export const resolvePlatformFontFamily = (
key: string,
value: unknown,
os: PlatformOSType = Platform.OS,
): unknown => {
if (os === 'android' && value === SYSTEM_FONT_FAMILY && FONT_FAMILY_KEY.test(key)) {
return undefined;
}
return value;
};
3 changes: 2 additions & 1 deletion src/components/theme/style/style.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
ThemeType,
useTheme,
} from '../theme/theme.service';
import { resolvePlatformFontFamily } from './platformFontFamily';

// eslint-disable-next-line @typescript-eslint/no-explicit-any

Check warning on line 17 in src/components/theme/style/style.service.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

'@typescript-eslint/no-explicit-any' rule is disabled but never reported
export type StyleType = Record<string, any>;
export type Styles<T> = StyleSheet.NamedStyles<T>;

Expand Down Expand Up @@ -48,7 +49,7 @@

return useMemo(() => {
return StyleService.createThemed(styles, theme);
}, [theme]);

Check warning on line 52 in src/components/theme/style/style.service.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

React Hook useMemo has a missing dependency: 'styles'. Either include it or remove the dependency array
};

/**
Expand Down Expand Up @@ -92,7 +93,7 @@
/**
* @returns stylesheet mapped to theme
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any

Check warning on line 96 in src/components/theme/style/style.service.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

'@typescript-eslint/no-explicit-any' rule is disabled but never reported
static createThemed = <T extends Styles<T>>(styles: Styles<T>, theme: ThemeType): any => {
const themed: StyleType = {};
for (const key in styles) {
Expand All @@ -108,7 +109,7 @@
const themed: StyleType = {};
for (const key in style) {
const value = style[key];
themed[key] = ThemeService.getValue(value, theme, value);
themed[key] = resolvePlatformFontFamily(key, ThemeService.getValue(value, theme, value));
}
return themed;
};
Expand Down
10 changes: 10 additions & 0 deletions src/components/theme/style/style.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ describe('@style-service: service method checks', () => {
});
});

it('should resolve font families for the running platform', () => {
// Jest runs as iOS: `System` stays. The Android rule is covered in platformFontFamily.spec.ts.
const value = StyleService.createThemedEntry(
{ fontFamily: 'text-font-family', fontWeight: '600' },
{ 'text-font-family': 'System' },
);

expect(value).toEqual({ fontFamily: 'System', fontWeight: '600' });
});

});

describe('@useStyled: functional component checks', () => {
Expand Down
2 changes: 2 additions & 0 deletions website/docs/guides/branding.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ To have every weight resolve on Android, register the faces under one family nam

Then set only `text-font-family` to that family name; the weight tokens pick the face on both platforms. A separate family per weight, as in the example above, works on Android only when each family that carries a heavy weight token also ships a `_bold` file.

The default `text-font-family` is `System`, the platform font. React Native Android treats any `fontFamily` name as a custom family and rounds the weight to regular or bold, so on Android Eva drops a `System` family from the resolved styles and lets the platform font keep the exact weight (`500`, `600`, …). A custom family name is passed through unchanged.

---

## Summary
Expand Down
Loading