Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c165b2e
feat(core): Added support for style direction property (ltr/rtl)
CatchABus Feb 3, 2025
1b63592
fix: Android text-align left and right misbehaved in rtl apps
CatchABus Feb 3, 2025
aa64ab8
fix: Corrected text alignment when layout direction is rtl
CatchABus Feb 3, 2025
da46c92
feat: Added start/end types for horizontal alignment
CatchABus Feb 3, 2025
4771081
fix(ios): Button default text alignment should be center for both pla…
CatchABus Feb 5, 2025
b551883
fix: Android labels should respect layout direction
CatchABus Feb 5, 2025
91bc44f
fix: Corrected iOS flex-direction row-reverse
CatchABus Feb 5, 2025
c31e330
feat: Added hasRtlSupport helper function
CatchABus Feb 6, 2025
ba60e8c
fix: Use TextAlignment API only on Labels and when rtl support is ena…
CatchABus Feb 6, 2025
25b61ab
test: Added new automated tests for label text alignment
CatchABus Feb 6, 2025
e5b8926
chore: Removed unneeded direction defaults
CatchABus Feb 6, 2025
c41e3ba
fix: Corrected iOS automated tests that failed
CatchABus Feb 6, 2025
cd4fa5a
ref: Convert method hasRtlSupport to static as it was confusing
CatchABus Feb 6, 2025
20fee44
ref: Text alignment improvements
CatchABus Feb 6, 2025
b1200d3
chore: Removed unneeded tests function
CatchABus Feb 6, 2025
ecdffd5
feat: Added support for application-specific layout direction
CatchABus Feb 15, 2025
e16c6a6
chore: Compatibility corrections
CatchABus Nov 5, 2025
5357e06
chore: Minor import correction
CatchABus Nov 5, 2025
82763e6
chore: Added null-check for ios button
CatchABus Nov 5, 2025
94965f2
chore: Added missing import
CatchABus Nov 5, 2025
c65b5bc
chore: Corrected a failing test
CatchABus Nov 5, 2025
4854f88
feat: Added support for deciding text ellipsis based on direction
CatchABus Nov 5, 2025
f82dc3d
chore: Added a property that was accidentally removed
CatchABus Nov 5, 2025
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
3 changes: 2 additions & 1 deletion apps/automated/src/application/application-tests.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ export function testAndroidApplicationInitialized() {
TKUnit.assert(
// @ts-expect-error
Application.android.foregroundActivity.isNativeScriptActivity,
'Android foregroundActivity.isNativeScriptActivity is false.'
'Android foregroundActivity.isNativeScriptActivity is false.',
);
TKUnit.assert(Application.android.startActivity, 'Android startActivity not initialized.');
TKUnit.assert(Application.android.nativeApp, 'Android nativeApp not initialized.');
TKUnit.assert(Application.android.orientation(), 'Android orientation not initialized.');
TKUnit.assert(Utils.android.getPackageName(), 'Android packageName not initialized.');
TKUnit.assert(Application.android.systemAppearance(), 'Android system appearance not initialized.');
TKUnit.assert(Application.android.layoutDirection(), 'Android layout direction not initialized.');
}

export function testSystemAppearance() {
Expand Down
1 change: 1 addition & 0 deletions apps/automated/src/application/application-tests.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function testIOSApplicationInitialized() {
TKUnit.assert(Application.ios.systemAppearance(), 'iOS system appearance not initialized.');
}

TKUnit.assert(Application.ios.layoutDirection(), 'iOS layout direction not initialized.');
TKUnit.assert(Application.ios.window, 'iOS window not initialized.');
TKUnit.assert(Application.ios.rootController, 'iOS root controller not initialized.');
}
Expand Down
17 changes: 12 additions & 5 deletions apps/automated/src/ui/label/label-tests-native.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@ import * as labelModule from '@nativescript/core/ui/label';
import { Color, CoreTypes } from '@nativescript/core';
import { AndroidHelper } from '@nativescript/core/ui/core/view';

const UNEXPECTED_VALUE = 'unexpected value';

export function getNativeTextAlignment(label: labelModule.Label): string {
let gravity = label.android.getGravity();
let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK;
const alignment = label.android.getTextAlignment();

if (hGravity === android.view.Gravity.START && alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) {
return 'initial';
}

if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.LEFT) {
if (hGravity === android.view.Gravity.LEFT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) {
return CoreTypes.TextAlignment.left;
}

if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.CENTER_HORIZONTAL) {
if (hGravity === android.view.Gravity.CENTER_HORIZONTAL && alignment === android.view.View.TEXT_ALIGNMENT_CENTER) {
return CoreTypes.TextAlignment.center;
}

if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.RIGHT) {
if (hGravity === android.view.Gravity.RIGHT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) {
return CoreTypes.TextAlignment.right;
}

return 'unexpected value';
return UNEXPECTED_VALUE;
}

export function getNativeBackgroundColor(label: labelModule.Label): Color {
Expand Down
1 change: 0 additions & 1 deletion apps/automated/src/ui/label/label-tests-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ import * as labelModule from '@nativescript/core/ui/label';
import * as colorModule from '@nativescript/core/color';

export declare function getNativeTextAlignment(label: labelModule.Label): string;

export declare function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color;
4 changes: 2 additions & 2 deletions apps/automated/src/ui/label/label-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ export class LabelTest extends testModule.UITest<Label> {

if (testLabel.android) {
actualTextSize = testLabel.android.getTextSize();
const density = Utils.layout.getDisplayDensity();
expSize = fontSize * density;
// This will cover the case of device text size being affected by a11y
expSize = android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_SP, fontSize, testLabel._context.getResources().getDisplayMetrics());
TKUnit.assertAreClose(actualTextSize, expSize, 0.1, 'Wrong native FontSize');

actualColors = testLabel.android.getTextColors();
Expand Down
50 changes: 43 additions & 7 deletions apps/automated/src/ui/styling/root-views-css-classes-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const LANDSCAPE_ORIENTATION_CSS_CLASS = 'ns-landscape';
const UNKNOWN_ORIENTATION_CSS_CLASS = 'ns-unknown';
const DARK_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-dark';
const LIGHT_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-light';
const LTR_LAYOUT_DIRECTION_CSS_CLASS = 'ns-ltr';
const RTL_LAYOUT_DIRECTION_CSS_CLASS = 'ns-rtl';

function _test_root_css_class(view: View, isModal: boolean, shouldSetClassName: boolean) {
if (shouldSetClassName) {
Expand Down Expand Up @@ -78,12 +80,14 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean
}

const cssClasses = rootView.cssClasses;
let appOrientation;

let appOrientation: 'portrait' | 'landscape' | 'unknown';
if (isAndroid) {
appOrientation = Application.android.orientation;
appOrientation = Application.android.orientation();
} else {
appOrientation = Application.ios.orientation;
appOrientation = Application.ios.orientation();
}

if (appOrientation === 'portrait') {
TKUnit.assertTrue(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is missing`);
TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`);
Expand All @@ -92,7 +96,7 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean
TKUnit.assertTrue(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is missing`);
TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`);
TKUnit.assertFalse(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present`);
} else if (appOrientation === 'landscape') {
} else {
TKUnit.assertTrue(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is missing`);
TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`);
TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`);
Expand All @@ -109,12 +113,14 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b
}

const cssClasses = rootView.cssClasses;
let systemAppearance;

let systemAppearance: 'dark' | 'light' | null;
if (isAndroid) {
systemAppearance = Application.android.systemAppearance;
systemAppearance = Application.android.systemAppearance();
} else {
systemAppearance = Application.ios.systemAppearance;
systemAppearance = Application.ios.systemAppearance();
}

if (isIOS && !__VISIONOS__ && Utils.SDK_VERSION <= 12) {
TKUnit.assertFalse(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`);
TKUnit.assertFalse(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`);
Expand All @@ -131,6 +137,36 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b
}
}

function _test_layout_direction_css_class(rootView: View, shouldSetClassName: boolean) {
if (shouldSetClassName) {
rootView.className = CLASS_NAME;
}

const cssClasses = rootView.cssClasses;

let appLayoutDirection: CoreTypes.LayoutDirectionType | null;
if (isAndroid) {
appLayoutDirection = Application.android.layoutDirection();
} else {
appLayoutDirection = Application.ios.layoutDirection();
}

if (appLayoutDirection === 'ltr') {
TKUnit.assertTrue(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`);
TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
} else if (appLayoutDirection === 'rtl') {
TKUnit.assertTrue(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`);
TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
} else {
TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
}

if (shouldSetClassName) {
TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`);
}
}

// Application root view
export function test_root_view_root_css_class() {
const rootView = Application.getRootView();
Expand Down
103 changes: 86 additions & 17 deletions packages/core/application/application-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Frame } from '../ui/frame';
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
import type { StyleScope } from '../ui/styling/style-scope';
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType } from '.';
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, UnhandledErrorEventData } from './application-interfaces';
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, LayoutDirectionChangedEventData, UnhandledErrorEventData } from './application-interfaces';
import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
import { getAppMainEntry, isAppInBackground, setAppInBackground, setAppMainEntry } from './helpers-common';
import { getNativeScriptGlobals } from '../globals/global-utils';
Expand All @@ -30,6 +30,12 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [
`${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.dark}`,
];

// prettier-ignore
const LAYOUT_DIRECTION_CSS_CLASSES = [
`${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.ltr}`,
`${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.rtl}`,
];

// SDK Version CSS classes
let sdkVersionClasses: string[] = [];

Expand Down Expand Up @@ -164,6 +170,12 @@ interface ApplicationEvents {
*/
on(event: 'systemAppearanceChanged', callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any): void;

/**
* This event is raised when the operating system layout direction changes
* between ltr and rtl.
*/
on(event: 'layoutDirectionChanged', callback: (args: LayoutDirectionChangedEventData) => void, thisArg?: any): void;

on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any): void;
}

Expand All @@ -180,6 +192,7 @@ export class ApplicationCommon {
readonly discardedErrorEvent = 'discardedError';
readonly orientationChangedEvent = 'orientationChanged';
readonly systemAppearanceChangedEvent = 'systemAppearanceChanged';
readonly layoutDirectionChangedEvent = 'layoutDirectionChanged';
readonly fontScaleChangedEvent = 'fontScaleChanged';
readonly livesyncEvent = 'livesync';
readonly loadAppCssEvent = 'loadAppCss';
Expand Down Expand Up @@ -215,6 +228,21 @@ export class ApplicationCommon {
notify: ApplicationEvents['notify'] = globalEvents.notify.bind(globalEvents);
hasListeners: ApplicationEvents['hasListeners'] = globalEvents.hasListeners.bind(globalEvents);

private _orientation: 'portrait' | 'landscape' | 'unknown';
private _systemAppearance: 'dark' | 'light' | null;
private _layoutDirection: CoreTypes.LayoutDirectionType | null;
private _inBackground: boolean = false;
private _suspended: boolean = false;
private _cssFile = './app.css';

protected mainEntry: NavigationEntry;

public started = false;
/**
* Boolean to enable/disable systemAppearanceChanged
*/
public autoSystemAppearanceChanged = true;

/**
* @internal - should not be constructed by the user.
*/
Expand Down Expand Up @@ -321,6 +349,7 @@ export class ApplicationCommon {
const deviceType = Device.deviceType.toLowerCase();
const orientation = this.orientation();
const systemAppearance = this.systemAppearance();
const layoutDirection = this.layoutDirection();

if (platform) {
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${platform}`);
Expand All @@ -338,6 +367,10 @@ export class ApplicationCommon {
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${systemAppearance}`);
}

if (layoutDirection) {
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${layoutDirection}`);
}

rootView.cssClasses.add(CSSUtils.ROOT_VIEW_CSS_CLASS);
const rootViewCssClasses = CSSUtils.getSystemCssClasses();
rootViewCssClasses.forEach((c) => rootView.cssClasses.add(c));
Expand Down Expand Up @@ -447,12 +480,11 @@ export class ApplicationCommon {
bindableResources.set(res);
}

private cssFile = './app.css';
/**
* Sets css file name for the application.
*/
setCssFileName(cssFileName: string) {
this.cssFile = cssFileName;
this._cssFile = cssFileName;
this.notify(<CssChangedEventData>{
eventName: this.cssChangedEvent,
object: this,
Expand All @@ -464,7 +496,7 @@ export class ApplicationCommon {
* Gets css file name for the application.
*/
getCssFileName(): string {
return this.cssFile;
return this._cssFile;
}

/**
Expand Down Expand Up @@ -507,8 +539,6 @@ export class ApplicationCommon {
throw new Error('run() Not implemented.');
}

private _orientation: 'portrait' | 'landscape' | 'unknown';

protected getOrientation(): 'portrait' | 'landscape' | 'unknown' {
// override in platform specific Application class
throw new Error('getOrientation() not implemented');
Expand Down Expand Up @@ -568,8 +598,6 @@ export class ApplicationCommon {
return getNativeScriptGlobals().launched;
}

private _systemAppearance: 'dark' | 'light' | null;

protected getSystemAppearance(): 'dark' | 'light' | null {
// override in platform specific Application class
throw new Error('getSystemAppearance() not implemented');
Expand All @@ -595,11 +623,6 @@ export class ApplicationCommon {
return (this._systemAppearance ??= this.getSystemAppearance());
}

/**
* Boolean to enable/disable systemAppearanceChanged
*/
autoSystemAppearanceChanged = true;

/**
* enable/disable systemAppearanceChanged
*/
Expand Down Expand Up @@ -632,6 +655,56 @@ export class ApplicationCommon {
rootView._onCssStateChange();
}

protected getLayoutDirection(): CoreTypes.LayoutDirectionType | null {
// override in platform specific Application class
throw new Error('getLayoutDirection() not implemented');
}

protected setLayoutDirection(value: CoreTypes.LayoutDirectionType) {
if (this._layoutDirection === value) {
return;
}
this._layoutDirection = value;
this.layoutDirectionChanged(this.getRootView(), value);
this.notify(<LayoutDirectionChangedEventData>{
eventName: this.layoutDirectionChangedEvent,
android: this.android,
ios: this.ios,
newValue: value,
object: this,
});
}

layoutDirection(): CoreTypes.LayoutDirectionType | null {
// return cached value, or get it from the platform specific override
return (this._layoutDirection ??= this.getLayoutDirection());
}

/**
* Updates root view classes including those of modals
* @param rootView the root view
* @param newLayoutDirection the new layout direction change
*/
layoutDirectionChanged(rootView: View, newLayoutDirection: CoreTypes.LayoutDirectionType): void {
if (!rootView) {
return;
}

const newLayoutDirectionCssClass = `${CSSUtils.CLASS_PREFIX}${newLayoutDirection}`;
this.applyCssClass(rootView, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true);

const rootModalViews = rootView._getRootModalViews();
rootModalViews.forEach((rootModalView) => {
this.applyCssClass(rootModalView as View, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true);

// Trigger state change for root modal view classes and media queries
rootModalView._onCssStateChange();
});

// Trigger state change for root view classes and media queries
rootView._onCssStateChange();
}

get inBackground() {
return isAppInBackground();
}
Expand All @@ -648,8 +721,6 @@ export class ApplicationCommon {
});
}

private _suspended: boolean = false;

get suspended() {
return this._suspended;
}
Expand All @@ -667,8 +738,6 @@ export class ApplicationCommon {
});
}

public started = false;

get android(): AndroidApplicationType {
return undefined;
}
Expand Down
Loading
Loading