Skip to content

feat(core): Added support for style direction property (ltr/rtl) #10691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
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;
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
105 changes: 85 additions & 20 deletions packages/core/application/application-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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 IAndroidApplication, iOSApplication as IiOSApplication } 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';

// prettier-ignore
const ORIENTATION_CSS_CLASSES = [
Expand All @@ -27,6 +27,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}`,
];

const globalEvents = global.NativeScriptGlobals.events;

// helper interface to correctly type Application event handlers
Expand Down Expand Up @@ -105,6 +111,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 @@ -121,6 +133,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 @@ -156,6 +169,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 @@ -262,6 +290,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 @@ -279,6 +308,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 @@ -309,8 +342,6 @@ export class ApplicationCommon {
// implement in platform specific files (iOS only for now)
}

protected mainEntry: NavigationEntry;

/**
* @returns The main entry of the application
*/
Expand Down Expand Up @@ -390,12 +421,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 @@ -407,7 +437,7 @@ export class ApplicationCommon {
* Gets css file name for the application.
*/
getCssFileName(): string {
return this.cssFile;
return this._cssFile;
}

/**
Expand Down Expand Up @@ -450,8 +480,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 @@ -511,8 +539,6 @@ export class ApplicationCommon {
return global.NativeScriptGlobals && global.NativeScriptGlobals.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 @@ -538,11 +564,6 @@ export class ApplicationCommon {
return (this._systemAppearance ??= this.getSystemAppearance());
}

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

/**
* enable/disable systemAppearanceChanged
*/
Expand Down Expand Up @@ -575,7 +596,55 @@ export class ApplicationCommon {
rootView._onCssStateChange();
}

private _inBackground: boolean = false;
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 this._inBackground;
Expand All @@ -593,8 +662,6 @@ export class ApplicationCommon {
});
}

private _suspended: boolean = false;

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

public started = false;

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